tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 33a83be7c3287322604749086e0e79d7ab0bf74f
parent 389d9db63cc81f88f3e391d928754ce437342c97
Author: Titouan Thibaud <tthibaud@mozilla.com>
Date:   Wed, 19 Nov 2025 09:06:48 +0000

Bug 1989604 - Fix sync button loading forever on Tabs Tray r=android-reviewers,pollymce

WorkManagerSyncManager's workersLiveData might skip the RUNNING emission if WorkManagerSyncWorker.doWork's execution is really quick (in the case where the last sync was within the past 5 seconds and the sync operation is skipped). The previous code was relying on RUNNING to be received to start set isSyncActive to true, and would only dispatch onIdle() when receiving a non-RUNNING state if isSyncActive was already true.

The new implementation should be more reliable, as it will trigger onIdle when receiving a "finished" state, even if the running state wasn't previously received. With that, we make sure we correctly stop the loading state on screen.

Differential Revision: https://phabricator.services.mozilla.com/D272098

Diffstat:
Mmobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/sync/SyncManager.kt | 7+++++--
Mmobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/sync/WorkManagerSyncManager.kt | 20+++++++-------------
Amobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/sync/FakeSyncStatusObserver.kt | 29+++++++++++++++++++++++++++++
Mmobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/sync/WorkManagerSyncManagerTest.kt | 148+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 189 insertions(+), 15 deletions(-)

diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/sync/SyncManager.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/sync/SyncManager.kt @@ -4,6 +4,8 @@ package mozilla.components.service.fxa.sync +import androidx.annotation.VisibleForTesting +import androidx.work.WorkInfo import mozilla.components.concept.storage.KeyProvider import mozilla.components.concept.sync.SyncableStore import mozilla.components.service.fxa.SyncConfig @@ -117,7 +119,7 @@ internal interface SyncDispatcher : Closeable, Observable<SyncStatusObserver> { ) fun startPeriodicSync(unit: TimeUnit, period: Long, initialDelay: Long) fun stopPeriodicSync() - fun workersStateChanged(isRunning: Boolean) + fun workersStateChanged(currentWorkStates: List<WorkInfo.State>?) } /** @@ -130,7 +132,8 @@ internal abstract class SyncManager( open val logger = Logger("SyncManager") // A SyncDispatcher instance bound to an account and a set of syncable stores. - private var syncDispatcher: SyncDispatcher? = null + @VisibleForTesting + internal var syncDispatcher: SyncDispatcher? = null private val syncStatusObserverRegistry = ObserverRegistry<SyncStatusObserver>() diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/sync/WorkManagerSyncManager.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/sync/WorkManagerSyncManager.kt @@ -124,14 +124,8 @@ internal object WorkersLiveDataObserver { if (workersLiveData.hasObservers()) return // This must be called on the UI thread. - workersLiveData.observeForever { - val isRunning = when (it?.any { worker -> worker.state == WorkInfo.State.RUNNING }) { - null -> false - false -> false - true -> true - } - - dispatcher?.workersStateChanged(isRunning) + workersLiveData.observeForever { workers -> + dispatcher?.workersStateChanged(workers?.map { it.state }) // TODO process errors coming out of worker.outputData } @@ -157,13 +151,13 @@ internal class WorkManagerSyncDispatcher( stopPeriodicSync() } - override fun workersStateChanged(isRunning: Boolean) { - if (isSyncActive && !isRunning) { - notifyObservers { onIdle() } - isSyncActive = false - } else if (!isSyncActive && isRunning) { + override fun workersStateChanged(currentWorkStates: List<WorkInfo.State>?) { + if (currentWorkStates?.any { it == WorkInfo.State.RUNNING } == true) { notifyObservers { onStarted() } isSyncActive = true + } else if (currentWorkStates?.any { it.isFinished } == true) { + notifyObservers { onIdle() } + isSyncActive = false } } diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/sync/FakeSyncStatusObserver.kt b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/sync/FakeSyncStatusObserver.kt @@ -0,0 +1,29 @@ +/* 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.fxa.sync + +import java.lang.Exception + +class FakeSyncStatusObserver : SyncStatusObserver { + sealed class Event { + object OnStarted : Event() + object OnIdle : Event() + data class OnError(val error: Exception?) : Event() + } + + val events = mutableListOf<Event>() + + override fun onStarted() { + events.add(Event.OnStarted) + } + + override fun onIdle() { + events.add(Event.OnIdle) + } + + override fun onError(error: Exception?) { + events.add(Event.OnError(error)) + } +} diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/sync/WorkManagerSyncManagerTest.kt b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/sync/WorkManagerSyncManagerTest.kt @@ -5,8 +5,15 @@ package mozilla.components.service.fxa.sync import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.work.Configuration +import androidx.work.WorkInfo import androidx.work.WorkerParameters import androidx.work.impl.utils.taskexecutor.TaskExecutor +import androidx.work.testing.WorkManagerTestInitHelper +import mozilla.components.service.fxa.SyncConfig +import mozilla.components.service.fxa.SyncEngine +import mozilla.components.service.fxa.sync.FakeSyncStatusObserver.Event.OnIdle +import mozilla.components.service.fxa.sync.FakeSyncStatusObserver.Event.OnStarted import mozilla.components.service.fxa.sync.WorkManagerSyncWorker.Companion.SYNC_STAGGER_BUFFER_MS import mozilla.components.service.fxa.sync.WorkManagerSyncWorker.Companion.engineSyncTimestamp import mozilla.components.support.test.mock @@ -14,6 +21,7 @@ import mozilla.components.support.test.robolectric.testContext import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -33,6 +41,11 @@ class WorkManagerSyncManagerTest { `when`(mockParam.taskExecutor).thenReturn(mockTaskExecutor) `when`(mockTaskExecutor.serialTaskExecutor).thenReturn(mock()) `when`(mockParam.tags).thenReturn(mockTags) + + WorkManagerTestInitHelper.initializeTestWorkManager( + testContext, + Configuration.Builder().build(), + ) } @Test @@ -94,4 +107,139 @@ class WorkManagerSyncManagerTest { assert(workerManagerSyncWorker.isDebounced()) assertFalse(workerManagerSyncWorker.lastSyncedWithinStaggerBuffer("test")) } + + @Test + fun `WHEN workerStateChanged receives RUNNING state THEN onStarted is notified`() { + val observer = FakeSyncStatusObserver() + val syncManager = createSyncManager(observer) + + syncManager.syncDispatcher?.workersStateChanged(listOf(WorkInfo.State.RUNNING)) + + assertEquals(listOf(OnStarted), observer.events) + assertTrue(syncManager.isSyncActive()) + } + + @Test + fun `WHEN workerStateChanged receives SUCCEEDED state THEN onIdle is notified`() { + val observer = FakeSyncStatusObserver() + val syncManager = createSyncManager(observer) + + syncManager.syncDispatcher?.workersStateChanged(listOf(WorkInfo.State.ENQUEUED)) + syncManager.syncDispatcher?.workersStateChanged(listOf(WorkInfo.State.SUCCEEDED)) + + assertEquals(listOf(OnIdle), observer.events) + assertFalse(syncManager.isSyncActive()) + } + + @Test + fun `WHEN workerStateChanged receives FAILED state THEN onIdle is notified`() { + val observer = FakeSyncStatusObserver() + val syncManager = createSyncManager(observer) + + syncManager.syncDispatcher?.workersStateChanged(listOf(WorkInfo.State.ENQUEUED)) + syncManager.syncDispatcher?.workersStateChanged(listOf(WorkInfo.State.FAILED)) + + assertEquals(listOf(OnIdle), observer.events) + assertFalse(syncManager.isSyncActive()) + } + + @Test + fun `WHEN workerStateChanged receives CANCELLED state THEN onIdle is notified`() { + val observer = FakeSyncStatusObserver() + val syncManager = createSyncManager(observer) + + syncManager.syncDispatcher?.workersStateChanged(listOf(WorkInfo.State.CANCELLED)) + + assertEquals(listOf(OnIdle), observer.events) + assertFalse(syncManager.isSyncActive()) + } + + @Test + fun `WHEN workerStateChanged receives null state THEN nothing happens`() { + val observer = FakeSyncStatusObserver() + val syncManager = createSyncManager(observer) + + syncManager.syncDispatcher?.workersStateChanged(null) + + assertTrue(observer.events.isEmpty()) + assertFalse(syncManager.isSyncActive()) + } + + @Test + fun `WHEN workerStateChanged receives empty list THEN nothing happens`() { + val observer = FakeSyncStatusObserver() + val syncManager = createSyncManager(observer) + + syncManager.syncDispatcher?.workersStateChanged(emptyList()) + + assertTrue(observer.events.isEmpty()) + assertFalse(syncManager.isSyncActive()) + } + + @Test + fun `WHEN workerStateChanged receives ENQUEUED state THEN nothing happens`() { + val observer = FakeSyncStatusObserver() + val syncManager = createSyncManager(observer) + + syncManager.syncDispatcher?.workersStateChanged(listOf(WorkInfo.State.ENQUEUED)) + + assertTrue(observer.events.isEmpty()) + assertFalse(syncManager.isSyncActive()) + } + + @Test + fun `WHEN workerStateChanged receives RUNNING then SUCCEEDED THEN both onStarted and onIdle are notified in right order`() { + val observer = FakeSyncStatusObserver() + val syncManager = createSyncManager(observer) + + syncManager.syncDispatcher?.workersStateChanged(listOf(WorkInfo.State.ENQUEUED)) + syncManager.syncDispatcher?.workersStateChanged(listOf(WorkInfo.State.RUNNING)) + syncManager.syncDispatcher?.workersStateChanged(listOf(WorkInfo.State.SUCCEEDED)) + + assertEquals(listOf(OnStarted, OnIdle), observer.events) + assertFalse(syncManager.isSyncActive()) + } + + @Test + fun `WHEN workerStateChanged is called with multiple workers in different states including one RUNNING THEN onStarted is notified`() { + val observer = FakeSyncStatusObserver() + val syncManager = createSyncManager(observer) + + syncManager.syncDispatcher?.workersStateChanged(listOf(WorkInfo.State.ENQUEUED, WorkInfo.State.RUNNING)) + + // Verify onStarted was called (because at least one is RUNNING) + assertEquals(listOf(OnStarted), observer.events) + assertTrue(syncManager.isSyncActive()) + } + + @Test + fun `WHEN workerStateChanged is called with multiple workers in different states including one SUCCEEDED THEN onIdle is notified`() { + val observer = FakeSyncStatusObserver() + val syncManager = createSyncManager(observer) + + syncManager.syncDispatcher?.workersStateChanged(listOf(WorkInfo.State.SUCCEEDED, WorkInfo.State.FAILED)) + + assertEquals(listOf(OnIdle), observer.events) + assertFalse(syncManager.isSyncActive()) + } + + @Test + fun `WHEN workerStateChanged is called with multiple workers in different states including one SUCCEEDED and one RUNNING THEN onStarted is notified`() { + val observer = FakeSyncStatusObserver() + val syncManager = createSyncManager(observer) + + syncManager.syncDispatcher?.workersStateChanged(listOf(WorkInfo.State.SUCCEEDED, WorkInfo.State.RUNNING)) + + assertEquals(listOf(OnStarted), observer.events) + assertTrue(syncManager.isSyncActive()) + } + + private fun createSyncManager(observer: FakeSyncStatusObserver): WorkManagerSyncManager = + WorkManagerSyncManager( + testContext, + SyncConfig(supportedEngines = setOf(SyncEngine.Tabs), periodicSyncConfig = null), + ).apply { + registerSyncStatusObserver(observer) + start() + } }