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

Part 1: concept-sync

parent d1fc18d3
......@@ -24,6 +24,10 @@ projects:
path: components/concept/storage
description: 'An abstract definition of a browser storage layer.'
publish: true
concept-sync:
path: components/concept/sync
description: 'An abstract definition of a browser data synchronization layer.'
publish: true
feature-awesomebar:
path: components/feature/awesomebar
description: 'Component connecting a concept-toolbar with a concept-awesomebar.'
......
......@@ -27,6 +27,7 @@ dependencies {
// These dependencies are part of this module's public API.
api Dependencies.mozilla_places
api project(':concept-storage')
api project(':concept-sync')
implementation project(':support-utils')
......
......@@ -15,10 +15,10 @@ import mozilla.components.concept.storage.HistoryAutocompleteResult
import mozilla.components.concept.storage.HistoryStorage
import mozilla.components.concept.storage.PageObservation
import mozilla.components.concept.storage.SearchResult
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.concept.sync.SyncError
import mozilla.components.concept.sync.SyncOk
import mozilla.components.concept.sync.SyncStatus
import mozilla.components.concept.sync.SyncableStore
import mozilla.components.concept.storage.VisitType
import mozilla.components.support.utils.segmentAwareDomainMatch
import org.mozilla.places.PlacesConnection
......
# [Android Components](../../../README.md) > Concept > Sync
The `concept-sync` component contains interfaces and types that describe various aspects of data synchronization.
This abstraction makes it possible to create different implementations of synchronization backends, without tightly
coupling concrete implementations of storage, accounts and sync sub-systems.
## 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)):
```Groovy
implementation "org.mozilla.components:concept-sync:{latest-version}"
```
### Integration
TODO
## License
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/
/* 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
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
// Necessary because we use 'suspend'. Fun fact: this module will compile just fine without this
// dependency, but it will crash at runtime.
// Included via 'api' because this module is unusable without coroutines.
api Dependencies.kotlin_coroutines
// Observables are part of the public API of this module.
api project(':support-base')
}
apply from: '../../../publish.gradle'
ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# 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
<!-- 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 xmlns:android="http://schemas.android.com/apk/res/android"
package="mozilla.components.concept.sync" />
package mozilla.components.concept.sync
import kotlinx.coroutines.Deferred
/**
* An auth-related exception type, for use with [AuthException].
*/
enum class AuthExceptionType(val msg: String) {
KEY_INFO("Missing key info")
}
/**
* An exception which may happen while obtaining auth information using [OAuthAccount].
*/
class AuthException(type: AuthExceptionType) : java.lang.Exception(type.msg)
/**
* Facilitates testing consumers of FirefoxAccount.
*/
interface OAuthAccount : AutoCloseable {
fun beginOAuthFlow(scopes: Array<String>, wantsKeys: Boolean): Deferred<String>
fun beginPairingFlow(pairingUrl: String, scopes: Array<String>): Deferred<String>
fun getProfile(ignoreCache: Boolean): Deferred<Profile>
fun getProfile(): Deferred<Profile>
fun completeOAuthFlow(code: String, state: String): Deferred<Unit>
fun getAccessToken(singleScope: String): Deferred<AccessTokenInfo>
fun getTokenServerEndpointURL(): String
fun toJSONString(): String
suspend fun authInfo(singleScope: String): AuthInfo {
val tokenServerURL = this.getTokenServerEndpointURL()
val tokenInfo = this.getAccessToken(singleScope).await()
val keyInfo = tokenInfo.key ?: throw AuthException(AuthExceptionType.KEY_INFO)
return AuthInfo(
kid = keyInfo.kid,
fxaAccessToken = tokenInfo.token,
syncKey = keyInfo.k,
tokenServerUrl = tokenServerURL
)
}
}
/**
* A Firefox Sync friendly auth object which can be obtained from [OAuthAccount].
*/
data class AuthInfo(
val kid: String,
val fxaAccessToken: String,
val syncKey: String,
val tokenServerUrl: String
)
/**
* Observer interface which lets its users monitor account state changes and major events.
*/
interface AccountObserver {
/**
* Account just got logged out.
*/
fun onLoggedOut()
/**
* Account was successfully authenticated.
* @param account An authenticated instance of a [OAuthAccount].
*/
fun onAuthenticated(account: OAuthAccount)
/**
* Account's profile is now available.
* @param profile A fresh version of account's [Profile].
*/
fun onProfileUpdated(profile: Profile)
/**
* Account manager encountered an error. Inspect [error] for details.
* @param error A specific error encountered.
*/
fun onError(error: Exception)
}
data class Avatar(
val url: String,
val isDefault: Boolean
)
data class Profile(
val uid: String?,
val email: String?,
val avatar: Avatar?,
val displayName: String?
)
/**
* Scoped key data.
*
* @property kid The JWK key identifier.
* @property k The JWK key data.
*/
data class OAuthScopedKey(
val kty: String,
val scope: String,
val kid: String,
val k: String
)
/**
* The result of authentication with FxA via an OAuth flow.
*
* @property token The access token produced by the flow.
* @property key An OAuthScopedKey if present.
* @property expiresAt The expiry date timestamp of this token since unix epoch (in seconds).
*/
data class AccessTokenInfo(
val scope: String,
val token: String,
val key: OAuthScopedKey?,
val expiresAt: Long
)
/* 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.sync
import mozilla.components.support.base.observer.Observable
import java.io.Closeable
import java.lang.Exception
interface SyncManager : Observable<SyncStatusObserver> {
fun authenticated(account: OAuthAccount)
fun loggedOut()
fun addStore(name: String, store: SyncableStore)
fun removeStore(name: String)
/**
* Kick-off an immediate sync.
*
* @param startup Boolean flag indicating if sync is being requested in a startup situation.
*/
fun syncNow(startup: Boolean = false)
fun createDispatcher(stores: Map<String, SyncableStore>, account: OAuthAccount): SyncDispatcher
}
interface SyncDispatcher : Closeable, Observable<SyncStatusObserver> {
fun isSyncActive(): Boolean
/**
* Kick-off an immediate sync.
*
* @param startup Boolean flag indicating if sync is being requested in a startup situation.
*/
fun syncNow(startup: Boolean = false)
fun startPeriodicSync()
fun stopPeriodicSync()
}
/**
* An interface for consumers that wish to observer "sync lifecycle" events.
*/
interface SyncStatusObserver {
/**
* Gets called at the start of a sync, before any configured syncable is synchronized.
*/
fun onStarted()
/**
* Gets called at the end of a sync, after every configured syncable has been synchronized.
*/
fun onIdle()
/**
* Gets called if sync encounters an error that's worthy of processing by status observers.
* @param error Optional relevant exception.
*/
fun onError(error: Exception?)
}
/**
* A set of results of running a sync operation for multiple instances of [SyncableStore].
*/
typealias SyncResult = Map<String, StoreSyncStatus>
data class StoreSyncStatus(val status: SyncStatus)
......@@ -2,7 +2,7 @@
* 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
package mozilla.components.concept.sync
import java.lang.Exception
......
......@@ -7,13 +7,13 @@ package mozilla.components.feature.accounts
import android.content.Context
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.runBlocking
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.concept.sync.Profile
import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.service.fxa.AccountStorage
import mozilla.components.service.fxa.Config
import mozilla.components.service.fxa.FirefoxAccountShaped
import mozilla.components.service.fxa.FxaAccountManager
import mozilla.components.service.fxa.FxaNetworkException
import mozilla.components.service.fxa.Profile
import mozilla.components.service.fxa.SharedPrefAccountStorage
import mozilla.components.support.test.any
import mozilla.components.support.test.mock
......@@ -37,9 +37,9 @@ class TestableFxaAccountManager(
config: Config,
scopes: Array<String>,
accountStorage: AccountStorage = SharedPrefAccountStorage(context),
val block: () -> FirefoxAccountShaped = { mock() }
) : FxaAccountManager(context, config, scopes, accountStorage) {
override fun createAccount(config: Config): FirefoxAccountShaped {
val block: () -> OAuthAccount = { mock() }
) : FxaAccountManager(context, config, scopes, null, accountStorage) {
override fun createAccount(config: Config): OAuthAccount {
return block()
}
}
......@@ -49,7 +49,7 @@ class FirefoxAccountsAuthFeatureTest {
@Test
fun `begin authentication`() {
val accountStorage = mock<AccountStorage>()
val mockAccount: FirefoxAccountShaped = mock()
val mockAccount: OAuthAccount = mock()
val profile = Profile(
uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
......@@ -98,7 +98,7 @@ class FirefoxAccountsAuthFeatureTest {
@Test
fun `begin authentication with errors`() {
val accountStorage = mock<AccountStorage>()
val mockAccount: FirefoxAccountShaped = mock()
val mockAccount: OAuthAccount = mock()
val profile = Profile(
uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
......
......@@ -22,7 +22,7 @@ android {
}
dependencies {
implementation project(':concept-storage')
implementation project(':concept-sync')
implementation project(':service-firefox-accounts')
// ObserverRegistry is part of this module's public API, so 'support-base' becomes necessary for
......
......@@ -6,34 +6,17 @@ package mozilla.components.feature.sync
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import mozilla.components.concept.storage.SyncError
import mozilla.components.concept.storage.SyncableStore
import mozilla.components.service.fxa.FirefoxAccountShaped
import mozilla.components.concept.sync.AuthException
import mozilla.components.concept.sync.AuthInfo
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.concept.sync.StoreSyncStatus
import mozilla.components.concept.sync.SyncError
import mozilla.components.concept.sync.SyncResult
import mozilla.components.concept.sync.SyncStatusObserver
import mozilla.components.concept.sync.SyncableStore
import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.base.observer.Observable
import mozilla.components.support.base.observer.ObserverRegistry
import java.lang.Exception
/**
* An interface for consumers that wish to observer "sync lifecycle" events.
*/
interface SyncStatusObserver {
/**
* Gets called at the start of a sync, before any configured syncable is synchronized.
*/
fun onStarted()
/**
* Gets called at the end of a sync, after every configured syncable has been synchronized.
*/
fun onIdle()
/**
* Gets called if sync encounters an error that's worthy of processing by status observers.
* @param error Optional relevant exception.
*/
fun onError(error: Exception?)
}
val registry = ObserverRegistry<SyncStatusObserver>()
......@@ -52,7 +35,8 @@ val registry = ObserverRegistry<SyncStatusObserver>()
*/
class FirefoxSyncFeature<AuthType>(
private val syncableStores: Map<String, SyncableStore<AuthType>>,
private val reifyAuth: suspend (authInfo: FxaAuthInfo) -> AuthType
private val syncScope: String,
private val reifyAuth: suspend (authInfo: AuthInfo) -> AuthType
) : Observable<SyncStatusObserver> by registry {
private val logger = Logger("feature-sync")
......@@ -76,7 +60,7 @@ class FirefoxSyncFeature<AuthType>(
* @param account [FirefoxAccountShaped] for which to perform a sync.
* @return a [SyncResult] indicating result of synchronization of configured stores.
*/
suspend fun sync(account: FirefoxAccountShaped): SyncResult = syncMutex.withLock { withListeners {
suspend fun sync(account: OAuthAccount): SyncResult = syncMutex.withLock { withListeners {
if (syncableStores.isEmpty()) {
return@withListeners mapOf()
}
......@@ -84,7 +68,7 @@ class FirefoxSyncFeature<AuthType>(
val results = mutableMapOf<String, StoreSyncStatus>()
val reifiedAuthInfo = try {
reifyAuth(account.authInfo())
reifyAuth(account.authInfo(syncScope))
} catch (e: AuthException) {
syncableStores.keys.forEach { storeName ->
results[storeName] = StoreSyncStatus(SyncError(e))
......@@ -93,7 +77,7 @@ class FirefoxSyncFeature<AuthType>(
}
syncableStores.keys.forEach { storeName ->
results[storeName] = syncStore(syncableStores[storeName]!!, storeName, reifiedAuthInfo)
results[storeName] = syncStore(syncableStores.getValue(storeName), storeName, reifiedAuthInfo)
}
return@withListeners results
......
/* 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.feature.sync
import mozilla.components.concept.storage.SyncStatus
import mozilla.components.service.fxa.FirefoxAccount
import mozilla.components.service.fxa.FirefoxAccountShaped
import java.lang.Exception
/**
* An auth-related exception type, for use with [AuthException].
*/
enum class AuthExceptionType(val msg: String) {
KEY_INFO("Missing key info")
}
/**
* An exception which may happen while obtaining auth information using [FirefoxAccount].
*/
class AuthException(type: AuthExceptionType) : Exception(type.msg)
/**
* A set of results of running a sync operation for all configured stores.
*/
typealias SyncResult = Map<String, StoreSyncStatus>
data class StoreSyncStatus(val status: SyncStatus)
/**
* A Firefox Sync friendly auth object which can be obtained from [FirefoxAccount].
*/
data class FxaAuthInfo(
val kid: String,
val fxaAccessToken: String,
val syncKey: String,
val tokenServerUrl: String
)
@Suppress("ThrowsCount")
internal suspend fun FirefoxAccountShaped.authInfo(): FxaAuthInfo {
val syncScope = "https://identity.mozilla.com/apps/oldsync"
val tokenServerURL = this.getTokenServerEndpointURL()
val tokenInfo = this.getAccessToken(syncScope).await()
val keyInfo = tokenInfo.key ?: throw AuthException(AuthExceptionType.KEY_INFO)
return FxaAuthInfo(
kid = keyInfo.kid,
fxaAccessToken = tokenInfo.token,
syncKey = keyInfo.k,
tokenServerUrl = tokenServerURL
)
}
......@@ -28,6 +28,9 @@ android {
}
dependencies {
// Types defined in concept-sync are part of the public API of this module.
api project(':concept-sync')
// Parts of this dependency are typealiase'd or are otherwise part of this module's public API.
api Dependencies.mozilla_fxa
......
/* 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
typealias AccessTokenInfo = mozilla.appservices.fxaclient.AccessTokenInfo
typealias OAuthScopedKey = mozilla.appservices.fxaclient.ScopedKey
......@@ -6,14 +6,15 @@ package mozilla.components.service.fxa
import android.content.Context
import android.content.SharedPreferences
import mozilla.components.concept.sync.OAuthAccount
const val FXA_STATE_PREFS_KEY = "fxaAppState"
const val FXA_STATE_KEY = "fxaState"
interface AccountStorage {
@Throws(Exception::class)
fun read(): FirefoxAccountShaped?
fun write(account: FirefoxAccountShaped)
fun read(): OAuthAccount?
fun write(account: OAuthAccount)
fun clear()
}
......@@ -21,7 +22,7 @@ class SharedPrefAccountStorage(val context: Context) : AccountStorage {
/**
* @throws FxaException if JSON failed to parse into a [FirefoxAccount].
*/
override fun read(): FirefoxAccountShaped? {
override fun read(): OAuthAccount? {
val savedJSON = accountPreferences().getString(FXA_STATE_KEY, null)
?: return null
......@@ -29,7 +30,7 @@ class SharedPrefAccountStorage(val context: Context) : AccountStorage {
return FirefoxAccount.fromJSONString(savedJSON)
}
override fun write(account: FirefoxAccountShaped) {
override fun write(account: OAuthAccount) {
accountPreferences()
.edit()
.putString(FXA_STATE_KEY, account.toJSONString())
......
......@@ -13,27 +13,17 @@ import kotlinx.coroutines.plus
import mozilla.appservices.fxaclient.FirefoxAccount as InternalFxAcct