Skip to content

Commit 4b47a7d

Browse files
MickeyMozGrisha Kruglov
and
Grisha Kruglov
committed
4129: Improve/simplify sync and device APIs r=Amejia481,jonalmeida a=grigoryk This patch series aims to improve the API surface which exposes sync and device management. ## First patch: Refresh device constellation and polls for events during account init (restore, login, signup, recovery, etc). This allows API consumers to worry somewhat less about having to do this manually. Refresh is performed only for devices that have SEND_TAB capability enabled. In the future we'll have "system events" that we'd like to poll as soon as possible, which this enables. ## Second patch: Introduces a `debounce` parameter that can be passed into `syncNowAsync`. When `debounce=true`, sync request will be ignored if sync already ran recently. With this change in place, applications can simply invoke `syncNowAsync(debounce = true)` in `onResume`, without having to worry about running sync twice in a row in case of startup (sync is executed during account manager initialization). This change lets us trivially fix mozilla-mobile#3284. ## Third patch: Simplifies internal implementation of `FxaDeviceConstellation`, removing `PollingDeviceManager` entirely. Public API is now both simpler and provides applications with more control: polling for device events is now separate from refreshing constellation state. Lots of new tests added. Co-authored-by: Grisha Kruglov <[email protected]>
2 parents 3c8dd6f + 093a07c commit 4b47a7d

File tree

14 files changed

+531
-570
lines changed

14 files changed

+531
-570
lines changed

components/concept/sync/src/main/java/mozilla/components/concept/sync/Devices.kt

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,6 @@ interface DeviceConstellation : Observable<DeviceEventsObserver> {
4848
*/
4949
fun registerDeviceObserver(observer: DeviceConstellationObserver, owner: LifecycleOwner, autoPause: Boolean)
5050

51-
/**
52-
* Get all devices in the constellation.
53-
* @return A list of all devices in the constellation, or `null` on failure.
54-
*/
55-
fun fetchAllDevicesAsync(): Deferred<List<Device>?>
56-
5751
/**
5852
* Set name of the current device.
5953
* @param name New device name.
@@ -84,29 +78,19 @@ interface DeviceConstellation : Observable<DeviceEventsObserver> {
8478
fun processRawEventAsync(payload: String): Deferred<Boolean>
8579

8680
/**
87-
* Poll for events targeted at the current [Device]. It's expected that if a [DeviceEvent] was
88-
* returned after a poll, it will not be returned in consequent polls.
89-
* @return A list of [DeviceEvent] instances that are currently pending for this [Device], or `null` on failure.
90-
*/
91-
fun pollForEventsAsync(): Deferred<List<DeviceEvent>?>
92-
93-
/**
94-
* Begin periodically refreshing constellation state, including polling for events.
95-
*/
96-
fun startPeriodicRefresh()
97-
98-
/**
99-
* Stop periodically refreshing constellation state and polling for events.
81+
* Refreshes [ConstellationState]. Registered [DeviceConstellationObserver] observers will be notified.
82+
*
83+
* @return A [Deferred] that will be resolved with a success flag once operation is complete.
10084
*/
101-
fun stopPeriodicRefresh()
85+
fun refreshDevicesAsync(): Deferred<Boolean>
10286

10387
/**
104-
* Refreshes [ConstellationState] and polls for device events.
88+
* Polls for any pending [DeviceEvent] events.
89+
* In case of new events, registered [DeviceEventsObserver] observers will be notified.
10590
*
106-
* @return A [Deferred] that will be resolved with a success flag once operation is complete. Failure may
107-
* indicate a partial success.
91+
* @return A [Deferred] that will be resolved with a success flag once operation is complete.
10892
*/
109-
fun refreshDeviceStateAsync(): Deferred<Boolean>
93+
fun pollForEventsAsync(): Deferred<Boolean>
11094
}
11195

11296
/**

components/service/firefox-accounts/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ GlobalSyncableStoreProvider.configureStore("bookmarks" to bookmarksStorage)
4848
val accountManager = FxaAccountManager(
4949
context = this,
5050
serverConfig = ServerConfig.release(CLIENT_ID, REDIRECT_URL),
51-
deviceConfig =DeviceConfig(
51+
deviceConfig = DeviceConfig(
5252
name = "Sample app",
5353
type = DeviceType.MOBILE,
5454
capabilities = setOf(DeviceCapability.SEND_TAB)
@@ -63,6 +63,10 @@ accountManager.register(accountObserver, owner = this, autoPause = true)
6363
accountManager.registerForSyncEvents(syncObserver, owner = this, autoPause = true)
6464

6565
// Observe incoming device events (e.g. SEND_TAB events from other devices).
66+
// Note that since the device is configured with a SEND_TAB capability, device constellation will be
67+
// automatically updated during any account initialization flow (restore, login, sign-up, recovery).
68+
// It is up to the application to keep it up-to-date beyond that.
69+
// See `account.deviceConstellation().refreshDeviceStateAsync()`.
6670
accountManager.registerForDeviceEvents(deviceEventsObserver, owner = this, autoPause = true)
6771

6872
// Now that all of the observers we care about are registered, kick off the account manager.

components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FirefoxAccount.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ class FirefoxAccount internal constructor(
9090
) : this(InternalFxAcct(config, persistCallback))
9191

9292
override fun close() {
93-
deviceConstellation.stopPeriodicRefresh()
9493
job.cancel()
9594
inner.close()
9695
}

components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FxaDeviceConstellation.kt

Lines changed: 61 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44

55
package mozilla.components.service.fxa
66

7+
import androidx.annotation.GuardedBy
78
import androidx.lifecycle.LifecycleOwner
89
import kotlinx.coroutines.CoroutineScope
910
import kotlinx.coroutines.Deferred
1011
import kotlinx.coroutines.Dispatchers
1112
import kotlinx.coroutines.async
1213
import kotlinx.coroutines.launch
13-
import mozilla.appservices.fxaclient.AccountEvent
1414
import mozilla.appservices.fxaclient.FirefoxAccount
1515
import mozilla.components.concept.sync.ConstellationState
1616
import mozilla.components.concept.sync.Device
@@ -22,8 +22,6 @@ import mozilla.components.concept.sync.DeviceEventOutgoing
2222
import mozilla.components.concept.sync.DeviceEventsObserver
2323
import mozilla.components.concept.sync.DevicePushSubscription
2424
import mozilla.components.concept.sync.DeviceType
25-
import mozilla.components.service.fxa.manager.DeviceManagerProvider
26-
import mozilla.components.service.fxa.manager.PollingDeviceManager
2725
import mozilla.components.support.base.log.logger.Logger
2826
import mozilla.components.support.base.observer.Observable
2927
import mozilla.components.support.base.observer.ObserverRegistry
@@ -39,19 +37,12 @@ class FxaDeviceConstellation(
3937
private val logger = Logger("FxaDeviceConstellation")
4038

4139
private val deviceObserverRegistry = ObserverRegistry<DeviceConstellationObserver>()
42-
private val deviceManager = PollingDeviceManager(this, scope, deviceObserverRegistry)
4340

44-
init {
45-
DeviceManagerProvider.deviceManager = deviceManager
41+
@GuardedBy("this")
42+
private var constellationState: ConstellationState? = null
4643

47-
deviceManager.register(object : DeviceEventsObserver {
48-
override fun onEvents(events: List<DeviceEvent>) {
49-
// TODO notifyObserversOnMainThread
50-
CoroutineScope(Dispatchers.Main).launch {
51-
notifyObservers { onEvents(events) }
52-
}
53-
}
54-
})
44+
override fun state(): ConstellationState? = synchronized(this) {
45+
return constellationState
5546
}
5647

5748
override fun initDeviceAsync(
@@ -77,33 +68,7 @@ class FxaDeviceConstellation(
7768
override fun processRawEventAsync(payload: String): Deferred<Boolean> {
7869
return scope.async {
7970
handleFxaExceptions(logger, "processing raw events") {
80-
val events = account.handlePushMessage(payload).filter {
81-
it is AccountEvent.TabReceived
82-
}.map {
83-
(it as AccountEvent.TabReceived).into()
84-
}
85-
deviceManager.processEvents(events)
86-
}
87-
}
88-
}
89-
90-
override fun pollForEventsAsync(): Deferred<List<DeviceEvent>?> {
91-
// Currently ignoring non-TabReceived events.
92-
return scope.async {
93-
handleFxaExceptions(logger, "polling for device events", { null }) {
94-
account.pollDeviceCommands().filter {
95-
it is AccountEvent.TabReceived
96-
}.map {
97-
(it as AccountEvent.TabReceived).into()
98-
}
99-
}
100-
}
101-
}
102-
103-
override fun fetchAllDevicesAsync(): Deferred<List<Device>?> {
104-
return scope.async {
105-
handleFxaExceptions(logger, "fetching all devices", { null }) {
106-
account.getDevices().map { it.into() }
71+
processEvents(account.handlePushMessage(payload).map { it.into() })
10772
}
10873
}
10974
}
@@ -122,7 +87,7 @@ class FxaDeviceConstellation(
12287
account.setDeviceDisplayName(name)
12388
}
12489
// See the latest device (name) changes after changing it.
125-
val refreshDevices = deviceManager.refreshDevicesAsync().await()
90+
val refreshDevices = refreshDevicesAsync().await()
12691

12792
rename && refreshDevices
12893
}
@@ -151,26 +116,67 @@ class FxaDeviceConstellation(
151116
}
152117
}
153118

154-
override fun state(): ConstellationState? {
155-
return deviceManager.constellationState()
119+
override fun pollForEventsAsync(): Deferred<Boolean> {
120+
return scope.async {
121+
val events = handleFxaExceptions(logger, "polling for device events", { null }) {
122+
account.pollDeviceCommands().map { it.into() }
123+
}
124+
125+
if (events == null) {
126+
false
127+
} else {
128+
processEvents(events)
129+
true
130+
}
131+
}
156132
}
157133

158-
override fun startPeriodicRefresh() {
159-
deviceManager.startPolling()
134+
private fun processEvents(events: List<DeviceEvent>) = CoroutineScope(Dispatchers.Main).launch {
135+
notifyObservers { onEvents(events) }
160136
}
161137

162-
override fun stopPeriodicRefresh() {
163-
deviceManager.stopPolling()
138+
override fun refreshDevicesAsync(): Deferred<Boolean> = synchronized(this) {
139+
return scope.async {
140+
logger.info("Refreshing device list...")
141+
142+
// Attempt to fetch devices, or bail out on failure.
143+
val allDevices = fetchAllDevicesAsync().await() ?: return@async false
144+
145+
// Find the current device.
146+
val currentDevice = allDevices.find { it.isCurrentDevice }
147+
// Filter out the current devices.
148+
val otherDevices = allDevices.filter { !it.isCurrentDevice }
149+
150+
val newState = ConstellationState(currentDevice, otherDevices)
151+
constellationState = newState
152+
153+
CoroutineScope(Dispatchers.Main).launch {
154+
// NB: at this point, 'constellationState' might have changed.
155+
// Notify with an immutable, local 'newState' instead.
156+
deviceObserverRegistry.notifyObservers { onDevicesUpdate(newState) }
157+
}
158+
159+
// Check if our current device's push subscription needs to be renewed.
160+
constellationState?.currentDevice?.let {
161+
if (it.subscriptionExpired) {
162+
logger.info("Current device needs push endpoint registration")
163+
}
164+
}
165+
166+
logger.info("Refreshed device list; saw ${allDevices.size} device(s).")
167+
true
168+
}
164169
}
165170

166-
override fun refreshDeviceStateAsync(): Deferred<Boolean> {
171+
/**
172+
* Get all devices in the constellation.
173+
* @return A list of all devices in the constellation, or `null` on failure.
174+
*/
175+
private fun fetchAllDevicesAsync(): Deferred<List<Device>?> {
167176
return scope.async {
168-
val refreshedDevices = deviceManager.refreshDevicesAsync().await()
169-
val events = pollForEventsAsync().await()?.let {
170-
deviceManager.processEvents(it)
171-
true
172-
} ?: false
173-
refreshedDevices && events
177+
handleFxaExceptions(logger, "fetching all devices", { null }) {
178+
account.getDevices().map { it.into() }
179+
}
174180
}
175181
}
176182
}

components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Types.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,12 @@ fun mozilla.components.concept.sync.TabData.into(): TabHistoryEntry {
159159
)
160160
}
161161

162+
fun AccountEvent.into(): mozilla.components.concept.sync.DeviceEvent {
163+
return when (this) {
164+
is AccountEvent.TabReceived -> this.into()
165+
}
166+
}
167+
162168
fun AccountEvent.TabReceived.into(): mozilla.components.concept.sync.DeviceEvent.TabReceived {
163169
return mozilla.components.concept.sync.DeviceEvent.TabReceived(
164170
from = this.from?.into(),

components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/FxaAccountManager.kt

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import kotlinx.coroutines.launch
1818
import kotlinx.coroutines.cancel
1919
import mozilla.components.concept.sync.AccountObserver
2020
import mozilla.components.concept.sync.AuthException
21+
import mozilla.components.concept.sync.DeviceCapability
2122
import mozilla.components.concept.sync.DeviceEvent
2223
import mozilla.components.concept.sync.DeviceEventsObserver
2324
import mozilla.components.concept.sync.OAuthAccount
@@ -321,8 +322,12 @@ open class FxaAccountManager(
321322
* Request an immediate synchronization, as configured according to [syncConfig].
322323
*
323324
* @param startup Boolean flag indicating if sync is being requested in a startup situation.
325+
* @param debounce Boolean flag indicating if this sync may be debounced (in case another sync executed recently).
324326
*/
325-
fun syncNowAsync(startup: Boolean = false): Deferred<Unit> = CoroutineScope(coroutineContext).async {
327+
fun syncNowAsync(
328+
startup: Boolean = false,
329+
debounce: Boolean = false
330+
): Deferred<Unit> = CoroutineScope(coroutineContext).async {
326331
// Make sure auth cache is populated before we try to sync.
327332
maybeUpdateSyncAuthInfoCache()
328333

@@ -333,7 +338,7 @@ open class FxaAccountManager(
333338
"Sync is not configured. Construct this class with a 'syncConfig' or use 'setSyncConfig'"
334339
)
335340
}
336-
syncManager?.now(startup)
341+
syncManager?.now(startup, debounce)
337342
}
338343
Unit
339344
}
@@ -562,9 +567,7 @@ open class FxaAccountManager(
562567
deviceConfig.name, deviceConfig.type, deviceConfig.capabilities
563568
).await()
564569

565-
maybeUpdateSyncAuthInfoCache()
566-
567-
notifyObservers { onAuthenticated(account, true) }
570+
postAuthenticated(true)
568571

569572
Event.FetchProfile
570573
}
@@ -583,19 +586,14 @@ open class FxaAccountManager(
583586
logger.warn("Failed to ensure device capabilities.")
584587
}
585588

586-
// We used to perform a periodic device event polling, but for now we do not.
587-
// This cancels any periodic jobs device may still have active.
588-
// See https://github.com/mozilla-mobile/android-components/issues/3433
589-
logger.info("Stopping periodic refresh of the device constellation")
590-
account.deviceConstellation().stopPeriodicRefresh()
591-
592-
maybeUpdateSyncAuthInfoCache()
593-
594-
notifyObservers { onAuthenticated(account, false) }
589+
postAuthenticated(false)
595590

596591
Event.FetchProfile
597592
}
598593
Event.SignedInShareableAccount -> {
594+
// Note that we are not registering an account persistence callback here like
595+
// we do in other `AuthenticatedNoProfile` methods, because it would have been
596+
// already registered while handling `Event.SignInShareableAccount`.
599597
logger.info("Registering device constellation observer")
600598
account.deviceConstellation().register(deviceEventsIntegration)
601599

@@ -605,9 +603,7 @@ open class FxaAccountManager(
605603
deviceConfig.name, deviceConfig.type, deviceConfig.capabilities
606604
).await()
607605

608-
maybeUpdateSyncAuthInfoCache()
609-
610-
notifyObservers { onAuthenticated(account, true) }
606+
postAuthenticated(true)
611607

612608
Event.FetchProfile
613609
}
@@ -627,9 +623,7 @@ open class FxaAccountManager(
627623
deviceConfig.name, deviceConfig.type, deviceConfig.capabilities
628624
).await()
629625

630-
maybeUpdateSyncAuthInfoCache()
631-
632-
notifyObservers { onAuthenticated(account, false) }
626+
postAuthenticated(false)
633627

634628
Event.FetchProfile
635629
}
@@ -753,6 +747,21 @@ open class FxaAccountManager(
753747
return null
754748
}
755749

750+
private suspend fun postAuthenticated(newAccount: Boolean) {
751+
// Before any sync workers have a chance to access it, make sure our SyncAuthInfo cache is hot.
752+
maybeUpdateSyncAuthInfoCache()
753+
754+
// Notify our internal (sync) and external (app logic) observers.
755+
notifyObservers { onAuthenticated(account, newAccount) }
756+
757+
// If device supports SEND_TAB...
758+
if (deviceConfig.capabilities.contains(DeviceCapability.SEND_TAB)) {
759+
// ... update constellation state, and poll for any pending device events.
760+
account.deviceConstellation().refreshDevicesAsync().await()
761+
account.deviceConstellation().pollForEventsAsync().await()
762+
}
763+
}
764+
756765
@VisibleForTesting
757766
open fun createAccount(config: ServerConfig): OAuthAccount {
758767
return FirefoxAccount(config)

0 commit comments

Comments
 (0)