summaryrefslogtreecommitdiffstats
path: root/mobile/android/android-components/components/service
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 05:35:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 05:35:29 +0000
commit59203c63bb777a3bacec32fb8830fba33540e809 (patch)
tree58298e711c0ff0575818c30485b44a2f21bf28a0 /mobile/android/android-components/components/service
parentAdding upstream version 126.0.1. (diff)
downloadfirefox-59203c63bb777a3bacec32fb8830fba33540e809.tar.xz
firefox-59203c63bb777a3bacec32fb8830fba33540e809.zip
Adding upstream version 127.0.upstream/127.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mobile/android/android-components/components/service')
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/AccountStorage.kt19
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FirefoxAccount.kt11
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FxaDeviceConstellation.kt3
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Types.kt25
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/FxaAccountManager.kt84
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/State.kt31
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncAction.kt6
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncState.kt5
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStore.kt1
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStoreSupport.kt32
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaAccountManagerTest.kt192
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/manager/StateKtTest.kt4
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/store/SyncStoreSupportTest.kt106
-rw-r--r--mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/Glean.kt4
-rw-r--r--mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/config/Configuration.kt4
-rw-r--r--mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/private/MetricAliases.kt87
-rw-r--r--mobile/android/android-components/components/service/nimbus/messaging.fml.yaml29
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/resources/pocket/stories_recommendations_response.json84
-rw-r--r--mobile/android/android-components/components/service/pocket/src/test/resources/pocket/story_recommendation_response.json20
-rw-r--r--mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/GeckoLoginStorageDelegate.kt9
20 files changed, 454 insertions, 302 deletions
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/AccountStorage.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/AccountStorage.kt
index 37af0f5b76..e5f8a665eb 100644
--- a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/AccountStorage.kt
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/AccountStorage.kt
@@ -11,7 +11,6 @@ import mozilla.appservices.fxaclient.FxaRustAuthState
import mozilla.components.concept.base.crash.CrashReporting
import mozilla.components.concept.sync.AccountEvent
import mozilla.components.concept.sync.AccountEventsObserver
-import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.concept.sync.StatePersistenceCallback
import mozilla.components.lib.dataprotect.SecureAbove22Preferences
import mozilla.components.service.fxa.manager.FxaAccountManager
@@ -26,16 +25,16 @@ const val FXA_STATE_KEY = "fxaState"
* Represents state of our account on disk - is it new, or restored?
*/
internal sealed class AccountOnDisk : WithAccount {
- data class Restored(val account: OAuthAccount) : AccountOnDisk() {
+ data class Restored(val account: FirefoxAccount) : AccountOnDisk() {
override fun account() = account
}
- data class New(val account: OAuthAccount) : AccountOnDisk() {
+ data class New(val account: FirefoxAccount) : AccountOnDisk() {
override fun account() = account
}
}
internal interface WithAccount {
- fun account(): OAuthAccount
+ fun account(): FirefoxAccount
}
/**
@@ -80,16 +79,16 @@ open class StorageWrapper(
}
}
- private fun watchAccount(account: OAuthAccount) {
+ private fun watchAccount(account: FirefoxAccount) {
account.registerPersistenceCallback(statePersistenceCallback)
account.deviceConstellation().register(accountEventsIntegration)
}
/**
- * Exists strictly for testing purposes, allowing tests to specify their own implementation of [OAuthAccount].
+ * Exists strictly for testing purposes, allowing tests to specify their own implementation of [FirefoxAccount].
*/
@VisibleForTesting
- open fun obtainAccount(): OAuthAccount = FirefoxAccount(serverConfig, crashReporter)
+ open fun obtainAccount(): FirefoxAccount = FirefoxAccount(serverConfig, crashReporter)
}
/**
@@ -110,7 +109,7 @@ internal class AccountEventsIntegration(
internal interface AccountStorage {
@Throws(Exception::class)
- fun read(): OAuthAccount?
+ fun read(): FirefoxAccount?
fun write(accountState: String)
fun clear()
}
@@ -156,7 +155,7 @@ internal class SharedPrefAccountStorage(
* @throws FxaException if JSON failed to parse into a [FirefoxAccount].
*/
@Throws(FxaException::class)
- override fun read(): OAuthAccount? {
+ override fun read(): FirefoxAccount? {
val savedJSON = accountPreferences().getString(FXA_STATE_KEY, null)
?: return null
@@ -244,7 +243,7 @@ internal class SecureAbove22AccountStorage(
* @throws FxaException if JSON failed to parse into a [FirefoxAccount].
*/
@Throws(FxaException::class)
- override fun read(): OAuthAccount? {
+ override fun read(): FirefoxAccount? {
return store.getString(KEY_ACCOUNT_STATE).also {
// If account state is missing, but we expected it to be present, report an exception.
if (it == null && prefs.getBoolean(PREF_KEY_HAS_STATE, false)) {
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FirefoxAccount.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FirefoxAccount.kt
index 7fc31785e3..14e3e7d105 100644
--- a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FirefoxAccount.kt
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FirefoxAccount.kt
@@ -17,6 +17,7 @@ import mozilla.components.concept.sync.DeviceConstellation
import mozilla.components.concept.sync.FxAEntryPoint
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.concept.sync.StatePersistenceCallback
+import mozilla.components.concept.sync.UserData
import mozilla.components.support.base.log.logger.Logger
typealias PersistCallback = mozilla.appservices.fxaclient.FxaClient.PersistCallback
@@ -96,6 +97,10 @@ class FirefoxAccount internal constructor(
internal fun getAuthState() = inner.getAuthState()
+ internal fun simulateNetworkError() = inner.simulateNetworkError()
+ internal fun simulateTemporaryAuthTokenIssue() = inner.simulateTemporaryAuthTokenIssue()
+ internal fun simulatePermanentAuthTokenIssue() = inner.simulatePermanentAuthTokenIssue()
+
override suspend fun beginOAuthFlow(
scopes: Set<String>,
entryPoint: FxAEntryPoint,
@@ -128,6 +133,12 @@ class FirefoxAccount internal constructor(
}
}
+ override suspend fun setUserData(userData: UserData) {
+ handleFxaExceptions(logger, "setUserData", { null }) {
+ inner.setUserData(userData.into())
+ }
+ }
+
override fun getCurrentDeviceId(): 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.
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FxaDeviceConstellation.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FxaDeviceConstellation.kt
index 99c4fb196f..88e4eebd4c 100644
--- a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FxaDeviceConstellation.kt
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FxaDeviceConstellation.kt
@@ -191,6 +191,9 @@ class FxaDeviceConstellation(
crashReporter?.submitCaughtException(error)
}
}
+ is DeviceCommandOutgoing.CloseTab -> {
+ account.closeTabs(targetDeviceId, outgoingCommand.urls)
+ }
else -> logger.debug("Skipped sending unsupported command type: $outgoingCommand")
}
null
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Types.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Types.kt
index 737ff3c273..d489686cdd 100644
--- a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Types.kt
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Types.kt
@@ -18,6 +18,7 @@ import mozilla.components.concept.sync.DeviceCapability
import mozilla.components.concept.sync.DeviceType
import mozilla.components.concept.sync.OAuthScopedKey
import mozilla.components.concept.sync.SyncAuthInfo
+import mozilla.components.concept.sync.UserData
import mozilla.appservices.fxaclient.DeviceCapability as RustDeviceCapability
import mozilla.appservices.fxaclient.DevicePushSubscription as RustDevicePushSubscription
import mozilla.appservices.sync15.DeviceType as RustDeviceType
@@ -106,6 +107,20 @@ fun Profile.into(): mozilla.components.concept.sync.Profile {
)
}
+/**
+ * Converts the android-components defined [UserData] type into
+ * the application-services one, so consumers of android-components
+ * do not have to know about application services.
+ */
+fun UserData.into(): mozilla.appservices.fxaclient.UserData {
+ return mozilla.appservices.fxaclient.UserData(
+ sessionToken,
+ uid,
+ email,
+ verified,
+ )
+}
+
internal fun RustDeviceType.into(): DeviceType {
return when (this) {
RustDeviceType.DESKTOP -> DeviceType.DESKTOP
@@ -139,6 +154,7 @@ fun DeviceType.into(): RustDeviceType {
fun DeviceCapability.into(): RustDeviceCapability {
return when (this) {
DeviceCapability.SEND_TAB -> RustDeviceCapability.SEND_TAB
+ DeviceCapability.CLOSE_TABS -> RustDeviceCapability.CLOSE_TABS
}
}
@@ -149,6 +165,7 @@ fun DeviceCapability.into(): RustDeviceCapability {
fun RustDeviceCapability.into(): DeviceCapability {
return when (this) {
RustDeviceCapability.SEND_TAB -> DeviceCapability.SEND_TAB
+ RustDeviceCapability.CLOSE_TABS -> DeviceCapability.CLOSE_TABS
}
}
@@ -240,6 +257,7 @@ fun AccountEvent.into(): mozilla.components.concept.sync.AccountEvent {
fun IncomingDeviceCommand.into(): mozilla.components.concept.sync.DeviceCommandIncoming {
return when (this) {
is IncomingDeviceCommand.TabReceived -> this.into()
+ is IncomingDeviceCommand.TabsClosed -> this.into()
}
}
@@ -249,3 +267,10 @@ fun IncomingDeviceCommand.TabReceived.into(): mozilla.components.concept.sync.De
entries = this.payload.entries.map { it.into() },
)
}
+
+fun IncomingDeviceCommand.TabsClosed.into(): mozilla.components.concept.sync.DeviceCommandIncoming.TabsClosed {
+ return mozilla.components.concept.sync.DeviceCommandIncoming.TabsClosed(
+ from = this.sender?.into(),
+ urls = this.payload.urls,
+ )
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/FxaAccountManager.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/FxaAccountManager.kt
index 03a7d25635..71232c0815 100644
--- a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/FxaAccountManager.kt
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/FxaAccountManager.kt
@@ -8,9 +8,11 @@ import android.content.Context
import androidx.annotation.GuardedBy
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LifecycleOwner
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.appservices.fxaclient.FxaStateCheckerEvent
import mozilla.appservices.fxaclient.FxaStateCheckerState
@@ -26,6 +28,7 @@ import mozilla.components.concept.sync.FxAEntryPoint
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.concept.sync.Profile
import mozilla.components.concept.sync.ServiceResult
+import mozilla.components.concept.sync.UserData
import mozilla.components.service.fxa.AccessTokenUnexpectedlyWithoutKey
import mozilla.components.service.fxa.AccountManagerException
import mozilla.components.service.fxa.AccountOnDisk
@@ -344,11 +347,14 @@ open class FxaAccountManager(
* @param pairingUrl Optional pairing URL in case a pairing flow is being initiated.
* @param entrypoint an enum representing the feature entrypoint requesting the URL.
* the entrypoint is used in telemetry.
+ * @param authScopes The oAuth scopes being requested, if none are provided
+ * we default to the scopes provided when constructing [FxaAccountManager]
* @return An authentication url which is to be presented to the user.
*/
suspend fun beginAuthentication(
pairingUrl: String? = null,
entrypoint: FxAEntryPoint,
+ authScopes: Set<String> = scopes,
): String? = withContext(coroutineContext) {
// It's possible that at this point authentication is considered to be "in-progress".
// For example, if user started authentication flow, but cancelled it (closing a custom tab)
@@ -357,9 +363,9 @@ open class FxaAccountManager(
processQueue(Event.Progress.CancelAuth)
val event = if (pairingUrl != null) {
- Event.Account.BeginPairingFlow(pairingUrl, entrypoint)
+ Event.Account.BeginPairingFlow(pairingUrl, entrypoint, authScopes)
} else {
- Event.Account.BeginEmailFlow(entrypoint)
+ Event.Account.BeginEmailFlow(entrypoint, authScopes)
}
// Process the event, then use the new state to check the result of the operation
@@ -375,6 +381,16 @@ open class FxaAccountManager(
}
/**
+ * Sets the user's data received from the web content.
+ * **NOTE**: This is only useful for applications that are user agents, that
+ * require the user's session token, and thus isn't a part of the state machine
+ * @param userData: The user's data as given by the web channel, including the session token
+ */
+ suspend fun setUserData(userData: UserData) = withContext(coroutineContext) {
+ account.setUserData(userData)
+ }
+
+ /**
* Finalize authentication that was started via [beginAuthentication].
*
* If authentication wasn't started via this manager we won't accept this authentication attempt,
@@ -590,10 +606,10 @@ open class FxaAccountManager(
} else {
null
}
- val entrypoint = if (via is Event.Account.BeginEmailFlow) {
- via.entrypoint
+ val (entrypoint, authScopes) = if (via is Event.Account.BeginEmailFlow) {
+ Pair(via.entrypoint, via.scopes)
} else if (via is Event.Account.BeginPairingFlow) {
- via.entrypoint
+ Pair(via.entrypoint, via.scopes)
} else {
// This should be impossible, both `BeginPairingFlow` and `BeginEmailFlow`
// have a required `entrypoint` and we are matching against only instances
@@ -601,7 +617,7 @@ open class FxaAccountManager(
throw IllegalStateException("BeginningAuthentication with a flow that is neither email nor pairing")
}
val result = withRetries(logger, MAX_NETWORK_RETRIES) {
- pairingUrl.asAuthFlowUrl(account, scopes, entrypoint = entrypoint)
+ pairingUrl.asAuthFlowUrl(account, authScopes, entrypoint = entrypoint)
}
when (result) {
is Result.Success -> {
@@ -935,4 +951,60 @@ open class FxaAccountManager(
accountManager.syncStatusObserverRegistry.notifyObservers { onError(error) }
}
}
+
+ /**
+ * Hook this up to the secret debug menu to simulate a network error
+ *
+ * Typical usage is:
+ * - `adb logcat | grep fxa_client`
+ * - Trigger this via the secret debug menu item.
+ * - Watch the logs. You should see the client perform a call to `get_profile', see a
+ * network error, then recover.
+ * - Note: the logs will be more clear once we switch the code to using the app-services state
+ * machine.
+ * - Check the UI, it should be in an authenticated state.
+ */
+ public fun simulateNetworkError() {
+ account.simulateNetworkError()
+ CoroutineScope(coroutineContext).launch {
+ refreshProfile(true)
+ }
+ }
+
+ /**
+ * Hook this up to the secret debug menu to simulate a temporary auth error
+ *
+ * Typical usage is:
+ * - `adb logcat | grep fxa_client`
+ * - Trigger this via the secret debug menu item.
+ * - Watch the logs. You should see the client perform a call to `get_profile', see an
+ * auth error, then recover.
+ * - Check the UI, it should be in an authenticated state.
+ */
+ public fun simulateTemporaryAuthTokenIssue() {
+ account.simulateTemporaryAuthTokenIssue()
+ SyncAuthInfoCache(context).clear()
+ CoroutineScope(coroutineContext).launch {
+ refreshProfile(true)
+ }
+ }
+
+ /**
+ * Hook this up to the secret debug menu to simulate an unrecoverable auth error
+ *
+ * Typical usage is:
+ * - `adb logcat | grep fxa_client`
+ * - Trigger this via the secret debug menu item.
+ * - Initiaite a sync, or perform some other action that requires authentication.
+ * - Watch the logs. You should see the client perform a call to `get_profile', see an
+ * auth error, then fail to recover.
+ * - Check the UI, it should be in an authentication problems state.
+ */
+ public fun simulatePermanentAuthTokenIssue() {
+ account.simulatePermanentAuthTokenIssue()
+ SyncAuthInfoCache(context).clear()
+ CoroutineScope(coroutineContext).launch {
+ refreshProfile(true)
+ }
+ }
}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/State.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/State.kt
index 111715703a..50858c2bbe 100644
--- a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/State.kt
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/State.kt
@@ -52,10 +52,31 @@ import mozilla.components.service.fxa.FxaAuthData
* State transitions are described by a transition matrix, which is described in [State.next].
*/
-internal sealed class AccountState {
+/**
+ * Represents a [State.Idle] in the accounts state machine detailing the state of the account
+ * lifecycle.
+ */
+sealed class AccountState {
+ /**
+ * Account is logged in and authenticated.
+ */
object Authenticated : AccountState()
+
+ /**
+ * Account is authenticating.
+ *
+ * @property oAuthUrl OAuth URL to be loaded to go through the authentication flow.
+ */
data class Authenticating(val oAuthUrl: String) : AccountState()
+
+ /**
+ * Account needs to be re-authenticated (e.g. due to a password change).
+ */
object AuthenticationProblem : AccountState()
+
+ /**
+ * No authenticated account is available (e.g. account is logged out).
+ */
object NotAuthenticated : AccountState()
}
@@ -70,8 +91,12 @@ internal enum class ProgressState {
internal sealed class Event {
internal sealed class Account : Event() {
internal object Start : Account()
- data class BeginEmailFlow(val entrypoint: FxAEntryPoint) : Account()
- data class BeginPairingFlow(val pairingUrl: String?, val entrypoint: FxAEntryPoint) : Account()
+ data class BeginEmailFlow(val entrypoint: FxAEntryPoint, val scopes: Set<String>) : Account()
+ data class BeginPairingFlow(
+ val pairingUrl: String?,
+ val entrypoint: FxAEntryPoint,
+ val scopes: Set<String>,
+ ) : Account()
data class AuthenticationError(val operation: String, val errorCountWithinTheTimeWindow: Int = 1) : Account() {
override fun toString(): String {
return "${this.javaClass.simpleName} - $operation"
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncAction.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncAction.kt
index b09e7f3dcc..eb2d7960cf 100644
--- a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncAction.kt
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncAction.kt
@@ -6,6 +6,7 @@ package mozilla.components.service.fxa.store
import mozilla.components.concept.sync.ConstellationState
import mozilla.components.lib.state.Action
+import mozilla.components.service.fxa.manager.AccountState
/**
* Actions for updating the global [SyncState] via [SyncStore].
@@ -22,6 +23,11 @@ sealed class SyncAction : Action {
data class UpdateAccount(val account: Account?) : SyncAction()
/**
+ * Update the [SyncState.accountState] of the [SyncStore].
+ */
+ data class UpdateAccountState(val accountState: AccountState) : SyncAction()
+
+ /**
* Update the [SyncState.constellationState] of the [SyncStore].
*/
data class UpdateDeviceConstellation(val deviceConstellation: ConstellationState) : SyncAction()
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncState.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncState.kt
index 86c9d05d2a..91e3870251 100644
--- a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncState.kt
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncState.kt
@@ -9,18 +9,21 @@ import mozilla.components.concept.sync.ConstellationState
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.concept.sync.Profile
import mozilla.components.lib.state.State
+import mozilla.components.service.fxa.manager.AccountState
import mozilla.components.service.fxa.sync.WorkManagerSyncManager
/**
* Global state of Sync.
*
* @property status The current status of Sync.
- * @property account The current Sync account, if any.
+ * @property account The current Sync [Account], if any.
+ * @property accountState The current [AccountState] of Sync.
* @property constellationState The current constellation state, if any.
*/
data class SyncState(
val status: SyncStatus = SyncStatus.NotInitialized,
val account: Account? = null,
+ val accountState: AccountState = AccountState.NotAuthenticated,
val constellationState: ConstellationState? = null,
) : State
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStore.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStore.kt
index 50f4b6747e..487d9bd1ea 100644
--- a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStore.kt
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStore.kt
@@ -22,6 +22,7 @@ private fun reduce(syncState: SyncState, syncAction: SyncAction): SyncState {
return when (syncAction) {
is SyncAction.UpdateSyncStatus -> syncState.copy(status = syncAction.status)
is SyncAction.UpdateAccount -> syncState.copy(account = syncAction.account)
+ is SyncAction.UpdateAccountState -> syncState.copy(accountState = syncAction.accountState)
is SyncAction.UpdateDeviceConstellation ->
syncState.copy(constellationState = syncAction.deviceConstellation)
}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStoreSupport.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStoreSupport.kt
index 4feda96e60..7674d8a8fc 100644
--- a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStoreSupport.kt
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStoreSupport.kt
@@ -10,12 +10,15 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.concept.sync.AccountObserver
+import mozilla.components.concept.sync.AuthFlowError
import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.ConstellationState
import mozilla.components.concept.sync.DeviceConstellationObserver
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.concept.sync.Profile
+import mozilla.components.service.fxa.manager.AccountState
import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.service.fxa.manager.SCOPE_PROFILE
import mozilla.components.service.fxa.sync.SyncStatusObserver
import java.lang.Exception
@@ -95,6 +98,24 @@ internal class FxaAccountObserver(
private val autoPause: Boolean,
private val coroutineScope: CoroutineScope,
) : AccountObserver {
+ override fun onReady(authenticatedAccount: OAuthAccount?) {
+ coroutineScope.launch {
+ if (authenticatedAccount == null) {
+ return@launch
+ }
+
+ val syncAccount =
+ authenticatedAccount.getProfile()?.toAccount(authenticatedAccount) ?: return@launch
+ store.dispatch(SyncAction.UpdateAccount(account = syncAccount))
+
+ val accountState = when (authenticatedAccount.checkAuthorizationStatus(SCOPE_PROFILE)) {
+ true -> AccountState.Authenticated
+ null, false -> AccountState.AuthenticationProblem
+ }
+ store.dispatch(SyncAction.UpdateAccountState(accountState = accountState))
+ }
+ }
+
override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
coroutineScope.launch(Dispatchers.Main) {
account.deviceConstellation().registerDeviceObserver(
@@ -106,12 +127,23 @@ internal class FxaAccountObserver(
coroutineScope.launch {
val syncAccount = account.getProfile()?.toAccount(account) ?: return@launch
store.dispatch(SyncAction.UpdateAccount(syncAccount))
+ store.dispatch(SyncAction.UpdateAccountState(AccountState.Authenticated))
}
}
+ override fun onAuthenticationProblems() {
+ store.dispatch(SyncAction.UpdateAccountState(accountState = AccountState.AuthenticationProblem))
+ }
+
override fun onLoggedOut() {
store.dispatch(SyncAction.UpdateSyncStatus(SyncStatus.LoggedOut))
store.dispatch(SyncAction.UpdateAccount(null))
+ store.dispatch(SyncAction.UpdateAccountState(accountState = AccountState.NotAuthenticated))
+ }
+
+ override fun onFlowError(error: AuthFlowError) {
+ store.dispatch(SyncAction.UpdateAccount(account = null))
+ store.dispatch(SyncAction.UpdateAccountState(accountState = AccountState.NotAuthenticated))
}
}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaAccountManagerTest.kt b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaAccountManagerTest.kt
index afc2fc3ee9..3c6a3421f2 100644
--- a/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaAccountManagerTest.kt
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaAccountManagerTest.kt
@@ -74,13 +74,13 @@ internal class TestableStorageWrapper(
manager: FxaAccountManager,
accountEventObserverRegistry: ObserverRegistry<AccountEventsObserver>,
serverConfig: FxaConfig,
- private val block: () -> OAuthAccount = {
- val account: OAuthAccount = mock()
+ private val block: () -> FirefoxAccount = {
+ val account: FirefoxAccount = mock()
`when`(account.deviceConstellation()).thenReturn(mock())
account
},
) : StorageWrapper(manager, accountEventObserverRegistry, serverConfig) {
- override fun obtainAccount(): OAuthAccount = block()
+ override fun obtainAccount(): FirefoxAccount = block()
}
// Same as the actual account manager, except we get to control how FirefoxAccountShaped instances
@@ -95,8 +95,8 @@ internal open class TestableFxaAccountManager(
syncConfig: SyncConfig? = null,
coroutineContext: CoroutineContext,
crashReporter: CrashReporting? = null,
- block: () -> OAuthAccount = {
- val account: OAuthAccount = mock()
+ block: () -> FirefoxAccount = {
+ val account: FirefoxAccount = mock()
`when`(account.deviceConstellation()).thenReturn(mock())
account
},
@@ -201,7 +201,7 @@ class FxaAccountManagerTest {
val accountStorage: AccountStorage = mock()
val profile = Profile("testUid", "test@example.com", null, "Test Profile")
val constellation: DeviceConstellation = mockDeviceConstellation()
- val account = StatePersistenceTestableAccount(profile, constellation)
+ val account = statePersistenceTestableAccount(profile, constellation)
val manager = TestableFxaAccountManager(
testContext,
@@ -218,17 +218,18 @@ class FxaAccountManagerTest {
// We have an account at the start.
`when`(accountStorage.read()).thenReturn(account)
- assertNull(account.persistenceCallback)
+ verify(account, never()).registerPersistenceCallback(any())
manager.start()
// Assert that persistence callback is set.
- assertNotNull(account.persistenceCallback)
+ val captor = argumentCaptor<StatePersistenceCallback>()
+ verify(account).registerPersistenceCallback(captor.capture())
// Assert that ensureCapabilities fired, but not the device initialization (since we're restoring).
verify(constellation).finalizeDevice(eq(AuthType.Existing), any())
// Assert that persistence callback is interacting with the storage layer.
- account.persistenceCallback!!.persist("test")
+ captor.value.persist("test")
verify(accountStorage).write("test")
}
@@ -237,7 +238,7 @@ class FxaAccountManagerTest {
val accountStorage: AccountStorage = mock()
val profile = Profile("testUid", "test@example.com", null, "Test Profile")
val constellation: DeviceConstellation = mockDeviceConstellation()
- val account = StatePersistenceTestableAccount(profile, constellation)
+ val account = statePersistenceTestableAccount(profile, constellation)
val manager = TestableFxaAccountManager(
testContext,
@@ -254,17 +255,18 @@ class FxaAccountManagerTest {
// We have an account at the start.
`when`(accountStorage.read()).thenReturn(account)
- assertNull(account.persistenceCallback)
+ verify(account, never()).registerPersistenceCallback(any())
manager.start()
// Assert that persistence callback is set.
- assertNotNull(account.persistenceCallback)
+ val captor = argumentCaptor<StatePersistenceCallback>()
+ verify(account).registerPersistenceCallback(captor.capture())
// Assert that finalizeDevice fired with a correct auth type. 3 times since we re-try.
verify(constellation, times(3)).finalizeDevice(eq(AuthType.Existing), any())
// Assert that persistence callback is interacting with the storage layer.
- account.persistenceCallback!!.persist("test")
+ captor.value.persist("test")
verify(accountStorage).write("test")
// Since we weren't able to finalize the account state, we're no longer authenticated.
@@ -276,7 +278,7 @@ class FxaAccountManagerTest {
val accountStorage: AccountStorage = mock()
val profile = Profile("testUid", "test@example.com", null, "Test Profile")
val constellation: DeviceConstellation = mockDeviceConstellation()
- val account = StatePersistenceTestableAccount(profile, constellation, ableToRecoverFromAuthError = false)
+ val account = statePersistenceTestableAccount(profile, constellation, ableToRecoverFromAuthError = false)
val accountObserver: AccountObserver = mock()
val manager = TestableFxaAccountManager(
@@ -295,19 +297,19 @@ class FxaAccountManagerTest {
// We have an account at the start.
`when`(accountStorage.read()).thenReturn(account)
- assertNull(account.persistenceCallback)
+ verify(account, never()).registerPersistenceCallback(any())
assertFalse(manager.accountNeedsReauth())
- assertFalse(account.authErrorDetectedCalled)
- assertFalse(account.checkAuthorizationStatusCalled)
+ verify(account, never()).authErrorDetected()
+ verify(account, never()).checkAuthorizationStatus(any())
verify(accountObserver, never()).onAuthenticationProblems()
manager.start()
assertTrue(manager.accountNeedsReauth())
verify(accountObserver, times(1)).onAuthenticationProblems()
- assertTrue(account.authErrorDetectedCalled)
- assertTrue(account.checkAuthorizationStatusCalled)
+ verify(account).authErrorDetected()
+ verify(account).checkAuthorizationStatus(any())
}
@Test(expected = FxaPanicException::class)
@@ -315,7 +317,7 @@ class FxaAccountManagerTest {
val accountStorage: AccountStorage = mock()
val profile = Profile("testUid", "test@example.com", null, "Test Profile")
val constellation: DeviceConstellation = mock()
- val account = StatePersistenceTestableAccount(profile, constellation)
+ val account = statePersistenceTestableAccount(profile, constellation)
val accountObserver: AccountObserver = mock()
val manager = TestableFxaAccountManager(
@@ -339,7 +341,7 @@ class FxaAccountManagerTest {
// We have an account at the start.
`when`(accountStorage.read()).thenReturn(account)
- assertNull(account.persistenceCallback)
+ verify(account, never()).registerPersistenceCallback(any())
assertFalse(manager.accountNeedsReauth())
verify(accountObserver, never()).onAuthenticationProblems()
@@ -352,7 +354,7 @@ class FxaAccountManagerTest {
val accountStorage: AccountStorage = mock()
val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
val constellation: DeviceConstellation = mockDeviceConstellation()
- val account = StatePersistenceTestableAccount(profile, constellation)
+ val account = statePersistenceTestableAccount(profile, constellation)
val accountObserver: AccountObserver = mock()
// We are not using the "prepareHappy..." helper method here, because our account isn't a mock,
// but an actual implementation of the interface.
@@ -388,7 +390,9 @@ class FxaAccountManagerTest {
verify(constellation).finalizeDevice(eq(AuthType.Signin), any())
// Assert that persistence callback is interacting with the storage layer.
- account.persistenceCallback!!.persist("test")
+ val captor = argumentCaptor<StatePersistenceCallback>()
+ verify(account).registerPersistenceCallback(captor.capture())
+ captor.value.persist("test")
verify(accountStorage).write("test")
}
@@ -397,7 +401,7 @@ class FxaAccountManagerTest {
val accountStorage: AccountStorage = mock()
val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
val constellation: DeviceConstellation = mockDeviceConstellation()
- val account = StatePersistenceTestableAccount(profile, constellation)
+ val account = statePersistenceTestableAccount(profile, constellation)
val accountObserver: AccountObserver = mock()
// We are not using the "prepareHappy..." helper method here, because our account isn't a mock,
// but an actual implementation of the interface.
@@ -423,7 +427,7 @@ class FxaAccountManagerTest {
manager.finishAuthentication(FxaAuthData(AuthType.Signin, "dummyCode", UNEXPECTED_AUTH_STATE))
assertTrue(manager.authenticatedAccount() == null)
- // Start authentication. StatePersistenceTestableAccount will produce state=EXPECTED_AUTH_STATE.
+ // Start authentication. statePersistenceTestableAccount will produce state=EXPECTED_AUTH_STATE.
assertEquals(testAuthFlowUrl(entrypoint = "home-menu").url, manager.beginAuthentication(entrypoint = entryPoint))
// Now attempt to finish it with a correct state.
@@ -435,97 +439,17 @@ class FxaAccountManagerTest {
assertEquals(account, manager.authenticatedAccount())
}
- class StatePersistenceTestableAccount(
- private val profile: Profile,
- private val constellation: DeviceConstellation,
- val ableToRecoverFromAuthError: Boolean = false,
- val tokenServerEndpointUrl: String? = null,
- val accessToken: (() -> AccessTokenInfo)? = null,
- ) : OAuthAccount {
-
- var persistenceCallback: StatePersistenceCallback? = null
- var checkAuthorizationStatusCalled = false
- var authErrorDetectedCalled = false
-
- override suspend fun beginOAuthFlow(scopes: Set<String>, entryPoint: FxAEntryPoint): AuthFlowUrl? {
- return AuthFlowUrl(EXPECTED_AUTH_STATE, testAuthFlowUrl(entrypoint = entryPoint.entryName).url)
- }
-
- override suspend fun beginPairingFlow(pairingUrl: String, scopes: Set<String>, entryPoint: FxAEntryPoint): AuthFlowUrl? {
- return AuthFlowUrl(EXPECTED_AUTH_STATE, testAuthFlowUrl(entrypoint = entryPoint.entryName).url)
- }
-
- override suspend fun getProfile(ignoreCache: Boolean): Profile? {
- return profile
- }
-
- override fun getCurrentDeviceId(): String? {
- return "testFxaDeviceId"
- }
-
- override fun getSessionToken(): String? {
- return null
- }
-
- override suspend fun completeOAuthFlow(code: String, state: String): Boolean {
- return true
- }
-
- override suspend fun getAccessToken(singleScope: String): AccessTokenInfo? {
- val token = accessToken?.invoke()
- if (token != null) return token
-
- fail()
- return null
- }
-
- override fun authErrorDetected() {
- authErrorDetectedCalled = true
- }
-
- override suspend fun checkAuthorizationStatus(singleScope: String): Boolean? {
- checkAuthorizationStatusCalled = true
- return ableToRecoverFromAuthError
- }
-
- override suspend fun getTokenServerEndpointURL(): String? {
- if (tokenServerEndpointUrl != null) return tokenServerEndpointUrl
-
- fail()
- return ""
- }
-
- override suspend fun getManageAccountURL(entryPoint: FxAEntryPoint): String? {
- return "https://firefox.com/settings"
- }
-
- override fun getPairingAuthorityURL(): String {
- return "https://firefox.com/pair"
- }
-
- override fun registerPersistenceCallback(callback: StatePersistenceCallback) {
- persistenceCallback = callback
- }
-
- override fun deviceConstellation(): DeviceConstellation {
- return constellation
- }
-
- override suspend fun disconnect(): Boolean {
- return true
- }
-
- override fun toJSONString(): String {
- fail()
- return ""
- }
-
- override fun close() {
- // Only expect 'close' to be called if we can't recover from an auth error.
- if (ableToRecoverFromAuthError) {
- fail()
- }
- }
+ suspend fun statePersistenceTestableAccount(profile: Profile, constellation: DeviceConstellation, ableToRecoverFromAuthError: Boolean = false): FirefoxAccount {
+ val account = mock<FirefoxAccount>()
+ `when`(account.getProfile(anyBoolean())).thenReturn(profile)
+ `when`(account.deviceConstellation()).thenReturn(constellation)
+ `when`(account.checkAuthorizationStatus(any())).thenReturn(ableToRecoverFromAuthError)
+ `when`(account.beginOAuthFlow(any(), any())).thenReturn(testAuthFlowUrl(entrypoint = "home-menu"))
+ `when`(account.beginPairingFlow(any(), any(), any())).thenReturn(testAuthFlowUrl(entrypoint = "home-menu"))
+ `when`(account.completeOAuthFlow(anyString(), anyString())).thenReturn(true)
+ `when`(account.getCurrentDeviceId()).thenReturn("testFxaDeviceId")
+
+ return account
}
@Test
@@ -596,7 +520,7 @@ class FxaAccountManagerTest {
@Test
fun `with persisted account and profile`() = runTest {
val accountStorage = mock<AccountStorage>()
- val mockAccount: OAuthAccount = mock()
+ val mockAccount: FirefoxAccount = mock()
val constellation: DeviceConstellation = mock()
val profile = Profile(
"testUid",
@@ -667,7 +591,7 @@ class FxaAccountManagerTest {
@Test
fun `happy authentication and profile flow`() = runTest {
- val mockAccount: OAuthAccount = mock()
+ val mockAccount: FirefoxAccount = mock()
val constellation: DeviceConstellation = mock()
`when`(mockAccount.deviceConstellation()).thenReturn(constellation)
val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
@@ -712,7 +636,7 @@ class FxaAccountManagerTest {
@Test(expected = FxaPanicException::class)
fun `fxa panic during initDevice flow`() = runTest {
- val mockAccount: OAuthAccount = mock()
+ val mockAccount: FirefoxAccount = mock()
val constellation: DeviceConstellation = mock()
`when`(mockAccount.deviceConstellation()).thenReturn(constellation)
val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
@@ -740,7 +664,7 @@ class FxaAccountManagerTest {
@Test(expected = FxaPanicException::class)
fun `fxa panic during pairing flow`() = runTest {
- val mockAccount: OAuthAccount = mock()
+ val mockAccount: FirefoxAccount = mock()
`when`(mockAccount.deviceConstellation()).thenReturn(mock())
val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
val accountStorage = mock<AccountStorage>()
@@ -769,7 +693,7 @@ class FxaAccountManagerTest {
@Test
fun `happy pairing authentication and profile flow`() = runTest {
- val mockAccount: OAuthAccount = mock()
+ val mockAccount: FirefoxAccount = mock()
val constellation: DeviceConstellation = mock()
`when`(mockAccount.deviceConstellation()).thenReturn(constellation)
val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
@@ -806,7 +730,7 @@ class FxaAccountManagerTest {
@Test
fun `repeated unfinished authentication attempts succeed`() = runTest {
- val mockAccount: OAuthAccount = mock()
+ val mockAccount: FirefoxAccount = mock()
val constellation: DeviceConstellation = mock()
`when`(mockAccount.deviceConstellation()).thenReturn(constellation)
val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
@@ -863,7 +787,7 @@ class FxaAccountManagerTest {
@Test
fun `unhappy authentication flow`() = runTest {
val accountStorage = mock<AccountStorage>()
- val mockAccount: OAuthAccount = mock()
+ val mockAccount: FirefoxAccount = mock()
val constellation: DeviceConstellation = mock()
val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
val accountObserver: AccountObserver = mock()
@@ -911,7 +835,7 @@ class FxaAccountManagerTest {
@Test
fun `unhappy pairing authentication flow`() = runTest {
val accountStorage = mock<AccountStorage>()
- val mockAccount: OAuthAccount = mock()
+ val mockAccount: FirefoxAccount = mock()
val constellation: DeviceConstellation = mock()
val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
val accountObserver: AccountObserver = mock()
@@ -970,7 +894,7 @@ class FxaAccountManagerTest {
@Test
fun `authentication issues are propagated via AccountObserver`() = runTest {
- val mockAccount: OAuthAccount = mock()
+ val mockAccount: FirefoxAccount = mock()
val constellation: DeviceConstellation = mock()
`when`(mockAccount.deviceConstellation()).thenReturn(constellation)
val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
@@ -1025,7 +949,7 @@ class FxaAccountManagerTest {
@Test
fun `authentication issues are recoverable via checkAuthorizationState`() = runTest {
- val mockAccount: OAuthAccount = mock()
+ val mockAccount: FirefoxAccount = mock()
val constellation: DeviceConstellation = mock()
`when`(mockAccount.deviceConstellation()).thenReturn(constellation)
val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
@@ -1072,7 +996,7 @@ class FxaAccountManagerTest {
@Test
fun `authentication recovery flow has a circuit breaker`() = runTest {
- val mockAccount: OAuthAccount = mock()
+ val mockAccount: FirefoxAccount = mock()
val constellation: DeviceConstellation = mock()
`when`(mockAccount.deviceConstellation()).thenReturn(constellation)
val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
@@ -1163,7 +1087,7 @@ class FxaAccountManagerTest {
@Test
fun `unhappy profile fetching flow`() = runTest {
val accountStorage = mock<AccountStorage>()
- val mockAccount: OAuthAccount = mock()
+ val mockAccount: FirefoxAccount = mock()
val constellation: DeviceConstellation = mock()
`when`(mockAccount.deviceConstellation()).thenReturn(constellation)
@@ -1232,7 +1156,7 @@ class FxaAccountManagerTest {
@Test
fun `profile fetching flow hit an unrecoverable auth problem`() = runTest {
val accountStorage = mock<AccountStorage>()
- val mockAccount: OAuthAccount = mock()
+ val mockAccount: FirefoxAccount = mock()
val constellation: DeviceConstellation = mock()
`when`(mockAccount.getCurrentDeviceId()).thenReturn("testDeviceId")
@@ -1292,7 +1216,7 @@ class FxaAccountManagerTest {
@Test
fun `profile fetching flow hit an unrecoverable auth problem for which we can't determine a recovery state`() = runTest {
val accountStorage = mock<AccountStorage>()
- val mockAccount: OAuthAccount = mock()
+ val mockAccount: FirefoxAccount = mock()
val constellation: DeviceConstellation = mock()
`when`(mockAccount.deviceConstellation()).thenReturn(constellation)
@@ -1353,7 +1277,7 @@ class FxaAccountManagerTest {
@Test
fun `profile fetching flow hit a recoverable auth problem`() = runTest {
val accountStorage = mock<AccountStorage>()
- val mockAccount: OAuthAccount = mock()
+ val mockAccount: FirefoxAccount = mock()
val constellation: DeviceConstellation = mock()
val captor = argumentCaptor<AuthType>()
@@ -1437,7 +1361,7 @@ class FxaAccountManagerTest {
@Test(expected = FxaPanicException::class)
fun `profile fetching flow hit an fxa panic, which is re-thrown`() = runTest {
val accountStorage = mock<AccountStorage>()
- val mockAccount: OAuthAccount = mock()
+ val mockAccount: FirefoxAccount = mock()
val constellation: DeviceConstellation = mock()
`when`(mockAccount.getCurrentDeviceId()).thenReturn("testDeviceId")
@@ -1564,7 +1488,7 @@ class FxaAccountManagerTest {
}
private suspend fun prepareHappyAuthenticationFlow(
- mockAccount: OAuthAccount,
+ mockAccount: FirefoxAccount,
profile: Profile,
accountStorage: AccountStorage,
accountObserver: AccountObserver,
@@ -1608,7 +1532,7 @@ class FxaAccountManagerTest {
}
private suspend fun prepareUnhappyAuthenticationFlow(
- mockAccount: OAuthAccount,
+ mockAccount: FirefoxAccount,
profile: Profile,
accountStorage: AccountStorage,
accountObserver: AccountObserver,
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/manager/StateKtTest.kt b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/manager/StateKtTest.kt
index 6323861f0e..df37088ca7 100644
--- a/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/manager/StateKtTest.kt
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/manager/StateKtTest.kt
@@ -83,8 +83,8 @@ class StateKtTest {
private fun instantiateEvent(eventClassSimpleName: String): Event {
return when (eventClassSimpleName) {
"Start" -> Event.Account.Start
- "BeginPairingFlow" -> Event.Account.BeginPairingFlow("http://some.pairing.url.com", mock())
- "BeginEmailFlow" -> Event.Account.BeginEmailFlow(mock())
+ "BeginPairingFlow" -> Event.Account.BeginPairingFlow("http://some.pairing.url.com", mock(), mock())
+ "BeginEmailFlow" -> Event.Account.BeginEmailFlow(mock(), mock())
"CancelAuth" -> Event.Progress.CancelAuth
"StartedOAuthFlow" -> Event.Progress.StartedOAuthFlow("https://example.com/oauth-start")
"AuthenticationError" -> Event.Account.AuthenticationError("fxa op")
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/store/SyncStoreSupportTest.kt b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/store/SyncStoreSupportTest.kt
index e0d90118fa..975fd6a481 100644
--- a/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/store/SyncStoreSupportTest.kt
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/store/SyncStoreSupportTest.kt
@@ -12,13 +12,16 @@ import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
+import mozilla.components.concept.sync.AuthFlowError
import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.Avatar
import mozilla.components.concept.sync.ConstellationState
import mozilla.components.concept.sync.DeviceConstellation
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.concept.sync.Profile
+import mozilla.components.service.fxa.manager.AccountState
import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.service.fxa.manager.SCOPE_PROFILE
import mozilla.components.support.test.any
import mozilla.components.support.test.coMock
import mozilla.components.support.test.eq
@@ -26,9 +29,11 @@ import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.mock
import mozilla.components.support.test.whenever
import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
import java.lang.Exception
@OptIn(ExperimentalCoroutinesApi::class)
@@ -115,7 +120,7 @@ class SyncStoreSupportTest {
}
@Test
- fun `GIVEN account observer WHEN onAuthenticated observed with profile THEN account state updated`() = coroutineScope.runTest {
+ fun `GIVEN account observer WHEN onAuthenticated observed with profile THEN account and account state are updated`() = coroutineScope.runTest {
val profile = generateProfile()
val constellation = mock<DeviceConstellation>()
val account = coMock<OAuthAccount> {
@@ -125,6 +130,8 @@ class SyncStoreSupportTest {
whenever(getProfile()).thenReturn(profile)
}
+ assertEquals(AccountState.NotAuthenticated, store.state.accountState)
+
accountObserver.onAuthenticated(account, mock<AuthType.Existing>())
runCurrent()
@@ -138,10 +145,11 @@ class SyncStoreSupportTest {
)
store.waitUntilIdle()
assertEquals(expected, store.state.account)
+ assertEquals(AccountState.Authenticated, store.state.accountState)
}
@Test
- fun `GIVEN account observer WHEN onAuthenticated observed without profile THEN account not updated`() = coroutineScope.runTest {
+ fun `GIVEN account observer WHEN onAuthenticated observed without profile THEN account and account state are not updated`() = coroutineScope.runTest {
val constellation = mock<DeviceConstellation>()
val account = coMock<OAuthAccount> {
whenever(deviceConstellation()).thenReturn(constellation)
@@ -152,11 +160,12 @@ class SyncStoreSupportTest {
runCurrent()
store.waitUntilIdle()
- assertEquals(null, store.state.account)
+ assertNull(store.state.account)
+ assertEquals(AccountState.NotAuthenticated, store.state.accountState)
}
@Test
- fun `GIVEN user is logged in WHEN onLoggedOut observed THEN sync status and account updated`() = coroutineScope.runTest {
+ fun `GIVEN user is logged in WHEN onLoggedOut observed THEN sync status and account states are updated`() = coroutineScope.runTest {
val account = coMock<OAuthAccount> {
whenever(deviceConstellation()).thenReturn(mock())
whenever(getProfile()).thenReturn(null)
@@ -169,7 +178,94 @@ class SyncStoreSupportTest {
store.waitUntilIdle()
assertEquals(SyncStatus.LoggedOut, store.state.status)
- assertEquals(null, store.state.account)
+ assertNull(store.state.account)
+ assertEquals(AccountState.NotAuthenticated, store.state.accountState)
+ }
+
+ @Test
+ fun `GIVEN account observer WHEN onAuthenticationProblems observed THEN account state is updated`() = coroutineScope.runTest {
+ assertEquals(AccountState.NotAuthenticated, store.state.accountState)
+
+ accountObserver.onAuthenticationProblems()
+ runCurrent()
+
+ store.waitUntilIdle()
+ assertEquals(AccountState.AuthenticationProblem, store.state.accountState)
+ }
+
+ @Test
+ fun `GIVEN account observer WHEN onFlowError observed THEN account state is updated`() = coroutineScope.runTest {
+ assertNull(store.state.account)
+ assertEquals(AccountState.NotAuthenticated, store.state.accountState)
+
+ accountObserver.onFlowError(mock<AuthFlowError>())
+ runCurrent()
+
+ store.waitUntilIdle()
+ assertNull(store.state.account)
+ assertEquals(AccountState.NotAuthenticated, store.state.accountState)
+ }
+
+ @Test
+ fun `GIVEN account observer WHEN onReady observed with profile THEN account states are updated`() = coroutineScope.runTest {
+ val profile = generateProfile()
+ val currentDeviceId = "id"
+ val sessionToken = "token"
+ val constellation = mock<DeviceConstellation>()
+ val authenticatedAccount = coMock<OAuthAccount> {
+ whenever(deviceConstellation()).thenReturn(constellation)
+ whenever(getCurrentDeviceId()).thenReturn(currentDeviceId)
+ whenever(getSessionToken()).thenReturn(sessionToken)
+ whenever(getProfile()).thenReturn(profile)
+ }
+ val account = Account(
+ uid = profile.uid,
+ email = profile.email,
+ avatar = profile.avatar,
+ displayName = profile.displayName,
+ currentDeviceId = currentDeviceId,
+ sessionToken = sessionToken,
+ )
+
+ assertNull(store.state.account)
+ assertEquals(AccountState.NotAuthenticated, store.state.accountState)
+
+ `when`(authenticatedAccount.checkAuthorizationStatus(eq(SCOPE_PROFILE))).thenReturn(false)
+
+ accountObserver.onReady(authenticatedAccount = authenticatedAccount)
+ runCurrent()
+
+ store.waitUntilIdle()
+ assertEquals(account, store.state.account)
+ assertEquals(AccountState.AuthenticationProblem, store.state.accountState)
+
+ `when`(authenticatedAccount.checkAuthorizationStatus(eq(SCOPE_PROFILE))).thenReturn(true)
+
+ accountObserver.onReady(authenticatedAccount = authenticatedAccount)
+ runCurrent()
+
+ store.waitUntilIdle()
+ assertEquals(account, store.state.account)
+ assertEquals(AccountState.Authenticated, store.state.accountState)
+ }
+
+ @Test
+ fun `GIVEN account observer WHEN onReady observed without profile THEN account states are not updated`() = coroutineScope.runTest {
+ val constellation = mock<DeviceConstellation>()
+ val account = coMock<OAuthAccount> {
+ whenever(deviceConstellation()).thenReturn(constellation)
+ whenever(getProfile()).thenReturn(null)
+ }
+
+ assertNull(store.state.account)
+ assertEquals(AccountState.NotAuthenticated, store.state.accountState)
+
+ accountObserver.onReady(account)
+ runCurrent()
+
+ store.waitUntilIdle()
+ assertNull(store.state.account)
+ assertEquals(AccountState.NotAuthenticated, store.state.accountState)
}
@Test
diff --git a/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/Glean.kt b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/Glean.kt
index 232ac1b83f..2990220b31 100644
--- a/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/Glean.kt
+++ b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/Glean.kt
@@ -116,8 +116,8 @@ object Glean {
*
* @param enabled Map of metrics' enabled state.
*/
- fun setMetricsEnabledConfig(enabled: Map<String, Boolean>) {
- GleanCore.setMetricsEnabledConfig(JSONObject(enabled).toString())
+ fun applyServerKnobsConfig(enabled: Map<String, Boolean>) {
+ GleanCore.applyServerKnobsConfig(JSONObject(enabled).toString())
}
/**
diff --git a/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/config/Configuration.kt b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/config/Configuration.kt
index 11911c7046..382078d3c9 100644
--- a/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/config/Configuration.kt
+++ b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/config/Configuration.kt
@@ -19,6 +19,8 @@ import mozilla.telemetry.glean.config.Configuration as GleanCoreConfiguration
* @property channel (optional )the release channel the application is on, if known. This will be
* sent along with all the pings, in the `client_info` section.
* @property maxEvents (optional) the number of events to store before the events ping is sent
+ * @property enableEventTimestamps (Experimental) Whether to add a wallclock timestamp to all events.
+ * @property delayPingLifetimeIo Whether Glean should delay persistence of data from metrics with ping lifetime.
*/
data class Configuration @JvmOverloads constructor(
val httpClient: PingUploader,
@@ -26,6 +28,7 @@ data class Configuration @JvmOverloads constructor(
val channel: String? = null,
val maxEvents: Int? = null,
val enableEventTimestamps: Boolean = false,
+ val delayPingLifetimeIo: Boolean = false,
) {
// The following is required to support calling our API from Java.
companion object {
@@ -45,6 +48,7 @@ data class Configuration @JvmOverloads constructor(
maxEvents = maxEvents,
httpClient = httpClient,
enableEventTimestamps = enableEventTimestamps,
+ delayPingLifetimeIo = delayPingLifetimeIo,
)
}
}
diff --git a/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/private/MetricAliases.kt b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/private/MetricAliases.kt
index e6e0be9424..b98af6a480 100644
--- a/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/private/MetricAliases.kt
+++ b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/private/MetricAliases.kt
@@ -4,8 +4,6 @@
package mozilla.components.service.glean.private
-import androidx.annotation.VisibleForTesting
-
typealias CommonMetricData = mozilla.telemetry.glean.private.CommonMetricData
typealias EventExtras = mozilla.telemetry.glean.private.EventExtras
typealias Lifetime = mozilla.telemetry.glean.private.Lifetime
@@ -18,12 +16,14 @@ typealias CounterMetricType = mozilla.telemetry.glean.private.CounterMetricType
typealias CustomDistributionMetricType = mozilla.telemetry.glean.private.CustomDistributionMetricType
typealias DatetimeMetricType = mozilla.telemetry.glean.private.DatetimeMetricType
typealias DenominatorMetricType = mozilla.telemetry.glean.private.DenominatorMetricType
+typealias EventMetricType<T> = mozilla.telemetry.glean.private.EventMetricType<T>
typealias HistogramMetricBase = mozilla.telemetry.glean.private.HistogramBase
typealias HistogramType = mozilla.telemetry.glean.private.HistogramType
typealias LabeledMetricType<T> = mozilla.telemetry.glean.private.LabeledMetricType<T>
typealias MemoryDistributionMetricType = mozilla.telemetry.glean.private.MemoryDistributionMetricType
typealias MemoryUnit = mozilla.telemetry.glean.private.MemoryUnit
typealias NumeratorMetricType = mozilla.telemetry.glean.private.NumeratorMetricType
+typealias ObjectSerialize = mozilla.telemetry.glean.private.ObjectSerialize
typealias PingType<T> = mozilla.telemetry.glean.private.PingType<T>
typealias QuantityMetricType = mozilla.telemetry.glean.private.QuantityMetricType
typealias RateMetricType = mozilla.telemetry.glean.private.RateMetricType
@@ -31,89 +31,8 @@ typealias RecordedExperiment = mozilla.telemetry.glean.private.RecordedExperimen
typealias StringListMetricType = mozilla.telemetry.glean.private.StringListMetricType
typealias StringMetricType = mozilla.telemetry.glean.private.StringMetricType
typealias TextMetricType = mozilla.telemetry.glean.private.TextMetricType
-typealias TimeUnit = mozilla.telemetry.glean.private.TimeUnit
typealias TimespanMetricType = mozilla.telemetry.glean.private.TimespanMetricType
+typealias TimeUnit = mozilla.telemetry.glean.private.TimeUnit
typealias TimingDistributionMetricType = mozilla.telemetry.glean.private.TimingDistributionMetricType
typealias UrlMetricType = mozilla.telemetry.glean.private.UrlMetricType
typealias UuidMetricType = mozilla.telemetry.glean.private.UuidMetricType
-
-// FIXME(bug 1885170): Wrap the Glean SDK `EventMetricType` to overwrite the `testGetValue` function.
-/**
- * This implements the developer facing API for recording events.
- *
- * Instances of this class type are automatically generated by the parsers at built time,
- * allowing developers to record events that were previously registered in the metrics.yaml file.
- *
- * The Events API only exposes the [record] method, which takes care of validating the input
- * data and making sure that limits are enforced.
- */
-class EventMetricType<ExtraObject> internal constructor(
- private var inner: mozilla.telemetry.glean.private.EventMetricType<ExtraObject>,
-) where ExtraObject : EventExtras {
- /**
- * The public constructor used by automatically generated metrics.
- */
- constructor(meta: CommonMetricData, allowedExtraKeys: List<String>) :
- this(inner = mozilla.telemetry.glean.private.EventMetricType(meta, allowedExtraKeys))
-
- /**
- * Record an event by using the information provided by the instance of this class.
- *
- * @param extra The event extra properties.
- * Values are converted to strings automatically
- * This is used for events where additional richer context is needed.
- * The maximum length for values is 100 bytes.
- *
- * Note: `extra` is not optional here to avoid overlapping with the above definition of `record`.
- * If no `extra` data is passed the above function will be invoked correctly.
- */
- fun record(extra: ExtraObject? = null) {
- inner.record(extra)
- }
-
- /**
- * Returns the stored value for testing purposes only. This function will attempt to await the
- * last task (if any) writing to the the metric's storage engine before returning a value.
- *
- * @param pingName represents the name of the ping to retrieve the metric for.
- * Defaults to the first value in `sendInPings`.
- * @return value of the stored events
- */
- @VisibleForTesting(otherwise = VisibleForTesting.NONE)
- @JvmOverloads
- fun testGetValue(pingName: String? = null): List<mozilla.telemetry.glean.private.RecordedEvent>? {
- var events = inner.testGetValue(pingName)
- if (events == null) {
- return events
- }
-
- // Remove the `glean_timestamp` extra.
- // This is added by Glean and does not need to be exposed to testing.
- for (event in events) {
- if (event.extra == null) {
- continue
- }
-
- // We know it's not null
- var map = event.extra!!.toMutableMap()
- map.remove("glean_timestamp")
- if (map.isEmpty()) {
- event.extra = null
- } else {
- event.extra = map
- }
- }
-
- return events
- }
-
- /**
- * Returns the number of errors recorded for the given metric.
- *
- * @param errorType The type of the error recorded.
- * @return the number of errors recorded for the metric.
- */
- @VisibleForTesting(otherwise = VisibleForTesting.NONE)
- fun testGetNumRecordedErrors(errorType: mozilla.components.service.glean.testing.ErrorType) =
- inner.testGetNumRecordedErrors(errorType)
-}
diff --git a/mobile/android/android-components/components/service/nimbus/messaging.fml.yaml b/mobile/android/android-components/components/service/nimbus/messaging.fml.yaml
index c53456e31e..a37818f8e8 100644
--- a/mobile/android/android-components/components/service/nimbus/messaging.fml.yaml
+++ b/mobile/android/android-components/components/service/nimbus/messaging.fml.yaml
@@ -111,16 +111,20 @@ objects:
default: {}
title:
type: Option<Text>
- description: "The title text displayed to the user"
+ description: The title text displayed to the user
default: null
text:
type: Text
- description: "The message text displayed to the user"
+ description: The message text displayed to the user
# This should never be defaulted.
default: ""
+ microsurveyConfig:
+ type: Option<MicrosurveyConfig>
+ description: Optional configuration data for a microsurvey.
+ default: null
is-control:
type: Boolean
- description: "Indicates if this message is the control message, if true shouldn't be displayed"
+ description: Indicates if this message is the control message, if true shouldn't be displayed
default: false
experiment:
type: Option<ExperimentSlug>
@@ -183,6 +187,17 @@ objects:
How often, in minutes, the notification message worker will wake up and check for new
messages.
default: 240 # 4 hours
+ MicrosurveyConfig:
+ description: Attributes relating to microsurvey content.
+ fields:
+ target-feature:
+ type: MicrosurveyTargetFeature
+ description: The type of feature the microsurvey is for e.g. Printing.
+ default: unknown # Should not be defaulted
+ options:
+ description: The list of options to present to the user e.g. "Satisfied, Dissatisfied...".
+ type: List<Text>
+ default: [] # Should not be defaulted
enums:
ControlMessageBehavior:
@@ -192,3 +207,11 @@ enums:
description: The next eligible message should be shown.
show-none:
description: The surface should show no message.
+
+ MicrosurveyTargetFeature:
+ description: The specific feature the microsurvey is for e.g Printing.
+ variants:
+ printing:
+ description: The printing feature.
+ unknown:
+ description: No target feature set. Only used in an invalid experiment configuration.
diff --git a/mobile/android/android-components/components/service/pocket/src/test/resources/pocket/stories_recommendations_response.json b/mobile/android/android-components/components/service/pocket/src/test/resources/pocket/stories_recommendations_response.json
index da2b9a2953..f410fd7d3d 100644
--- a/mobile/android/android-components/components/service/pocket/src/test/resources/pocket/stories_recommendations_response.json
+++ b/mobile/android/android-components/components/service/pocket/src/test/resources/pocket/stories_recommendations_response.json
@@ -1,44 +1,44 @@
{
- "recommendations": [
- {
- "category": "general",
- "url": "https://getpocket.com/explore/item/how-to-remember-anything-you-really-want-to-remember-backed-by-science",
- "title": "How to Remember Anything You Really Want to Remember, Backed by Science",
- "imageUrl": "https://img-getpocket.cdn.mozilla.net/{wh}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fpocket-image-cache.com%2F1200x%2Ffilters%3Aformat(jpg)%3Aextract_focal()%2Fhttps%253A%252F%252Fwww.incimages.com%252Fuploaded_files%252Fimage%252F1920x1080%252Fgetty-862457080_394628.jpg",
- "publisher": "Pocket",
- "timeToRead": 3
- },
- {
- "category": "general",
- "url": "https://www.thecut.com/article/i-dont-want-to-be-like-a-family-with-my-co-workers.html",
- "title": "‘I Don’t Want to Be Like a Family With My Co-Workers’",
- "imageUrl": "https://img-getpocket.cdn.mozilla.net/{wh}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fpyxis.nymag.com%2Fv1%2Fimgs%2Fac8%2Fd22%2F315cd0cf1e3a43edfe0e0548f2edbcb1a1-ask-a-boss.1x.rsocial.w1200.jpg",
- "publisher": "The Cut",
- "timeToRead": 5
- },
- {
- "category": "general",
- "url": "https://www.newyorker.com/news/q-and-a/how-america-failed-in-afghanistan",
- "title": "How America Failed in Afghanistan",
- "imageUrl": "https://img-getpocket.cdn.mozilla.net/{wh}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fmedia.newyorker.com%2Fphotos%2F6119484157b611aec9c99b43%2F16%3A9%2Fw_1280%2Cc_limit%2FChotiner-Afghanistan01.jpg",
- "publisher": "The New Yorker",
- "timeToRead": 14
- },
- {
- "category": "general",
- "url": "https://www.technologyreview.com/2021/08/15/1031804/digital-beauty-filters-photoshop-photo-editing-colorism-racism/",
- "title": "How digital beauty filters perpetuate colorism",
- "imageUrl": "https://img-getpocket.cdn.mozilla.net/{wh}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fwp.technologyreview.com%2Fwp-content%2Fuploads%2F2021%2F08%2FBeautyScoreColorism.jpg%3Fresize%3D1200%2C600",
- "publisher": "MIT Technology Review",
- "timeToRead": 11
- },
- {
- "category": "general",
- "url": "https://getpocket.com/explore/item/how-to-get-rid-of-black-mold-naturally",
- "title": "How to Get Rid of Black Mold Naturally",
- "imageUrl": "https://img-getpocket.cdn.mozilla.net/{wh}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fpocket-image-cache.com%2F1200x%2Ffilters%3Aformat(jpg)%3Aextract_focal()%2Fhttps%253A%252F%252Fpocket-syndicated-images.s3.amazonaws.com%252Farticles%252F6757%252F1628024495_6109ae86db6cc.png",
- "publisher": "Pocket",
- "timeToRead": 4
- }
- ]
+ "recommendations": [
+ {
+ "category": "general",
+ "url": "https://getpocket.com/explore/item/how-to-remember-anything-you-really-want-to-remember-backed-by-science",
+ "title": "How to Remember Anything You Really Want to Remember, Backed by Science",
+ "imageUrl": "https://img-getpocket.cdn.mozilla.net/{wh}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fpocket-image-cache.com%2F1200x%2Ffilters%3Aformat(jpg)%3Aextract_focal()%2Fhttps%253A%252F%252Fwww.incimages.com%252Fuploaded_files%252Fimage%252F1920x1080%252Fgetty-862457080_394628.jpg",
+ "publisher": "Pocket",
+ "timeToRead": 3
+ },
+ {
+ "category": "general",
+ "url": "https://www.thecut.com/article/i-dont-want-to-be-like-a-family-with-my-co-workers.html",
+ "title": "‘I Don’t Want to Be Like a Family With My Co-Workers’",
+ "imageUrl": "https://img-getpocket.cdn.mozilla.net/{wh}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fpyxis.nymag.com%2Fv1%2Fimgs%2Fac8%2Fd22%2F315cd0cf1e3a43edfe0e0548f2edbcb1a1-ask-a-boss.1x.rsocial.w1200.jpg",
+ "publisher": "The Cut",
+ "timeToRead": 5
+ },
+ {
+ "category": "general",
+ "url": "https://www.newyorker.com/news/q-and-a/how-america-failed-in-afghanistan",
+ "title": "How America Failed in Afghanistan",
+ "imageUrl": "https://img-getpocket.cdn.mozilla.net/{wh}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fmedia.newyorker.com%2Fphotos%2F6119484157b611aec9c99b43%2F16%3A9%2Fw_1280%2Cc_limit%2FChotiner-Afghanistan01.jpg",
+ "publisher": "The New Yorker",
+ "timeToRead": 14
+ },
+ {
+ "category": "general",
+ "url": "https://www.technologyreview.com/2021/08/15/1031804/digital-beauty-filters-photoshop-photo-editing-colorism-racism/",
+ "title": "How digital beauty filters perpetuate colorism",
+ "imageUrl": "https://img-getpocket.cdn.mozilla.net/{wh}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fwp.technologyreview.com%2Fwp-content%2Fuploads%2F2021%2F08%2FBeautyScoreColorism.jpg%3Fresize%3D1200%2C600",
+ "publisher": "MIT Technology Review",
+ "timeToRead": 11
+ },
+ {
+ "category": "general",
+ "url": "https://getpocket.com/explore/item/how-to-get-rid-of-black-mold-naturally",
+ "title": "How to Get Rid of Black Mold Naturally",
+ "imageUrl": "https://img-getpocket.cdn.mozilla.net/{wh}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fpocket-image-cache.com%2F1200x%2Ffilters%3Aformat(jpg)%3Aextract_focal()%2Fhttps%253A%252F%252Fpocket-syndicated-images.s3.amazonaws.com%252Farticles%252F6757%252F1628024495_6109ae86db6cc.png",
+ "publisher": "Pocket",
+ "timeToRead": 4
+ }
+ ]
}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/resources/pocket/story_recommendation_response.json b/mobile/android/android-components/components/service/pocket/src/test/resources/pocket/story_recommendation_response.json
index 8fa6e33ad7..99b6ef44d0 100644
--- a/mobile/android/android-components/components/service/pocket/src/test/resources/pocket/story_recommendation_response.json
+++ b/mobile/android/android-components/components/service/pocket/src/test/resources/pocket/story_recommendation_response.json
@@ -1,12 +1,12 @@
{
- "recommendations": [
- {
- "category": "science",
- "url": "https://getpocket.com/explore/item/you-think-you-know-what-blue-is-but-you-have-no-idea",
- "title": "You Think You Know What Blue Is, But You Have No Idea",
- "imageUrl": "https://img-getpocket.cdn.mozilla.net/{wh}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fpocket-image-cache.com%2F1200x%2Ffilters%3Aformat(jpg)%3Aextract_focal()%2Fhttps%253A%252F%252Fpocket-syndicated-images.s3.amazonaws.com%252Farticles%252F3713%252F1584373694_GettyImages-83522858.jpg",
- "publisher": "Pocket",
- "timeToRead": 3
- }
- ]
+ "recommendations": [
+ {
+ "category": "science",
+ "url": "https://getpocket.com/explore/item/you-think-you-know-what-blue-is-but-you-have-no-idea",
+ "title": "You Think You Know What Blue Is, But You Have No Idea",
+ "imageUrl": "https://img-getpocket.cdn.mozilla.net/{wh}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fpocket-image-cache.com%2F1200x%2Ffilters%3Aformat(jpg)%3Aextract_focal()%2Fhttps%253A%252F%252Fpocket-syndicated-images.s3.amazonaws.com%252Farticles%252F3713%252F1584373694_GettyImages-83522858.jpg",
+ "publisher": "Pocket",
+ "timeToRead": 3
+ }
+ ]
}
diff --git a/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/GeckoLoginStorageDelegate.kt b/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/GeckoLoginStorageDelegate.kt
index 892930e28d..f66b727cf1 100644
--- a/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/GeckoLoginStorageDelegate.kt
+++ b/mobile/android/android-components/components/service/sync-logins/src/main/java/mozilla/components/service/sync/logins/GeckoLoginStorageDelegate.kt
@@ -4,6 +4,7 @@
package mozilla.components.service.sync.logins
+import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
@@ -42,10 +43,15 @@ import mozilla.components.concept.storage.LoginsStorage
* what the result of the operation will be: saving a new login,
* updating an existing login, or filling in a blank username.
* - If the user accepts: GV calls [onLoginSave] with the [LoginEntry]
+ *
+ * @param loginStorage The [LoginsStorage] used for looking up saved credentials to autofill.
+ * @param scope [CoroutineScope] for long running operations. Defaults to using the [Dispatchers.IO].
+ * @param isLoginAutofillEnabled callback allowing to limit [loginStorage] operations if autofill is disabled.
*/
class GeckoLoginStorageDelegate(
private val loginStorage: Lazy<LoginsStorage>,
private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO),
+ private val isLoginAutofillEnabled: () -> Boolean = { false },
) : LoginStorageDelegate {
override fun onLoginUsed(login: Login) {
@@ -55,6 +61,9 @@ class GeckoLoginStorageDelegate(
}
override fun onLoginFetch(domain: String): Deferred<List<Login>> {
+ if (!isLoginAutofillEnabled()) {
+ return CompletableDeferred(listOf())
+ }
return scope.async {
loginStorage.value.getByBaseDomain(domain)
}