From bc988b9db7cb3c3ddecc2afc0595e2256135cc67 Mon Sep 17 00:00:00 2001 From: Jeff Boek Date: Thu, 22 Jan 2026 11:40:46 -0800 Subject: [PATCH 01/10] Bug 1978601 - Adds required dependencies to concept-storage for the `StorageMaintenanceWorker` --- .../android-components/components/concept/storage/build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mobile/android/android-components/components/concept/storage/build.gradle b/mobile/android/android-components/components/concept/storage/build.gradle index 895c6d0ca1e50..d84f281fdfeb9 100644 --- a/mobile/android/android-components/components/concept/storage/build.gradle +++ b/mobile/android/android-components/components/concept/storage/build.gradle @@ -20,9 +20,12 @@ dependencies { // Included via 'api' because this module is unusable without coroutines. api libs.kotlinx.coroutines + implementation project(':components:support-base') implementation project(':components:support-ktx') + implementation project(':components:support-utils') implementation libs.androidx.annotation + implementation libs.androidx.work.runtime testImplementation project(':components:support-test') From d6226e900557a19c5f72cb4b3c8f80b2c9005065 Mon Sep 17 00:00:00 2001 From: Jeff Boek Date: Fri, 23 Jan 2026 14:16:40 -0800 Subject: [PATCH 02/10] Bug 1978601 - Moves `StorageMaintenanceWorker` and `StorageExtensions` to `concept-storage` --- .../components/browser/storage/sync/PlacesHistoryStorage.kt | 2 ++ .../browser/storage/sync/PlacesHistoryStorageWorker.kt | 3 ++- .../browser/storage/sync/PlacesHistoryStorageTest.kt | 6 +++++- .../components/concept/storage}/StorageExtensions.kt | 3 +-- .../components/concept/storage}/StorageMaintenanceWorker.kt | 6 +----- 5 files changed, 11 insertions(+), 9 deletions(-) rename mobile/android/android-components/components/{browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync => concept/storage/src/main/java/mozilla/components/concept/storage}/StorageExtensions.kt (94%) rename mobile/android/android-components/components/{browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync => concept/storage/src/main/java/mozilla/components/concept/storage}/StorageMaintenanceWorker.kt (83%) diff --git a/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/PlacesHistoryStorage.kt b/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/PlacesHistoryStorage.kt index e171a49ba0c98..fbf48ff85a0a3 100644 --- a/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/PlacesHistoryStorage.kt +++ b/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/PlacesHistoryStorage.kt @@ -29,6 +29,8 @@ import mozilla.components.concept.storage.SearchResult import mozilla.components.concept.storage.TopFrecentSiteInfo import mozilla.components.concept.storage.VisitInfo import mozilla.components.concept.storage.VisitType +import mozilla.components.concept.storage.constraints +import mozilla.components.concept.storage.periodicStorageWorkRequest import mozilla.components.concept.sync.SyncAuthInfo import mozilla.components.concept.sync.SyncStatus import mozilla.components.concept.sync.SyncableStore diff --git a/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/PlacesHistoryStorageWorker.kt b/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/PlacesHistoryStorageWorker.kt index f167f87c7bcaa..97afbeced6873 100644 --- a/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/PlacesHistoryStorageWorker.kt +++ b/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/PlacesHistoryStorageWorker.kt @@ -6,6 +6,7 @@ package mozilla.components.browser.storage.sync import android.content.Context import androidx.work.WorkerParameters +import mozilla.components.concept.storage.StorageMaintenanceWorker import mozilla.components.support.base.log.logger.Logger /** @@ -14,7 +15,7 @@ import mozilla.components.support.base.log.logger.Logger * If there is a failure or the worker constraints are no longer met during execution, * active write operations on [PlacesStorage] are cancelled. * - * See also [StorageMaintenanceWorker]. + * See also [mozilla.components.concept.storage.StorageMaintenanceWorker]. */ internal class PlacesHistoryStorageWorker(context: Context, params: WorkerParameters) : StorageMaintenanceWorker(context, params) { diff --git a/mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesHistoryStorageTest.kt b/mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesHistoryStorageTest.kt index b43b6dccbf55d..9333156a05734 100644 --- a/mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesHistoryStorageTest.kt +++ b/mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesHistoryStorageTest.kt @@ -23,7 +23,10 @@ import mozilla.components.concept.storage.HistoryMetadataKey import mozilla.components.concept.storage.HistoryMetadataObservation import mozilla.components.concept.storage.PageObservation import mozilla.components.concept.storage.PageVisit +import mozilla.components.concept.storage.StorageMaintenanceWorker import mozilla.components.concept.storage.VisitType +import mozilla.components.concept.storage.constraints +import mozilla.components.concept.storage.periodicStorageWorkRequest import mozilla.components.concept.sync.SyncAuthInfo import mozilla.components.concept.sync.SyncStatus import mozilla.components.support.test.any @@ -619,7 +622,8 @@ class PlacesHistoryStorageTest { } assertEquals(request.workSpec.isPeriodic, true) - assertEquals(request.workSpec.intervalDuration, TimeUnit.HOURS.toMillis(StorageMaintenanceWorker.WORKER_PERIOD_IN_HOURS)) + assertEquals(request.workSpec.intervalDuration, TimeUnit.HOURS.toMillis( + StorageMaintenanceWorker.WORKER_PERIOD_IN_HOURS)) assertEquals(request.workSpec.constraints.requiresBatteryNotLow(), true) assertEquals(request.workSpec.constraints.requiresDeviceIdle(), true) } diff --git a/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/StorageExtensions.kt b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/StorageExtensions.kt similarity index 94% rename from mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/StorageExtensions.kt rename to mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/StorageExtensions.kt index 79bba0480c0ca..0e6512802428d 100644 --- a/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/StorageExtensions.kt +++ b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/StorageExtensions.kt @@ -2,12 +2,11 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package mozilla.components.browser.storage.sync +package mozilla.components.concept.storage import androidx.work.Constraints import androidx.work.PeriodicWorkRequest import androidx.work.PeriodicWorkRequestBuilder -import mozilla.components.concept.storage.Storage import java.util.concurrent.TimeUnit /** diff --git a/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/StorageMaintenanceWorker.kt b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/StorageMaintenanceWorker.kt similarity index 83% rename from mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/StorageMaintenanceWorker.kt rename to mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/StorageMaintenanceWorker.kt index 873c68a082644..3ff4f2f3d24fd 100644 --- a/mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/StorageMaintenanceWorker.kt +++ b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/StorageMaintenanceWorker.kt @@ -1,8 +1,4 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.browser.storage.sync +package mozilla.components.concept.storage import android.content.Context import androidx.work.CoroutineWorker From e0ca097ef4695421d6fbf3d8ff119890963fcdb4 Mon Sep 17 00:00:00 2001 From: Jeff Boek Date: Fri, 23 Jan 2026 14:20:13 -0800 Subject: [PATCH 03/10] Bug 1978601 - Conforms `SyncableLoginsStorage` to `Storage` --- .../mozilla/components/concept/storage/LoginsStorage.kt | 2 +- .../components/service/sync/logins/SyncableLoginsStorage.kt | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/LoginsStorage.kt b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/LoginsStorage.kt index bc15ecd66f1d7..bdf5a88bb1534 100644 --- a/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/LoginsStorage.kt +++ b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/LoginsStorage.kt @@ -162,7 +162,7 @@ data class EncryptedLogin( /** * An interface describing a storage layer for logins/passwords. */ -interface LoginsStorage : AutoCloseable { +interface LoginsStorage : Storage, AutoCloseable { /** * Clears out all local state, bringing us back to the state before the first write (or sync). */ diff --git a/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/SyncableLoginsStorage.kt b/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/SyncableLoginsStorage.kt index a5034778218c8..273bba64db6cf 100644 --- a/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/SyncableLoginsStorage.kt +++ b/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/SyncableLoginsStorage.kt @@ -129,11 +129,15 @@ class SyncableLoginsStorage( /** * "Warms up" this storage layer by establishing the database connection. */ - suspend fun warmUp() = withContext(coroutineContext) { + override suspend fun warmUp() = withContext(coroutineContext) { logElapsedTime(logger, "Warming up storage") { conn.await() } Unit } + override suspend fun runMaintenance(dbSizeLimit: UInt) { + getStorage().runMaintenance() + } + /** * @throws [LoginsApiException] if the storage is locked, and on unexpected * errors (IO failure, rust panics, etc) From 94408b00d0cbdf4b6c20dd452f609d9d0d20cedf Mon Sep 17 00:00:00 2001 From: Jeff Boek Date: Fri, 23 Jan 2026 14:42:30 -0800 Subject: [PATCH 04/10] Bug 1978601 - Implement `StorageMaintenanceRegistry` for `SyncableLoginsStorage` --- .../storage/sync/PlacesHistoryStorageTest.kt | 6 ++- .../concept/storage/LoginsStorage.kt | 18 +++++++- .../storage/StorageMaintenanceWorker.kt | 4 ++ .../service/sync-logins/build.gradle | 3 ++ .../logins/GlobalLoginsDependencyProvider.kt | 37 +++++++++++++++ .../sync/logins/SyncableLoginsStorage.kt | 29 ++++++++++++ .../logins/SyncableLoginsStorageWorker.kt | 45 +++++++++++++++++++ 7 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/GlobalLoginsDependencyProvider.kt create mode 100644 mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/SyncableLoginsStorageWorker.kt diff --git a/mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesHistoryStorageTest.kt b/mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesHistoryStorageTest.kt index 9333156a05734..e8a6847549198 100644 --- a/mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesHistoryStorageTest.kt +++ b/mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesHistoryStorageTest.kt @@ -622,8 +622,10 @@ class PlacesHistoryStorageTest { } assertEquals(request.workSpec.isPeriodic, true) - assertEquals(request.workSpec.intervalDuration, TimeUnit.HOURS.toMillis( - StorageMaintenanceWorker.WORKER_PERIOD_IN_HOURS)) + assertEquals( + request.workSpec.intervalDuration, + TimeUnit.HOURS.toMillis(StorageMaintenanceWorker.WORKER_PERIOD_IN_HOURS), + ) assertEquals(request.workSpec.constraints.requiresBatteryNotLow(), true) assertEquals(request.workSpec.constraints.requiresDeviceIdle(), true) } diff --git a/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/LoginsStorage.kt b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/LoginsStorage.kt index bdf5a88bb1534..721dd246a630d 100644 --- a/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/LoginsStorage.kt +++ b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/LoginsStorage.kt @@ -162,7 +162,7 @@ data class EncryptedLogin( /** * An interface describing a storage layer for logins/passwords. */ -interface LoginsStorage : Storage, AutoCloseable { +interface LoginsStorage : Storage, StorageMaintenanceRegistry, AutoCloseable { /** * Clears out all local state, bringing us back to the state before the first write (or sync). */ @@ -251,6 +251,22 @@ interface LoginsStorage : Storage, AutoCloseable { * @return A list of [Login] objects, representing matching logins. */ suspend fun getByBaseDomain(origin: String): List + + override fun registerStorageMaintenanceWorker() { + // Implemented by concrete implementation of `LoginsStorage` + } + + override fun unregisterStorageMaintenanceWorker(uniqueWorkName: String) { + // Implemented by concrete implementation of `LoginsStorage` + } + + override suspend fun runMaintenance(dbSizeLimit: UInt) { + // Implemented by concrete implementation of `LoginsStorage` + } + + override suspend fun warmUp() { + // Implemented by concrete implementation of `LoginsStorage` + } } /** diff --git a/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/StorageMaintenanceWorker.kt b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/StorageMaintenanceWorker.kt index 3ff4f2f3d24fd..adca1c68f254f 100644 --- a/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/StorageMaintenanceWorker.kt +++ b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/StorageMaintenanceWorker.kt @@ -1,3 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + package mozilla.components.concept.storage import android.content.Context diff --git a/mobile/android/android-components/components/service/sync-logins/build.gradle b/mobile/android/android-components/components/service/sync-logins/build.gradle index 5a902b2b7bf84..d4e9e096c8f38 100644 --- a/mobile/android/android-components/components/service/sync-logins/build.gradle +++ b/mobile/android/android-components/components/service/sync-logins/build.gradle @@ -25,12 +25,15 @@ dependencies { exclude group: 'org.mozilla.telemetry', module: 'glean' } api ComponentsDependencies.mozilla_appservices_sync15 + implementation libs.androidx.work.runtime implementation project(':components:concept-storage') implementation project(':components:support-utils') implementation libs.kotlinx.coroutines implementation libs.mozilla.glean + + testImplementation libs.androidx.work.testing } apply from: '../../../common-config.gradle' diff --git a/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/GlobalLoginsDependencyProvider.kt b/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/GlobalLoginsDependencyProvider.kt new file mode 100644 index 0000000000000..a171ef2cb18cc --- /dev/null +++ b/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/GlobalLoginsDependencyProvider.kt @@ -0,0 +1,37 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.service.sync.logins + +import androidx.annotation.VisibleForTesting +import mozilla.components.concept.storage.LoginsStorage + +/** + * Provides global access to the dependencies needed for logins storage operations. + * */ +object GlobalLoginsDependencyProvider { + + @VisibleForTesting + internal var loginsStorage: LoginsStorage? = null + + /** + * Initializes logins storage for running the maintenance task via [SyncableLoginsStorageWorker]. + * This method should be called in client application's onCreate method and before + * [SyncableLoginsStorage.registerStorageMaintenanceWorker] in order to run the worker while + * the app is not running. + * */ + fun initialize(loginsStorage: LoginsStorage) { + this.loginsStorage = loginsStorage + } + + /** + * Provides [LoginsStorage] globally when needed for [SyncableLoginsStorageWorker] + * to run maintenance on the storage. + * */ + internal fun requireLoginsStorage(): LoginsStorage { + return requireNotNull(loginsStorage) { + "GlobalLoginsDependencyProvider.initialize must be called before accessing the Logins storage" + } + } +} diff --git a/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/SyncableLoginsStorage.kt b/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/SyncableLoginsStorage.kt index 273bba64db6cf..ce4d400f164d3 100644 --- a/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/SyncableLoginsStorage.kt +++ b/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/SyncableLoginsStorage.kt @@ -7,6 +7,8 @@ package mozilla.components.service.sync.logins import android.content.Context import android.content.SharedPreferences import androidx.core.content.edit +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.WorkManager import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred @@ -22,6 +24,8 @@ import mozilla.components.concept.storage.KeyGenerationReason import mozilla.components.concept.storage.Login import mozilla.components.concept.storage.LoginEntry import mozilla.components.concept.storage.LoginsStorage +import mozilla.components.concept.storage.constraints +import mozilla.components.concept.storage.periodicStorageWorkRequest import mozilla.components.concept.sync.SyncableStore import mozilla.components.lib.dataprotect.SecureAbove22Preferences import mozilla.components.support.base.log.logger.Logger @@ -284,4 +288,29 @@ class SyncableLoginsStorage( prefs.edit { putInt(UNDECRYPTABLE_LOGINS_CLEANED_KEY, ++cleanedPref) } } } + + /** + * Enqueues a periodic storage maintenance worker to WorkManager. + */ + override fun registerStorageMaintenanceWorker() { + WorkManager.getInstance(context).enqueueUniquePeriodicWork( + SyncableLoginsStorageWorker.UNIQUE_NAME, + ExistingPeriodicWorkPolicy.KEEP, + periodicStorageWorkRequest( + tag = SyncableLoginsStorageWorker.UNIQUE_NAME, + ) { + constraints { + setRequiresBatteryNotLow(true) + setRequiresDeviceIdle(true) + } + }, + ) + } + + override fun unregisterStorageMaintenanceWorker(uniqueWorkName: String) { + WorkManager.getInstance(context).also { + it.cancelUniqueWork(SyncableLoginsStorageWorker.UNIQUE_NAME) + it.cancelAllWorkByTag(SyncableLoginsStorageWorker.UNIQUE_NAME) + } + } } diff --git a/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/SyncableLoginsStorageWorker.kt b/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/SyncableLoginsStorageWorker.kt new file mode 100644 index 0000000000000..65708c54865e8 --- /dev/null +++ b/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/SyncableLoginsStorageWorker.kt @@ -0,0 +1,45 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.service.sync.logins + +import android.content.Context +import androidx.work.WorkerParameters +import mozilla.components.concept.storage.StorageMaintenanceWorker +import mozilla.components.support.base.log.logger.Logger + +/** + * A WorkManager Worker that executes [SyncableLoginsStorage.runMaintenance]. + * + * If there is a failure or the worker constraints are no longer met during execution, + * active write operations on [SyncableLoginsStorage] are cancelled. + * + * See also [mozilla.components.concept.storage.StorageMaintenanceWorker]. + */ +internal class SyncableLoginsStorageWorker(context: Context, params: WorkerParameters) : + StorageMaintenanceWorker(context, params) { + + val logger = Logger(PLACES_HISTORY_STORAGE_WORKER_TAG) + + override suspend fun operate() { + GlobalLoginsDependencyProvider.requireLoginsStorage() + .runMaintenance(DB_SIZE_LIMIT_IN_BYTES.toUInt()) + } + + override fun onError(exception: Exception) { + GlobalLoginsDependencyProvider.requireLoginsStorage().cancelWrites() + logger.error("An exception occurred while running the maintenance task: ${exception.message}") + } + + companion object { + private const val IDENTIFIER_PREFIX = "mozilla.components.service.sync.logins" + private const val PLACES_HISTORY_STORAGE_WORKER_TAG = "$IDENTIFIER_PREFIX.SyncableLoginsStorageWorker" + + internal const val UNIQUE_NAME = "$IDENTIFIER_PREFIX.SyncableLoginsStorageWorker" + + // The implementation of `runMaintenance` on `DatabaseLoginsStorage` doesn't take any input + // but `Storage` requires that we pass a value. + internal const val DB_SIZE_LIMIT_IN_BYTES = Int.MAX_VALUE + } +} From d10d5f3035a132fcaa9235f8c7bf027cdd30ca87 Mon Sep 17 00:00:00 2001 From: Jeff Boek Date: Fri, 23 Jan 2026 15:40:19 -0800 Subject: [PATCH 05/10] Bug 1978601 - Adds tests for `SyncableLoginsStorageWorker` --- .../android-components/.buildconfig.yml | 1 + .../service/sync-logins/build.gradle | 7 ++ .../GlobalLoginsDependencyProviderTest.kt | 33 +++++++ .../logins/SyncableLoginsStorageWorkerTest.kt | 95 +++++++++++++++++++ 4 files changed, 136 insertions(+) create mode 100644 mobile/android/android-components/components/service/sync-logins/src/test/java/mozilla/components/service/sync/logins/GlobalLoginsDependencyProviderTest.kt create mode 100644 mobile/android/android-components/components/service/sync-logins/src/test/java/mozilla/components/service/sync/logins/SyncableLoginsStorageWorkerTest.kt diff --git a/mobile/android/android-components/.buildconfig.yml b/mobile/android/android-components/.buildconfig.yml index e1e08dfaee9c0..e6042efaececc 100644 --- a/mobile/android/android-components/.buildconfig.yml +++ b/mobile/android/android-components/.buildconfig.yml @@ -2284,6 +2284,7 @@ projects: - components:lib-publicsuffixlist - components:support-base - components:support-ktx + - components:support-test - components:support-utils - components:tooling-lint components:support-android-test: diff --git a/mobile/android/android-components/components/service/sync-logins/build.gradle b/mobile/android/android-components/components/service/sync-logins/build.gradle index d4e9e096c8f38..803163901d7c4 100644 --- a/mobile/android/android-components/components/service/sync-logins/build.gradle +++ b/mobile/android/android-components/components/service/sync-logins/build.gradle @@ -33,7 +33,14 @@ dependencies { implementation libs.kotlinx.coroutines implementation libs.mozilla.glean + testImplementation project(':components:support-test') + + testImplementation libs.androidx.test.core + testImplementation libs.androidx.test.junit testImplementation libs.androidx.work.testing + testImplementation libs.kotlin.reflect + testImplementation libs.kotlinx.coroutines.test + testImplementation libs.robolectric } apply from: '../../../common-config.gradle' diff --git a/mobile/android/android-components/components/service/sync-logins/src/test/java/mozilla/components/service/sync/logins/GlobalLoginsDependencyProviderTest.kt b/mobile/android/android-components/components/service/sync-logins/src/test/java/mozilla/components/service/sync/logins/GlobalLoginsDependencyProviderTest.kt new file mode 100644 index 0000000000000..617e10451e355 --- /dev/null +++ b/mobile/android/android-components/components/service/sync-logins/src/test/java/mozilla/components/service/sync/logins/GlobalLoginsDependencyProviderTest.kt @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.service.sync.logins + +import mozilla.components.concept.storage.LoginsStorage +import mozilla.components.support.test.mock +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +class GlobalLoginsDependencyProviderTest { + + @Before + @After + fun cleanUp() { + GlobalLoginsDependencyProvider.loginsStorage = null + } + + @Test(expected = IllegalArgumentException::class) + fun `requirePlacesStorage called without calling initialize, exception returned`() { + GlobalLoginsDependencyProvider.requireLoginsStorage() + } + + @Test + fun `requirePlacesStorage called after calling initialize, loginsStorage returned`() { + val loginsStorage = mock() + GlobalLoginsDependencyProvider.initialize(lazy { loginsStorage }) + assertEquals(loginsStorage, GlobalLoginsDependencyProvider.requireLoginsStorage()) + } +} diff --git a/mobile/android/android-components/components/service/sync-logins/src/test/java/mozilla/components/service/sync/logins/SyncableLoginsStorageWorkerTest.kt b/mobile/android/android-components/components/service/sync-logins/src/test/java/mozilla/components/service/sync/logins/SyncableLoginsStorageWorkerTest.kt new file mode 100644 index 0000000000000..11d92d7e9c39d --- /dev/null +++ b/mobile/android/android-components/components/service/sync-logins/src/test/java/mozilla/components/service/sync/logins/SyncableLoginsStorageWorkerTest.kt @@ -0,0 +1,95 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.service.sync.logins + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.work.ListenableWorker.Result +import androidx.work.testing.TestListenableWorkerBuilder +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.test.runTest +import mozilla.components.concept.storage.LoginsStorage +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import kotlin.reflect.KVisibility + +@RunWith(AndroidJUnit4::class) +class SyncableLoginsStorageWorkerTest { + + @After + fun tearDown() { + GlobalLoginsDependencyProvider.loginsStorage = null + } + + @Test + fun `PlacesHistoryStorage's runMaintenance is called when worker's startWork is called`() = + runTest { + val loginsStorage = mock() + GlobalLoginsDependencyProvider.initialize(lazy { loginsStorage }) + val worker = + TestListenableWorkerBuilder( + testContext, + ).build() + + worker.doWork() + verify(loginsStorage).runMaintenance(SyncableLoginsStorageWorker.DB_SIZE_LIMIT_IN_BYTES.toUInt()) + } + + @Test + fun `PlacesHistoryStorage's runMaintenance operation is successful, successful result returned by the worker`() = + runTest { + val loginsStorage = mock() + GlobalLoginsDependencyProvider.initialize(lazy { loginsStorage }) + val worker = + TestListenableWorkerBuilder( + testContext, + ).build() + + val result = worker.doWork() + assertEquals(Result.success(), result) + } + + @Test + fun `PlacesHistoryStorage's runMaintenance is called, exception is thrown and failure result is returned`() = + runTest { + val loginsStorage = mock() + `when`(loginsStorage.runMaintenance(SyncableLoginsStorageWorker.DB_SIZE_LIMIT_IN_BYTES.toUInt())) + .thenThrow(CancellationException()) + GlobalLoginsDependencyProvider.initialize(lazy { loginsStorage }) + val worker = + TestListenableWorkerBuilder( + testContext, + ).build() + + val result = worker.doWork() + assertEquals(Result.failure(), result) + } + + @Test + fun `PlacesHistoryStorage's runMaintenance is called, exception is thrown and active write operations are cancelled`() = + runTest { + val loginsStorage = mock() + `when`(loginsStorage.runMaintenance(SyncableLoginsStorageWorker.DB_SIZE_LIMIT_IN_BYTES.toUInt())) + .thenThrow(CancellationException()) + GlobalLoginsDependencyProvider.initialize(lazy { loginsStorage }) + val worker = + TestListenableWorkerBuilder( + testContext, + ).build() + + worker.doWork() + verify(loginsStorage).cancelWrites() + } + + @Test + fun `PlacesHistoryStorageWorker's visibility is internal`() { + assertEquals(SyncableLoginsStorageWorker::class.visibility, KVisibility.INTERNAL) + } +} From 70af84be29e929eac316256eb5195905e8970ac3 Mon Sep 17 00:00:00 2001 From: Jeff Boek Date: Fri, 23 Jan 2026 14:51:07 -0800 Subject: [PATCH 06/10] Bug 1978601 - `registerStorageMaintenanceWorker` for Logins --- .../service/sync/logins/GlobalLoginsDependencyProvider.kt | 6 +++--- .../app/src/main/java/org/mozilla/fenix/FenixApplication.kt | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/GlobalLoginsDependencyProvider.kt b/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/GlobalLoginsDependencyProvider.kt index a171ef2cb18cc..ea618aae2a3cd 100644 --- a/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/GlobalLoginsDependencyProvider.kt +++ b/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/GlobalLoginsDependencyProvider.kt @@ -13,7 +13,7 @@ import mozilla.components.concept.storage.LoginsStorage object GlobalLoginsDependencyProvider { @VisibleForTesting - internal var loginsStorage: LoginsStorage? = null + internal var loginsStorage: Lazy? = null /** * Initializes logins storage for running the maintenance task via [SyncableLoginsStorageWorker]. @@ -21,7 +21,7 @@ object GlobalLoginsDependencyProvider { * [SyncableLoginsStorage.registerStorageMaintenanceWorker] in order to run the worker while * the app is not running. * */ - fun initialize(loginsStorage: LoginsStorage) { + fun initialize(loginsStorage: Lazy) { this.loginsStorage = loginsStorage } @@ -30,7 +30,7 @@ object GlobalLoginsDependencyProvider { * to run maintenance on the storage. * */ internal fun requireLoginsStorage(): LoginsStorage { - return requireNotNull(loginsStorage) { + return requireNotNull(loginsStorage?.value) { "GlobalLoginsDependencyProvider.initialize must be called before accessing the Logins storage" } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt index c5933c384212b..f915247c4a82b 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt @@ -52,6 +52,7 @@ import mozilla.components.feature.top.sites.TopSitesProviderConfig import mozilla.components.feature.webcompat.reporter.WebCompatReporterFeature import mozilla.components.lib.crash.CrashReporter import mozilla.components.service.fxa.manager.SyncEnginesStorage +import mozilla.components.service.sync.logins.GlobalLoginsDependencyProvider import mozilla.components.service.sync.logins.LoginsApiException import mozilla.components.support.AppServicesInitializer import mozilla.components.support.base.ext.areNotificationsEnabledSafe @@ -297,6 +298,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider, ThemeProvider // it is needed while the app is not running and WorkManager wakes up the app // for the periodic task. GlobalPlacesDependencyProvider.initialize(components.core.historyStorage) + GlobalLoginsDependencyProvider.initialize(lazy { components.core.passwordsStorage }) GlobalSyncedTabsCommandsProvider.initialize(lazy { components.backgroundServices.syncedTabsCommands }) @@ -484,6 +486,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider, ThemeProvider // the app for the periodic task, it will require a globally provided places storage // to run the maintenance on. components.core.historyStorage.registerStorageMaintenanceWorker() + components.core.passwordsStorage.registerStorageMaintenanceWorker() } } From b9e4feb1767a302c448bcded99d05aaef8c2cc46 Mon Sep 17 00:00:00 2001 From: Jeff Boek Date: Fri, 23 Jan 2026 14:58:43 -0800 Subject: [PATCH 07/10] Bug 1978601 - `Conforms `AutofillCreditCardsAddressesStorage` to `Storage` --- .../concept/storage/CreditCardsAddressesStorage.kt | 10 +++++++++- .../autofill/AutofillCreditCardsAddressesStorage.kt | 6 +++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/CreditCardsAddressesStorage.kt b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/CreditCardsAddressesStorage.kt index 3358021e9ac81..ed4940ff89718 100644 --- a/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/CreditCardsAddressesStorage.kt +++ b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/CreditCardsAddressesStorage.kt @@ -19,7 +19,7 @@ import java.util.Locale /** * An interface which defines read/write methods for credit card and address data. */ -interface CreditCardsAddressesStorage { +interface CreditCardsAddressesStorage : Storage { /** * Inserts the provided credit card into the database, and returns @@ -126,6 +126,14 @@ interface CreditCardsAddressesStorage { * Removes any encrypted data from this storage. Useful after encountering key loss. */ suspend fun scrubEncryptedData() + + override suspend fun runMaintenance(dbSizeLimit: UInt) { + // Implemented by concrete implementation of `CreditCardsAddressesStorage` + } + + override suspend fun warmUp() { + // Implemented by concrete implementation of `CreditCardsAddressesStorage` + } } /** diff --git a/mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/AutofillCreditCardsAddressesStorage.kt b/mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/AutofillCreditCardsAddressesStorage.kt index c48559577d981..cc765bdf73f27 100644 --- a/mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/AutofillCreditCardsAddressesStorage.kt +++ b/mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/AutofillCreditCardsAddressesStorage.kt @@ -54,11 +54,15 @@ class AutofillCreditCardsAddressesStorage( /** * "Warms up" this storage layer by establishing the database connection. */ - suspend fun warmUp() = withContext(coroutineContext) { + override suspend fun warmUp() = withContext(coroutineContext) { logElapsedTime(logger, "Warming up storage") { conn } Unit } + override suspend fun runMaintenance(dbSizeLimit: UInt) { + conn.getStorage().runMaintenance() + } + override suspend fun addCreditCard( creditCardFields: NewCreditCardFields, ): CreditCard = withContext(coroutineContext) { From 246307362a8165c1bf632256b5acc17d20f68fd3 Mon Sep 17 00:00:00 2001 From: Jeff Boek Date: Fri, 23 Jan 2026 15:14:40 -0800 Subject: [PATCH 08/10] Bug 1978601 - Implement `StorageMaintenanceRegistry` for `AutofillCreditCardsAddressesStorage` --- .../storage/CreditCardsAddressesStorage.kt | 10 ++++- .../service/sync-autofill/build.gradle | 2 + .../AutofillCreditCardsAddressesStorage.kt | 31 ++++++++++++- .../sync/autofill/AutofillStorageWorker.kt | 45 +++++++++++++++++++ .../GlobalAutofillDependencyProvider.kt | 38 ++++++++++++++++ 5 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/AutofillStorageWorker.kt create mode 100644 mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/GlobalAutofillDependencyProvider.kt diff --git a/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/CreditCardsAddressesStorage.kt b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/CreditCardsAddressesStorage.kt index ed4940ff89718..28ac1052e37cd 100644 --- a/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/CreditCardsAddressesStorage.kt +++ b/mobile/android/android-components/components/concept/storage/src/main/java/mozilla/components/concept/storage/CreditCardsAddressesStorage.kt @@ -19,7 +19,7 @@ import java.util.Locale /** * An interface which defines read/write methods for credit card and address data. */ -interface CreditCardsAddressesStorage : Storage { +interface CreditCardsAddressesStorage : Storage, StorageMaintenanceRegistry { /** * Inserts the provided credit card into the database, and returns @@ -134,6 +134,14 @@ interface CreditCardsAddressesStorage : Storage { override suspend fun warmUp() { // Implemented by concrete implementation of `CreditCardsAddressesStorage` } + + override fun registerStorageMaintenanceWorker() { + // Implemented by concrete implementation of `CreditCardsAddressesStorage` + } + + override fun unregisterStorageMaintenanceWorker(uniqueWorkName: String) { + // Implemented by concrete implementation of `CreditCardsAddressesStorage` + } } /** diff --git a/mobile/android/android-components/components/service/sync-autofill/build.gradle b/mobile/android/android-components/components/service/sync-autofill/build.gradle index ae5406ce89442..823b5a3ca68a8 100644 --- a/mobile/android/android-components/components/service/sync-autofill/build.gradle +++ b/mobile/android/android-components/components/service/sync-autofill/build.gradle @@ -15,6 +15,7 @@ android { dependencies { api ComponentsDependencies.mozilla_appservices_autofill + implementation libs.androidx.work.runtime api project(':components:concept-base') api project(':components:concept-storage') @@ -30,6 +31,7 @@ dependencies { testImplementation ComponentsDependencies.mozilla_appservices_init_rust_components testImplementation libs.androidx.test.core testImplementation libs.androidx.test.junit + testImplementation libs.androidx.work.testing testImplementation libs.kotlinx.coroutines.test testImplementation libs.robolectric } diff --git a/mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/AutofillCreditCardsAddressesStorage.kt b/mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/AutofillCreditCardsAddressesStorage.kt index cc765bdf73f27..28eca514a19a1 100644 --- a/mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/AutofillCreditCardsAddressesStorage.kt +++ b/mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/AutofillCreditCardsAddressesStorage.kt @@ -7,6 +7,8 @@ package mozilla.components.service.sync.autofill import android.content.Context import androidx.annotation.GuardedBy import androidx.annotation.VisibleForTesting +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.WorkManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.withContext @@ -18,6 +20,8 @@ import mozilla.components.concept.storage.CreditCardsAddressesStorage import mozilla.components.concept.storage.NewCreditCardFields import mozilla.components.concept.storage.UpdatableAddressFields import mozilla.components.concept.storage.UpdatableCreditCardFields +import mozilla.components.concept.storage.constraints +import mozilla.components.concept.storage.periodicStorageWorkRequest import mozilla.components.concept.sync.SyncableStore import mozilla.components.lib.dataprotect.SecureAbove22Preferences import mozilla.components.support.base.log.logger.Logger @@ -36,7 +40,7 @@ const val AUTOFILL_DB_NAME = "autofill.sqlite" * Used for storing encryption key material. */ class AutofillCreditCardsAddressesStorage( - context: Context, + private val context: Context, securePrefs: Lazy, ) : CreditCardsAddressesStorage, SyncableStore, AutoCloseable { private val logger = Logger("AutofillCCAddressesStorage") @@ -185,6 +189,31 @@ class AutofillCreditCardsAddressesStorage( coroutineContext.cancel() conn.close() } + + /** + * Enqueues a periodic storage maintenance worker to WorkManager. + */ + override fun registerStorageMaintenanceWorker() { + WorkManager.getInstance(context).enqueueUniquePeriodicWork( + AutofillStorageWorker.UNIQUE_NAME, + ExistingPeriodicWorkPolicy.KEEP, + periodicStorageWorkRequest( + tag = AutofillStorageWorker.UNIQUE_NAME, + ) { + constraints { + setRequiresBatteryNotLow(true) + setRequiresDeviceIdle(true) + } + }, + ) + } + + override fun unregisterStorageMaintenanceWorker(uniqueWorkName: String) { + WorkManager.getInstance(context).also { + it.cancelUniqueWork(AutofillStorageWorker.UNIQUE_NAME) + it.cancelAllWorkByTag(AutofillStorageWorker.UNIQUE_NAME) + } + } } /** diff --git a/mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/AutofillStorageWorker.kt b/mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/AutofillStorageWorker.kt new file mode 100644 index 0000000000000..472eda217ff1c --- /dev/null +++ b/mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/AutofillStorageWorker.kt @@ -0,0 +1,45 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.service.sync.autofill + +import android.content.Context +import androidx.work.WorkerParameters +import mozilla.components.concept.storage.StorageMaintenanceWorker +import mozilla.components.support.base.log.logger.Logger + +/** + * A WorkManager Worker that executes [AutofillCreditCardsAddressesStorage.runMaintenance]. + * + * If there is a failure or the worker constraints are no longer met during execution, + * active write operations on [AutofillCreditCardsAddressesStorage] are cancelled. + * + * See also [mozilla.components.concept.storage.StorageMaintenanceWorker]. + */ +internal class AutofillStorageWorker(context: Context, params: WorkerParameters) : + StorageMaintenanceWorker(context, params) { + + val logger = Logger(AUTOFILL_STORAGE_WORKER_TAG) + + override suspend fun operate() { + GlobalAutofillDependencyProvider.requireAutofillStorage() + .runMaintenance(DB_SIZE_LIMIT_IN_BYTES.toUInt()) + } + + override fun onError(exception: Exception) { + GlobalAutofillDependencyProvider.requireAutofillStorage().cancelWrites() + logger.error("An exception occurred while running the maintenance task: ${exception.message}") + } + + companion object { + private const val IDENTIFIER_PREFIX = "mozilla.components.service.sync.autofill" + private const val AUTOFILL_STORAGE_WORKER_TAG = "$IDENTIFIER_PREFIX.AutofillStorageWorker" + + internal const val UNIQUE_NAME = "$IDENTIFIER_PREFIX.AutofillStorageWorker" + + // The implementation of `runMaintenance` on `DatabaseLoginsStorage` doesn't take any input + // but `Storage` requires that we pass a value. + internal const val DB_SIZE_LIMIT_IN_BYTES = Int.MAX_VALUE + } +} diff --git a/mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/GlobalAutofillDependencyProvider.kt b/mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/GlobalAutofillDependencyProvider.kt new file mode 100644 index 0000000000000..e2b62fae7c8e2 --- /dev/null +++ b/mobile/android/android-components/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/GlobalAutofillDependencyProvider.kt @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.service.sync.autofill + +import androidx.annotation.VisibleForTesting +import mozilla.components.concept.storage.CreditCardsAddressesStorage +import mozilla.components.concept.storage.LoginsStorage + +/** + * Provides global access to the dependencies needed for autofill storage operations. + * */ +object GlobalAutofillDependencyProvider { + + @VisibleForTesting + internal var autofillStorage: Lazy? = null + + /** + * Initializes logins storage for running the maintenance task via [AutofillStorageWorker]. + * This method should be called in client application's onCreate method and before + * [AutofillCreditCardsAddressesStorage.registerStorageMaintenanceWorker] in order to run the worker while + * the app is not running. + * */ + fun initialize(autofillStorage: Lazy) { + this.autofillStorage = autofillStorage + } + + /** + * Provides [LoginsStorage] globally when needed for [AutofillStorageWorker] + * to run maintenance on the storage. + * */ + internal fun requireAutofillStorage(): CreditCardsAddressesStorage { + return requireNotNull(autofillStorage?.value) { + "GlobalAutofillDependencyProvider.initialize must be called before accessing the Autofill storage" + } + } +} From 7a45b806b26fcb1e60bbeccf52d2bf5f2aec4631 Mon Sep 17 00:00:00 2001 From: Jeff Boek Date: Fri, 23 Jan 2026 15:43:25 -0800 Subject: [PATCH 09/10] Bug 1978601 - Adds tests for `AutofillStorageWorker` --- .../autofill/AutofillStorageWorkerTest.kt | 90 +++++++++++++++++++ .../GlobalAutofillDependencyProviderTest.kt | 34 +++++++ 2 files changed, 124 insertions(+) create mode 100644 mobile/android/android-components/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/AutofillStorageWorkerTest.kt create mode 100644 mobile/android/android-components/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/GlobalAutofillDependencyProviderTest.kt diff --git a/mobile/android/android-components/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/AutofillStorageWorkerTest.kt b/mobile/android/android-components/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/AutofillStorageWorkerTest.kt new file mode 100644 index 0000000000000..8db62f0ff936c --- /dev/null +++ b/mobile/android/android-components/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/AutofillStorageWorkerTest.kt @@ -0,0 +1,90 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.service.sync.autofill + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.work.ListenableWorker.Result +import androidx.work.testing.TestListenableWorkerBuilder +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.test.runTest +import mozilla.components.concept.storage.CreditCardsAddressesStorage +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import kotlin.reflect.KVisibility + +@RunWith(AndroidJUnit4::class) +class AutofillStorageWorkerTest { + + @After + fun tearDown() { + GlobalAutofillDependencyProvider.autofillStorage = null + } + + @Test + fun `CreditCardsAddressesStorage's runMaintenance is called when worker's startWork is called`() = + runTest { + val autofillStorage = mock() + GlobalAutofillDependencyProvider.initialize(lazy { autofillStorage }) + val worker = + TestListenableWorkerBuilder( + testContext, + ).build() + + worker.doWork() + verify(autofillStorage).runMaintenance(AutofillStorageWorker.DB_SIZE_LIMIT_IN_BYTES.toUInt()) + } + + @Test + fun `CreditCardsAddressesStorage's runMaintenance operation is successful, successful result returned by the worker`() = + runTest { + val autofillStorage = mock() + GlobalAutofillDependencyProvider.initialize(lazy { autofillStorage }) + val worker = + TestListenableWorkerBuilder( + testContext, + ).build() + + val result = worker.doWork() + assertEquals(Result.success(), result) + } + + @Test + fun `CreditCardsAddressesStorage's runMaintenance is called, exception is thrown and failure result is returned`() = + runTest { + val autofillStorage = mock() + `when`(autofillStorage.runMaintenance(AutofillStorageWorker.DB_SIZE_LIMIT_IN_BYTES.toUInt())) + .thenThrow(CancellationException()) + GlobalAutofillDependencyProvider.initialize(lazy { autofillStorage }) + val worker = + TestListenableWorkerBuilder( + testContext, + ).build() + + val result = worker.doWork() + assertEquals(Result.failure(), result) + } + + @Test + fun `CreditCardsAddressesStorage's runMaintenance is called, exception is thrown and active write operations are cancelled`() = + runTest { + val autofillStorage = mock() + `when`(autofillStorage.runMaintenance(AutofillStorageWorker.DB_SIZE_LIMIT_IN_BYTES.toUInt())) + .thenThrow(CancellationException()) + GlobalAutofillDependencyProvider.initialize(lazy { autofillStorage }) + val worker = + TestListenableWorkerBuilder( + testContext, + ).build() + + worker.doWork() + verify(autofillStorage).cancelWrites() + } +} diff --git a/mobile/android/android-components/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/GlobalAutofillDependencyProviderTest.kt b/mobile/android/android-components/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/GlobalAutofillDependencyProviderTest.kt new file mode 100644 index 0000000000000..1b623367025df --- /dev/null +++ b/mobile/android/android-components/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/GlobalAutofillDependencyProviderTest.kt @@ -0,0 +1,34 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.service.sync.autofill + +import mozilla.components.concept.storage.CreditCardsAddressesStorage +import mozilla.components.concept.storage.LoginsStorage +import mozilla.components.support.test.mock +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +class GlobalAutofillDependencyProviderTest { + + @Before + @After + fun cleanUp() { + GlobalAutofillDependencyProvider.autofillStorage = null + } + + @Test(expected = IllegalArgumentException::class) + fun `requirePlacesStorage called without calling initialize, exception returned`() { + GlobalAutofillDependencyProvider.requireAutofillStorage() + } + + @Test + fun `requirePlacesStorage called after calling initialize, loginsStorage returned`() { + val loginsStorage = mock() + GlobalAutofillDependencyProvider.initialize(lazy { loginsStorage }) + assertEquals(loginsStorage, GlobalAutofillDependencyProvider.requireAutofillStorage()) + } +} From 2124ed9c287a7ad14ef1b7dbcebf765106755b1b Mon Sep 17 00:00:00 2001 From: Jeff Boek Date: Fri, 23 Jan 2026 15:18:00 -0800 Subject: [PATCH 10/10] Bug 1978601 - `registerStorageMaintenanceWorker` for Autofill --- .../app/src/main/java/org/mozilla/fenix/FenixApplication.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt index f915247c4a82b..a8eb119d45b09 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt @@ -52,6 +52,7 @@ import mozilla.components.feature.top.sites.TopSitesProviderConfig import mozilla.components.feature.webcompat.reporter.WebCompatReporterFeature import mozilla.components.lib.crash.CrashReporter import mozilla.components.service.fxa.manager.SyncEnginesStorage +import mozilla.components.service.sync.autofill.GlobalAutofillDependencyProvider import mozilla.components.service.sync.logins.GlobalLoginsDependencyProvider import mozilla.components.service.sync.logins.LoginsApiException import mozilla.components.support.AppServicesInitializer @@ -299,6 +300,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider, ThemeProvider // for the periodic task. GlobalPlacesDependencyProvider.initialize(components.core.historyStorage) GlobalLoginsDependencyProvider.initialize(lazy { components.core.passwordsStorage }) + GlobalAutofillDependencyProvider.initialize(lazy { components.core.autofillStorage }) GlobalSyncedTabsCommandsProvider.initialize(lazy { components.backgroundServices.syncedTabsCommands }) @@ -487,6 +489,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider, ThemeProvider // to run the maintenance on. components.core.historyStorage.registerStorageMaintenanceWorker() components.core.passwordsStorage.registerStorageMaintenanceWorker() + components.core.autofillStorage.registerStorageMaintenanceWorker() } }