diff --git a/app/build.gradle b/app/build.gradle index fd2e6c5d3..9cd45aa86 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -178,6 +178,7 @@ dependencies { implementation Deps.mozilla_browser_storage_sync implementation Deps.mozilla_browser_toolbar + implementation Deps.mozilla_feature_accounts implementation Deps.mozilla_feature_awesomebar implementation Deps.mozilla_feature_contextmenu implementation Deps.mozilla_feature_customtabs @@ -190,6 +191,7 @@ dependencies { implementation Deps.mozilla_feature_findinpage implementation Deps.mozilla_feature_session_bundling + implementation Deps.mozilla_service_firefox_accounts implementation Deps.mozilla_service_fretboard implementation Deps.mozilla_service_glean diff --git a/app/src/main/java/org/mozilla/fenix/AppRequestInterceptor.kt b/app/src/main/java/org/mozilla/fenix/AppRequestInterceptor.kt index 121ed5559..63d3655dd 100644 --- a/app/src/main/java/org/mozilla/fenix/AppRequestInterceptor.kt +++ b/app/src/main/java/org/mozilla/fenix/AppRequestInterceptor.kt @@ -9,6 +9,7 @@ import mozilla.components.browser.errorpages.ErrorPages import mozilla.components.browser.errorpages.ErrorType import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.request.RequestInterceptor +import org.mozilla.fenix.ext.components import org.mozilla.fenix.settings.AboutPage import org.mozilla.fenix.settings.SettingsFragment @@ -23,7 +24,7 @@ class AppRequestInterceptor(private val context: Context) : RequestInterceptor { return RequestInterceptor.InterceptionResponse.Content(page, encoding = base64) } - else -> null + else -> context.components.services.accountsAuthFeature.interceptor.onLoadRequest(session, uri) } } diff --git a/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt b/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt new file mode 100644 index 000000000..afc1fcdb7 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt @@ -0,0 +1,32 @@ +/* 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 mozilla.components.service.fxa.Config +import mozilla.components.service.fxa.FxaAccountManager + +/** + * Component group for background services. These are the components that need to be accessed from within a + * background worker. + */ +class BackgroundServices( + context: Context +) { + companion object { + const val CLIENT_ID = "a2270f727f45f648" + const val REDIRECT_URL = "https://accounts.firefox.com/oauth/success/$CLIENT_ID" + const val SUCCESS_PATH = "connect_another_device?showSuccessMessage=true" + } + + // This is slightly messy - here we need to know the union of all "scopes" + // needed by components which rely on FxA integration. If this list + // grows too far we probably want to find a way to determine the set + // at runtime. + private val scopes: Array = arrayOf("profile", "https://identity.mozilla.com/apps/oldsync") + private val config = Config.release(CLIENT_ID, REDIRECT_URL) + + val accountManager = FxaAccountManager(context, config, scopes).also { it.init() } +} 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 6193a4405..7d474d096 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Components.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Components.kt @@ -10,6 +10,8 @@ import android.content.Context * Provides access to all components. */ class Components(private val context: Context) { + val backgroundServices by lazy { BackgroundServices(context) } + val services by lazy { Services(backgroundServices.accountManager, useCases.tabsUseCases) } val core by lazy { Core(context) } val search by lazy { Search(context) } val useCases by lazy { UseCases(context, core.sessionManager, search.searchEngineManager) } diff --git a/app/src/main/java/org/mozilla/fenix/components/Services.kt b/app/src/main/java/org/mozilla/fenix/components/Services.kt new file mode 100644 index 000000000..1fb56af88 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/components/Services.kt @@ -0,0 +1,26 @@ +/* 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 mozilla.components.feature.accounts.FirefoxAccountsAuthFeature +import mozilla.components.feature.tabs.TabsUseCases +import mozilla.components.service.fxa.FxaAccountManager + +/** + * Component group which encapsulates foreground-friendly services. + */ +class Services( + private val accountManager: FxaAccountManager, + private val tabsUseCases: TabsUseCases +) { + val accountsAuthFeature by lazy { + FirefoxAccountsAuthFeature( + accountManager, + tabsUseCases, + redirectUrl = BackgroundServices.REDIRECT_URL, + successPath = BackgroundServices.SUCCESS_PATH + ) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt index 9a8cfcbac..b8d51591e 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt @@ -22,6 +22,10 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlin.coroutines.CoroutineContext import java.io.File +import mozilla.components.service.fxa.AccountObserver +import mozilla.components.service.fxa.FirefoxAccountShaped +import mozilla.components.service.fxa.FxaUnauthorizedException +import mozilla.components.service.fxa.Profile import mozilla.components.support.ktx.android.graphics.toDataUri import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.FenixApplication @@ -38,10 +42,11 @@ import org.mozilla.fenix.R.string.pref_key_accessibility import org.mozilla.fenix.R.string.pref_key_language import org.mozilla.fenix.R.string.pref_key_data_choices import org.mozilla.fenix.R.string.pref_key_about +import org.mozilla.fenix.R.string.pref_key_sign_in +import org.mozilla.fenix.R.string.pref_key_account -@Suppress("TooManyFunctions") -class SettingsFragment : PreferenceFragmentCompat(), CoroutineScope { - +@SuppressWarnings("TooManyFunctions") +class SettingsFragment : PreferenceFragmentCompat(), CoroutineScope, AccountObserver { private lateinit var job: Job override val coroutineContext: CoroutineContext get() = Dispatchers.Main + job @@ -49,6 +54,7 @@ class SettingsFragment : PreferenceFragmentCompat(), CoroutineScope { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) job = Job() + setupAccountUI() } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { @@ -103,6 +109,46 @@ class SettingsFragment : PreferenceFragmentCompat(), CoroutineScope { job.cancel() } + private fun setupAccountUI() { + val signIn = context?.getPreferenceKey(pref_key_sign_in) + val firefoxAccountKey = context?.getPreferenceKey(pref_key_account) + + val preferenceSignIn = findPreference(signIn) + val preferenceFirefoxAccount = findPreference(firefoxAccountKey) + + preferenceSignIn.isVisible = true + preferenceFirefoxAccount.isVisible = false + preferenceSignIn.onPreferenceClickListener = getClickListenerForSignIn() + + val accountManager = requireComponents.backgroundServices.accountManager + // Observe account changes to keep the UI up-to-date. + accountManager.register(this, owner = this) + + // TODO an authenticated state will mark 'preferenceSignIn' as invisible; currently that behaviour is non-ideal: + // a "sign in" UI will be displayed at first, and then quickly animate away. + // Ideally we don't want it to be displayed at all. + accountManager.authenticatedAccount()?.let { setIsAuthenticated(it) } + accountManager.accountProfile()?.let { updateAccountProfile(it) } + } + + private fun getClickListenerForSignIn(): OnPreferenceClickListener { + return OnPreferenceClickListener { + requireComponents.services.accountsAuthFeature.beginAuthentication() + // TODO the "back button" behaviour here is pretty poor. The sign-in web content populates session history, + // so pressing "back" after signing in won't take us back into the settings screen, but rather up the + // session history stack. + // We could auto-close this tab once we get to the end of the authentication process? + // Via an interceptor, perhaps. + view?.let { + Navigation.findNavController(it) + .navigate( + SettingsFragmentDirections.actionGlobalBrowser(null) + ) + } + true + } + } + /** * Shrinks the wordmark resolution on first run to ensure About Page loads quickly */ @@ -189,6 +235,53 @@ class SettingsFragment : PreferenceFragmentCompat(), CoroutineScope { } } + // --- AccountObserver interfaces --- + override fun onAuthenticated(account: FirefoxAccountShaped) { + setIsAuthenticated(account) + } + + override fun onError(error: Exception) { + // TODO we could display some error states in this UI. + when (error) { + is FxaUnauthorizedException -> {} + } + } + + override fun onLoggedOut() { + setIsLoggedOut() + } + + override fun onProfileUpdated(profile: Profile) { + updateAccountProfile(profile) + } + + // --- Account UI helpers --- + private fun setIsAuthenticated(account: FirefoxAccountShaped) { + val preferenceSignIn = findPreference(context?.getPreferenceKey(pref_key_sign_in)) + val preferenceFirefoxAccount = findPreference(context?.getPreferenceKey(pref_key_account)) + + preferenceSignIn.isVisible = false + preferenceSignIn.onPreferenceClickListener = null + preferenceFirefoxAccount.isVisible = true + } + + private fun setIsLoggedOut() { + val preferenceSignIn = findPreference(context?.getPreferenceKey(pref_key_sign_in)) + val preferenceFirefoxAccount = findPreference(context?.getPreferenceKey(pref_key_account)) + + preferenceSignIn.isVisible = true + + // TODO this isn't quite right, as we'll have an "Account" preference category title still visible on the screen + preferenceFirefoxAccount.isVisible = false + preferenceSignIn.onPreferenceClickListener = getClickListenerForSignIn() + } + + private fun updateAccountProfile(profile: Profile) { + val preferenceFirefoxAccount = findPreference(context?.getPreferenceKey(pref_key_account)) + preferenceFirefoxAccount.title = profile.displayName.orEmpty() + preferenceFirefoxAccount.summary = profile.email.orEmpty() + } + companion object { const val wordmarkScalingFactor = 2 const val wordmarkPath = "wordmark.b64" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e9335b912..4f43a09f6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -102,6 +102,8 @@ Get your tabs, bookmarks, logins, history and more on all your devices + + Firefox Account Language diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index ccd6b0abb..42484c98d 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -18,8 +18,7 @@ + android:title="@string/preferences_account_default_name" />