diff --git a/app/build.gradle b/app/build.gradle
index 929247027..53f96cf77 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -396,6 +396,9 @@ dependencies {
implementation Deps.mozilla_browser_storage_sync
implementation Deps.mozilla_browser_toolbar
+ implementation Deps.mozilla_support_extensions
+ implementation Deps.mozilla_feature_addons
+
implementation Deps.mozilla_feature_accounts
implementation Deps.mozilla_feature_app_links
implementation Deps.mozilla_feature_awesomebar
diff --git a/app/metrics.yaml b/app/metrics.yaml
index 00b93cc00..16b6f8637 100644
--- a/app/metrics.yaml
+++ b/app/metrics.yaml
@@ -82,7 +82,7 @@ events:
A string containing the name of the item the user tapped. These items include:
Settings, Library, Help, Desktop Site toggle on/off, Find in Page, New Tab,
Private Tab, Share, Report Site Issue, Back/Forward button, Reload Button, Quit,
- Reader Mode On, Reader Mode Off, Open In App, Add to Firefox Home
+ Reader Mode On, Reader Mode Off, Open In App, Add to Firefox Home, Add-ons Manager
bugs:
- https://github.com/mozilla-mobile/fenix/issues/1024
data_reviews:
diff --git a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt
index 3cda7d960..c181a8a34 100644
--- a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt
+++ b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt
@@ -18,6 +18,7 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import mozilla.appservices.Megazord
+import mozilla.components.browser.session.Session
import mozilla.components.concept.push.PushProcessor
import mozilla.components.service.experiments.Experiments
import mozilla.components.service.glean.Glean
@@ -31,6 +32,7 @@ import mozilla.components.support.ktx.android.content.runOnlyInMainProcess
import mozilla.components.support.locale.LocaleAwareApplication
import mozilla.components.support.rusthttp.RustHttpConfig
import mozilla.components.support.rustlog.RustLog
+import mozilla.components.support.webextensions.WebExtensionSupport
import org.mozilla.fenix.components.Components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.session.NotificationSessionObserver
@@ -116,6 +118,8 @@ open class FenixApplication : LocaleAwareApplication() {
// Make sure the engine is initialized and ready to use.
components.core.engine.warmUp()
+ initializeWebExtensionSupport()
+
// Just to make sure it is impossible for any application-services pieces
// to invoke parts of itself that require complete megazord initialization
// before that process completes, we wait here, if necessary.
@@ -277,4 +281,29 @@ open class FenixApplication : LocaleAwareApplication() {
StrictMode.setVmPolicy(builder.build())
}
}
+
+ private fun initializeWebExtensionSupport() {
+ try {
+ WebExtensionSupport.initialize(
+ components.core.engine,
+ components.core.store,
+ onNewTabOverride = {
+ _, engineSession, url ->
+ val session = Session(url)
+ components.core.sessionManager.add(session, true, engineSession)
+ session.id
+ },
+ onCloseTabOverride = {
+ _, sessionId -> components.tabsUseCases.removeTab(sessionId)
+ },
+ onSelectTabOverride = {
+ _, sessionId ->
+ val selected = components.core.sessionManager.findSessionById(sessionId)
+ selected?.let { components.tabsUseCases.selectTab(it) }
+ }
+ )
+ } catch (e: UnsupportedOperationException) {
+ Logger.error("Failed to initialize web extension support", e)
+ }
+ }
}
diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt
index 4df8affcd..1db13313d 100644
--- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt
+++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt
@@ -23,10 +23,12 @@ import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.NavigationUI
import kotlinx.android.synthetic.main.activity_home.navigationToolbarStub
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
+import mozilla.components.browser.state.state.WebExtensionState
import mozilla.components.concept.engine.EngineView
import mozilla.components.service.fxa.sync.SyncReason
import mozilla.components.support.base.feature.UserInteractionHandler
@@ -35,6 +37,7 @@ import mozilla.components.support.ktx.kotlin.toNormalizedUrl
import mozilla.components.support.locale.LocaleAwareAppCompatActivity
import mozilla.components.support.utils.SafeIntent
import mozilla.components.support.utils.toSafeIntent
+import mozilla.components.support.webextensions.WebExtensionPopupFeature
import org.mozilla.fenix.browser.UriOpenedObserver
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
@@ -70,6 +73,7 @@ import org.mozilla.fenix.utils.BrowsersCache
@SuppressWarnings("TooManyFunctions", "LargeClass")
open class HomeActivity : LocaleAwareAppCompatActivity() {
+ private var webExtScope: CoroutineScope? = null
lateinit var themeManager: ThemeManager
lateinit var browsingModeManager: BrowsingModeManager
@@ -79,6 +83,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity() {
private var isToolbarInflated = false
+ private val webExtensionPopupFeature by lazy {
+ WebExtensionPopupFeature(components.core.store, ::openPopup)
+ }
+
private val navHost by lazy {
supportFragmentManager.findFragmentById(R.id.container) as NavHostFragment
}
@@ -126,6 +134,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity() {
?.also { components.analytics.metrics.track(Event.OpenedApp(it)) }
}
supportActionBar?.hide()
+
+ lifecycle.addObserver(webExtensionPopupFeature)
}
@CallSuper
@@ -377,6 +387,14 @@ open class HomeActivity : LocaleAwareAppCompatActivity() {
return DefaultThemeManager(browsingModeManager.mode, this)
}
+ private fun openPopup(webExtensionState: WebExtensionState) {
+ val action = NavGraphDirections.actionGlobalWebExtensionActionPopupFragment(
+ webExtensionId = webExtensionState.id,
+ webExtensionTitle = webExtensionState.name
+ )
+ navHost.navController.navigate(action)
+ }
+
companion object {
const val OPEN_TO_BROWSER = "open_to_browser"
const val OPEN_TO_BROWSER_AND_LOAD = "open_to_browser_and_load"
diff --git a/app/src/main/java/org/mozilla/fenix/addons/AddonDetailsFragment.kt b/app/src/main/java/org/mozilla/fenix/addons/AddonDetailsFragment.kt
new file mode 100644
index 000000000..57ff50878
--- /dev/null
+++ b/app/src/main/java/org/mozilla/fenix/addons/AddonDetailsFragment.kt
@@ -0,0 +1,109 @@
+/* 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.addons
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.text.method.LinkMovementMethod
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.text.HtmlCompat
+import androidx.fragment.app.Fragment
+import kotlinx.android.synthetic.main.fragment_add_on_details.view.*
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.ui.translate
+import org.mozilla.fenix.R
+import org.mozilla.fenix.ext.showToolbar
+import java.text.DateFormat
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+/**
+ * A fragment to show the details of an add-on.
+ */
+class AddonDetailsFragment : Fragment() {
+ private val addon: Addon by lazy {
+ AddonDetailsFragmentArgs.fromBundle(requireNotNull(arguments)).addon
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ return inflater.inflate(R.layout.fragment_add_on_details, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ bind(addon, view)
+ }
+
+ private fun bind(addon: Addon, view: View) {
+ val title = addon.translatableName.translate()
+ showToolbar(title)
+
+ bindDetails(addon, view)
+ bindAuthors(addon, view)
+ bindVersion(addon, view)
+ bindLastUpdated(addon, view)
+ bindWebsite(addon, view)
+ bindRating(addon, view)
+ }
+
+ private fun bindRating(addon: Addon, view: View) {
+ addon.rating?.let {
+ val ratingView = view.rating_view
+ val userCountView = view.users_count
+
+ val ratingContentDescription =
+ getString(R.string.mozac_feature_addons_rating_content_description)
+ ratingView.contentDescription = String.format(ratingContentDescription, it.average)
+ ratingView.rating = it.average
+
+ userCountView.text = getFormattedAmount(it.reviews)
+ }
+ }
+
+ private fun bindWebsite(addon: Addon, view: View) {
+ view.home_page_text.setOnClickListener {
+ val intent =
+ Intent(Intent.ACTION_VIEW).setData(Uri.parse(addon.siteUrl))
+ startActivity(intent)
+ }
+ }
+
+ private fun bindLastUpdated(addon: Addon, view: View) {
+ view.last_updated_text.text = formatDate(addon.updatedAt)
+ }
+
+ private fun bindVersion(addon: Addon, view: View) {
+ view.version_text.text = addon.version
+ }
+
+ private fun bindAuthors(addon: Addon, view: View) {
+ view.author_text.text = addon.authors.joinToString { author ->
+ author.name + " \n"
+ }
+ }
+
+ private fun bindDetails(addon: Addon, view: View) {
+ val detailsView = view.details
+ val detailsText = addon.translatableDescription.translate()
+
+ val parsedText = detailsText.replace("\n", " ")
+ val text = HtmlCompat.fromHtml(parsedText, HtmlCompat.FROM_HTML_MODE_COMPACT)
+
+ detailsView.text = text
+ detailsView.movementMethod = LinkMovementMethod.getInstance()
+ }
+
+ private fun formatDate(text: String): String {
+ val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault())
+ return DateFormat.getDateInstance().format(formatter.parse(text)!!)
+ }
+}
diff --git a/app/src/main/java/org/mozilla/fenix/addons/AddonInternalSettingsFragment.kt b/app/src/main/java/org/mozilla/fenix/addons/AddonInternalSettingsFragment.kt
new file mode 100644
index 000000000..48a6feb23
--- /dev/null
+++ b/app/src/main/java/org/mozilla/fenix/addons/AddonInternalSettingsFragment.kt
@@ -0,0 +1,55 @@
+/* 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.addons
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import kotlinx.android.synthetic.main.fragment_add_on_internal_settings.*
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.ui.translate
+import org.mozilla.fenix.R
+import org.mozilla.fenix.ext.requireComponents
+import org.mozilla.fenix.ext.showToolbar
+
+/**
+ * A fragment to show the internal settings of an add-on.
+ */
+class AddonInternalSettingsFragment : Fragment() {
+ private val addon: Addon by lazy {
+ AddonDetailsFragmentArgs.fromBundle(requireNotNull(arguments)).addon
+ }
+ private lateinit var engineSession: EngineSession
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ engineSession = requireComponents.core.engine.createSession()
+
+ return inflater.inflate(R.layout.fragment_add_on_internal_settings, container, false)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ showToolbar(addon.translatableName.translate())
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ addonSettingsEngineView.render(engineSession)
+ engineSession.loadUrl(addon.installedState!!.optionsPageUrl)
+ }
+
+ override fun onDestroyView() {
+ engineSession.close()
+ super.onDestroyView()
+ }
+}
diff --git a/app/src/main/java/org/mozilla/fenix/addons/AddonPermissionsDetailsFragment.kt b/app/src/main/java/org/mozilla/fenix/addons/AddonPermissionsDetailsFragment.kt
new file mode 100644
index 000000000..191a7ed49
--- /dev/null
+++ b/app/src/main/java/org/mozilla/fenix/addons/AddonPermissionsDetailsFragment.kt
@@ -0,0 +1,70 @@
+/* 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.addons
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.StringRes
+import androidx.fragment.app.Fragment
+import androidx.recyclerview.widget.LinearLayoutManager
+import kotlinx.android.synthetic.main.fragment_add_on_permissions.view.*
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.ui.AddonPermissionsAdapter
+import mozilla.components.feature.addons.ui.translate
+import org.mozilla.fenix.R
+import org.mozilla.fenix.ext.showToolbar
+
+private const val LEARN_MORE_URL =
+ "https://support.mozilla.org/kb/permission-request-messages-firefox-extensions"
+
+/**
+ * A fragment to show the permissions of an add-on.
+ */
+class AddonPermissionsDetailsFragment : Fragment(), View.OnClickListener {
+ private val addon: Addon by lazy {
+ AddonDetailsFragmentArgs.fromBundle(requireNotNull(arguments)).addon
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ return inflater.inflate(R.layout.fragment_add_on_permissions, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ showToolbar(addon.translatableName.translate())
+
+ bindPermissions(addon, view)
+ bindLearnMore(view)
+ }
+
+ private fun bindPermissions(addon: Addon, view: View) {
+ view.add_ons_permissions.apply {
+ layoutManager = LinearLayoutManager(requireContext())
+ val sortedPermissions = addon.translatePermissions().map {
+ @StringRes val stringId = it
+ getString(stringId)
+ }.sorted()
+ adapter = AddonPermissionsAdapter(sortedPermissions)
+ }
+ }
+
+ private fun bindLearnMore(view: View) {
+ view.learn_more_label.setOnClickListener(this)
+ }
+
+ override fun onClick(v: View?) {
+ val intent =
+ Intent(Intent.ACTION_VIEW).setData(Uri.parse(LEARN_MORE_URL))
+ startActivity(intent)
+ }
+}
diff --git a/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt b/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt
new file mode 100644
index 000000000..ead13842d
--- /dev/null
+++ b/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt
@@ -0,0 +1,172 @@
+/* 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.addons
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.navigation.Navigation
+import androidx.recyclerview.widget.LinearLayoutManager
+import kotlinx.android.synthetic.main.fragment_add_ons_management.*
+import kotlinx.android.synthetic.main.fragment_add_ons_management.view.*
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.AddonManagerException
+import mozilla.components.feature.addons.ui.AddonsManagerAdapter
+import mozilla.components.feature.addons.ui.AddonsManagerAdapterDelegate
+import mozilla.components.feature.addons.ui.PermissionsDialogFragment
+import mozilla.components.feature.addons.ui.translatedName
+import org.mozilla.fenix.R
+import org.mozilla.fenix.ext.components
+import org.mozilla.fenix.ext.showToolbar
+
+/**
+ * Fragment use for managing add-ons.
+ */
+@Suppress("TooManyFunctions")
+class AddonsManagementFragment : Fragment(), AddonsManagerAdapterDelegate {
+ private val scope = CoroutineScope(Dispatchers.IO)
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ return inflater.inflate(R.layout.fragment_add_ons_management, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ bindRecyclerView(view)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ showToolbar(getString(R.string.preferences_addons))
+ }
+
+ override fun onStart() {
+ super.onStart()
+ findPreviousDialogFragment()?.let { dialog ->
+ dialog.onPositiveButtonClicked = onPositiveButtonClicked
+ }
+ }
+
+ override fun onAddonItemClicked(addon: Addon) {
+ if (addon.isInstalled()) {
+ showInstalledAddonDetailsFragment(addon)
+ } else {
+ showDetailsFragment(addon)
+ }
+ }
+
+ override fun onInstallAddonButtonClicked(addon: Addon) {
+ showPermissionDialog(addon)
+ }
+
+ override fun onNotYetSupportedSectionClicked(unsupportedAddons: ArrayList) {
+ showNotYetSupportedAddonFragment(unsupportedAddons)
+ }
+
+ private fun bindRecyclerView(view: View) {
+ val recyclerView = view.add_ons_list
+ recyclerView.layoutManager = LinearLayoutManager(requireContext())
+ scope.launch {
+ try {
+ val addons = requireContext().components.addonManager.getAddons()
+
+ scope.launch(Dispatchers.Main) {
+ val adapter = AddonsManagerAdapter(
+ requireContext().components.addonCollectionProvider,
+ this@AddonsManagementFragment,
+ addons
+ )
+ recyclerView.adapter = adapter
+ }
+ } catch (e: AddonManagerException) {
+ scope.launch(Dispatchers.Main) {
+ showSnackBar(view, getString(R.string.mozac_feature_addons_failed_to_query_add_ons))
+ }
+ }
+ }
+ }
+
+ private fun showInstalledAddonDetailsFragment(addon: Addon) {
+ val directions =
+ AddonsManagementFragmentDirections.actionAddonsManagementFragmentToInstalledAddonDetails(
+ addon
+ )
+ Navigation.findNavController(requireView()).navigate(directions)
+ }
+
+ private fun showDetailsFragment(addon: Addon) {
+ val directions =
+ AddonsManagementFragmentDirections.actionAddonsManagementFragmentToAddonDetailsFragment(
+ addon
+ )
+ Navigation.findNavController(requireView()).navigate(directions)
+ }
+
+ private fun showNotYetSupportedAddonFragment(unsupportedAddons: ArrayList) {
+ val directions =
+ AddonsManagementFragmentDirections.actionAddonsManagementFragmentToNotYetSupportedAddonFragment(
+ unsupportedAddons.toTypedArray()
+ )
+ Navigation.findNavController(requireView()).navigate(directions)
+ }
+
+ private fun findPreviousDialogFragment(): PermissionsDialogFragment? {
+ return parentFragmentManager.findFragmentByTag(PERMISSIONS_DIALOG_FRAGMENT_TAG) as? PermissionsDialogFragment
+ }
+
+ private fun hasExistingPermissionDialogFragment(): Boolean {
+ return findPreviousDialogFragment() != null
+ }
+
+ private fun showPermissionDialog(addon: Addon) {
+ if (!hasExistingPermissionDialogFragment()) {
+ val dialog = PermissionsDialogFragment.newInstance(
+ addon = addon,
+ onPositiveButtonClicked = onPositiveButtonClicked
+ )
+ dialog.show(parentFragmentManager, PERMISSIONS_DIALOG_FRAGMENT_TAG)
+ }
+ }
+
+ private val onPositiveButtonClicked: ((Addon) -> Unit) = { addon ->
+ addonProgressOverlay.visibility = View.VISIBLE
+ requireContext().components.addonManager.installAddon(
+ addon,
+ onSuccess = {
+ this@AddonsManagementFragment.view?.let { view ->
+ showSnackBar(
+ view,
+ getString(
+ R.string.mozac_feature_addons_successfully_installed,
+ it.translatedName
+ )
+ )
+ bindRecyclerView(view)
+ }
+
+ addonProgressOverlay?.visibility = View.GONE
+ },
+ onError = { _, _ ->
+ this@AddonsManagementFragment.view?.let { view ->
+ showSnackBar(view, getString(R.string.mozac_feature_addons_failed_to_install, addon.translatedName))
+ }
+ addonProgressOverlay?.visibility = View.GONE
+ }
+ )
+ }
+
+ companion object {
+ private const val PERMISSIONS_DIALOG_FRAGMENT_TAG = "ADDONS_PERMISSIONS_DIALOG_FRAGMENT"
+ }
+}
diff --git a/app/src/main/java/org/mozilla/fenix/addons/Extensions.kt b/app/src/main/java/org/mozilla/fenix/addons/Extensions.kt
new file mode 100644
index 000000000..306a6d29e
--- /dev/null
+++ b/app/src/main/java/org/mozilla/fenix/addons/Extensions.kt
@@ -0,0 +1,31 @@
+/* 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.addons
+
+import android.view.View
+import org.mozilla.fenix.components.FenixSnackbar
+import java.text.NumberFormat
+import java.util.Locale
+
+/**
+ * Get the formatted number amount for the current default locale.
+ *
+ * @param amount The number of addons to be formatted for the current default locale..
+ */
+internal fun getFormattedAmount(amount: Int): String {
+ return NumberFormat.getNumberInstance(Locale.getDefault()).format(amount)
+}
+
+/**
+ * Shows the Fenix Snackbar in the given view along with the provided text.
+ *
+ * @param view A [View] used to determine a parent for the [FenixSnackbar].
+ * @param text The text to display in the [FenixSnackbar].
+ */
+internal fun showSnackBar(view: View, text: String) {
+ FenixSnackbar.make(view, FenixSnackbar.LENGTH_SHORT)
+ .setText(text)
+ .show()
+}
diff --git a/app/src/main/java/org/mozilla/fenix/addons/InstalledAddonDetailsFragment.kt b/app/src/main/java/org/mozilla/fenix/addons/InstalledAddonDetailsFragment.kt
new file mode 100644
index 000000000..e8dde6b9f
--- /dev/null
+++ b/app/src/main/java/org/mozilla/fenix/addons/InstalledAddonDetailsFragment.kt
@@ -0,0 +1,167 @@
+/* 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.addons
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Switch
+import androidx.fragment.app.Fragment
+import androidx.navigation.Navigation
+import androidx.navigation.findNavController
+import kotlinx.android.synthetic.main.fragment_installed_add_on_details.view.*
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.ui.translate
+import org.mozilla.fenix.R
+import org.mozilla.fenix.ext.components
+import org.mozilla.fenix.ext.showToolbar
+import mozilla.components.feature.addons.ui.translatedName
+
+/**
+ * An activity to show the details of a installed add-on.
+ */
+class InstalledAddonDetailsFragment : Fragment() {
+ private lateinit var addon: Addon
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ if (!::addon.isInitialized) {
+ addon = AddonDetailsFragmentArgs.fromBundle(requireNotNull(arguments)).addon
+ }
+
+ return inflater.inflate(R.layout.fragment_installed_add_on_details, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ bind(view)
+ }
+
+ private fun bind(view: View) {
+ val title = addon.translatableName.translate()
+ showToolbar(title)
+
+ bindEnableSwitch(view)
+ bindSettings(view)
+ bindDetails(view)
+ bindPermissions(view)
+ bindRemoveButton(view)
+ }
+
+ private fun bindEnableSwitch(view: View) {
+ val switch = view.enable_switch
+ switch.setState(addon.isEnabled())
+ switch.setOnCheckedChangeListener { _, isChecked ->
+ if (isChecked) {
+ requireContext().components.addonManager.enableAddon(
+ addon,
+ onSuccess = {
+ switch.setState(true)
+ this.addon = it
+ showSnackBar(
+ view,
+ getString(R.string.mozac_feature_addons_successfully_enabled, addon.translatedName)
+ )
+ },
+ onError = {
+ showSnackBar(
+ view,
+ getString(R.string.mozac_feature_addons_failed_to_enable, addon.translatedName)
+ )
+ }
+ )
+ } else {
+ requireContext().components.addonManager.disableAddon(
+ addon,
+ onSuccess = {
+ switch.setState(false)
+ this.addon = it
+ showSnackBar(
+ view,
+ getString(R.string.mozac_feature_addons_successfully_disabled, addon.translatedName)
+ )
+ },
+ onError = {
+ showSnackBar(
+ view,
+ getString(R.string.mozac_feature_addons_failed_to_disable, addon.translatedName)
+ )
+ }
+ )
+ }
+ }
+ }
+
+ private fun bindSettings(view: View) {
+ view.settings.apply {
+ isEnabled = addon.installedState?.optionsPageUrl != null
+ setOnClickListener {
+ val directions =
+ InstalledAddonDetailsFragmentDirections.actionInstalledAddonFragmentToAddonInternalSettingsFragment(
+ addon
+ )
+ Navigation.findNavController(this).navigate(directions)
+ }
+ }
+ }
+
+ private fun bindDetails(view: View) {
+ view.details.setOnClickListener {
+ val directions =
+ InstalledAddonDetailsFragmentDirections.actionInstalledAddonFragmentToAddonDetailsFragment(
+ addon
+ )
+ Navigation.findNavController(view).navigate(directions)
+ }
+ }
+
+ private fun bindPermissions(view: View) {
+ view.permissions.setOnClickListener {
+ val directions =
+ InstalledAddonDetailsFragmentDirections.actionInstalledAddonFragmentToAddonPermissionsDetailsFragment(
+ addon
+ )
+ Navigation.findNavController(view).navigate(directions)
+ }
+ }
+
+ private fun bindRemoveButton(view: View) {
+ view.remove_add_on.setOnClickListener {
+ requireContext().components.addonManager.uninstallAddon(
+ addon,
+ onSuccess = {
+ showSnackBar(
+ view,
+ getString(R.string.mozac_feature_addons_successfully_uninstalled, addon.translatedName)
+ )
+ view.findNavController().popBackStack()
+ },
+ onError = { _, _ ->
+ showSnackBar(
+ view,
+ getString(
+ R.string.mozac_feature_addons_failed_to_uninstall,
+ addon.translatedName
+ )
+ )
+ }
+ )
+ }
+ }
+
+ private fun Switch.setState(checked: Boolean) {
+ val text = if (checked) {
+ R.string.mozac_feature_addons_settings_on
+ } else {
+ R.string.mozac_feature_addons_settings_off
+ }
+ setText(text)
+ isChecked = checked
+ }
+}
diff --git a/app/src/main/java/org/mozilla/fenix/addons/NotYetSupportedAddonFragment.kt b/app/src/main/java/org/mozilla/fenix/addons/NotYetSupportedAddonFragment.kt
new file mode 100644
index 000000000..4b95308b3
--- /dev/null
+++ b/app/src/main/java/org/mozilla/fenix/addons/NotYetSupportedAddonFragment.kt
@@ -0,0 +1,76 @@
+/* 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.addons
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.recyclerview.widget.LinearLayoutManager
+import kotlinx.android.synthetic.main.fragment_not_yet_supported_addons.view.*
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.ui.UnsupportedAddonsAdapter
+import mozilla.components.feature.addons.ui.UnsupportedAddonsAdapterDelegate
+import org.mozilla.fenix.R
+import org.mozilla.fenix.ext.components
+import org.mozilla.fenix.ext.showToolbar
+
+private const val LEARN_MORE_URL =
+ "https://support.mozilla.org/kb/add-compatibility-firefox-preview"
+
+/**
+ * Fragment for displaying and managing add-ons that are not yet supported by the browser.
+ */
+class NotYetSupportedAddonFragment : Fragment(), UnsupportedAddonsAdapterDelegate {
+ private val addons: List by lazy {
+ NotYetSupportedAddonFragmentArgs.fromBundle(requireNotNull(arguments)).addons.toList()
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_not_yet_supported_addons, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ view.unsupported_add_ons_list.apply {
+ layoutManager = LinearLayoutManager(requireContext())
+ adapter = UnsupportedAddonsAdapter(
+ addonManager = requireContext().components.addonManager,
+ unsupportedAddonsAdapterDelegate = this@NotYetSupportedAddonFragment,
+ unsupportedAddons = addons
+ )
+ }
+
+ view.learn_more_label.setOnClickListener {
+ val intent = Intent(Intent.ACTION_VIEW).setData(Uri.parse(LEARN_MORE_URL))
+ startActivity(intent)
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ showToolbar(getString(R.string.mozac_feature_addons_unsupported_section))
+ }
+
+ override fun onUninstallError(addonId: String, throwable: Throwable) {
+ this@NotYetSupportedAddonFragment.view?.let { view ->
+ showSnackBar(view, getString(R.string.mozac_feature_addons_failed_to_remove, ""))
+ }
+ }
+
+ override fun onUninstallSuccess() {
+ this@NotYetSupportedAddonFragment.view?.let { view ->
+ showSnackBar(view, getString(R.string.mozac_feature_addons_successfully_removed, ""))
+ }
+ }
+}
diff --git a/app/src/main/java/org/mozilla/fenix/addons/WebExtensionActionPopupFragment.kt b/app/src/main/java/org/mozilla/fenix/addons/WebExtensionActionPopupFragment.kt
new file mode 100644
index 000000000..33b2af952
--- /dev/null
+++ b/app/src/main/java/org/mozilla/fenix/addons/WebExtensionActionPopupFragment.kt
@@ -0,0 +1,101 @@
+/* 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.addons
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import kotlinx.android.synthetic.main.fragment_add_on_internal_settings.*
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.browser.state.action.WebExtensionAction
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.concept.engine.window.WindowRequest
+import mozilla.components.lib.state.ext.consumeFrom
+import org.mozilla.fenix.R
+import org.mozilla.fenix.ext.requireComponents
+import org.mozilla.fenix.ext.showToolbar
+
+/**
+ * A fragment to show the web extension action popup with [EngineView].
+ */
+class WebExtensionActionPopupFragment : Fragment(), EngineSession.Observer {
+ private val webExtensionTitle: String? by lazy {
+ WebExtensionActionPopupFragmentArgs.fromBundle(requireNotNull(arguments)).webExtensionTitle
+ }
+ private val webExtensionId: String by lazy {
+ WebExtensionActionPopupFragmentArgs.fromBundle(requireNotNull(arguments)).webExtensionId
+ }
+ private var engineSession: EngineSession? = null
+ private val coreComponents by lazy { requireComponents.core }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ // Grab the [EngineSession] from the store when the view is created if it is available.
+ if (engineSession == null) {
+ engineSession = coreComponents.store.state.extensions[webExtensionId]?.popupSession
+ }
+
+ return inflater.inflate(R.layout.fragment_add_on_internal_settings, container, false)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ val title = webExtensionTitle ?: webExtensionId
+ showToolbar(title)
+ }
+
+ override fun onStart() {
+ super.onStart()
+ engineSession?.register(this)
+ }
+
+ override fun onStop() {
+ super.onStop()
+ engineSession?.unregister(this)
+ }
+
+ override fun onWindowRequest(windowRequest: WindowRequest) {
+ if (windowRequest.type == WindowRequest.Type.CLOSE) {
+ activity?.onBackPressed()
+ }
+ }
+
+ @ExperimentalCoroutinesApi
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ val session = engineSession
+ // If we have the session, render it otherwise consume it from the store.
+ if (session != null) {
+ addonSettingsEngineView.render(session)
+ consumePopupSession()
+ } else {
+ consumeFrom(coreComponents.store) { state ->
+ state.extensions[webExtensionId]?.let { extState ->
+ extState.popupSession?.let {
+ if (engineSession == null) {
+ addonSettingsEngineView.render(it)
+ it.register(this)
+ consumePopupSession()
+ engineSession = it
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun consumePopupSession() {
+ coreComponents.store.dispatch(
+ WebExtensionAction.UpdatePopupSessionAction(webExtensionId, popupSession = null)
+ )
+ }
+}
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 93f2edcc0..2294d2689 100644
--- a/app/src/main/java/org/mozilla/fenix/components/Components.kt
+++ b/app/src/main/java/org/mozilla/fenix/components/Components.kt
@@ -5,10 +5,18 @@
package org.mozilla.fenix.components
import android.content.Context
+import mozilla.components.feature.addons.AddonManager
+import mozilla.components.feature.addons.amo.AddonCollectionProvider
+import mozilla.components.feature.addons.update.AddonUpdater
+import mozilla.components.feature.addons.update.DefaultAddonUpdater
+import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
import mozilla.components.support.migration.state.MigrationStore
import org.mozilla.fenix.test.Mockable
import org.mozilla.fenix.utils.ClipboardHandler
+import java.util.concurrent.TimeUnit
+
+private const val DAY_IN_MINUTES = 24 * 60L
/**
* Provides access to all components.
@@ -49,6 +57,24 @@ class Components(private val context: Context) {
migrationStore
)
}
+
+ /**
+ * Add-on
+ */
+ val addonCollectionProvider by lazy {
+ AddonCollectionProvider(context, core.client, maxCacheAgeInMinutes = DAY_IN_MINUTES)
+ }
+
+ val addonUpdater by lazy {
+ DefaultAddonUpdater(context, AddonUpdater.Frequency(1, TimeUnit.DAYS))
+ }
+
+ val addonManager by lazy {
+ AddonManager(core.store, core.engine, addonCollectionProvider, addonUpdater)
+ }
+
+ val tabsUseCases: TabsUseCases by lazy { TabsUseCases(core.sessionManager) }
+
val analytics by lazy { Analytics(context) }
val publicSuffixList by lazy { PublicSuffixList(context) }
val clipboardHandler by lazy { ClipboardHandler(context) }
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 024b88abf..65489e4de 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
@@ -353,7 +353,7 @@ sealed class Event {
SETTINGS, LIBRARY, HELP, DESKTOP_VIEW_ON, DESKTOP_VIEW_OFF, FIND_IN_PAGE, NEW_TAB,
NEW_PRIVATE_TAB, SHARE, REPORT_SITE_ISSUE, BACK, FORWARD, RELOAD, STOP, OPEN_IN_FENIX,
SAVE_TO_COLLECTION, ADD_TO_FIREFOX_HOME, ADD_TO_HOMESCREEN, QUIT, READER_MODE_ON,
- READER_MODE_OFF, OPEN_IN_APP, BOOKMARK, READER_MODE_APPEARANCE
+ READER_MODE_OFF, OPEN_IN_APP, BOOKMARK, READER_MODE_APPEARANCE, ADDONS_MANAGER
}
override val extras: Map?
diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt
index 77ad05f06..1775ebfa5 100644
--- a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt
+++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt
@@ -207,6 +207,13 @@ class DefaultBrowserToolbarController(
ToolbarMenu.Item.Help -> {
activity.components.useCases.tabsUseCases.addTab.invoke(getSupportUrl())
}
+ ToolbarMenu.Item.AddonsManager -> {
+ navController.nav(
+ R.id.browserFragment,
+ BrowserFragmentDirections
+ .actionBrowserFragmentToAddonsManagementFragment()
+ )
+ }
ToolbarMenu.Item.SaveToCollection -> {
activity.components.analytics.metrics
.track(Event.CollectionSaveButtonPressed(TELEMETRY_BROWSER_IDENTIFIER))
@@ -340,6 +347,7 @@ class DefaultBrowserToolbarController(
Event.BrowserMenuItemTapped.Item.READER_MODE_APPEARANCE
ToolbarMenu.Item.OpenInApp -> Event.BrowserMenuItemTapped.Item.OPEN_IN_APP
ToolbarMenu.Item.Bookmark -> Event.BrowserMenuItemTapped.Item.BOOKMARK
+ ToolbarMenu.Item.AddonsManager -> Event.BrowserMenuItemTapped.Item.ADDONS_MANAGER
}
activity.components.analytics.metrics.track(Event.BrowserMenuItemTapped(eventItem))
diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt
index ea28de186..3a016c41f 100644
--- a/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt
+++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt
@@ -10,8 +10,8 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
-import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.menu.BrowserMenuHighlight
+import mozilla.components.browser.menu.WebExtensionBrowserMenuBuilder
import mozilla.components.browser.menu.item.BrowserMenuDivider
import mozilla.components.browser.menu.item.BrowserMenuHighlightableItem
import mozilla.components.browser.menu.item.BrowserMenuHighlightableSwitch
@@ -45,7 +45,14 @@ class DefaultToolbarMenu(
private var currentUrlIsBookmarked = false
private var isBookmarkedJob: Job? = null
- override val menuBuilder by lazy { BrowserMenuBuilder(menuItems, endOfMenuAlwaysVisible = true) }
+ override val menuBuilder by lazy {
+ WebExtensionBrowserMenuBuilder(
+ menuItems,
+ endOfMenuAlwaysVisible = true,
+ store = context.components.core.store,
+ appendExtensionActionAtStart = true
+ )
+ }
override val menuToolbar by lazy {
val forward = BrowserMenuItemToolbar.TwoStateButton(
@@ -157,6 +164,7 @@ class DefaultToolbarMenu(
desktopMode,
addToFirefoxHome,
addToHomescreen.apply { visible = ::shouldShowAddToHomescreen },
+ addons,
findInPage,
privateTab,
newTab,
@@ -173,6 +181,14 @@ class DefaultToolbarMenu(
if (shouldReverseItems) { menuItems.reversed() } else { menuItems }
}
+ private val addons = BrowserMenuImageText(
+ context.getString(R.string.browser_menu_addon_manager),
+ R.drawable.mozac_ic_extensions,
+ primaryTextColor()
+ ) {
+ onItemTapped.invoke(ToolbarMenu.Item.AddonsManager)
+ }
+
private val help = BrowserMenuImageText(
context.getString(R.string.browser_menu_help),
R.drawable.ic_help,
diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt
index ced4a10ce..66325eebc 100644
--- a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt
+++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt
@@ -26,6 +26,7 @@ interface ToolbarMenu {
object SaveToCollection : Item()
object AddToFirefoxHome : Item()
object AddToHomeScreen : Item()
+ object AddonsManager : Item()
object Quit : Item()
data class ReaderMode(val isChecked: Boolean) : Item()
object OpenInApp : Item()
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 a247e0b2b..9fc970eef 100644
--- a/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt
+++ b/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt
@@ -51,6 +51,7 @@ import org.mozilla.fenix.R.string.pref_key_theme
import org.mozilla.fenix.R.string.pref_key_toolbar
import org.mozilla.fenix.R.string.pref_key_tracking_protection_settings
import org.mozilla.fenix.R.string.pref_key_your_rights
+import org.mozilla.fenix.R.string.pref_key_addons
import org.mozilla.fenix.components.PrivateShortcutCreateManager
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.application
@@ -185,7 +186,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
findPreference(getPreferenceKey(pref_key_passwords))?.apply {
isVisible = FeatureFlags.logins
}
- findPreference(getPreferenceKey(R.string.pref_key_advanced))?.apply {
+ findPreference(getPreferenceKey(pref_key_language))?.apply {
isVisible = FeatureFlags.fenixLanguagePicker
}
}
@@ -214,6 +215,10 @@ class SettingsFragment : PreferenceFragmentCompat() {
resources.getString(pref_key_language) -> {
SettingsFragmentDirections.actionSettingsFragmentToLocaleSettingsFragment()
}
+ resources.getString(pref_key_addons) -> {
+ SettingsFragmentDirections.actionSettingsFragmentToAddonsFragment()
+ }
+
resources.getString(pref_key_make_default_browser) -> {
SettingsFragmentDirections.actionSettingsFragmentToDefaultBrowserSettingsFragment()
}
diff --git a/app/src/main/res/drawable/addon_textview_selector.xml b/app/src/main/res/drawable/addon_textview_selector.xml
new file mode 100644
index 000000000..9caeefd35
--- /dev/null
+++ b/app/src/main/res/drawable/addon_textview_selector.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/mozac_ic_extensions_black.xml b/app/src/main/res/drawable/mozac_ic_extensions_black.xml
new file mode 100644
index 000000000..097a4a094
--- /dev/null
+++ b/app/src/main/res/drawable/mozac_ic_extensions_black.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/mozac_ic_permissions.xml b/app/src/main/res/drawable/mozac_ic_permissions.xml
new file mode 100644
index 000000000..6817c3819
--- /dev/null
+++ b/app/src/main/res/drawable/mozac_ic_permissions.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_addons.xml b/app/src/main/res/layout/activity_addons.xml
new file mode 100644
index 000000000..e8b4ef833
--- /dev/null
+++ b/app/src/main/res/layout/activity_addons.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/layout/fragment_add_on_details.xml b/app/src/main/res/layout/fragment_add_on_details.xml
new file mode 100644
index 000000000..d4a39db92
--- /dev/null
+++ b/app/src/main/res/layout/fragment_add_on_details.xml
@@ -0,0 +1,154 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_add_on_internal_settings.xml b/app/src/main/res/layout/fragment_add_on_internal_settings.xml
new file mode 100644
index 000000000..0479ab618
--- /dev/null
+++ b/app/src/main/res/layout/fragment_add_on_internal_settings.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_add_on_permissions.xml b/app/src/main/res/layout/fragment_add_on_permissions.xml
new file mode 100644
index 000000000..70f72538c
--- /dev/null
+++ b/app/src/main/res/layout/fragment_add_on_permissions.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_add_ons_management.xml b/app/src/main/res/layout/fragment_add_ons_management.xml
new file mode 100644
index 000000000..a7cd5a6b1
--- /dev/null
+++ b/app/src/main/res/layout/fragment_add_ons_management.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_installed_add_on_details.xml b/app/src/main/res/layout/fragment_installed_add_on_details.xml
new file mode 100644
index 000000000..57f9055dd
--- /dev/null
+++ b/app/src/main/res/layout/fragment_installed_add_on_details.xml
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_not_yet_supported_addons.xml b/app/src/main/res/layout/fragment_not_yet_supported_addons.xml
new file mode 100644
index 000000000..5154d8683
--- /dev/null
+++ b/app/src/main/res/layout/fragment_not_yet_supported_addons.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/overlay_add_on_progress.xml b/app/src/main/res/layout/overlay_add_on_progress.xml
new file mode 100644
index 000000000..1f0d17c8b
--- /dev/null
+++ b/app/src/main/res/layout/overlay_add_on_progress.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml
index 0fb506505..fd65edd23 100644
--- a/app/src/main/res/navigation/nav_graph.xml
+++ b/app/src/main/res/navigation/nav_graph.xml
@@ -51,6 +51,10 @@
android:id="@+id/action_global_homeFragment"
app:destination="@id/homeFragment" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/preference_keys.xml b/app/src/main/res/values/preference_keys.xml
index f4f73d9b1..c41013781 100644
--- a/app/src/main/res/values/preference_keys.xml
+++ b/app/src/main/res/values/preference_keys.xml
@@ -28,8 +28,7 @@
pref_key_delete_permissions_on_quitpref_key_delete_browsing_data_on_quit_categoriespref_key_last_known_mode_private
-
-
+ pref_key_addonspref_key_last_maintenancepref_key_helppref_key_rate
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 7688faf4f..406e2730a 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -65,6 +65,8 @@
Edit bookmark
+ Add-ons Manager
+
HelpWhat’s New
@@ -242,6 +244,8 @@
Account settingsOpen links in apps
+
+ Add-ons
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index 349243d95..ac0d5d61b 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -104,12 +104,16 @@
+ android:key="@string/pref_key_advanced">
+
+ android:title="@string/preferences_language"
+ app:isPreferenceVisible="false" />
error_type: The error type of the error page encountered
|2020-09-01 |
| events.app_opened |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user opened the app |[1](https://github.com/mozilla-mobile/fenix/pull/1067#issuecomment-474598673)|
source: The method used to open Fenix. Possible values are: `app_icon`, `custom_tab` or `link`
|2020-09-01 |
-| events.browser_menu_action |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A browser menu item was tapped |[1](https://github.com/mozilla-mobile/fenix/pull/1214#issue-264756708), [2](https://github.com/mozilla-mobile/fenix/pull/5098#issuecomment-529658996), [3](https://github.com/mozilla-mobile/fenix/pull/6310)|
item: A string containing the name of the item the user tapped. These items include: Settings, Library, Help, Desktop Site toggle on/off, Find in Page, New Tab, Private Tab, Share, Report Site Issue, Back/Forward button, Reload Button, Quit, Reader Mode On, Reader Mode Off, Open In App, Add to Firefox Home
|2020-09-01 |
+| events.browser_menu_action |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A browser menu item was tapped |[1](https://github.com/mozilla-mobile/fenix/pull/1214#issue-264756708), [2](https://github.com/mozilla-mobile/fenix/pull/5098#issuecomment-529658996), [3](https://github.com/mozilla-mobile/fenix/pull/6310)|
item: A string containing the name of the item the user tapped. These items include: Settings, Library, Help, Desktop Site toggle on/off, Find in Page, New Tab, Private Tab, Share, Report Site Issue, Back/Forward button, Reload Button, Quit, Reader Mode On, Reader Mode Off, Open In App, Add to Firefox Home, Add-ons Manager
|2020-09-01 |
| events.entered_url |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user entered a url |[1](https://github.com/mozilla-mobile/fenix/pull/1067#issuecomment-474598673)|
autocomplete: A boolean that tells us whether the URL was autofilled by an Autocomplete suggestion
|2020-09-01 |
| events.opened_link |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user opened a link with Fenix |[1](https://github.com/mozilla-mobile/fenix/pull/5975)|
mode: The mode the link was opened in. Either 'PRIVATE' or 'NORMAL'
|2020-09-01 |
| events.performed_search |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user performed a search |[1](https://github.com/mozilla-mobile/fenix/pull/1067#issuecomment-474598673), [2](https://github.com/mozilla-mobile/fenix/pull/1677)|
source: A string that tells us how the user performed the search. Possible values are: * default.action * default.suggestion * shortcut.action * shortcut.suggestion