1
0
Fork 0

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>
master
Gabriel Luong 2020-02-04 01:41:52 -05:00 committed by GitHub
parent 4eb71ce235
commit 64a4a7f422
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1412 additions and 11 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Events.browserMenuActionKeys, String>?

View File

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

View File

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

View File

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

View File

@ -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<Preference>(getPreferenceKey(pref_key_passwords))?.apply {
isVisible = FeatureFlags.logins
}
findPreference<PreferenceCategory>(getPreferenceKey(R.string.pref_key_advanced))?.apply {
findPreference<Preference>(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()
}

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@ -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" />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -51,6 +51,10 @@
android:id="@+id/action_global_homeFragment"
app:destination="@id/homeFragment" />
<action
android:id="@+id/action_global_webExtensionActionPopupFragment"
app:destination="@id/webExtensionActionPopupFragment" />
<fragment
android:id="@+id/homeFragment"
android:name="org.mozilla.fenix.home.HomeFragment"
@ -201,6 +205,9 @@
<action
android:id="@+id/action_browserFragment_to_trackingProtectionPanelDialogFragment"
app:destination="@id/trackingProtectionPanelDialogFragment" />
<action
android:id="@+id/action_browserFragment_to_addonsManagementFragment"
app:destination="@id/addonsManagementFragment" />
</fragment>
<fragment
@ -409,6 +416,9 @@
<action
android:id="@+id/action_settingsFragment_to_localeSettingsFragment"
app:destination="@id/localeSettingsFragment" />
<action
android:id="@+id/action_settingsFragment_to_addonsFragment"
app:destination="@id/addonsManagementFragment" />
</fragment>
<fragment
android:id="@+id/dataChoicesFragment"
@ -702,4 +712,72 @@
android:id="@+id/saveLoginSettingFragment"
android:name="org.mozilla.fenix.settings.logins.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>

View File

@ -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_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_addons" translatable="false">pref_key_addons</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_rate" translatable="false">pref_key_rate</string>

View File

@ -65,6 +65,8 @@
<!-- Content description (not visible, for screen readers etc.): Un-bookmark the current page -->
<string name="browser_menu_edit_bookmark">Edit bookmark</string>
<!-- 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>
<!-- Browser menu button that sends a to a the what's new article -->
<string name="browser_menu_whats_new">Whats New</string>
@ -242,6 +244,8 @@
<string name="preferences_account_settings">Account settings</string>
<!-- Preference for open links in third party apps -->
<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 -->
<!-- Preference for triggering sync -->

View File

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

View File

@ -325,6 +325,15 @@ class DefaultBrowserToolbarControllerTest {
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
fun handleToolbarAddToHomeScreenPress() {
val item = ToolbarMenu.Item.AddToHomeScreen

View File

@ -100,6 +100,9 @@ object Deps {
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_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_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}"

File diff suppressed because one or more lines are too long