summaryrefslogtreecommitdiffstats
path: root/mobile/android/android-components/components/concept/sync
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:35:49 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:35:49 +0000
commitd8bbc7858622b6d9c278469aab701ca0b609cddf (patch)
treeeff41dc61d9f714852212739e6b3738b82a2af87 /mobile/android/android-components/components/concept/sync
parentReleasing progress-linux version 125.0.3-1~progress7.99u1. (diff)
downloadfirefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.tar.xz
firefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.zip
Merging upstream version 126.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mobile/android/android-components/components/concept/sync')
-rw-r--r--mobile/android/android-components/components/concept/sync/README.md26
-rw-r--r--mobile/android/android-components/components/concept/sync/build.gradle37
-rw-r--r--mobile/android/android-components/components/concept/sync/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/concept/sync/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/AccountEvent.kt65
-rw-r--r--mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/Devices.kt175
-rw-r--r--mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/OAuthAccount.kt358
-rw-r--r--mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/Sync.kt53
8 files changed, 739 insertions, 0 deletions
diff --git a/mobile/android/android-components/components/concept/sync/README.md b/mobile/android/android-components/components/concept/sync/README.md
new file mode 100644
index 0000000000..787a6a3af2
--- /dev/null
+++ b/mobile/android/android-components/components/concept/sync/README.md
@@ -0,0 +1,26 @@
+# [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/
diff --git a/mobile/android/android-components/components/concept/sync/build.gradle b/mobile/android/android-components/components/concept/sync/build.gradle
new file mode 100644
index 0000000000..31a356155a
--- /dev/null
+++ b/mobile/android/android-components/components/concept/sync/build.gradle
@@ -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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.concept.sync'
+}
+
+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 ComponentsDependencies.kotlin_coroutines
+
+ // Observables are part of the public API of this module.
+ api project(':support-base')
+}
+
+apply from: '../../../android-lint.gradle'
+apply from: '../../../publish.gradle'
+ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
diff --git a/mobile/android/android-components/components/concept/sync/proguard-rules.pro b/mobile/android/android-components/components/concept/sync/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/concept/sync/proguard-rules.pro
@@ -0,0 +1,21 @@
+# 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
diff --git a/mobile/android/android-components/components/concept/sync/src/main/AndroidManifest.xml b/mobile/android/android-components/components/concept/sync/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/concept/sync/src/main/AndroidManifest.xml
@@ -0,0 +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/. -->
+<manifest />
diff --git a/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/AccountEvent.kt b/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/AccountEvent.kt
new file mode 100644
index 0000000000..fe46cc5b90
--- /dev/null
+++ b/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/AccountEvent.kt
@@ -0,0 +1,65 @@
+/* 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
+
+/**
+ * Allows monitoring events targeted at the current account/device.
+ */
+interface AccountEventsObserver {
+ /** The callback called when an account event is received */
+ fun onEvents(events: List<AccountEvent>)
+}
+
+typealias OuterDeviceCommandIncoming = DeviceCommandIncoming
+
+/**
+ * Incoming account events.
+ */
+sealed class AccountEvent {
+ /** An incoming command from another device */
+ data class DeviceCommandIncoming(val command: OuterDeviceCommandIncoming) : AccountEvent()
+
+ /** The account's profile was updated */
+ object ProfileUpdated : AccountEvent()
+
+ /** The authentication state of the account changed - eg, the password changed */
+ object AccountAuthStateChanged : AccountEvent()
+
+ /** The account itself was destroyed */
+ object AccountDestroyed : AccountEvent()
+
+ /** Another device connected to the account */
+ data class DeviceConnected(val deviceName: String) : AccountEvent()
+
+ /** A device (possibly this one) disconnected from the account */
+ data class DeviceDisconnected(val deviceId: String, val isLocalDevice: Boolean) : AccountEvent()
+
+ /** An unknown account event. Should be gracefully ignore */
+ object Unknown : AccountEvent()
+}
+
+/**
+ * Incoming device commands (ie, targeted at the current device.)
+ */
+sealed class DeviceCommandIncoming {
+ /** A command to open a list of tabs on the current device */
+ class TabReceived(val from: Device?, val entries: List<TabData>) : DeviceCommandIncoming()
+}
+
+/**
+ * Outgoing device commands (ie, targeted at other devices.)
+ */
+sealed class DeviceCommandOutgoing {
+ /** A command to open a tab on another device */
+ class SendTab(val title: String, val url: String) : DeviceCommandOutgoing()
+}
+
+/**
+ * Information about a tab sent with tab related commands.
+ */
+data class TabData(
+ val title: String,
+ val url: String,
+)
diff --git a/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/Devices.kt b/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/Devices.kt
new file mode 100644
index 0000000000..94b022ce20
--- /dev/null
+++ b/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/Devices.kt
@@ -0,0 +1,175 @@
+/* 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 android.content.Context
+import androidx.annotation.MainThread
+import androidx.lifecycle.LifecycleOwner
+import mozilla.components.support.base.observer.Observable
+
+/**
+ * Represents a result of interacting with a backend service which may return an authentication error.
+ */
+sealed class ServiceResult {
+ /**
+ * All good.
+ */
+ object Ok : ServiceResult()
+
+ /**
+ * Auth error.
+ */
+ object AuthError : ServiceResult()
+
+ /**
+ * Error that isn't auth.
+ */
+ object OtherError : ServiceResult()
+}
+
+/**
+ * Describes available interactions with the current device and other devices associated with an [OAuthAccount].
+ */
+interface DeviceConstellation : Observable<AccountEventsObserver> {
+ /**
+ * Perform actions necessary to finalize device initialization based on [authType].
+ * @param authType Type of an authentication event we're experiencing.
+ * @param config A [DeviceConfig] that describes current device.
+ * @return A boolean success flag.
+ */
+ suspend fun finalizeDevice(authType: AuthType, config: DeviceConfig): ServiceResult
+
+ /**
+ * Current state of the constellation. May be missing if state was never queried.
+ * @return [ConstellationState] describes current and other known devices in the constellation.
+ */
+ fun state(): ConstellationState?
+
+ /**
+ * Allows monitoring state of the device constellation via [DeviceConstellationObserver].
+ * Use this to be notified of changes to the current device or other devices.
+ */
+ @MainThread
+ fun registerDeviceObserver(observer: DeviceConstellationObserver, owner: LifecycleOwner, autoPause: Boolean)
+
+ /**
+ * Set name of the current device.
+ * @param name New device name.
+ * @param context An application context, used for updating internal caches.
+ * @return A boolean success flag.
+ */
+ suspend fun setDeviceName(name: String, context: Context): Boolean
+
+ /**
+ * Set a [DevicePushSubscription] for the current device.
+ * @param subscription A new [DevicePushSubscription].
+ * @return A boolean success flag.
+ */
+ suspend fun setDevicePushSubscription(subscription: DevicePushSubscription): Boolean
+
+ /**
+ * Send a command to a specified device.
+ * @param targetDeviceId A device ID of the recipient.
+ * @param outgoingCommand An event to send.
+ * @return A boolean success flag.
+ */
+ suspend fun sendCommandToDevice(targetDeviceId: String, outgoingCommand: DeviceCommandOutgoing): Boolean
+
+ /**
+ * Process a raw event, obtained via a push message or some other out-of-band mechanism.
+ * @param payload A raw, plaintext payload to be processed.
+ * @return A boolean success flag.
+ */
+ suspend fun processRawEvent(payload: String): Boolean
+
+ /**
+ * Refreshes [ConstellationState]. Registered [DeviceConstellationObserver] observers will be notified.
+ *
+ * @return A boolean success flag.
+ */
+ suspend fun refreshDevices(): Boolean
+
+ /**
+ * Polls for any pending [DeviceCommandIncoming] commands.
+ * In case of new commands, registered [AccountEventsObserver] observers will be notified.
+ *
+ * @return A boolean success flag.
+ */
+ suspend fun pollForCommands(): Boolean
+}
+
+/**
+ * Describes current device and other devices in the constellation.
+ */
+// N.B.: currentDevice should not be nullable.
+// See https://github.com/mozilla-mobile/android-components/issues/8768
+data class ConstellationState(val currentDevice: Device?, val otherDevices: List<Device>)
+
+/**
+ * Allows monitoring constellation state.
+ */
+interface DeviceConstellationObserver {
+ fun onDevicesUpdate(constellation: ConstellationState)
+}
+
+/**
+ * Describes a type of the physical device in the constellation.
+ */
+enum class DeviceType {
+ DESKTOP,
+ MOBILE,
+ TABLET,
+ TV,
+ VR,
+ UNKNOWN,
+}
+
+/**
+ * Describes an Autopush-compatible push channel subscription.
+ */
+data class DevicePushSubscription(
+ val endpoint: String,
+ val publicKey: String,
+ val authKey: String,
+)
+
+/**
+ * Configuration for the current device.
+ *
+ * @property name An initial name to use for the device record which will be created during authentication.
+ * This can be changed later via [DeviceConstellation.setDeviceName].
+ * @property type Type of a device - mobile, desktop - used for displaying identifying icons on other devices.
+ * This cannot be changed once device record is created.
+ * @property capabilities A set of device capabilities, such as SEND_TAB.
+ * @property secureStateAtRest A flag indicating whether or not to use encrypted storage for the persisted account
+ * state.
+ */
+data class DeviceConfig(
+ val name: String,
+ val type: DeviceType,
+ val capabilities: Set<DeviceCapability>,
+ val secureStateAtRest: Boolean = false,
+)
+
+/**
+ * Capabilities that a [Device] may have.
+ */
+enum class DeviceCapability {
+ SEND_TAB,
+}
+
+/**
+ * Describes a device in the [DeviceConstellation].
+ */
+data class Device(
+ val id: String,
+ val displayName: String,
+ val deviceType: DeviceType,
+ val isCurrentDevice: Boolean,
+ val lastAccessTime: Long?,
+ val capabilities: List<DeviceCapability>,
+ val subscriptionExpired: Boolean,
+ val subscription: DevicePushSubscription?,
+)
diff --git a/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/OAuthAccount.kt b/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/OAuthAccount.kt
new file mode 100644
index 0000000000..7737d4bc36
--- /dev/null
+++ b/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/OAuthAccount.kt
@@ -0,0 +1,358 @@
+/* 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 kotlinx.coroutines.Deferred
+
+/**
+ * An object that represents a login flow initiated by [OAuthAccount].
+ * @property state OAuth state parameter, identifying a specific authentication flow.
+ * This string is randomly generated during [OAuthAccount.beginOAuthFlow] and [OAuthAccount.beginPairingFlow].
+ * @property url Url which needs to be loaded to go through the authentication flow identified by [state].
+ */
+data class AuthFlowUrl(val state: String, val url: String)
+
+/**
+ * Represents a specific type of an "in-flight" migration state that could result from intermittent
+ * issues during [OAuthAccount.migrateFromAccount].
+ */
+enum class InFlightMigrationState(val reuseSessionToken: Boolean) {
+ /**
+ * "Copy" in-flight migration present. Can retry migration via [OAuthAccount.retryMigrateFromSessionToken].
+ */
+ COPY_SESSION_TOKEN(false),
+
+ /**
+ * "Reuse" in-flight migration present. Can retry migration via [OAuthAccount.retryMigrateFromSessionToken].
+ */
+ REUSE_SESSION_TOKEN(true),
+}
+
+/**
+ * Data structure describing FxA and Sync credentials necessary to sign-in into an FxA account.
+ */
+data class MigratingAccountInfo(
+ val sessionToken: String,
+ val kSync: String,
+ val kXCS: String,
+)
+
+/**
+ * Representing all the possible entry points into FxA
+ *
+ * These entry points will be reflected in the authentication URL and will be tracked
+ * in server telemetry to allow studying authentication entry points independently.
+ *
+ * If you are introducing a new path to the firefox accounts sign in please add a new entry point
+ * here.
+ */
+interface FxAEntryPoint {
+ val entryName: String
+}
+
+/**
+ * Facilitates testing consumers of FirefoxAccount.
+ */
+interface OAuthAccount : AutoCloseable {
+
+ /**
+ * Constructs a URL used to begin the OAuth flow for the requested scopes and keys.
+ *
+ * @param scopes List of OAuth scopes for which the client wants access
+ * @param entryPoint The UI entryPoint used to start this flow. An arbitrary
+ * string which is recorded in telemetry by the server to help analyze the
+ * most effective touchpoints
+ * @return [AuthFlowUrl] if available, `null` in case of a failure
+ */
+ suspend fun beginOAuthFlow(
+ scopes: Set<String>,
+ entryPoint: FxAEntryPoint,
+ ): AuthFlowUrl?
+
+ /**
+ * Constructs a URL used to begin the pairing flow for the requested scopes and pairingUrl.
+ *
+ * @param pairingUrl URL string for pairing
+ * @param scopes List of OAuth scopes for which the client wants access
+ * @param entryPoint The UI entryPoint used to start this flow. An arbitrary
+ * string which is recorded in telemetry by the server to help analyze the
+ * most effective touchpoints
+ * @return [AuthFlowUrl] if available, `null` in case of a failure
+ */
+ suspend fun beginPairingFlow(
+ pairingUrl: String,
+ scopes: Set<String>,
+ entryPoint: FxAEntryPoint,
+ ): AuthFlowUrl?
+
+ /**
+ * Returns current FxA Device ID for an authenticated account.
+ *
+ * @return Current device's FxA ID, if available. `null` otherwise.
+ */
+ fun getCurrentDeviceId(): String?
+
+ /**
+ * Returns session token for an authenticated account.
+ *
+ * @return Current account's session token, if available. `null` otherwise.
+ */
+ fun getSessionToken(): String?
+
+ /**
+ * Fetches the profile object for the current client either from the existing cached state
+ * or from the server (requires the client to have access to the profile scope).
+ *
+ * @param ignoreCache Fetch the profile information directly from the server
+ * @return Profile (optional, if successfully retrieved) representing the user's basic profile info
+ */
+ suspend fun getProfile(ignoreCache: Boolean = false): Profile?
+
+ /**
+ * Authenticates the current account using the [code] and [state] parameters obtained via the
+ * OAuth flow initiated by [beginOAuthFlow].
+ *
+ * Modifies the FirefoxAccount state.
+ * @param code OAuth code string
+ * @param state state token string
+ * @return Deferred boolean representing success or failure
+ */
+ suspend fun completeOAuthFlow(code: String, state: String): Boolean
+
+ /**
+ * Tries to fetch an access token for the given scope.
+ *
+ * @param singleScope Single OAuth scope (no spaces) for which the client wants access
+ * @return [AccessTokenInfo] that stores the token, along with its scope, key and
+ * expiration timestamp (in seconds) since epoch when complete
+ */
+ suspend fun getAccessToken(singleScope: String): AccessTokenInfo?
+
+ /**
+ * Call this whenever an authentication error was encountered while using an access token
+ * issued by [getAccessToken].
+ */
+ fun authErrorDetected()
+
+ /**
+ * This method should be called when a request made with an OAuth token failed with an
+ * authentication error. It will re-build cached state and perform a connectivity check.
+ *
+ * In time, fxalib will grow a similar method, at which point we'll just relay to it.
+ * See https://github.com/mozilla/application-services/issues/1263
+ *
+ * @param singleScope An oauth scope for which to check authorization state.
+ * @return An optional [Boolean] flag indicating if we're connected, or need to go through
+ * re-authentication. A null result means we were not able to determine state at this time.
+ */
+ suspend fun checkAuthorizationStatus(singleScope: String): Boolean?
+
+ /**
+ * Fetches the token server endpoint, for authentication using the SAML bearer flow.
+ *
+ * @return Token server endpoint URL string, `null` if it couldn't be obtained.
+ */
+ suspend fun getTokenServerEndpointURL(): String?
+
+ /**
+ * Fetches the URL for the user to manage their account
+ *
+ * @param entryPoint A string which will be included as a query param in the URL for metrics.
+ * @return The URL which should be opened in a browser tab.
+ */
+ suspend fun getManageAccountURL(entryPoint: FxAEntryPoint): String?
+
+ /**
+ * Get the pairing URL to navigate to on the Authority side (typically a computer).
+ *
+ * @return The URL to show the pairing user
+ */
+ fun getPairingAuthorityURL(): String
+
+ /**
+ * Registers a callback for when the account state gets persisted
+ *
+ * @param callback the account state persistence callback
+ */
+ fun registerPersistenceCallback(callback: StatePersistenceCallback)
+
+ /**
+ * Returns the device constellation for the current account
+ *
+ * @return Device constellation for the current account
+ */
+ fun deviceConstellation(): DeviceConstellation
+
+ /**
+ * Reset internal account state and destroy current device record.
+ * Use this when device record is no longer relevant, e.g. while logging out. On success, other
+ * devices will no longer see the current device in their device lists.
+ *
+ * @return A [Deferred] that will be resolved with a success flag once operation is complete.
+ * Failure indicates that we may have failed to destroy current device record. Nothing to do for
+ * the consumer; device record will be cleaned up eventually via TTL.
+ */
+ suspend fun disconnect(): Boolean
+
+ /**
+ * Serializes the current account's authentication state as a JSON string, for persistence in
+ * the Android KeyStore/shared preferences. The authentication state can be restored using
+ * [FirefoxAccount.fromJSONString].
+ *
+ * @return String containing the authentication details in JSON format
+ */
+ fun toJSONString(): String
+}
+
+/**
+ * Describes a delegate object that is used by [OAuthAccount] to persist its internal state as it changes.
+ */
+interface StatePersistenceCallback {
+ /**
+ * @param data Account state representation as a string (e.g. as json).
+ */
+ fun persist(data: String)
+}
+
+sealed class AuthType {
+ /**
+ * Account restored from hydrated state on disk.
+ */
+ object Existing : AuthType()
+
+ /**
+ * Account created in response to a sign-in.
+ */
+ object Signin : AuthType()
+
+ /**
+ * Account created in response to a sign-up.
+ */
+ object Signup : AuthType()
+
+ /**
+ * Account created via pairing (similar to sign-in, but without requiring credentials).
+ */
+ object Pairing : AuthType()
+
+ /**
+ * Account was created for an unknown external reason, hopefully identified by [action].
+ */
+ data class OtherExternal(val action: String?) : AuthType()
+
+ /**
+ * Account created via a shared account state from another app via the copy token flow.
+ */
+ object MigratedCopy : AuthType()
+
+ /**
+ * Account created via a shared account state from another app via the reuse token flow.
+ */
+ object MigratedReuse : AuthType()
+
+ /**
+ * Existing account was recovered from an authentication problem.
+ */
+ object Recovered : AuthType()
+}
+
+/**
+ * Different types of errors that may be encountered during authorization.
+ * Intermittent network problems are the most common reason for these errors.
+ */
+enum class AuthFlowError {
+ /**
+ * Couldn't begin authorization, i.e. failed to obtain an authorization URL.
+ */
+ FailedToBeginAuth,
+
+ /**
+ * Couldn't complete authorization after user entered valid credentials/paired correctly.
+ */
+ FailedToCompleteAuth,
+}
+
+/**
+ * Observer interface which lets its users monitor account state changes and major events.
+ * (XXX - there's some tension between this and the
+ * mozilla.components.concept.sync.AccountEvent we should resolve!)
+ */
+interface AccountObserver {
+ /**
+ * Account state has been resolved and can now be queried.
+ *
+ * @param authenticatedAccount Currently resolved authenticated account, if any.
+ */
+ fun onReady(authenticatedAccount: OAuthAccount?) = Unit
+
+ /**
+ * Account just got logged out.
+ */
+ fun onLoggedOut() = Unit
+
+ /**
+ * Account was successfully authenticated.
+ *
+ * @param account An authenticated instance of a [OAuthAccount].
+ * @param authType Describes what kind of authentication event caused this invocation.
+ */
+ fun onAuthenticated(account: OAuthAccount, authType: AuthType) = Unit
+
+ /**
+ * Account's profile is now available.
+ * @param profile A fresh version of account's [Profile].
+ */
+ fun onProfileUpdated(profile: Profile) = Unit
+
+ /**
+ * Account needs to be re-authenticated (e.g. due to a password change).
+ */
+ fun onAuthenticationProblems() = Unit
+
+ /**
+ * Encountered an error during an authentication or migration flow.
+ * @param error Exact error encountered.
+ */
+ fun onFlowError(error: AuthFlowError) = Unit
+}
+
+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,
+)
diff --git a/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/Sync.kt b/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/Sync.kt
new file mode 100644
index 0000000000..51b24d0752
--- /dev/null
+++ b/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/Sync.kt
@@ -0,0 +1,53 @@
+/* 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
+
+/**
+ * Results of running a sync via [SyncableStore.sync].
+ */
+sealed class SyncStatus {
+ /**
+ * Sync succeeded successfully.
+ */
+ object Ok : SyncStatus()
+
+ /**
+ * Sync completed with an error.
+ */
+ data class Error(val exception: Exception) : SyncStatus()
+}
+
+/**
+ * A Firefox Sync friendly auth object which can be obtained from [OAuthAccount].
+ *
+ * Why is there a Firefox Sync-shaped authentication object at the concept level, you ask?
+ * Mainly because this is what the [SyncableStore] consumes in order to actually perform
+ * synchronization, which is in turn implemented by `places`-backed storage layer.
+ * If this class lived in `services-firefox-accounts`, we'd end up with an ugly dependency situation
+ * between services and storage components.
+ *
+ * Turns out that building a generic description of an authentication/synchronization layer is not
+ * quite the way to go when you only have a single, legacy implementation.
+ *
+ * However, this may actually improve once we retire the tokenserver from the architecture.
+ * We could also consider a heavier use of generics, as well.
+ */
+data class SyncAuthInfo(
+ val kid: String,
+ val fxaAccessToken: String,
+ val fxaAccessTokenExpiresAt: Long,
+ val syncKey: String,
+ val tokenServerUrl: String,
+)
+
+/**
+ * Describes a "sync" entry point for a storage layer.
+ */
+interface SyncableStore {
+ /**
+ * Registers this storage with a sync manager.
+ */
+ fun registerWithSyncManager()
+}