Commit a0aa405e authored by Grisha Kruglov's avatar Grisha Kruglov Committed by Grisha Kruglov
Browse files

Part 2: Move FirefoxSyncFeature into service-sync, rename as StorageSync

We will need to access this functionality from the SyncDispatcher implementation,
and so we need to move it out of a feature component. This code is not really
a "feature" in a sense of how our other features behave, either.
parent a8c300c8
......@@ -188,6 +188,10 @@ projects:
path: components/service/sync-logins
description: 'A library for integrating with Firefox Sync - Logins.'
publish: true
service-sync:
path: components/service/sync
description: 'A library for managing data synchronization.'
publish: true
service-fretboard:
path: components/service/fretboard
description: 'An Android framework for segmenting users in order to run A/B tests and rollout features gradually.'
......
# [Android Components](../../../README.md) > Service > Sync
A library for managing data synchronization.
## Usage
### Setting up the dependency
Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)):
```
implementation "org.mozilla.components:service-sync:{latest-version}"
```
/* 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/. */
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion config.compileSdkVersion
defaultConfig {
minSdkVersion config.minSdkVersion
targetSdkVersion config.targetSdkVersion
}
lintOptions {
warningsAsErrors true
abortOnError true
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
consumerProguardFiles 'proguard-rules-consumer.pro'
}
}
}
dependencies {
// Types defined in concept-sync are part of this module's public API.
api project(':concept-sync')
implementation project(':concept-storage')
implementation project(':support-base')
implementation Dependencies.arch_workmanager
implementation Dependencies.kotlin_stdlib
implementation Dependencies.kotlin_coroutines
testImplementation project(':service-firefox-accounts')
testImplementation project(':support-test')
testImplementation Dependencies.testing_junit
testImplementation Dependencies.testing_robolectric
testImplementation Dependencies.testing_mockito
}
apply from: '../../../publish.gradle'
ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
# ProGuard rules for consumers of this library.
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /Users/sebastian/Library/Android/sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<manifest package="mozilla.components.service.sync"
xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
......@@ -2,8 +2,9 @@
* 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.feature.sync
package mozilla.components.service.sync
import android.support.annotation.VisibleForTesting
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import mozilla.components.concept.sync.AuthException
......@@ -18,46 +19,38 @@ import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.base.observer.Observable
import mozilla.components.support.base.observer.ObserverRegistry
val registry = ObserverRegistry<SyncStatusObserver>()
/**
* A feature implementation which orchestrates data synchronization of a set of [SyncableStore] which
* all share a common [AuthType].
*
* [AuthType] provides us with a layer of indirection that allows consumers of [FirefoxSyncFeature]
* [AuthType] provides us with a layer of indirection that allows consumers of [StorageSync]
* to use entirely different types of [SyncableStore], without this feature needing to depend on
* their specific implementations. Those implementations might have heavy native dependencies
* (e.g. places and logins depend on native libraries), and we do not want to force a consumer which
* only cares about syncing logins to have to import a places native library.
*
* @param reifyAuth A conversion method which reifies a generic [FxaAuthInfo] into an object of
* @param reifyAuth A conversion method which reifies a generic [AuthInfo] into an object of
* type [AuthType].
*/
class FirefoxSyncFeature<AuthType>(
private val syncableStores: Map<String, SyncableStore<AuthType>>,
private val syncScope: String,
private val reifyAuth: suspend (authInfo: AuthInfo) -> AuthType
) : Observable<SyncStatusObserver> by registry {
private val logger = Logger("feature-sync")
class StorageSync<AuthType>(
private val syncableStores: Map<String, SyncableStore<AuthType>>,
private val syncScope: String,
private val reifyAuth: suspend (authInfo: AuthInfo) -> AuthType
) : Observable<SyncStatusObserver> by ObserverRegistry() {
private val logger = Logger("StorageSync")
/**
* Sync operation exposed by this feature is guarded by a mutex, ensuring that only one Sync
* may be running at any given time.
*/
private var syncMutex = Mutex()
/**
* @return A [Boolean] indicating if any sync operations are currently running.
*/
fun syncRunning(): Boolean {
return syncMutex.isLocked
}
@VisibleForTesting
internal var syncMutex = Mutex()
/**
* Performs a sync of configured [SyncableStore] history instance. This method guarantees that
* only one sync may be running at any given time.
*
* @param account [FirefoxAccountShaped] for which to perform a sync.
* @param account [OAuthAccount] for which to perform a sync.
* @return a [SyncResult] indicating result of synchronization of configured stores.
*/
suspend fun sync(account: OAuthAccount): SyncResult = syncMutex.withLock { withListeners {
......@@ -84,9 +77,9 @@ class FirefoxSyncFeature<AuthType>(
} }
private suspend fun syncStore(
store: SyncableStore<AuthType>,
storeName: String,
account: AuthType
store: SyncableStore<AuthType>,
storeName: String,
account: AuthType
): StoreSyncStatus {
return StoreSyncStatus(store.sync(account).also {
if (it is SyncError) {
......
......@@ -2,17 +2,19 @@
* 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.feature.sync
package mozilla.components.service.sync
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.runBlocking
import mozilla.components.concept.storage.SyncError
import mozilla.components.concept.storage.SyncOk
import mozilla.components.concept.storage.SyncStatus
import mozilla.components.concept.storage.SyncableStore
import mozilla.components.service.fxa.AccessTokenInfo
import mozilla.components.service.fxa.FirefoxAccountShaped
import mozilla.components.service.fxa.OAuthScopedKey
import mozilla.components.concept.sync.AccessTokenInfo
import mozilla.components.concept.sync.AuthInfo
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.concept.sync.OAuthScopedKey
import mozilla.components.concept.sync.SyncError
import mozilla.components.concept.sync.SyncOk
import mozilla.components.concept.sync.SyncStatus
import mozilla.components.concept.sync.SyncStatusObserver
import mozilla.components.concept.sync.SyncableStore
import mozilla.components.support.test.any
import mozilla.components.support.test.mock
import org.junit.Assert.assertEquals
......@@ -25,29 +27,21 @@ import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import java.lang.Exception
class FirefoxSyncFeatureTest {
data class TestAuthType(
val kid: String,
val fxaAccessToken: String,
val syncKey: String,
val tokenServerUrl: String
)
private fun FxaAuthInfo.into(): TestAuthType {
return TestAuthType(this.kid, this.fxaAccessToken, this.syncKey, this.tokenServerUrl)
}
class StorageSyncTest {
private val testAuth = AuthInfo("kid", "token", "key", "server")
@Test
fun `sync with no stores`() = runBlocking {
val feature = FirefoxSyncFeature(mapOf()) { it.into() }
val feature = StorageSync(mapOf(), "sync-scope") { it }
val results = feature.sync(mock())
assertTrue(results.isEmpty())
}
@Test
fun `sync with configured stores`() = runBlocking {
val mockAccount: FirefoxAccountShaped = mock()
val mockAccount: OAuthAccount = mock()
`when`(mockAccount.getTokenServerEndpointURL()).thenReturn("dummyUrl")
`when`(mockAccount.authInfo("sync-scope")).thenReturn(testAuth)
val mockAccessTokenInfo = AccessTokenInfo(
scope = "scope", key = OAuthScopedKey("kty", "scope", "kid", "k"), token = "token", expiresAt = 0
......@@ -55,41 +49,42 @@ class FirefoxSyncFeatureTest {
`when`(mockAccount.getAccessToken(any())).thenReturn(CompletableDeferred((mockAccessTokenInfo)))
// Single store, different result types.
val testStore: SyncableStore<TestAuthType> = mock()
val feature = FirefoxSyncFeature(mapOf("testStore" to testStore)) { it.into() }
val testStore: SyncableStore<AuthInfo> = mock()
val feature = StorageSync(mapOf("testStore" to testStore), "sync-scope") { it }
`when`(testStore.sync(any())).thenReturn(SyncOk)
var results = feature.sync(mockAccount)
assertTrue(results["testStore"]!!.status is SyncOk)
assertTrue(results.getValue("testStore").status is SyncOk)
assertEquals(1, results.size)
`when`(testStore.sync(any())).thenReturn(SyncError(Exception("test")))
results = feature.sync(mockAccount)
var error = results["testStore"]!!.status
var error = results.getValue("testStore").status
assertTrue(error is SyncError)
assertEquals("test", (error as SyncError).exception.message)
assertEquals(1, results.size)
// Multiple stores, different result types.
val anotherStore: SyncableStore<TestAuthType> = mock()
val anotherFeature = FirefoxSyncFeature(mapOf(
Pair("testStore", testStore),
Pair("goodStore", anotherStore))
) { it.into() }
val anotherStore: SyncableStore<AuthInfo> = mock()
val anotherFeature = StorageSync(mapOf(
Pair("testStore", testStore),
Pair("goodStore", anotherStore)),
"sync-scope"
) { it }
`when`(anotherStore.sync(any())).thenReturn(SyncOk)
results = anotherFeature.sync(mockAccount)
assertEquals(2, results.size)
error = results["testStore"]!!.status
error = results.getValue("testStore").status
assertTrue(error is SyncError)
assertEquals("test", (error as SyncError).exception.message)
assertTrue(results["goodStore"]!!.status is SyncOk)
assertTrue(results.getValue("goodStore").status is SyncOk)
}
@Test
fun `sync status can be observed`() = runBlocking {
val mockAccount: FirefoxAccountShaped = mock()
val mockAccount: OAuthAccount = mock()
`when`(mockAccount.getTokenServerEndpointURL()).thenReturn("dummyUrl")
val mockAccessTokenInfo = AccessTokenInfo(
......@@ -110,19 +105,19 @@ class FirefoxSyncFeatureTest {
}
// A store that runs verifications during a sync.
val testStore = object : SyncableStore<TestAuthType> {
override suspend fun sync(authInfo: TestAuthType): SyncStatus {
val testStore = object : SyncableStore<AuthInfo> {
override suspend fun sync(authInfo: AuthInfo): SyncStatus {
verifier.verify()
return SyncOk
}
}
val syncStatusObserver: SyncStatusObserver = mock()
val feature = FirefoxSyncFeature(mapOf("testStore" to testStore)) { it.into() }
val feature = StorageSync(mapOf("testStore" to testStore), "sync-scope") { it }
// These assertions will run while sync is in progress.
verifier.addVerifyBlock {
assertTrue(feature.syncRunning())
assertTrue(feature.syncMutex.isLocked)
verify(syncStatusObserver, times(1)).onStarted()
verify(syncStatusObserver, never()).onIdle()
}
......@@ -130,12 +125,12 @@ class FirefoxSyncFeatureTest {
feature.register(syncStatusObserver)
verify(syncStatusObserver, never()).onStarted()
verify(syncStatusObserver, never()).onIdle()
assertFalse(feature.syncRunning())
assertFalse(feature.syncMutex.isLocked)
feature.sync(mockAccount)
verify(syncStatusObserver, times(1)).onStarted()
verify(syncStatusObserver, times(1)).onIdle()
assertFalse(feature.syncRunning())
assertFalse(feature.syncMutex.isLocked)
}
}
......@@ -39,7 +39,7 @@ dependencies {
implementation project(':concept-storage')
implementation project(':browser-storage-sync')
implementation project(':service-firefox-accounts')
implementation project(':feature-sync')
implementation project(':service-sync')
implementation Dependencies.support_constraintlayout
}
......@@ -21,10 +21,10 @@ import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.concept.sync.Profile
import mozilla.components.concept.sync.SyncError
import mozilla.components.concept.sync.SyncStatusObserver
import mozilla.components.feature.sync.FirefoxSyncFeature
import mozilla.components.service.fxa.FxaAccountManager
import mozilla.components.service.fxa.Config
import mozilla.components.service.fxa.FxaException
import mozilla.components.service.sync.StorageSync
import mozilla.components.support.base.log.Log
import mozilla.components.support.base.log.sink.AndroidLogSink
import java.lang.Exception
......@@ -43,7 +43,7 @@ class MainActivity : AppCompatActivity(), LoginFragment.OnLoginCompleteListener,
)
}
private val featureSync by lazy {
FirefoxSyncFeature(
StorageSync(
syncableStores = mapOf(historyStoreName to historyStorage),
syncScope = "https://identity.mozilla.com/apps/oldsync"
) { authInfo ->
......
......@@ -37,9 +37,9 @@ android {
dependencies {
implementation project(':concept-storage')
implementation project(':feature-sync')
implementation project(':service-firefox-accounts')
implementation project(':service-sync-logins')
implementation project(':service-sync')
implementation Dependencies.kotlin_stdlib
......
......@@ -20,11 +20,11 @@ import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.concept.sync.Profile
import mozilla.components.concept.sync.SyncError
import mozilla.components.feature.sync.FirefoxSyncFeature
import mozilla.components.service.fxa.FxaAccountManager
import mozilla.components.service.fxa.Config
import mozilla.components.service.fxa.FirefoxAccount
import mozilla.components.service.fxa.FxaException
import mozilla.components.service.sync.StorageSync
import mozilla.components.service.sync.logins.AsyncLoginsStorageAdapter
import mozilla.components.service.sync.logins.SyncableLoginsStore
import mozilla.components.service.sync.logins.SyncUnlockInfo
......@@ -47,7 +47,7 @@ open class MainActivity : AppCompatActivity(), LoginFragment.OnLoginCompleteList
}
private val featureSync by lazy {
FirefoxSyncFeature(
StorageSync(
syncableStores = mapOf(Pair(loginsStoreName, loginsStore)),
syncScope = "https://identity.mozilla.com/apps/oldsync"
) { authInfo ->
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment