/* 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 android.os.Build import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting.Companion.PRIVATE import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import mozilla.components.browser.storage.sync.PlacesBookmarksStorage import mozilla.components.browser.storage.sync.PlacesHistoryStorage import mozilla.components.browser.storage.sync.RemoteTabsStorage import mozilla.components.concept.sync.AccountObserver import mozilla.components.concept.sync.AuthType import mozilla.components.concept.sync.DeviceCapability import mozilla.components.concept.sync.DeviceConfig import mozilla.components.concept.sync.DeviceType import mozilla.components.concept.sync.OAuthAccount import mozilla.components.feature.accounts.push.FxaPushSupportFeature import mozilla.components.feature.accounts.push.SendTabFeature import mozilla.components.feature.syncedtabs.SyncedTabsAutocompleteProvider import mozilla.components.feature.syncedtabs.storage.SyncedTabsStorage import mozilla.components.lib.crash.CrashReporter import mozilla.components.service.fxa.PeriodicSyncConfig 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.service.fxa.manager.SCOPE_SESSION import mozilla.components.service.fxa.manager.SCOPE_SYNC import mozilla.components.service.fxa.store.SyncStore import mozilla.components.service.fxa.store.SyncStoreSupport import mozilla.components.service.fxa.sync.GlobalSyncableStoreProvider import mozilla.components.service.glean.private.NoExtras import mozilla.components.service.sync.autofill.AutofillCreditCardsAddressesStorage import mozilla.components.service.sync.logins.SyncableLoginsStorage import mozilla.components.support.utils.RunWhenReadyQueue import org.mozilla.fenix.Config import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.GleanMetrics.SyncAuth import org.mozilla.fenix.R import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.maxActiveTime import org.mozilla.fenix.ext.settings import org.mozilla.fenix.perf.StrictModeManager import org.mozilla.fenix.perf.lazyMonitored import org.mozilla.fenix.sync.SyncedTabsIntegration /** * Component group for background services. These are the components that need to be accessed from within a * background worker. */ @Suppress("LongParameterList") class BackgroundServices( private val context: Context, private val push: Push, crashReporter: CrashReporter, historyStorage: Lazy, bookmarkStorage: Lazy, passwordsStorage: Lazy, remoteTabsStorage: Lazy, creditCardsStorage: Lazy, strictMode: StrictModeManager, ) { // Allows executing tasks which depend on the account manager, but do not need to eagerly initialize it. val accountManagerAvailableQueue = RunWhenReadyQueue() fun defaultDeviceName(context: Context): String = context.getString( R.string.default_device_name_2, context.getString(R.string.app_name), Build.MANUFACTURER, Build.MODEL, ) val serverConfig = FxaServer.config(context) private val deviceConfig = DeviceConfig( name = defaultDeviceName(context), type = DeviceType.MOBILE, // NB: flipping this flag back and worth is currently not well supported and may need hand-holding. // Consult with the android-components peers before changing. // See https://github.com/mozilla/application-services/issues/1308 capabilities = setOf(DeviceCapability.SEND_TAB), // Enable encryption for account state on supported API levels (23+). // Just on Nightly and local builds for now. // Enabling this for all channels is tracked in https://github.com/mozilla-mobile/fenix/issues/6704 secureStateAtRest = Config.channel.isNightlyOrDebug, ) @VisibleForTesting val supportedEngines = setOfNotNull( SyncEngine.History, SyncEngine.Bookmarks, SyncEngine.Passwords, SyncEngine.Tabs, SyncEngine.CreditCards, if (FeatureFlags.syncAddressesFeature) SyncEngine.Addresses else null, ) private val syncConfig = SyncConfig(supportedEngines, PeriodicSyncConfig(periodMinutes = 240)) // four hours private val creditCardKeyProvider by lazyMonitored { creditCardsStorage.value.crypto } private val passwordKeyProvider by lazyMonitored { passwordsStorage.value.crypto } init { // Make the "history", "bookmark", "passwords", "tabs", "credit cards" stores // accessible to workers spawned by the sync manager. GlobalSyncableStoreProvider.configureStore(SyncEngine.History to historyStorage) GlobalSyncableStoreProvider.configureStore(SyncEngine.Bookmarks to bookmarkStorage) GlobalSyncableStoreProvider.configureStore( storePair = SyncEngine.Passwords to passwordsStorage, keyProvider = lazy { passwordKeyProvider }, ) GlobalSyncableStoreProvider.configureStore(SyncEngine.Tabs to remoteTabsStorage) GlobalSyncableStoreProvider.configureStore( storePair = SyncEngine.CreditCards to creditCardsStorage, keyProvider = lazy { creditCardKeyProvider }, ) if (FeatureFlags.syncAddressesFeature) { GlobalSyncableStoreProvider.configureStore(SyncEngine.Addresses to creditCardsStorage) } } private val telemetryAccountObserver = TelemetryAccountObserver( context, ) val accountAbnormalities = AccountAbnormalities(context, crashReporter, strictMode) val syncStore by lazyMonitored { SyncStore() } private lateinit var syncStoreSupport: SyncStoreSupport val accountManager by lazyMonitored { makeAccountManager(context, serverConfig, deviceConfig, syncConfig, crashReporter) } val syncedTabsStorage by lazyMonitored { SyncedTabsStorage(accountManager, context.components.core.store, remoteTabsStorage.value, maxActiveTime) } val syncedTabsAutocompleteProvider by lazyMonitored { SyncedTabsAutocompleteProvider(syncedTabsStorage) } @VisibleForTesting(otherwise = PRIVATE) fun makeAccountManager( context: Context, serverConfig: ServerConfig, deviceConfig: DeviceConfig, syncConfig: SyncConfig?, crashReporter: CrashReporter?, ) = FxaAccountManager( context, serverConfig, deviceConfig, syncConfig, setOf( // We don't need to specify sync scope explicitly, but `syncConfig` may be disabled due to // an 'experiments' flag. In that case, sync scope necessary for syncing won't be acquired // during authentication unless we explicitly specify it below. // This is a good example of an information leak at the API level. // See https://github.com/mozilla-mobile/android-components/issues/3732 SCOPE_SYNC, // Necessary to enable "Manage Account" functionality and ability to generate OAuth // codes for certain scopes. SCOPE_SESSION, ), crashReporter, ).also { accountManager -> // Register a telemetry account observer to keep track of FxA auth metrics. accountManager.register(telemetryAccountObserver) // Register an "abnormal fxa behaviour" middleware to keep track of events such as // unexpected logouts. accountManager.register(accountAbnormalities) accountManager.register(AccountManagerReadyObserver(accountManagerAvailableQueue)) // Enable push if it's configured. push.feature?.let { autoPushFeature -> FxaPushSupportFeature(context, accountManager, autoPushFeature, crashReporter) .initialize() } SendTabFeature(accountManager) { device, tabs -> notificationManager.showReceivedTabs(context, device, tabs) } SyncedTabsIntegration(context, accountManager).launch() syncStoreSupport = SyncStoreSupport(syncStore, lazyOf(accountManager)).also { it.initialize() } MainScope().launch { accountManager.start() } } /** * Provides notification functionality, manages notification channels. */ private val notificationManager by lazyMonitored { NotificationManager(context) } } private class AccountManagerReadyObserver( private val accountManagerAvailableQueue: RunWhenReadyQueue, ) : AccountObserver { override fun onReady(authenticatedAccount: OAuthAccount?) { accountManagerAvailableQueue.ready() } } @VisibleForTesting(otherwise = PRIVATE) internal class TelemetryAccountObserver( private val context: Context, ) : AccountObserver { override fun onAuthenticated(account: OAuthAccount, authType: AuthType) { context.settings().signedInFxaAccount = true when (authType) { // User signed-in into an existing FxA account. AuthType.Signin -> { SyncAuth.signIn.record(NoExtras()) context.components.analytics.experiments.recordEvent("sync_auth.sign_in") } // User created a new FxA account. AuthType.Signup -> SyncAuth.signUp.record(NoExtras()) // User paired to an existing account via QR code scanning. AuthType.Pairing -> SyncAuth.paired.record(NoExtras()) // Account Manager recovered a broken FxA auth state, without direct user involvement. AuthType.Recovered -> SyncAuth.recovered.record(NoExtras()) // User signed-in into an FxA account via unknown means. // Exact mechanism identified by the 'action' param. is AuthType.OtherExternal -> SyncAuth.otherExternal.record(NoExtras()) // User signed-in into an FxA account shared from another locally installed app using the copy flow. AuthType.MigratedCopy, // User signed-in into an FxA account shared from another locally installed app using the reuse flow. AuthType.MigratedReuse, // Account restored from a hydrated state on disk (e.g. during startup). AuthType.Existing, -> { // no-op, events not recorded in Glean } } } override fun onLoggedOut() { SyncAuth.signOut.record(NoExtras()) context.settings().signedInFxaAccount = false } }