new extension management
This commit is contained in:
+104
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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 https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package eu.weblibre.flutter_mozilla_components
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import eu.weblibre.flutter_mozilla_components.addons.FlutterAddonSettingsFragment
|
||||
import io.flutter.plugin.common.StandardMessageCodec
|
||||
import io.flutter.plugin.platform.PlatformView
|
||||
import io.flutter.plugin.platform.PlatformViewFactory
|
||||
|
||||
private const val OPTIONS_PAGE_URL_KEY = "optionsPageUrl"
|
||||
|
||||
class AddonSettingsViewFactory(
|
||||
private val activityProvider: () -> Activity?,
|
||||
) : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
|
||||
override fun create(context: Context?, id: Int, args: Any?): PlatformView {
|
||||
val activity = activityProvider()
|
||||
?: throw IllegalStateException("No activity available when creating AddonSettingsView")
|
||||
val optionsPageUrl = (args as? Map<*, *>)?.get(OPTIONS_PAGE_URL_KEY) as? String
|
||||
?: throw IllegalArgumentException("Missing optionsPageUrl creation param")
|
||||
|
||||
return NativeAddonSettingsView(activity, optionsPageUrl)
|
||||
}
|
||||
}
|
||||
|
||||
private class NativeAddonSettingsView(
|
||||
activity: Activity,
|
||||
private val optionsPageUrl: String,
|
||||
) : PlatformView {
|
||||
private val fragmentActivity = activity as? FragmentActivity
|
||||
?: throw IllegalStateException("Addon settings view requires a FragmentActivity host")
|
||||
private val containerId = View.generateViewId()
|
||||
private val fragmentTag = "addon_settings_$containerId"
|
||||
private val container: FrameLayout = FrameLayout(activity).apply {
|
||||
id = containerId
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
)
|
||||
}
|
||||
private val attachStateListener = object : View.OnAttachStateChangeListener {
|
||||
override fun onViewAttachedToWindow(view: View) {
|
||||
container.removeOnAttachStateChangeListener(this)
|
||||
container.post { attachFragment() }
|
||||
}
|
||||
|
||||
override fun onViewDetachedFromWindow(view: View) = Unit
|
||||
}
|
||||
|
||||
override fun onFlutterViewAttached(flutterView: View) {
|
||||
super.onFlutterViewAttached(flutterView)
|
||||
|
||||
if (container.isAttachedToWindow) {
|
||||
container.post { attachFragment() }
|
||||
} else {
|
||||
container.removeOnAttachStateChangeListener(attachStateListener)
|
||||
container.addOnAttachStateChangeListener(attachStateListener)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getView(): View = container
|
||||
|
||||
override fun dispose() {
|
||||
val fm = fragmentActivity.supportFragmentManager
|
||||
if (!fragmentActivity.isFinishing && !fragmentActivity.isDestroyed && !fm.isStateSaved) {
|
||||
fm.findFragmentByTag(fragmentTag)?.let { fragment ->
|
||||
fm.beginTransaction().remove(fragment).commitNowAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun attachFragment() {
|
||||
if (fragmentActivity.isFinishing || fragmentActivity.isDestroyed) {
|
||||
return
|
||||
}
|
||||
|
||||
val fm = fragmentActivity.supportFragmentManager
|
||||
if (fm.isStateSaved) {
|
||||
return
|
||||
}
|
||||
|
||||
if (fragmentActivity.findViewById<View>(containerId) == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val existingFragment = fm.findFragmentByTag(fragmentTag)
|
||||
if (existingFragment is FlutterAddonSettingsFragment) {
|
||||
return
|
||||
}
|
||||
|
||||
fm.beginTransaction()
|
||||
.replace(containerId, FlutterAddonSettingsFragment.create(optionsPageUrl), fragmentTag)
|
||||
.commitNow()
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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 https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package eu.weblibre.flutter_mozilla_components
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import eu.weblibre.flutter_mozilla_components.addons.FlutterAddonPopupFragment
|
||||
import io.flutter.plugin.common.StandardMessageCodec
|
||||
import io.flutter.plugin.platform.PlatformView
|
||||
import io.flutter.plugin.platform.PlatformViewFactory
|
||||
|
||||
private const val EXTENSION_ID_KEY = "extensionId"
|
||||
|
||||
class AddonPopupViewFactory(
|
||||
private val activityProvider: () -> Activity?,
|
||||
) : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
|
||||
override fun create(context: Context?, id: Int, args: Any?): PlatformView {
|
||||
val activity = activityProvider()
|
||||
?: throw IllegalStateException("No activity available when creating AddonPopupView")
|
||||
val extensionId = (args as? Map<*, *>)?.get(EXTENSION_ID_KEY) as? String
|
||||
?: throw IllegalArgumentException("Missing extensionId creation param")
|
||||
|
||||
return NativeAddonPopupView(activity, extensionId)
|
||||
}
|
||||
}
|
||||
|
||||
private class NativeAddonPopupView(
|
||||
activity: Activity,
|
||||
private val extensionId: String,
|
||||
) : PlatformView {
|
||||
private val fragmentActivity = activity as? FragmentActivity
|
||||
?: throw IllegalStateException("Addon popup view requires a FragmentActivity host")
|
||||
private val containerId = View.generateViewId()
|
||||
private val fragmentTag = "addon_popup_$containerId"
|
||||
private val container: FrameLayout = FrameLayout(activity).apply {
|
||||
id = containerId
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
)
|
||||
}
|
||||
private val attachStateListener = object : View.OnAttachStateChangeListener {
|
||||
override fun onViewAttachedToWindow(view: View) {
|
||||
container.removeOnAttachStateChangeListener(this)
|
||||
container.post { attachFragment() }
|
||||
}
|
||||
|
||||
override fun onViewDetachedFromWindow(view: View) = Unit
|
||||
}
|
||||
|
||||
override fun onFlutterViewAttached(flutterView: View) {
|
||||
super.onFlutterViewAttached(flutterView)
|
||||
|
||||
if (container.isAttachedToWindow) {
|
||||
container.post { attachFragment() }
|
||||
} else {
|
||||
container.removeOnAttachStateChangeListener(attachStateListener)
|
||||
container.addOnAttachStateChangeListener(attachStateListener)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getView(): View = container
|
||||
|
||||
override fun dispose() {
|
||||
val fm = fragmentActivity.supportFragmentManager
|
||||
if (!fragmentActivity.isFinishing && !fragmentActivity.isDestroyed && !fm.isStateSaved) {
|
||||
fm.findFragmentByTag(fragmentTag)?.let { fragment ->
|
||||
fm.beginTransaction().remove(fragment).commitNowAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun attachFragment() {
|
||||
if (fragmentActivity.isFinishing || fragmentActivity.isDestroyed) {
|
||||
return
|
||||
}
|
||||
|
||||
val fm = fragmentActivity.supportFragmentManager
|
||||
if (fm.isStateSaved) {
|
||||
return
|
||||
}
|
||||
|
||||
if (fragmentActivity.findViewById<View>(containerId) == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val existingFragment = fm.findFragmentByTag(fragmentTag)
|
||||
if (existingFragment is FlutterAddonPopupFragment) {
|
||||
return
|
||||
}
|
||||
|
||||
fm.beginTransaction()
|
||||
.replace(containerId, FlutterAddonPopupFragment.create(extensionId), fragmentTag)
|
||||
.commitNow()
|
||||
}
|
||||
}
|
||||
+4
-9
@@ -22,7 +22,6 @@ import androidx.annotation.CallSuper
|
||||
import androidx.core.content.edit
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.preference.PreferenceManager
|
||||
import eu.weblibre.flutter_mozilla_components.addons.WebExtensionActionPopupActivity
|
||||
import eu.weblibre.flutter_mozilla_components.addons.WebExtensionPromptFeature
|
||||
import eu.weblibre.flutter_mozilla_components.databinding.FragmentBrowserBinding
|
||||
import eu.weblibre.flutter_mozilla_components.ext.getPreferenceKey
|
||||
@@ -555,14 +554,10 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
|
||||
protected open fun onEngineSetupComplete() {}
|
||||
|
||||
private fun openPopup(webExtensionState: WebExtensionState) {
|
||||
val intent = Intent(
|
||||
components.profileApplicationContext,
|
||||
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)
|
||||
components.addonEvents.onWebExtensionPopupRequested(
|
||||
webExtensionState.id,
|
||||
webExtensionState.name ?: "",
|
||||
) {}
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
|
||||
+19
-2
@@ -21,6 +21,7 @@ import eu.weblibre.flutter_mozilla_components.pigeons.GeckoTabContentEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoViewportEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.QueryParameterStripping
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.ReaderViewController
|
||||
import eu.weblibre.flutter_mozilla_components.addons.AddonPrefs
|
||||
import eu.weblibre.flutter_mozilla_components.api.GeckoViewportApiImpl
|
||||
import eu.weblibre.flutter_mozilla_components.api.GeckoEngineSettingsApiImpl
|
||||
import eu.weblibre.flutter_mozilla_components.feature.DefaultSelectionActionDelegate
|
||||
@@ -49,7 +50,7 @@ import java.util.concurrent.TimeUnit
|
||||
private const val HISTORY_METADATA_MAX_AGE_IN_MS = 14L * 24 * 60 * 60 * 1000 // 14 days
|
||||
private const val DEFAULT_QUERY_PARAMETER_STRIPPING_STRIP_LIST =
|
||||
"__hsfp __hssc __hstc __s _bhlid _branch_match_id _branch_referrer _gl _hsenc _kx _openstat at_recipient_id at_recipient_list bbeml bsft_clkid bsft_uid dclid et_rid fb_action_ids fb_comment_id fbclid gbraid gclid guce_referrer guce_referrer_sig hsCtaTracking igshid irclickid mc_eid mkt_tok ml_subscriber ml_subscriber_hash msclkid mtm_cid oft_c oft_ck oft_d oft_id oft_ids oft_k oft_lk oft_sk oly_anon_id oly_enc_id pk_cid rb_clickid s_cid sc_customer sc_eh sc_uid sms_click sms_source sms_uph srsltid ss_email_id syclid ttclid twclid unicorn_click_id vero_conv vero_id vgo_ee wbraid wickedid yclid ymclid ysclid"
|
||||
|
||||
|
||||
object GlobalComponents {
|
||||
private var _components: Components? = null
|
||||
private var currentMode: ComponentsMode? = null
|
||||
@@ -268,7 +269,23 @@ object GlobalComponents {
|
||||
},
|
||||
onUpdatePermissionRequest = newComponents.core.addonUpdater::onUpdatePermissionRequest,
|
||||
onExtensionsLoaded = { extensions ->
|
||||
newComponents.core.addonUpdater.registerForFutureUpdates(extensions)
|
||||
val addonPrefs = AddonPrefs.get(applicationContext)
|
||||
val autoUpdateEnabled =
|
||||
addonPrefs.getBoolean(AddonPrefs.PREF_AUTO_UPDATE_ENABLED, true)
|
||||
val autoUpdateDisabledAddonIds =
|
||||
addonPrefs.getStringSet(AddonPrefs.PREF_AUTO_UPDATE_DISABLED_IDS, emptySet())
|
||||
?: emptySet()
|
||||
val localFileAddonIds =
|
||||
addonPrefs.getStringSet(AddonPrefs.PREF_LOCAL_FILE_ADDON_IDS, emptySet())
|
||||
?: emptySet()
|
||||
if (autoUpdateEnabled) {
|
||||
newComponents.core.addonUpdater.registerForFutureUpdates(
|
||||
extensions.filterNot { extension ->
|
||||
autoUpdateDisabledAddonIds.contains(extension.id) ||
|
||||
localFileAddonIds.contains(extension.id)
|
||||
},
|
||||
)
|
||||
}
|
||||
newComponents.core.supportedAddonsChecker.registerForChecks()
|
||||
},
|
||||
)
|
||||
|
||||
-107
@@ -1,107 +0,0 @@
|
||||
/* 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.weblibre.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.weblibre.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)!!)
|
||||
}
|
||||
}
|
||||
-110
@@ -1,110 +0,0 @@
|
||||
/* 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.weblibre.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.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import eu.weblibre.flutter_mozilla_components.GlobalComponents
|
||||
import eu.weblibre.flutter_mozilla_components.R
|
||||
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
|
||||
|
||||
/**
|
||||
* An activity to show the internal settings of an add-on with [EngineView].
|
||||
*
|
||||
* Used when the addon's manifest specifies `openOptionsPageInTab = false`,
|
||||
* rendering the settings page inside an [AddonPopupBaseFragment] with proper
|
||||
* prompt and download support.
|
||||
*/
|
||||
class AddonInternalSettingsActivity : AppCompatActivity() {
|
||||
private val components by lazy {
|
||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_add_on_settings)
|
||||
|
||||
val addon = requireNotNull(
|
||||
intent.getParcelableExtra<Addon>("add_on"),
|
||||
)
|
||||
|
||||
title = addon.translateName(this)
|
||||
|
||||
val fragment = AddonInternalSettingsFragment.create(addon)
|
||||
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.addonSettingsContainer, fragment)
|
||||
.commit()
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (!fragment.onBackPressed()) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? =
|
||||
when (name) {
|
||||
EngineView::class.java.name -> components.core.engine.createView(context, attrs).asView()
|
||||
else -> super.onCreateView(parent, name, context, attrs)
|
||||
}
|
||||
|
||||
/**
|
||||
* A fragment to show the internal settings of an add-on with [EngineView].
|
||||
*
|
||||
* Creates a fresh engine session and loads the addon's options page URL into it.
|
||||
*/
|
||||
class AddonInternalSettingsFragment : AddonPopupBaseFragment() {
|
||||
|
||||
private val addonSettingsEngineView: EngineView
|
||||
get() = requireView().findViewById<View>(R.id.addonSettingsEngineView) as EngineView
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
initializeSession()
|
||||
return inflater.inflate(R.layout.fragment_add_on_settings, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val optionsPageUrl = arguments?.getParcelableCompat("add_on", Addon::class.java)
|
||||
?.installedState?.optionsPageUrl
|
||||
|
||||
if (optionsPageUrl != null) {
|
||||
engineSession?.let { session ->
|
||||
addonSettingsEngineView.render(session)
|
||||
session.loadUrl(optionsPageUrl)
|
||||
}
|
||||
} else {
|
||||
activity?.finish()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(addon: Addon) = AddonInternalSettingsFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable("add_on", addon)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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 https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package eu.weblibre.flutter_mozilla_components.addons
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
|
||||
object AddonPrefs {
|
||||
const val PREFS_NAME = "addon_prefs"
|
||||
const val PREF_AUTO_UPDATE_ENABLED = "addon_auto_update_enabled"
|
||||
// Legacy key name — kept to preserve previously pinned local-install preferences.
|
||||
const val PREF_AUTO_UPDATE_DISABLED_IDS = "pinned_local_addon_ids"
|
||||
const val PREF_LOCAL_FILE_ADDON_IDS = "local_file_addon_ids"
|
||||
const val PREF_MANUAL_UPDATE_ATTEMPT_PREFIX = "manual_addon_update_attempt."
|
||||
|
||||
fun get(context: Context): SharedPreferences =
|
||||
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
}
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
/* 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.weblibre.flutter_mozilla_components.addons
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.addCallback
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import eu.weblibre.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)
|
||||
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
finishAndRemoveTask()
|
||||
}
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
supportFragmentManager.beginTransaction().apply {
|
||||
replace(R.id.container, AddonsFragment())
|
||||
commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
return true
|
||||
}
|
||||
}
|
||||
-155
@@ -1,155 +0,0 @@
|
||||
/* 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.weblibre.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.weblibre.flutter_mozilla_components.Components
|
||||
import eu.weblibre.flutter_mozilla_components.GlobalComponents
|
||||
import eu.weblibre.flutter_mozilla_components.ProfileContext
|
||||
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.weblibre.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 by lazy {
|
||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||
}
|
||||
|
||||
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.core.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) {
|
||||
val profileContext = ProfileContext(requireContext(), components.profileApplicationContext.relativePath)
|
||||
|
||||
recyclerView = rootView.findViewById(R.id.add_ons_list)
|
||||
recyclerView.layoutManager = LinearLayoutManager(profileContext)
|
||||
|
||||
scope.launch {
|
||||
try {
|
||||
addons = components.core.addonManager.getAddons()
|
||||
|
||||
scope.launch(Dispatchers.Main) {
|
||||
adapter = AddonsManagerAdapter(
|
||||
this@AddonsFragment,
|
||||
addons,
|
||||
store = components.core.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.core.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
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
/* 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.weblibre.flutter_mozilla_components.addons
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import eu.weblibre.flutter_mozilla_components.ProfileContext
|
||||
import mozilla.components.browser.state.action.WebExtensionAction
|
||||
import mozilla.components.concept.engine.EngineSession
|
||||
import mozilla.components.concept.engine.EngineView
|
||||
import mozilla.components.lib.state.ext.consumeFrom
|
||||
import mozilla.components.support.locale.ActivityContextWrapper
|
||||
|
||||
class FlutterAddonPopupFragment : AddonPopupBaseFragment(), EngineSession.Observer {
|
||||
private var addonPopupEngineView: EngineView? = null
|
||||
private var sessionConsumed
|
||||
get() = arguments?.getBoolean("isSessionConsumed", false) ?: false
|
||||
set(value) {
|
||||
arguments?.putBoolean("isSessionConsumed", value)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: android.view.LayoutInflater,
|
||||
container: android.view.ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View {
|
||||
val extensionId = requireNotNull(arguments?.getString(ARG_EXTENSION_ID))
|
||||
|
||||
components.core.store.state.extensions[extensionId]?.popupSession?.let {
|
||||
initializeSession(it)
|
||||
}
|
||||
|
||||
val profileContext = ProfileContext(
|
||||
requireContext(),
|
||||
components.profileApplicationContext.relativePath,
|
||||
)
|
||||
val engineView = components.core.engine.createView(profileContext, null)
|
||||
addonPopupEngineView = engineView
|
||||
|
||||
val originalContext =
|
||||
ActivityContextWrapper.getOriginalContext(requireActivity()) ?: requireActivity()
|
||||
engineView.setActivityContext(originalContext)
|
||||
|
||||
val root = FrameLayout(profileContext)
|
||||
val nativeView = engineView.asView()
|
||||
nativeView.layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
)
|
||||
root.addView(nativeView)
|
||||
return root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val extensionId = requireNotNull(arguments?.getString(ARG_EXTENSION_ID))
|
||||
val currentSession = engineSession
|
||||
|
||||
if (currentSession != null) {
|
||||
addonPopupEngineView?.render(currentSession)
|
||||
consumePopupSession(extensionId)
|
||||
} else {
|
||||
consumeFrom(components.core.store) { state ->
|
||||
state.extensions[extensionId]?.let { extState ->
|
||||
val popupSession = extState.popupSession
|
||||
if (popupSession != null) {
|
||||
initializeSession(popupSession)
|
||||
addonPopupEngineView?.render(popupSession)
|
||||
popupSession.register(this)
|
||||
consumePopupSession(extensionId)
|
||||
engineSession = popupSession
|
||||
} else if (sessionConsumed) {
|
||||
activity?.onBackPressedDispatcher?.onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
addonPopupEngineView?.setActivityContext(null)
|
||||
addonPopupEngineView = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun consumePopupSession(extensionId: String) {
|
||||
components.core.store.dispatch(
|
||||
WebExtensionAction.UpdatePopupSessionAction(extensionId, popupSession = null),
|
||||
)
|
||||
sessionConsumed = true
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARG_EXTENSION_ID = "extension_id"
|
||||
|
||||
fun create(extensionId: String) = FlutterAddonPopupFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(ARG_EXTENSION_ID, extensionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/* 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.weblibre.flutter_mozilla_components.addons
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import eu.weblibre.flutter_mozilla_components.ProfileContext
|
||||
import mozilla.components.concept.engine.EngineView
|
||||
import mozilla.components.support.locale.ActivityContextWrapper
|
||||
|
||||
class FlutterAddonSettingsFragment : AddonPopupBaseFragment() {
|
||||
private var addonSettingsEngineView: EngineView? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: android.view.LayoutInflater,
|
||||
container: android.view.ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View {
|
||||
initializeSession()
|
||||
|
||||
val profileContext = ProfileContext(
|
||||
requireContext(),
|
||||
components.profileApplicationContext.relativePath,
|
||||
)
|
||||
val engineView = components.core.engine.createView(profileContext, null)
|
||||
addonSettingsEngineView = engineView
|
||||
|
||||
val originalContext =
|
||||
ActivityContextWrapper.getOriginalContext(requireActivity()) ?: requireActivity()
|
||||
engineView.setActivityContext(originalContext)
|
||||
|
||||
val root = FrameLayout(profileContext)
|
||||
val nativeView = engineView.asView()
|
||||
nativeView.layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
)
|
||||
root.addView(nativeView)
|
||||
return root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val optionsPageUrl = arguments?.getString(ARG_OPTIONS_PAGE_URL)
|
||||
|
||||
if (optionsPageUrl != null) {
|
||||
engineSession?.let { session ->
|
||||
addonSettingsEngineView?.render(session)
|
||||
session.loadUrl(optionsPageUrl)
|
||||
}
|
||||
} else {
|
||||
activity?.onBackPressedDispatcher?.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
addonSettingsEngineView?.setActivityContext(null)
|
||||
addonSettingsEngineView = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARG_OPTIONS_PAGE_URL = "options_page_url"
|
||||
|
||||
fun create(optionsPageUrl: String) = FlutterAddonSettingsFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(ARG_OPTIONS_PAGE_URL, optionsPageUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-221
@@ -1,221 +0,0 @@
|
||||
/* 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.weblibre.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.weblibre.flutter_mozilla_components.Components
|
||||
import eu.weblibre.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.weblibre.flutter_mozilla_components.R
|
||||
|
||||
/**
|
||||
* An activity to show the details of a installed add-on.
|
||||
*/
|
||||
class InstalledAddonDetailsActivity : AppCompatActivity() {
|
||||
private val components by lazy {
|
||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun bindAddon(addon: Addon) {
|
||||
scope.launch {
|
||||
try {
|
||||
val addons = components.core.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,
|
||||
R.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.core.addonManager.enableAddon(
|
||||
addon,
|
||||
onSuccess = {
|
||||
switch.setState(true)
|
||||
Toast.makeText(
|
||||
this,
|
||||
getString(R.string.mozac_feature_addons_successfully_enabled, addon.translateName(this)),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
},
|
||||
onError = {
|
||||
Toast.makeText(
|
||||
this,
|
||||
getString(R.string.mozac_feature_addons_failed_to_enable, addon.translateName(this)),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
},
|
||||
)
|
||||
} else {
|
||||
components.core.addonManager.disableAddon(
|
||||
addon,
|
||||
onSuccess = {
|
||||
switch.setState(false)
|
||||
Toast.makeText(
|
||||
this,
|
||||
getString(R.string.mozac_feature_addons_successfully_disabled, addon.translateName(this)),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
},
|
||||
onError = {
|
||||
Toast.makeText(
|
||||
this,
|
||||
getString(R.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 optionsPageUrl = addon.installedState?.optionsPageUrl ?: return@setOnClickListener
|
||||
if (addon.installedState?.openOptionsPageInTab == true) {
|
||||
// Open settings in a browser tab, reusing an existing tab if already open.
|
||||
components.useCases.tabsUseCases.selectOrAddTab(
|
||||
url = optionsPageUrl,
|
||||
ignoreFragment = true,
|
||||
)
|
||||
val intent = packageManager.getLaunchIntentForPackage(packageName)
|
||||
intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
startActivity(intent)
|
||||
} else {
|
||||
// Open settings in an internal view with proper extension API support.
|
||||
val intent = Intent(this, AddonInternalSettingsActivity::class.java)
|
||||
intent.putExtra("add_on", addon)
|
||||
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.core.addonManager.setAddonAllowedInPrivateBrowsing(
|
||||
addon,
|
||||
isChecked,
|
||||
onSuccess = {
|
||||
switch.isChecked = isChecked
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindRemoveButton(addon: Addon) {
|
||||
findViewById<View>(R.id.remove_add_on).setOnClickListener {
|
||||
components.core.addonManager.uninstallAddon(
|
||||
addon,
|
||||
onSuccess = {
|
||||
Toast.makeText(
|
||||
this,
|
||||
getString(R.string.mozac_feature_addons_successfully_uninstalled, addon.translateName(this)),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
finish()
|
||||
},
|
||||
onError = { _, _ ->
|
||||
Toast.makeText(
|
||||
this,
|
||||
getString(R.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()
|
||||
}
|
||||
-106
@@ -1,106 +0,0 @@
|
||||
/* 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.weblibre.flutter_mozilla_components.addons
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.net.toUri
|
||||
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.ktx.android.view.setupPersistentInsets
|
||||
import mozilla.components.support.utils.ext.getParcelableExtraCompat
|
||||
import eu.weblibre.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?) {
|
||||
enableEdgeToEdge(SystemBarStyle.dark(Color.TRANSPARENT))
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_add_on_permissions)
|
||||
window.setupPersistentInsets()
|
||||
|
||||
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(LEARN_MORE_URL.toUri())
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
+730
-65
@@ -7,102 +7,767 @@
|
||||
package eu.weblibre.flutter_mozilla_components.api
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import eu.weblibre.flutter_mozilla_components.GlobalComponents
|
||||
import eu.weblibre.flutter_mozilla_components.addons.AddonInternalSettingsActivity
|
||||
import eu.weblibre.flutter_mozilla_components.addons.AddonsActivity
|
||||
import eu.weblibre.flutter_mozilla_components.addons.AddonPrefs
|
||||
import eu.weblibre.flutter_mozilla_components.ext.toWebPBytes
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.AddonDisabledReason
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.AddonIncognito
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.AddonInfo
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.AddonStoreInfo
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.AddonUpdateAttemptInfo
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.AddonUpdateStatus
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoAddonsApi
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.WebExtensionActionType
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import mozilla.components.concept.fetch.MutableHeaders
|
||||
import mozilla.components.concept.fetch.Request
|
||||
import mozilla.components.concept.engine.webextension.InstallationMethod
|
||||
import mozilla.components.concept.engine.webextension.WebExtensionInstallException
|
||||
import mozilla.components.feature.addons.Addon
|
||||
import mozilla.components.feature.addons.update.AddonUpdater
|
||||
import mozilla.components.feature.addons.update.DefaultAddonUpdater
|
||||
import mozilla.components.feature.addons.ui.displayName
|
||||
import mozilla.components.feature.addons.ui.summary
|
||||
import mozilla.components.feature.addons.ui.translateDescription
|
||||
import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_POSTPONED
|
||||
import org.json.JSONObject
|
||||
|
||||
class GeckoAddonsApiImpl(private val context: Context) : GeckoAddonsApi {
|
||||
private val components by lazy {
|
||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||
}
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
override fun startAddonManagerActivity() {
|
||||
val intent = Intent(context, AddonsActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
context.startActivity(intent)
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private val updateAttemptStorage by lazy {
|
||||
DefaultAddonUpdater.UpdateAttemptStorage(context.applicationContext)
|
||||
}
|
||||
private val prefs by lazy {
|
||||
AddonPrefs.get(context.applicationContext)
|
||||
}
|
||||
|
||||
override fun startAddonSettingsActivity(extensionId: String) {
|
||||
companion object {
|
||||
private const val LOCAL_ADDON_UPDATE_SOURCE_MISSING_MESSAGE =
|
||||
"No remote update source is available for this locally installed extension."
|
||||
private const val LOCAL_ADDON_UPDATE_POSTPONED_MESSAGE =
|
||||
"Update downloaded and will be applied after restarting the app."
|
||||
private const val DEFAULT_AMO_SERVER_URL = "https://addons.mozilla.org"
|
||||
private const val PERIODIC_UPDATE_RESTORE_DELAY_MS = 10_000L
|
||||
}
|
||||
|
||||
override fun getAddons(allowCache: Boolean, callback: (Result<List<AddonInfo>>) -> Unit) {
|
||||
scope.launch {
|
||||
val addon = runCatching {
|
||||
components.core.addonManager.getAddons()
|
||||
.find { it.id == extensionId }
|
||||
}.getOrNull()
|
||||
|
||||
if (addon == null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
startAddonManagerActivity()
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
val optionsPageUrl = addon.installedState?.optionsPageUrl
|
||||
if (optionsPageUrl.isNullOrEmpty()) {
|
||||
withContext(Dispatchers.Main) {
|
||||
startAddonManagerActivity()
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
if (addon.installedState?.openOptionsPageInTab == true) {
|
||||
components.useCases.tabsUseCases.selectOrAddTab(
|
||||
url = optionsPageUrl,
|
||||
ignoreFragment = true,
|
||||
)
|
||||
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
|
||||
launchIntent?.addFlags(
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TOP or
|
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP,
|
||||
)
|
||||
if (launchIntent != null) {
|
||||
context.startActivity(launchIntent)
|
||||
} else {
|
||||
startAddonManagerActivity()
|
||||
runCatching {
|
||||
components.core.addonManager.getAddons(allowCache = allowCache)
|
||||
.map { addon ->
|
||||
addon.toPigeon(
|
||||
context = context,
|
||||
isAutoUpdateEnabled = isAutoUpdateEffectivelyEnabledForAddon(addon.id),
|
||||
isLocalFileInstalled = isLocalFileInstalledAddon(addon.id),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val intent = Intent(context, AddonInternalSettingsActivity::class.java)
|
||||
intent.putExtra("add_on", addon)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
}.fold(
|
||||
onSuccess = { callback(Result.success(it)) },
|
||||
onFailure = { callback(Result.failure(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAddonById(
|
||||
addonId: String,
|
||||
allowCache: Boolean,
|
||||
callback: (Result<AddonInfo?>) -> Unit,
|
||||
) {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
val installedAddon = components.core.addonManager.getAddonByID(addonId)
|
||||
(installedAddon ?: components.core.addonManager.getAddons(allowCache = allowCache)
|
||||
.find { it.id == addonId })?.toPigeon(
|
||||
context = context,
|
||||
isAutoUpdateEnabled = isAutoUpdateEffectivelyEnabledForAddon(addonId),
|
||||
isLocalFileInstalled = isLocalFileInstalledAddon(addonId),
|
||||
)
|
||||
}.fold(
|
||||
onSuccess = { callback(Result.success(it)) },
|
||||
onFailure = { callback(Result.failure(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAddonStoreInfo(addonId: String, callback: (Result<AddonStoreInfo?>) -> Unit) {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
fetchAddonStoreInfo(addonId)
|
||||
}.fold(
|
||||
onSuccess = { callback(Result.success(it)) },
|
||||
onFailure = { callback(Result.failure(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun invokeAddonAction(extensionId: String, actionType: WebExtensionActionType) {
|
||||
when(actionType) {
|
||||
WebExtensionActionType.BROWSER -> components.features.webExtensionToolbarFeature.invokeAddonBrowserAction(extensionId)
|
||||
WebExtensionActionType.PAGE -> components.features.webExtensionToolbarFeature.invokeAddonPageAction(extensionId)
|
||||
scope.launch {
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
when(actionType) {
|
||||
WebExtensionActionType.BROWSER -> components.features.webExtensionToolbarFeature.invokeAddonBrowserAction(extensionId)
|
||||
WebExtensionActionType.PAGE -> components.features.webExtensionToolbarFeature.invokeAddonPageAction(extensionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun enableAddon(addonId: String, callback: (Result<AddonInfo>) -> Unit) {
|
||||
withInstalledAddon(addonId, callback) { addon, result ->
|
||||
components.core.addonManager.enableAddon(
|
||||
addon,
|
||||
onSuccess = { updatedAddon ->
|
||||
result(
|
||||
Result.success(
|
||||
updatedAddon.toPigeon(
|
||||
context = context,
|
||||
isAutoUpdateEnabled = isAutoUpdateEffectivelyEnabledForAddon(updatedAddon.id),
|
||||
isLocalFileInstalled = isLocalFileInstalledAddon(updatedAddon.id),
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
onError = { throwable ->
|
||||
result(Result.failure(throwable))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun disableAddon(addonId: String, callback: (Result<AddonInfo>) -> Unit) {
|
||||
withInstalledAddon(addonId, callback) { addon, result ->
|
||||
components.core.addonManager.disableAddon(
|
||||
addon,
|
||||
onSuccess = { updatedAddon ->
|
||||
result(
|
||||
Result.success(
|
||||
updatedAddon.toPigeon(
|
||||
context = context,
|
||||
isAutoUpdateEnabled = isAutoUpdateEffectivelyEnabledForAddon(updatedAddon.id),
|
||||
isLocalFileInstalled = isLocalFileInstalledAddon(updatedAddon.id),
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
onError = { throwable ->
|
||||
result(Result.failure(throwable))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setAddonAllowedInPrivateBrowsing(
|
||||
addonId: String,
|
||||
allowed: Boolean,
|
||||
callback: (Result<AddonInfo>) -> Unit,
|
||||
) {
|
||||
withInstalledAddon(addonId, callback) { addon, result ->
|
||||
components.core.addonManager.setAddonAllowedInPrivateBrowsing(
|
||||
addon,
|
||||
allowed,
|
||||
onSuccess = { updatedAddon ->
|
||||
result(
|
||||
Result.success(
|
||||
updatedAddon.toPigeon(
|
||||
context = context,
|
||||
isAutoUpdateEnabled = isAutoUpdateEffectivelyEnabledForAddon(updatedAddon.id),
|
||||
isLocalFileInstalled = isLocalFileInstalledAddon(updatedAddon.id),
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
onError = { throwable ->
|
||||
result(Result.failure(throwable))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setAddonAutoUpdateEnabledForAddon(
|
||||
addonId: String,
|
||||
enabled: Boolean,
|
||||
callback: (Result<AddonInfo>) -> Unit,
|
||||
) {
|
||||
withInstalledAddon(addonId, callback) { addon, result ->
|
||||
if (enabled && isLocalFileInstalledAddon(addon.id)) {
|
||||
result(
|
||||
Result.success(
|
||||
addon.toPigeon(
|
||||
context = context,
|
||||
isAutoUpdateEnabled = false,
|
||||
isLocalFileInstalled = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
return@withInstalledAddon
|
||||
}
|
||||
|
||||
setAddonAutoUpdateEnabledForAddonInternal(addon.id, enabled)
|
||||
result(
|
||||
Result.success(
|
||||
addon.toPigeon(
|
||||
context = context,
|
||||
isAutoUpdateEnabled = isAutoUpdateEffectivelyEnabledForAddon(addon.id),
|
||||
isLocalFileInstalled = isLocalFileInstalledAddon(addon.id),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun uninstallAddon(addonId: String, callback: (Result<Unit>) -> Unit) {
|
||||
withInstalledAddon(addonId, callback) { addon, result ->
|
||||
components.core.addonManager.uninstallAddon(
|
||||
addon,
|
||||
onSuccess = {
|
||||
clearAddonAutoUpdatePreference(addon.id)
|
||||
clearLocalFileInstalledAddon(addon.id)
|
||||
clearManualUpdateAttempt(addon.id)
|
||||
result(Result.success(Unit))
|
||||
},
|
||||
onError = { _, throwable ->
|
||||
result(Result.failure(throwable))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun triggerAddonUpdate(
|
||||
addonId: String,
|
||||
callback: (Result<AddonUpdateAttemptInfo?>) -> Unit,
|
||||
) {
|
||||
if (isLocalFileInstalledAddon(addonId)) {
|
||||
runLocalFileAddonUpdate(addonId, callback)
|
||||
return
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
runManagedAddonUpdate(addonId) { attempt ->
|
||||
callback(Result.success(attempt))
|
||||
}
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
callback(Result.failure(throwable))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun triggerAllAddonUpdates(callback: (Result<Unit>) -> Unit) {
|
||||
scope.launch {
|
||||
try {
|
||||
val addons = components.core.addonManager.getAddons()
|
||||
.filter { it.isInstalled() && it.isSupported() }
|
||||
.filter { addon -> isAddonAutoUpdateEnabledForAddon(addon.id) }
|
||||
.filterNot { addon -> isLocalFileInstalledAddon(addon.id) }
|
||||
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
addons.forEach { addon ->
|
||||
scheduleManagedAddonUpdate(addon.id)
|
||||
}
|
||||
}
|
||||
|
||||
callback(Result.success(Unit))
|
||||
} catch (throwable: Throwable) {
|
||||
callback(Result.failure(throwable))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLastAddonUpdateAttempt(
|
||||
addonId: String,
|
||||
callback: (Result<AddonUpdateAttemptInfo?>) -> Unit,
|
||||
) {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
val updaterAttempt = updateAttemptStorage.findUpdateAttemptBy(addonId)?.toPigeon()
|
||||
val manualAttempt = getManualUpdateAttempt(addonId)
|
||||
listOfNotNull(updaterAttempt, manualAttempt)
|
||||
.maxByOrNull { it.dateMillisecondsSinceEpoch }
|
||||
}.fold(
|
||||
onSuccess = { callback(Result.success(it)) },
|
||||
onFailure = { callback(Result.failure(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun isAddonAutoUpdateEnabled(callback: (Result<Boolean>) -> Unit) {
|
||||
callback(Result.success(prefs.getBoolean(AddonPrefs.PREF_AUTO_UPDATE_ENABLED, true)))
|
||||
}
|
||||
|
||||
override fun setAddonAutoUpdateEnabled(enabled: Boolean, callback: (Result<Unit>) -> Unit) {
|
||||
scope.launch {
|
||||
try {
|
||||
prefs.edit().putBoolean(AddonPrefs.PREF_AUTO_UPDATE_ENABLED, enabled).apply()
|
||||
|
||||
val addons = components.core.addonManager.getAddons()
|
||||
.filter { it.isInstalled() && it.isSupported() }
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
if (enabled) {
|
||||
addons.forEach { addon ->
|
||||
updateAddonAutoUpdateRegistration(addon.id)
|
||||
}
|
||||
} else {
|
||||
addons.forEach { addon ->
|
||||
components.core.addonUpdater.unregisterForFutureUpdates(addon.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callback(Result.success(Unit))
|
||||
} catch (throwable: Throwable) {
|
||||
callback(Result.failure(throwable))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun installAddon(url: String, callback: (Result<Unit>) -> Unit) {
|
||||
val installMethod = if (url.startsWith("file://")) {
|
||||
val isLocalFileInstall = url.startsWith("file://")
|
||||
val installMethod = if (isLocalFileInstall) {
|
||||
InstallationMethod.FROM_FILE
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
components.core.addonManager.installAddon(
|
||||
url = url,
|
||||
installationMethod = installMethod,
|
||||
onSuccess = { _ ->
|
||||
callback(Result.success(Unit))
|
||||
},
|
||||
onError = { e ->
|
||||
callback(Result.failure(e))
|
||||
scope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
performAddonInstall(url, isLocalFileInstall, callback)
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
callback(Result.failure(throwable))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> withInstalledAddon(
|
||||
addonId: String,
|
||||
callback: (Result<T>) -> Unit,
|
||||
block: suspend (Addon, (Result<T>) -> Unit) -> Unit,
|
||||
) {
|
||||
scope.launch {
|
||||
val addon = runCatching {
|
||||
components.core.addonManager.getAddonByID(addonId)
|
||||
}.getOrNull()
|
||||
|
||||
if (addon == null) {
|
||||
callback(Result.failure(IllegalStateException("Addon $addonId not found")))
|
||||
return@launch
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
block(addon, callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isAutoUpdateEnabled(): Boolean {
|
||||
return prefs.getBoolean(AddonPrefs.PREF_AUTO_UPDATE_ENABLED, true)
|
||||
}
|
||||
|
||||
private fun isAddonAutoUpdateEnabledForAddon(addonId: String): Boolean {
|
||||
return !getAddonAutoUpdateDisabledIds().contains(addonId)
|
||||
}
|
||||
|
||||
private fun isAutoUpdateEffectivelyEnabledForAddon(addonId: String): Boolean {
|
||||
return isAddonAutoUpdateEnabledForAddon(addonId) && !isLocalFileInstalledAddon(addonId)
|
||||
}
|
||||
|
||||
private fun isLocalFileInstalledAddon(addonId: String): Boolean {
|
||||
return getLocalFileAddonIds().contains(addonId)
|
||||
}
|
||||
|
||||
private fun getAddonAutoUpdateDisabledIds(): Set<String> {
|
||||
return prefs.getStringSet(AddonPrefs.PREF_AUTO_UPDATE_DISABLED_IDS, emptySet()) ?: emptySet()
|
||||
}
|
||||
|
||||
private fun getLocalFileAddonIds(): Set<String> {
|
||||
return prefs.getStringSet(AddonPrefs.PREF_LOCAL_FILE_ADDON_IDS, emptySet()) ?: emptySet()
|
||||
}
|
||||
|
||||
private suspend fun fetchAddonStoreInfo(addonId: String): AddonStoreInfo? {
|
||||
val response = components.core.client.fetch(
|
||||
Request(
|
||||
url = addonStoreInfoUrl(addonId),
|
||||
method = Request.Method.GET,
|
||||
headers = MutableHeaders("Accept" to "application/json"),
|
||||
),
|
||||
)
|
||||
if (response.status !in 200..299) {
|
||||
return null
|
||||
}
|
||||
|
||||
val responseBody = response.body.useStream { stream ->
|
||||
String(stream.readAllBytes(), Charsets.UTF_8)
|
||||
}
|
||||
val json = JSONObject(responseBody)
|
||||
val currentVersion = json.optJSONObject("current_version") ?: return null
|
||||
val latestVersion = currentVersion.optString("version")
|
||||
val latestXpiUrl = currentVersion.optJSONObject("file")?.optString("url").orEmpty()
|
||||
if (latestVersion.isBlank() || latestXpiUrl.isBlank()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return AddonStoreInfo(
|
||||
latestVersion = latestVersion,
|
||||
latestXpiUrl = latestXpiUrl,
|
||||
)
|
||||
}
|
||||
|
||||
private fun addonStoreInfoUrl(addonId: String): String {
|
||||
val baseUrl = components.addonCollection?.serverURL?.trimEnd('/') ?: DEFAULT_AMO_SERVER_URL
|
||||
return "$baseUrl/api/v5/addons/addon/$addonId/"
|
||||
}
|
||||
|
||||
private fun saveManualUpdateAttempt(
|
||||
addonId: String,
|
||||
status: AddonUpdateStatus,
|
||||
message: String? = null,
|
||||
) {
|
||||
prefs.edit()
|
||||
.putLong(manualAttemptTimestampKey(addonId), System.currentTimeMillis())
|
||||
.putString(manualAttemptStatusKey(addonId), status.name)
|
||||
.putString(manualAttemptMessageKey(addonId), message)
|
||||
.apply()
|
||||
}
|
||||
|
||||
private fun getManualUpdateAttempt(addonId: String): AddonUpdateAttemptInfo? {
|
||||
val timestamp = prefs.getLong(manualAttemptTimestampKey(addonId), -1L)
|
||||
if (timestamp < 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
val status = prefs.getString(manualAttemptStatusKey(addonId), null)
|
||||
?.let { runCatching { AddonUpdateStatus.valueOf(it) }.getOrNull() }
|
||||
|
||||
return AddonUpdateAttemptInfo(
|
||||
addonId = addonId,
|
||||
dateMillisecondsSinceEpoch = timestamp,
|
||||
status = status,
|
||||
message = prefs.getString(manualAttemptMessageKey(addonId), null),
|
||||
)
|
||||
}
|
||||
|
||||
private fun clearManualUpdateAttempt(addonId: String) {
|
||||
prefs.edit()
|
||||
.remove(manualAttemptTimestampKey(addonId))
|
||||
.remove(manualAttemptStatusKey(addonId))
|
||||
.remove(manualAttemptMessageKey(addonId))
|
||||
.apply()
|
||||
}
|
||||
|
||||
private fun manualAttemptTimestampKey(addonId: String): String {
|
||||
return "$AddonPrefs.PREF_MANUAL_UPDATE_ATTEMPT_PREFIX$addonId.timestamp"
|
||||
}
|
||||
|
||||
private fun manualAttemptStatusKey(addonId: String): String {
|
||||
return "$AddonPrefs.PREF_MANUAL_UPDATE_ATTEMPT_PREFIX$addonId.status"
|
||||
}
|
||||
|
||||
private fun manualAttemptMessageKey(addonId: String): String {
|
||||
return "$AddonPrefs.PREF_MANUAL_UPDATE_ATTEMPT_PREFIX$addonId.message"
|
||||
}
|
||||
|
||||
private fun performAddonInstall(
|
||||
url: String,
|
||||
isLocalFileInstall: Boolean,
|
||||
callback: (Result<Unit>) -> Unit,
|
||||
) {
|
||||
val installationMethod = if (isLocalFileInstall) InstallationMethod.FROM_FILE else null
|
||||
|
||||
components.core.addonManager.installAddon(
|
||||
url = url,
|
||||
installationMethod = installationMethod,
|
||||
onSuccess = { installedAddon ->
|
||||
applyInstallUpdatePolicy(installedAddon.id, isLocalFileInstall)
|
||||
callback(Result.success(Unit))
|
||||
},
|
||||
onError = { error ->
|
||||
callback(Result.failure(error))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun applyInstallUpdatePolicy(addonId: String, isLocalFileInstall: Boolean) {
|
||||
if (isLocalFileInstall) {
|
||||
markAddonAsLocalFileInstalled(addonId)
|
||||
setAddonAutoUpdateEnabledForAddonInternal(addonId, false)
|
||||
} else {
|
||||
clearLocalFileInstalledAddon(addonId)
|
||||
updateAddonAutoUpdateRegistration(addonId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun runLocalFileAddonUpdate(
|
||||
addonId: String,
|
||||
callback: (Result<AddonUpdateAttemptInfo?>) -> Unit,
|
||||
) {
|
||||
withInstalledAddon(addonId, callback) { addon, result ->
|
||||
val storeInfo = fetchAddonStoreInfo(addonId)
|
||||
if (storeInfo == null) {
|
||||
saveManualUpdateAttempt(
|
||||
addonId = addonId,
|
||||
status = AddonUpdateStatus.ERROR,
|
||||
message = LOCAL_ADDON_UPDATE_SOURCE_MISSING_MESSAGE,
|
||||
)
|
||||
result(Result.failure(IllegalStateException(LOCAL_ADDON_UPDATE_SOURCE_MISSING_MESSAGE)))
|
||||
return@withInstalledAddon
|
||||
}
|
||||
|
||||
if (addon.installedState?.version == storeInfo.latestVersion) {
|
||||
saveManualUpdateAttempt(
|
||||
addonId = addonId,
|
||||
status = AddonUpdateStatus.NO_UPDATE_AVAILABLE,
|
||||
)
|
||||
result(Result.success(getManualUpdateAttempt(addonId)))
|
||||
return@withInstalledAddon
|
||||
}
|
||||
|
||||
components.core.addonManager.uninstallAddon(
|
||||
addon,
|
||||
onSuccess = {
|
||||
performAddonInstall(
|
||||
url = storeInfo.latestXpiUrl,
|
||||
isLocalFileInstall = false,
|
||||
callback = { installResult ->
|
||||
installResult.fold(
|
||||
onSuccess = {
|
||||
saveManualUpdateAttempt(
|
||||
addonId = addonId,
|
||||
status = AddonUpdateStatus.SUCCESSFULLY_UPDATED,
|
||||
)
|
||||
result(Result.success(getManualUpdateAttempt(addonId)))
|
||||
},
|
||||
onFailure = { throwable ->
|
||||
if (isPostponedInstallException(throwable)) {
|
||||
clearLocalFileInstalledAddon(addonId)
|
||||
updateAddonAutoUpdateRegistration(addonId)
|
||||
saveManualUpdateAttempt(
|
||||
addonId = addonId,
|
||||
status = AddonUpdateStatus.SUCCESSFULLY_UPDATED,
|
||||
message = LOCAL_ADDON_UPDATE_POSTPONED_MESSAGE,
|
||||
)
|
||||
result(Result.success(getManualUpdateAttempt(addonId)))
|
||||
return@fold
|
||||
}
|
||||
|
||||
saveManualUpdateAttempt(
|
||||
addonId = addonId,
|
||||
status = AddonUpdateStatus.ERROR,
|
||||
message = throwable.message,
|
||||
)
|
||||
result(Result.failure(throwable))
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
onError = { _, throwable ->
|
||||
saveManualUpdateAttempt(
|
||||
addonId = addonId,
|
||||
status = AddonUpdateStatus.ERROR,
|
||||
message = throwable.message,
|
||||
)
|
||||
result(Result.failure(throwable))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setAddonAutoUpdateEnabledForAddonInternal(addonId: String, enabled: Boolean) {
|
||||
val disabledIds = getAddonAutoUpdateDisabledIds().toMutableSet()
|
||||
val changed = if (enabled) {
|
||||
disabledIds.remove(addonId)
|
||||
} else {
|
||||
disabledIds.add(addonId)
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
prefs.edit().putStringSet(AddonPrefs.PREF_AUTO_UPDATE_DISABLED_IDS, disabledIds).apply()
|
||||
}
|
||||
|
||||
updateAddonAutoUpdateRegistration(addonId)
|
||||
}
|
||||
|
||||
private fun clearAddonAutoUpdatePreference(addonId: String) {
|
||||
val disabledIds = getAddonAutoUpdateDisabledIds().toMutableSet()
|
||||
if (disabledIds.remove(addonId)) {
|
||||
prefs.edit().putStringSet(AddonPrefs.PREF_AUTO_UPDATE_DISABLED_IDS, disabledIds).apply()
|
||||
}
|
||||
}
|
||||
|
||||
private fun markAddonAsLocalFileInstalled(addonId: String) {
|
||||
val localFileAddonIds = getLocalFileAddonIds().toMutableSet()
|
||||
if (localFileAddonIds.add(addonId)) {
|
||||
prefs.edit().putStringSet(AddonPrefs.PREF_LOCAL_FILE_ADDON_IDS, localFileAddonIds).apply()
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearLocalFileInstalledAddon(addonId: String) {
|
||||
val localFileAddonIds = getLocalFileAddonIds().toMutableSet()
|
||||
if (localFileAddonIds.remove(addonId)) {
|
||||
prefs.edit().putStringSet(AddonPrefs.PREF_LOCAL_FILE_ADDON_IDS, localFileAddonIds).apply()
|
||||
}
|
||||
}
|
||||
|
||||
private fun isPostponedInstallException(throwable: Throwable): Boolean {
|
||||
val installThrowable = when (throwable) {
|
||||
is WebExtensionInstallException -> throwable.cause
|
||||
else -> throwable.cause
|
||||
}
|
||||
|
||||
return installThrowable is org.mozilla.geckoview.WebExtension.InstallException &&
|
||||
installThrowable.code == ERROR_POSTPONED
|
||||
}
|
||||
|
||||
private fun updateAddonAutoUpdateRegistration(addonId: String) {
|
||||
if (
|
||||
isAutoUpdateEnabled() &&
|
||||
isAddonAutoUpdateEnabledForAddon(addonId) &&
|
||||
!isLocalFileInstalledAddon(addonId)
|
||||
) {
|
||||
components.core.addonUpdater.registerForFutureUpdates(addonId)
|
||||
} else {
|
||||
components.core.addonUpdater.unregisterForFutureUpdates(addonId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleManagedAddonUpdate(addonId: String) {
|
||||
val shouldRestorePeriodicRegistration = isAutoUpdateEnabled() &&
|
||||
isAddonAutoUpdateEnabledForAddon(addonId)
|
||||
if (shouldRestorePeriodicRegistration) {
|
||||
components.core.addonUpdater.unregisterForFutureUpdates(addonId)
|
||||
}
|
||||
|
||||
components.core.addonUpdater.update(addonId)
|
||||
|
||||
if (shouldRestorePeriodicRegistration) {
|
||||
scope.launch {
|
||||
delay(PERIODIC_UPDATE_RESTORE_DELAY_MS)
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
updateAddonAutoUpdateRegistration(addonId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun runManagedAddonUpdate(
|
||||
addonId: String,
|
||||
onComplete: (AddonUpdateAttemptInfo?) -> Unit,
|
||||
) {
|
||||
val shouldRestorePeriodicRegistration = isAutoUpdateEnabled() &&
|
||||
isAddonAutoUpdateEnabledForAddon(addonId)
|
||||
if (shouldRestorePeriodicRegistration) {
|
||||
components.core.addonUpdater.unregisterForFutureUpdates(addonId)
|
||||
}
|
||||
|
||||
components.core.addonManager.updateAddon(addonId) { status ->
|
||||
val pigeonStatus = status.toPigeon()
|
||||
val message = (status as? AddonUpdater.Status.Error)?.message
|
||||
saveManualUpdateAttempt(addonId, pigeonStatus, message)
|
||||
onComplete(getManualUpdateAttempt(addonId))
|
||||
|
||||
if (shouldRestorePeriodicRegistration) {
|
||||
scope.launch {
|
||||
delay(PERIODIC_UPDATE_RESTORE_DELAY_MS)
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
updateAddonAutoUpdateRegistration(addonId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Addon.toPigeon(
|
||||
context: Context,
|
||||
isAutoUpdateEnabled: Boolean,
|
||||
isLocalFileInstalled: Boolean,
|
||||
): AddonInfo {
|
||||
val installedState = installedState
|
||||
val localizedName = displayName(context)
|
||||
val localizedSummary = summary(context)
|
||||
val localizedDescription = if (translatableDescription.isNotEmpty()) {
|
||||
translateDescription(context)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
return AddonInfo(
|
||||
id = id,
|
||||
displayName = localizedName,
|
||||
summary = localizedSummary,
|
||||
description = localizedDescription,
|
||||
downloadUrl = downloadUrl,
|
||||
version = version,
|
||||
installedVersion = installedState?.version,
|
||||
translatedPermissions = translatePermissions(context),
|
||||
translatedRequiredDataCollectionPermissions =
|
||||
translateRequiredDataCollectionPermissions(context),
|
||||
authorName = author?.name,
|
||||
authorUrl = author?.url,
|
||||
homepageUrl = homepageUrl,
|
||||
detailUrl = detailUrl,
|
||||
ratingUrl = ratingUrl,
|
||||
ratingAverage = rating?.average?.toDouble(),
|
||||
ratingReviews = rating?.reviews?.toLong(),
|
||||
createdAt = createdAt,
|
||||
updatedAt = updatedAt,
|
||||
icon = provideIcon()?.toWebPBytes(),
|
||||
isInstalled = isInstalled(),
|
||||
isEnabled = isEnabled(),
|
||||
isSupported = isSupported(),
|
||||
isAllowedInPrivateBrowsing = isAllowedInPrivateBrowsing(),
|
||||
isAutoUpdateEnabled = isAutoUpdateEnabled,
|
||||
isLocalFileInstalled = isLocalFileInstalled,
|
||||
optionsPageUrl = installedState?.optionsPageUrl,
|
||||
openOptionsPageInTab = installedState?.openOptionsPageInTab ?: false,
|
||||
disabledReason = installedState?.disabledReason?.toPigeon(),
|
||||
incognito = incognito.toPigeon(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun Addon.DisabledReason.toPigeon(): AddonDisabledReason {
|
||||
return when (this) {
|
||||
Addon.DisabledReason.UNSUPPORTED -> AddonDisabledReason.UNSUPPORTED
|
||||
Addon.DisabledReason.BLOCKLISTED -> AddonDisabledReason.BLOCKLISTED
|
||||
Addon.DisabledReason.USER_REQUESTED -> AddonDisabledReason.USER_REQUESTED
|
||||
Addon.DisabledReason.NOT_CORRECTLY_SIGNED -> AddonDisabledReason.NOT_CORRECTLY_SIGNED
|
||||
Addon.DisabledReason.INCOMPATIBLE -> AddonDisabledReason.INCOMPATIBLE
|
||||
Addon.DisabledReason.SOFT_BLOCKED -> AddonDisabledReason.SOFT_BLOCKED
|
||||
}
|
||||
}
|
||||
|
||||
private fun Addon.Incognito.toPigeon(): AddonIncognito {
|
||||
return when (this) {
|
||||
Addon.Incognito.SPANNING -> AddonIncognito.SPANNING
|
||||
Addon.Incognito.SPLIT -> AddonIncognito.SPLIT
|
||||
Addon.Incognito.NOT_ALLOWED -> AddonIncognito.NOT_ALLOWED
|
||||
}
|
||||
}
|
||||
|
||||
private fun AddonUpdater.UpdateAttempt.toPigeon(): AddonUpdateAttemptInfo {
|
||||
return AddonUpdateAttemptInfo(
|
||||
addonId = addonId,
|
||||
dateMillisecondsSinceEpoch = date.time,
|
||||
status = status?.toPigeon(),
|
||||
message = (status as? AddonUpdater.Status.Error)?.message,
|
||||
)
|
||||
}
|
||||
|
||||
private fun AddonUpdater.Status.toPigeon(): AddonUpdateStatus {
|
||||
return when (this) {
|
||||
AddonUpdater.Status.NotInstalled -> AddonUpdateStatus.NOT_INSTALLED
|
||||
AddonUpdater.Status.SuccessfullyUpdated -> AddonUpdateStatus.SUCCESSFULLY_UPDATED
|
||||
AddonUpdater.Status.NoUpdateAvailable -> AddonUpdateStatus.NO_UPDATE_AVAILABLE
|
||||
is AddonUpdater.Status.Error -> AddonUpdateStatus.ERROR
|
||||
}
|
||||
}
|
||||
|
||||
+10
@@ -10,6 +10,8 @@ import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.view.View
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import eu.weblibre.flutter_mozilla_components.AddonPopupViewFactory
|
||||
import eu.weblibre.flutter_mozilla_components.AddonSettingsViewFactory
|
||||
import eu.weblibre.flutter_mozilla_components.BrowserFragment
|
||||
import eu.weblibre.flutter_mozilla_components.GeckoViewFactory
|
||||
import eu.weblibre.flutter_mozilla_components.EngineProvider
|
||||
@@ -145,6 +147,14 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
|
||||
_flutterEvents
|
||||
)
|
||||
)
|
||||
_flutterPluginBinding.platformViewRegistry.registerViewFactory(
|
||||
"eu.weblibre/addon_settings",
|
||||
AddonSettingsViewFactory(activityProvider = { this.activity }),
|
||||
)
|
||||
_flutterPluginBinding.platformViewRegistry.registerViewFactory(
|
||||
"eu.weblibre/addon_popup",
|
||||
AddonPopupViewFactory(activityProvider = { this.activity }),
|
||||
)
|
||||
isPlatformViewRegistered = true
|
||||
|
||||
isGeckoInitialized = false
|
||||
|
||||
+791
-217
File diff suppressed because it is too large
Load Diff
-157
@@ -1,157 +0,0 @@
|
||||
<?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:fitsSystemWindows="true"
|
||||
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="@color/icons" />
|
||||
|
||||
<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>
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
<?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.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true" >
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
<?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:fitsSystemWindows="true"
|
||||
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="@color/icons" />
|
||||
|
||||
</RelativeLayout>
|
||||
-94
@@ -1,94 +0,0 @@
|
||||
<?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:fitsSystemWindows="true"
|
||||
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:textSize="18sp"
|
||||
app:drawableStartCompat="@drawable/mozac_ic_preferences"
|
||||
app:drawableTint="@color/icons" />
|
||||
|
||||
<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:textSize="18sp"
|
||||
app:drawableStartCompat="@drawable/mozac_ic_information_24"
|
||||
app:drawableTint="@color/icons" />
|
||||
|
||||
<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: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_2"
|
||||
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>
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
<?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"
|
||||
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>
|
||||
@@ -1,27 +0,0 @@
|
||||
<?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>
|
||||
@@ -66,37 +66,6 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:theme="@style/Theme.AppCompat.Light"
|
||||
android:name="eu.weblibre.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.weblibre.flutter_mozilla_components.addons.AddonDetailsActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/mozac_feature_addons_addons" />
|
||||
|
||||
<activity android:name="eu.weblibre.flutter_mozilla_components.addons.InstalledAddonDetailsActivity"
|
||||
android:label="@string/mozac_feature_addons_addons"
|
||||
android:parentActivityName="eu.weblibre.flutter_mozilla_components.addons.AddonsActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.AppCompat.Light" />
|
||||
|
||||
<activity
|
||||
android:name="eu.weblibre.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.weblibre.flutter_mozilla_components.addons.AddonInternalSettingsActivity"
|
||||
android:label="@string/mozac_feature_addons_addons"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.AppCompat.Light" />
|
||||
|
||||
<activity
|
||||
android:name="eu.weblibre.flutter_mozilla_components.addons.WebExtensionActionPopupActivity"
|
||||
android:label="@string/mozac_feature_addons_addons"
|
||||
|
||||
@@ -38,6 +38,12 @@ export 'src/pigeons/gecko.g.dart'
|
||||
show
|
||||
AddTabParams,
|
||||
AddonCollection,
|
||||
AddonDisabledReason,
|
||||
AddonIncognito,
|
||||
AddonInfo,
|
||||
AddonStoreInfo,
|
||||
AddonUpdateAttemptInfo,
|
||||
AddonUpdateStatus,
|
||||
AppLinksMode,
|
||||
AudioHitResult,
|
||||
AutoplayStatus,
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'package:rxdart/rxdart.dart';
|
||||
|
||||
typedef ExtensionDataEvent = ({String extensionId, WebExtensionData? data});
|
||||
typedef ExtensionIconEvent = ({String extensionId, Uint8List bytes});
|
||||
typedef ExtensionPopupEvent = ({String extensionId, String extensionName});
|
||||
|
||||
final _apiInstance = GeckoAddonsApi();
|
||||
|
||||
@@ -24,6 +25,7 @@ class GeckoAddonService extends GeckoAddonEvents {
|
||||
|
||||
final _browserIconSubject = ReplaySubject<ExtensionIconEvent>();
|
||||
final _pageIconSubject = ReplaySubject<ExtensionIconEvent>();
|
||||
final _popupSubject = PublishSubject<ExtensionPopupEvent>();
|
||||
|
||||
Stream<ExtensionDataEvent> get browserExtensionStream =>
|
||||
_browserExtensionSubject.stream;
|
||||
@@ -33,13 +35,18 @@ class GeckoAddonService extends GeckoAddonEvents {
|
||||
Stream<ExtensionIconEvent> get browserIconStream =>
|
||||
_browserIconSubject.stream;
|
||||
Stream<ExtensionIconEvent> get pageIconStream => _pageIconSubject.stream;
|
||||
Stream<ExtensionPopupEvent> get popupStream => _popupSubject.stream;
|
||||
|
||||
Future<void> startAddonManagerActivity() {
|
||||
return _api.startAddonManagerActivity();
|
||||
Future<List<AddonInfo>> getAddons({bool allowCache = true}) {
|
||||
return _api.getAddons(allowCache);
|
||||
}
|
||||
|
||||
Future<void> startAddonSettingsActivity(String extensionId) {
|
||||
return _api.startAddonSettingsActivity(extensionId);
|
||||
Future<AddonInfo?> getAddonById(String addonId, {bool allowCache = true}) {
|
||||
return _api.getAddonById(addonId, allowCache);
|
||||
}
|
||||
|
||||
Future<AddonStoreInfo?> getAddonStoreInfo(String addonId) {
|
||||
return _api.getAddonStoreInfo(addonId);
|
||||
}
|
||||
|
||||
Future<void> invokeAddonAction(
|
||||
@@ -53,6 +60,52 @@ class GeckoAddonService extends GeckoAddonEvents {
|
||||
return _api.installAddon(url.toString());
|
||||
}
|
||||
|
||||
Future<AddonInfo> enableAddon(String addonId) {
|
||||
return _api.enableAddon(addonId);
|
||||
}
|
||||
|
||||
Future<AddonInfo> disableAddon(String addonId) {
|
||||
return _api.disableAddon(addonId);
|
||||
}
|
||||
|
||||
Future<AddonInfo> setAddonAllowedInPrivateBrowsing(
|
||||
String addonId,
|
||||
bool allowed,
|
||||
) {
|
||||
return _api.setAddonAllowedInPrivateBrowsing(addonId, allowed);
|
||||
}
|
||||
|
||||
Future<AddonInfo> setAddonAutoUpdateEnabledForAddon(
|
||||
String addonId,
|
||||
bool enabled,
|
||||
) {
|
||||
return _api.setAddonAutoUpdateEnabledForAddon(addonId, enabled);
|
||||
}
|
||||
|
||||
Future<void> uninstallAddon(String addonId) {
|
||||
return _api.uninstallAddon(addonId);
|
||||
}
|
||||
|
||||
Future<AddonUpdateAttemptInfo?> triggerAddonUpdate(String addonId) {
|
||||
return _api.triggerAddonUpdate(addonId);
|
||||
}
|
||||
|
||||
Future<void> triggerAllAddonUpdates() {
|
||||
return _api.triggerAllAddonUpdates();
|
||||
}
|
||||
|
||||
Future<AddonUpdateAttemptInfo?> getLastAddonUpdateAttempt(String addonId) {
|
||||
return _api.getLastAddonUpdateAttempt(addonId);
|
||||
}
|
||||
|
||||
Future<bool> isAddonAutoUpdateEnabled() {
|
||||
return _api.isAddonAutoUpdateEnabled();
|
||||
}
|
||||
|
||||
Future<void> setAddonAutoUpdateEnabled({required bool enabled}) {
|
||||
return _api.setAddonAutoUpdateEnabled(enabled);
|
||||
}
|
||||
|
||||
@override
|
||||
void onRemoveWebExtensionAction(
|
||||
int sequence,
|
||||
@@ -115,6 +168,11 @@ class GeckoAddonService extends GeckoAddonEvents {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onWebExtensionPopupRequested(String extensionId, String extensionName) {
|
||||
_popupSubject.add((extensionId: extensionId, extensionName: extensionName));
|
||||
}
|
||||
|
||||
GeckoAddonService.setUp({
|
||||
BinaryMessenger? binaryMessenger,
|
||||
GeckoAddonsApi? api,
|
||||
@@ -132,5 +190,6 @@ class GeckoAddonService extends GeckoAddonEvents {
|
||||
await _pageExtensionSubject.close();
|
||||
await _browserIconSubject.close();
|
||||
await _pageIconSubject.close();
|
||||
await _popupSubject.close();
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -667,6 +667,112 @@ class WebExtensionData {
|
||||
);
|
||||
}
|
||||
|
||||
enum AddonDisabledReason {
|
||||
unsupported,
|
||||
blocklisted,
|
||||
userRequested,
|
||||
notCorrectlySigned,
|
||||
incompatible,
|
||||
softBlocked,
|
||||
}
|
||||
|
||||
enum AddonIncognito { spanning, split, notAllowed }
|
||||
|
||||
enum AddonUpdateStatus {
|
||||
notInstalled,
|
||||
successfullyUpdated,
|
||||
noUpdateAvailable,
|
||||
error,
|
||||
}
|
||||
|
||||
class AddonInfo {
|
||||
final String id;
|
||||
final String displayName;
|
||||
final String? summary;
|
||||
final String description;
|
||||
final String downloadUrl;
|
||||
final String version;
|
||||
final String? installedVersion;
|
||||
final List<String> translatedPermissions;
|
||||
final List<String> translatedRequiredDataCollectionPermissions;
|
||||
final String? authorName;
|
||||
final String? authorUrl;
|
||||
final String homepageUrl;
|
||||
final String detailUrl;
|
||||
final String ratingUrl;
|
||||
final double? ratingAverage;
|
||||
final int? ratingReviews;
|
||||
final String createdAt;
|
||||
final String updatedAt;
|
||||
final Uint8List? icon;
|
||||
final bool isInstalled;
|
||||
final bool isEnabled;
|
||||
final bool isSupported;
|
||||
final bool isAllowedInPrivateBrowsing;
|
||||
final bool isAutoUpdateEnabled;
|
||||
final bool isLocalFileInstalled;
|
||||
final String? optionsPageUrl;
|
||||
final bool openOptionsPageInTab;
|
||||
final AddonDisabledReason? disabledReason;
|
||||
final AddonIncognito incognito;
|
||||
|
||||
const AddonInfo({
|
||||
required this.id,
|
||||
required this.displayName,
|
||||
this.summary,
|
||||
required this.description,
|
||||
required this.downloadUrl,
|
||||
required this.version,
|
||||
this.installedVersion,
|
||||
required this.translatedPermissions,
|
||||
required this.translatedRequiredDataCollectionPermissions,
|
||||
this.authorName,
|
||||
this.authorUrl,
|
||||
required this.homepageUrl,
|
||||
required this.detailUrl,
|
||||
required this.ratingUrl,
|
||||
this.ratingAverage,
|
||||
this.ratingReviews,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.icon,
|
||||
required this.isInstalled,
|
||||
required this.isEnabled,
|
||||
required this.isSupported,
|
||||
required this.isAllowedInPrivateBrowsing,
|
||||
required this.isAutoUpdateEnabled,
|
||||
required this.isLocalFileInstalled,
|
||||
this.optionsPageUrl,
|
||||
required this.openOptionsPageInTab,
|
||||
this.disabledReason,
|
||||
this.incognito = AddonIncognito.spanning,
|
||||
});
|
||||
}
|
||||
|
||||
class AddonStoreInfo {
|
||||
final String latestVersion;
|
||||
final String latestXpiUrl;
|
||||
|
||||
const AddonStoreInfo({
|
||||
required this.latestVersion,
|
||||
required this.latestXpiUrl,
|
||||
});
|
||||
}
|
||||
|
||||
class AddonUpdateAttemptInfo {
|
||||
final String addonId;
|
||||
final int dateMillisecondsSinceEpoch;
|
||||
final AddonUpdateStatus? status;
|
||||
final String? message;
|
||||
|
||||
const AddonUpdateAttemptInfo({
|
||||
required this.addonId,
|
||||
required this.dateMillisecondsSinceEpoch,
|
||||
this.status,
|
||||
this.message,
|
||||
});
|
||||
}
|
||||
|
||||
enum GeckoSuggestionType { session, clipboard, history }
|
||||
|
||||
class GeckoSuggestion {
|
||||
@@ -1738,14 +1844,49 @@ abstract class GeckoSelectionActionEvents {
|
||||
|
||||
@HostApi()
|
||||
abstract class GeckoAddonsApi {
|
||||
void startAddonManagerActivity();
|
||||
@async
|
||||
List<AddonInfo> getAddons(bool allowCache);
|
||||
|
||||
void startAddonSettingsActivity(String extensionId);
|
||||
@async
|
||||
AddonInfo? getAddonById(String addonId, bool allowCache);
|
||||
|
||||
@async
|
||||
AddonStoreInfo? getAddonStoreInfo(String addonId);
|
||||
|
||||
void invokeAddonAction(String extensionId, WebExtensionActionType actionType);
|
||||
|
||||
@async
|
||||
AddonInfo enableAddon(String addonId);
|
||||
|
||||
@async
|
||||
AddonInfo disableAddon(String addonId);
|
||||
|
||||
@async
|
||||
AddonInfo setAddonAllowedInPrivateBrowsing(String addonId, bool allowed);
|
||||
|
||||
@async
|
||||
AddonInfo setAddonAutoUpdateEnabledForAddon(String addonId, bool enabled);
|
||||
|
||||
@async
|
||||
void uninstallAddon(String addonId);
|
||||
|
||||
@async
|
||||
AddonUpdateAttemptInfo? triggerAddonUpdate(String addonId);
|
||||
|
||||
@async
|
||||
void triggerAllAddonUpdates();
|
||||
|
||||
@async
|
||||
AddonUpdateAttemptInfo? getLastAddonUpdateAttempt(String addonId);
|
||||
|
||||
@async
|
||||
void installAddon(String url);
|
||||
|
||||
@async
|
||||
bool isAddonAutoUpdateEnabled();
|
||||
|
||||
@async
|
||||
void setAddonAutoUpdateEnabled(bool enabled);
|
||||
}
|
||||
|
||||
@FlutterApi()
|
||||
@@ -1769,6 +1910,8 @@ abstract class GeckoAddonEvents {
|
||||
WebExtensionActionType actionType,
|
||||
Uint8List icon,
|
||||
);
|
||||
|
||||
void onWebExtensionPopupRequested(String extensionId, String extensionName);
|
||||
}
|
||||
|
||||
@HostApi()
|
||||
|
||||
Reference in New Issue
Block a user