[components] Account manager state machine refactoring
This commit is contained in:
committed by
mergify[bot]
parent
b30bd15687
commit
ee28f42d71
@@ -19,17 +19,17 @@ typealias OuterDeviceCommandIncoming = DeviceCommandIncoming
|
||||
*/
|
||||
sealed class AccountEvent {
|
||||
/** An incoming command from another device */
|
||||
class DeviceCommandIncoming(val command: OuterDeviceCommandIncoming) : AccountEvent()
|
||||
data class DeviceCommandIncoming(val command: OuterDeviceCommandIncoming) : AccountEvent()
|
||||
/** The account's profile was updated */
|
||||
class ProfileUpdated : AccountEvent()
|
||||
object ProfileUpdated : AccountEvent()
|
||||
/** The authentication state of the account changed - eg, the password changed */
|
||||
class AccountAuthStateChanged : AccountEvent()
|
||||
object AccountAuthStateChanged : AccountEvent()
|
||||
/** The account itself was destroyed */
|
||||
class AccountDestroyed : AccountEvent()
|
||||
object AccountDestroyed : AccountEvent()
|
||||
/** Another device connected to the account */
|
||||
class DeviceConnected(val deviceName: String) : AccountEvent()
|
||||
data class DeviceConnected(val deviceName: String) : AccountEvent()
|
||||
/** A device (possibly this one) disconnected from the account */
|
||||
class DeviceDisconnected(val deviceId: String, val isLocalDevice: Boolean) : AccountEvent()
|
||||
data class DeviceDisconnected(val deviceId: String, val isLocalDevice: Boolean) : AccountEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,36 +6,39 @@ package mozilla.components.concept.sync
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import kotlinx.coroutines.Deferred
|
||||
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> {
|
||||
/**
|
||||
* Register current device in the associated [DeviceConstellation].
|
||||
*
|
||||
* @param name An initial name for the current device. This may be changed via [setDeviceNameAsync].
|
||||
* @param type Type of the current device. This can't be changed.
|
||||
* @param capabilities A list of capabilities that the current device claims to have.
|
||||
* @return A [Deferred] that will be resolved with a success flag once operation is complete.
|
||||
* 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.
|
||||
*/
|
||||
fun initDeviceAsync(
|
||||
name: String,
|
||||
type: DeviceType = DeviceType.MOBILE,
|
||||
capabilities: Set<DeviceCapability>
|
||||
): Deferred<Boolean>
|
||||
|
||||
/**
|
||||
* Ensure that all passed in [capabilities] are configured.
|
||||
* This may involve backend service registration, or other work involving network/disc access.
|
||||
* @param capabilities A list of capabilities to configure. This is expected to be the same or
|
||||
* longer list than what was passed into [initDeviceAsync]. Removing capabilities is currently
|
||||
* not supported.
|
||||
* @return A [Deferred] that will be resolved with a success flag once operation is complete.
|
||||
*/
|
||||
fun ensureCapabilitiesAsync(capabilities: Set<DeviceCapability>): Deferred<Boolean>
|
||||
suspend fun finalizeDevice(authType: AuthType, config: DeviceConfig): ServiceResult
|
||||
|
||||
/**
|
||||
* Current state of the constellation. May be missing if state was never queried.
|
||||
@@ -53,46 +56,46 @@ interface DeviceConstellation : Observable<AccountEventsObserver> {
|
||||
* Set name of the current device.
|
||||
* @param name New device name.
|
||||
* @param context An application context, used for updating internal caches.
|
||||
* @return A [Deferred] that will be resolved with a success flag once operation is complete.
|
||||
* @return A boolean success flag.
|
||||
*/
|
||||
fun setDeviceNameAsync(name: String, context: Context): Deferred<Boolean>
|
||||
suspend fun setDeviceName(name: String, context: Context): Boolean
|
||||
|
||||
/**
|
||||
* Set a [DevicePushSubscription] for the current device.
|
||||
* @param subscription A new [DevicePushSubscription].
|
||||
* @return A [Deferred] that will be resolved with a success flag once operation is complete.
|
||||
* @return A boolean success flag.
|
||||
*/
|
||||
fun setDevicePushSubscriptionAsync(subscription: DevicePushSubscription): Deferred<Boolean>
|
||||
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 [Deferred] that will be resolved with a success flag once operation is complete.
|
||||
* @return A boolean success flag.
|
||||
*/
|
||||
fun sendCommandToDeviceAsync(targetDeviceId: String, outgoingCommand: DeviceCommandOutgoing): Deferred<Boolean>
|
||||
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 [Deferred] that will be resolved with a success flag once operation is complete.
|
||||
* @return A boolean success flag.
|
||||
*/
|
||||
fun processRawEventAsync(payload: String): Deferred<Boolean>
|
||||
suspend fun processRawEvent(payload: String): Boolean
|
||||
|
||||
/**
|
||||
* Refreshes [ConstellationState]. Registered [DeviceConstellationObserver] observers will be notified.
|
||||
*
|
||||
* @return A [Deferred] that will be resolved with a success flag once operation is complete.
|
||||
* @return A boolean success flag.
|
||||
*/
|
||||
fun refreshDevicesAsync(): Deferred<Boolean>
|
||||
suspend fun refreshDevices(): Boolean
|
||||
|
||||
/**
|
||||
* Polls for any pending [DeviceCommandIncoming] commands.
|
||||
* In case of new commands, registered [AccountEventsObserver] observers will be notified.
|
||||
*
|
||||
* @return A [Deferred] that will be resolved with a success flag once operation is complete.
|
||||
* @return A boolean success flag.
|
||||
*/
|
||||
fun pollForCommandsAsync(): Deferred<Boolean>
|
||||
suspend fun pollForCommands(): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,6 +131,24 @@ data class DevicePushSubscription(
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -7,17 +7,6 @@ package mozilla.components.concept.sync
|
||||
import kotlinx.coroutines.Deferred
|
||||
import org.json.JSONObject
|
||||
|
||||
/**
|
||||
* An auth-related exception type, for use with [AuthException].
|
||||
*
|
||||
* @property msg string value of the auth exception type
|
||||
*/
|
||||
enum class AuthExceptionType(val msg: String) {
|
||||
KEY_INFO("Missing key info"),
|
||||
NO_TOKEN("Missing access token"),
|
||||
UNAUTHORIZED("Unauthorized")
|
||||
}
|
||||
|
||||
/**
|
||||
* The access-type determines whether the code can be exchanged for a refresh token for
|
||||
* offline use or not.
|
||||
@@ -29,40 +18,39 @@ enum class AccessType(val msg: String) {
|
||||
OFFLINE("offline")
|
||||
}
|
||||
|
||||
/**
|
||||
* An exception which may happen while obtaining auth information using [OAuthAccount].
|
||||
*/
|
||||
class AuthException(type: AuthExceptionType, cause: Exception? = null) : Throwable(type.msg, cause)
|
||||
|
||||
/**
|
||||
* 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.beginOAuthFlowAsync] and [OAuthAccount.beginPairingFlowAsync].
|
||||
* 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.migrateFromSessionTokenAsync] or [OAuthAccount.copyFromSessionTokenAsync].
|
||||
* issues during [OAuthAccount.migrateFromAccount].
|
||||
*/
|
||||
enum class InFlightMigrationState {
|
||||
enum class InFlightMigrationState(val reuseSessionToken: Boolean) {
|
||||
/**
|
||||
* No in-flight migration.
|
||||
* "Copy" in-flight migration present. Can retry migration via [OAuthAccount.retryMigrateFromSessionToken].
|
||||
*/
|
||||
NONE,
|
||||
COPY_SESSION_TOKEN(false),
|
||||
|
||||
/**
|
||||
* "Copy" in-flight migration present. Can retry migration via [OAuthAccount.retryMigrateFromSessionTokenAsync].
|
||||
* "Reuse" in-flight migration present. Can retry migration via [OAuthAccount.retryMigrateFromSessionToken].
|
||||
*/
|
||||
COPY_SESSION_TOKEN,
|
||||
|
||||
/**
|
||||
* "Reuse" in-flight migration present. Can retry migration via [OAuthAccount.retryMigrateFromSessionTokenAsync].
|
||||
*/
|
||||
REUSE_SESSION_TOKEN
|
||||
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
|
||||
)
|
||||
|
||||
/**
|
||||
* Facilitates testing consumers of FirefoxAccount.
|
||||
*/
|
||||
@@ -76,12 +64,12 @@ interface OAuthAccount : AutoCloseable {
|
||||
* @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 Deferred AuthFlowUrl that resolves to the flow URL when complete
|
||||
* @return [AuthFlowUrl] if available, `null` in case of a failure
|
||||
*/
|
||||
fun beginOAuthFlowAsync(
|
||||
suspend fun beginOAuthFlow(
|
||||
scopes: Set<String>,
|
||||
entryPoint: String = "android-components"
|
||||
): Deferred<AuthFlowUrl?>
|
||||
): AuthFlowUrl?
|
||||
|
||||
/**
|
||||
* Constructs a URL used to begin the pairing flow for the requested scopes and pairingUrl.
|
||||
@@ -91,13 +79,13 @@ interface OAuthAccount : AutoCloseable {
|
||||
* @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 Deferred AuthFlowUrl Optional that resolves to the flow URL when complete
|
||||
* @return [AuthFlowUrl] if available, `null` in case of a failure
|
||||
*/
|
||||
fun beginPairingFlowAsync(
|
||||
suspend fun beginPairingFlow(
|
||||
pairingUrl: String,
|
||||
scopes: Set<String>,
|
||||
entryPoint: String = "android-components"
|
||||
): Deferred<AuthFlowUrl?>
|
||||
): AuthFlowUrl?
|
||||
|
||||
/**
|
||||
* Returns current FxA Device ID for an authenticated account.
|
||||
@@ -123,12 +111,12 @@ interface OAuthAccount : AutoCloseable {
|
||||
* the code can be exchanged for a refresh token to be used offline or not
|
||||
* @return Deferred authorized auth code string, or `null` in case of failure.
|
||||
*/
|
||||
fun authorizeOAuthCodeAsync(
|
||||
suspend fun authorizeOAuthCode(
|
||||
clientId: String,
|
||||
scopes: Array<String>,
|
||||
state: String,
|
||||
accessType: AccessType = AccessType.ONLINE
|
||||
): Deferred<String?>
|
||||
): String?
|
||||
|
||||
/**
|
||||
* Fetches the profile object for the current client either from the existing cached state
|
||||
@@ -137,18 +125,18 @@ interface OAuthAccount : AutoCloseable {
|
||||
* @param ignoreCache Fetch the profile information directly from the server
|
||||
* @return Profile (optional, if successfully retrieved) representing the user's basic profile info
|
||||
*/
|
||||
fun getProfileAsync(ignoreCache: Boolean = false): Deferred<Profile?>
|
||||
suspend fun getProfile(ignoreCache: Boolean = false): Profile?
|
||||
|
||||
/**
|
||||
* Authenticates the current account using the [code] and [state] parameters obtained via the
|
||||
* OAuth flow initiated by [beginOAuthFlowAsync].
|
||||
* 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
|
||||
*/
|
||||
fun completeOAuthFlowAsync(code: String, state: String): Deferred<Boolean>
|
||||
suspend fun completeOAuthFlow(code: String, state: String): Boolean
|
||||
|
||||
/**
|
||||
* Tries to fetch an access token for the given scope.
|
||||
@@ -157,11 +145,11 @@ interface OAuthAccount : AutoCloseable {
|
||||
* @return [AccessTokenInfo] that stores the token, along with its scope, key and
|
||||
* expiration timestamp (in seconds) since epoch when complete
|
||||
*/
|
||||
fun getAccessTokenAsync(singleScope: String): Deferred<AccessTokenInfo?>
|
||||
suspend fun getAccessToken(singleScope: String): AccessTokenInfo?
|
||||
|
||||
/**
|
||||
* Call this whenever an authentication error was encountered while using an access token
|
||||
* issued by [getAccessTokenAsync].
|
||||
* issued by [getAccessToken].
|
||||
*/
|
||||
fun authErrorDetected()
|
||||
|
||||
@@ -176,7 +164,7 @@ interface OAuthAccount : AutoCloseable {
|
||||
* @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.
|
||||
*/
|
||||
fun checkAuthorizationStatusAsync(singleScope: String): Deferred<Boolean?>
|
||||
suspend fun checkAuthorizationStatus(singleScope: String): Boolean?
|
||||
|
||||
/**
|
||||
* Fetches the token server endpoint, for authentication using the SAML bearer flow.
|
||||
@@ -203,36 +191,23 @@ interface OAuthAccount : AutoCloseable {
|
||||
* Attempts to migrate from an existing session token without user input.
|
||||
* Passed-in session token will be reused.
|
||||
*
|
||||
* @param sessionToken token string to use for login
|
||||
* @param kSync sync string for login
|
||||
* @param kXCS XCS string for login
|
||||
* @param authInfo Auth info necessary for signing in
|
||||
* @param reuseSessionToken Whether or not session token should be reused; reusing session token
|
||||
* means that FxA device record will be inherited
|
||||
* @return JSON object with the result of the migration or 'null' if it failed.
|
||||
* For up-to-date schema, see underlying implementation in https://github.com/mozilla/application-services/blob/v0.49.0/components/fxa-client/src/migrator.rs#L10
|
||||
* For up-to-date schema, see underlying implementation in
|
||||
* https://github.com/mozilla/application-services/blob/v0.49.0/components/fxa-client/src/migrator.rs#L10
|
||||
* At the moment, it's just "{total_duration: long}".
|
||||
*/
|
||||
fun migrateFromSessionTokenAsync(sessionToken: String, kSync: String, kXCS: String): Deferred<JSONObject?>
|
||||
|
||||
/**
|
||||
* Attempts to migrate from an existing session token without user input.
|
||||
* New session token will be created.
|
||||
*
|
||||
* @param sessionToken token string to use for login
|
||||
* @param kSync sync string for login
|
||||
* @param kXCS XCS string for login
|
||||
* @return JSON object with the result of the migration or 'null' if it failed.
|
||||
* For up-to-date schema, see underlying implementation in https://github.com/mozilla/application-services/blob/v0.49.0/components/fxa-client/src/migrator.rs#L10
|
||||
* At the moment, it's just "{total_duration: long}".
|
||||
*/
|
||||
fun copyFromSessionTokenAsync(sessionToken: String, kSync: String, kXCS: String): Deferred<JSONObject?>
|
||||
suspend fun migrateFromAccount(authInfo: MigratingAccountInfo, reuseSessionToken: Boolean): JSONObject?
|
||||
|
||||
/**
|
||||
* Checks if there's a migration in-flight. An in-flight migration means that we've tried to migrate
|
||||
* via either [migrateFromSessionTokenAsync] or [copyFromSessionTokenAsync], and failed for intermittent
|
||||
* (e.g. network)
|
||||
* reasons. When an in-flight migration is present, we can retry using [retryMigrateFromSessionTokenAsync].
|
||||
* @return InFlightMigrationState indicating specific migration state.
|
||||
* via [migrateFromAccount], and failed for intermittent (e.g. network) reasons. When an in-flight
|
||||
* migration is present, we can retry using [retryMigrateFromSessionToken].
|
||||
* @return InFlightMigrationState indicating specific migration state. [null] if not in a migration state.
|
||||
*/
|
||||
fun isInMigrationState(): InFlightMigrationState
|
||||
fun isInMigrationState(): InFlightMigrationState?
|
||||
|
||||
/**
|
||||
* Retries an in-flight migration attempt.
|
||||
@@ -240,7 +215,7 @@ interface OAuthAccount : AutoCloseable {
|
||||
* For up-to-date schema, see underlying implementation in https://github.com/mozilla/application-services/blob/v0.49.0/components/fxa-client/src/migrator.rs#L10
|
||||
* At the moment, it's just "{total_duration: long}".
|
||||
*/
|
||||
fun retryMigrateFromSessionTokenAsync(): Deferred<JSONObject?>
|
||||
suspend fun retryMigrateFromSessionToken(): JSONObject?
|
||||
|
||||
/**
|
||||
* Returns the device constellation for the current account
|
||||
@@ -258,7 +233,7 @@ interface OAuthAccount : AutoCloseable {
|
||||
* 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.
|
||||
*/
|
||||
fun disconnectAsync(): Deferred<Boolean>
|
||||
suspend fun disconnect(): Boolean
|
||||
|
||||
/**
|
||||
* Serializes the current account's authentication state as a JSON string, for persistence in
|
||||
@@ -307,9 +282,14 @@ sealed class AuthType {
|
||||
data class OtherExternal(val action: String?) : AuthType()
|
||||
|
||||
/**
|
||||
* Account created via a shared account state from another app.
|
||||
* Account created via a shared account state from another app via the copy token flow.
|
||||
*/
|
||||
object Shared : AuthType()
|
||||
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.
|
||||
@@ -317,6 +297,27 @@ sealed class AuthType {
|
||||
object Recovered : AuthType()
|
||||
}
|
||||
|
||||
/**
|
||||
* Different types of errors that may be encountered during authorization and migration flows.
|
||||
* 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,
|
||||
|
||||
/**
|
||||
* Unrecoverable error during account migration.
|
||||
*/
|
||||
FailedToMigrate
|
||||
}
|
||||
|
||||
/**
|
||||
* Observer interface which lets its users monitor account state changes and major events.
|
||||
* (XXX - there's some tension between this and the
|
||||
@@ -346,6 +347,12 @@ interface AccountObserver {
|
||||
* 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(
|
||||
|
||||
@@ -8,6 +8,9 @@ import android.content.Context
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.components.concept.push.PushProcessor
|
||||
import mozilla.components.concept.sync.AuthType
|
||||
import mozilla.components.concept.sync.ConstellationState
|
||||
@@ -122,7 +125,9 @@ internal class AccountObserver(
|
||||
logger.debug("Subscribing for FxaPushScope ($fxaPushScope) events.")
|
||||
|
||||
push.subscribe(fxaPushScope) { subscription ->
|
||||
account.deviceConstellation().setDevicePushSubscriptionAsync(subscription.into())
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
account.deviceConstellation().setDevicePushSubscription(subscription.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +196,9 @@ internal class AutoPushObserver(
|
||||
val rawEvent = message ?: return
|
||||
|
||||
accountManager.withConstellation {
|
||||
processRawEventAsync(String(rawEvent))
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
processRawEvent(String(rawEvent))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,7 +217,9 @@ internal class AutoPushObserver(
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
account.deviceConstellation().setDevicePushSubscriptionAsync(subscription.into())
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
account.deviceConstellation().setDevicePushSubscription(subscription.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -325,7 +334,9 @@ class OneTimeFxaPushReset(
|
||||
|
||||
pushFeature.unsubscribe(pushScope)
|
||||
pushFeature.subscribe(newPushScope) { subscription ->
|
||||
account.deviceConstellation().setDevicePushSubscriptionAsync(subscription.into())
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
account.deviceConstellation().setDevicePushSubscription(subscription.into())
|
||||
}
|
||||
}
|
||||
|
||||
preference(context).edit().putString(PREF_FXA_SCOPE, newPushScope).apply()
|
||||
|
||||
@@ -54,7 +54,7 @@ internal class EventsObserver(
|
||||
override fun onEvents(events: List<AccountEvent>) {
|
||||
events.asSequence()
|
||||
.filterIsInstance<AccountEvent.DeviceCommandIncoming>()
|
||||
.map({ it.command })
|
||||
.map { it.command }
|
||||
.filterIsInstance<DeviceCommandIncoming.TabReceived>()
|
||||
.forEach { command ->
|
||||
logger.debug("Showing ${command.entries.size} tab(s) received from deviceID=${command.from?.id}")
|
||||
|
||||
@@ -75,10 +75,10 @@ class SendTabUseCases(
|
||||
it.id == deviceId
|
||||
}
|
||||
device?.let {
|
||||
return constellation.sendCommandToDeviceAsync(
|
||||
return constellation.sendCommandToDevice(
|
||||
device.id,
|
||||
SendTab(tab.title, tab.url)
|
||||
).await()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,10 +131,10 @@ class SendTabUseCases(
|
||||
// Get a list of device-tab combinations that we want to send.
|
||||
return block(devices).map { (device, tab) ->
|
||||
// Send the tab!
|
||||
constellation.sendCommandToDeviceAsync(
|
||||
constellation.sendCommandToDevice(
|
||||
device.id,
|
||||
SendTab(tab.title, tab.url)
|
||||
).await()
|
||||
)
|
||||
}.fold(true) { acc, result ->
|
||||
// Collect the results and reduce them into one final result.
|
||||
acc and result
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
package mozilla.components.feature.accounts.push
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import mozilla.components.concept.sync.AuthType
|
||||
import mozilla.components.concept.sync.DeviceConstellation
|
||||
import mozilla.components.concept.sync.OAuthAccount
|
||||
@@ -35,7 +35,6 @@ import org.robolectric.RobolectricTestRunner
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class AccountObserverTest {
|
||||
|
||||
private val context: Context = mock()
|
||||
private val accountManager: FxaAccountManager = mock()
|
||||
private val pushFeature: AutoPushFeature = mock()
|
||||
private val pushScope: String = "testScope"
|
||||
@@ -151,7 +150,7 @@ class AccountObserverTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `notify account of new subscriptions`() {
|
||||
fun `notify account of new subscriptions`() = runBlocking {
|
||||
val observer = AccountObserver(
|
||||
testContext,
|
||||
pushFeature,
|
||||
@@ -164,7 +163,8 @@ class AccountObserverTest {
|
||||
|
||||
observer.onAuthenticated(account, AuthType.Signin)
|
||||
|
||||
verify(constellation).setDevicePushSubscriptionAsync(any())
|
||||
verify(constellation).setDevicePushSubscription(any())
|
||||
Unit
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
|
||||
package mozilla.components.feature.accounts.push
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.TestCoroutineDispatcher
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import mozilla.components.concept.sync.DeviceConstellation
|
||||
import mozilla.components.concept.sync.OAuthAccount
|
||||
import mozilla.components.feature.push.AutoPushFeature
|
||||
@@ -25,8 +30,10 @@ class AutoPushObserverTest {
|
||||
private val constellation: DeviceConstellation = mock()
|
||||
private val pushFeature: AutoPushFeature = mock()
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
@Test
|
||||
fun `messages are forwarded to account manager`() {
|
||||
fun `messages are forwarded to account manager`() = runBlocking {
|
||||
Dispatchers.setMain(TestCoroutineDispatcher())
|
||||
val observer = AutoPushObserver(manager, mock(), "test")
|
||||
|
||||
`when`(manager.authenticatedAccount()).thenReturn(account)
|
||||
@@ -34,30 +41,35 @@ class AutoPushObserverTest {
|
||||
|
||||
observer.onMessageReceived("test", "foobar".toByteArray())
|
||||
|
||||
verify(constellation).processRawEventAsync("foobar")
|
||||
verify(constellation).processRawEvent("foobar")
|
||||
Unit
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `account manager is not invoked if no account is available`() {
|
||||
fun `account manager is not invoked if no account is available`() = runBlocking {
|
||||
val observer = AutoPushObserver(manager, mock(), "test")
|
||||
|
||||
observer.onMessageReceived("test", "foobar".toByteArray())
|
||||
|
||||
verify(constellation, never()).setDevicePushSubscriptionAsync(any())
|
||||
verify(constellation, never()).processRawEventAsync("foobar")
|
||||
verify(constellation, never()).setDevicePushSubscription(any())
|
||||
verify(constellation, never()).processRawEvent("foobar")
|
||||
Unit
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `messages are not forwarded to account manager if they are for a different scope`() {
|
||||
fun `messages are not forwarded to account manager if they are for a different scope`() = runBlocking {
|
||||
val observer = AutoPushObserver(manager, mock(), "fake")
|
||||
|
||||
observer.onMessageReceived("test", "foobar".toByteArray())
|
||||
|
||||
verify(constellation, never()).processRawEventAsync(any())
|
||||
verify(constellation, never()).processRawEvent(any())
|
||||
Unit
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
@Test
|
||||
fun `subscription changes are forwarded to account manager`() {
|
||||
fun `subscription changes are forwarded to account manager`() = runBlocking {
|
||||
Dispatchers.setMain(TestCoroutineDispatcher())
|
||||
val observer = AutoPushObserver(manager, pushFeature, "test")
|
||||
|
||||
whenSubscribe()
|
||||
@@ -67,22 +79,24 @@ class AutoPushObserverTest {
|
||||
|
||||
observer.onSubscriptionChanged("test")
|
||||
|
||||
verify(constellation).setDevicePushSubscriptionAsync(any())
|
||||
verify(constellation).setDevicePushSubscription(any())
|
||||
Unit
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `do nothing if there is no account manager`() {
|
||||
fun `do nothing if there is no account manager`() = runBlocking {
|
||||
val observer = AutoPushObserver(manager, pushFeature, "test")
|
||||
|
||||
whenSubscribe()
|
||||
|
||||
observer.onSubscriptionChanged("test")
|
||||
|
||||
verify(constellation, never()).setDevicePushSubscriptionAsync(any())
|
||||
verify(constellation, never()).setDevicePushSubscription(any())
|
||||
Unit
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `subscription changes are not forwarded to account manager if they are for a different scope`() {
|
||||
fun `subscription changes are not forwarded to account manager if they are for a different scope`() = runBlocking {
|
||||
val observer = AutoPushObserver(manager, mock(), "fake")
|
||||
|
||||
`when`(manager.authenticatedAccount()).thenReturn(account)
|
||||
@@ -90,7 +104,7 @@ class AutoPushObserverTest {
|
||||
|
||||
observer.onSubscriptionChanged("test")
|
||||
|
||||
verify(constellation, never()).setDevicePushSubscriptionAsync(any())
|
||||
verify(constellation, never()).setDevicePushSubscription(any())
|
||||
verifyZeroInteractions(pushFeature)
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class EventsObserverTest {
|
||||
val callback: (Device?, List<TabData>) -> Unit = mock()
|
||||
val observer = EventsObserver(callback)
|
||||
val events = listOf(
|
||||
AccountEvent.ProfileUpdated(),
|
||||
AccountEvent.ProfileUpdated,
|
||||
AccountEvent.DeviceCommandIncoming(command = DeviceCommandIncoming.TabReceived(mock(), mock())),
|
||||
AccountEvent.DeviceCommandIncoming(command = DeviceCommandIncoming.TabReceived(mock(), mock()))
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
package mozilla.components.feature.accounts.push
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import mozilla.components.concept.sync.DeviceConstellation
|
||||
import mozilla.components.concept.sync.OAuthAccount
|
||||
import mozilla.components.feature.accounts.push.FxaPushSupportFeature.Companion.PUSH_SCOPE_PREFIX
|
||||
@@ -68,7 +69,7 @@ class OneTimeFxaPushResetTest {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@Test
|
||||
fun `existing invalid scope format is updated`() {
|
||||
fun `existing invalid scope format is updated`() = runBlocking {
|
||||
preference(testContext).edit().putString(PREF_FXA_SCOPE, "12345").apply()
|
||||
|
||||
val validPushScope = PUSH_SCOPE_PREFIX + "12345"
|
||||
@@ -92,7 +93,7 @@ class OneTimeFxaPushResetTest {
|
||||
|
||||
verify(pushFeature).unsubscribe(eq("12345"), any(), any())
|
||||
verify(pushFeature).subscribe(eq(validPushScope), nullable(), any(), any())
|
||||
verify(constellation).setDevicePushSubscriptionAsync(any())
|
||||
verify(constellation).setDevicePushSubscription(any())
|
||||
assertEquals(validPushScope, preference(testContext).getString(PREF_FXA_SCOPE, null))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
package mozilla.components.feature.accounts.push
|
||||
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
@@ -49,12 +48,12 @@ class SendTabUseCasesTest {
|
||||
val device: Device = generateDevice()
|
||||
|
||||
`when`(state.otherDevices).thenReturn(listOf(device))
|
||||
`when`(constellation.sendCommandToDeviceAsync(any(), any()))
|
||||
.thenReturn(CompletableDeferred(true))
|
||||
`when`(constellation.sendCommandToDevice(any(), any()))
|
||||
.thenReturn(true)
|
||||
|
||||
useCases.sendToDeviceAsync(device.id, TabData("Title", "http://example.com"))
|
||||
|
||||
verify(constellation).sendCommandToDeviceAsync(any(), any())
|
||||
verify(constellation).sendCommandToDevice(any(), any())
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -64,12 +63,12 @@ class SendTabUseCasesTest {
|
||||
val tab = TabData("Title", "http://example.com")
|
||||
|
||||
`when`(state.otherDevices).thenReturn(listOf(device))
|
||||
`when`(constellation.sendCommandToDeviceAsync(any(), any()))
|
||||
.thenReturn(CompletableDeferred(true))
|
||||
`when`(constellation.sendCommandToDevice(any(), any()))
|
||||
.thenReturn(true)
|
||||
|
||||
useCases.sendToDeviceAsync(device.id, listOf(tab, tab))
|
||||
|
||||
verify(constellation, times(2)).sendCommandToDeviceAsync(any(), any())
|
||||
verify(constellation, times(2)).sendCommandToDevice(any(), any())
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -80,16 +79,16 @@ class SendTabUseCasesTest {
|
||||
|
||||
useCases.sendToDeviceAsync("123", listOf(tab, tab))
|
||||
|
||||
verify(constellation, never()).sendCommandToDeviceAsync(any(), any())
|
||||
verify(constellation, never()).sendCommandToDevice(any(), any())
|
||||
|
||||
`when`(device.id).thenReturn("123")
|
||||
`when`(state.otherDevices).thenReturn(listOf(device))
|
||||
`when`(constellation.sendCommandToDeviceAsync(any(), any()))
|
||||
.thenReturn(CompletableDeferred(false))
|
||||
`when`(constellation.sendCommandToDevice(any(), any()))
|
||||
.thenReturn(false)
|
||||
|
||||
useCases.sendToDeviceAsync("123", listOf(tab, tab))
|
||||
|
||||
verify(constellation, never()).sendCommandToDeviceAsync(any(), any())
|
||||
verify(constellation, never()).sendCommandToDevice(any(), any())
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -100,19 +99,19 @@ class SendTabUseCasesTest {
|
||||
|
||||
useCases.sendToDeviceAsync("123", tab)
|
||||
|
||||
verify(constellation, never()).sendCommandToDeviceAsync(any(), any())
|
||||
verify(constellation, never()).sendCommandToDevice(any(), any())
|
||||
|
||||
`when`(state.otherDevices).thenReturn(listOf(device))
|
||||
`when`(constellation.sendCommandToDeviceAsync(any(), any()))
|
||||
.thenReturn(CompletableDeferred(false))
|
||||
`when`(constellation.sendCommandToDevice(any(), any()))
|
||||
.thenReturn(false)
|
||||
|
||||
useCases.sendToDeviceAsync("456", tab)
|
||||
|
||||
verify(constellation, never()).sendCommandToDeviceAsync(any(), any())
|
||||
verify(constellation, never()).sendCommandToDevice(any(), any())
|
||||
|
||||
useCases.sendToDeviceAsync("123", tab)
|
||||
|
||||
verify(constellation).sendCommandToDeviceAsync(any(), any())
|
||||
verify(constellation).sendCommandToDevice(any(), any())
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -123,19 +122,19 @@ class SendTabUseCasesTest {
|
||||
|
||||
useCases.sendToDeviceAsync("123", listOf(tab))
|
||||
|
||||
verify(constellation, never()).sendCommandToDeviceAsync(any(), any())
|
||||
verify(constellation, never()).sendCommandToDevice(any(), any())
|
||||
|
||||
`when`(state.otherDevices).thenReturn(listOf(device))
|
||||
`when`(constellation.sendCommandToDeviceAsync(any(), any()))
|
||||
.thenReturn(CompletableDeferred(false))
|
||||
`when`(constellation.sendCommandToDevice(any(), any()))
|
||||
.thenReturn(false)
|
||||
|
||||
useCases.sendToDeviceAsync("456", listOf(tab))
|
||||
|
||||
verify(constellation, never()).sendCommandToDeviceAsync(any(), any())
|
||||
verify(constellation, never()).sendCommandToDevice(any(), any())
|
||||
|
||||
useCases.sendToDeviceAsync("123", listOf(tab))
|
||||
|
||||
verify(constellation).sendCommandToDeviceAsync(any(), any())
|
||||
verify(constellation).sendCommandToDevice(any(), any())
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -145,14 +144,14 @@ class SendTabUseCasesTest {
|
||||
val device2: Device = generateDevice()
|
||||
|
||||
`when`(state.otherDevices).thenReturn(listOf(device, device2))
|
||||
`when`(constellation.sendCommandToDeviceAsync(any(), any()))
|
||||
.thenReturn(CompletableDeferred(false))
|
||||
`when`(constellation.sendCommandToDevice(any(), any()))
|
||||
.thenReturn(false)
|
||||
|
||||
val tab = TabData("Mozilla", "https://mozilla.org")
|
||||
|
||||
useCases.sendToAllAsync(tab)
|
||||
|
||||
verify(constellation, times(2)).sendCommandToDeviceAsync(any(), any())
|
||||
verify(constellation, times(2)).sendCommandToDevice(any(), any())
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -162,15 +161,15 @@ class SendTabUseCasesTest {
|
||||
val device2: Device = generateDevice()
|
||||
|
||||
`when`(state.otherDevices).thenReturn(listOf(device, device2))
|
||||
`when`(constellation.sendCommandToDeviceAsync(any(), any()))
|
||||
.thenReturn(CompletableDeferred(false))
|
||||
`when`(constellation.sendCommandToDevice(any(), any()))
|
||||
.thenReturn(false)
|
||||
|
||||
val tab = TabData("Mozilla", "https://mozilla.org")
|
||||
val tab2 = TabData("Firefox", "https://firefox.com")
|
||||
|
||||
useCases.sendToAllAsync(listOf(tab, tab2))
|
||||
|
||||
verify(constellation, times(4)).sendCommandToDeviceAsync(any(), any())
|
||||
verify(constellation, times(4)).sendCommandToDevice(any(), any())
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -183,7 +182,7 @@ class SendTabUseCasesTest {
|
||||
runBlocking {
|
||||
useCases.sendToAllAsync(tab)
|
||||
|
||||
verify(constellation, never()).sendCommandToDeviceAsync(any(), any())
|
||||
verify(constellation, never()).sendCommandToDevice(any(), any())
|
||||
|
||||
`when`(device.id).thenReturn("123")
|
||||
`when`(device2.id).thenReturn("456")
|
||||
@@ -191,7 +190,7 @@ class SendTabUseCasesTest {
|
||||
|
||||
useCases.sendToAllAsync(tab)
|
||||
|
||||
verify(constellation, never()).sendCommandToDeviceAsync(any(), any())
|
||||
verify(constellation, never()).sendCommandToDevice(any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,7 +205,7 @@ class SendTabUseCasesTest {
|
||||
runBlocking {
|
||||
useCases.sendToAllAsync(tab)
|
||||
|
||||
verify(constellation, never()).sendCommandToDeviceAsync(any(), any())
|
||||
verify(constellation, never()).sendCommandToDevice(any(), any())
|
||||
|
||||
`when`(device.id).thenReturn("123")
|
||||
`when`(device2.id).thenReturn("456")
|
||||
@@ -214,8 +213,8 @@ class SendTabUseCasesTest {
|
||||
|
||||
useCases.sendToAllAsync(listOf(tab, tab2))
|
||||
|
||||
verify(constellation, never()).sendCommandToDeviceAsync(eq("123"), any())
|
||||
verify(constellation, never()).sendCommandToDeviceAsync(eq("456"), any())
|
||||
verify(constellation, never()).sendCommandToDevice(eq("123"), any())
|
||||
verify(constellation, never()).sendCommandToDevice(eq("456"), any())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,17 +226,17 @@ class SendTabUseCasesTest {
|
||||
|
||||
useCases.sendToDeviceAsync("123", listOf(tab, tab))
|
||||
|
||||
verify(constellation, never()).sendCommandToDeviceAsync(any(), any())
|
||||
verify(constellation, never()).sendCommandToDevice(any(), any())
|
||||
|
||||
`when`(device.id).thenReturn("123")
|
||||
`when`(state.otherDevices).thenReturn(listOf(device))
|
||||
`when`(constellation.sendCommandToDeviceAsync(any(), any()))
|
||||
.thenReturn(CompletableDeferred(true))
|
||||
.thenReturn(CompletableDeferred(true))
|
||||
`when`(constellation.sendCommandToDevice(any(), any()))
|
||||
.thenReturn(true)
|
||||
.thenReturn(true)
|
||||
|
||||
val result = useCases.sendToDeviceAsync("123", listOf(tab, tab))
|
||||
|
||||
verify(constellation, never()).sendCommandToDeviceAsync(any(), any())
|
||||
verify(constellation, never()).sendCommandToDevice(any(), any())
|
||||
Assert.assertFalse(result.await())
|
||||
}
|
||||
|
||||
|
||||
@@ -34,13 +34,13 @@ class FirefoxAccountsAuthFeature(
|
||||
) {
|
||||
fun beginAuthentication(context: Context) {
|
||||
beginAuthenticationAsync(context) {
|
||||
accountManager.beginAuthenticationAsync().await()
|
||||
accountManager.beginAuthentication()
|
||||
}
|
||||
}
|
||||
|
||||
fun beginPairingAuthentication(context: Context, pairingUrl: String) {
|
||||
beginAuthenticationAsync(context) {
|
||||
accountManager.beginAuthenticationAsync(pairingUrl).await()
|
||||
accountManager.beginAuthentication(pairingUrl)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,11 +81,13 @@ class FirefoxAccountsAuthFeature(
|
||||
val state = parsedUri.getQueryParameter("state") as String
|
||||
|
||||
// Notify the state machine about our success.
|
||||
accountManager.finishAuthenticationAsync(FxaAuthData(
|
||||
authType = authType,
|
||||
code = code,
|
||||
state = state
|
||||
))
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
accountManager.finishAuthentication(FxaAuthData(
|
||||
authType = authType,
|
||||
code = code,
|
||||
state = state
|
||||
))
|
||||
}
|
||||
|
||||
return RequestInterceptor.InterceptionResponse.Url(redirectUrl)
|
||||
}
|
||||
|
||||
@@ -7,9 +7,11 @@ package mozilla.components.feature.accounts
|
||||
import android.content.Context
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.concept.engine.EngineSession
|
||||
@@ -299,12 +301,14 @@ class FxaWebChannelFeature(
|
||||
return null
|
||||
}
|
||||
|
||||
accountManager.finishAuthenticationAsync(FxaAuthData(
|
||||
authType = authType,
|
||||
code = code,
|
||||
state = state,
|
||||
declinedEngines = declinedEngines?.toSyncEngines()
|
||||
))
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
accountManager.finishAuthentication(FxaAuthData(
|
||||
authType = authType,
|
||||
code = code,
|
||||
state = state,
|
||||
declinedEngines = declinedEngines?.toSyncEngines()
|
||||
))
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
package mozilla.components.feature.accounts
|
||||
|
||||
import android.content.Context
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import mozilla.components.concept.sync.DeviceType
|
||||
import mozilla.components.concept.sync.OAuthAccount
|
||||
@@ -21,10 +20,11 @@ import org.mockito.ArgumentMatchers.anyBoolean
|
||||
import org.mockito.ArgumentMatchers.anyString
|
||||
import org.mockito.Mockito.`when`
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import mozilla.components.concept.engine.request.RequestInterceptor
|
||||
import mozilla.components.concept.sync.AuthFlowUrl
|
||||
import mozilla.components.concept.sync.AuthType
|
||||
import mozilla.components.service.fxa.DeviceConfig
|
||||
import mozilla.components.concept.sync.DeviceConfig
|
||||
import mozilla.components.service.fxa.FxaAuthData
|
||||
import mozilla.components.service.fxa.Server
|
||||
import org.junit.Assert.assertEquals
|
||||
@@ -32,6 +32,7 @@ import org.junit.Assert.assertNull
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.verify
|
||||
import org.robolectric.annotation.Config
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
// Same as the actual account manager, except we get to control how FirefoxAccountShaped instances
|
||||
// are created. This is necessary because due to some build issues (native dependencies not available
|
||||
@@ -41,107 +42,101 @@ class TestableFxaAccountManager(
|
||||
context: Context,
|
||||
config: ServerConfig,
|
||||
scopes: Set<String>,
|
||||
coroutineContext: CoroutineContext,
|
||||
val block: () -> OAuthAccount = { mock() }
|
||||
) : FxaAccountManager(context, config, DeviceConfig("test", DeviceType.MOBILE, setOf()), null, scopes) {
|
||||
|
||||
override fun createAccount(config: ServerConfig): OAuthAccount {
|
||||
) : FxaAccountManager(context, config, DeviceConfig("test", DeviceType.MOBILE, setOf()), null, scopes, null, coroutineContext) {
|
||||
override fun obtainAccount(config: ServerConfig): OAuthAccount {
|
||||
return block()
|
||||
}
|
||||
}
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class FirefoxAccountsAuthFeatureTest {
|
||||
class TestOnBeginAuthentication : (Context, String) -> Unit {
|
||||
var url: String? = null
|
||||
|
||||
override fun invoke(context: Context, authUrl: String) {
|
||||
url = authUrl
|
||||
}
|
||||
}
|
||||
|
||||
// Note that tests that involve secure storage specify API=21, because of issues testing secure storage on
|
||||
// 23+ API levels. See https://github.com/mozilla-mobile/android-components/issues/4956
|
||||
|
||||
@Config(sdk = [22])
|
||||
@Test
|
||||
fun `begin authentication`() {
|
||||
val manager = prepareAccountManagerForSuccessfulAuthentication()
|
||||
val authLabmda = TestOnBeginAuthentication()
|
||||
|
||||
runBlocking {
|
||||
val feature = FirefoxAccountsAuthFeature(
|
||||
manager,
|
||||
"somePath",
|
||||
this.coroutineContext,
|
||||
authLabmda
|
||||
)
|
||||
feature.beginAuthentication(testContext)
|
||||
fun `begin authentication`() = runBlocking {
|
||||
val manager = prepareAccountManagerForSuccessfulAuthentication(
|
||||
this.coroutineContext
|
||||
)
|
||||
val authUrl = CompletableDeferred<String>()
|
||||
val feature = FirefoxAccountsAuthFeature(
|
||||
manager,
|
||||
"somePath",
|
||||
this.coroutineContext
|
||||
) { _, url ->
|
||||
authUrl.complete(url)
|
||||
}
|
||||
|
||||
assertEquals("auth://url", authLabmda.url)
|
||||
feature.beginAuthentication(testContext)
|
||||
authUrl.await()
|
||||
assertEquals("auth://url", authUrl.getCompleted())
|
||||
}
|
||||
|
||||
@Config(sdk = [22])
|
||||
@Test
|
||||
fun `begin pairing authentication`() {
|
||||
val manager = prepareAccountManagerForSuccessfulAuthentication()
|
||||
val authLabmda = TestOnBeginAuthentication()
|
||||
|
||||
runBlocking {
|
||||
val feature = FirefoxAccountsAuthFeature(
|
||||
manager,
|
||||
"somePath",
|
||||
this.coroutineContext,
|
||||
authLabmda
|
||||
)
|
||||
feature.beginPairingAuthentication(testContext, "auth://pair")
|
||||
fun `begin pairing authentication`() = runBlocking {
|
||||
val manager = prepareAccountManagerForSuccessfulAuthentication(
|
||||
this.coroutineContext
|
||||
)
|
||||
val authUrl = CompletableDeferred<String>()
|
||||
val feature = FirefoxAccountsAuthFeature(
|
||||
manager,
|
||||
"somePath",
|
||||
this.coroutineContext
|
||||
) { _, url ->
|
||||
authUrl.complete(url)
|
||||
}
|
||||
|
||||
assertEquals("auth://url", authLabmda.url)
|
||||
feature.beginPairingAuthentication(testContext, "auth://pair")
|
||||
authUrl.await()
|
||||
assertEquals("auth://url", authUrl.getCompleted())
|
||||
}
|
||||
|
||||
@Config(sdk = [22])
|
||||
@Test
|
||||
fun `begin authentication with errors`() {
|
||||
val manager = prepareAccountManagerForFailedAuthentication()
|
||||
val authLambda = TestOnBeginAuthentication()
|
||||
fun `begin authentication with errors`() = runBlocking {
|
||||
val manager = prepareAccountManagerForFailedAuthentication(
|
||||
this.coroutineContext
|
||||
)
|
||||
val authUrl = CompletableDeferred<String>()
|
||||
|
||||
runBlocking {
|
||||
val feature = FirefoxAccountsAuthFeature(
|
||||
manager,
|
||||
"somePath",
|
||||
this.coroutineContext,
|
||||
authLambda
|
||||
)
|
||||
feature.beginAuthentication(testContext)
|
||||
val feature = FirefoxAccountsAuthFeature(
|
||||
manager,
|
||||
"somePath",
|
||||
this.coroutineContext
|
||||
) { _, url ->
|
||||
authUrl.complete(url)
|
||||
}
|
||||
|
||||
feature.beginAuthentication(testContext)
|
||||
authUrl.await()
|
||||
// Fallback url is invoked.
|
||||
assertEquals("https://accounts.firefox.com/signin", authLambda.url)
|
||||
assertEquals("https://accounts.firefox.com/signin", authUrl.getCompleted())
|
||||
}
|
||||
|
||||
@Config(sdk = [22])
|
||||
@Test
|
||||
fun `begin pairing authentication with errors`() {
|
||||
val manager = prepareAccountManagerForFailedAuthentication()
|
||||
val authLambda = TestOnBeginAuthentication()
|
||||
fun `begin pairing authentication with errors`() = runBlocking {
|
||||
val manager = prepareAccountManagerForFailedAuthentication(
|
||||
this.coroutineContext
|
||||
)
|
||||
val authUrl = CompletableDeferred<String>()
|
||||
|
||||
runBlocking {
|
||||
val feature = FirefoxAccountsAuthFeature(
|
||||
manager,
|
||||
"somePath",
|
||||
this.coroutineContext,
|
||||
authLambda
|
||||
)
|
||||
feature.beginPairingAuthentication(testContext, "auth://pair")
|
||||
val feature = FirefoxAccountsAuthFeature(
|
||||
manager,
|
||||
"somePath",
|
||||
this.coroutineContext
|
||||
) { _, url ->
|
||||
authUrl.complete(url)
|
||||
}
|
||||
|
||||
feature.beginPairingAuthentication(testContext, "auth://pair")
|
||||
authUrl.await()
|
||||
// Fallback url is invoked.
|
||||
assertEquals("https://accounts.firefox.com/signin", authLambda.url)
|
||||
assertEquals("https://accounts.firefox.com/signin", authUrl.getCompleted())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `auth interceptor`() {
|
||||
fun `auth interceptor`() = runBlocking {
|
||||
val manager = mock<FxaAccountManager>()
|
||||
val redirectUrl = "https://accounts.firefox.com/oauth/success/123"
|
||||
val feature = FirefoxAccountsAuthFeature(
|
||||
@@ -152,26 +147,26 @@ class FirefoxAccountsAuthFeatureTest {
|
||||
|
||||
// Non-final FxA url.
|
||||
assertNull(feature.interceptor.onLoadRequest(mock(), "https://accounts.firefox.com/not/the/right/url", null, false, false, false, false, false))
|
||||
verify(manager, never()).finishAuthenticationAsync(any())
|
||||
verify(manager, never()).finishAuthentication(any())
|
||||
|
||||
// Non-FxA url.
|
||||
assertNull(feature.interceptor.onLoadRequest(mock(), "https://www.wikipedia.org", null, false, false, false, false, false))
|
||||
verify(manager, never()).finishAuthenticationAsync(any())
|
||||
verify(manager, never()).finishAuthentication(any())
|
||||
|
||||
// Redirect url, without code/state.
|
||||
assertNull(feature.interceptor.onLoadRequest(mock(), "https://accounts.firefox.com/oauth/success/123/", null, false, false, false, false, false))
|
||||
verify(manager, never()).finishAuthenticationAsync(any())
|
||||
verify(manager, never()).finishAuthentication(any())
|
||||
|
||||
// Redirect url, without code/state.
|
||||
assertNull(feature.interceptor.onLoadRequest(mock(), "https://accounts.firefox.com/oauth/success/123/test", null, false, false, false, false, false))
|
||||
verify(manager, never()).finishAuthenticationAsync(any())
|
||||
verify(manager, never()).finishAuthentication(any())
|
||||
|
||||
// Code+state, no action.
|
||||
assertEquals(
|
||||
RequestInterceptor.InterceptionResponse.Url(redirectUrl),
|
||||
feature.interceptor.onLoadRequest(mock(), "https://accounts.firefox.com/oauth/success/123/?code=testCode1&state=testState1", null, false, false, false, false, false)
|
||||
)
|
||||
verify(manager).finishAuthenticationAsync(
|
||||
verify(manager).finishAuthentication(
|
||||
FxaAuthData(authType = AuthType.OtherExternal(null), code = "testCode1", state = "testState1")
|
||||
)
|
||||
|
||||
@@ -180,7 +175,7 @@ class FirefoxAccountsAuthFeatureTest {
|
||||
RequestInterceptor.InterceptionResponse.Url(redirectUrl),
|
||||
feature.interceptor.onLoadRequest(mock(), "https://accounts.firefox.com/oauth/success/123/?code=testCode2&state=testState2&action=signin", null, false, false, false, false, false)
|
||||
)
|
||||
verify(manager).finishAuthenticationAsync(
|
||||
verify(manager).finishAuthentication(
|
||||
FxaAuthData(authType = AuthType.Signin, code = "testCode2", state = "testState2")
|
||||
)
|
||||
|
||||
@@ -189,7 +184,7 @@ class FirefoxAccountsAuthFeatureTest {
|
||||
RequestInterceptor.InterceptionResponse.Url(redirectUrl),
|
||||
feature.interceptor.onLoadRequest(mock(), "https://accounts.firefox.com/oauth/success/123/?code=testCode3&state=testState3&action=signup", null, false, false, false, false, false)
|
||||
)
|
||||
verify(manager).finishAuthenticationAsync(
|
||||
verify(manager).finishAuthentication(
|
||||
FxaAuthData(authType = AuthType.Signup, code = "testCode3", state = "testState3")
|
||||
)
|
||||
|
||||
@@ -198,7 +193,7 @@ class FirefoxAccountsAuthFeatureTest {
|
||||
RequestInterceptor.InterceptionResponse.Url(redirectUrl),
|
||||
feature.interceptor.onLoadRequest(mock(), "https://accounts.firefox.com/oauth/success/123/?code=testCode4&state=testState4&action=pairing", null, false, false, false, false, false)
|
||||
)
|
||||
verify(manager).finishAuthenticationAsync(
|
||||
verify(manager).finishAuthentication(
|
||||
FxaAuthData(authType = AuthType.Pairing, code = "testCode4", state = "testState4")
|
||||
)
|
||||
|
||||
@@ -207,58 +202,62 @@ class FirefoxAccountsAuthFeatureTest {
|
||||
RequestInterceptor.InterceptionResponse.Url(redirectUrl),
|
||||
feature.interceptor.onLoadRequest(mock(), "https://accounts.firefox.com/oauth/success/123/?code=testCode5&state=testState5&action=someNewActionType", null, false, false, false, false, false)
|
||||
)
|
||||
verify(manager).finishAuthenticationAsync(
|
||||
verify(manager).finishAuthentication(
|
||||
FxaAuthData(authType = AuthType.OtherExternal("someNewActionType"), code = "testCode5", state = "testState5")
|
||||
)
|
||||
Unit
|
||||
}
|
||||
|
||||
@Config(sdk = [22])
|
||||
private fun prepareAccountManagerForSuccessfulAuthentication(): TestableFxaAccountManager {
|
||||
private suspend fun prepareAccountManagerForSuccessfulAuthentication(
|
||||
coroutineContext: CoroutineContext
|
||||
): TestableFxaAccountManager {
|
||||
val mockAccount: OAuthAccount = mock()
|
||||
val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
|
||||
|
||||
`when`(mockAccount.getProfileAsync(anyBoolean())).thenReturn(CompletableDeferred(profile))
|
||||
`when`(mockAccount.beginOAuthFlowAsync(any(), anyString())).thenReturn(CompletableDeferred(AuthFlowUrl("authState", "auth://url")))
|
||||
`when`(mockAccount.beginPairingFlowAsync(anyString(), any(), anyString())).thenReturn(CompletableDeferred(AuthFlowUrl("authState", "auth://url")))
|
||||
`when`(mockAccount.completeOAuthFlowAsync(anyString(), anyString())).thenReturn(CompletableDeferred(true))
|
||||
`when`(mockAccount.deviceConstellation()).thenReturn(mock())
|
||||
`when`(mockAccount.getProfile(anyBoolean())).thenReturn(profile)
|
||||
`when`(mockAccount.beginOAuthFlow(any(), anyString())).thenReturn(AuthFlowUrl("authState", "auth://url"))
|
||||
`when`(mockAccount.beginPairingFlow(anyString(), any(), anyString())).thenReturn(AuthFlowUrl("authState", "auth://url"))
|
||||
`when`(mockAccount.completeOAuthFlow(anyString(), anyString())).thenReturn(true)
|
||||
|
||||
val manager = TestableFxaAccountManager(
|
||||
testContext,
|
||||
ServerConfig(Server.RELEASE, "dummyId", "bad://url"),
|
||||
setOf("test-scope")
|
||||
setOf("test-scope"),
|
||||
coroutineContext
|
||||
) {
|
||||
mockAccount
|
||||
}
|
||||
|
||||
runBlocking {
|
||||
manager.initAsync().await()
|
||||
}
|
||||
manager.start()
|
||||
|
||||
return manager
|
||||
}
|
||||
|
||||
@Config(sdk = [22])
|
||||
private fun prepareAccountManagerForFailedAuthentication(): TestableFxaAccountManager {
|
||||
private suspend fun prepareAccountManagerForFailedAuthentication(
|
||||
coroutineContext: CoroutineContext
|
||||
): TestableFxaAccountManager {
|
||||
val mockAccount: OAuthAccount = mock()
|
||||
val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
|
||||
|
||||
`when`(mockAccount.getProfileAsync(anyBoolean())).thenReturn(CompletableDeferred(profile))
|
||||
|
||||
`when`(mockAccount.beginOAuthFlowAsync(any(), anyString())).thenReturn(CompletableDeferred(value = null))
|
||||
`when`(mockAccount.beginPairingFlowAsync(anyString(), any(), anyString())).thenReturn(CompletableDeferred(value = null))
|
||||
`when`(mockAccount.completeOAuthFlowAsync(anyString(), anyString())).thenReturn(CompletableDeferred(true))
|
||||
`when`(mockAccount.getProfile(anyBoolean())).thenReturn(profile)
|
||||
`when`(mockAccount.deviceConstellation()).thenReturn(mock())
|
||||
`when`(mockAccount.beginOAuthFlow(any(), anyString())).thenReturn(null)
|
||||
`when`(mockAccount.beginPairingFlow(anyString(), any(), anyString())).thenReturn(null)
|
||||
`when`(mockAccount.completeOAuthFlow(anyString(), anyString())).thenReturn(true)
|
||||
|
||||
val manager = TestableFxaAccountManager(
|
||||
testContext,
|
||||
ServerConfig(Server.RELEASE, "dummyId", "bad://url"),
|
||||
setOf("test-scope")
|
||||
setOf("test-scope"),
|
||||
coroutineContext
|
||||
) {
|
||||
mockAccount
|
||||
}
|
||||
|
||||
runBlocking {
|
||||
manager.initAsync().await()
|
||||
}
|
||||
manager.start()
|
||||
|
||||
return manager
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
package mozilla.components.feature.accounts
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import mozilla.components.browser.state.action.EngineAction
|
||||
import mozilla.components.browser.state.action.TabListAction
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
@@ -253,7 +253,6 @@ class FxaWebChannelFeatureTest {
|
||||
val ext: WebExtension = mock()
|
||||
val port: Port = mock()
|
||||
val expectedEngines: Set<SyncEngine> = setOf(SyncEngine.History)
|
||||
val logoutDeferred = CompletableDeferred<Unit>()
|
||||
val messageHandler = argumentCaptor<MessageHandler>()
|
||||
val responseToTheWebChannel = argumentCaptor<JSONObject>()
|
||||
|
||||
@@ -263,7 +262,6 @@ class FxaWebChannelFeatureTest {
|
||||
whenever(accountManager.accountProfile()).thenReturn(profile)
|
||||
whenever(accountManager.authenticatedAccount()).thenReturn(account)
|
||||
whenever(accountManager.supportedSyncEngines()).thenReturn(expectedEngines)
|
||||
whenever(accountManager.logoutAsync()).thenReturn(logoutDeferred)
|
||||
|
||||
val webchannelFeature = prepareFeatureForTest(ext, port, engineSession, expectedEngines, emptySet(), accountManager)
|
||||
webchannelFeature.start()
|
||||
@@ -308,7 +306,6 @@ class FxaWebChannelFeatureTest {
|
||||
val ext: WebExtension = mock()
|
||||
val port: Port = mock()
|
||||
val expectedEngines: Set<SyncEngine> = setOf(SyncEngine.History)
|
||||
val logoutDeferred = CompletableDeferred<Unit>()
|
||||
val messageHandler = argumentCaptor<MessageHandler>()
|
||||
val responseToTheWebChannel = argumentCaptor<JSONObject>()
|
||||
|
||||
@@ -317,7 +314,6 @@ class FxaWebChannelFeatureTest {
|
||||
whenever(accountManager.accountProfile()).thenReturn(null)
|
||||
whenever(accountManager.authenticatedAccount()).thenReturn(account)
|
||||
whenever(accountManager.supportedSyncEngines()).thenReturn(expectedEngines)
|
||||
whenever(accountManager.logoutAsync()).thenReturn(logoutDeferred)
|
||||
|
||||
val webchannelFeature = prepareFeatureForTest(ext, port, engineSession, expectedEngines, emptySet(), accountManager)
|
||||
webchannelFeature.start()
|
||||
@@ -485,14 +481,14 @@ class FxaWebChannelFeatureTest {
|
||||
|
||||
// Receiving an oauth-login message account manager accepts the request
|
||||
@Test
|
||||
fun `COMMAND_OAUTH_LOGIN web-channel must be processed through when the accountManager accepts the request`() {
|
||||
fun `COMMAND_OAUTH_LOGIN web-channel must be processed through when the accountManager accepts the request`() = runBlocking {
|
||||
val accountManager: FxaAccountManager = mock() // syncConfig is null by default (is not configured)
|
||||
val engineSession: EngineSession = mock()
|
||||
val ext: WebExtension = mock()
|
||||
val port: Port = mock()
|
||||
val messageHandler = argumentCaptor<MessageHandler>()
|
||||
|
||||
whenever(accountManager.finishAuthenticationAsync(any())).thenReturn(CompletableDeferred(false))
|
||||
whenever(accountManager.finishAuthentication(any())).thenReturn(false)
|
||||
WebExtensionController.installedExtensions[FxaWebChannelFeature.WEB_CHANNEL_EXTENSION_ID] = ext
|
||||
|
||||
val webchannelFeature = prepareFeatureForTest(ext, port, engineSession, null, emptySet(), accountManager)
|
||||
@@ -517,14 +513,14 @@ class FxaWebChannelFeatureTest {
|
||||
|
||||
// Receiving an oauth-login message account manager refuses the request
|
||||
@Test
|
||||
fun `COMMAND_OAUTH_LOGIN web-channel must be processed when the accountManager refuses the request`() {
|
||||
fun `COMMAND_OAUTH_LOGIN web-channel must be processed when the accountManager refuses the request`() = runBlocking {
|
||||
val accountManager: FxaAccountManager = mock() // syncConfig is null by default (is not configured)
|
||||
val engineSession: EngineSession = mock()
|
||||
val ext: WebExtension = mock()
|
||||
val port: Port = mock()
|
||||
val messageHandler = argumentCaptor<MessageHandler>()
|
||||
|
||||
whenever(accountManager.finishAuthenticationAsync(any())).thenReturn(CompletableDeferred(false))
|
||||
whenever(accountManager.finishAuthentication(any())).thenReturn(false)
|
||||
WebExtensionController.installedExtensions[FxaWebChannelFeature.WEB_CHANNEL_EXTENSION_ID] = ext
|
||||
|
||||
val webchannelFeature = prepareFeatureForTest(ext, port, engineSession, null, emptySet(), accountManager)
|
||||
@@ -663,7 +659,7 @@ class FxaWebChannelFeatureTest {
|
||||
.getBoolean("ok")
|
||||
}
|
||||
|
||||
private fun verifyOauthLogin(action: String, expectedAuthType: AuthType, code: String, state: String, declined: Set<SyncEngine>?, messageHandler: MessageHandler, accountManager: FxaAccountManager) {
|
||||
private suspend fun verifyOauthLogin(action: String, expectedAuthType: AuthType, code: String, state: String, declined: Set<SyncEngine>?, messageHandler: MessageHandler, accountManager: FxaAccountManager) {
|
||||
val jsonToWebChannel = jsonOauthLogin(action, code, state, declined ?: emptySet())
|
||||
val port = mock<Port>()
|
||||
whenever(port.senderUrl()).thenReturn("https://foo.bar/email")
|
||||
@@ -675,7 +671,7 @@ class FxaWebChannelFeatureTest {
|
||||
state = state,
|
||||
declinedEngines = declined ?: emptySet()
|
||||
)
|
||||
verify(accountManager).finishAuthenticationAsync(expectedAuthData)
|
||||
verify(accountManager).finishAuthentication(expectedAuthData)
|
||||
}
|
||||
|
||||
private fun jsonOauthLogin(action: String, code: String, state: String, declined: Set<SyncEngine>): JSONObject {
|
||||
|
||||
@@ -57,10 +57,8 @@ internal class DefaultController(
|
||||
*/
|
||||
override fun syncAccount() {
|
||||
scope.launch {
|
||||
accountManager.withConstellation {
|
||||
refreshDevicesAsync().await()
|
||||
}
|
||||
accountManager.syncNowAsync(SyncReason.User)
|
||||
accountManager.withConstellation { refreshDevices() }
|
||||
accountManager.syncNow(SyncReason.User)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
package mozilla.components.feature.syncedtabs.controller
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.test.TestCoroutineDispatcher
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
@@ -160,11 +159,11 @@ class DefaultControllerTest {
|
||||
|
||||
`when`(accountManager.authenticatedAccount()).thenReturn(account)
|
||||
`when`(account.deviceConstellation()).thenReturn(constellation)
|
||||
`when`(constellation.refreshDevicesAsync()).thenReturn(CompletableDeferred(true))
|
||||
`when`(constellation.refreshDevices()).thenReturn(true)
|
||||
|
||||
controller.syncAccount()
|
||||
|
||||
verify(constellation).refreshDevicesAsync()
|
||||
verify(accountManager).syncNowAsync(SyncReason.User, false)
|
||||
verify(constellation).refreshDevices()
|
||||
verify(accountManager).syncNow(SyncReason.User, false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ dependencies {
|
||||
testImplementation files(configurations.jnaForTest.copyRecursive().files)
|
||||
|
||||
testImplementation Dependencies.mozilla_full_megazord_forUnitTests
|
||||
testImplementation Dependencies.kotlin_reflect
|
||||
}
|
||||
|
||||
apply from: '../../../publish.gradle'
|
||||
|
||||
@@ -4,53 +4,30 @@
|
||||
|
||||
package mozilla.components.service.fxa
|
||||
|
||||
import mozilla.components.concept.sync.DeviceCapability
|
||||
import mozilla.components.concept.sync.DeviceType
|
||||
import mozilla.components.service.fxa.manager.FxaAccountManager
|
||||
import mozilla.components.service.fxa.sync.GlobalSyncableStoreProvider
|
||||
|
||||
typealias ServerConfig = mozilla.appservices.fxaclient.Config
|
||||
typealias Server = mozilla.appservices.fxaclient.Config.Server
|
||||
|
||||
/**
|
||||
* 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 [FxaDeviceConstellation].
|
||||
*
|
||||
* @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. This set can be expanded by
|
||||
* re-initializing [FxaAccountManager] with a new set (e.g. on app restart).
|
||||
* Shrinking a set of capabilities is currently not supported.
|
||||
*
|
||||
* @property secureStateAtRest A flag indicating whether or not to use encrypted storage for the persisted account
|
||||
* state. If set to `true`, [SecureAbove22AccountStorage] will be used as a storage layer. As the name suggests,
|
||||
* account state will only by encrypted on Android API 23+. Otherwise, even if this flag is set to `true`, account state
|
||||
* will be stored in plaintext.
|
||||
*
|
||||
* Default value of `false` configures the plaintext version of account storage to be used, [SharedPrefAccountStorage].
|
||||
*
|
||||
* Switching of this flag's values is supported; account state will be migrated between the underlying storage layers.
|
||||
* @property periodMinutes How frequently periodic sync should happen.
|
||||
* @property initialDelayMinutes What should the initial delay for the periodic sync be.
|
||||
*/
|
||||
data class DeviceConfig(
|
||||
val name: String,
|
||||
val type: DeviceType,
|
||||
val capabilities: Set<DeviceCapability>,
|
||||
val secureStateAtRest: Boolean = false
|
||||
data class PeriodicSyncConfig(
|
||||
val periodMinutes: Int = 240,
|
||||
val initialDelayMinutes: Int = 5
|
||||
)
|
||||
|
||||
/**
|
||||
* Configuration for sync.
|
||||
*
|
||||
* @property supportedEngines A set of supported sync engines, exposed via [GlobalSyncableStoreProvider].
|
||||
* @property syncPeriodInMinutes Optional, how frequently periodic sync should happen. If this is `null`,
|
||||
* periodic syncing will be disabled.
|
||||
* @property periodicSyncConfig Optional configuration for running sync periodically.
|
||||
* Periodic sync is disabled if this is `null`.
|
||||
*/
|
||||
data class SyncConfig(
|
||||
val supportedEngines: Set<SyncEngine>,
|
||||
val syncPeriodInMinutes: Long? = null
|
||||
val periodicSyncConfig: PeriodicSyncConfig?
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,22 +5,21 @@
|
||||
package mozilla.components.service.fxa
|
||||
|
||||
import android.net.Uri
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.plus
|
||||
import mozilla.appservices.fxaclient.AuthorizationParams
|
||||
import kotlinx.coroutines.withContext
|
||||
import mozilla.appservices.fxaclient.FirefoxAccount as InternalFxAcct
|
||||
import mozilla.components.concept.sync.AccessType
|
||||
import mozilla.components.concept.sync.AuthFlowUrl
|
||||
import mozilla.components.concept.sync.MigratingAccountInfo
|
||||
import mozilla.components.concept.sync.DeviceConstellation
|
||||
import mozilla.components.concept.sync.OAuthAccount
|
||||
import mozilla.components.concept.sync.StatePersistenceCallback
|
||||
import mozilla.components.concept.base.crash.CrashReporting
|
||||
import mozilla.components.support.base.log.logger.Logger
|
||||
import org.json.JSONObject
|
||||
|
||||
typealias PersistCallback = mozilla.appservices.fxaclient.FirefoxAccount.PersistCallback
|
||||
|
||||
@@ -30,7 +29,7 @@ typealias PersistCallback = mozilla.appservices.fxaclient.FirefoxAccount.Persist
|
||||
@Suppress("TooManyFunctions")
|
||||
class FirefoxAccount internal constructor(
|
||||
private val inner: InternalFxAcct,
|
||||
private val crashReporter: CrashReporting? = null
|
||||
crashReporter: CrashReporting? = null
|
||||
) : OAuthAccount {
|
||||
private val job = SupervisorJob()
|
||||
private val scope = CoroutineScope(Dispatchers.IO) + job
|
||||
@@ -77,14 +76,6 @@ class FirefoxAccount internal constructor(
|
||||
/**
|
||||
* Construct a FirefoxAccount from a [Config], a clientId, and a redirectUri.
|
||||
*
|
||||
* @param persistCallback This callback will be called every time the [FirefoxAccount]
|
||||
* internal state has mutated.
|
||||
* The FirefoxAccount instance can be later restored using the
|
||||
* [FirefoxAccount.fromJSONString]` class method.
|
||||
* It is the responsibility of the consumer to ensure the persisted data
|
||||
* is saved in a secure location, as it can contain Sync Keys and
|
||||
* OAuth tokens.
|
||||
*
|
||||
* @param crashReporter A crash reporter instance.
|
||||
*
|
||||
* Note that it is not necessary to `close` the Config if this constructor is used (however
|
||||
@@ -92,9 +83,8 @@ class FirefoxAccount internal constructor(
|
||||
*/
|
||||
constructor(
|
||||
config: ServerConfig,
|
||||
persistCallback: PersistCallback? = null,
|
||||
crashReporter: CrashReporting? = null
|
||||
) : this(InternalFxAcct(config, persistCallback), crashReporter)
|
||||
) : this(InternalFxAcct(config), crashReporter)
|
||||
|
||||
override fun close() {
|
||||
job.cancel()
|
||||
@@ -102,10 +92,11 @@ class FirefoxAccount internal constructor(
|
||||
}
|
||||
|
||||
override fun registerPersistenceCallback(callback: StatePersistenceCallback) {
|
||||
logger.info("Registering persistence callback")
|
||||
persistCallback.setCallback(callback)
|
||||
}
|
||||
|
||||
override fun beginOAuthFlowAsync(scopes: Set<String>, entryPoint: String) = scope.async {
|
||||
override suspend fun beginOAuthFlow(scopes: Set<String>, entryPoint: String) = withContext(scope.coroutineContext) {
|
||||
handleFxaExceptions(logger, "begin oauth flow", { null }) {
|
||||
val url = inner.beginOAuthFlow(scopes.toTypedArray(), entryPoint)
|
||||
val state = Uri.parse(url).getQueryParameter("state")!!
|
||||
@@ -113,7 +104,10 @@ class FirefoxAccount internal constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun beginPairingFlowAsync(pairingUrl: String, scopes: Set<String>, entryPoint: String) = scope.async {
|
||||
override suspend fun beginPairingFlow(
|
||||
pairingUrl: String,
|
||||
scopes: Set<String>, entryPoint: String
|
||||
) = withContext(scope.coroutineContext) {
|
||||
// Eventually we should specify this as a param here, but for now, let's
|
||||
// use a generic value (it's used only for server-side telemetry, so the
|
||||
// actual value doesn't matter much)
|
||||
@@ -124,7 +118,7 @@ class FirefoxAccount internal constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun getProfileAsync(ignoreCache: Boolean) = scope.async {
|
||||
override suspend fun getProfile(ignoreCache: Boolean) = withContext(scope.coroutineContext) {
|
||||
handleFxaExceptions(logger, "getProfile", { null }) {
|
||||
inner.getProfile(ignoreCache).into()
|
||||
}
|
||||
@@ -142,12 +136,12 @@ class FirefoxAccount internal constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun authorizeOAuthCodeAsync(
|
||||
override suspend fun authorizeOAuthCode(
|
||||
clientId: String,
|
||||
scopes: Array<String>,
|
||||
state: String,
|
||||
accessType: AccessType
|
||||
) = scope.async {
|
||||
) = withContext(scope.coroutineContext) {
|
||||
handleFxaExceptions(logger, "authorizeOAuthCode", { null }) {
|
||||
val params = AuthorizationParams(clientId, scopes, state, accessType.msg)
|
||||
inner.authorizeOAuthCode(params)
|
||||
@@ -155,9 +149,9 @@ class FirefoxAccount internal constructor(
|
||||
}
|
||||
|
||||
override fun getSessionToken(): String? {
|
||||
// This is awkward, yes. Underlying method simply reads some data from in-memory state, and yet it throws
|
||||
// in case that data isn't there. See https://github.com/mozilla/application-services/issues/2202.
|
||||
return try {
|
||||
// This is awkward, yes. Underlying method simply reads some data from in-memory state, and yet it throws
|
||||
// in case that data isn't there. See https://github.com/mozilla/application-services/issues/2202.
|
||||
inner.getSessionToken()
|
||||
} catch (e: FxaPanicException) {
|
||||
throw e
|
||||
@@ -166,21 +160,24 @@ class FirefoxAccount internal constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun migrateFromSessionTokenAsync(sessionToken: String, kSync: String, kXCS: String) = scope.async {
|
||||
handleFxaExceptions(logger, "migrateFromSessionToken", { null }) {
|
||||
inner.migrateFromSessionToken(sessionToken, kSync, kXCS)
|
||||
}
|
||||
}
|
||||
|
||||
override fun copyFromSessionTokenAsync(sessionToken: String, kSync: String, kXCS: String) = scope.async {
|
||||
handleFxaExceptions(logger, "copyFromSessionToken", { null }) {
|
||||
inner.copyFromSessionToken(sessionToken, kSync, kXCS)
|
||||
override suspend fun migrateFromAccount(
|
||||
authInfo: MigratingAccountInfo,
|
||||
reuseSessionToken: Boolean
|
||||
) = withContext(scope.coroutineContext) {
|
||||
if (reuseSessionToken) {
|
||||
handleFxaExceptions(logger, "migrateFromSessionToken", { null }) {
|
||||
inner.migrateFromSessionToken(authInfo.sessionToken, authInfo.kSync, authInfo.kXCS)
|
||||
}
|
||||
} else {
|
||||
handleFxaExceptions(logger, "copyFromSessionToken", { null }) {
|
||||
inner.copyFromSessionToken(authInfo.sessionToken, authInfo.kSync, authInfo.kXCS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun isInMigrationState() = inner.isInMigrationState().into()
|
||||
|
||||
override fun retryMigrateFromSessionTokenAsync(): Deferred<JSONObject?> = scope.async {
|
||||
override suspend fun retryMigrateFromSessionToken() = withContext(scope.coroutineContext) {
|
||||
handleFxaExceptions(logger, "retryMigrateFromSessionToken", { null }) {
|
||||
inner.retryMigrateFromSessionToken()
|
||||
}
|
||||
@@ -201,13 +198,13 @@ class FirefoxAccount internal constructor(
|
||||
return inner.getConnectionSuccessURL()
|
||||
}
|
||||
|
||||
override fun completeOAuthFlowAsync(code: String, state: String) = scope.async {
|
||||
override suspend fun completeOAuthFlow(code: String, state: String) = withContext(scope.coroutineContext) {
|
||||
handleFxaExceptions(logger, "complete oauth flow") {
|
||||
inner.completeOAuthFlow(code, state)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAccessTokenAsync(singleScope: String) = scope.async {
|
||||
override suspend fun getAccessToken(singleScope: String) = withContext(scope.coroutineContext) {
|
||||
handleFxaExceptions(logger, "get access token", { null }) {
|
||||
inner.getAccessToken(singleScope).into()
|
||||
}
|
||||
@@ -219,7 +216,7 @@ class FirefoxAccount internal constructor(
|
||||
inner.clearAccessTokenCache()
|
||||
}
|
||||
|
||||
override fun checkAuthorizationStatusAsync(singleScope: String) = scope.async {
|
||||
override suspend fun checkAuthorizationStatus(singleScope: String) = withContext(scope.coroutineContext) {
|
||||
// Now that internal token caches are cleared, we can perform a connectivity check.
|
||||
// Do so by requesting a new access token using an internally-stored "refresh token".
|
||||
// Success here means that we're still able to connect - our cached access token simply expired.
|
||||
@@ -242,7 +239,7 @@ class FirefoxAccount internal constructor(
|
||||
// Re-throw all other exceptions.
|
||||
}
|
||||
|
||||
override fun disconnectAsync() = scope.async {
|
||||
override suspend fun disconnect() = withContext(scope.coroutineContext) {
|
||||
// TODO can this ever throw FxaUnauthorizedException? would that even make sense? or is that a bug?
|
||||
handleFxaExceptions(logger, "disconnect", { false }) {
|
||||
inner.disconnect()
|
||||
|
||||
@@ -5,29 +5,36 @@
|
||||
package mozilla.components.service.fxa
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import mozilla.appservices.fxaclient.FirefoxAccount
|
||||
import mozilla.appservices.fxaclient.FxaException
|
||||
import mozilla.components.concept.sync.ConstellationState
|
||||
import mozilla.components.concept.sync.Device
|
||||
import mozilla.components.concept.sync.DeviceCapability
|
||||
import mozilla.components.concept.sync.DeviceConstellation
|
||||
import mozilla.components.concept.sync.DeviceConstellationObserver
|
||||
import mozilla.components.concept.sync.AccountEvent
|
||||
import mozilla.components.concept.sync.AccountEventsObserver
|
||||
import mozilla.components.concept.sync.AuthType
|
||||
import mozilla.components.concept.sync.DeviceCommandOutgoing
|
||||
import mozilla.components.concept.sync.DeviceConfig
|
||||
import mozilla.components.concept.sync.DevicePushSubscription
|
||||
import mozilla.components.concept.sync.DeviceType
|
||||
import mozilla.components.concept.base.crash.CrashReporting
|
||||
import mozilla.components.concept.sync.ServiceResult
|
||||
import mozilla.components.support.base.log.logger.Logger
|
||||
import mozilla.components.support.base.observer.Observable
|
||||
import mozilla.components.support.base.observer.ObserverRegistry
|
||||
import mozilla.components.support.sync.telemetry.SyncTelemetry
|
||||
|
||||
internal sealed class FxaDeviceConstellationException : Exception() {
|
||||
/**
|
||||
* Failure while ensuring device capabilities.
|
||||
*/
|
||||
class EnsureCapabilitiesFailed : FxaDeviceConstellationException()
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides an implementation of [DeviceConstellation] backed by a [FirefoxAccount].
|
||||
*/
|
||||
@@ -46,31 +53,68 @@ class FxaDeviceConstellation(
|
||||
|
||||
override fun state(): ConstellationState? = constellationState
|
||||
|
||||
override fun initDeviceAsync(
|
||||
name: String,
|
||||
type: DeviceType,
|
||||
capabilities: Set<DeviceCapability>
|
||||
): Deferred<Boolean> {
|
||||
return scope.async {
|
||||
handleFxaExceptions(logger, "initializing device") {
|
||||
account.initializeDevice(name, type.into(), capabilities.map { it.into() }.toSet())
|
||||
@VisibleForTesting
|
||||
internal enum class DeviceFinalizeAction {
|
||||
Initialize,
|
||||
EnsureCapabilities,
|
||||
None
|
||||
}
|
||||
|
||||
@Suppress("ComplexMethod")
|
||||
@Throws(FxaPanicException::class)
|
||||
override suspend fun finalizeDevice(
|
||||
authType: AuthType,
|
||||
config: DeviceConfig
|
||||
): ServiceResult = withContext(scope.coroutineContext) {
|
||||
val finalizeAction = when (authType) {
|
||||
AuthType.Signin,
|
||||
AuthType.Signup,
|
||||
AuthType.Pairing,
|
||||
is AuthType.OtherExternal,
|
||||
AuthType.MigratedCopy -> DeviceFinalizeAction.Initialize
|
||||
AuthType.Existing,
|
||||
AuthType.MigratedReuse -> DeviceFinalizeAction.EnsureCapabilities
|
||||
AuthType.Recovered -> DeviceFinalizeAction.None
|
||||
}
|
||||
|
||||
if (finalizeAction == DeviceFinalizeAction.None) {
|
||||
ServiceResult.Ok
|
||||
} else {
|
||||
val capabilities = config.capabilities.map { it.into() }.toSet()
|
||||
if (finalizeAction == DeviceFinalizeAction.Initialize) {
|
||||
try {
|
||||
account.initializeDevice(config.name, config.type.into(), capabilities)
|
||||
ServiceResult.Ok
|
||||
} catch (e: FxaPanicException) {
|
||||
throw e
|
||||
} catch (e: FxaUnauthorizedException) {
|
||||
ServiceResult.AuthError
|
||||
} catch (e: FxaException) {
|
||||
ServiceResult.OtherError
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
account.ensureCapabilities(capabilities)
|
||||
ServiceResult.Ok
|
||||
} catch (e: FxaPanicException) {
|
||||
throw e
|
||||
} catch (e: FxaUnauthorizedException) {
|
||||
// Unless we've added a new capability, in practice 'ensureCapabilities' isn't
|
||||
// actually expected to do any work: everything should have been done by initializeDevice.
|
||||
// So if it did, and failed, let's report this so that we're aware of this!
|
||||
// See https://github.com/mozilla-mobile/android-components/issues/8164
|
||||
crashReporter?.submitCaughtException(FxaDeviceConstellationException.EnsureCapabilitiesFailed())
|
||||
ServiceResult.AuthError
|
||||
} catch (e: FxaException) {
|
||||
ServiceResult.OtherError
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun ensureCapabilitiesAsync(capabilities: Set<DeviceCapability>): Deferred<Boolean> {
|
||||
return scope.async {
|
||||
handleFxaExceptions(logger, "ensuring capabilities") {
|
||||
account.ensureCapabilities(capabilities.map { it.into() }.toSet())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun processRawEventAsync(payload: String): Deferred<Boolean> {
|
||||
return scope.async {
|
||||
handleFxaExceptions(logger, "processing raw commands") {
|
||||
processEvents(account.handlePushMessage(payload).map { it.into() })
|
||||
}
|
||||
override suspend fun processRawEvent(payload: String) = withContext(scope.coroutineContext) {
|
||||
handleFxaExceptions(logger, "processing raw commands") {
|
||||
processEvents(account.handlePushMessage(payload).map { it.into() })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,108 +126,99 @@ class FxaDeviceConstellation(
|
||||
deviceObserverRegistry.register(observer, owner, autoPause)
|
||||
}
|
||||
|
||||
override fun setDeviceNameAsync(name: String, context: Context): Deferred<Boolean> {
|
||||
return scope.async {
|
||||
val rename = handleFxaExceptions(logger, "changing device name") {
|
||||
account.setDeviceDisplayName(name)
|
||||
}
|
||||
FxaDeviceSettingsCache(context).updateCachedName(name)
|
||||
// See the latest device (name) changes after changing it.
|
||||
val refreshDevices = refreshDevicesAsync().await()
|
||||
override suspend fun setDeviceName(name: String, context: Context) = withContext(scope.coroutineContext) {
|
||||
val rename = handleFxaExceptions(logger, "changing device name") {
|
||||
account.setDeviceDisplayName(name)
|
||||
}
|
||||
FxaDeviceSettingsCache(context).updateCachedName(name)
|
||||
// See the latest device (name) changes after changing it.
|
||||
|
||||
rename && refreshDevices
|
||||
rename && refreshDevices()
|
||||
}
|
||||
|
||||
override suspend fun setDevicePushSubscription(
|
||||
subscription: DevicePushSubscription
|
||||
) = withContext(scope.coroutineContext) {
|
||||
handleFxaExceptions(logger, "updating device push subscription") {
|
||||
account.setDevicePushSubscription(
|
||||
subscription.endpoint, subscription.publicKey, subscription.authKey
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setDevicePushSubscriptionAsync(subscription: DevicePushSubscription): Deferred<Boolean> {
|
||||
return scope.async {
|
||||
handleFxaExceptions(logger, "updating device push subscription") {
|
||||
account.setDevicePushSubscription(
|
||||
subscription.endpoint, subscription.publicKey, subscription.authKey
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendCommandToDeviceAsync(
|
||||
override suspend fun sendCommandToDevice(
|
||||
targetDeviceId: String,
|
||||
outgoingCommand: DeviceCommandOutgoing
|
||||
): Deferred<Boolean> {
|
||||
return scope.async {
|
||||
handleFxaExceptions(logger, "sending device command") {
|
||||
when (outgoingCommand) {
|
||||
is DeviceCommandOutgoing.SendTab -> {
|
||||
account.sendSingleTab(targetDeviceId, outgoingCommand.title, outgoingCommand.url)
|
||||
SyncTelemetry.processFxaTelemetry(account.gatherTelemetry(), crashReporter)
|
||||
}
|
||||
else -> logger.debug("Skipped sending unsupported command type: $outgoingCommand")
|
||||
) = withContext(scope.coroutineContext) {
|
||||
handleFxaExceptions(logger, "sending device command") {
|
||||
when (outgoingCommand) {
|
||||
is DeviceCommandOutgoing.SendTab -> {
|
||||
account.sendSingleTab(targetDeviceId, outgoingCommand.title, outgoingCommand.url)
|
||||
SyncTelemetry.processFxaTelemetry(account.gatherTelemetry(), crashReporter)
|
||||
}
|
||||
else -> logger.debug("Skipped sending unsupported command type: $outgoingCommand")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Poll for missed commands. Commands are the only event-type that can be
|
||||
// polled for, although missed commands will be delivered as AccountEvents.
|
||||
override fun pollForCommandsAsync(): Deferred<Boolean> {
|
||||
return scope.async {
|
||||
val events = handleFxaExceptions(logger, "polling for device commands", { null }) {
|
||||
account.pollDeviceCommands().map { AccountEvent.DeviceCommandIncoming(command = it.into()) }
|
||||
}
|
||||
override suspend fun pollForCommands() = withContext(scope.coroutineContext) {
|
||||
val events = handleFxaExceptions(logger, "polling for device commands", { null }) {
|
||||
account.pollDeviceCommands().map { AccountEvent.DeviceCommandIncoming(command = it.into()) }
|
||||
}
|
||||
|
||||
if (events == null) {
|
||||
false
|
||||
} else {
|
||||
processEvents(events)
|
||||
SyncTelemetry.processFxaTelemetry(account.gatherTelemetry(), crashReporter)
|
||||
true
|
||||
}
|
||||
if (events == null) {
|
||||
false
|
||||
} else {
|
||||
processEvents(events)
|
||||
SyncTelemetry.processFxaTelemetry(account.gatherTelemetry(), crashReporter)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun processEvents(events: List<AccountEvent>) = CoroutineScope(Dispatchers.Main).launch {
|
||||
private fun processEvents(events: List<AccountEvent>) {
|
||||
notifyObservers { onEvents(events) }
|
||||
}
|
||||
|
||||
override fun refreshDevicesAsync(): Deferred<Boolean> = scope.async {
|
||||
logger.info("Refreshing device list...")
|
||||
override suspend fun refreshDevices(): Boolean {
|
||||
return withContext(scope.coroutineContext) {
|
||||
logger.info("Refreshing device list...")
|
||||
|
||||
// Attempt to fetch devices, or bail out on failure.
|
||||
val allDevices = fetchAllDevicesAsync().await() ?: return@async false
|
||||
// Attempt to fetch devices, or bail out on failure.
|
||||
val allDevices = fetchAllDevices() ?: return@withContext false
|
||||
|
||||
// Find the current device.
|
||||
val currentDevice = allDevices.find { it.isCurrentDevice }?.also {
|
||||
// Check if our current device's push subscription needs to be renewed.
|
||||
if (it.subscriptionExpired) {
|
||||
logger.info("Current device needs push endpoint registration")
|
||||
// Find the current device.
|
||||
val currentDevice = allDevices.find { it.isCurrentDevice }?.also {
|
||||
// Check if our current device's push subscription needs to be renewed.
|
||||
if (it.subscriptionExpired) {
|
||||
logger.info("Current device needs push endpoint registration")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out the current devices.
|
||||
val otherDevices = allDevices.filter { !it.isCurrentDevice }
|
||||
// Filter out the current devices.
|
||||
val otherDevices = allDevices.filter { !it.isCurrentDevice }
|
||||
|
||||
val newState = ConstellationState(currentDevice, otherDevices)
|
||||
constellationState = newState
|
||||
val newState = ConstellationState(currentDevice, otherDevices)
|
||||
constellationState = newState
|
||||
|
||||
logger.info("Refreshed device list; saw ${allDevices.size} device(s).")
|
||||
logger.info("Refreshed device list; saw ${allDevices.size} device(s).")
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
// NB: at this point, 'constellationState' might have changed.
|
||||
// Notify with an immutable, local 'newState' instead.
|
||||
deviceObserverRegistry.notifyObservers { onDevicesUpdate(newState) }
|
||||
}
|
||||
|
||||
true
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all devices in the constellation.
|
||||
* @return A list of all devices in the constellation, or `null` on failure.
|
||||
*/
|
||||
private fun fetchAllDevicesAsync(): Deferred<List<Device>?> {
|
||||
return scope.async {
|
||||
handleFxaExceptions(logger, "fetching all devices", { null }) {
|
||||
account.getDevices().map { it.into() }
|
||||
}
|
||||
private suspend fun fetchAllDevices(): List<Device>? {
|
||||
return handleFxaExceptions(logger, "fetching all devices", { null }) {
|
||||
account.getDevices().map { it.into() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,6 @@ import mozilla.appservices.fxaclient.MigrationState
|
||||
import mozilla.appservices.fxaclient.Profile
|
||||
import mozilla.appservices.fxaclient.ScopedKey
|
||||
import mozilla.appservices.fxaclient.TabHistoryEntry
|
||||
import mozilla.components.concept.sync.AuthException
|
||||
import mozilla.components.concept.sync.AuthExceptionType
|
||||
import mozilla.components.concept.sync.AuthType
|
||||
import mozilla.components.concept.sync.Avatar
|
||||
import mozilla.components.concept.sync.DeviceCapability
|
||||
@@ -49,7 +47,11 @@ data class FxaAuthData(
|
||||
val code: String,
|
||||
val state: String,
|
||||
val declinedEngines: Set<SyncEngine>? = null
|
||||
)
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return "authType: $authType, code: XXX, state: XXX, declinedEngines: $declinedEngines"
|
||||
}
|
||||
}
|
||||
|
||||
// The rest of this file describes translations between fxaclient's internal type definitions and analogous
|
||||
// types defined by concept-sync. It's a little tedious, but ensures decoupling between abstract
|
||||
@@ -70,10 +72,10 @@ fun AccessTokenInfo.into(): mozilla.components.concept.sync.AccessTokenInfo {
|
||||
* may be used for data synchronization.
|
||||
*
|
||||
* @return An [SyncAuthInfo] which is guaranteed to have a sync key.
|
||||
* @throws AuthException if [AccessTokenInfo] didn't have key information.
|
||||
* @throws IllegalStateException if [AccessTokenInfo] didn't have key information.
|
||||
*/
|
||||
fun mozilla.components.concept.sync.AccessTokenInfo.asSyncAuthInfo(tokenServerUrl: String): SyncAuthInfo {
|
||||
val keyInfo = this.key ?: throw AuthException(AuthExceptionType.KEY_INFO)
|
||||
val keyInfo = this.key ?: throw IllegalStateException("missing OAuthScopedKey")
|
||||
|
||||
return SyncAuthInfo(
|
||||
kid = keyInfo.kid,
|
||||
@@ -212,11 +214,11 @@ fun AccountEvent.into(): mozilla.components.concept.sync.AccountEvent {
|
||||
is AccountEvent.IncomingDeviceCommand ->
|
||||
mozilla.components.concept.sync.AccountEvent.DeviceCommandIncoming(command = this.command.into())
|
||||
is AccountEvent.ProfileUpdated ->
|
||||
mozilla.components.concept.sync.AccountEvent.ProfileUpdated()
|
||||
mozilla.components.concept.sync.AccountEvent.ProfileUpdated
|
||||
is AccountEvent.AccountAuthStateChanged ->
|
||||
mozilla.components.concept.sync.AccountEvent.AccountAuthStateChanged()
|
||||
mozilla.components.concept.sync.AccountEvent.AccountAuthStateChanged
|
||||
is AccountEvent.AccountDestroyed ->
|
||||
mozilla.components.concept.sync.AccountEvent.AccountDestroyed()
|
||||
mozilla.components.concept.sync.AccountEvent.AccountDestroyed
|
||||
is AccountEvent.DeviceConnected ->
|
||||
mozilla.components.concept.sync.AccountEvent.DeviceConnected(deviceName = this.deviceName)
|
||||
is AccountEvent.DeviceDisconnected ->
|
||||
@@ -241,9 +243,9 @@ fun IncomingDeviceCommand.TabReceived.into(): mozilla.components.concept.sync.De
|
||||
/**
|
||||
* Conversion function from fxaclient's data structure to ours.
|
||||
*/
|
||||
fun MigrationState.into(): InFlightMigrationState {
|
||||
fun MigrationState.into(): InFlightMigrationState? {
|
||||
return when (this) {
|
||||
MigrationState.NONE -> InFlightMigrationState.NONE
|
||||
MigrationState.NONE -> null
|
||||
MigrationState.COPY_SESSION_TOKEN -> InFlightMigrationState.COPY_SESSION_TOKEN
|
||||
MigrationState.REUSE_SESSION_TOKEN -> InFlightMigrationState.REUSE_SESSION_TOKEN
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,169 +4,135 @@
|
||||
|
||||
package mozilla.components.service.fxa.manager
|
||||
|
||||
import mozilla.components.concept.sync.AuthType
|
||||
import mozilla.components.service.fxa.FxaAuthData
|
||||
import mozilla.components.service.fxa.sharing.ShareableAccount
|
||||
|
||||
/**
|
||||
* States of the [FxaAccountManager].
|
||||
*/
|
||||
enum class AccountState {
|
||||
Start,
|
||||
internal enum class AccountState {
|
||||
NotAuthenticated,
|
||||
AuthenticationProblem,
|
||||
CanAutoRetryAuthenticationViaTokenCopy,
|
||||
CanAutoRetryAuthenticationViaTokenReuse,
|
||||
AuthenticatedNoProfile,
|
||||
AuthenticatedWithProfile,
|
||||
IncompleteMigration,
|
||||
Authenticated,
|
||||
AuthenticationProblem
|
||||
}
|
||||
|
||||
internal enum class ProgressState {
|
||||
Initializing,
|
||||
BeginningAuthentication,
|
||||
CompletingAuthentication,
|
||||
MigratingAccount,
|
||||
RecoveringFromAuthProblem,
|
||||
LoggingOut,
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for [FxaAccountManager] state machine events.
|
||||
* Events aren't a simple enum class because we might want to pass data along with some of the events.
|
||||
*/
|
||||
internal sealed class Event {
|
||||
override fun toString(): String {
|
||||
// For a better logcat experience.
|
||||
return this.javaClass.simpleName
|
||||
internal sealed class Account : Event() {
|
||||
internal object Start : Account()
|
||||
object BeginEmailFlow : Account()
|
||||
data class BeginPairingFlow(val pairingUrl: String?) : Account()
|
||||
data class AuthenticationError(val operation: String, val errorCountWithinTheTimeWindow: Int = 1) : Account() {
|
||||
override fun toString(): String {
|
||||
return "${this.javaClass.simpleName} - $operation"
|
||||
}
|
||||
}
|
||||
|
||||
data class MigrateFromAccount(val account: ShareableAccount, val reuseSessionToken: Boolean) : Account() {
|
||||
override fun toString(): String {
|
||||
return this.javaClass.simpleName
|
||||
}
|
||||
}
|
||||
|
||||
object RetryMigration : Account()
|
||||
|
||||
object Logout : Account()
|
||||
}
|
||||
|
||||
internal object Init : Event()
|
||||
internal sealed class Progress : Event() {
|
||||
object AccountNotFound : Progress()
|
||||
object AccountRestored : Progress()
|
||||
data class IncompleteMigration(val reuseSessionToken: Boolean) : Progress()
|
||||
|
||||
object AccountNotFound : Event()
|
||||
object AccountRestored : Event()
|
||||
data class AuthData(val authData: FxaAuthData) : Progress()
|
||||
data class Migrated(val reusedSessionToken: Boolean) : Progress()
|
||||
|
||||
object Authenticate : Event()
|
||||
data class Authenticated(val authData: FxaAuthData) : Event() {
|
||||
override fun toString(): String {
|
||||
// data classes define their own toString, so we override it here as well as in the base
|
||||
// class to avoid exposing 'code' and 'state' in logs.
|
||||
return this.javaClass.simpleName
|
||||
}
|
||||
}
|
||||
object FailedToCompleteMigration : Progress()
|
||||
object FailedToBeginAuth : Progress()
|
||||
object FailedToCompleteAuthRestore : Progress()
|
||||
object FailedToCompleteAuth : Progress()
|
||||
|
||||
/**
|
||||
* Fired during account init, if an in-flight copy migration was detected.
|
||||
*/
|
||||
object InFlightCopyMigration : Event()
|
||||
object FailedToRecoverFromAuthenticationProblem : Progress()
|
||||
object RecoveredFromAuthenticationProblem : Progress()
|
||||
|
||||
/**
|
||||
* Fired during account init, if an in-flight copy migration was detected.
|
||||
*/
|
||||
object InFlightReuseMigration : Event()
|
||||
object LoggedOut : Progress()
|
||||
|
||||
data class AuthenticationError(val operation: String, val errorCountWithinTheTimeWindow: Int = 1) : Event() {
|
||||
override fun toString(): String {
|
||||
return "${this.javaClass.simpleName} - $operation"
|
||||
}
|
||||
}
|
||||
data class SignInShareableAccount(val account: ShareableAccount, val reuseAccount: Boolean) : Event() {
|
||||
override fun toString(): String {
|
||||
return this.javaClass.simpleName
|
||||
}
|
||||
}
|
||||
data class SignedInShareableAccount(val reuseAccount: Boolean) : Event()
|
||||
|
||||
/**
|
||||
* Fired during SignInShareableAccount(reuseAccount=true) processing if an intermittent problem is encountered.
|
||||
*/
|
||||
object RetryLaterViaTokenReuse : Event()
|
||||
|
||||
/**
|
||||
* Fired during SignInShareableAccount(reuseAccount=false) processing if an intermittent problem is encountered.
|
||||
*/
|
||||
object RetryLaterViaTokenCopy : Event()
|
||||
|
||||
/**
|
||||
* Fired to trigger a migration retry.
|
||||
*/
|
||||
object RetryMigration : Event()
|
||||
|
||||
object RecoveredFromAuthenticationProblem : Event()
|
||||
object FetchProfile : Event()
|
||||
object FetchedProfile : Event()
|
||||
|
||||
object FailedToAuthenticate : Event()
|
||||
object FailedToFetchProfile : Event()
|
||||
|
||||
object Logout : Event()
|
||||
|
||||
data class Pair(val pairingUrl: String) : Event() {
|
||||
override fun toString(): String {
|
||||
// data classes define their own toString, so we override it here as well as in the base
|
||||
// class to avoid exposing the 'pairingUrl' in logs.
|
||||
return this.javaClass.simpleName
|
||||
}
|
||||
data class CompletedAuthentication(val authType: AuthType) : Progress()
|
||||
}
|
||||
}
|
||||
|
||||
internal object FxaStateMatrix {
|
||||
/**
|
||||
* State transition matrix.
|
||||
* @return An optional [AccountState] if provided state+event combination results in a
|
||||
* state transition. Note that states may transition into themselves.
|
||||
*/
|
||||
internal fun nextState(state: AccountState, event: Event): AccountState? =
|
||||
when (state) {
|
||||
AccountState.Start -> when (event) {
|
||||
Event.Init -> AccountState.Start
|
||||
Event.AccountNotFound -> AccountState.NotAuthenticated
|
||||
Event.AccountRestored -> AccountState.AuthenticatedNoProfile
|
||||
|
||||
Event.InFlightCopyMigration -> AccountState.CanAutoRetryAuthenticationViaTokenCopy
|
||||
Event.InFlightReuseMigration -> AccountState.CanAutoRetryAuthenticationViaTokenReuse
|
||||
|
||||
else -> null
|
||||
}
|
||||
AccountState.NotAuthenticated -> when (event) {
|
||||
Event.Authenticate -> AccountState.NotAuthenticated
|
||||
Event.FailedToAuthenticate -> AccountState.NotAuthenticated
|
||||
|
||||
is Event.SignInShareableAccount -> AccountState.NotAuthenticated
|
||||
is Event.SignedInShareableAccount -> AccountState.AuthenticatedNoProfile
|
||||
is Event.RetryLaterViaTokenCopy -> AccountState.CanAutoRetryAuthenticationViaTokenCopy
|
||||
is Event.RetryLaterViaTokenReuse -> AccountState.CanAutoRetryAuthenticationViaTokenReuse
|
||||
|
||||
is Event.Pair -> AccountState.NotAuthenticated
|
||||
is Event.Authenticated -> AccountState.AuthenticatedNoProfile
|
||||
else -> null
|
||||
}
|
||||
AccountState.CanAutoRetryAuthenticationViaTokenCopy -> when (event) {
|
||||
Event.RetryMigration -> AccountState.CanAutoRetryAuthenticationViaTokenCopy
|
||||
Event.FailedToAuthenticate -> AccountState.NotAuthenticated
|
||||
is Event.SignedInShareableAccount -> AccountState.AuthenticatedNoProfile
|
||||
Event.RetryLaterViaTokenCopy -> AccountState.CanAutoRetryAuthenticationViaTokenCopy
|
||||
Event.Logout -> AccountState.NotAuthenticated
|
||||
else -> null
|
||||
}
|
||||
AccountState.CanAutoRetryAuthenticationViaTokenReuse -> when (event) {
|
||||
Event.RetryMigration -> AccountState.CanAutoRetryAuthenticationViaTokenReuse
|
||||
Event.FailedToAuthenticate -> AccountState.NotAuthenticated
|
||||
is Event.SignedInShareableAccount -> AccountState.AuthenticatedNoProfile
|
||||
Event.RetryLaterViaTokenReuse -> AccountState.CanAutoRetryAuthenticationViaTokenReuse
|
||||
Event.Logout -> AccountState.NotAuthenticated
|
||||
else -> null
|
||||
}
|
||||
AccountState.AuthenticatedNoProfile -> when (event) {
|
||||
is Event.AuthenticationError -> AccountState.AuthenticationProblem
|
||||
Event.FetchProfile -> AccountState.AuthenticatedNoProfile
|
||||
Event.FetchedProfile -> AccountState.AuthenticatedWithProfile
|
||||
Event.FailedToFetchProfile -> AccountState.AuthenticatedNoProfile
|
||||
Event.FailedToAuthenticate -> AccountState.NotAuthenticated
|
||||
Event.Logout -> AccountState.NotAuthenticated
|
||||
else -> null
|
||||
}
|
||||
AccountState.AuthenticatedWithProfile -> when (event) {
|
||||
is Event.AuthenticationError -> AccountState.AuthenticationProblem
|
||||
Event.Logout -> AccountState.NotAuthenticated
|
||||
else -> null
|
||||
}
|
||||
AccountState.AuthenticationProblem -> when (event) {
|
||||
Event.Authenticate -> AccountState.AuthenticationProblem
|
||||
Event.FailedToAuthenticate -> AccountState.AuthenticationProblem
|
||||
Event.RecoveredFromAuthenticationProblem -> AccountState.AuthenticatedNoProfile
|
||||
is Event.Authenticated -> AccountState.AuthenticatedNoProfile
|
||||
Event.Logout -> AccountState.NotAuthenticated
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
internal sealed class State {
|
||||
data class Idle(val accountState: AccountState) : State()
|
||||
data class Active(val progressState: ProgressState) : State()
|
||||
}
|
||||
|
||||
internal fun State.next(event: Event): State? = when (this) {
|
||||
// Reacting to external events.
|
||||
is State.Idle -> when (this.accountState) {
|
||||
AccountState.NotAuthenticated -> when (event) {
|
||||
Event.Account.Start -> State.Active(ProgressState.Initializing)
|
||||
Event.Account.BeginEmailFlow -> State.Active(ProgressState.BeginningAuthentication)
|
||||
is Event.Account.BeginPairingFlow -> State.Active(ProgressState.BeginningAuthentication)
|
||||
is Event.Account.MigrateFromAccount -> State.Active(ProgressState.MigratingAccount)
|
||||
else -> null
|
||||
}
|
||||
AccountState.IncompleteMigration -> when (event) {
|
||||
is Event.Account.RetryMigration -> State.Active(ProgressState.MigratingAccount)
|
||||
is Event.Account.Logout -> State.Active(ProgressState.LoggingOut)
|
||||
else -> null
|
||||
}
|
||||
AccountState.Authenticated -> when (event) {
|
||||
is Event.Account.AuthenticationError -> State.Active(ProgressState.RecoveringFromAuthProblem)
|
||||
is Event.Account.Logout -> State.Active(ProgressState.LoggingOut)
|
||||
else -> null
|
||||
}
|
||||
AccountState.AuthenticationProblem -> when (event) {
|
||||
is Event.Account.Logout -> State.Active(ProgressState.LoggingOut)
|
||||
is Event.Account.BeginEmailFlow -> State.Active(ProgressState.BeginningAuthentication)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
// Reacting to internal events.
|
||||
is State.Active -> when (this.progressState) {
|
||||
ProgressState.Initializing -> when (event) {
|
||||
Event.Progress.AccountNotFound -> State.Idle(AccountState.NotAuthenticated)
|
||||
Event.Progress.AccountRestored -> State.Active(ProgressState.CompletingAuthentication)
|
||||
is Event.Progress.IncompleteMigration -> State.Active(ProgressState.MigratingAccount)
|
||||
else -> null
|
||||
}
|
||||
ProgressState.BeginningAuthentication -> when (event) {
|
||||
is Event.Progress.AuthData -> State.Active(ProgressState.CompletingAuthentication)
|
||||
Event.Progress.FailedToBeginAuth -> State.Idle(AccountState.NotAuthenticated)
|
||||
else -> null
|
||||
}
|
||||
ProgressState.CompletingAuthentication -> when (event) {
|
||||
is Event.Progress.CompletedAuthentication -> State.Idle(AccountState.Authenticated)
|
||||
is Event.Account.AuthenticationError -> State.Active(ProgressState.RecoveringFromAuthProblem)
|
||||
Event.Progress.FailedToCompleteAuthRestore -> State.Idle(AccountState.NotAuthenticated)
|
||||
Event.Progress.FailedToCompleteAuth -> State.Idle(AccountState.NotAuthenticated)
|
||||
else -> null
|
||||
}
|
||||
ProgressState.MigratingAccount -> when (event) {
|
||||
is Event.Progress.Migrated -> State.Active(ProgressState.CompletingAuthentication)
|
||||
Event.Progress.FailedToCompleteMigration -> State.Idle(AccountState.NotAuthenticated)
|
||||
is Event.Progress.IncompleteMigration -> State.Idle(AccountState.IncompleteMigration)
|
||||
else -> null
|
||||
}
|
||||
ProgressState.RecoveringFromAuthProblem -> when (event) {
|
||||
Event.Progress.RecoveredFromAuthenticationProblem -> State.Idle(AccountState.Authenticated)
|
||||
Event.Progress.FailedToRecoverFromAuthenticationProblem -> State.Idle(AccountState.AuthenticationProblem)
|
||||
else -> null
|
||||
}
|
||||
ProgressState.LoggingOut -> when (event) {
|
||||
Event.Progress.LoggedOut -> State.Idle(AccountState.NotAuthenticated)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import mozilla.components.concept.sync.MigratingAccountInfo
|
||||
import mozilla.components.support.ktx.kotlin.toHexString
|
||||
import mozilla.components.support.ktx.kotlin.toSha256Digest
|
||||
|
||||
@@ -18,16 +19,7 @@ import mozilla.components.support.ktx.kotlin.toSha256Digest
|
||||
data class ShareableAccount(
|
||||
val email: String,
|
||||
val sourcePackage: String,
|
||||
val authInfo: ShareableAuthInfo
|
||||
)
|
||||
|
||||
/**
|
||||
* Data structure describing FxA and Sync credentials necessary to share an FxA account.
|
||||
*/
|
||||
data class ShareableAuthInfo(
|
||||
val sessionToken: String,
|
||||
val kSync: String,
|
||||
val kXCS: String
|
||||
val authInfo: MigratingAccountInfo
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -109,7 +101,7 @@ object AccountSharing {
|
||||
ShareableAccount(
|
||||
email = email,
|
||||
sourcePackage = packageName,
|
||||
authInfo = ShareableAuthInfo(sessionToken, kSync, kXSCS)
|
||||
authInfo = MigratingAccountInfo(sessionToken, kSync, kXSCS)
|
||||
)
|
||||
} else {
|
||||
null
|
||||
|
||||
@@ -97,7 +97,7 @@ object GlobalSyncableStoreProvider {
|
||||
internal interface SyncDispatcher : Closeable, Observable<SyncStatusObserver> {
|
||||
fun isSyncActive(): Boolean
|
||||
fun syncNow(reason: SyncReason, debounce: Boolean = false)
|
||||
fun startPeriodicSync(unit: TimeUnit, period: Long)
|
||||
fun startPeriodicSync(unit: TimeUnit, period: Long, initialDelay: Long)
|
||||
fun stopPeriodicSync()
|
||||
fun workersStateChanged(isRunning: Boolean)
|
||||
}
|
||||
@@ -107,31 +107,9 @@ internal interface SyncDispatcher : Closeable, Observable<SyncStatusObserver> {
|
||||
* @param syncConfig A [SyncConfig] object describing how sync should behave.
|
||||
*/
|
||||
@SuppressWarnings("TooManyFunctions")
|
||||
abstract class SyncManager(
|
||||
internal abstract class SyncManager(
|
||||
private val syncConfig: SyncConfig
|
||||
) {
|
||||
companion object {
|
||||
// Periodically sync in the background, to make our syncs a little more incremental.
|
||||
// This isn't strictly necessary, and could be considered an optimization.
|
||||
//
|
||||
// Assuming that we synchronize during app startup, our trade-offs are:
|
||||
// - not syncing in the background at all might mean longer syncs, more arduous startup syncs
|
||||
// - on a slow mobile network, these delays could be significant
|
||||
// - a delay during startup sync may affect user experience, since our data will be stale
|
||||
// for longer
|
||||
// - however, background syncing eats up some of the device resources
|
||||
// - ... so we only do so a few times per day
|
||||
// - we also rely on the OS and the WorkManager APIs to minimize those costs. It promises to
|
||||
// bundle together tasks from different applications that have similar resource-consumption
|
||||
// profiles. Specifically, we need device radio to be ON to talk to our servers; OS will run
|
||||
// our periodic syncs bundled with another tasks that also need radio to be ON, thus "spreading"
|
||||
// the overall cost.
|
||||
//
|
||||
// If we wanted to be very fancy, this period could be driven by how much new activity an
|
||||
// account is actually expected to generate. For now, it's just a hard-coded constant.
|
||||
val SYNC_PERIOD_UNIT = TimeUnit.MINUTES
|
||||
}
|
||||
|
||||
open val logger = Logger("SyncManager")
|
||||
|
||||
// A SyncDispatcher instance bound to an account and a set of syncable stores.
|
||||
@@ -164,18 +142,15 @@ abstract class SyncManager(
|
||||
if (syncDispatcher == null) {
|
||||
logger.info("Sync is not enabled. Ignoring 'sync now' request.")
|
||||
}
|
||||
syncDispatcher?.let {
|
||||
logger.debug("Requesting immediate sync, reason: $reason, debounce: $debounce")
|
||||
it.syncNow(reason, debounce)
|
||||
}
|
||||
syncDispatcher?.syncNow(reason, debounce)
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables synchronization, with behaviour described in [syncConfig].
|
||||
*/
|
||||
internal fun start(reason: SyncReason) = synchronized(this) {
|
||||
internal fun start() = synchronized(this) {
|
||||
logger.debug("Enabling...")
|
||||
syncDispatcher = initDispatcher(newDispatcher(syncDispatcher, syncConfig.supportedEngines), reason)
|
||||
syncDispatcher = initDispatcher(newDispatcher(syncDispatcher, syncConfig.supportedEngines))
|
||||
logger.debug("set and initialized new dispatcher: $syncDispatcher")
|
||||
}
|
||||
|
||||
@@ -184,8 +159,6 @@ abstract class SyncManager(
|
||||
*/
|
||||
internal fun stop() = synchronized(this) {
|
||||
logger.debug("Disabling...")
|
||||
syncDispatcher?.unregister(dispatcherStatusObserver)
|
||||
syncDispatcher?.stopPeriodicSync()
|
||||
syncDispatcher?.close()
|
||||
syncDispatcher = null
|
||||
}
|
||||
@@ -208,11 +181,14 @@ abstract class SyncManager(
|
||||
return createDispatcher(supportedEngines)
|
||||
}
|
||||
|
||||
private fun initDispatcher(dispatcher: SyncDispatcher, reason: SyncReason): SyncDispatcher {
|
||||
private fun initDispatcher(dispatcher: SyncDispatcher): SyncDispatcher {
|
||||
dispatcher.register(dispatcherStatusObserver)
|
||||
dispatcher.syncNow(reason)
|
||||
if (syncConfig.syncPeriodInMinutes != null) {
|
||||
dispatcher.startPeriodicSync(SYNC_PERIOD_UNIT, syncConfig.syncPeriodInMinutes)
|
||||
syncConfig.periodicSyncConfig?.let {
|
||||
dispatcher.startPeriodicSync(
|
||||
TimeUnit.MINUTES,
|
||||
period = it.periodMinutes.toLong(),
|
||||
initialDelay = it.initialDelayMinutes.toLong()
|
||||
)
|
||||
}
|
||||
dispatcherUpdated(dispatcher)
|
||||
return dispatcher
|
||||
|
||||
@@ -67,10 +67,10 @@ internal class WorkManagerSyncManager(
|
||||
init {
|
||||
WorkersLiveDataObserver.init(context)
|
||||
|
||||
if (syncConfig.syncPeriodInMinutes == null) {
|
||||
if (syncConfig.periodicSyncConfig == null) {
|
||||
logger.info("Periodic syncing is disabled.")
|
||||
} else {
|
||||
logger.info("Periodic syncing enabled at a ${syncConfig.syncPeriodInMinutes} interval")
|
||||
logger.info("Periodic syncing enabled: ${syncConfig.periodicSyncConfig}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ internal object WorkersLiveDataObserver {
|
||||
}
|
||||
}
|
||||
|
||||
class WorkManagerSyncDispatcher(
|
||||
internal class WorkManagerSyncDispatcher(
|
||||
private val context: Context,
|
||||
private val supportedEngines: Set<SyncEngine>
|
||||
) : SyncDispatcher, Observable<SyncStatusObserver> by ObserverRegistry(), Closeable {
|
||||
@@ -178,6 +178,7 @@ class WorkManagerSyncDispatcher(
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
unregisterObservers()
|
||||
stopPeriodicSync()
|
||||
}
|
||||
|
||||
@@ -185,14 +186,14 @@ class WorkManagerSyncDispatcher(
|
||||
* Periodic background syncing is mainly intended to reduce workload when we sync during
|
||||
* application startup.
|
||||
*/
|
||||
override fun startPeriodicSync(unit: TimeUnit, period: Long) {
|
||||
override fun startPeriodicSync(unit: TimeUnit, period: Long, initialDelay: Long) {
|
||||
logger.debug("Starting periodic syncing, period = $period, time unit = $unit")
|
||||
// Use the 'replace' policy as a simple way to upgrade periodic worker configurations across
|
||||
// application versions. We do this instead of versioning workers.
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
||||
SyncWorkerName.Periodic.name,
|
||||
ExistingPeriodicWorkPolicy.REPLACE,
|
||||
periodicSyncWorkRequest(unit, period)
|
||||
periodicSyncWorkRequest(unit, period, initialDelay)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -205,11 +206,11 @@ class WorkManagerSyncDispatcher(
|
||||
WorkManager.getInstance(context).cancelUniqueWork(SyncWorkerName.Periodic.name)
|
||||
}
|
||||
|
||||
private fun periodicSyncWorkRequest(unit: TimeUnit, period: Long): PeriodicWorkRequest {
|
||||
private fun periodicSyncWorkRequest(unit: TimeUnit, period: Long, initialDelay: Long): PeriodicWorkRequest {
|
||||
val data = getWorkerData(SyncReason.Scheduled)
|
||||
// Periodic interval must be at least PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS,
|
||||
// e.g. not more frequently than 15 minutes.
|
||||
return PeriodicWorkRequestBuilder<WorkManagerSyncWorker>(period, unit)
|
||||
return PeriodicWorkRequestBuilder<WorkManagerSyncWorker>(period, unit, initialDelay, unit)
|
||||
.setConstraints(
|
||||
Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
@@ -258,7 +259,7 @@ class WorkManagerSyncDispatcher(
|
||||
}
|
||||
}
|
||||
|
||||
class WorkManagerSyncWorker(
|
||||
internal class WorkManagerSyncWorker(
|
||||
private val context: Context,
|
||||
private val params: WorkerParameters
|
||||
) : CoroutineWorker(context, params) {
|
||||
@@ -467,7 +468,7 @@ private const val SYNC_STATE_PREFS_KEY = "syncPrefs"
|
||||
private const val SYNC_LAST_SYNCED_KEY = "lastSynced"
|
||||
private const val SYNC_STATE_KEY = "persistedState"
|
||||
|
||||
private const val SYNC_STAGGER_BUFFER_MS = 10 * 60 * 1000L // 10 minutes.
|
||||
private const val SYNC_STAGGER_BUFFER_MS = 5 * 60 * 1000L // 5 minutes.
|
||||
private const val SYNC_STARTUP_DELAY_MS = 5 * 1000L // 5 seconds.
|
||||
|
||||
fun getLastSynced(context: Context): Long {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,8 @@ import mozilla.components.concept.sync.DeviceCommandIncoming
|
||||
import mozilla.components.concept.sync.DeviceCommandOutgoing
|
||||
import mozilla.components.concept.sync.AccountEventsObserver
|
||||
import mozilla.components.concept.sync.AccountEvent
|
||||
import mozilla.components.concept.sync.AuthType
|
||||
import mozilla.components.concept.sync.DeviceConfig
|
||||
import mozilla.components.concept.sync.DevicePushSubscription
|
||||
import mozilla.components.concept.sync.DeviceType
|
||||
import mozilla.components.concept.sync.TabData
|
||||
@@ -38,8 +40,10 @@ import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.Mockito.reset
|
||||
import org.mockito.Mockito.times
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.verifyZeroInteractions
|
||||
import mozilla.appservices.fxaclient.Device as NativeDevice
|
||||
import mozilla.appservices.fxaclient.FirefoxAccount as NativeFirefoxAccount
|
||||
import mozilla.appservices.syncmanager.DeviceType as RustDeviceType
|
||||
@@ -61,21 +65,44 @@ class FxaDeviceConstellationTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initializing device`() = runBlocking(coroutinesTestRule.testDispatcher) {
|
||||
constellation.initDeviceAsync("test name", DeviceType.TABLET, setOf()).await()
|
||||
verify(account).initializeDevice("test name", NativeDevice.Type.TABLET, setOf())
|
||||
|
||||
constellation.initDeviceAsync("VR device", DeviceType.VR, setOf(DeviceCapability.SEND_TAB)).await()
|
||||
verify(account).initializeDevice("VR device", NativeDevice.Type.VR, setOf(mozilla.appservices.fxaclient.Device.Capability.SEND_TAB))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure capabilities`() = runBlocking(coroutinesTestRule.testDispatcher) {
|
||||
constellation.ensureCapabilitiesAsync(setOf()).await()
|
||||
verify(account).ensureCapabilities(setOf())
|
||||
|
||||
constellation.ensureCapabilitiesAsync(setOf(DeviceCapability.SEND_TAB)).await()
|
||||
verify(account).ensureCapabilities(setOf(mozilla.appservices.fxaclient.Device.Capability.SEND_TAB))
|
||||
fun `finalize device`() = runBlocking(coroutinesTestRule.testDispatcher) {
|
||||
fun expectedFinalizeAction(authType: AuthType): FxaDeviceConstellation.DeviceFinalizeAction = when (authType) {
|
||||
AuthType.Existing -> FxaDeviceConstellation.DeviceFinalizeAction.EnsureCapabilities
|
||||
AuthType.Signin -> FxaDeviceConstellation.DeviceFinalizeAction.Initialize
|
||||
AuthType.Signup -> FxaDeviceConstellation.DeviceFinalizeAction.Initialize
|
||||
AuthType.Pairing -> FxaDeviceConstellation.DeviceFinalizeAction.Initialize
|
||||
is AuthType.OtherExternal -> FxaDeviceConstellation.DeviceFinalizeAction.Initialize
|
||||
AuthType.MigratedCopy -> FxaDeviceConstellation.DeviceFinalizeAction.Initialize
|
||||
AuthType.MigratedReuse -> FxaDeviceConstellation.DeviceFinalizeAction.EnsureCapabilities
|
||||
AuthType.Recovered -> FxaDeviceConstellation.DeviceFinalizeAction.None
|
||||
}
|
||||
fun initAuthType(simpleClassName: String): AuthType = when (simpleClassName) {
|
||||
"Existing" -> AuthType.Existing
|
||||
"Signin" -> AuthType.Signin
|
||||
"Signup" -> AuthType.Signup
|
||||
"Pairing" -> AuthType.Pairing
|
||||
"OtherExternal" -> AuthType.OtherExternal("test")
|
||||
"MigratedCopy" -> AuthType.MigratedCopy
|
||||
"MigratedReuse" -> AuthType.MigratedReuse
|
||||
"Recovered" -> AuthType.Recovered
|
||||
else -> throw AssertionError("Unknown AuthType: $simpleClassName")
|
||||
}
|
||||
val config = DeviceConfig("test name", DeviceType.TABLET, setOf(DeviceCapability.SEND_TAB))
|
||||
AuthType::class.sealedSubclasses.map { initAuthType(it.simpleName!!) }.forEach {
|
||||
constellation.finalizeDevice(it, config)
|
||||
when (expectedFinalizeAction(it)) {
|
||||
FxaDeviceConstellation.DeviceFinalizeAction.Initialize -> {
|
||||
verify(account).initializeDevice("test name", NativeDevice.Type.TABLET, setOf(mozilla.appservices.fxaclient.Device.Capability.SEND_TAB))
|
||||
}
|
||||
FxaDeviceConstellation.DeviceFinalizeAction.EnsureCapabilities -> {
|
||||
verify(account).ensureCapabilities(setOf(mozilla.appservices.fxaclient.Device.Capability.SEND_TAB))
|
||||
}
|
||||
FxaDeviceConstellation.DeviceFinalizeAction.None -> {
|
||||
verifyZeroInteractions(account)
|
||||
}
|
||||
}
|
||||
reset(account)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -86,7 +113,7 @@ class FxaDeviceConstellationTest {
|
||||
|
||||
// Can't update cached value in an empty cache
|
||||
try {
|
||||
constellation.setDeviceNameAsync("new name", testContext).await()
|
||||
constellation.setDeviceName("new name", testContext)
|
||||
fail()
|
||||
} catch (e: IllegalStateException) {}
|
||||
|
||||
@@ -94,7 +121,7 @@ class FxaDeviceConstellationTest {
|
||||
cache.setToCache(DeviceSettings("someId", "test name", RustDeviceType.MOBILE))
|
||||
|
||||
// No device state observer.
|
||||
assertTrue(constellation.setDeviceNameAsync("new name", testContext).await())
|
||||
assertTrue(constellation.setDeviceName("new name", testContext))
|
||||
verify(account, times(2)).setDeviceDisplayName("new name")
|
||||
|
||||
assertEquals(DeviceSettings("someId", "new name", RustDeviceType.MOBILE), cache.getCached())
|
||||
@@ -109,7 +136,7 @@ class FxaDeviceConstellationTest {
|
||||
}
|
||||
constellation.registerDeviceObserver(observer, startedLifecycleOwner(), false)
|
||||
|
||||
assertTrue(constellation.setDeviceNameAsync("another name", testContext).await())
|
||||
assertTrue(constellation.setDeviceName("another name", testContext))
|
||||
verify(account).setDeviceDisplayName("another name")
|
||||
|
||||
assertEquals(DeviceSettings("someId", "another name", RustDeviceType.MOBILE), cache.getCached())
|
||||
@@ -123,7 +150,7 @@ class FxaDeviceConstellationTest {
|
||||
@ExperimentalCoroutinesApi
|
||||
fun `set device push subscription`() = runBlocking(coroutinesTestRule.testDispatcher) {
|
||||
val subscription = DevicePushSubscription("http://endpoint.com", "pk", "auth key")
|
||||
constellation.setDevicePushSubscriptionAsync(subscription).await()
|
||||
constellation.setDevicePushSubscription(subscription)
|
||||
|
||||
verify(account).setDevicePushSubscription("http://endpoint.com", "pk", "auth key")
|
||||
}
|
||||
@@ -133,7 +160,7 @@ class FxaDeviceConstellationTest {
|
||||
fun `process raw device command`() = runBlocking(coroutinesTestRule.testDispatcher) {
|
||||
// No commands, no observer.
|
||||
`when`(account.handlePushMessage("raw events payload")).thenReturn(emptyArray())
|
||||
assertTrue(constellation.processRawEventAsync("raw events payload").await())
|
||||
assertTrue(constellation.processRawEvent("raw events payload"))
|
||||
|
||||
// No commands, with observer.
|
||||
val eventsObserver = object : AccountEventsObserver {
|
||||
@@ -146,7 +173,7 @@ class FxaDeviceConstellationTest {
|
||||
|
||||
// No commands, with an observer.
|
||||
constellation.register(eventsObserver)
|
||||
assertTrue(constellation.processRawEventAsync("raw events payload").await())
|
||||
assertTrue(constellation.processRawEvent("raw events payload"))
|
||||
assertEquals(listOf<AccountEvent.DeviceCommandIncoming>(), eventsObserver.latestEvents)
|
||||
|
||||
// Some commands, with an observer. More detailed command handling tests below.
|
||||
@@ -157,7 +184,7 @@ class FxaDeviceConstellationTest {
|
||||
command = IncomingDeviceCommand.TabReceived(testDevice1, arrayOf(testTab1))
|
||||
)
|
||||
))
|
||||
assertTrue(constellation.processRawEventAsync("raw events payload").await())
|
||||
assertTrue(constellation.processRawEvent("raw events payload"))
|
||||
|
||||
val events = eventsObserver.latestEvents!!
|
||||
val command = (events[0] as AccountEvent.DeviceCommandIncoming).command
|
||||
@@ -168,9 +195,9 @@ class FxaDeviceConstellationTest {
|
||||
@Test
|
||||
fun `send command to device`() = runBlocking(coroutinesTestRule.testDispatcher) {
|
||||
`when`(account.gatherTelemetry()).thenReturn("{}")
|
||||
assertTrue(constellation.sendCommandToDeviceAsync(
|
||||
assertTrue(constellation.sendCommandToDevice(
|
||||
"targetID", DeviceCommandOutgoing.SendTab("Mozilla", "https://www.mozilla.org")
|
||||
).await())
|
||||
))
|
||||
|
||||
verify(account).sendSingleTab("targetID", "Mozilla", "https://www.mozilla.org")
|
||||
}
|
||||
@@ -181,7 +208,7 @@ class FxaDeviceConstellationTest {
|
||||
// No devices, no observers.
|
||||
`when`(account.getDevices()).thenReturn(emptyArray())
|
||||
|
||||
constellation.refreshDevicesAsync().await()
|
||||
constellation.refreshDevices()
|
||||
|
||||
val observer = object : DeviceConstellationObserver {
|
||||
var state: ConstellationState? = null
|
||||
@@ -193,7 +220,7 @@ class FxaDeviceConstellationTest {
|
||||
constellation.registerDeviceObserver(observer, startedLifecycleOwner(), false)
|
||||
|
||||
// No devices, with an observer.
|
||||
constellation.refreshDevicesAsync().await()
|
||||
constellation.refreshDevices()
|
||||
assertEquals(ConstellationState(null, listOf()), observer.state)
|
||||
|
||||
val testDevice1 = testDevice("test1", false)
|
||||
@@ -202,14 +229,14 @@ class FxaDeviceConstellationTest {
|
||||
|
||||
// Single device, no current device.
|
||||
`when`(account.getDevices()).thenReturn(arrayOf(testDevice1))
|
||||
constellation.refreshDevicesAsync().await()
|
||||
constellation.refreshDevices()
|
||||
|
||||
assertEquals(ConstellationState(null, listOf(testDevice1.into())), observer.state)
|
||||
assertEquals(ConstellationState(null, listOf(testDevice1.into())), constellation.state())
|
||||
|
||||
// Current device, no other devices.
|
||||
`when`(account.getDevices()).thenReturn(arrayOf(currentDevice))
|
||||
constellation.refreshDevicesAsync().await()
|
||||
constellation.refreshDevices()
|
||||
assertEquals(ConstellationState(currentDevice.into(), listOf()), observer.state)
|
||||
assertEquals(ConstellationState(currentDevice.into(), listOf()), constellation.state())
|
||||
|
||||
@@ -217,7 +244,7 @@ class FxaDeviceConstellationTest {
|
||||
`when`(account.getDevices()).thenReturn(arrayOf(
|
||||
currentDevice, testDevice1, testDevice2
|
||||
))
|
||||
constellation.refreshDevicesAsync().await()
|
||||
constellation.refreshDevices()
|
||||
|
||||
assertEquals(ConstellationState(currentDevice.into(), listOf(testDevice1.into(), testDevice2.into())), observer.state)
|
||||
assertEquals(ConstellationState(currentDevice.into(), listOf(testDevice1.into(), testDevice2.into())), constellation.state())
|
||||
@@ -227,7 +254,7 @@ class FxaDeviceConstellationTest {
|
||||
`when`(account.getDevices()).thenReturn(arrayOf(
|
||||
currentDeviceExpired, testDevice2
|
||||
))
|
||||
constellation.refreshDevicesAsync().await()
|
||||
constellation.refreshDevices()
|
||||
|
||||
assertEquals(ConstellationState(currentDeviceExpired.into(), listOf(testDevice2.into())), observer.state)
|
||||
assertEquals(ConstellationState(currentDeviceExpired.into(), listOf(testDevice2.into())), constellation.state())
|
||||
@@ -239,7 +266,7 @@ class FxaDeviceConstellationTest {
|
||||
// No commands, no observers.
|
||||
`when`(account.gatherTelemetry()).thenReturn("{}")
|
||||
`when`(account.pollDeviceCommands()).thenReturn(emptyArray())
|
||||
assertTrue(constellation.pollForCommandsAsync().await())
|
||||
assertTrue(constellation.pollForCommands())
|
||||
|
||||
val eventsObserver = object : AccountEventsObserver {
|
||||
var latestEvents: List<AccountEvent>? = null
|
||||
@@ -251,14 +278,14 @@ class FxaDeviceConstellationTest {
|
||||
|
||||
// No commands, with an observer.
|
||||
constellation.register(eventsObserver)
|
||||
assertTrue(constellation.pollForCommandsAsync().await())
|
||||
assertTrue(constellation.pollForCommands())
|
||||
assertEquals(listOf<AccountEvent>(), eventsObserver.latestEvents)
|
||||
|
||||
// Some commands.
|
||||
`when`(account.pollDeviceCommands()).thenReturn(arrayOf(
|
||||
IncomingDeviceCommand.TabReceived(null, emptyArray())
|
||||
))
|
||||
assertTrue(constellation.pollForCommandsAsync().await())
|
||||
assertTrue(constellation.pollForCommands())
|
||||
|
||||
var command = (eventsObserver.latestEvents!![0] as AccountEvent.DeviceCommandIncoming).command
|
||||
assertEquals(null, (command as DeviceCommandIncoming.TabReceived).from)
|
||||
@@ -274,7 +301,7 @@ class FxaDeviceConstellationTest {
|
||||
`when`(account.pollDeviceCommands()).thenReturn(arrayOf(
|
||||
IncomingDeviceCommand.TabReceived(testDevice1, emptyArray())
|
||||
))
|
||||
assertTrue(constellation.pollForCommandsAsync().await())
|
||||
assertTrue(constellation.pollForCommands())
|
||||
|
||||
Assert.assertNotNull(eventsObserver.latestEvents)
|
||||
assertEquals(1, eventsObserver.latestEvents!!.size)
|
||||
@@ -286,7 +313,7 @@ class FxaDeviceConstellationTest {
|
||||
`when`(account.pollDeviceCommands()).thenReturn(arrayOf(
|
||||
IncomingDeviceCommand.TabReceived(testDevice2, arrayOf(testTab1))
|
||||
))
|
||||
assertTrue(constellation.pollForCommandsAsync().await())
|
||||
assertTrue(constellation.pollForCommands())
|
||||
|
||||
command = (eventsObserver.latestEvents!![0] as AccountEvent.DeviceCommandIncoming).command
|
||||
assertEquals(testDevice2.into(), (command as DeviceCommandIncoming.TabReceived).from)
|
||||
@@ -296,7 +323,7 @@ class FxaDeviceConstellationTest {
|
||||
`when`(account.pollDeviceCommands()).thenReturn(arrayOf(
|
||||
IncomingDeviceCommand.TabReceived(testDevice2, arrayOf(testTab1, testTab3))
|
||||
))
|
||||
assertTrue(constellation.pollForCommandsAsync().await())
|
||||
assertTrue(constellation.pollForCommands())
|
||||
|
||||
command = (eventsObserver.latestEvents!![0] as AccountEvent.DeviceCommandIncoming).command
|
||||
assertEquals(testDevice2.into(), (command as DeviceCommandIncoming.TabReceived).from)
|
||||
@@ -307,7 +334,7 @@ class FxaDeviceConstellationTest {
|
||||
IncomingDeviceCommand.TabReceived(testDevice2, arrayOf(testTab1, testTab2)),
|
||||
IncomingDeviceCommand.TabReceived(testDevice1, arrayOf(testTab3))
|
||||
))
|
||||
assertTrue(constellation.pollForCommandsAsync().await())
|
||||
assertTrue(constellation.pollForCommands())
|
||||
|
||||
command = (eventsObserver.latestEvents!![0] as AccountEvent.DeviceCommandIncoming).command
|
||||
assertEquals(testDevice2.into(), (command as DeviceCommandIncoming.TabReceived).from)
|
||||
@@ -321,7 +348,7 @@ class FxaDeviceConstellationTest {
|
||||
// `when`(account.pollDeviceCommands()).thenThrow(FxaPanicException("Don't panic!"))
|
||||
// try {
|
||||
// runBlocking(coroutinesTestRule.testDispatcher) {
|
||||
// constellation.refreshAsync().await()
|
||||
// constellation.refreshAsync()
|
||||
// }
|
||||
// fail()
|
||||
// } catch (e: FxaPanicException) {}
|
||||
@@ -329,12 +356,12 @@ class FxaDeviceConstellationTest {
|
||||
// // Network exception are handled.
|
||||
// `when`(account.pollDeviceCommands()).thenThrow(FxaNetworkException("four oh four"))
|
||||
// runBlocking(coroutinesTestRule.testDispatcher) {
|
||||
// Assert.assertFalse(constellation.refreshAsync().await())
|
||||
// Assert.assertFalse(constellation.refreshAsync())
|
||||
// }
|
||||
// // Unspecified exception are handled.
|
||||
// `when`(account.pollDeviceCommands()).thenThrow(FxaUnspecifiedException("hmmm..."))
|
||||
// runBlocking(coroutinesTestRule.testDispatcher) {
|
||||
// Assert.assertFalse(constellation.refreshAsync().await())
|
||||
// Assert.assertFalse(constellation.refreshAsync())
|
||||
// }
|
||||
// // Unauthorized exception are handled.
|
||||
// val authErrorObserver = object : AuthErrorObserver {
|
||||
@@ -352,7 +379,7 @@ class FxaDeviceConstellationTest {
|
||||
// val authException = FxaUnauthorizedException("oh no you didn't!")
|
||||
// `when`(account.pollDeviceCommands()).thenThrow(authException)
|
||||
// runBlocking(coroutinesTestRule.testDispatcher) {
|
||||
// Assert.assertFalse(constellation.refreshAsync().await())
|
||||
// Assert.assertFalse(constellation.refreshAsync())
|
||||
// }
|
||||
// assertEquals(authErrorObserver.latestException!!.cause, authException)
|
||||
}
|
||||
|
||||
@@ -1,166 +0,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/. */
|
||||
|
||||
package mozilla.components.service.fxa.manager
|
||||
|
||||
import mozilla.components.concept.sync.AuthType
|
||||
import mozilla.components.service.fxa.FxaAuthData
|
||||
import mozilla.components.support.test.mock
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
class FxaStateMatrixTest {
|
||||
@Test
|
||||
fun `state transitions`() {
|
||||
// State 'Start'.
|
||||
var state = AccountState.Start
|
||||
|
||||
assertEquals(AccountState.Start, FxaStateMatrix.nextState(state, Event.Init))
|
||||
assertEquals(AccountState.NotAuthenticated, FxaStateMatrix.nextState(state, Event.AccountNotFound))
|
||||
assertEquals(AccountState.AuthenticatedNoProfile, FxaStateMatrix.nextState(state, Event.AccountRestored))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.Authenticate))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.Authenticated(FxaAuthData(AuthType.Signin, "code", "state"))))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.FetchProfile))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.FetchedProfile))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.FailedToFetchProfile))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.FailedToAuthenticate))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.Logout))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.AuthenticationError("test")))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.SignInShareableAccount(mock(), false)))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.SignedInShareableAccount(false)))
|
||||
assertEquals(AccountState.CanAutoRetryAuthenticationViaTokenReuse, FxaStateMatrix.nextState(state, Event.InFlightReuseMigration))
|
||||
assertEquals(AccountState.CanAutoRetryAuthenticationViaTokenCopy, FxaStateMatrix.nextState(state, Event.InFlightCopyMigration))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.RetryLaterViaTokenReuse))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.RetryLaterViaTokenCopy))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.RetryMigration))
|
||||
|
||||
// State 'NotAuthenticated'.
|
||||
state = AccountState.NotAuthenticated
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.Init))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.AccountNotFound))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.AccountRestored))
|
||||
assertEquals(AccountState.NotAuthenticated, FxaStateMatrix.nextState(state, Event.Authenticate))
|
||||
assertEquals(AccountState.NotAuthenticated, FxaStateMatrix.nextState(state, Event.Pair("auth://pair")))
|
||||
assertEquals(AccountState.AuthenticatedNoProfile, FxaStateMatrix.nextState(state, Event.Authenticated(FxaAuthData(AuthType.Signup, "code", "state"))))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.FetchProfile))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.FetchedProfile))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.FailedToFetchProfile))
|
||||
assertEquals(AccountState.NotAuthenticated, FxaStateMatrix.nextState(state, Event.FailedToAuthenticate))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.Logout))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.AuthenticationError("test")))
|
||||
assertEquals(AccountState.NotAuthenticated, FxaStateMatrix.nextState(state, Event.SignInShareableAccount(mock(), false)))
|
||||
assertEquals(AccountState.AuthenticatedNoProfile, FxaStateMatrix.nextState(state, Event.SignedInShareableAccount(false)))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.InFlightReuseMigration))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.InFlightCopyMigration))
|
||||
assertEquals(AccountState.CanAutoRetryAuthenticationViaTokenReuse, FxaStateMatrix.nextState(state, Event.RetryLaterViaTokenReuse))
|
||||
assertEquals(AccountState.CanAutoRetryAuthenticationViaTokenCopy, FxaStateMatrix.nextState(state, Event.RetryLaterViaTokenCopy))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.RetryMigration))
|
||||
|
||||
// State 'AuthenticatedNoProfile'.
|
||||
state = AccountState.AuthenticatedNoProfile
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.Init))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.AccountNotFound))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.AccountRestored))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.Authenticate))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.Authenticated(FxaAuthData(AuthType.Pairing, "code", "state"))))
|
||||
assertEquals(AccountState.AuthenticatedNoProfile, FxaStateMatrix.nextState(state, Event.FetchProfile))
|
||||
assertEquals(AccountState.AuthenticatedWithProfile, FxaStateMatrix.nextState(state, Event.FetchedProfile))
|
||||
assertEquals(AccountState.AuthenticatedNoProfile, FxaStateMatrix.nextState(state, Event.FailedToFetchProfile))
|
||||
assertEquals(AccountState.NotAuthenticated, FxaStateMatrix.nextState(state, Event.FailedToAuthenticate))
|
||||
assertEquals(AccountState.NotAuthenticated, FxaStateMatrix.nextState(state, Event.Logout))
|
||||
assertEquals(AccountState.AuthenticationProblem, FxaStateMatrix.nextState(state, Event.AuthenticationError("test")))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.SignInShareableAccount(mock(), false)))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.SignedInShareableAccount(false)))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.InFlightReuseMigration))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.InFlightCopyMigration))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.RetryLaterViaTokenReuse))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.RetryLaterViaTokenCopy))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.RetryMigration))
|
||||
|
||||
// State 'AuthenticatedWithProfile'.
|
||||
state = AccountState.AuthenticatedWithProfile
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.Init))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.AccountNotFound))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.AccountRestored))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.Authenticate))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.Authenticated(FxaAuthData(AuthType.OtherExternal("oi"), "code", "state"))))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.FetchProfile))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.FetchedProfile))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.FailedToFetchProfile))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.FailedToAuthenticate))
|
||||
assertEquals(AccountState.NotAuthenticated, FxaStateMatrix.nextState(state, Event.Logout))
|
||||
assertEquals(AccountState.AuthenticationProblem, FxaStateMatrix.nextState(state, Event.AuthenticationError("test")))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.SignInShareableAccount(mock(), false)))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.SignedInShareableAccount(false)))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.InFlightReuseMigration))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.InFlightCopyMigration))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.RetryLaterViaTokenReuse))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.RetryLaterViaTokenCopy))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.RetryMigration))
|
||||
|
||||
// State 'AuthenticationProblem'.
|
||||
state = AccountState.AuthenticationProblem
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.Init))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.AccountNotFound))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.AccountRestored))
|
||||
assertEquals(AccountState.AuthenticationProblem, FxaStateMatrix.nextState(state, Event.Authenticate))
|
||||
assertEquals(AccountState.AuthenticatedNoProfile, FxaStateMatrix.nextState(state, Event.Authenticated(FxaAuthData(AuthType.Signin, "code", "state"))))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.FetchProfile))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.FetchedProfile))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.FailedToFetchProfile))
|
||||
assertEquals(AccountState.AuthenticationProblem, FxaStateMatrix.nextState(state, Event.FailedToAuthenticate))
|
||||
assertEquals(AccountState.NotAuthenticated, FxaStateMatrix.nextState(state, Event.Logout))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.AuthenticationError("test")))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.SignInShareableAccount(mock(), false)))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.SignedInShareableAccount(false)))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.InFlightReuseMigration))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.InFlightCopyMigration))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.RetryLaterViaTokenReuse))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.RetryLaterViaTokenCopy))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.RetryMigration))
|
||||
|
||||
// State 'CanAutoRetryAuthenticationViaTokenReuse'.
|
||||
state = AccountState.CanAutoRetryAuthenticationViaTokenReuse
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.Init))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.AccountNotFound))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.AccountRestored))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.Authenticate))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.Authenticated(FxaAuthData(AuthType.Signin, "code", "state"))))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.FetchProfile))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.FetchedProfile))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.FailedToFetchProfile))
|
||||
assertEquals(AccountState.NotAuthenticated, FxaStateMatrix.nextState(state, Event.FailedToAuthenticate))
|
||||
assertEquals(AccountState.NotAuthenticated, FxaStateMatrix.nextState(state, Event.Logout))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.AuthenticationError("test")))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.SignInShareableAccount(mock(), false)))
|
||||
assertEquals(AccountState.AuthenticatedNoProfile, FxaStateMatrix.nextState(state, Event.SignedInShareableAccount(false)))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.InFlightReuseMigration))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.InFlightCopyMigration))
|
||||
assertEquals(AccountState.CanAutoRetryAuthenticationViaTokenReuse, FxaStateMatrix.nextState(state, Event.RetryLaterViaTokenReuse))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.RetryLaterViaTokenCopy))
|
||||
assertEquals(AccountState.CanAutoRetryAuthenticationViaTokenReuse, FxaStateMatrix.nextState(state, Event.RetryMigration))
|
||||
|
||||
// State 'CanAutoRetryAuthenticationViaTokenCopy'.
|
||||
state = AccountState.CanAutoRetryAuthenticationViaTokenCopy
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.Init))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.AccountNotFound))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.AccountRestored))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.Authenticate))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.Authenticated(FxaAuthData(AuthType.Signin, "code", "state"))))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.FetchProfile))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.FetchedProfile))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.FailedToFetchProfile))
|
||||
assertEquals(AccountState.NotAuthenticated, FxaStateMatrix.nextState(state, Event.FailedToAuthenticate))
|
||||
assertEquals(AccountState.NotAuthenticated, FxaStateMatrix.nextState(state, Event.Logout))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.AuthenticationError("test")))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.SignInShareableAccount(mock(), false)))
|
||||
assertEquals(AccountState.AuthenticatedNoProfile, FxaStateMatrix.nextState(state, Event.SignedInShareableAccount(false)))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.InFlightReuseMigration))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.InFlightCopyMigration))
|
||||
assertNull(FxaStateMatrix.nextState(state, Event.RetryLaterViaTokenReuse))
|
||||
assertEquals(AccountState.CanAutoRetryAuthenticationViaTokenCopy, FxaStateMatrix.nextState(state, Event.RetryLaterViaTokenCopy))
|
||||
assertEquals(AccountState.CanAutoRetryAuthenticationViaTokenCopy, FxaStateMatrix.nextState(state, Event.RetryMigration))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/* 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.manager
|
||||
|
||||
import mozilla.components.support.test.mock
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class StateKtTest {
|
||||
private fun assertNextStateForStateEventPair(state: State, event: Event, nextState: State?) {
|
||||
val expectedNextState = when (state) {
|
||||
is State.Idle -> when (state.accountState) {
|
||||
AccountState.NotAuthenticated -> when (event) {
|
||||
Event.Account.Start -> State.Active(ProgressState.Initializing)
|
||||
Event.Account.BeginEmailFlow -> State.Active(ProgressState.BeginningAuthentication)
|
||||
is Event.Account.BeginPairingFlow -> State.Active(ProgressState.BeginningAuthentication)
|
||||
is Event.Account.MigrateFromAccount -> State.Active(ProgressState.MigratingAccount)
|
||||
else -> null
|
||||
}
|
||||
AccountState.IncompleteMigration -> when (event) {
|
||||
Event.Account.RetryMigration -> State.Active(ProgressState.MigratingAccount)
|
||||
Event.Account.Logout -> State.Active(ProgressState.LoggingOut)
|
||||
else -> null
|
||||
}
|
||||
AccountState.Authenticated -> when (event) {
|
||||
is Event.Account.AuthenticationError -> State.Active(ProgressState.RecoveringFromAuthProblem)
|
||||
Event.Account.Logout -> State.Active(ProgressState.LoggingOut)
|
||||
else -> null
|
||||
}
|
||||
AccountState.AuthenticationProblem -> when (event) {
|
||||
Event.Account.BeginEmailFlow -> State.Active(ProgressState.BeginningAuthentication)
|
||||
Event.Account.Logout -> State.Active(ProgressState.LoggingOut)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
is State.Active -> when (state.progressState) {
|
||||
ProgressState.Initializing -> when (event) {
|
||||
Event.Progress.AccountNotFound -> State.Idle(AccountState.NotAuthenticated)
|
||||
Event.Progress.AccountRestored -> State.Active(ProgressState.CompletingAuthentication)
|
||||
is Event.Progress.IncompleteMigration -> State.Active(ProgressState.MigratingAccount)
|
||||
else -> null
|
||||
}
|
||||
ProgressState.BeginningAuthentication -> when (event) {
|
||||
Event.Progress.FailedToBeginAuth -> State.Idle(AccountState.NotAuthenticated)
|
||||
is Event.Progress.AuthData -> State.Active(ProgressState.CompletingAuthentication)
|
||||
else -> null
|
||||
}
|
||||
ProgressState.CompletingAuthentication -> when (event) {
|
||||
Event.Progress.FailedToCompleteAuth -> State.Idle(AccountState.NotAuthenticated)
|
||||
Event.Progress.FailedToCompleteAuthRestore -> State.Idle(AccountState.NotAuthenticated)
|
||||
is Event.Progress.CompletedAuthentication -> State.Idle(AccountState.Authenticated)
|
||||
else -> null
|
||||
}
|
||||
ProgressState.MigratingAccount -> when (event) {
|
||||
Event.Progress.FailedToCompleteMigration -> State.Idle(AccountState.NotAuthenticated)
|
||||
is Event.Progress.Migrated -> State.Active(ProgressState.CompletingAuthentication)
|
||||
is Event.Progress.IncompleteMigration -> State.Idle(AccountState.IncompleteMigration)
|
||||
else -> null
|
||||
}
|
||||
ProgressState.RecoveringFromAuthProblem -> when (event) {
|
||||
Event.Progress.RecoveredFromAuthenticationProblem -> State.Idle(AccountState.Authenticated)
|
||||
Event.Progress.FailedToRecoverFromAuthenticationProblem -> State.Idle(AccountState.AuthenticationProblem)
|
||||
else -> null
|
||||
}
|
||||
ProgressState.LoggingOut -> when (event) {
|
||||
Event.Progress.LoggedOut -> State.Idle(AccountState.NotAuthenticated)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(expectedNextState, nextState)
|
||||
}
|
||||
|
||||
private fun instantiateEvent(eventClassSimpleName: String): Event {
|
||||
return when (eventClassSimpleName) {
|
||||
"Start" -> Event.Account.Start
|
||||
"BeginPairingFlow" -> Event.Account.BeginPairingFlow("http://some.pairing.url.com")
|
||||
"BeginEmailFlow" -> Event.Account.BeginEmailFlow
|
||||
"AuthenticationError" -> Event.Account.AuthenticationError("fxa op")
|
||||
"MigrateFromAccount" -> Event.Account.MigrateFromAccount(mock(), true)
|
||||
"RetryMigration" -> Event.Account.RetryMigration
|
||||
"Logout" -> Event.Account.Logout
|
||||
"AccountNotFound" -> Event.Progress.AccountNotFound
|
||||
"AccountRestored" -> Event.Progress.AccountRestored
|
||||
"AuthData" -> Event.Progress.AuthData(mock())
|
||||
"IncompleteMigration" -> Event.Progress.IncompleteMigration(true)
|
||||
"Migrated" -> Event.Progress.Migrated(true)
|
||||
"LoggedOut" -> Event.Progress.LoggedOut
|
||||
"FailedToRecoverFromAuthenticationProblem" -> Event.Progress.FailedToRecoverFromAuthenticationProblem
|
||||
"RecoveredFromAuthenticationProblem" -> Event.Progress.RecoveredFromAuthenticationProblem
|
||||
"CompletedAuthentication" -> Event.Progress.CompletedAuthentication(mock())
|
||||
"FailedToBeginAuth" -> Event.Progress.FailedToBeginAuth
|
||||
"FailedToCompleteAuth" -> Event.Progress.FailedToCompleteAuth
|
||||
"FailedToCompleteMigration" -> Event.Progress.FailedToCompleteMigration
|
||||
"FailedToCompleteAuthRestore" -> Event.Progress.FailedToCompleteAuthRestore
|
||||
else -> {
|
||||
throw AssertionError("Unknown event: $eventClassSimpleName")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `state transition matrix`() {
|
||||
// We want to test every combination of state/event. Do that by iterating over entire sets.
|
||||
ProgressState.values().forEach { state ->
|
||||
Event.Progress::class.sealedSubclasses.map { instantiateEvent(it.simpleName!!) }.forEach {
|
||||
val ss = State.Active(state)
|
||||
assertNextStateForStateEventPair(
|
||||
ss,
|
||||
it,
|
||||
ss.next(it)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AccountState.values().forEach { state ->
|
||||
Event.Account::class.sealedSubclasses.map { instantiateEvent(it.simpleName!!) }.forEach {
|
||||
val ss = State.Idle(state)
|
||||
assertNextStateForStateEventPair(
|
||||
ss,
|
||||
it,
|
||||
ss.next(it)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import android.content.pm.Signature
|
||||
import android.content.pm.SigningInfo
|
||||
import android.database.Cursor
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import mozilla.components.concept.sync.MigratingAccountInfo
|
||||
import mozilla.components.service.fxa.sharing.AccountSharing.KEY_EMAIL
|
||||
import mozilla.components.service.fxa.sharing.AccountSharing.KEY_KSYNC
|
||||
import mozilla.components.service.fxa.sharing.AccountSharing.KEY_KXSCS
|
||||
@@ -166,7 +167,7 @@ class AccountSharingTest {
|
||||
val expectedAccountRelease = ShareableAccount(
|
||||
"user@mozilla.org",
|
||||
packageNameRelease,
|
||||
ShareableAuthInfo(
|
||||
MigratingAccountInfo(
|
||||
"sessionToken".toByteArray().toHexString(),
|
||||
"ksync".toByteArray().toHexString(),
|
||||
"kxscs"
|
||||
@@ -177,7 +178,7 @@ class AccountSharingTest {
|
||||
val expectedAccountBeta = ShareableAccount(
|
||||
"user@mozilla.org",
|
||||
packageNameBeta,
|
||||
ShareableAuthInfo(
|
||||
MigratingAccountInfo(
|
||||
"sessionToken".toByteArray().toHexString(),
|
||||
"ksync".toByteArray().toHexString(),
|
||||
"kxscs"
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
package mozilla.components.support.migration
|
||||
|
||||
import android.content.Context
|
||||
import mozilla.components.concept.sync.MigratingAccountInfo
|
||||
import mozilla.components.service.fxa.manager.FxaAccountManager
|
||||
import mozilla.components.service.fxa.manager.SignInWithShareableAccountResult
|
||||
import mozilla.components.service.fxa.manager.MigrationResult
|
||||
import mozilla.components.service.fxa.sharing.ShareableAccount
|
||||
import mozilla.components.service.fxa.sharing.ShareableAuthInfo
|
||||
import mozilla.components.support.base.log.logger.Logger
|
||||
import mozilla.components.support.migration.FxaMigrationResult.Failure
|
||||
import mozilla.components.support.migration.FxaMigrationResult.Success.SignedInIntoAuthenticatedAccount
|
||||
@@ -147,7 +147,7 @@ private object AuthenticatedAccountProcessor {
|
||||
): Result<FxaMigrationResult> {
|
||||
require(fennecAccount.authInfo != null) { "authInfo must be present in order to sign-in a FennecAccount" }
|
||||
|
||||
val fennecAuthInfo = ShareableAuthInfo(
|
||||
val fennecAuthInfo = MigratingAccountInfo(
|
||||
sessionToken = fennecAccount.authInfo.sessionToken,
|
||||
kSync = fennecAccount.authInfo.kSync,
|
||||
kXCS = fennecAccount.authInfo.kXCS
|
||||
@@ -158,13 +158,13 @@ private object AuthenticatedAccountProcessor {
|
||||
authInfo = fennecAuthInfo
|
||||
)
|
||||
|
||||
val signInResult = accountManager.signInWithShareableAccountAsync(
|
||||
val signInResult = accountManager.migrateFromAccount(
|
||||
shareableAccount,
|
||||
reuseSessionToken = true
|
||||
).await()
|
||||
)
|
||||
|
||||
return when (signInResult) {
|
||||
SignInWithShareableAccountResult.Failure -> {
|
||||
MigrationResult.Failure -> {
|
||||
Result.Failure(
|
||||
FxaMigrationException(Failure.FailedToSignIntoAuthenticatedAccount(
|
||||
email = fennecAccount.email,
|
||||
@@ -172,13 +172,13 @@ private object AuthenticatedAccountProcessor {
|
||||
))
|
||||
)
|
||||
}
|
||||
SignInWithShareableAccountResult.Success -> {
|
||||
MigrationResult.Success -> {
|
||||
Result.Success(SignedInIntoAuthenticatedAccount(
|
||||
email = fennecAccount.email,
|
||||
stateLabel = fennecAccount.stateLabel
|
||||
))
|
||||
}
|
||||
SignInWithShareableAccountResult.WillRetry -> {
|
||||
MigrationResult.WillRetry -> {
|
||||
Result.Success(FxaMigrationResult.Success.WillAutoRetrySignInLater(
|
||||
email = fennecAccount.email,
|
||||
stateLabel = fennecAccount.stateLabel
|
||||
|
||||
@@ -5,10 +5,9 @@
|
||||
package mozilla.components.support.migration
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import mozilla.components.service.fxa.manager.FxaAccountManager
|
||||
import mozilla.components.service.fxa.manager.SignInWithShareableAccountResult
|
||||
import mozilla.components.service.fxa.manager.MigrationResult
|
||||
import mozilla.components.service.fxa.sharing.ShareableAccount
|
||||
import mozilla.components.support.test.any
|
||||
import mozilla.components.support.test.argumentCaptor
|
||||
@@ -82,9 +81,7 @@ class FennecFxaMigrationTest {
|
||||
val fxaPath = File(getTestPath("fxa"), "married-v4.json")
|
||||
val accountManager: FxaAccountManager = mock()
|
||||
|
||||
`when`(accountManager.signInWithShareableAccountAsync(any(), eq(true))).thenReturn(
|
||||
CompletableDeferred(SignInWithShareableAccountResult.Success)
|
||||
)
|
||||
`when`(accountManager.migrateFromAccount(any(), eq(true))).thenReturn(MigrationResult.Success)
|
||||
|
||||
with(FennecFxaMigration.migrate(fxaPath, testContext, accountManager) as Result.Success) {
|
||||
assertEquals(FxaMigrationResult.Success.SignedInIntoAuthenticatedAccount::class, this.value::class)
|
||||
@@ -92,7 +89,7 @@ class FennecFxaMigrationTest {
|
||||
assertEquals("Married", (this.value as FxaMigrationResult.Success.SignedInIntoAuthenticatedAccount).stateLabel)
|
||||
|
||||
val captor = argumentCaptor<ShareableAccount>()
|
||||
verify(accountManager).signInWithShareableAccountAsync(captor.capture(), eq(true))
|
||||
verify(accountManager).migrateFromAccount(captor.capture(), eq(true))
|
||||
|
||||
assertEquals("test@example.com", captor.value.email)
|
||||
assertEquals("252fsvj8932vj32movj97325hjfksdhfjstrg23yurt267r23", captor.value.authInfo.kSync)
|
||||
@@ -106,9 +103,7 @@ class FennecFxaMigrationTest {
|
||||
val fxaPath = File(getTestPath("fxa"), "cohabiting-v4.json")
|
||||
val accountManager: FxaAccountManager = mock()
|
||||
|
||||
`when`(accountManager.signInWithShareableAccountAsync(any(), eq(true))).thenReturn(
|
||||
CompletableDeferred(SignInWithShareableAccountResult.Success)
|
||||
)
|
||||
`when`(accountManager.migrateFromAccount(any(), eq(true))).thenReturn(MigrationResult.Success)
|
||||
|
||||
with(FennecFxaMigration.migrate(fxaPath, testContext, accountManager) as Result.Success) {
|
||||
assertEquals(FxaMigrationResult.Success.SignedInIntoAuthenticatedAccount::class, this.value::class)
|
||||
@@ -116,7 +111,7 @@ class FennecFxaMigrationTest {
|
||||
assertEquals("Cohabiting", (this.value as FxaMigrationResult.Success.SignedInIntoAuthenticatedAccount).stateLabel)
|
||||
|
||||
val captor = argumentCaptor<ShareableAccount>()
|
||||
verify(accountManager).signInWithShareableAccountAsync(captor.capture(), eq(true))
|
||||
verify(accountManager).migrateFromAccount(captor.capture(), eq(true))
|
||||
|
||||
assertEquals("test@example.com", captor.value.email)
|
||||
assertEquals("252bc4ccc3a239fsdfsdf32fg32wf3w4e3472d41d1a204890", captor.value.authInfo.kSync)
|
||||
@@ -130,9 +125,7 @@ class FennecFxaMigrationTest {
|
||||
val fxaPath = File(getTestPath("fxa"), "cohabiting-v4.json")
|
||||
val accountManager: FxaAccountManager = mock()
|
||||
|
||||
`when`(accountManager.signInWithShareableAccountAsync(any(), eq(true))).thenReturn(
|
||||
CompletableDeferred(SignInWithShareableAccountResult.WillRetry)
|
||||
)
|
||||
`when`(accountManager.migrateFromAccount(any(), eq(true))).thenReturn(MigrationResult.WillRetry)
|
||||
|
||||
with(FennecFxaMigration.migrate(fxaPath, testContext, accountManager) as Result.Success) {
|
||||
assertEquals(FxaMigrationResult.Success.WillAutoRetrySignInLater::class, this.value::class)
|
||||
@@ -140,7 +133,7 @@ class FennecFxaMigrationTest {
|
||||
assertEquals("Cohabiting", (this.value as FxaMigrationResult.Success.WillAutoRetrySignInLater).stateLabel)
|
||||
|
||||
val captor = argumentCaptor<ShareableAccount>()
|
||||
verify(accountManager).signInWithShareableAccountAsync(captor.capture(), eq(true))
|
||||
verify(accountManager).migrateFromAccount(captor.capture(), eq(true))
|
||||
|
||||
assertEquals("test@example.com", captor.value.email)
|
||||
assertEquals("252bc4ccc3a239fsdfsdf32fg32wf3w4e3472d41d1a204890", captor.value.authInfo.kSync)
|
||||
@@ -154,9 +147,7 @@ class FennecFxaMigrationTest {
|
||||
val fxaPath = File(getTestPath("fxa"), "married-v4.json")
|
||||
val accountManager: FxaAccountManager = mock()
|
||||
|
||||
`when`(accountManager.signInWithShareableAccountAsync(any(), eq(true))).thenReturn(
|
||||
CompletableDeferred(SignInWithShareableAccountResult.Failure)
|
||||
)
|
||||
`when`(accountManager.migrateFromAccount(any(), eq(true))).thenReturn(MigrationResult.Failure)
|
||||
|
||||
with(FennecFxaMigration.migrate(fxaPath, testContext, accountManager) as Result.Failure) {
|
||||
val unwrapped = this.throwables.first() as FxaMigrationException
|
||||
@@ -166,7 +157,7 @@ class FennecFxaMigrationTest {
|
||||
assertEquals("Married", (unwrapped.failure as FxaMigrationResult.Failure.FailedToSignIntoAuthenticatedAccount).stateLabel)
|
||||
|
||||
val captor = argumentCaptor<ShareableAccount>()
|
||||
verify(accountManager).signInWithShareableAccountAsync(captor.capture(), eq(true))
|
||||
verify(accountManager).migrateFromAccount(captor.capture(), eq(true))
|
||||
|
||||
assertEquals("test@example.com", captor.value.email)
|
||||
assertEquals("252fsvj8932vj32movj97325hjfksdhfjstrg23yurt267r23", captor.value.authInfo.kSync)
|
||||
@@ -222,9 +213,7 @@ class FennecFxaMigrationTest {
|
||||
val fxaPath = File(getTestPath("fxa"), "cohabiting-v4.json")
|
||||
val accountManager: FxaAccountManager = mock()
|
||||
|
||||
`when`(accountManager.signInWithShareableAccountAsync(any(), eq(true))).thenReturn(
|
||||
CompletableDeferred(SignInWithShareableAccountResult.Failure)
|
||||
)
|
||||
`when`(accountManager.migrateFromAccount(any(), eq(true))).thenReturn(MigrationResult.Failure)
|
||||
|
||||
with(FennecFxaMigration.migrate(fxaPath, testContext, accountManager) as Result.Failure) {
|
||||
val unwrapped = this.throwables.first() as FxaMigrationException
|
||||
@@ -234,7 +223,7 @@ class FennecFxaMigrationTest {
|
||||
assertEquals("Cohabiting", unwrappedFailure.stateLabel)
|
||||
|
||||
val captor = argumentCaptor<ShareableAccount>()
|
||||
verify(accountManager).signInWithShareableAccountAsync(captor.capture(), eq(true))
|
||||
verify(accountManager).migrateFromAccount(captor.capture(), eq(true))
|
||||
|
||||
assertEquals("test@example.com", captor.value.email)
|
||||
assertEquals("252bc4ccc3a239fsdfsdf32fg32wf3w4e3472d41d1a204890", captor.value.authInfo.kSync)
|
||||
|
||||
@@ -28,14 +28,13 @@ import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.verifyZeroInteractions
|
||||
import java.io.File
|
||||
import java.lang.IllegalStateException
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import mozilla.appservices.places.BookmarkRoot
|
||||
import mozilla.components.concept.engine.Engine
|
||||
import mozilla.components.concept.engine.webextension.WebExtension
|
||||
import mozilla.components.feature.addons.amo.AddonCollectionProvider
|
||||
import mozilla.components.feature.addons.update.AddonUpdater
|
||||
import mozilla.components.feature.top.sites.PinnedSiteStorage
|
||||
import mozilla.components.service.fxa.manager.SignInWithShareableAccountResult
|
||||
import mozilla.components.service.fxa.manager.MigrationResult
|
||||
import mozilla.components.service.fxa.sharing.ShareableAccount
|
||||
import mozilla.components.service.sync.logins.SyncableLoginsStorage
|
||||
import mozilla.components.concept.base.crash.CrashReporting
|
||||
@@ -581,9 +580,7 @@ class FennecMigratorTest {
|
||||
.setBrowserDbPath(File(getTestPath("combined"), "basic/browser.db").absolutePath)
|
||||
.build()
|
||||
|
||||
`when`(accountManager.signInWithShareableAccountAsync(any(), eq(true))).thenReturn(
|
||||
CompletableDeferred(SignInWithShareableAccountResult.Success)
|
||||
)
|
||||
`when`(accountManager.migrateFromAccount(any(), eq(true))).thenReturn(MigrationResult.Success)
|
||||
|
||||
with(migrator.migrateAsync(mock()).await()) {
|
||||
assertEquals(1, this.size)
|
||||
@@ -592,7 +589,7 @@ class FennecMigratorTest {
|
||||
}
|
||||
|
||||
val captor = argumentCaptor<ShareableAccount>()
|
||||
verify(accountManager).signInWithShareableAccountAsync(captor.capture(), eq(true))
|
||||
verify(accountManager).migrateFromAccount(captor.capture(), eq(true))
|
||||
|
||||
assertEquals("test@example.com", captor.value.email)
|
||||
// This is going to be package name (org.mozilla.firefox) in actual builds.
|
||||
@@ -620,9 +617,7 @@ class FennecMigratorTest {
|
||||
.setBrowserDbPath(File(getTestPath("combined"), "basic/browser.db").absolutePath)
|
||||
.build()
|
||||
|
||||
`when`(accountManager.signInWithShareableAccountAsync(any(), eq(true))).thenReturn(
|
||||
CompletableDeferred(SignInWithShareableAccountResult.WillRetry)
|
||||
)
|
||||
`when`(accountManager.migrateFromAccount(any(), eq(true))).thenReturn(MigrationResult.WillRetry)
|
||||
|
||||
with(migrator.migrateAsync(mock()).await()) {
|
||||
assertEquals(1, this.size)
|
||||
@@ -631,7 +626,7 @@ class FennecMigratorTest {
|
||||
}
|
||||
|
||||
val captor = argumentCaptor<ShareableAccount>()
|
||||
verify(accountManager).signInWithShareableAccountAsync(captor.capture(), eq(true))
|
||||
verify(accountManager).migrateFromAccount(captor.capture(), eq(true))
|
||||
|
||||
assertEquals("test@example.com", captor.value.email)
|
||||
// This is going to be package name (org.mozilla.firefox) in actual builds.
|
||||
@@ -660,9 +655,7 @@ class FennecMigratorTest {
|
||||
.build()
|
||||
|
||||
// For now, we don't treat sign-in failure any different from success. E.g. it's a one-shot attempt.
|
||||
`when`(accountManager.signInWithShareableAccountAsync(any(), eq(true))).thenReturn(
|
||||
CompletableDeferred(SignInWithShareableAccountResult.Failure)
|
||||
)
|
||||
`when`(accountManager.migrateFromAccount(any(), eq(true))).thenReturn(MigrationResult.Failure)
|
||||
|
||||
with(migrator.migrateAsync(mock()).await()) {
|
||||
assertEquals(1, this.size)
|
||||
@@ -671,7 +664,7 @@ class FennecMigratorTest {
|
||||
}
|
||||
|
||||
val captor = argumentCaptor<ShareableAccount>()
|
||||
verify(accountManager).signInWithShareableAccountAsync(captor.capture(), eq(true))
|
||||
verify(accountManager).migrateFromAccount(captor.capture(), eq(true))
|
||||
|
||||
assertEquals("test@example.com", captor.value.email)
|
||||
// This is going to be package name (org.mozilla.firefox) in actual builds.
|
||||
|
||||
@@ -72,7 +72,7 @@ open class MainActivity : AppCompatActivity(), LoginFragment.OnLoginCompleteList
|
||||
},
|
||||
onScanResult = { pairingUrl ->
|
||||
launch {
|
||||
val url = account.beginPairingFlowAsync(pairingUrl, scopes).await()
|
||||
val url = account.beginPairingFlow(pairingUrl, scopes)
|
||||
if (url == null) {
|
||||
Log.log(
|
||||
Log.Priority.ERROR,
|
||||
@@ -91,7 +91,7 @@ open class MainActivity : AppCompatActivity(), LoginFragment.OnLoginCompleteList
|
||||
|
||||
findViewById<View>(R.id.buttonCustomTabs).setOnClickListener {
|
||||
launch {
|
||||
account.beginOAuthFlowAsync(scopes).await()?.let {
|
||||
account.beginOAuthFlow(scopes)?.let {
|
||||
openTab(it.url)
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@ open class MainActivity : AppCompatActivity(), LoginFragment.OnLoginCompleteList
|
||||
|
||||
findViewById<View>(R.id.buttonWebView).setOnClickListener {
|
||||
launch {
|
||||
account.beginOAuthFlowAsync(scopes).await()?.let {
|
||||
account.beginOAuthFlow(scopes)?.let {
|
||||
openWebView(it.url)
|
||||
}
|
||||
}
|
||||
@@ -123,8 +123,8 @@ open class MainActivity : AppCompatActivity(), LoginFragment.OnLoginCompleteList
|
||||
private fun initAccount(): FirefoxAccount {
|
||||
getAuthenticatedAccount()?.let {
|
||||
launch {
|
||||
it.getProfileAsync(true).await()?.let {
|
||||
displayProfile(it)
|
||||
it.getProfile(true)?.let { profile ->
|
||||
displayProfile(profile)
|
||||
}
|
||||
}
|
||||
return it
|
||||
@@ -189,8 +189,8 @@ open class MainActivity : AppCompatActivity(), LoginFragment.OnLoginCompleteList
|
||||
|
||||
private fun displayAndPersistProfile(code: String, state: String) {
|
||||
launch {
|
||||
account.completeOAuthFlowAsync(code, state).await()
|
||||
account.getProfileAsync().await()?.let {
|
||||
account.completeOAuthFlow(code, state)
|
||||
account.getProfile()?.let {
|
||||
displayProfile(it)
|
||||
}
|
||||
account.toJSONString().let {
|
||||
|
||||
@@ -16,6 +16,7 @@ import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.components.concept.sync.AccountObserver
|
||||
import mozilla.components.concept.sync.AuthType
|
||||
import mozilla.components.concept.sync.DeviceConfig
|
||||
import mozilla.components.concept.sync.DeviceType
|
||||
import mozilla.components.concept.sync.OAuthAccount
|
||||
import mozilla.components.concept.sync.Profile
|
||||
@@ -23,8 +24,8 @@ import mozilla.components.service.fxa.FirefoxAccount
|
||||
import mozilla.components.lib.dataprotect.SecureAbove22Preferences
|
||||
import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient
|
||||
import mozilla.components.service.fxa.manager.FxaAccountManager
|
||||
import mozilla.components.service.fxa.DeviceConfig
|
||||
import mozilla.components.service.fxa.FxaAuthData
|
||||
import mozilla.components.service.fxa.PeriodicSyncConfig
|
||||
import mozilla.components.service.fxa.Server
|
||||
import mozilla.components.service.fxa.ServerConfig
|
||||
import mozilla.components.service.fxa.SyncConfig
|
||||
@@ -59,7 +60,7 @@ class MainActivity : AppCompatActivity(), LoginFragment.OnLoginCompleteListener,
|
||||
applicationContext,
|
||||
ServerConfig(Server.RELEASE, CLIENT_ID, REDIRECT_URL),
|
||||
DeviceConfig("A-C Logins Sync Sample", DeviceType.MOBILE, setOf()),
|
||||
SyncConfig(setOf(SyncEngine.Passwords))
|
||||
SyncConfig(setOf(SyncEngine.Passwords), PeriodicSyncConfig())
|
||||
)
|
||||
}
|
||||
|
||||
@@ -98,12 +99,12 @@ class MainActivity : AppCompatActivity(), LoginFragment.OnLoginCompleteListener,
|
||||
// kicking off the accountManager.
|
||||
GlobalSyncableStoreProvider.configureStore(SyncEngine.Passwords to loginsStorage)
|
||||
|
||||
accountManager.initAsync().await()
|
||||
accountManager.start()
|
||||
}
|
||||
|
||||
findViewById<View>(R.id.buttonWebView).setOnClickListener {
|
||||
launch {
|
||||
val authUrl = accountManager.beginAuthenticationAsync().await()
|
||||
val authUrl = accountManager.beginAuthentication()
|
||||
if (authUrl == null) {
|
||||
Toast.makeText(this@MainActivity, "Account auth error", Toast.LENGTH_LONG).show()
|
||||
return@launch
|
||||
@@ -119,7 +120,7 @@ class MainActivity : AppCompatActivity(), LoginFragment.OnLoginCompleteListener,
|
||||
override fun onLoggedOut() {}
|
||||
|
||||
override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
|
||||
accountManager.syncNowAsync(SyncReason.User)
|
||||
launch { accountManager.syncNow(SyncReason.User) }
|
||||
}
|
||||
|
||||
@Suppress("EmptyFunctionBlock")
|
||||
@@ -148,9 +149,9 @@ class MainActivity : AppCompatActivity(), LoginFragment.OnLoginCompleteListener,
|
||||
|
||||
override fun onLoginComplete(code: String, state: String, action: String, fragment: LoginFragment) {
|
||||
launch {
|
||||
accountManager.finishAuthenticationAsync(
|
||||
accountManager.finishAuthentication(
|
||||
FxaAuthData(action.toAuthType(), code = code, state = state)
|
||||
).await()
|
||||
)
|
||||
supportFragmentManager.popBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import mozilla.components.browser.storage.sync.PlacesBookmarksStorage
|
||||
import mozilla.components.browser.storage.sync.PlacesHistoryStorage
|
||||
import mozilla.components.concept.storage.BookmarkNode
|
||||
@@ -29,12 +30,13 @@ import mozilla.components.concept.sync.DeviceCommandOutgoing
|
||||
import mozilla.components.concept.sync.DeviceType
|
||||
import mozilla.components.concept.sync.AccountEventsObserver
|
||||
import mozilla.components.concept.sync.AccountEvent
|
||||
import mozilla.components.concept.sync.AuthFlowError
|
||||
import mozilla.components.concept.sync.DeviceConfig
|
||||
import mozilla.components.concept.sync.OAuthAccount
|
||||
import mozilla.components.concept.sync.Profile
|
||||
import mozilla.components.lib.dataprotect.SecureAbove22Preferences
|
||||
import mozilla.components.lib.dataprotect.generateEncryptionKey
|
||||
import mozilla.components.service.fxa.manager.FxaAccountManager
|
||||
import mozilla.components.service.fxa.DeviceConfig
|
||||
import mozilla.components.service.fxa.ServerConfig
|
||||
import mozilla.components.service.fxa.SyncConfig
|
||||
import mozilla.components.service.fxa.sync.GlobalSyncableStoreProvider
|
||||
@@ -42,6 +44,7 @@ import mozilla.components.service.fxa.sync.SyncStatusObserver
|
||||
import mozilla.components.support.base.log.Log
|
||||
import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient
|
||||
import mozilla.components.service.fxa.FxaAuthData
|
||||
import mozilla.components.service.fxa.PeriodicSyncConfig
|
||||
import mozilla.components.service.fxa.Server
|
||||
import mozilla.components.service.fxa.SyncEngine
|
||||
import mozilla.components.service.fxa.sync.SyncReason
|
||||
@@ -92,7 +95,7 @@ class MainActivity :
|
||||
),
|
||||
SyncConfig(
|
||||
setOf(SyncEngine.History, SyncEngine.Bookmarks, SyncEngine.Passwords),
|
||||
syncPeriodInMinutes = 15L
|
||||
periodicSyncConfig = PeriodicSyncConfig(periodMinutes = 15, initialDelayMinutes = 5)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -119,24 +122,16 @@ class MainActivity :
|
||||
|
||||
findViewById<View>(R.id.buttonSignIn).setOnClickListener {
|
||||
launch {
|
||||
val authUrl = accountManager.beginAuthenticationAsync().await()
|
||||
if (authUrl == null) {
|
||||
val txtView: TextView = findViewById(R.id.fxaStatusView)
|
||||
txtView.text = getString(R.string.account_error, null)
|
||||
return@launch
|
||||
}
|
||||
openWebView(authUrl)
|
||||
accountManager.beginAuthentication()?.let { openWebView(it) }
|
||||
}
|
||||
}
|
||||
|
||||
findViewById<View>(R.id.buttonLogout).setOnClickListener {
|
||||
launch {
|
||||
accountManager.logoutAsync().await()
|
||||
}
|
||||
launch { accountManager.logout() }
|
||||
}
|
||||
|
||||
findViewById<View>(R.id.refreshDevice).setOnClickListener {
|
||||
launch { accountManager.authenticatedAccount()?.deviceConstellation()?.refreshDevicesAsync()?.await() }
|
||||
launch { accountManager.authenticatedAccount()?.deviceConstellation()?.refreshDevices() }
|
||||
}
|
||||
|
||||
findViewById<View>(R.id.sendTab).setOnClickListener {
|
||||
@@ -148,9 +143,9 @@ class MainActivity :
|
||||
}
|
||||
|
||||
targets?.forEach {
|
||||
constellation.sendCommandToDeviceAsync(
|
||||
constellation.sendCommandToDevice(
|
||||
it.id, DeviceCommandOutgoing.SendTab("Sample tab", "https://www.mozilla.org")
|
||||
).await()
|
||||
)
|
||||
}
|
||||
|
||||
Toast.makeText(
|
||||
@@ -171,17 +166,20 @@ class MainActivity :
|
||||
// Observe incoming device commands.
|
||||
accountManager.registerForAccountEvents(accountEventsObserver, owner = this, autoPause = true)
|
||||
|
||||
launch {
|
||||
GlobalSyncableStoreProvider.configureStore(SyncEngine.History to historyStorage)
|
||||
GlobalSyncableStoreProvider.configureStore(SyncEngine.Bookmarks to bookmarksStorage)
|
||||
GlobalSyncableStoreProvider.configureStore(SyncEngine.Passwords to passwordsStorage)
|
||||
GlobalSyncableStoreProvider.configureStore(SyncEngine.History to historyStorage)
|
||||
GlobalSyncableStoreProvider.configureStore(SyncEngine.Bookmarks to bookmarksStorage)
|
||||
GlobalSyncableStoreProvider.configureStore(SyncEngine.Passwords to passwordsStorage)
|
||||
|
||||
launch {
|
||||
// Now that our account state observer is registered, we can kick off the account manager.
|
||||
accountManager.initAsync().await()
|
||||
accountManager.start()
|
||||
}
|
||||
|
||||
findViewById<View>(R.id.buttonSync).setOnClickListener {
|
||||
accountManager.syncNowAsync(SyncReason.User)
|
||||
launch {
|
||||
accountManager.syncNow(SyncReason.User)
|
||||
accountManager.authenticatedAccount()?.deviceConstellation()?.pollForCommands()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,9 +192,9 @@ class MainActivity :
|
||||
override fun onLoginComplete(code: String, state: String, action: String, fragment: LoginFragment) {
|
||||
launch {
|
||||
supportFragmentManager.popBackStack()
|
||||
accountManager.finishAuthenticationAsync(
|
||||
accountManager.finishAuthentication(
|
||||
FxaAuthData(action.toAuthType(), code = code, state = state)
|
||||
).await()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,24 +220,26 @@ class MainActivity :
|
||||
|
||||
private val deviceConstellationObserver = object : DeviceConstellationObserver {
|
||||
override fun onDevicesUpdate(constellation: ConstellationState) {
|
||||
val currentDevice = constellation.currentDevice
|
||||
launch {
|
||||
val currentDevice = constellation.currentDevice
|
||||
|
||||
val currentDeviceView: TextView = findViewById(R.id.currentDevice)
|
||||
if (currentDevice != null) {
|
||||
currentDeviceView.text = getString(
|
||||
R.string.full_device_details,
|
||||
currentDevice.id, currentDevice.displayName, currentDevice.deviceType,
|
||||
currentDevice.subscriptionExpired, currentDevice.subscription,
|
||||
currentDevice.capabilities, currentDevice.lastAccessTime
|
||||
)
|
||||
} else {
|
||||
currentDeviceView.text = getString(R.string.current_device_unknown)
|
||||
val currentDeviceView: TextView = findViewById(R.id.currentDevice)
|
||||
if (currentDevice != null) {
|
||||
currentDeviceView.text = getString(
|
||||
R.string.full_device_details,
|
||||
currentDevice.id, currentDevice.displayName, currentDevice.deviceType,
|
||||
currentDevice.subscriptionExpired, currentDevice.subscription,
|
||||
currentDevice.capabilities, currentDevice.lastAccessTime
|
||||
)
|
||||
} else {
|
||||
currentDeviceView.text = getString(R.string.current_device_unknown)
|
||||
}
|
||||
|
||||
val devicesFragment = supportFragmentManager.findFragmentById(R.id.devices_fragment) as DeviceFragment
|
||||
devicesFragment.updateDevices(constellation.otherDevices)
|
||||
|
||||
Toast.makeText(this@MainActivity, "Devices updated", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
val devicesFragment = supportFragmentManager.findFragmentById(R.id.devices_fragment) as DeviceFragment
|
||||
devicesFragment.updateDevices(constellation.otherDevices)
|
||||
|
||||
Toast.makeText(this@MainActivity, "Devices updated", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,7 +253,6 @@ class MainActivity :
|
||||
when (it.command) {
|
||||
is DeviceCommandIncoming.TabReceived -> {
|
||||
val cmd = it.command as DeviceCommandIncoming.TabReceived
|
||||
txtView.text = "A tab was received"
|
||||
var tabsStringified = "Tab(s) from: ${cmd.from?.displayName}\n"
|
||||
cmd.entries.forEach { tab ->
|
||||
tabsStringified += "${tab.title}: ${tab.url}\n"
|
||||
@@ -362,6 +361,20 @@ class MainActivity :
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFlowError(error: AuthFlowError) {
|
||||
launch {
|
||||
val txtView: TextView = findViewById(R.id.fxaStatusView)
|
||||
txtView.text = getString(
|
||||
R.string.account_error,
|
||||
when (error) {
|
||||
AuthFlowError.FailedToBeginAuth -> "Failed to begin authentication"
|
||||
AuthFlowError.FailedToCompleteAuth -> "Failed to complete authentication"
|
||||
AuthFlowError.FailedToMigrate -> "Failed to migrate"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val syncObserver = object : SyncStatusObserver {
|
||||
@@ -378,7 +391,7 @@ class MainActivity :
|
||||
syncStatus?.text = getString(R.string.sync_idle)
|
||||
|
||||
val historyResultTextView: TextView = findViewById(R.id.historySyncResult)
|
||||
val visitedCount = historyStorage.value.getVisited().size
|
||||
val visitedCount = withContext(Dispatchers.IO) { historyStorage.value.getVisited().size }
|
||||
// visitedCount is passed twice: to get the correct plural form, and then as
|
||||
// an argument for string formatting.
|
||||
historyResultTextView.text = resources.getQuantityString(
|
||||
@@ -386,22 +399,24 @@ class MainActivity :
|
||||
)
|
||||
|
||||
val bookmarksResultTextView: TextView = findViewById(R.id.bookmarksSyncResult)
|
||||
val bookmarksRoot = bookmarksStorage.value.getTree("root________", recursive = true)
|
||||
if (bookmarksRoot == null) {
|
||||
bookmarksResultTextView.text = getString(R.string.no_bookmarks_root)
|
||||
} else {
|
||||
var bookmarksRootAndChildren = "BOOKMARKS\n"
|
||||
fun addTreeNode(node: BookmarkNode, depth: Int) {
|
||||
val desc = " ".repeat(depth * 2) + "${node.title} - ${node.url} (${node.guid})\n"
|
||||
bookmarksRootAndChildren += desc
|
||||
node.children?.forEach {
|
||||
addTreeNode(it, depth + 1)
|
||||
bookmarksResultTextView.setHorizontallyScrolling(true)
|
||||
bookmarksResultTextView.movementMethod = ScrollingMovementMethod.getInstance()
|
||||
bookmarksResultTextView.text = withContext(Dispatchers.IO) {
|
||||
val bookmarksRoot = bookmarksStorage.value.getTree("root________", recursive = true)
|
||||
if (bookmarksRoot == null) {
|
||||
getString(R.string.no_bookmarks_root)
|
||||
} else {
|
||||
var bookmarksRootAndChildren = "BOOKMARKS\n"
|
||||
fun addTreeNode(node: BookmarkNode, depth: Int) {
|
||||
val desc = " ".repeat(depth * 2) + "${node.title} - ${node.url} (${node.guid})\n"
|
||||
bookmarksRootAndChildren += desc
|
||||
node.children?.forEach {
|
||||
addTreeNode(it, depth + 1)
|
||||
}
|
||||
}
|
||||
addTreeNode(bookmarksRoot, 0)
|
||||
bookmarksRootAndChildren
|
||||
}
|
||||
addTreeNode(bookmarksRoot, 0)
|
||||
bookmarksResultTextView.setHorizontallyScrolling(true)
|
||||
bookmarksResultTextView.setMovementMethod(ScrollingMovementMethod.getInstance())
|
||||
bookmarksResultTextView.text = bookmarksRootAndChildren
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user