implemented addon support

This commit is contained in:
Fabian Freund
2024-10-26 10:09:21 +02:00
parent 1323d6bdaa
commit 91a4319b28
26 changed files with 1716 additions and 3 deletions
@@ -10,7 +10,10 @@ import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.annotation.CallSuper
import androidx.fragment.app.Fragment
import eu.lensai.flutter_mozilla_components.addons.WebExtensionActionPopupActivity
import eu.lensai.flutter_mozilla_components.addons.WebExtensionPromptFeature
import eu.lensai.flutter_mozilla_components.databinding.FragmentBrowserBinding
import mozilla.components.browser.state.state.WebExtensionState
import mozilla.components.concept.engine.EngineView
import mozilla.components.feature.app.links.AppLinksFeature
import mozilla.components.feature.downloads.DownloadsFeature
@@ -29,6 +32,7 @@ import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.ktx.android.arch.lifecycle.addObservers
import mozilla.components.support.locale.ActivityContextWrapper
import mozilla.components.support.utils.ext.requestInPlacePermissions
import mozilla.components.support.webextensions.WebExtensionPopupObserver
/**
* Base fragment extended by [BrowserFragment] and [ExternalAppBrowserFragment].
@@ -41,9 +45,11 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
private val downloadsFeature = ViewBoundFeatureWrapper<DownloadsFeature>()
private val appLinksFeature = ViewBoundFeatureWrapper<AppLinksFeature>()
private val promptFeature = ViewBoundFeatureWrapper<PromptFeature>()
private val webExtensionPromptFeature = ViewBoundFeatureWrapper<WebExtensionPromptFeature>()
private val sitePermissionsFeature = ViewBoundFeatureWrapper<SitePermissionsFeature>()
private val swipeRefreshFeature = ViewBoundFeatureWrapper<SwipeRefreshFeature>()
protected val sessionId: String?
get() = arguments?.getString(SESSION_ID_KEY)
@@ -215,20 +221,41 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
view = binding.root,
)
webExtensionPromptFeature.set(
feature = WebExtensionPromptFeature(
store = components.store,
context = requireContext(),
fragmentManager = parentFragmentManager,
),
owner = this,
view = binding.root
)
val secureWindowFeature = SecureWindowFeature(
window = requireActivity().window,
store = components.store,
customTabId = sessionId,
)
val webExtensionPopupObserver = WebExtensionPopupObserver(components.store, ::openPopup)
// Observe the lifecycle for supported features
lifecycle.addObservers(
secureWindowFeature,
webExtensionPopupObserver,
)
return binding.root
}
private fun openPopup(webExtensionState: WebExtensionState) {
val intent = Intent(requireContext().applicationContext, WebExtensionActionPopupActivity::class.java)
intent.putExtra("web_extension_id", webExtensionState.id)
intent.putExtra("web_extension_name", webExtensionState.name)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent)
}
@CallSuper
override fun onBackPressed(): Boolean =
listOf(sessionFeature).any { it.onBackPressed() }
@@ -58,9 +58,13 @@ class Components(
) {
private val runtime by lazy {
// Allow for exfiltrating Gecko metrics through the Glean SDK.
val builder = GeckoRuntimeSettings.Builder().aboutConfigEnabled(true)
val builder = GeckoRuntimeSettings.Builder()
.aboutConfigEnabled(true)
.extensionsWebAPIEnabled(true)
builder.experimentDelegate(NimbusExperimentDelegate())
builder.crashHandler(CrashHandlerService::class.java)
GeckoRuntime.create(applicationContext, builder.build())
}
@@ -2,7 +2,6 @@ package eu.lensai.flutter_mozilla_components
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import eu.lensai.flutter_mozilla_components.GlobalComponents
class NotificationActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@@ -0,0 +1,107 @@
/* 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 eu.lensai.flutter_mozilla_components.addons
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.View
import android.widget.RatingBar
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.text.HtmlCompat
import mozilla.components.feature.addons.R as MozComp
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.ui.translateDescription
import mozilla.components.feature.addons.ui.translateName
import mozilla.components.support.utils.ext.getParcelableExtraCompat
import eu.lensai.flutter_mozilla_components.R
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Locale
/**
* An activity to show the details of an add-on.
*/
class AddonDetailsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_on_details)
val addon = requireNotNull(
intent.getParcelableExtraCompat("add_on", Addon::class.java),
)
bind(addon)
}
private fun bind(addon: Addon) {
title = addon.translateName(this)
bindDetails(addon)
bindAuthor(addon)
bindVersion(addon)
bindLastUpdated(addon)
bindWebsite(addon)
bindRating(addon)
}
private fun bindRating(addon: Addon) {
addon.rating?.let {
val ratingView = findViewById<RatingBar>(R.id.rating_view)
val userCountView = findViewById<TextView>(R.id.users_count)
val ratingContentDescription = getString(MozComp.string.mozac_feature_addons_rating_content_description_2)
ratingView.contentDescription = String.format(ratingContentDescription, it.average)
ratingView.rating = it.average
userCountView.text = getFormattedAmount(it.reviews)
}
}
private fun bindWebsite(addon: Addon) {
findViewById<View>(R.id.home_page_text).setOnClickListener {
val intent =
Intent(Intent.ACTION_VIEW).setData(Uri.parse(addon.homepageUrl))
startActivity(intent)
}
}
private fun bindLastUpdated(addon: Addon) {
val lastUpdatedView = findViewById<TextView>(R.id.last_updated_text)
lastUpdatedView.text = formatDate(addon.updatedAt)
}
private fun bindVersion(addon: Addon) {
val versionView = findViewById<TextView>(R.id.version_text)
versionView.text = addon.version
}
private fun bindAuthor(addon: Addon) {
val authorsView = findViewById<TextView>(R.id.author_text)
authorsView.text = addon.author?.name.orEmpty()
}
private fun bindDetails(addon: Addon) {
val detailsView = findViewById<TextView>(R.id.details)
val detailsText = addon.translateDescription(this)
val parsedText = detailsText.replace("\n", "<br/>")
val text = HtmlCompat.fromHtml(parsedText, HtmlCompat.FROM_HTML_MODE_COMPACT)
detailsView.text = text
detailsView.movementMethod = LinkMovementMethod.getInstance()
}
private fun formatDate(text: String): String {
val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault())
return DateFormat.getDateInstance().format(formatter.parse(text)!!)
}
}
@@ -0,0 +1,100 @@
/* 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 eu.lensai.flutter_mozilla_components.addons
import android.content.Context
import android.os.Bundle
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import eu.lensai.flutter_mozilla_components.Components
import eu.lensai.flutter_mozilla_components.GlobalComponents
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.EngineView
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.ui.translateName
import mozilla.components.support.utils.ext.getParcelableCompat
import mozilla.components.support.utils.ext.getParcelableExtraCompat
import eu.lensai.flutter_mozilla_components.R
import mozilla.components.browser.state.store.BrowserStore
/**
* An activity to show the settings of an add-on.
*/
class AddonSettingsActivity : AppCompatActivity() {
private val components: Components by lazy { GlobalComponents.components!! }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_on_settings)
val addon = requireNotNull(
intent.getParcelableExtraCompat("add_on", Addon::class.java),
)
title = addon.translateName(this)
supportFragmentManager
.beginTransaction()
.replace(R.id.addonSettingsContainer, AddonSettingsFragment.create(addon))
.commit()
}
override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? =
when (name) {
EngineView::class.java.name -> components.engine.createView(context, attrs).asView()
else -> super.onCreateView(parent, name, context, attrs)
}
/**
* A fragment to show the settings of an add-on with [EngineView].
*/
class AddonSettingsFragment : Fragment() {
private val components: Components by lazy { GlobalComponents.components!! }
private lateinit var optionsPageUrl: String
private lateinit var engineSession: EngineSession
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
optionsPageUrl = requireNotNull(
arguments?.getParcelableCompat(
"add_on",
Addon::class.java,
)?.installedState?.optionsPageUrl,
)
engineSession = components.engine.createSession()
return inflater.inflate(R.layout.fragment_add_on_settings, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val addonSettingsEngineView = view.findViewById<View>(R.id.addonSettingsEngineView) as EngineView
addonSettingsEngineView.render(engineSession)
engineSession.loadUrl(optionsPageUrl)
}
override fun onDestroyView() {
engineSession.close()
super.onDestroyView()
}
companion object {
/**
* Create an [AddonSettingsFragment] with add_on as a required parameter.
*/
fun create(addon: Addon) = AddonSettingsFragment().apply {
arguments = Bundle().apply {
putParcelable("add_on", addon)
}
}
}
}
}
@@ -0,0 +1,26 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package eu.lensai.flutter_mozilla_components.addons
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import eu.lensai.flutter_mozilla_components.R
/**
* An activity to manage add-ons.
*/
class AddonsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_on_main)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction().apply {
replace(R.id.container, AddonsFragment())
commit()
}
}
}
}
@@ -0,0 +1,149 @@
/* 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 eu.lensai.flutter_mozilla_components.addons
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import eu.lensai.flutter_mozilla_components.Components
import eu.lensai.flutter_mozilla_components.GlobalComponents
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.support.base.feature.ViewBoundFeatureWrapper
import eu.lensai.flutter_mozilla_components.R
import mozilla.components.feature.addons.R as MozComp
/**
* Fragment use for managing add-ons.
*/
class AddonsFragment : Fragment(), AddonsManagerAdapterDelegate {
private val components: Components by lazy { GlobalComponents.components!! }
private val webExtensionPromptFeature = ViewBoundFeatureWrapper<WebExtensionPromptFeature>()
private lateinit var recyclerView: RecyclerView
private val scope = CoroutineScope(Dispatchers.IO)
private lateinit var addons: List<Addon>
private var adapter: AddonsManagerAdapter? = null
private val addonProgressOverlay: View
get() = requireView().findViewById(R.id.addonProgressOverlay)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
return inflater.inflate(R.layout.fragment_add_ons, container, false)
}
override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) {
super.onViewCreated(rootView, savedInstanceState)
bindRecyclerView(rootView)
webExtensionPromptFeature.set(
feature = WebExtensionPromptFeature(
store = components.store,
context = requireContext(),
fragmentManager = parentFragmentManager,
),
owner = this,
view = rootView,
)
}
override fun onStart() {
super.onStart()
this@AddonsFragment.view?.let { view ->
bindRecyclerView(view)
}
addonProgressOverlay.visibility = View.GONE
}
private fun bindRecyclerView(rootView: View) {
recyclerView = rootView.findViewById(R.id.add_ons_list)
recyclerView.layoutManager = LinearLayoutManager(requireContext())
scope.launch {
try {
addons = components.addonManager.getAddons()
scope.launch(Dispatchers.Main) {
adapter = AddonsManagerAdapter(
this@AddonsFragment,
addons,
store = components.store,
)
recyclerView.adapter = adapter
}
} catch (e: AddonManagerException) {
scope.launch(Dispatchers.Main) {
Toast.makeText(
activity,
MozComp.string.mozac_feature_addons_failed_to_query_extensions,
Toast.LENGTH_SHORT,
).show()
}
}
}
}
override fun onAddonItemClicked(addon: Addon) {
if (addon.isInstalled()) {
val intent = Intent(context, InstalledAddonDetailsActivity::class.java)
intent.putExtra("add_on", addon)
startActivity(intent)
} else {
val intent = Intent(context, AddonDetailsActivity::class.java)
intent.putExtra("add_on", addon)
startActivity(intent)
}
}
override fun onInstallAddonButtonClicked(addon: Addon) {
if (isInstallationInProgress) {
return
}
installAddon(addon)
}
private val installAddon: ((Addon) -> Unit) = { addon ->
addonProgressOverlay.visibility = View.VISIBLE
isInstallationInProgress = true
components.addonManager.installAddon(
url = addon.downloadUrl,
onSuccess = {
runIfFragmentIsAttached {
isInstallationInProgress = false
this@AddonsFragment.view?.let { view ->
bindRecyclerView(view)
}
addonProgressOverlay.visibility = View.GONE
}
},
onError = { _ ->
runIfFragmentIsAttached {
addonProgressOverlay.visibility = View.GONE
isInstallationInProgress = false
}
},
)
}
/**
* Whether or not an add-on installation is in progress.
*/
private var isInstallationInProgress = false
}
@@ -0,0 +1,24 @@
/* 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 eu.lensai.flutter_mozilla_components.addons
import androidx.fragment.app.Fragment
import java.text.NumberFormat
import java.util.Locale
internal fun getFormattedAmount(amount: Int): String {
return NumberFormat.getNumberInstance(Locale.getDefault()).format(amount)
}
/**
* Run the [block] only if the [Fragment] is attached.
*
* @param block A callback to be executed if the container [Fragment] is attached.
*/
internal inline fun Fragment.runIfFragmentIsAttached(block: () -> Unit) {
context?.let {
block()
}
}
@@ -0,0 +1,202 @@
/* 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 eu.lensai.flutter_mozilla_components.addons
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SwitchCompat
import androidx.core.view.isVisible
import eu.lensai.flutter_mozilla_components.Components
import eu.lensai.flutter_mozilla_components.GlobalComponents
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.translateName
import mozilla.components.support.utils.ext.getParcelableExtraCompat
import eu.lensai.flutter_mozilla_components.R
import mozilla.components.feature.addons.R as MozComp
/**
* An activity to show the details of a installed add-on.
*/
class InstalledAddonDetailsActivity : AppCompatActivity() {
private val components: Components by lazy { GlobalComponents.components!! }
private val scope = CoroutineScope(Dispatchers.IO)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_installed_add_on_details)
val addon = requireNotNull(
intent.getParcelableExtraCompat("add_on", Addon::class.java),
).also {
bindUI(it)
}
bindAddon(addon)
}
private fun bindAddon(addon: Addon) {
scope.launch {
try {
val addons = components.addonManager.getAddons()
scope.launch(Dispatchers.Main) {
addons.find { addon.id == it.id }.let {
if (it == null) {
throw AddonManagerException(Exception("Addon ${addon.id} not found"))
} else {
bindUI(it)
}
}
}
} catch (e: AddonManagerException) {
scope.launch(Dispatchers.Main) {
Toast.makeText(
baseContext,
MozComp.string.mozac_feature_addons_failed_to_query_extensions,
Toast.LENGTH_SHORT,
).show()
}
}
}
}
private fun bindUI(addon: Addon) {
title = addon.translateName(this)
bindEnableSwitch(addon)
bindSettings(addon)
bindDetails(addon)
bindPermissions(addon)
bindAllowInPrivateBrowsingSwitch(addon)
bindRemoveButton(addon)
}
private fun bindEnableSwitch(addon: Addon) {
val switch = findViewById<SwitchCompat>(R.id.enable_switch)
switch.setState(addon.isEnabled())
switch.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
components.addonManager.enableAddon(
addon,
onSuccess = {
switch.setState(true)
Toast.makeText(
this,
getString(MozComp.string.mozac_feature_addons_successfully_enabled, addon.translateName(this)),
Toast.LENGTH_SHORT,
).show()
},
onError = {
Toast.makeText(
this,
getString(MozComp.string.mozac_feature_addons_failed_to_enable, addon.translateName(this)),
Toast.LENGTH_SHORT,
).show()
},
)
} else {
components.addonManager.disableAddon(
addon,
onSuccess = {
switch.setState(false)
Toast.makeText(
this,
getString(MozComp.string.mozac_feature_addons_successfully_disabled, addon.translateName(this)),
Toast.LENGTH_SHORT,
).show()
},
onError = {
Toast.makeText(
this,
getString(MozComp.string.mozac_feature_addons_failed_to_disable, addon.translateName(this)),
Toast.LENGTH_SHORT,
).show()
},
)
}
}
}
private fun bindSettings(addon: Addon) {
val view = findViewById<View>(R.id.settings)
view.isVisible = shouldSettingsBeVisible(addon)
view.isEnabled = shouldSettingsBeVisible(addon)
view.setOnClickListener {
val intent = Intent(this, AddonSettingsActivity::class.java)
intent.putExtra("add_on", addon)
this.startActivity(intent)
}
}
private fun bindDetails(addon: Addon) {
findViewById<View>(R.id.details).setOnClickListener {
val intent = Intent(this, AddonDetailsActivity::class.java)
intent.putExtra("add_on", addon)
this.startActivity(intent)
}
}
private fun bindPermissions(addon: Addon) {
findViewById<View>(R.id.permissions).setOnClickListener {
val intent = Intent(this, PermissionsDetailsActivity::class.java)
intent.putExtra("add_on", addon)
this.startActivity(intent)
}
}
private fun bindAllowInPrivateBrowsingSwitch(addon: Addon) {
val switch = findViewById<SwitchCompat>(R.id.allow_in_private_browsing_switch)
switch.isChecked = addon.isAllowedInPrivateBrowsing()
switch.setOnCheckedChangeListener { _, isChecked ->
components.addonManager.setAddonAllowedInPrivateBrowsing(
addon,
isChecked,
onSuccess = {
switch.isChecked = isChecked
},
)
}
}
private fun bindRemoveButton(addon: Addon) {
findViewById<View>(R.id.remove_add_on).setOnClickListener {
components.addonManager.uninstallAddon(
addon,
onSuccess = {
Toast.makeText(
this,
getString(MozComp.string.mozac_feature_addons_successfully_uninstalled, addon.translateName(this)),
Toast.LENGTH_SHORT,
).show()
finish()
},
onError = { _, _ ->
Toast.makeText(
this,
getString(MozComp.string.mozac_feature_addons_failed_to_uninstall, addon.translateName(this)),
Toast.LENGTH_SHORT,
).show()
},
)
}
}
private fun SwitchCompat.setState(checked: Boolean) {
isChecked = checked
}
private fun shouldSettingsBeVisible(addon: Addon) = !addon.installedState?.optionsPageUrl.isNullOrEmpty()
}
@@ -0,0 +1,93 @@
/* 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 eu.lensai.flutter_mozilla_components.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 android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.ui.translateName
import mozilla.components.support.utils.ext.getParcelableExtraCompat
import eu.lensai.flutter_mozilla_components.R
private const val LEARN_MORE_URL =
"https://support.mozilla.org/kb/permission-request-messages-firefox-extensions"
/**
* An activity to show the permissions of an add-on.
*/
class PermissionsDetailsActivity : AppCompatActivity(), View.OnClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_on_permissions)
val addon = requireNotNull(
intent.getParcelableExtraCompat("add_on", Addon::class.java),
)
title = addon.translateName(this)
bindPermissions(addon)
bindLearnMore()
}
private fun bindPermissions(addon: Addon) {
val recyclerView = findViewById<RecyclerView>(R.id.add_ons_permissions)
recyclerView.layoutManager = LinearLayoutManager(this)
val sortedPermissions = addon.translatePermissions(this).sorted()
recyclerView.adapter = PermissionsAdapter(sortedPermissions)
}
private fun bindLearnMore() {
findViewById<View>(R.id.learn_more_label).setOnClickListener(this)
}
/**
* An adapter for displaying the permissions of an add-on.
*/
class PermissionsAdapter(
private val permissions: List<String>,
) :
RecyclerView.Adapter<PermissionViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PermissionViewHolder {
val context = parent.context
val inflater = LayoutInflater.from(context)
val view = inflater.inflate(R.layout.add_ons_permission_item, parent, false)
val titleView = view.findViewById<TextView>(R.id.permission)
return PermissionViewHolder(
view,
titleView,
)
}
override fun getItemCount() = permissions.size
override fun onBindViewHolder(holder: PermissionViewHolder, position: Int) {
val permission = permissions[position]
holder.textView.text = permission
}
}
/**
* A view holder for displaying the permissions of an add-on.
*/
class PermissionViewHolder(
val view: View,
val textView: TextView,
) : RecyclerView.ViewHolder(view)
override fun onClick(v: View?) {
val intent =
Intent(Intent.ACTION_VIEW).setData(Uri.parse(LEARN_MORE_URL))
startActivity(intent)
}
}
@@ -0,0 +1,127 @@
/* 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 eu.lensai.flutter_mozilla_components.addons
import android.content.Context
import android.os.Bundle
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import eu.lensai.flutter_mozilla_components.Components
import eu.lensai.flutter_mozilla_components.GlobalComponents
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 eu.lensai.flutter_mozilla_components.R
/**
* An activity to show the pop up action of a web extension.
*/
class WebExtensionActionPopupActivity : AppCompatActivity() {
private val components: Components by lazy { GlobalComponents.components!! }
private lateinit var webExtensionId: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_on_settings)
webExtensionId = requireNotNull(intent.getStringExtra("web_extension_id"))
intent.getStringExtra("web_extension_name")?.let {
title = it
}
supportFragmentManager
.beginTransaction()
.replace(R.id.addonSettingsContainer, WebExtensionActionPopupFragment.create(webExtensionId))
.commit()
}
override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? =
when (name) {
EngineView::class.java.name -> components.engine.createView(context, attrs).asView()
else -> super.onCreateView(parent, name, context, attrs)
}
/**
* A fragment to show the web extension action popup with [EngineView].
*/
class WebExtensionActionPopupFragment : Fragment(), EngineSession.Observer {
private val components: Components by lazy { GlobalComponents.components!! }
private var engineSession: EngineSession? = null
private lateinit var webExtensionId: String
private val addonSettingsEngineView: EngineView
get() = requireView().findViewById<View>(R.id.addonSettingsEngineView) as EngineView
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
webExtensionId = requireNotNull(arguments?.getString("web_extension_id"))
engineSession = components.store.state.extensions[webExtensionId]?.popupSession
return inflater.inflate(R.layout.fragment_add_on_settings, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val session = engineSession
if (session != null) {
addonSettingsEngineView.render(session)
consumePopupSession()
} else {
consumeFrom(components.store) { state ->
state.extensions[webExtensionId]?.let { extState ->
extState.popupSession?.let {
if (engineSession == null) {
addonSettingsEngineView.render(it)
consumePopupSession()
engineSession = it
}
}
}
}
}
}
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?.onBackPressedDispatcher?.onBackPressed()
}
}
private fun consumePopupSession() {
components.store.dispatch(
WebExtensionAction.UpdatePopupSessionAction(webExtensionId, popupSession = null),
)
}
companion object {
/**
* Create an [WebExtensionActionPopupFragment] with webExtensionId as a required parameter.
*/
fun create(webExtensionId: String) = WebExtensionActionPopupFragment().apply {
arguments = Bundle().apply {
putString("web_extension_id", webExtensionId)
}
}
}
}
}
@@ -0,0 +1,365 @@
/* 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 eu.lensai.flutter_mozilla_components.addons
import android.content.Context
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.FragmentManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.mapNotNull
import mozilla.components.browser.state.action.WebExtensionAction
import mozilla.components.browser.state.state.extension.WebExtensionPromptRequest
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.webextension.PermissionPromptResponse
import mozilla.components.concept.engine.webextension.WebExtensionInstallException
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.ui.AddonInstallationDialogFragment
import mozilla.components.feature.addons.ui.PermissionsDialogFragment
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.base.feature.LifecycleAwareFeature
import mozilla.components.support.ktx.android.content.appVersionName
import mozilla.components.ui.widgets.withCenterAlignedButtons
import eu.lensai.flutter_mozilla_components.R
import mozilla.components.feature.addons.R as MozComp
/**
* Feature implementation for handling [WebExtensionPromptRequest] and showing the respective UI.
*/
class WebExtensionPromptFeature(
private val store: BrowserStore,
private val context: Context,
private val fragmentManager: FragmentManager,
) : LifecycleAwareFeature {
/**
* Whether or not an add-on installation is in progress.
*/
private var isInstallationInProgress = false
private var scope: CoroutineScope? = null
/**
* Starts observing the selected session to listen for window requests
* and opens / closes tabs as needed.
*/
override fun start() {
scope = store.flowScoped { flow ->
flow.mapNotNull { state ->
state.webExtensionPromptRequest
}.distinctUntilChanged().collect { promptRequest ->
when (promptRequest) {
is WebExtensionPromptRequest.AfterInstallation -> {
handleAfterInstallationRequest(promptRequest)
}
is WebExtensionPromptRequest.BeforeInstallation.InstallationFailed -> {
handleBeforeInstallationRequest(promptRequest)
consumePromptRequest()
}
}
}
}
tryToReAttachButtonHandlersToPreviousDialog()
}
private fun handleAfterInstallationRequest(promptRequest: WebExtensionPromptRequest.AfterInstallation) {
// The install flow in Fenix relies on an [Addon] object so let's convert the (GeckoView)
// extension into a minimal add-on. The missing metadata will be fetched when the user
// opens the add-ons manager.
val addon = Addon.newFromWebExtension(promptRequest.extension)
when (promptRequest) {
is WebExtensionPromptRequest.AfterInstallation.Permissions.Required -> handlePermissionRequest(
addon,
promptRequest,
)
is WebExtensionPromptRequest.AfterInstallation.PostInstallation -> handlePostInstallationRequest(
addon,
)
is WebExtensionPromptRequest.AfterInstallation.Permissions.Optional -> handleOptionalPermissionsRequest(
addon,
promptRequest,
)
}
}
private fun handlePostInstallationRequest(
addon: Addon,
) {
showPostInstallationDialog(addon)
}
private fun handlePermissionRequest(
addon: Addon,
promptRequest: WebExtensionPromptRequest.AfterInstallation.Permissions.Required,
) {
if (hasExistingPermissionDialogFragment()) return
showPermissionDialog(
addon = addon,
promptRequest = promptRequest,
permissions = promptRequest.permissions,
)
}
private fun handleOptionalPermissionsRequest(
addon: Addon,
promptRequest: WebExtensionPromptRequest.AfterInstallation.Permissions.Optional,
) {
val shouldGrantWithoutPrompt = Addon.localizePermissions(promptRequest.permissions, context).isEmpty()
// If we don't have any promptable permissions, just proceed.
if (shouldGrantWithoutPrompt) {
promptRequest.onConfirm(true)
consumePromptRequest()
return
}
showPermissionDialog(
// This is a bit of a hack so that the permission prompt only lists
// the optional permissions that are requested.
addon = addon.copy(permissions = promptRequest.permissions),
promptRequest = promptRequest,
permissions = promptRequest.permissions,
forOptionalPermissions = true,
)
}
private fun showPostInstallationDialog(addon: Addon) {
if (!isInstallationInProgress && !hasExistingAddonPostInstallationDialogFragment()) {
val dialog = AddonInstallationDialogFragment.newInstance(
addon = addon,
onDismissed = {
consumePromptRequest()
},
onConfirmButtonClicked = { _ ->
consumePromptRequest()
},
)
dialog.show(fragmentManager, POST_INSTALLATION_DIALOG_FRAGMENT_TAG)
}
}
/**
* Stops observing the selected session for incoming window requests.
*/
override fun stop() {
scope?.cancel()
}
@VisibleForTesting
internal fun showPermissionDialog(
addon: Addon,
promptRequest: WebExtensionPromptRequest.AfterInstallation.Permissions,
permissions: List<String> = emptyList(),
forOptionalPermissions: Boolean = false,
) {
if (!isInstallationInProgress && !hasExistingPermissionDialogFragment()) {
val dialog = PermissionsDialogFragment.newInstance(
addon = addon,
permissions = permissions,
forOptionalPermissions = forOptionalPermissions,
onPositiveButtonClicked = { _, privateBrowsingAllowed ->
handlePermissions(
promptRequest,
granted = true,
privateBrowsingAllowed = privateBrowsingAllowed,
)
},
onNegativeButtonClicked = {
when (promptRequest) {
is WebExtensionPromptRequest.AfterInstallation.Permissions.Optional -> {
promptRequest.onConfirm(false)
}
is WebExtensionPromptRequest.AfterInstallation.Permissions.Required -> {
promptRequest.onConfirm(PermissionPromptResponse(isPermissionsGranted = false))
}
}
consumePromptRequest()
},
)
dialog.show(
fragmentManager,
PERMISSIONS_DIALOG_FRAGMENT_TAG,
)
}
}
private fun tryToReAttachButtonHandlersToPreviousDialog() {
findPreviousDialogFragment()?.let { dialog ->
dialog.onPositiveButtonClicked = { addon, privateBrowsingAllowed ->
store.state.webExtensionPromptRequest?.let { promptRequest ->
if (promptRequest is WebExtensionPromptRequest.AfterInstallation.Permissions &&
addon.id == promptRequest.extension.id
) {
handlePermissions(
promptRequest,
granted = true,
privateBrowsingAllowed = privateBrowsingAllowed,
)
}
}
}
dialog.onNegativeButtonClicked = {
store.state.webExtensionPromptRequest?.let { promptRequest ->
handlePermissions(
promptRequest,
granted = false,
privateBrowsingAllowed = false,
)
}
}
}
}
private fun handlePermissions(
promptRequest: WebExtensionPromptRequest,
granted: Boolean,
privateBrowsingAllowed: Boolean,
) {
when (promptRequest) {
is WebExtensionPromptRequest.AfterInstallation.Permissions.Optional -> {
promptRequest.onConfirm(granted)
}
is WebExtensionPromptRequest.AfterInstallation.Permissions.Required -> {
val response = PermissionPromptResponse(
isPermissionsGranted = granted,
isPrivateModeGranted = privateBrowsingAllowed,
)
promptRequest.onConfirm(response)
}
is WebExtensionPromptRequest.AfterInstallation.PostInstallation -> {
// opt-out
}
is WebExtensionPromptRequest.BeforeInstallation.InstallationFailed -> {
// opt-out
}
}
consumePromptRequest()
}
private fun consumePromptRequest() {
store.dispatch(WebExtensionAction.ConsumePromptRequestWebExtensionAction)
}
private fun hasExistingPermissionDialogFragment(): Boolean {
return findPreviousDialogFragment() != null
}
private fun findPreviousDialogFragment(): PermissionsDialogFragment? {
return fragmentManager.findFragmentByTag(PERMISSIONS_DIALOG_FRAGMENT_TAG) as? PermissionsDialogFragment
}
private fun hasExistingAddonPostInstallationDialogFragment(): Boolean {
return fragmentManager.findFragmentByTag(POST_INSTALLATION_DIALOG_FRAGMENT_TAG)
as? AddonInstallationDialogFragment != null
}
private fun handleBeforeInstallationRequest(promptRequest: WebExtensionPromptRequest.BeforeInstallation) {
when (promptRequest) {
is WebExtensionPromptRequest.BeforeInstallation.InstallationFailed -> {
handleInstallationFailedRequest(
exception = promptRequest.exception,
)
consumePromptRequest()
}
}
}
@VisibleForTesting
internal fun handleInstallationFailedRequest(
exception: WebExtensionInstallException,
) {
val addonName = exception.extensionName ?: ""
var title = context.getString(MozComp.string.mozac_feature_addons_cant_install_extension, "")
val message = when (exception) {
is WebExtensionInstallException.Blocklisted -> {
context.getString(MozComp.string.mozac_feature_addons_blocklisted_1, addonName)
}
is WebExtensionInstallException.SoftBlocked -> {
context.getString(MozComp.string.mozac_feature_addons_soft_blocked, addonName)
}
is WebExtensionInstallException.UserCancelled -> {
// We don't want to show an error message when users cancel installation.
return
}
is WebExtensionInstallException.UnsupportedAddonType,
is WebExtensionInstallException.Unknown,
-> {
// Making sure we don't have a
// Title = Can't install extension
// Message = Failed to install $addonName
title = ""
if (addonName.isNotEmpty()) {
context.getString(MozComp.string.mozac_feature_addons_failed_to_install, addonName)
} else {
context.getString(MozComp.string.mozac_feature_addons_extension_failed_to_install)
}
}
is WebExtensionInstallException.NetworkFailure -> {
context.getString(MozComp.string.mozac_feature_addons_extension_failed_to_install_network_error)
}
is WebExtensionInstallException.CorruptFile -> {
context.getString(MozComp.string.mozac_feature_addons_extension_failed_to_install_corrupt_error)
}
is WebExtensionInstallException.NotSigned -> {
context.getString(
MozComp.string.mozac_feature_addons_extension_failed_to_install_not_signed_error,
)
}
is WebExtensionInstallException.Incompatible -> {
val appName = "Lensai"
val version = context.appVersionName
context.getString(
MozComp.string.mozac_feature_addons_failed_to_install_incompatible_error,
addonName,
appName,
version,
)
}
is WebExtensionInstallException.AdminInstallOnly -> {
context.getString(MozComp.string.mozac_feature_addons_admin_install_only, addonName)
}
}
showDialog(
title = title,
message = message,
)
}
@VisibleForTesting
internal fun showDialog(
title: String,
message: String,
) {
context.let {
AlertDialog.Builder(it).setTitle(title)
.setPositiveButton(android.R.string.ok) { _, _ -> }.setCancelable(false).setMessage(
message,
).show().withCenterAlignedButtons()
}
}
companion object {
private const val PERMISSIONS_DIALOG_FRAGMENT_TAG = "ADDONS_PERMISSIONS_DIALOG_FRAGMENT"
private const val POST_INSTALLATION_DIALOG_FRAGMENT_TAG =
"ADDONS_INSTALLATION_DIALOG_FRAGMENT"
}
}
@@ -14,6 +14,7 @@ import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.LastAccessAction
import mozilla.components.browser.state.action.ReaderAction
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.action.WebExtensionAction
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.feature.addons.logger
import mozilla.components.lib.state.Middleware
@@ -65,8 +66,11 @@ class FlutterEventMiddleware(private val flutterEvents: GeckoStateEvents) : Midd
) { _ -> }
}
}
is WebExtensionAction.UpdatePromptRequestWebExtensionAction -> {
logger.debug("Event fired: " + action.javaClass.name)
}
else -> {
//logger.debug("Event fired: " + action.javaClass.name)
logger.debug("Event fired: " + action.javaClass.name)
}
}
next(action)
@@ -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/.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="true" android:color="@android:color/black" />
<item android:state_checked="false" android:color="@color/photonGrey40" />
</selector>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M14.5,8c-0.971,0 -1,1 -1.75,1a0.765,0.765 0,0 1,-0.75 -0.75V5a1,1 0,0 0,-1 -1H7.75A0.765,0.765 0,0 1,7 3.25c0,-0.75 1,-0.779 1,-1.75C8,0.635 7.1,0 6,0S4,0.635 4,1.5c0,0.971 1,1 1,1.75a0.765,0.765 0,0 1,-0.75 0.75H1a1,1 0,0 0,-1 1v2.25A0.765,0.765 0,0 0,0.75 8c0.75,0 0.779,-1 1.75,-1C3.365,7 4,7.9 4,9s-0.635,2 -1.5,2c-0.971,0 -1,-1 -1.75,-1a0.765,0.765 0,0 0,-0.75 0.75V15a1,1 0,0 0,1 1h3.25a0.765,0.765 0,0 0,0.75 -0.75c0,-0.75 -1,-0.779 -1,-1.75 0,-0.865 0.9,-1.5 2,-1.5s2,0.635 2,1.5c0,0.971 -1,1 -1,1.75a0.765,0.765 0,0 0,0.75 0.75H11a1,1 0,0 0,1 -1v-3.25a0.765,0.765 0,0 1,0.75 -0.75c0.75,0 0.779,1 1.75,1 0.865,0 1.5,-0.9 1.5,-2s-0.635,-2 -1.5,-2z"
android:fillColor="@android:color/black"/>
</vector>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:autoMirrored="true"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M2,1h20c1.1,0 2,0.9 2,2v18c0,1.1 -0.9,2 -2,2H2c-1.1,0 -2,-0.9 -2,-2V3c0,-1.1 0.9,-2 2,-2z" />
<path
android:fillColor="#FFF"
android:pathData="M12,3h9c0.6,0 1,0.4 1,1v16c0,0.6 -0.4,1 -1,1h-9L12,3zM5.5,12.5l2.7,-3.7c0.2,-0.3 0.6,-0.3 0.8,-0.1l0.7,0.5c0.2,0.2 0.2,0.5 0,0.7L5.8,15c-0.2,0.2 -0.5,0.3 -0.8,0.1l-2.2,-2.2c-0.2,-0.2 -0.2,-0.5 0,-0.7l0.8,-0.8c0.2,-0.2 0.5,-0.2 0.7,0l1.2,1.1z" />
<path
android:fillColor="#FF000000"
android:pathData="M15,9l-1,1 2,2 -2,2 1,1 2,-2 2,2 1,-1 -2,-2 2,-2 -1,-1 -2,2.01L15,9z" />
</vector>
@@ -0,0 +1,156 @@
<?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"
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_author" />
<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_24"
app:tint="@android:color/black" />
<View
android:id="@+id/home_page_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@+id/home_page_label"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:background="@color/photonGrey40"
android:importantForAccessibility="no" />
<TextView
android:id="@+id/rating_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/home_page_divider"
android:text="@string/mozac_feature_addons_rating" />
<RatingBar
android:id="@+id/rating_view"
style="@style/Widget.AppCompat.RatingBar.Small"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:layout_below="@+id/home_page_divider"
android:layout_toStartOf="@+id/users_count"
android:isIndicator="true"
android:numStars="5" />
<TextView
android:id="@+id/users_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/home_page_divider"
android:layout_alignParentEnd="true"
android:layout_marginStart="6dp"
tools:text="591,642" />
</RelativeLayout>
</ScrollView>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="MergeRootFrame" />
@@ -0,0 +1,32 @@
<?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:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".addons.PermissionsDetailsActivity">
<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"
app:drawableEndCompat="@drawable/mozac_ic_link_24"
android:padding="16dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:background="?attr/selectableItemBackground"
android:text="@string/mozac_feature_addons_learn_more"
app:drawableTint="@android:color/black"
android:textColor="@android:color/black" />
</RelativeLayout>
@@ -0,0 +1,11 @@
<?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/addonSettingsContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="MergeRootFrame" />
@@ -0,0 +1,96 @@
<?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">
<androidx.appcompat.widget.SwitchCompat
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:text="@string/mozac_feature_addons_enabled"
android:padding="16dp"
android:textSize="18sp"/>
<TextView
android:id="@+id/settings"
android:layout_width="wrap_content"
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:color/black" />
<TextView
android:id="@+id/details"
android:layout_width="wrap_content"
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_24"
app:drawableTint="@android:color/black" />
<TextView
android:id="@+id/permissions"
android:layout_width="wrap_content"
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" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/allow_in_private_browsing_switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|end"
android:layout_below="@+id/permissions"
android:background="?android:attr/selectableItemBackground"
android:checked="false"
android:clickable="true"
android:focusable="true"
android:text="@string/mozac_feature_addons_settings_allow_in_private_browsing"
android:padding="16dp"
android:textSize="18sp"/>
<Button
android:id="@+id/remove_add_on"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/allow_in_private_browsing_switch"
android:layout_marginTop="16dp"
android:textColor="@color/photonRed50"
android:text="@string/mozac_feature_addons_remove" />
</RelativeLayout>
</ScrollView>
@@ -0,0 +1,31 @@
<?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:id="@+id/add_on_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<TextView
android:id="@+id/permission"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:textColor="@android:color/black"
tools:text="Access your data for all websites" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/photonGrey40"
android:importantForAccessibility="no" />
</LinearLayout>
@@ -0,0 +1,18 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_height="match_parent">
<mozilla.components.concept.engine.EngineView
tools:ignore="Instantiatable"
android:id="@+id/addonSettingsEngineView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -0,0 +1,27 @@
<?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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:visibility="gone"
layout="@layout/overlay_add_on_progress" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -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.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"
xmlns:app="http://schemas.android.com/apk/res-auto">
<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"
app:drawableStartCompat="@drawable/mozac_ic_extensions_black"
android:drawablePadding="8dp"
android:text="@string/mozac_extension_install_progress_caption"/>
</androidx.cardview.widget.CardView>
@@ -65,6 +65,42 @@
</intent-filter>
</activity>
<activity
android:theme="@style/Theme.AppCompat.Light"
android:name="eu.lensai.flutter_mozilla_components.addons.AddonsActivity"
android:label="@string/mozac_feature_addons_addons"
android:exported="false"
android:parentActivityName=".MainActivity" />
<activity
android:theme="@style/Theme.AppCompat.Light"
android:name="eu.lensai.flutter_mozilla_components.addons.AddonDetailsActivity"
android:exported="false"
android:label="@string/mozac_feature_addons_addons" />
<activity android:name="eu.lensai.flutter_mozilla_components.addons.InstalledAddonDetailsActivity"
android:label="@string/mozac_feature_addons_addons"
android:parentActivityName="eu.lensai.flutter_mozilla_components.addons.AddonsActivity"
android:exported="false"
android:theme="@style/Theme.AppCompat.Light" />
<activity
android:name="eu.lensai.flutter_mozilla_components.addons.PermissionsDetailsActivity"
android:label="@string/mozac_feature_addons_addons"
android:exported="false"
android:theme="@style/Theme.AppCompat.Light" />
<activity
android:name="eu.lensai.flutter_mozilla_components.addons.AddonSettingsActivity"
android:label="@string/mozac_feature_addons_addons"
android:exported="false"
android:theme="@style/Theme.AppCompat.Light" />
<activity
android:name="eu.lensai.flutter_mozilla_components.addons.WebExtensionActionPopupActivity"
android:label="@string/mozac_feature_addons_addons"
android:theme="@style/Theme.AppCompat.Light"/>
<service
android:name="eu.lensai.flutter_mozilla_components.DownloadService"
android:foregroundServiceType="dataSync" />