Provide add-on support (#8064)

Closes #5630, #6069, #6092, #6091, #6124, and #6147.

Co-authored-by: Simon Chae <chaesmn@gmail.com>
Co-authored-by: Arturo Mejia <arturomejiamarmol@gmail.com>
Co-authored-by: Christian Sadilek <christian.sadilek@gmail.com>
Co-authored-by: Gabriel Luong <gabriel.luong@gmail.com>
fennec/nightly
Gabriel Luong 4 years ago committed by GitHub
parent 4eb71ce235
commit 64a4a7f422
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -396,6 +396,9 @@ dependencies {
implementation Deps.mozilla_browser_storage_sync implementation Deps.mozilla_browser_storage_sync
implementation Deps.mozilla_browser_toolbar implementation Deps.mozilla_browser_toolbar
implementation Deps.mozilla_support_extensions
implementation Deps.mozilla_feature_addons
implementation Deps.mozilla_feature_accounts implementation Deps.mozilla_feature_accounts
implementation Deps.mozilla_feature_app_links implementation Deps.mozilla_feature_app_links
implementation Deps.mozilla_feature_awesomebar implementation Deps.mozilla_feature_awesomebar

@ -82,7 +82,7 @@ events:
A string containing the name of the item the user tapped. These items include: 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, 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, 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: bugs:
- https://github.com/mozilla-mobile/fenix/issues/1024 - https://github.com/mozilla-mobile/fenix/issues/1024
data_reviews: data_reviews:

@ -18,6 +18,7 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import mozilla.appservices.Megazord import mozilla.appservices.Megazord
import mozilla.components.browser.session.Session
import mozilla.components.concept.push.PushProcessor import mozilla.components.concept.push.PushProcessor
import mozilla.components.service.experiments.Experiments import mozilla.components.service.experiments.Experiments
import mozilla.components.service.glean.Glean 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.locale.LocaleAwareApplication
import mozilla.components.support.rusthttp.RustHttpConfig import mozilla.components.support.rusthttp.RustHttpConfig
import mozilla.components.support.rustlog.RustLog import mozilla.components.support.rustlog.RustLog
import mozilla.components.support.webextensions.WebExtensionSupport
import org.mozilla.fenix.components.Components import org.mozilla.fenix.components.Components
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.session.NotificationSessionObserver import org.mozilla.fenix.session.NotificationSessionObserver
@ -116,6 +118,8 @@ open class FenixApplication : LocaleAwareApplication() {
// Make sure the engine is initialized and ready to use. // Make sure the engine is initialized and ready to use.
components.core.engine.warmUp() components.core.engine.warmUp()
initializeWebExtensionSupport()
// Just to make sure it is impossible for any application-services pieces // Just to make sure it is impossible for any application-services pieces
// to invoke parts of itself that require complete megazord initialization // to invoke parts of itself that require complete megazord initialization
// before that process completes, we wait here, if necessary. // before that process completes, we wait here, if necessary.
@ -277,4 +281,29 @@ open class FenixApplication : LocaleAwareApplication() {
StrictMode.setVmPolicy(builder.build()) 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)
}
}
} }

@ -23,10 +23,12 @@ import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.NavigationUI import androidx.navigation.ui.NavigationUI
import kotlinx.android.synthetic.main.activity_home.navigationToolbarStub import kotlinx.android.synthetic.main.activity_home.navigationToolbarStub
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.browser.search.SearchEngine import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.state.WebExtensionState
import mozilla.components.concept.engine.EngineView import mozilla.components.concept.engine.EngineView
import mozilla.components.service.fxa.sync.SyncReason import mozilla.components.service.fxa.sync.SyncReason
import mozilla.components.support.base.feature.UserInteractionHandler 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.locale.LocaleAwareAppCompatActivity
import mozilla.components.support.utils.SafeIntent import mozilla.components.support.utils.SafeIntent
import mozilla.components.support.utils.toSafeIntent import mozilla.components.support.utils.toSafeIntent
import mozilla.components.support.webextensions.WebExtensionPopupFeature
import org.mozilla.fenix.browser.UriOpenedObserver import org.mozilla.fenix.browser.UriOpenedObserver
import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
@ -70,6 +73,7 @@ import org.mozilla.fenix.utils.BrowsersCache
@SuppressWarnings("TooManyFunctions", "LargeClass") @SuppressWarnings("TooManyFunctions", "LargeClass")
open class HomeActivity : LocaleAwareAppCompatActivity() { open class HomeActivity : LocaleAwareAppCompatActivity() {
private var webExtScope: CoroutineScope? = null
lateinit var themeManager: ThemeManager lateinit var themeManager: ThemeManager
lateinit var browsingModeManager: BrowsingModeManager lateinit var browsingModeManager: BrowsingModeManager
@ -79,6 +83,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity() {
private var isToolbarInflated = false private var isToolbarInflated = false
private val webExtensionPopupFeature by lazy {
WebExtensionPopupFeature(components.core.store, ::openPopup)
}
private val navHost by lazy { private val navHost by lazy {
supportFragmentManager.findFragmentById(R.id.container) as NavHostFragment supportFragmentManager.findFragmentById(R.id.container) as NavHostFragment
} }
@ -126,6 +134,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity() {
?.also { components.analytics.metrics.track(Event.OpenedApp(it)) } ?.also { components.analytics.metrics.track(Event.OpenedApp(it)) }
} }
supportActionBar?.hide() supportActionBar?.hide()
lifecycle.addObserver(webExtensionPopupFeature)
} }
@CallSuper @CallSuper
@ -377,6 +387,14 @@ open class HomeActivity : LocaleAwareAppCompatActivity() {
return DefaultThemeManager(browsingModeManager.mode, this) 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 { companion object {
const val OPEN_TO_BROWSER = "open_to_browser" const val OPEN_TO_BROWSER = "open_to_browser"
const val OPEN_TO_BROWSER_AND_LOAD = "open_to_browser_and_load" const val OPEN_TO_BROWSER_AND_LOAD = "open_to_browser_and_load"

@ -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", "<br/>")
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)!!)
}
}

@ -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()
}
}

@ -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)
}
}

@ -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<Addon>) {
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<Addon>) {
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"
}
}

@ -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()
}

@ -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
}
}

@ -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<Addon> 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, ""))
}
}
}

@ -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)
)
}
}

@ -5,10 +5,18 @@
package org.mozilla.fenix.components package org.mozilla.fenix.components
import android.content.Context 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.lib.publicsuffixlist.PublicSuffixList
import mozilla.components.support.migration.state.MigrationStore import mozilla.components.support.migration.state.MigrationStore
import org.mozilla.fenix.test.Mockable import org.mozilla.fenix.test.Mockable
import org.mozilla.fenix.utils.ClipboardHandler import org.mozilla.fenix.utils.ClipboardHandler
import java.util.concurrent.TimeUnit
private const val DAY_IN_MINUTES = 24 * 60L
/** /**
* Provides access to all components. * Provides access to all components.
@ -49,6 +57,24 @@ class Components(private val context: Context) {
migrationStore 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 analytics by lazy { Analytics(context) }
val publicSuffixList by lazy { PublicSuffixList(context) } val publicSuffixList by lazy { PublicSuffixList(context) }
val clipboardHandler by lazy { ClipboardHandler(context) } val clipboardHandler by lazy { ClipboardHandler(context) }

@ -353,7 +353,7 @@ sealed class Event {
SETTINGS, LIBRARY, HELP, DESKTOP_VIEW_ON, DESKTOP_VIEW_OFF, FIND_IN_PAGE, NEW_TAB, 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, 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, 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<Events.browserMenuActionKeys, String>? override val extras: Map<Events.browserMenuActionKeys, String>?

@ -207,6 +207,13 @@ class DefaultBrowserToolbarController(
ToolbarMenu.Item.Help -> { ToolbarMenu.Item.Help -> {
activity.components.useCases.tabsUseCases.addTab.invoke(getSupportUrl()) activity.components.useCases.tabsUseCases.addTab.invoke(getSupportUrl())
} }
ToolbarMenu.Item.AddonsManager -> {
navController.nav(
R.id.browserFragment,
BrowserFragmentDirections
.actionBrowserFragmentToAddonsManagementFragment()
)
}
ToolbarMenu.Item.SaveToCollection -> { ToolbarMenu.Item.SaveToCollection -> {
activity.components.analytics.metrics activity.components.analytics.metrics
.track(Event.CollectionSaveButtonPressed(TELEMETRY_BROWSER_IDENTIFIER)) .track(Event.CollectionSaveButtonPressed(TELEMETRY_BROWSER_IDENTIFIER))
@ -340,6 +347,7 @@ class DefaultBrowserToolbarController(
Event.BrowserMenuItemTapped.Item.READER_MODE_APPEARANCE Event.BrowserMenuItemTapped.Item.READER_MODE_APPEARANCE
ToolbarMenu.Item.OpenInApp -> Event.BrowserMenuItemTapped.Item.OPEN_IN_APP ToolbarMenu.Item.OpenInApp -> Event.BrowserMenuItemTapped.Item.OPEN_IN_APP
ToolbarMenu.Item.Bookmark -> Event.BrowserMenuItemTapped.Item.BOOKMARK ToolbarMenu.Item.Bookmark -> Event.BrowserMenuItemTapped.Item.BOOKMARK
ToolbarMenu.Item.AddonsManager -> Event.BrowserMenuItemTapped.Item.ADDONS_MANAGER
} }
activity.components.analytics.metrics.track(Event.BrowserMenuItemTapped(eventItem)) activity.components.analytics.metrics.track(Event.BrowserMenuItemTapped(eventItem))

@ -10,8 +10,8 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.menu.BrowserMenuHighlight 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.BrowserMenuDivider
import mozilla.components.browser.menu.item.BrowserMenuHighlightableItem import mozilla.components.browser.menu.item.BrowserMenuHighlightableItem
import mozilla.components.browser.menu.item.BrowserMenuHighlightableSwitch import mozilla.components.browser.menu.item.BrowserMenuHighlightableSwitch
@ -45,7 +45,14 @@ class DefaultToolbarMenu(
private var currentUrlIsBookmarked = false private var currentUrlIsBookmarked = false
private var isBookmarkedJob: Job? = null 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 { override val menuToolbar by lazy {
val forward = BrowserMenuItemToolbar.TwoStateButton( val forward = BrowserMenuItemToolbar.TwoStateButton(
@ -157,6 +164,7 @@ class DefaultToolbarMenu(
desktopMode, desktopMode,
addToFirefoxHome, addToFirefoxHome,
addToHomescreen.apply { visible = ::shouldShowAddToHomescreen }, addToHomescreen.apply { visible = ::shouldShowAddToHomescreen },
addons,
findInPage, findInPage,
privateTab, privateTab,
newTab, newTab,
@ -173,6 +181,14 @@ class DefaultToolbarMenu(
if (shouldReverseItems) { menuItems.reversed() } else { menuItems } 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( private val help = BrowserMenuImageText(
context.getString(R.string.browser_menu_help), context.getString(R.string.browser_menu_help),
R.drawable.ic_help, R.drawable.ic_help,

@ -26,6 +26,7 @@ interface ToolbarMenu {
object SaveToCollection : Item() object SaveToCollection : Item()
object AddToFirefoxHome : Item() object AddToFirefoxHome : Item()
object AddToHomeScreen : Item() object AddToHomeScreen : Item()
object AddonsManager : Item()
object Quit : Item() object Quit : Item()
data class ReaderMode(val isChecked: Boolean) : Item() data class ReaderMode(val isChecked: Boolean) : Item()
object OpenInApp : Item() object OpenInApp : Item()

@ -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_toolbar
import org.mozilla.fenix.R.string.pref_key_tracking_protection_settings 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_your_rights
import org.mozilla.fenix.R.string.pref_key_addons
import org.mozilla.fenix.components.PrivateShortcutCreateManager import org.mozilla.fenix.components.PrivateShortcutCreateManager
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.application import org.mozilla.fenix.ext.application
@ -185,7 +186,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
findPreference<Preference>(getPreferenceKey(pref_key_passwords))?.apply { findPreference<Preference>(getPreferenceKey(pref_key_passwords))?.apply {
isVisible = FeatureFlags.logins isVisible = FeatureFlags.logins
} }
findPreference<PreferenceCategory>(getPreferenceKey(R.string.pref_key_advanced))?.apply { findPreference<Preference>(getPreferenceKey(pref_key_language))?.apply {
isVisible = FeatureFlags.fenixLanguagePicker isVisible = FeatureFlags.fenixLanguagePicker
} }
} }
@ -214,6 +215,10 @@ class SettingsFragment : PreferenceFragmentCompat() {
resources.getString(pref_key_language) -> { resources.getString(pref_key_language) -> {
SettingsFragmentDirections.actionSettingsFragmentToLocaleSettingsFragment() SettingsFragmentDirections.actionSettingsFragmentToLocaleSettingsFragment()
} }
resources.getString(pref_key_addons) -> {
SettingsFragmentDirections.actionSettingsFragmentToAddonsFragment()
}
resources.getString(pref_key_make_default_browser) -> { resources.getString(pref_key_make_default_browser) -> {
SettingsFragmentDirections.actionSettingsFragmentToDefaultBrowserSettingsFragment() SettingsFragmentDirections.actionSettingsFragmentToDefaultBrowserSettingsFragment()
} }

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="true" android:color="?android:attr/textColorPrimary" />
<item android:state_checked="false" android:color="@color/photonGrey40" />
</selector>

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:fillColor="?android:attr/textColorPrimary"
android:pathData="M14.5,8c-0.971,0 -1,1 -1.75,1a0.765,0.765 0,0 1,-0.75 -0.75V5a1,1 0,0 0,-1 -1H7.75A0.765,0.765 0,0 1,7 3.25c0,-0.75 1,-0.779 1,-1.75C8,0.635 7.1,0 6,0S4,0.635 4,1.5c0,0.971 1,1 1,1.75a0.765,0.765 0,0 1,-0.75 0.75H1a1,1 0,0 0,-1 1v2.25A0.765,0.765 0,0 0,0.75 8c0.75,0 0.779,-1 1.75,-1C3.365,7 4,7.9 4,9s-0.635,2 -1.5,2c-0.971,0 -1,-1 -1.75,-1a0.765,0.765 0,0 0,-0.75 0.75V15a1,1 0,0 0,1 1h3.25a0.765,0.765 0,0 0,0.75 -0.75c0,-0.75 -1,-0.779 -1,-1.75 0,-0.865 0.9,-1.5 2,-1.5s2,0.635 2,1.5c0,0.971 -1,1 -1,1.75a0.765,0.765 0,0 0,0.75 0.75H11a1,1 0,0 0,1 -1v-3.25a0.765,0.765 0,0 1,0.75 -0.75c0.75,0 0.779,1 1.75,1 0.865,0 1.5,-0.9 1.5,-2s-0.635,-2 -1.5,-2z" />
</vector>

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:autoMirrored="true"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M2,1h20c1.1,0 2,0.9 2,2v18c0,1.1 -0.9,2 -2,2H2c-1.1,0 -2,-0.9 -2,-2V3c0,-1.1 0.9,-2 2,-2z" />
<path
android:fillColor="#FFF"
android:pathData="M12,3h9c0.6,0 1,0.4 1,1v16c0,0.6 -0.4,1 -1,1h-9L12,3zM5.5,12.5l2.7,-3.7c0.2,-0.3 0.6,-0.3 0.8,-0.1l0.7,0.5c0.2,0.2 0.2,0.5 0,0.7L5.8,15c-0.2,0.2 -0.5,0.3 -0.8,0.1l-2.2,-2.2c-0.2,-0.2 -0.2,-0.5 0,-0.7l0.8,-0.8c0.2,-0.2 0.5,-0.2 0.7,0l1.2,1.1z" />
<path
android:fillColor="#FF000000"
android:pathData="M15,9l-1,1 2,2 -2,2 1,1 2,-2 2,2 1,-1 -2,-2 2,-2 -1,-1 -2,2.01L15,9z" />
</vector>

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="MergeRootFrame" />

@ -0,0 +1,154 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:layout_marginBottom="6dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
<TextView
android:id="@+id/details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
tools:text="@tools:sample/lorem/random" />
<TextView
android:id="@+id/author_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/details"
android:text="@string/mozac_feature_addons_authors" />
<TextView
android:id="@+id/author_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/details"
android:layout_alignParentEnd="true"
tools:text="@tools:sample/full_names" />
<View
android:id="@+id/author_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@+id/author_label"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:background="@color/photonGrey40"
android:importantForAccessibility="no" />
<TextView
android:id="@+id/version_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/author_divider"
android:text="@string/mozac_feature_addons_version" />
<TextView
android:id="@+id/version_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/author_divider"
android:layout_alignParentEnd="true"
tools:text="1.2.3" />
<View
android:id="@+id/version_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@+id/version_label"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:background="@color/photonGrey40"
android:importantForAccessibility="no" />
<TextView
android:id="@+id/last_updated_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/version_divider"
android:text="@string/mozac_feature_addons_last_updated" />
<TextView
android:id="@+id/last_updated_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/version_divider"
android:layout_alignParentEnd="true"
tools:text="Oct 16, 2019" />
<View
android:id="@+id/last_updated_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@+id/last_updated_label"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:background="@color/photonGrey40"
android:importantForAccessibility="no" />
<TextView
android:id="@+id/home_page_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/last_updated_divider"
android:text="@string/mozac_feature_addons_home_page" />
<ImageView
android:id="@+id/home_page_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/last_updated_divider"
android:layout_alignParentEnd="true"
android:contentDescription="@string/mozac_feature_addons_home_page"
android:src="@drawable/mozac_ic_link"
android:tint="?android:attr/textColorPrimary" />
<View
android:id="@+id/home_page_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@+id/home_page_label"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:background="@color/photonGrey40"
android:importantForAccessibility="no" />
<TextView
android:id="@+id/rating_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/home_page_divider"
android:text="@string/mozac_feature_addons_rating" />
<RatingBar
android:id="@+id/rating_view"
style="@style/Widget.AppCompat.RatingBar.Small"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:layout_below="@+id/home_page_divider"
android:layout_toStartOf="@+id/users_count"
android:isIndicator="true"
android:numStars="5" />
<TextView
android:id="@+id/users_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/home_page_divider"
android:layout_alignParentEnd="true"
android:layout_marginStart="6dp"
tools:text="591,642" />
</RelativeLayout>
</ScrollView>

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<mozilla.components.concept.engine.EngineView
android:id="@+id/addonSettingsEngineView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/add_ons_permissions"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/learn_more_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/add_ons_permissions"
android:background="?attr/selectableItemBackground"
android:drawableEnd="@drawable/mozac_ic_link"
android:padding="16dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:text="@string/mozac_feature_addons_learn_more"
app:drawableTint="?android:attr/textColorPrimary" />
</RelativeLayout>

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/add_ons_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".BrowserActivity" />
<include
android:id="@+id/addonProgressOverlay"
layout="@layout/overlay_add_on_progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:visibility="gone" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="6dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
<Switch
android:id="@+id/enable_switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|end"
android:background="?android:attr/selectableItemBackground"
android:checked="true"
android:clickable="true"
android:focusable="true"
android:padding="16dp"
android:text="@string/mozac_feature_addons_settings_on"
android:textSize="18sp" />
<TextView
android:id="@+id/settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/enable_switch"
android:background="?android:attr/selectableItemBackground"
android:drawablePadding="10dp"
android:padding="16dp"
android:text="@string/mozac_feature_addons_settings"
android:textColor="@drawable/addon_textview_selector"
android:textSize="18sp"
app:drawableStartCompat="@drawable/mozac_ic_preferences"
app:drawableTint="?android:attr/textColorPrimary" />
<TextView
android:id="@+id/details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/settings"
android:background="?android:attr/selectableItemBackground"
android:drawablePadding="6dp"
android:padding="16dp"
android:text="@string/mozac_feature_addons_details"
android:textColor="@drawable/addon_textview_selector"
android:textSize="18sp"
app:drawableStartCompat="@drawable/mozac_ic_information"
app:drawableTint="?android:attr/textColorPrimary" />
<TextView
android:id="@+id/permissions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/details"
android:background="?android:attr/selectableItemBackground"
android:drawablePadding="6dp"
android:padding="16dp"
android:text="@string/mozac_feature_addons_permissions"
android:textColor="@drawable/addon_textview_selector"
android:textSize="18sp"
app:drawableStartCompat="@drawable/mozac_ic_permissions" />
<Button
android:id="@+id/remove_add_on"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/permissions"
android:layout_marginTop="16dp"
android:text="@string/mozac_feature_addons_remove"
android:textColor="@color/photonRed50" />
</RelativeLayout>
</ScrollView>

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:orientation="horizontal"
android:paddingTop="16dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:text="@string/mozac_feature_addons_not_yet_supported_caption" />
<TextView
android:id="@+id/learn_more_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:background="?attr/selectableItemBackground"
android:text="@string/mozac_feature_addons_unsupported_learn_more" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/photonGrey30" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/unsupported_add_ons_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".BrowserActivity"/>
</LinearLayout>

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="1dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:gravity="start|center_vertical"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:drawableStart="@drawable/mozac_ic_extensions_black"
android:drawablePadding="8dp"
android:text="@string/mozac_add_on_install_progress_caption"/>
</androidx.cardview.widget.CardView>

@ -51,6 +51,10 @@
android:id="@+id/action_global_homeFragment" android:id="@+id/action_global_homeFragment"
app:destination="@id/homeFragment" /> app:destination="@id/homeFragment" />
<action
android:id="@+id/action_global_webExtensionActionPopupFragment"
app:destination="@id/webExtensionActionPopupFragment" />
<fragment <fragment
android:id="@+id/homeFragment" android:id="@+id/homeFragment"
android:name="org.mozilla.fenix.home.HomeFragment" android:name="org.mozilla.fenix.home.HomeFragment"
@ -201,6 +205,9 @@
<action <action
android:id="@+id/action_browserFragment_to_trackingProtectionPanelDialogFragment" android:id="@+id/action_browserFragment_to_trackingProtectionPanelDialogFragment"
app:destination="@id/trackingProtectionPanelDialogFragment" /> app:destination="@id/trackingProtectionPanelDialogFragment" />
<action
android:id="@+id/action_browserFragment_to_addonsManagementFragment"
app:destination="@id/addonsManagementFragment" />
</fragment> </fragment>
<fragment <fragment
@ -409,6 +416,9 @@
<action <action
android:id="@+id/action_settingsFragment_to_localeSettingsFragment" android:id="@+id/action_settingsFragment_to_localeSettingsFragment"
app:destination="@id/localeSettingsFragment" /> app:destination="@id/localeSettingsFragment" />
<action
android:id="@+id/action_settingsFragment_to_addonsFragment"
app:destination="@id/addonsManagementFragment" />
</fragment> </fragment>
<fragment <fragment
android:id="@+id/dataChoicesFragment" android:id="@+id/dataChoicesFragment"
@ -702,4 +712,72 @@
android:id="@+id/saveLoginSettingFragment" android:id="@+id/saveLoginSettingFragment"
android:name="org.mozilla.fenix.settings.logins.SaveLoginSettingFragment" android:name="org.mozilla.fenix.settings.logins.SaveLoginSettingFragment"
android:label="SaveLoginSettingFragment" /> android:label="SaveLoginSettingFragment" />
<fragment
android:id="@+id/addonsManagementFragment"
android:name="org.mozilla.fenix.addons.AddonsManagementFragment">
<action
android:id="@+id/action_addonsManagementFragment_to_addonDetailsFragment"
app:destination="@id/addonDetailsFragment" />
<action
android:id="@+id/action_addonsManagementFragment_to_installedAddonDetails"
app:destination="@id/installedAddonDetailsFragment" />
<action
android:id="@+id/action_addonsManagementFragment_to_notYetSupportedAddonFragment"
app:destination="@id/notYetSupportedAddonFragment" />
</fragment>
<fragment
android:id="@+id/addonDetailsFragment"
android:name="org.mozilla.fenix.addons.AddonDetailsFragment">
<argument
android:name="addon"
app:argType="mozilla.components.feature.addons.Addon" />
</fragment>
<fragment
android:id="@+id/installedAddonDetailsFragment"
android:name="org.mozilla.fenix.addons.InstalledAddonDetailsFragment">
<action
android:id="@+id/action_installedAddonFragment_to_addonInternalSettingsFragment"
app:destination="@id/addonInternalSettingsFragment" />
<action
android:id="@+id/action_installedAddonFragment_to_addonDetailsFragment"
app:destination="@id/addonDetailsFragment" />
<action
android:id="@+id/action_installedAddonFragment_to_addonPermissionsDetailsFragment"
app:destination="@id/addonPermissionsDetailFragment" />
<argument
android:name="addon"
app:argType="mozilla.components.feature.addons.Addon" />
</fragment>
<fragment
android:id="@+id/notYetSupportedAddonFragment"
android:name="org.mozilla.fenix.addons.NotYetSupportedAddonFragment">
<argument
android:name="addons"
app:argType="mozilla.components.feature.addons.Addon[]" />
</fragment>
<fragment
android:id="@+id/addonInternalSettingsFragment"
android:name="org.mozilla.fenix.addons.AddonInternalSettingsFragment">
<argument
android:name="addon"
app:argType="mozilla.components.feature.addons.Addon" />
</fragment>
<fragment
android:id="@+id/addonPermissionsDetailFragment"
android:name="org.mozilla.fenix.addons.AddonPermissionsDetailsFragment">
<argument
android:name="addon"
app:argType="mozilla.components.feature.addons.Addon" />
</fragment>
<fragment
android:id="@+id/webExtensionActionPopupFragment"
android:name="org.mozilla.fenix.addons.WebExtensionActionPopupFragment">
<argument
android:name="webExtensionId"
app:argType="string" />
<argument
android:name="webExtensionTitle"
app:argType="string"
app:nullable="true"/>
</fragment>
</navigation> </navigation>

@ -28,8 +28,7 @@
<string name="pref_key_delete_permissions_on_quit" translatable="false">pref_key_delete_permissions_on_quit</string> <string name="pref_key_delete_permissions_on_quit" translatable="false">pref_key_delete_permissions_on_quit</string>
<string name="pref_key_delete_browsing_data_on_quit_categories" translatable="false">pref_key_delete_browsing_data_on_quit_categories</string> <string name="pref_key_delete_browsing_data_on_quit_categories" translatable="false">pref_key_delete_browsing_data_on_quit_categories</string>
<string name="pref_key_last_known_mode_private" translatable="false">pref_key_last_known_mode_private</string> <string name="pref_key_last_known_mode_private" translatable="false">pref_key_last_known_mode_private</string>
<string name="pref_key_addons" translatable="false">pref_key_addons</string>
<string name="pref_key_last_maintenance" translatable="false">pref_key_last_maintenance</string> <string name="pref_key_last_maintenance" translatable="false">pref_key_last_maintenance</string>
<string name="pref_key_help" translatable="false">pref_key_help</string> <string name="pref_key_help" translatable="false">pref_key_help</string>
<string name="pref_key_rate" translatable="false">pref_key_rate</string> <string name="pref_key_rate" translatable="false">pref_key_rate</string>

@ -65,6 +65,8 @@
<!-- Content description (not visible, for screen readers etc.): Un-bookmark the current page --> <!-- Content description (not visible, for screen readers etc.): Un-bookmark the current page -->
<string name="browser_menu_edit_bookmark">Edit bookmark</string> <string name="browser_menu_edit_bookmark">Edit bookmark</string>
<!-- Browser menu button that sends a user to help articles --> <!-- Browser menu button that sends a user to help articles -->
<string name="browser_menu_addon_manager">Add-ons Manager</string>
<!-- Browser menu button that sends a user to help articles -->
<string name="browser_menu_help">Help</string> <string name="browser_menu_help">Help</string>
<!-- Browser menu button that sends a to a the what's new article --> <!-- Browser menu button that sends a to a the what's new article -->
<string name="browser_menu_whats_new">Whats New</string> <string name="browser_menu_whats_new">Whats New</string>
@ -242,6 +244,8 @@
<string name="preferences_account_settings">Account settings</string> <string name="preferences_account_settings">Account settings</string>
<!-- Preference for open links in third party apps --> <!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Open links in apps</string> <string name="preferences_open_links_in_apps">Open links in apps</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Add-ons</string>
<!-- Account Preferences --> <!-- Account Preferences -->
<!-- Preference for triggering sync --> <!-- Preference for triggering sync -->

@ -104,12 +104,16 @@
<PreferenceCategory <PreferenceCategory
android:title="@string/preferences_category_advanced" android:title="@string/preferences_category_advanced"
app:iconSpaceReserved="false" app:iconSpaceReserved="false"
android:key="@string/pref_key_advanced" android:key="@string/pref_key_advanced">
app:isPreferenceVisible="false"> <androidx.preference.Preference
android:icon="@drawable/mozac_ic_extensions_black"
android:key="@string/pref_key_addons"
android:title="@string/preferences_addons" />
<androidx.preference.Preference <androidx.preference.Preference
android:icon="@drawable/ic_language" android:icon="@drawable/ic_language"
android:key="@string/pref_key_language" android:key="@string/pref_key_language"
android:title="@string/preferences_language" /> android:title="@string/preferences_language"
app:isPreferenceVisible="false" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory

@ -325,6 +325,15 @@ class DefaultBrowserToolbarControllerTest {
verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.ADD_TO_FIREFOX_HOME)) } verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.ADD_TO_FIREFOX_HOME)) }
} }
@Test
fun handleToolbarAddonsManagerPress() = runBlockingTest {
val item = ToolbarMenu.Item.AddonsManager
controller.handleToolbarItemInteraction(item)
verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.ADDONS_MANAGER)) }
}
@Test @Test
fun handleToolbarAddToHomeScreenPress() { fun handleToolbarAddToHomeScreenPress() {
val item = ToolbarMenu.Item.AddToHomeScreen val item = ToolbarMenu.Item.AddToHomeScreen

@ -100,6 +100,9 @@ object Deps {
const val mozilla_browser_errorpages = "org.mozilla.components:browser-errorpages:${Versions.mozilla_android_components}" const val mozilla_browser_errorpages = "org.mozilla.components:browser-errorpages:${Versions.mozilla_android_components}"
const val mozilla_browser_storage_sync = "org.mozilla.components:browser-storage-sync:${Versions.mozilla_android_components}" const val mozilla_browser_storage_sync = "org.mozilla.components:browser-storage-sync:${Versions.mozilla_android_components}"
const val mozilla_feature_addons = "org.mozilla.components:feature-addons:${Versions.mozilla_android_components}"
const val mozilla_support_extensions = "org.mozilla.components:support-webextensions:${Versions.mozilla_android_components}"
const val mozilla_feature_accounts = "org.mozilla.components:feature-accounts:${Versions.mozilla_android_components}" const val mozilla_feature_accounts = "org.mozilla.components:feature-accounts:${Versions.mozilla_android_components}"
const val mozilla_feature_app_links = "org.mozilla.components:feature-app-links:${Versions.mozilla_android_components}" const val mozilla_feature_app_links = "org.mozilla.components:feature-app-links:${Versions.mozilla_android_components}"
const val mozilla_feature_awesomebar = "org.mozilla.components:feature-awesomebar:${Versions.mozilla_android_components}" const val mozilla_feature_awesomebar = "org.mozilla.components:feature-awesomebar:${Versions.mozilla_android_components}"

@ -86,7 +86,7 @@ The following metrics are added to the ping:
| download_notification.try_again |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user tapped on try again when a download fails in the download notification |[1](https://github.com/mozilla-mobile/fenix/pull/6554)||2020-09-01 | | download_notification.try_again |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user tapped on try again when a download fails in the download notification |[1](https://github.com/mozilla-mobile/fenix/pull/6554)||2020-09-01 |
| error_page.visited_error |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user encountered an error page |[1](https://github.com/mozilla-mobile/fenix/pull/2491#issuecomment-492414486)|<ul><li>error_type: The error type of the error page encountered</li></ul>|2020-09-01 | | error_page.visited_error |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user encountered an error page |[1](https://github.com/mozilla-mobile/fenix/pull/2491#issuecomment-492414486)|<ul><li>error_type: The error type of the error page encountered</li></ul>|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)|<ul><li>source: The method used to open Fenix. Possible values are: `app_icon`, `custom_tab` or `link`</li></ul>|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)|<ul><li>source: The method used to open Fenix. Possible values are: `app_icon`, `custom_tab` or `link`</li></ul>|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)|<ul><li>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 </li></ul>|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)|<ul><li>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 </li></ul>|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)|<ul><li>autocomplete: A boolean that tells us whether the URL was autofilled by an Autocomplete suggestion</li></ul>|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)|<ul><li>autocomplete: A boolean that tells us whether the URL was autofilled by an Autocomplete suggestion</li></ul>|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)|<ul><li>mode: The mode the link was opened in. Either 'PRIVATE' or 'NORMAL'</li></ul>|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)|<ul><li>mode: The mode the link was opened in. Either 'PRIVATE' or 'NORMAL'</li></ul>|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)|<ul><li>source: A string that tells us how the user performed the search. Possible values are: * default.action * default.suggestion * shortcut.action * shortcut.suggestion </li></ul>|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)|<ul><li>source: A string that tells us how the user performed the search. Possible values are: * default.action * default.suggestion * shortcut.action * shortcut.suggestion </li></ul>|2020-09-01 |

Loading…
Cancel
Save