implemented addon support
This commit is contained in:
+27
@@ -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() }
|
||||
|
||||
+5
-1
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
-1
@@ -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?) {
|
||||
|
||||
+107
@@ -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)!!)
|
||||
}
|
||||
}
|
||||
+100
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+149
@@ -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
|
||||
}
|
||||
+24
@@ -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()
|
||||
}
|
||||
}
|
||||
+202
@@ -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()
|
||||
}
|
||||
+93
@@ -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)
|
||||
}
|
||||
}
|
||||
+127
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+365
@@ -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"
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -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)
|
||||
|
||||
+10
@@ -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>
|
||||
+13
@@ -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>
|
||||
+21
@@ -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>
|
||||
|
||||
+156
@@ -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>
|
||||
+11
@@ -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" />
|
||||
|
||||
+32
@@ -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>
|
||||
+11
@@ -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" />
|
||||
+96
@@ -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>
|
||||
+31
@@ -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>
|
||||
+18
@@ -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>
|
||||
+24
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user