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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

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

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

View File

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

View File

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

View File

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

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

File diff suppressed because one or more lines are too long