diff --git a/app/metrics.yaml b/app/metrics.yaml index 512fc410c06f..d628f9860610 100644 --- a/app/metrics.yaml +++ b/app/metrics.yaml @@ -816,21 +816,65 @@ sync_auth: notification_emails: - fenix-core@mozilla.com expires: "2020-03-01" - scan_pairing: + sign_up: type: event description: > - A user pressed the scan pairing button on the sync authentication page + User registered a new Firefox Account, and was signed into it bugs: - - 1190 + - 4971 data_reviews: - - https://github.com/mozilla-mobile/fenix/pull/2745#issuecomment-494918532 + - https://github.com/mozilla-mobile/fenix/pull/4931#issuecomment-529740300 notification_emails: - - fenix-core@mozilla.com + - fenix-core@mozilla.com + expires: "2020-03-01" + paired: + type: event + description: > + User signed into FxA by pairing with a different Firefox browser, using a QR code + bugs: + - 4971 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/4931#issuecomment-529740300 + notification_emails: + - fenix-core@mozilla.com + expires: "2020-03-01" + auto_login: + type: event + description: > + User signed into FxA via an account shared from another locally installed Mozilla application (e.g. Fennec) + bugs: + - 4971 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/4931#issuecomment-529740300 + notification_emails: + - fenix-core@mozilla.com + expires: "2020-03-01" + recovered: + type: event + description: > + Account manager automatically recovered FxA authentication state without direct user involvement + bugs: + - 4971 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/4931#issuecomment-529740300 + notification_emails: + - fenix-core@mozilla.com expires: "2020-03-01" - create_account: + other_external: type: event description: > - A user pressed the create account button on the sync authentication page + User authenticated via FxA using an unknown mechanism. "Known" mechanisms are currently sign-in, sign-up and pairing + bugs: + - 4971 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/4931#issuecomment-529740300 + notification_emails: + - fenix-core@mozilla.com + expires: "2020-03-01" + scan_pairing: + type: event + description: > + A user pressed the scan pairing button on the sync authentication page bugs: - 1190 data_reviews: diff --git a/app/src/main/java/org/mozilla/fenix/AppRequestInterceptor.kt b/app/src/main/java/org/mozilla/fenix/AppRequestInterceptor.kt index d32ebe7383da..c548cab01d06 100644 --- a/app/src/main/java/org/mozilla/fenix/AppRequestInterceptor.kt +++ b/app/src/main/java/org/mozilla/fenix/AppRequestInterceptor.kt @@ -25,8 +25,13 @@ class AppRequestInterceptor(private val context: Context) : RequestInterceptor { } adjustTrackingProtection(host, context, session) - // Accounts uses interception to check for a "success URL" in the sign-in flow to finalize authentication. - return context.components.services.accountsAuthFeature.interceptor.onLoadRequest(session, uri) + + // WebChannel-driven authentication does not require a separate redirect interceptor. + return if (context.isInExperiment(Experiments.asFeatureWebChannelsDisabled)) { + context.components.services.accountsAuthFeature.interceptor.onLoadRequest(session, uri) + } else { + null + } } private fun adjustTrackingProtection(host: String, context: Context, session: EngineSession) { diff --git a/app/src/main/java/org/mozilla/fenix/Experiments.kt b/app/src/main/java/org/mozilla/fenix/Experiments.kt index e664ec831fe2..f1d6a1ca0842 100644 --- a/app/src/main/java/org/mozilla/fenix/Experiments.kt +++ b/app/src/main/java/org/mozilla/fenix/Experiments.kt @@ -19,6 +19,8 @@ object Experiments { val asFeatureSyncDisabled = ExperimentDescriptor("asFeatureSyncDisabled") // application services flag to disable Firefox Accounts pairing button. val asFeatureFxAPairingDisabled = ExperimentDescriptor("asFeatureFxAPairingDisabled") + // application services flag to disable Firefox Accounts WebChannel integration. + val asFeatureWebChannelsDisabled = ExperimentDescriptor("asFeatureWebChannelsDisabled") } val Context.app: FenixApplication diff --git a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt index 6291969e0e7e..91f92a35fdab 100644 --- a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt +++ b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt @@ -191,16 +191,14 @@ open class FenixApplication : Application() { // Sets the PushFeature as the singleton instance for push messages to go to. // We need the push feature setup here to deliver messages in the case where the service // starts up the app first. - if (components.backgroundServices.pushConfig != null) { - Logger.info("Push configuration found; initializing autopush..") - - val push = components.backgroundServices.push + components.backgroundServices.push?.let { autoPushFeature -> + Logger.info("AutoPushFeature is configured, initializing it...") // Install the AutoPush singleton to receive messages. - PushProcessor.install(push) + PushProcessor.install(autoPushFeature) // Initialize the service. This could potentially be done in a coroutine in the future. - push.initialize() + autoPushFeature.initialize() } } diff --git a/app/src/main/java/org/mozilla/fenix/IntentReceiverActivity.kt b/app/src/main/java/org/mozilla/fenix/IntentReceiverActivity.kt index 89243b1b998e..cd88cf842095 100644 --- a/app/src/main/java/org/mozilla/fenix/IntentReceiverActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/IntentReceiverActivity.kt @@ -68,6 +68,7 @@ class IntentReceiverActivity : Activity() { private fun setIntentActivity(intent: Intent) { val openToBrowser = when { components.utils.customTabIntentProcessor.matches(intent) -> { + // TODO this needs to change: https://github.com/mozilla-mobile/fenix/issues/5225 val activityClass = if (intent.hasExtra(EXTRA_AUTH_CUSTOM_TAB)) { AuthCustomTabActivity::class } else { diff --git a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt index 941862800c9b..bef44d40bd45 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt @@ -31,6 +31,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager +import mozilla.components.feature.accounts.FxaWebChannelFeature import mozilla.components.feature.app.links.AppLinksFeature import mozilla.components.feature.contextmenu.ContextMenuFeature import mozilla.components.feature.downloads.DownloadsFeature @@ -49,6 +50,7 @@ import mozilla.components.support.base.feature.BackHandler import mozilla.components.support.base.feature.PermissionsFeature import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.ktx.android.view.exitImmersiveModeIfNeeded +import org.mozilla.fenix.Experiments import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.IntentReceiverActivity @@ -70,6 +72,7 @@ import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.enterToImmersiveMode import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings +import org.mozilla.fenix.isInExperiment import org.mozilla.fenix.quickactionsheet.QuickActionSheetBehavior import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.theme.ThemeManager @@ -96,6 +99,7 @@ abstract class BaseBrowserFragment : Fragment(), BackHandler, SessionManager.Obs private val sitePermissionsFeature = ViewBoundFeatureWrapper() private val fullScreenFeature = ViewBoundFeatureWrapper() private val swipeRefreshFeature = ViewBoundFeatureWrapper() + private val webchannelIntegration = ViewBoundFeatureWrapper() var customTabSessionId: String? = null @@ -374,6 +378,20 @@ abstract class BaseBrowserFragment : Fragment(), BackHandler, SessionManager.Obs view.swipeRefresh.setOnChildScrollUpCallback { _, _ -> true } } + if (!requireContext().isInExperiment(Experiments.asFeatureWebChannelsDisabled)) { + webchannelIntegration.set( + feature = FxaWebChannelFeature( + requireContext(), + customTabSessionId, + requireComponents.core.engine, + requireComponents.core.sessionManager, + requireComponents.backgroundServices.accountManager + ), + owner = this, + view = view + ) + } + (activity as HomeActivity).updateThemeForSession(session) } } diff --git a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt index e32c37b606c8..ec3fc270809a 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt @@ -102,7 +102,6 @@ class BrowserFragment : BaseBrowserFragment(), BackHandler { val sessionManager = context.components.core.sessionManager return super.initializeUI(view)?.also { - readerViewFeature.set( feature = ReaderViewFeature( context, diff --git a/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt b/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt index aacabc5c4d66..7909d9342b8d 100644 --- a/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt +++ b/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt @@ -5,13 +5,10 @@ package org.mozilla.fenix.components import android.content.Context -import android.content.SharedPreferences import android.os.Build -import android.preference.PreferenceManager +import androidx.annotation.VisibleForTesting +import androidx.annotation.VisibleForTesting.PRIVATE import androidx.lifecycle.ProcessLifecycleOwner -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import mozilla.components.browser.storage.sync.PlacesBookmarksStorage import mozilla.components.browser.storage.sync.PlacesHistoryStorage import mozilla.components.concept.push.Bus @@ -38,6 +35,7 @@ import mozilla.components.support.base.log.logger.Logger import org.mozilla.fenix.Experiments import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings import org.mozilla.fenix.isInExperiment @@ -49,13 +47,18 @@ import org.mozilla.fenix.test.Mockable */ @Mockable class BackgroundServices( - context: Context, + private val context: Context, historyStorage: PlacesHistoryStorage, bookmarkStorage: PlacesBookmarksStorage ) { companion object { const val CLIENT_ID = "a2270f727f45f648" - const val REDIRECT_URL = "https://accounts.firefox.com/oauth/success/$CLIENT_ID" + + fun redirectUrl(context: Context) = if (context.isInExperiment(Experiments.asFeatureWebChannelsDisabled)) { + "https://accounts.firefox.com/oauth/success/$CLIENT_ID" + } else { + "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel" + } } fun defaultDeviceName(context: Context): String = context.getString( @@ -65,7 +68,7 @@ class BackgroundServices( Build.MODEL ) - private val serverConfig = ServerConfig.release(CLIENT_ID, REDIRECT_URL) + private val serverConfig = ServerConfig.release(CLIENT_ID, redirectUrl(context)) private val deviceConfig = DeviceConfig( name = defaultDeviceName(context), type = DeviceType.MOBILE, @@ -76,35 +79,16 @@ class BackgroundServices( capabilities = setOf(DeviceCapability.SEND_TAB) ) // If sync has been turned off on the server then disable syncing. - private val syncConfig = if (context.isInExperiment(Experiments.asFeatureSyncDisabled)) { + @VisibleForTesting(otherwise = PRIVATE) + val syncConfig = if (context.isInExperiment(Experiments.asFeatureSyncDisabled)) { null } else { SyncConfig(setOf(SyncEngine.HISTORY, SyncEngine.BOOKMARKS), syncPeriodInMinutes = 240L) // four hours } - val pushConfig by lazy { - val logger = Logger("PushConfig") - val projectIdKey = context.getString(R.string.pref_key_push_project_id) - val resId = context.resources.getIdentifier(projectIdKey, "string", context.packageName) - if (resId == 0) { - logger.warn("No firebase configuration found; cannot support push service.") - return@lazy null - } + private val pushService by lazy { FirebasePush() } - logger.debug("Creating push configuration for autopush.") - val projectId = context.resources.getString(resId) - PushConfig(projectId) - } - - val pushService by lazy { FirebasePush() } - - val push by lazy { - AutoPushFeature( - context = context, - service = pushService, - config = pushConfig!! - ) - } + val push by lazy { makePushConfig()?.let { makePush(it) } } init { // Make the "history" and "bookmark" stores accessible to workers spawned by the sync manager. @@ -116,53 +100,52 @@ class BackgroundServices( private val logger = Logger("DeviceEventsObserver") override fun onEvents(events: List) { logger.info("Received ${events.size} device event(s)") - events.filter { it is DeviceEvent.TabReceived }.forEach { - notificationManager.showReceivedTabs(it as DeviceEvent.TabReceived) + events.filterIsInstance().forEach { + notificationManager.showReceivedTabs(it) } } } - /** - * When we login/logout of FxA, we need to update our push subscriptions to match the newly - * logged in account. - * - * We added the push service to the AccountManager observer so that we can control when the - * service will start/stop. Firebase was added when landing the push service to ensure it works - * as expected without causing any (as many) side effects. - * - * In order to use Firebase with Leanplum and other marketing features, we need it always - * running so we cannot leave this code in place when we implement those features. - * - * We should have this removed when we are more confident - * of the send-tab/push feature: https://github.com/mozilla-mobile/fenix/issues/4063 - */ - private val accountObserver = object : AccountObserver { - override fun onLoggedOut() { - push.unsubscribeForType(PushType.Services) - - context.components.analytics.metrics.track(Event.SyncAuthSignOut) + private val telemetryAccountObserver = TelemetryAccountObserver( + context, + context.components.analytics.metrics + ) - context.settings.fxaSignedIn = false - } + private val pushAccountObserver by lazy { push?.let { PushAccountObserver(it) } } - override fun onAuthenticated(account: OAuthAccount, authType: AuthType) { - if (authType != AuthType.Existing) { - push.subscribeForType(PushType.Services) - } + val accountManager = makeAccountManager(context, serverConfig, deviceConfig, syncConfig) - context.components.analytics.metrics.track(Event.SyncAuthSignIn) + @VisibleForTesting(otherwise = PRIVATE) + fun makePush(pushConfig: PushConfig): AutoPushFeature { + return AutoPushFeature( + context = context, + service = pushService, + config = pushConfig + ) + } - context.settings.fxaSignedIn = true + @VisibleForTesting(otherwise = PRIVATE) + fun makePushConfig(): PushConfig? { + val logger = Logger("PushConfig") + val projectIdKey = context.getString(R.string.pref_key_push_project_id) + val resId = context.resources.getIdentifier(projectIdKey, "string", context.packageName) + if (resId == 0) { + logger.warn("No firebase configuration found; cannot support push service.") + return null } - } - private val preferences: SharedPreferences by lazy { - PreferenceManager.getDefaultSharedPreferences( - context - ) + logger.debug("Creating push configuration for autopush.") + val projectId = context.resources.getString(resId) + return PushConfig(projectId) } - val accountManager = FxaAccountManager( + @VisibleForTesting(otherwise = PRIVATE) + fun makeAccountManager( + context: Context, + serverConfig: ServerConfig, + deviceConfig: DeviceConfig, + syncConfig: SyncConfig? + ) = FxaAccountManager( context, serverConfig, deviceConfig, @@ -173,34 +156,38 @@ class BackgroundServices( // This is a good example of an information leak at the API level. // See https://github.com/mozilla-mobile/android-components/issues/3732 setOf("https://identity.mozilla.com/apps/oldsync") - ).also { + ).also { accountManager -> + // TODO this needs to change once we have a SyncManager context.settings.fxaHasSyncedItems = syncConfig?.supportedEngines?.isNotEmpty() ?: false - it.registerForDeviceEvents(deviceEventObserver, ProcessLifecycleOwner.get(), false) + accountManager.registerForDeviceEvents(deviceEventObserver, ProcessLifecycleOwner.get(), false) + + // Register a telemetry account observer to keep track of FxA auth metrics. + accountManager.register(telemetryAccountObserver) - // Enable push if we have the config. - if (pushConfig != null) { - // Register our account observer so we know how to update our push subscriptions. - it.register(accountObserver) + // Enable push if it's configured. + push?.let { autoPushFeature -> + // Register the push account observer so we know how to update our push subscriptions. + accountManager.register(pushAccountObserver!!) val logger = Logger("AutoPushFeature") // Notify observers for Services' messages. - push.registerForPushMessages( + autoPushFeature.registerForPushMessages( PushType.Services, object : Bus.Observer { override fun onEvent(type: PushType, message: String) { - it.authenticatedAccount()?.deviceConstellation() + accountManager.authenticatedAccount()?.deviceConstellation() ?.processRawEventAsync(message) } }) // Notify observers for subscription changes. - push.registerForSubscriptions(object : PushSubscriptionObserver { + autoPushFeature.registerForSubscriptions(object : PushSubscriptionObserver { override fun onSubscriptionAvailable(subscription: AutoPushSubscription) { // Update for only the services subscription. if (subscription.type == PushType.Services) { logger.info("New push subscription received for FxA") - it.authenticatedAccount()?.deviceConstellation() + accountManager.authenticatedAccount()?.deviceConstellation() ?.setDevicePushSubscriptionAsync( DevicePushSubscription( endpoint = subscription.endpoint, @@ -212,7 +199,7 @@ class BackgroundServices( } }) } - CoroutineScope(Dispatchers.Main).launch { it.initAsync().await() } + accountManager.initAsync() } /** @@ -222,3 +209,74 @@ class BackgroundServices( NotificationManager(context) } } + +@VisibleForTesting(otherwise = PRIVATE) +class TelemetryAccountObserver( + private val context: Context, + private val metricController: MetricController +) : AccountObserver { + override fun onAuthenticated(account: OAuthAccount, authType: AuthType) { + when (authType) { + // User signed-in into an existing FxA account. + AuthType.Signin -> + metricController.track(Event.SyncAuthSignIn) + + // User created a new FxA account. + AuthType.Signup -> + metricController.track(Event.SyncAuthSignUp) + + // User paired to an existing account via QR code scanning. + AuthType.Pairing -> + metricController.track(Event.SyncAuthPaired) + + // User signed-in into an FxA account shared from another locally installed app + // (e.g. Fennec). + AuthType.Shared -> + metricController.track(Event.SyncAuthFromShared) + + // Account Manager recovered a broken FxA auth state, without direct user involvement. + AuthType.Recovered -> + metricController.track(Event.SyncAuthRecovered) + + // User signed-in into an FxA account via unknown means. + // Exact mechanism identified by the 'action' param. + is AuthType.OtherExternal -> + metricController.track(Event.SyncAuthOtherExternal) + } + // Used by Leanplum as a context variable. + context.settings.fxaSignedIn = true + } + + override fun onLoggedOut() { + metricController.track(Event.SyncAuthSignOut) + // Used by Leanplum as a context variable. + context.settings.fxaSignedIn = false + } +} + +/** + * When we login/logout of FxA, we need to update our push subscriptions to match the newly + * logged in account. + * + * We added the push service to the AccountManager observer so that we can control when the + * service will start/stop. Firebase was added when landing the push service to ensure it works + * as expected without causing any (as many) side effects. + * + * In order to use Firebase with Leanplum and other marketing features, we need it always + * running so we cannot leave this code in place when we implement those features. + * + * We should have this removed when we are more confident + * of the send-tab/push feature: https://github.com/mozilla-mobile/fenix/issues/4063 + */ +@VisibleForTesting(otherwise = PRIVATE) +class PushAccountObserver(private val push: AutoPushFeature) : AccountObserver { + override fun onLoggedOut() { + push.unsubscribeForType(PushType.Services) + } + + override fun onAuthenticated(account: OAuthAccount, authType: AuthType) { + if (authType != AuthType.Existing) { + push.subscribeForType(PushType.Services) + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/components/Components.kt b/app/src/main/java/org/mozilla/fenix/components/Components.kt index 585f4fc8d28b..65e29c0be81d 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Components.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Components.kt @@ -16,7 +16,7 @@ class Components(private val context: Context) { val backgroundServices by lazy { BackgroundServices(context, core.historyStorage, core.bookmarksStorage) } - val services by lazy { Services(backgroundServices.accountManager) } + val services by lazy { Services(context, backgroundServices.accountManager) } val core by lazy { Core(context) } val search by lazy { Search(context) } val useCases by lazy { diff --git a/app/src/main/java/org/mozilla/fenix/components/Services.kt b/app/src/main/java/org/mozilla/fenix/components/Services.kt index 8d066b1f6138..1638841042d3 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Services.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Services.kt @@ -25,12 +25,13 @@ import org.mozilla.fenix.test.Mockable */ @Mockable class Services( + private val context: Context, private val accountManager: FxaAccountManager ) { val accountsAuthFeature by lazy { FirefoxAccountsAuthFeature( accountManager, - redirectUrl = BackgroundServices.REDIRECT_URL + redirectUrl = BackgroundServices.redirectUrl(context) ) { context, authUrl -> CoroutineScope(Dispatchers.Main).launch { val intent = SupportUtils.createAuthCustomTabIntent(context, authUrl) diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt index 8aa869c9df34..744f16eb37f6 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt @@ -225,15 +225,27 @@ private val Event.wrapper: EventWrapper<*>? is Event.SyncAuthSignIn -> EventWrapper( { SyncAuth.signIn.record(it) } ) + is Event.SyncAuthSignUp -> EventWrapper( + { SyncAuth.signUp.record(it) } + ) + is Event.SyncAuthPaired -> EventWrapper( + { SyncAuth.paired.record(it) } + ) + is Event.SyncAuthOtherExternal -> EventWrapper( + { SyncAuth.otherExternal.record(it) } + ) + is Event.SyncAuthFromShared -> EventWrapper( + { SyncAuth.autoLogin.record(it) } + ) + is Event.SyncAuthRecovered -> EventWrapper( + { SyncAuth.recovered.record(it) } + ) is Event.SyncAuthSignOut -> EventWrapper( { SyncAuth.signOut.record(it) } ) is Event.SyncAuthScanPairing -> EventWrapper( { SyncAuth.scanPairing.record(it) } ) - is Event.SyncAuthCreateAccount -> EventWrapper( - { SyncAuth.createAccount.record(it) } - ) is Event.SyncAccountOpened -> EventWrapper( { SyncAccount.opened.record(it) } ) diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt index a7e2566e380a..3c86d15c8c5f 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt @@ -64,10 +64,14 @@ sealed class Event { object LibraryClosed : Event() object SyncAuthOpened : Event() object SyncAuthClosed : Event() + object SyncAuthSignUp : Event() object SyncAuthSignIn : Event() object SyncAuthSignOut : Event() object SyncAuthScanPairing : Event() - object SyncAuthCreateAccount : Event() + object SyncAuthPaired : Event() + object SyncAuthRecovered : Event() + object SyncAuthOtherExternal : Event() + object SyncAuthFromShared : Event() object SyncAccountOpened : Event() object SyncAccountClosed : Event() object SyncAccountSyncNow : Event() diff --git a/app/src/main/res/values/preference_keys.xml b/app/src/main/res/values/preference_keys.xml index 88731cd26444..a335dd1685b2 100644 --- a/app/src/main/res/values/preference_keys.xml +++ b/app/src/main/res/values/preference_keys.xml @@ -57,7 +57,6 @@ pref_key_cached_account pref_key_sync_pair pref_key_sync_sign_in - pref_key_sync_create_account pref_key_sync_problem project_id pref_key_fxa_signed_in diff --git a/app/src/test/java/org/mozilla/fenix/components/BackgroundServicesTest.kt b/app/src/test/java/org/mozilla/fenix/components/BackgroundServicesTest.kt new file mode 100644 index 000000000000..cdbe6f74d15b --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/components/BackgroundServicesTest.kt @@ -0,0 +1,215 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.components + +import android.content.Context +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.verify +import mozilla.components.concept.sync.AccountObserver +import mozilla.components.concept.sync.AuthType +import mozilla.components.concept.sync.OAuthAccount +import mozilla.components.feature.push.AutoPushFeature +import mozilla.components.feature.push.PushConfig +import mozilla.components.feature.push.PushType +import mozilla.components.service.fxa.DeviceConfig +import mozilla.components.service.fxa.ServerConfig +import mozilla.components.service.fxa.SyncConfig +import mozilla.components.service.fxa.SyncEngine +import mozilla.components.service.fxa.manager.FxaAccountManager +import mozilla.components.support.base.observer.ObserverRegistry +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.mozilla.fenix.Experiments +import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.components.metrics.MetricController +import org.mozilla.fenix.isInExperiment + +class BackgroundServicesTest { + class TestableBackgroundServices( + val context: Context + ) : BackgroundServices(context, mockk(), mockk()) { + override fun makeAccountManager( + context: Context, + serverConfig: ServerConfig, + deviceConfig: DeviceConfig, + syncConfig: SyncConfig? + ) = mockk(relaxed = true) + + override fun makePushConfig() = mockk(relaxed = true) + override fun makePush(pushConfig: PushConfig) = mockk(relaxed = true) + } + + @Test + fun `experiment flags`() { + val context = mockk(relaxed = true) + + every { context.isInExperiment(eq(Experiments.asFeatureWebChannelsDisabled)) } returns false + assertEquals("urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel", BackgroundServices.redirectUrl(context)) + + every { context.isInExperiment(eq(Experiments.asFeatureWebChannelsDisabled)) } returns true + assertEquals("https://accounts.firefox.com/oauth/success/a2270f727f45f648", BackgroundServices.redirectUrl(context)) + + every { context.isInExperiment(eq(Experiments.asFeatureSyncDisabled)) } returns false + var backgroundServices = TestableBackgroundServices(context) + assertEquals( + SyncConfig(setOf(SyncEngine.HISTORY, SyncEngine.BOOKMARKS), syncPeriodInMinutes = 240L), + backgroundServices.syncConfig + ) + + every { context.isInExperiment(eq(Experiments.asFeatureSyncDisabled)) } returns true + backgroundServices = TestableBackgroundServices(context) + assertNull(backgroundServices.syncConfig) + } + + @Test + fun `push account observer`() { + val push = mockk() + val observer = PushAccountObserver(push) + val registry = ObserverRegistry() + registry.register(observer) + val account = mockk() + + // Being explicit here (vs using 'any()') ensures that any change to which PushType variants + // are being subscribed/unsubscribed will break these tests, forcing developer to expand them. + every { push.subscribeForType(PushType.Services) } just Runs + every { push.unsubscribeForType(PushType.Services) } just Runs + + // 'Existing' auth type doesn't trigger subscription - we're already subscribed. + registry.notifyObservers { onAuthenticated(account, AuthType.Existing) } + verify(exactly = 0) { push.subscribeForType(any()) } + + // Every other auth type does. + registry.notifyObservers { onAuthenticated(account, AuthType.Signin) } + verify(exactly = 1) { push.subscribeForType(eq(PushType.Services)) } + + registry.notifyObservers { onAuthenticated(account, AuthType.Signup) } + verify(exactly = 2) { push.subscribeForType(eq(PushType.Services)) } + + registry.notifyObservers { onAuthenticated(account, AuthType.Recovered) } + verify(exactly = 3) { push.subscribeForType(eq(PushType.Services)) } + + registry.notifyObservers { onAuthenticated(account, AuthType.Shared) } + verify(exactly = 4) { push.subscribeForType(eq(PushType.Services)) } + + registry.notifyObservers { onAuthenticated(account, AuthType.Pairing) } + verify(exactly = 5) { push.subscribeForType(eq(PushType.Services)) } + + registry.notifyObservers { onAuthenticated(account, AuthType.OtherExternal(null)) } + verify(exactly = 6) { push.subscribeForType(eq(PushType.Services)) } + + registry.notifyObservers { onAuthenticated(account, AuthType.OtherExternal("someAction")) } + verify(exactly = 7) { push.subscribeForType(eq(PushType.Services)) } + + // None of the above unsubscribed. + verify(exactly = 0) { push.unsubscribeForType(any()) } + + // Finally, log-out should unsubscribe. + registry.notifyObservers { onLoggedOut() } + verify(exactly = 1) { push.unsubscribeForType(eq(PushType.Services)) } + } + + @Test + fun `telemetry account observer`() { + val metrics = mockk() + every { metrics.track(any()) } just Runs + val observer = TelemetryAccountObserver(mockk(relaxed = true), metrics) + val registry = ObserverRegistry() + registry.register(observer) + val account = mockk() + + // Sign-in + registry.notifyObservers { onAuthenticated(account, AuthType.Signin) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignIn)) } + verify(exactly = 0) { metrics.track(eq(Event.SyncAuthSignUp)) } + verify(exactly = 0) { metrics.track(eq(Event.SyncAuthPaired)) } + verify(exactly = 0) { metrics.track(eq(Event.SyncAuthFromShared)) } + verify(exactly = 0) { metrics.track(eq(Event.SyncAuthRecovered)) } + verify(exactly = 0) { metrics.track(eq(Event.SyncAuthOtherExternal)) } + verify(exactly = 0) { metrics.track(eq(Event.SyncAuthSignOut)) } + + // Sign-up + registry.notifyObservers { onAuthenticated(account, AuthType.Signup) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignIn)) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignUp)) } + verify(exactly = 0) { metrics.track(eq(Event.SyncAuthPaired)) } + verify(exactly = 0) { metrics.track(eq(Event.SyncAuthFromShared)) } + verify(exactly = 0) { metrics.track(eq(Event.SyncAuthRecovered)) } + verify(exactly = 0) { metrics.track(eq(Event.SyncAuthOtherExternal)) } + verify(exactly = 0) { metrics.track(eq(Event.SyncAuthSignOut)) } + + // Pairing + registry.notifyObservers { onAuthenticated(account, AuthType.Pairing) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignIn)) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignUp)) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthPaired)) } + verify(exactly = 0) { metrics.track(eq(Event.SyncAuthFromShared)) } + verify(exactly = 0) { metrics.track(eq(Event.SyncAuthRecovered)) } + verify(exactly = 0) { metrics.track(eq(Event.SyncAuthOtherExternal)) } + verify(exactly = 0) { metrics.track(eq(Event.SyncAuthSignOut)) } + + // Auto-login/shared account + registry.notifyObservers { onAuthenticated(account, AuthType.Shared) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignIn)) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignUp)) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthPaired)) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthFromShared)) } + verify(exactly = 0) { metrics.track(eq(Event.SyncAuthRecovered)) } + verify(exactly = 0) { metrics.track(eq(Event.SyncAuthOtherExternal)) } + verify(exactly = 0) { metrics.track(eq(Event.SyncAuthSignOut)) } + + // Internally recovered + registry.notifyObservers { onAuthenticated(account, AuthType.Recovered) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignIn)) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignUp)) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthPaired)) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthFromShared)) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthRecovered)) } + verify(exactly = 0) { metrics.track(eq(Event.SyncAuthOtherExternal)) } + verify(exactly = 0) { metrics.track(eq(Event.SyncAuthSignOut)) } + + // Other external + registry.notifyObservers { onAuthenticated(account, AuthType.OtherExternal(null)) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignIn)) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignUp)) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthPaired)) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthFromShared)) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthRecovered)) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthOtherExternal)) } + verify(exactly = 0) { metrics.track(eq(Event.SyncAuthSignOut)) } + + registry.notifyObservers { onAuthenticated(account, AuthType.OtherExternal("someAction")) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignIn)) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignUp)) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthPaired)) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthFromShared)) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthRecovered)) } + verify(exactly = 2) { metrics.track(eq(Event.SyncAuthOtherExternal)) } + verify(exactly = 0) { metrics.track(eq(Event.SyncAuthSignOut)) } + + // NB: 'Existing' auth type isn't expected to record any auth telemetry. + registry.notifyObservers { onAuthenticated(account, AuthType.Existing) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignIn)) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignUp)) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthPaired)) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthFromShared)) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthRecovered)) } + verify(exactly = 2) { metrics.track(eq(Event.SyncAuthOtherExternal)) } + verify(exactly = 0) { metrics.track(eq(Event.SyncAuthSignOut)) } + + // Logout + registry.notifyObservers { onLoggedOut() } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignIn)) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignUp)) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthPaired)) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthFromShared)) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthRecovered)) } + verify(exactly = 2) { metrics.track(eq(Event.SyncAuthOtherExternal)) } + verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignOut)) } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/components/TestComponents.kt b/app/src/test/java/org/mozilla/fenix/components/TestComponents.kt index c4b1fbc6834f..c07ae1d8f973 100644 --- a/app/src/test/java/org/mozilla/fenix/components/TestComponents.kt +++ b/app/src/test/java/org/mozilla/fenix/components/TestComponents.kt @@ -13,7 +13,7 @@ class TestComponents(private val context: Context) : Components(context) { override val backgroundServices by lazy { mockk(relaxed = true) } - override val services by lazy { Services(backgroundServices.accountManager) } + override val services by lazy { Services(context, backgroundServices.accountManager) } override val core by lazy { TestCore(context) } override val search by lazy { Search(context) } override val useCases by lazy { diff --git a/docs/metrics.md b/docs/metrics.md index 095b79241eb7..b62cf1418d55 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -128,12 +128,16 @@ The following metrics are added to the ping: | sync_account.send_tab |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user sent the current tab to another FxA device |[1](https://github.com/mozilla-mobile/fenix/pull/5106)||2020-03-01 | | sync_account.sign_in_to_send_tab |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user pressed the "sign in to send tab" button inside the share tab menu |[1](https://github.com/mozilla-mobile/fenix/pull/5106)||2020-03-01 | | sync_account.sync_now |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user pressed the sync now button on the sync account page |[1](https://github.com/mozilla-mobile/fenix/pull/2745#issuecomment-494918532)||2020-03-01 | +| sync_auth.auto_login |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |User signed into FxA via an account shared from another locally installed Mozilla application (e.g. Fennec) |[1](https://github.com/mozilla-mobile/fenix/pull/4931#issuecomment-529740300)||2020-03-01 | | sync_auth.closed |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user closed the sync page |[1](https://github.com/mozilla-mobile/fenix/pull/2745#issuecomment-494918532)||2020-03-01 | -| sync_auth.create_account |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user pressed the create account button on the sync authentication page |[1](https://github.com/mozilla-mobile/fenix/pull/2745#issuecomment-494918532)||2020-03-01 | | sync_auth.opened |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user opened the sync authentication page |[1](https://github.com/mozilla-mobile/fenix/pull/2745#issuecomment-494918532)||2020-03-01 | +| sync_auth.other_external |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |User authenticated via FxA using an unknown mechanism. "Known" mechanisms are currently sign-in, sign-up and pairing |[1](https://github.com/mozilla-mobile/fenix/pull/4931#issuecomment-529740300)||2020-03-01 | +| sync_auth.paired |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |User signed into FxA by pairing with a different Firefox browser, using a QR code |[1](https://github.com/mozilla-mobile/fenix/pull/4931#issuecomment-529740300)||2020-03-01 | +| sync_auth.recovered |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |Account manager automatically recovered FxA authentication state without direct user involvement |[1](https://github.com/mozilla-mobile/fenix/pull/4931#issuecomment-529740300)||2020-03-01 | | sync_auth.scan_pairing |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user pressed the scan pairing button on the sync authentication page |[1](https://github.com/mozilla-mobile/fenix/pull/2745#issuecomment-494918532)||2020-03-01 | | sync_auth.sign_in |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user pressed the sign in button on the sync authentication page and was successfully signed in to FxA |[1](https://github.com/mozilla-mobile/fenix/pull/2745#issuecomment-494918532)||2020-03-01 | | sync_auth.sign_out |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user pressed the sign out button on the sync account page and was successfully signed out of FxA |[1](https://github.com/mozilla-mobile/fenix/pull/2745#issuecomment-494918532)||2020-03-01 | +| sync_auth.sign_up |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |User registered a new Firefox Account, and was signed into it |[1](https://github.com/mozilla-mobile/fenix/pull/4931#issuecomment-529740300)||2020-03-01 | ## metrics This is a built-in ping that is assembled out of the box by the Glean SDK.