Update breaking changes in the FxA/Sync integration

pull/159/head^2
Grisha Kruglov 4 years ago committed by Grisha Kruglov
parent d45e482373
commit 71b51146cb

@ -261,10 +261,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
components.backgroundServices.accountManagerAvailableQueue.runIfReadyOrQueue { components.backgroundServices.accountManagerAvailableQueue.runIfReadyOrQueue {
lifecycleScope.launch { lifecycleScope.launch {
// Make sure accountManager is initialized. // Make sure accountManager is initialized.
components.backgroundServices.accountManager.initAsync().await() components.backgroundServices.accountManager.start()
// If we're authenticated, kick-off a sync and a device state refresh. // If we're authenticated, kick-off a sync and a device state refresh.
components.backgroundServices.accountManager.authenticatedAccount()?.let { components.backgroundServices.accountManager.authenticatedAccount()?.let {
components.backgroundServices.accountManager.syncNowAsync( components.backgroundServices.accountManager.syncNow(
SyncReason.Startup, SyncReason.Startup,
debounce = true debounce = true
) )

@ -5,13 +5,11 @@
package org.mozilla.fenix.components package org.mozilla.fenix.components
import android.content.Context import android.content.Context
import android.content.SharedPreferences
import android.os.StrictMode import android.os.StrictMode
import androidx.annotation.GuardedBy import androidx.annotation.GuardedBy
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import mozilla.components.concept.sync.AccountObserver import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.AuthType import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.OAuthAccount import mozilla.components.concept.sync.OAuthAccount
@ -77,9 +75,18 @@ class AccountAbnormalities(
private val logger = Logger("AccountAbnormalities") private val logger = Logger("AccountAbnormalities")
private val prefs = StrictMode.allowThreadDiskReads().resetPoliciesAfter { private val prefs: SharedPreferences
context.getSharedPreferences(PREF_FXA_ABNORMALITIES, Context.MODE_PRIVATE) private val hadAccountPrior: Boolean
}
init {
val prefPair = StrictMode.allowThreadDiskReads().resetPoliciesAfter {
val p = context.getSharedPreferences(PREF_FXA_ABNORMALITIES, Context.MODE_PRIVATE)
val a = p.getBoolean(KEY_HAS_ACCOUNT, false)
Pair(p, a)
}
prefs = prefPair.first
hadAccountPrior = prefPair.second
}
/** /**
* Once [accountManager] is initialized, queries it to detect abnormal account states. * Once [accountManager] is initialized, queries it to detect abnormal account states.
@ -89,37 +96,28 @@ class AccountAbnormalities(
* @param initResult A deferred result of initializing [accountManager]. * @param initResult A deferred result of initializing [accountManager].
* @return A [Unit] deferred, resolved once [initResult] is resolved and state is processed for abnormalities. * @return A [Unit] deferred, resolved once [initResult] is resolved and state is processed for abnormalities.
*/ */
fun accountManagerInitializedAsync( fun accountManagerStarted(
accountManager: FxaAccountManager, accountManager: FxaAccountManager
initResult: Deferred<Unit> ) {
): Deferred<Unit> { check(!accountManagerConfigured) { "accountManagerStarted called twice" }
accountManagerConfigured = true accountManagerConfigured = true
return CoroutineScope(coroutineContext).async { // Behaviour considered abnormal:
// Wait for the account manager to finish initializing. If it's queried before the // - we had an account before, and it's no longer present during startup
// "init" deferred returns, we'll get inaccurate results.
initResult.await() // We use a flag in prefs to keep track of the fact that we have an authenticated
// account. This works because our account state is persisted in the application's
// Account manager finished initialization, we can now query it for the account state // directory, same as SharedPreferences. If user clears application data, both the
// and see if it doesn't match our expectations. // fxa state and our flag will be removed.
// Behaviour considered abnormal: val hasAccountNow = accountManager.authenticatedAccount() != null
// - we had an account before, and it's no longer present during startup if (hadAccountPrior && !hasAccountNow) {
prefs.edit().putBoolean(KEY_HAS_ACCOUNT, false).apply()
// We use a flag in prefs to keep track of the fact that we have an authenticated
// account. This works because our account state is persisted in the application's logger.warn("Missing expected account on startup")
// directory, same as SharedPreferences. If user clears application data, both the
// fxa state and our flag will be removed. crashReporter.submitCaughtException(
val hadAccountBefore = prefs.getBoolean(KEY_HAS_ACCOUNT, false) AbnormalFxaEvent.MissingExpectedAccountAfterStartup()
val hasAccountNow = accountManager.authenticatedAccount() != null )
if (hadAccountBefore && !hasAccountNow) {
prefs.edit().putBoolean(KEY_HAS_ACCOUNT, false).apply()
logger.warn("Missing expected account on startup")
crashReporter.submitCaughtException(
AbnormalFxaEvent.MissingExpectedAccountAfterStartup()
)
}
} }
} }
@ -152,8 +150,7 @@ class AccountAbnormalities(
} }
override fun onAuthenticated(account: OAuthAccount, authType: AuthType) { override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
check(accountManagerConfigured) { "onAuthenticated before account manager was configured" } // Not checking state of accountManagerConfigured because we'll race against account manager's start.
onAuthenticatedCalled = true onAuthenticatedCalled = true
// We don't check if KEY_HAS_ACCOUNT was already true: we will see onAuthenticated on every // We don't check if KEY_HAS_ACCOUNT was already true: we will see onAuthenticated on every

@ -8,19 +8,22 @@ import android.content.Context
import android.os.Build import android.os.Build
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.PRIVATE import androidx.annotation.VisibleForTesting.PRIVATE
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import mozilla.components.browser.storage.sync.PlacesBookmarksStorage import mozilla.components.browser.storage.sync.PlacesBookmarksStorage
import mozilla.components.browser.storage.sync.PlacesHistoryStorage import mozilla.components.browser.storage.sync.PlacesHistoryStorage
import mozilla.components.browser.storage.sync.RemoteTabsStorage import mozilla.components.browser.storage.sync.RemoteTabsStorage
import mozilla.components.concept.sync.AccountObserver import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.AuthType import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.DeviceCapability import mozilla.components.concept.sync.DeviceCapability
import mozilla.components.concept.sync.DeviceConfig
import mozilla.components.concept.sync.DeviceType import mozilla.components.concept.sync.DeviceType
import mozilla.components.concept.sync.OAuthAccount import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.feature.accounts.push.FxaPushSupportFeature import mozilla.components.feature.accounts.push.FxaPushSupportFeature
import mozilla.components.feature.accounts.push.SendTabFeature import mozilla.components.feature.accounts.push.SendTabFeature
import mozilla.components.feature.syncedtabs.storage.SyncedTabsStorage import mozilla.components.feature.syncedtabs.storage.SyncedTabsStorage
import mozilla.components.lib.crash.CrashReporter import mozilla.components.lib.crash.CrashReporter
import mozilla.components.service.fxa.DeviceConfig import mozilla.components.service.fxa.PeriodicSyncConfig
import mozilla.components.service.fxa.ServerConfig import mozilla.components.service.fxa.ServerConfig
import mozilla.components.service.fxa.SyncConfig import mozilla.components.service.fxa.SyncConfig
import mozilla.components.service.fxa.SyncEngine import mozilla.components.service.fxa.SyncEngine
@ -86,7 +89,7 @@ class BackgroundServices(
@VisibleForTesting @VisibleForTesting
val supportedEngines = val supportedEngines =
setOf(SyncEngine.History, SyncEngine.Bookmarks, SyncEngine.Passwords, SyncEngine.Tabs) setOf(SyncEngine.History, SyncEngine.Bookmarks, SyncEngine.Passwords, SyncEngine.Tabs)
private val syncConfig = SyncConfig(supportedEngines, syncPeriodInMinutes = 240L) // four hours private val syncConfig = SyncConfig(supportedEngines, PeriodicSyncConfig(periodMinutes = 240)) // four hours
init { init {
/* Make the "history", "bookmark", "passwords", and "tabs" stores accessible to workers /* Make the "history", "bookmark", "passwords", and "tabs" stores accessible to workers
@ -156,10 +159,10 @@ class BackgroundServices(
SyncedTabsIntegration(context, accountManager).launch() SyncedTabsIntegration(context, accountManager).launch()
accountAbnormalities.accountManagerInitializedAsync( MainScope().launch {
accountManager, accountManager.start()
accountManager.initAsync() accountAbnormalities.accountManagerStarted(accountManager)
) }
}.also { }.also {
accountManagerAvailableQueue.ready() accountManagerAvailableQueue.ready()
} }
@ -180,31 +183,33 @@ internal class TelemetryAccountObserver(
override fun onAuthenticated(account: OAuthAccount, authType: AuthType) { override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
when (authType) { when (authType) {
// User signed-in into an existing FxA account. // User signed-in into an existing FxA account.
AuthType.Signin -> AuthType.Signin -> Event.SyncAuthSignIn
metricController.track(Event.SyncAuthSignIn)
// User created a new FxA account. // User created a new FxA account.
AuthType.Signup -> AuthType.Signup -> Event.SyncAuthSignUp
metricController.track(Event.SyncAuthSignUp)
// User paired to an existing account via QR code scanning. // User paired to an existing account via QR code scanning.
AuthType.Pairing -> AuthType.Pairing -> Event.SyncAuthPaired
metricController.track(Event.SyncAuthPaired)
// User signed-in into an FxA account shared from another locally installed app using the reuse flow.
AuthType.MigratedReuse -> Event.SyncAuthFromSharedReuse
// User signed-in into an FxA account shared from another locally installed app // User signed-in into an FxA account shared from another locally installed app using the copy flow.
// (e.g. Fennec). AuthType.MigratedCopy -> Event.SyncAuthFromSharedCopy
AuthType.Shared ->
metricController.track(Event.SyncAuthFromShared)
// Account Manager recovered a broken FxA auth state, without direct user involvement. // Account Manager recovered a broken FxA auth state, without direct user involvement.
AuthType.Recovered -> AuthType.Recovered -> Event.SyncAuthRecovered
metricController.track(Event.SyncAuthRecovered)
// User signed-in into an FxA account via unknown means. // User signed-in into an FxA account via unknown means.
// Exact mechanism identified by the 'action' param. // Exact mechanism identified by the 'action' param.
is AuthType.OtherExternal -> is AuthType.OtherExternal -> Event.SyncAuthOtherExternal
metricController.track(Event.SyncAuthOtherExternal)
// Account restored from a hydrated state on disk (e.g. during startup).
AuthType.Existing -> null
}?.let {
metricController.track(it)
} }
// Used by Leanplum as a context variable. // Used by Leanplum as a context variable.
settings.fxaSignedIn = true settings.fxaSignedIn = true
} }

@ -67,7 +67,8 @@ sealed class Event {
object SyncAuthPaired : Event() object SyncAuthPaired : Event()
object SyncAuthRecovered : Event() object SyncAuthRecovered : Event()
object SyncAuthOtherExternal : Event() object SyncAuthOtherExternal : Event()
object SyncAuthFromShared : Event() object SyncAuthFromSharedReuse : Event()
object SyncAuthFromSharedCopy : Event()
object SyncAccountOpened : Event() object SyncAccountOpened : Event()
object SyncAccountClosed : Event() object SyncAccountClosed : Event()
object SyncAccountSyncNow : Event() object SyncAccountSyncNow : Event()

@ -269,7 +269,7 @@ private val Event.wrapper: EventWrapper<*>?
is Event.SyncAuthOtherExternal -> EventWrapper<NoExtraKeys>( is Event.SyncAuthOtherExternal -> EventWrapper<NoExtraKeys>(
{ SyncAuth.otherExternal.record(it) } { SyncAuth.otherExternal.record(it) }
) )
is Event.SyncAuthFromShared -> EventWrapper<NoExtraKeys>( is Event.SyncAuthFromSharedReuse, Event.SyncAuthFromSharedCopy -> EventWrapper<NoExtraKeys>(
{ SyncAuth.autoLogin.record(it) } { SyncAuth.autoLogin.record(it) }
) )
is Event.SyncAuthRecovered -> EventWrapper<NoExtraKeys>( is Event.SyncAuthRecovered -> EventWrapper<NoExtraKeys>(

@ -42,7 +42,7 @@ private val Event.name: String?
is Event.CollectionTabRestored -> "E_Collection_Tab_Opened" is Event.CollectionTabRestored -> "E_Collection_Tab_Opened"
is Event.SyncAuthSignUp -> "E_FxA_New_Signup" is Event.SyncAuthSignUp -> "E_FxA_New_Signup"
is Event.SyncAuthSignIn, Event.SyncAuthPaired, Event.SyncAuthOtherExternal -> "E_Sign_In_FxA" is Event.SyncAuthSignIn, Event.SyncAuthPaired, Event.SyncAuthOtherExternal -> "E_Sign_In_FxA"
is Event.SyncAuthFromShared -> "E_Sign_In_FxA_Fennec_to_Fenix" is Event.SyncAuthFromSharedCopy, Event.SyncAuthFromSharedReuse -> "E_Sign_In_FxA_Fennec_to_Fenix"
is Event.SyncAuthSignOut -> "E_Sign_Out_FxA" is Event.SyncAuthSignOut -> "E_Sign_Out_FxA"
is Event.ClearedPrivateData -> "E_Cleared_Private_Data" is Event.ClearedPrivateData -> "E_Cleared_Private_Data"
is Event.DismissedOnboarding -> "E_Dismissed_Onboarding" is Event.DismissedOnboarding -> "E_Dismissed_Onboarding"

@ -14,7 +14,7 @@ import kotlinx.android.synthetic.main.onboarding_automatic_signin.view.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
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.ShareableAccount
import mozilla.components.support.ktx.android.view.putCompoundDrawablesRelativeWithIntrinsicBounds import mozilla.components.support.ktx.android.view.putCompoundDrawablesRelativeWithIntrinsicBounds
import org.mozilla.fenix.R import org.mozilla.fenix.R
@ -56,8 +56,12 @@ class OnboardingAutomaticSignInViewHolder(
button.isEnabled = false button.isEnabled = false
val accountManager = context.components.backgroundServices.accountManager val accountManager = context.components.backgroundServices.accountManager
when (accountManager.signInWithShareableAccountAsync(shareableAccount).await()) { when (accountManager.migrateFromAccount(shareableAccount)) {
SignInWithShareableAccountResult.Failure -> { MigrationResult.WillRetry,
MigrationResult.Success -> {
// We consider both of these as a 'success'.
}
MigrationResult.Failure -> {
// Failed to sign-in (e.g. bad credentials). Allow to try again. // Failed to sign-in (e.g. bad credentials). Allow to try again.
button.text = context.getString(R.string.onboarding_firefox_account_auto_signin_confirm) button.text = context.getString(R.string.onboarding_firefox_account_auto_signin_confirm)
button.isEnabled = true button.isEnabled = true
@ -69,9 +73,6 @@ class OnboardingAutomaticSignInViewHolder(
context.getString(R.string.onboarding_firefox_account_automatic_signin_failed) context.getString(R.string.onboarding_firefox_account_automatic_signin_failed)
).show() ).show()
} }
SignInWithShareableAccountResult.WillRetry, SignInWithShareableAccountResult.Success -> {
// We consider both of these as a 'success'.
}
} }
} }

@ -143,7 +143,7 @@ class DefaultBookmarkController(
scope.launch { scope.launch {
store.dispatch(BookmarkFragmentAction.StartSync) store.dispatch(BookmarkFragmentAction.StartSync)
invokePendingDeletion() invokePendingDeletion()
activity.components.backgroundServices.accountManager.syncNowAsync(SyncReason.User).await() activity.components.backgroundServices.accountManager.syncNow(SyncReason.User)
// The current bookmark node we are viewing may be made invalid after syncing so we // The current bookmark node we are viewing may be made invalid after syncing so we
// check if the current node is valid and if it isn't we find the nearest valid ancestor // check if the current node is valid and if it isn't we find the nearest valid ancestor
// and open it // and open it

@ -344,7 +344,7 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
private suspend fun syncHistory() { private suspend fun syncHistory() {
val accountManager = requireComponents.backgroundServices.accountManager val accountManager = requireComponents.backgroundServices.accountManager
accountManager.syncNowAsync(SyncReason.User).await() accountManager.syncNow(SyncReason.User)
viewModel.invalidate() viewModel.invalidate()
} }
} }

@ -122,7 +122,7 @@ internal class OneTimeMessageDeliveryObserver(
authType: AuthType authType: AuthType
) { ) {
lazyAccount.value.withConstellation { lazyAccount.value.withConstellation {
processRawEventAsync(String(message)) MainScope().launch { processRawEvent(String(message)) }
} }
MainScope().launch { MainScope().launch {

@ -169,25 +169,32 @@ class AccountSettingsFragment : PreferenceFragmentCompat() {
updateSyncEngineStates() updateSyncEngineStates()
setDisabledWhileSyncing(accountManager.isSyncActive()) setDisabledWhileSyncing(accountManager.isSyncActive())
requirePreference<CheckBoxPreference>(R.string.pref_key_sync_history).apply { fun updateSyncEngineState(context: Context, engine: SyncEngine, newState: Boolean) {
setOnPreferenceChangeListener { _, newValue -> SyncEnginesStorage(context).setStatus(engine, newState)
SyncEnginesStorage(context).setStatus(SyncEngine.History, newValue as Boolean) viewLifecycleOwner.lifecycleScope.launch {
@Suppress("DeferredResultUnused") context.components.backgroundServices.accountManager.syncNow(SyncReason.EngineChange)
context.components.backgroundServices.accountManager.syncNowAsync(SyncReason.EngineChange)
true
} }
} }
requirePreference<CheckBoxPreference>(R.string.pref_key_sync_bookmarks).apply { fun SyncEngine.prefId(): Int = when (this) {
setOnPreferenceChangeListener { _, newValue -> SyncEngine.History -> R.string.pref_key_sync_history
SyncEnginesStorage(context).setStatus(SyncEngine.Bookmarks, newValue as Boolean) SyncEngine.Bookmarks -> R.string.pref_key_sync_bookmarks
@Suppress("DeferredResultUnused") SyncEngine.Passwords -> R.string.pref_key_sync_logins
context.components.backgroundServices.accountManager.syncNowAsync(SyncReason.EngineChange) SyncEngine.Tabs -> R.string.pref_key_sync_tabs
true else -> throw IllegalStateException("Accessing internal sync engines")
}
listOf(SyncEngine.History, SyncEngine.Bookmarks, SyncEngine.Tabs).forEach {
requirePreference<CheckBoxPreference>(it.prefId()).apply {
setOnPreferenceChangeListener { _, newValue ->
updateSyncEngineState(context, it, newValue as Boolean)
true
}
} }
} }
requirePreference<CheckBoxPreference>(R.string.pref_key_sync_logins).apply { // 'Passwords' listener is special, since we also display a pin protection warning.
requirePreference<CheckBoxPreference>(SyncEngine.Passwords.prefId()).apply {
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
val manager = val manager =
activity?.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager activity?.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
@ -195,9 +202,7 @@ class AccountSettingsFragment : PreferenceFragmentCompat() {
newValue == false || newValue == false ||
!context.settings().shouldShowSecurityPinWarningSync !context.settings().shouldShowSecurityPinWarningSync
) { ) {
SyncEnginesStorage(context).setStatus(SyncEngine.Passwords, newValue as Boolean) updateSyncEngineState(context, SyncEngine.Passwords, newValue as Boolean)
@Suppress("DeferredResultUnused")
context.components.backgroundServices.accountManager.syncNowAsync(SyncReason.EngineChange)
} else { } else {
showPinDialogWarning(newValue as Boolean) showPinDialogWarning(newValue as Boolean)
} }
@ -205,15 +210,6 @@ class AccountSettingsFragment : PreferenceFragmentCompat() {
} }
} }
requirePreference<CheckBoxPreference>(R.string.pref_key_sync_tabs).apply {
setOnPreferenceChangeListener { _, newValue ->
SyncEnginesStorage(context).setStatus(SyncEngine.Tabs, newValue as Boolean)
@Suppress("DeferredResultUnused")
context.components.backgroundServices.accountManager.syncNowAsync(SyncReason.EngineChange)
true
}
}
deviceConstellation?.registerDeviceObserver( deviceConstellation?.registerDeviceObserver(
deviceConstellationObserver, deviceConstellationObserver,
owner = this, owner = this,
@ -237,8 +233,9 @@ class AccountSettingsFragment : PreferenceFragmentCompat() {
setNegativeButton(getString(R.string.logins_warning_dialog_later)) { _: DialogInterface, _ -> setNegativeButton(getString(R.string.logins_warning_dialog_later)) { _: DialogInterface, _ ->
SyncEnginesStorage(context).setStatus(SyncEngine.Passwords, newValue) SyncEnginesStorage(context).setStatus(SyncEngine.Passwords, newValue)
@Suppress("DeferredResultUnused") viewLifecycleOwner.lifecycleScope.launch {
context.components.backgroundServices.accountManager.syncNowAsync(SyncReason.EngineChange) context.components.backgroundServices.accountManager.syncNow(SyncReason.EngineChange)
}
} }
setPositiveButton(getString(R.string.logins_warning_dialog_set_up_now)) { it: DialogInterface, _ -> setPositiveButton(getString(R.string.logins_warning_dialog_set_up_now)) { it: DialogInterface, _ ->
@ -278,13 +275,12 @@ class AccountSettingsFragment : PreferenceFragmentCompat() {
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
requireComponents.analytics.metrics.track(Event.SyncAccountSyncNow) requireComponents.analytics.metrics.track(Event.SyncAccountSyncNow)
// Trigger a sync. // Trigger a sync.
requireComponents.backgroundServices.accountManager.syncNowAsync(SyncReason.User) requireComponents.backgroundServices.accountManager.syncNow(SyncReason.User)
.await()
// Poll for device events & update devices. // Poll for device events & update devices.
accountManager.authenticatedAccount() accountManager.authenticatedAccount()
?.deviceConstellation()?.run { ?.deviceConstellation()?.run {
refreshDevicesAsync().await() refreshDevices()
pollForCommandsAsync().await() pollForCommands()
} }
} }
} }
@ -298,8 +294,7 @@ class AccountSettingsFragment : PreferenceFragmentCompat() {
context?.let { context?.let {
accountManager.authenticatedAccount() accountManager.authenticatedAccount()
?.deviceConstellation() ?.deviceConstellation()
?.setDeviceNameAsync(newValue, it) ?.setDeviceName(newValue, it)
?.await()
} }
} }
return true return true

@ -64,7 +64,7 @@ class SignOutFragment : BottomSheetDialogFragment() {
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
requireComponents requireComponents
.backgroundServices.accountAbnormalities.userRequestedLogout() .backgroundServices.accountAbnormalities.userRequestedLogout()
accountManager.logoutAsync().await() accountManager.logout()
}.invokeOnCompletion { }.invokeOnCompletion {
if (!findNavController().popBackStack(R.id.settingsFragment, false)) { if (!findNavController().popBackStack(R.id.settingsFragment, false)) {
dismiss() dismiss()

@ -70,6 +70,7 @@ class TurnOnSyncFragment : Fragment(), AccountObserver {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
requireComponents.backgroundServices.accountManager.register(this, owner = this)
requireComponents.analytics.metrics.track(Event.SyncAuthOpened) requireComponents.analytics.metrics.track(Event.SyncAuthOpened)
// App can be installed on devices with no camera modules. Like Android TV boxes. // App can be installed on devices with no camera modules. Like Android TV boxes.
@ -139,23 +140,13 @@ class TurnOnSyncFragment : Fragment(), AccountObserver {
// Since the snackbar can be presented in BrowserFragment or in SettingsFragment we must // Since the snackbar can be presented in BrowserFragment or in SettingsFragment we must
// base our display method on the padSnackbar argument // base our display method on the padSnackbar argument
if (args.padSnackbar) { FenixSnackbar.make(
FenixSnackbar.make( view = requireView(),
view = requireView(), duration = snackbarLength,
duration = snackbarLength, isDisplayedWithBrowserToolbar = args.padSnackbar
isDisplayedWithBrowserToolbar = true )
) .setText(snackbarText)
.setText(snackbarText) .show()
.show()
} else {
FenixSnackbar.make(
view = requireView(),
duration = snackbarLength,
isDisplayedWithBrowserToolbar = false
)
.setText(snackbarText)
.show()
}
} }
private fun navigateToPairWithEmail() { private fun navigateToPairWithEmail() {

@ -54,8 +54,7 @@ class ShareViewModel(application: Application) : AndroidViewModel(application) {
viewModelScope.launch(ioDispatcher) { viewModelScope.launch(ioDispatcher) {
fxaAccountManager.authenticatedAccount() fxaAccountManager.authenticatedAccount()
?.deviceConstellation() ?.deviceConstellation()
?.refreshDevicesAsync() ?.refreshDevices()
?.await()
val devicesShareOptions = buildDeviceList(fxaAccountManager, network) val devicesShareOptions = buildDeviceList(fxaAccountManager, network)
devicesListLiveData.postValue(devicesShareOptions) devicesListLiveData.postValue(devicesShareOptions)

@ -9,7 +9,6 @@ import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.verify import io.mockk.verify
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import mozilla.components.lib.crash.CrashReporter import mozilla.components.lib.crash.CrashReporter
import mozilla.components.service.fxa.manager.FxaAccountManager import mozilla.components.service.fxa.manager.FxaAccountManager
@ -35,12 +34,8 @@ class AccountAbnormalitiesTest {
assertEquals("userRequestedLogout before account manager was configured", e.message) assertEquals("userRequestedLogout before account manager was configured", e.message)
} }
try { // This doesn't throw, see method for details.
accountAbnormalities.onAuthenticated(mockk(), mockk()) accountAbnormalities.onAuthenticated(mockk(), mockk())
fail()
} catch (e: IllegalStateException) {
assertEquals("onAuthenticated before account manager was configured", e.message)
}
try { try {
accountAbnormalities.onLoggedOut() accountAbnormalities.onLoggedOut()
@ -58,10 +53,7 @@ class AccountAbnormalitiesTest {
val accountManager: FxaAccountManager = mockk(relaxed = true) val accountManager: FxaAccountManager = mockk(relaxed = true)
val accountAbnormalities = AccountAbnormalities(testContext, crashReporter, this.coroutineContext) val accountAbnormalities = AccountAbnormalities(testContext, crashReporter, this.coroutineContext)
accountAbnormalities.accountManagerInitializedAsync( accountAbnormalities.accountManagerStarted(accountManager)
accountManager,
CompletableDeferred(Unit).also { it.complete(Unit) }
).await()
// Logout action must be preceded by auth. // Logout action must be preceded by auth.
accountAbnormalities.userRequestedLogout() accountAbnormalities.userRequestedLogout()
@ -74,10 +66,7 @@ class AccountAbnormalitiesTest {
val accountManager: FxaAccountManager = mockk(relaxed = true) val accountManager: FxaAccountManager = mockk(relaxed = true)
val accountAbnormalities = AccountAbnormalities(testContext, crashReporter, this.coroutineContext) val accountAbnormalities = AccountAbnormalities(testContext, crashReporter, this.coroutineContext)
accountAbnormalities.accountManagerInitializedAsync( accountAbnormalities.accountManagerStarted(accountManager)
accountManager,
CompletableDeferred(Unit).also { it.complete(Unit) }
).await()
accountAbnormalities.onAuthenticated(mockk(), mockk()) accountAbnormalities.onAuthenticated(mockk(), mockk())
// So far, so good. A regular logout request while being authenticated. // So far, so good. A regular logout request while being authenticated.
@ -95,10 +84,7 @@ class AccountAbnormalitiesTest {
val accountManager: FxaAccountManager = mockk(relaxed = true) val accountManager: FxaAccountManager = mockk(relaxed = true)
val accountAbnormalities = AccountAbnormalities(testContext, crashReporter, this.coroutineContext) val accountAbnormalities = AccountAbnormalities(testContext, crashReporter, this.coroutineContext)
accountAbnormalities.accountManagerInitializedAsync( accountAbnormalities.accountManagerStarted(accountManager)
accountManager,
CompletableDeferred(Unit).also { it.complete(Unit) }
).await()
// User didn't request this logout. // User didn't request this logout.
accountAbnormalities.onLoggedOut() accountAbnormalities.onLoggedOut()
@ -111,10 +97,7 @@ class AccountAbnormalitiesTest {
val accountManager: FxaAccountManager = mockk(relaxed = true) val accountManager: FxaAccountManager = mockk(relaxed = true)
val accountAbnormalities = AccountAbnormalities(testContext, crashReporter, this.coroutineContext) val accountAbnormalities = AccountAbnormalities(testContext, crashReporter, this.coroutineContext)
accountAbnormalities.accountManagerInitializedAsync( accountAbnormalities.accountManagerStarted(accountManager)
accountManager,
CompletableDeferred(Unit).also { it.complete(Unit) }
).await()
accountAbnormalities.onAuthenticated(mockk(), mockk()) accountAbnormalities.onAuthenticated(mockk(), mockk())
verify { crashReporter wasNot Called } verify { crashReporter wasNot Called }
@ -124,10 +107,7 @@ class AccountAbnormalitiesTest {
val accountAbnormalities2 = AccountAbnormalities(testContext, crashReporter, this.coroutineContext) val accountAbnormalities2 = AccountAbnormalities(testContext, crashReporter, this.coroutineContext)
// mock accountManager doesn't have an account, but we expect it to have one since we // mock accountManager doesn't have an account, but we expect it to have one since we
// were authenticated before our "restart". // were authenticated before our "restart".
accountAbnormalities2.accountManagerInitializedAsync( accountAbnormalities2.accountManagerStarted(accountManager)
accountManager,
CompletableDeferred(Unit).also { it.complete(Unit) }
).await()
assertCaughtException<AbnormalFxaEvent.MissingExpectedAccountAfterStartup>(crashReporter) assertCaughtException<AbnormalFxaEvent.MissingExpectedAccountAfterStartup>(crashReporter)
} }
@ -138,10 +118,7 @@ class AccountAbnormalitiesTest {
val accountManager: FxaAccountManager = mockk(relaxed = true) val accountManager: FxaAccountManager = mockk(relaxed = true)
val accountAbnormalities = AccountAbnormalities(testContext, crashReporter, this.coroutineContext) val accountAbnormalities = AccountAbnormalities(testContext, crashReporter, this.coroutineContext)
accountAbnormalities.accountManagerInitializedAsync( accountAbnormalities.accountManagerStarted(accountManager)
accountManager,
CompletableDeferred(Unit).also { it.complete(Unit) }
).await()
// We saw an auth event, then user requested a logout. // We saw an auth event, then user requested a logout.
accountAbnormalities.onAuthenticated(mockk(), mockk()) accountAbnormalities.onAuthenticated(mockk(), mockk())

@ -78,10 +78,13 @@ class BackgroundServicesTest {
fun `telemetry account observer tracks shared event`() { fun `telemetry account observer tracks shared event`() {
val account = mockk<OAuthAccount>() val account = mockk<OAuthAccount>()
registry.notifyObservers { onAuthenticated(account, AuthType.Shared) } registry.notifyObservers { onAuthenticated(account, AuthType.MigratedReuse) }
verify { metrics.track(Event.SyncAuthFromShared) } verify { metrics.track(Event.SyncAuthFromSharedReuse) }
verify { settings.fxaSignedIn = true } verify { settings.fxaSignedIn = true }
confirmVerified(metrics, settings) confirmVerified(metrics, settings)
registry.notifyObservers { onAuthenticated(account, AuthType.MigratedCopy) }
verify { metrics.track(Event.SyncAuthFromSharedCopy) }
} }
@Test @Test

@ -7,16 +7,16 @@ package org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import io.mockk.every import io.mockk.every
import io.mockk.coEvery
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkObject import io.mockk.mockkObject
import io.mockk.unmockkObject
import io.mockk.verify import io.mockk.verify
import io.mockk.unmockkObject
import kotlinx.android.synthetic.main.onboarding_automatic_signin.view.* import kotlinx.android.synthetic.main.onboarding_automatic_signin.view.*
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runBlockingTest
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.ShareableAccount
import mozilla.components.support.test.robolectric.testContext import mozilla.components.support.test.robolectric.testContext
import org.junit.After import org.junit.After
@ -69,13 +69,30 @@ class OnboardingAutomaticSignInViewHolderTest {
} }
@Test @Test
fun `sign in on click`() = runBlocking { fun `sign in on click - MigrationResult Success`() = runBlocking {
val account = mockk<ShareableAccount> {
every { email } returns "email@example.com"
}
coEvery {
backgroundServices.accountManager.migrateFromAccount(account)
} returns MigrationResult.Success
val holder = OnboardingAutomaticSignInViewHolder(view, scope = this)
holder.bind(account)
holder.onClick(view.fxa_sign_in_button)
assertEquals("Signing in…", view.fxa_sign_in_button.text)
assertFalse(view.fxa_sign_in_button.isEnabled)
}
@Test
fun `sign in on click - MigrationResult WillRetry treated the same as Success`() = runBlocking {
val account = mockk<ShareableAccount> { val account = mockk<ShareableAccount> {
every { email } returns "email@example.com" every { email } returns "email@example.com"
} }
every { coEvery {
backgroundServices.accountManager.signInWithShareableAccountAsync(account) backgroundServices.accountManager.migrateFromAccount(account)
} returns CompletableDeferred(SignInWithShareableAccountResult.Success) } returns MigrationResult.WillRetry
val holder = OnboardingAutomaticSignInViewHolder(view, scope = this) val holder = OnboardingAutomaticSignInViewHolder(view, scope = this)
holder.bind(account) holder.bind(account)
@ -90,9 +107,9 @@ class OnboardingAutomaticSignInViewHolderTest {
val account = mockk<ShareableAccount> { val account = mockk<ShareableAccount> {
every { email } returns "email@example.com" every { email } returns "email@example.com"
} }
every { coEvery {
backgroundServices.accountManager.signInWithShareableAccountAsync(account) backgroundServices.accountManager.migrateFromAccount(account)
} returns CompletableDeferred(SignInWithShareableAccountResult.Failure) } returns MigrationResult.Failure
val holder = OnboardingAutomaticSignInViewHolder(view, scope = this) val holder = OnboardingAutomaticSignInViewHolder(view, scope = this)
holder.bind(account) holder.bind(account)

Loading…
Cancel
Save