update extension prompt feature
This commit is contained in:
+6
@@ -317,6 +317,12 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
useOnStreamChange(
|
||||||
|
addonService.openAddonSettingsStream,
|
||||||
|
onData: (addonId) async {
|
||||||
|
await AddonInternalSettingsRoute(addonId: addonId).push<void>(context);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
useOnAppLifecycleStateChange((previous, current) {
|
useOnAppLifecycleStateChange((previous, current) {
|
||||||
switch (current) {
|
switch (current) {
|
||||||
|
|||||||
+2
@@ -413,6 +413,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
|
|||||||
store = components.core.store,
|
store = components.core.store,
|
||||||
context = profileContext,
|
context = profileContext,
|
||||||
fragmentManager = parentFragmentManager,
|
fragmentManager = parentFragmentManager,
|
||||||
|
addonManager = components.core.addonManager,
|
||||||
|
addonEvents = components.addonEvents,
|
||||||
),
|
),
|
||||||
owner = this,
|
owner = this,
|
||||||
view = view
|
view = view
|
||||||
|
|||||||
+70
-39
@@ -6,8 +6,9 @@ package eu.weblibre.flutter_mozilla_components.addons
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoAddonEvents
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
@@ -18,6 +19,7 @@ import mozilla.components.browser.state.store.BrowserStore
|
|||||||
import mozilla.components.concept.engine.webextension.PermissionPromptResponse
|
import mozilla.components.concept.engine.webextension.PermissionPromptResponse
|
||||||
import mozilla.components.concept.engine.webextension.WebExtensionInstallException
|
import mozilla.components.concept.engine.webextension.WebExtensionInstallException
|
||||||
import mozilla.components.feature.addons.Addon
|
import mozilla.components.feature.addons.Addon
|
||||||
|
import mozilla.components.feature.addons.AddonManager
|
||||||
import mozilla.components.feature.addons.ui.AddonInstallationDialogFragment
|
import mozilla.components.feature.addons.ui.AddonInstallationDialogFragment
|
||||||
import mozilla.components.feature.addons.ui.PermissionsDialogFragment
|
import mozilla.components.feature.addons.ui.PermissionsDialogFragment
|
||||||
import mozilla.components.lib.state.ext.flowScoped
|
import mozilla.components.lib.state.ext.flowScoped
|
||||||
@@ -35,6 +37,8 @@ class WebExtensionPromptFeature(
|
|||||||
private val store: BrowserStore,
|
private val store: BrowserStore,
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val fragmentManager: FragmentManager,
|
private val fragmentManager: FragmentManager,
|
||||||
|
private val addonManager: AddonManager,
|
||||||
|
private val addonEvents: GeckoAddonEvents,
|
||||||
) : LifecycleAwareFeature {
|
) : LifecycleAwareFeature {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,10 +72,8 @@ class WebExtensionPromptFeature(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleAfterInstallationRequest(promptRequest: WebExtensionPromptRequest.AfterInstallation) {
|
private fun handleAfterInstallationRequest(promptRequest: WebExtensionPromptRequest.AfterInstallation) {
|
||||||
// The install flow in Fenix relies on an [Addon] object so let's convert the (GeckoView)
|
val installedState = addonManager.toInstalledState(promptRequest.extension)
|
||||||
// extension into a minimal add-on. The missing metadata will be fetched when the user
|
val addon = Addon.newFromWebExtension(promptRequest.extension, installedState)
|
||||||
// opens the add-ons manager.
|
|
||||||
val addon = Addon.newFromWebExtension(promptRequest.extension)
|
|
||||||
when (promptRequest) {
|
when (promptRequest) {
|
||||||
is WebExtensionPromptRequest.AfterInstallation.Permissions.Required -> handlePermissionRequest(
|
is WebExtensionPromptRequest.AfterInstallation.Permissions.Required -> handlePermissionRequest(
|
||||||
addon,
|
addon,
|
||||||
@@ -105,6 +107,7 @@ class WebExtensionPromptFeature(
|
|||||||
promptRequest = promptRequest,
|
promptRequest = promptRequest,
|
||||||
permissions = promptRequest.permissions,
|
permissions = promptRequest.permissions,
|
||||||
origins = promptRequest.origins,
|
origins = promptRequest.origins,
|
||||||
|
dataCollectionPermissions = promptRequest.dataCollectionPermissions,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,21 +115,28 @@ class WebExtensionPromptFeature(
|
|||||||
addon: Addon,
|
addon: Addon,
|
||||||
promptRequest: WebExtensionPromptRequest.AfterInstallation.Permissions.Optional,
|
promptRequest: WebExtensionPromptRequest.AfterInstallation.Permissions.Optional,
|
||||||
) {
|
) {
|
||||||
val shouldGrantWithoutPrompt = Addon.localizePermissions(promptRequest.permissions, context).isEmpty()
|
val shouldGrantWithoutPrompt = Addon.localizePermissions(
|
||||||
|
promptRequest.permissions,
|
||||||
|
context,
|
||||||
|
).isEmpty() && promptRequest.origins.isEmpty() && promptRequest.dataCollectionPermissions.isEmpty()
|
||||||
|
|
||||||
// If we don't have any promptable permissions, just proceed.
|
// If we don't have any promptable permissions, just proceed.
|
||||||
if (shouldGrantWithoutPrompt) {
|
if (shouldGrantWithoutPrompt) {
|
||||||
promptRequest.onConfirm(true)
|
handlePermissions(
|
||||||
consumePromptRequest()
|
promptRequest = promptRequest,
|
||||||
|
granted = true,
|
||||||
|
privateBrowsingAllowed = false,
|
||||||
|
technicalAndInteractionDataGranted = false,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
showPermissionDialog(
|
showPermissionDialog(
|
||||||
// This is a bit of a hack so that the permission prompt only lists
|
addon = addon,
|
||||||
// the optional permissions that are requested.
|
|
||||||
addon = addon.copy(permissions = promptRequest.permissions),
|
|
||||||
promptRequest = promptRequest,
|
promptRequest = promptRequest,
|
||||||
permissions = promptRequest.permissions,
|
permissions = promptRequest.permissions,
|
||||||
|
origins = promptRequest.origins,
|
||||||
|
dataCollectionPermissions = promptRequest.dataCollectionPermissions,
|
||||||
forOptionalPermissions = true,
|
forOptionalPermissions = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -141,6 +151,10 @@ class WebExtensionPromptFeature(
|
|||||||
onConfirmButtonClicked = { _ ->
|
onConfirmButtonClicked = { _ ->
|
||||||
consumePromptRequest()
|
consumePromptRequest()
|
||||||
},
|
},
|
||||||
|
onExtensionSettingsLinkClicked = {
|
||||||
|
openAddonSettings(it.id)
|
||||||
|
consumePromptRequest()
|
||||||
|
},
|
||||||
)
|
)
|
||||||
dialog.show(fragmentManager, POST_INSTALLATION_DIALOG_FRAGMENT_TAG)
|
dialog.show(fragmentManager, POST_INSTALLATION_DIALOG_FRAGMENT_TAG)
|
||||||
}
|
}
|
||||||
@@ -159,6 +173,7 @@ class WebExtensionPromptFeature(
|
|||||||
promptRequest: WebExtensionPromptRequest.AfterInstallation.Permissions,
|
promptRequest: WebExtensionPromptRequest.AfterInstallation.Permissions,
|
||||||
permissions: List<String> = emptyList(),
|
permissions: List<String> = emptyList(),
|
||||||
origins: List<String> = emptyList(),
|
origins: List<String> = emptyList(),
|
||||||
|
dataCollectionPermissions: List<String> = emptyList(),
|
||||||
forOptionalPermissions: Boolean = false,
|
forOptionalPermissions: Boolean = false,
|
||||||
) {
|
) {
|
||||||
if (!isInstallationInProgress && !hasExistingPermissionDialogFragment()) {
|
if (!isInstallationInProgress && !hasExistingPermissionDialogFragment()) {
|
||||||
@@ -166,26 +181,23 @@ class WebExtensionPromptFeature(
|
|||||||
addon = addon,
|
addon = addon,
|
||||||
permissions = permissions,
|
permissions = permissions,
|
||||||
origins = origins,
|
origins = origins,
|
||||||
dataCollectionPermissions = emptyList(),
|
dataCollectionPermissions = dataCollectionPermissions,
|
||||||
forOptionalPermissions = forOptionalPermissions,
|
forOptionalPermissions = forOptionalPermissions,
|
||||||
onPositiveButtonClicked = { _, privateBrowsingAllowed, _ ->
|
onPositiveButtonClicked = { _, privateBrowsingAllowed, technicalAndInteractionDataGranted ->
|
||||||
handlePermissions(
|
handlePermissions(
|
||||||
promptRequest,
|
promptRequest,
|
||||||
granted = true,
|
granted = true,
|
||||||
privateBrowsingAllowed = privateBrowsingAllowed,
|
privateBrowsingAllowed = privateBrowsingAllowed,
|
||||||
|
technicalAndInteractionDataGranted = technicalAndInteractionDataGranted,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onNegativeButtonClicked = {
|
onNegativeButtonClicked = {
|
||||||
when (promptRequest) {
|
handlePermissions(
|
||||||
is WebExtensionPromptRequest.AfterInstallation.Permissions.Optional -> {
|
promptRequest = promptRequest,
|
||||||
promptRequest.onConfirm(false)
|
granted = false,
|
||||||
}
|
privateBrowsingAllowed = false,
|
||||||
|
technicalAndInteractionDataGranted = false,
|
||||||
is WebExtensionPromptRequest.AfterInstallation.Permissions.Required -> {
|
)
|
||||||
promptRequest.onConfirm(PermissionPromptResponse(isPermissionsGranted = false))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
consumePromptRequest()
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
dialog.show(
|
dialog.show(
|
||||||
@@ -197,7 +209,7 @@ class WebExtensionPromptFeature(
|
|||||||
|
|
||||||
private fun tryToReAttachButtonHandlersToPreviousDialog() {
|
private fun tryToReAttachButtonHandlersToPreviousDialog() {
|
||||||
findPreviousDialogFragment()?.let { dialog ->
|
findPreviousDialogFragment()?.let { dialog ->
|
||||||
dialog.onPositiveButtonClicked = { addon, privateBrowsingAllowed, _ ->
|
dialog.onPositiveButtonClicked = { addon, privateBrowsingAllowed, technicalAndInteractionDataGranted ->
|
||||||
store.state.webExtensionPromptRequest?.let { promptRequest ->
|
store.state.webExtensionPromptRequest?.let { promptRequest ->
|
||||||
if (promptRequest is WebExtensionPromptRequest.AfterInstallation.Permissions &&
|
if (promptRequest is WebExtensionPromptRequest.AfterInstallation.Permissions &&
|
||||||
addon.id == promptRequest.extension.id
|
addon.id == promptRequest.extension.id
|
||||||
@@ -206,26 +218,43 @@ class WebExtensionPromptFeature(
|
|||||||
promptRequest,
|
promptRequest,
|
||||||
granted = true,
|
granted = true,
|
||||||
privateBrowsingAllowed = privateBrowsingAllowed,
|
privateBrowsingAllowed = privateBrowsingAllowed,
|
||||||
|
technicalAndInteractionDataGranted = technicalAndInteractionDataGranted,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dialog.onNegativeButtonClicked = {
|
dialog.onNegativeButtonClicked = {
|
||||||
store.state.webExtensionPromptRequest?.let { promptRequest ->
|
store.state.webExtensionPromptRequest?.let { promptRequest ->
|
||||||
handlePermissions(
|
if (promptRequest is WebExtensionPromptRequest.AfterInstallation.Permissions) {
|
||||||
promptRequest,
|
handlePermissions(
|
||||||
granted = false,
|
promptRequest,
|
||||||
privateBrowsingAllowed = false,
|
granted = false,
|
||||||
)
|
privateBrowsingAllowed = false,
|
||||||
|
technicalAndInteractionDataGranted = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findPreviousPostInstallationDialogFragment()?.let { dialog ->
|
||||||
|
dialog.onDismissed = {
|
||||||
|
store.state.webExtensionPromptRequest?.let {
|
||||||
|
consumePromptRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dialog.onExtensionSettingsLinkClicked = {
|
||||||
|
openAddonSettings(it.id)
|
||||||
|
consumePromptRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handlePermissions(
|
private fun handlePermissions(
|
||||||
promptRequest: WebExtensionPromptRequest,
|
promptRequest: WebExtensionPromptRequest.AfterInstallation.Permissions,
|
||||||
granted: Boolean,
|
granted: Boolean,
|
||||||
privateBrowsingAllowed: Boolean,
|
privateBrowsingAllowed: Boolean,
|
||||||
|
technicalAndInteractionDataGranted: Boolean,
|
||||||
) {
|
) {
|
||||||
when (promptRequest) {
|
when (promptRequest) {
|
||||||
is WebExtensionPromptRequest.AfterInstallation.Permissions.Optional -> {
|
is WebExtensionPromptRequest.AfterInstallation.Permissions.Optional -> {
|
||||||
@@ -236,21 +265,18 @@ class WebExtensionPromptFeature(
|
|||||||
val response = PermissionPromptResponse(
|
val response = PermissionPromptResponse(
|
||||||
isPermissionsGranted = granted,
|
isPermissionsGranted = granted,
|
||||||
isPrivateModeGranted = privateBrowsingAllowed,
|
isPrivateModeGranted = privateBrowsingAllowed,
|
||||||
|
isTechnicalAndInteractionDataGranted = technicalAndInteractionDataGranted,
|
||||||
)
|
)
|
||||||
promptRequest.onConfirm(response)
|
promptRequest.onConfirm(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
is WebExtensionPromptRequest.AfterInstallation.PostInstallation -> {
|
|
||||||
// opt-out
|
|
||||||
}
|
|
||||||
|
|
||||||
is WebExtensionPromptRequest.BeforeInstallation.InstallationFailed -> {
|
|
||||||
// opt-out
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
consumePromptRequest()
|
consumePromptRequest()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun openAddonSettings(addonId: String) {
|
||||||
|
addonEvents.onOpenAddonSettingsRequested(addonId) { }
|
||||||
|
}
|
||||||
|
|
||||||
private fun consumePromptRequest() {
|
private fun consumePromptRequest() {
|
||||||
store.dispatch(WebExtensionAction.ConsumePromptRequestWebExtensionAction)
|
store.dispatch(WebExtensionAction.ConsumePromptRequestWebExtensionAction)
|
||||||
}
|
}
|
||||||
@@ -263,6 +289,11 @@ class WebExtensionPromptFeature(
|
|||||||
return fragmentManager.findFragmentByTag(PERMISSIONS_DIALOG_FRAGMENT_TAG) as? PermissionsDialogFragment
|
return fragmentManager.findFragmentByTag(PERMISSIONS_DIALOG_FRAGMENT_TAG) as? PermissionsDialogFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun findPreviousPostInstallationDialogFragment(): AddonInstallationDialogFragment? {
|
||||||
|
return fragmentManager.findFragmentByTag(POST_INSTALLATION_DIALOG_FRAGMENT_TAG)
|
||||||
|
as? AddonInstallationDialogFragment
|
||||||
|
}
|
||||||
|
|
||||||
private fun hasExistingAddonPostInstallationDialogFragment(): Boolean {
|
private fun hasExistingAddonPostInstallationDialogFragment(): Boolean {
|
||||||
return fragmentManager.findFragmentByTag(POST_INSTALLATION_DIALOG_FRAGMENT_TAG)
|
return fragmentManager.findFragmentByTag(POST_INSTALLATION_DIALOG_FRAGMENT_TAG)
|
||||||
as? AddonInstallationDialogFragment != null
|
as? AddonInstallationDialogFragment != null
|
||||||
@@ -355,7 +386,7 @@ class WebExtensionPromptFeature(
|
|||||||
message: String,
|
message: String,
|
||||||
) {
|
) {
|
||||||
context.let {
|
context.let {
|
||||||
AlertDialog.Builder(it).setTitle(title)
|
MaterialAlertDialogBuilder(it).setTitle(title)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ -> }.setCancelable(false).setMessage(
|
.setPositiveButton(android.R.string.ok) { _, _ -> }.setCancelable(false).setMessage(
|
||||||
message,
|
message,
|
||||||
).show().withCenterAlignedButtons()
|
).show().withCenterAlignedButtons()
|
||||||
|
|||||||
+17
@@ -9235,6 +9235,23 @@ class GeckoAddonEvents(private val binaryMessenger: BinaryMessenger, private val
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fun onOpenAddonSettingsRequested(addonIdArg: String, callback: (Result<Unit>) -> Unit)
|
||||||
|
{
|
||||||
|
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||||
|
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onOpenAddonSettingsRequested$separatedMessageChannelSuffix"
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
|
||||||
|
channel.send(listOf(addonIdArg)) {
|
||||||
|
if (it is List<*>) {
|
||||||
|
if (it.size > 1) {
|
||||||
|
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
|
||||||
|
} else {
|
||||||
|
callback(Result.success(Unit))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback(Result.failure(GeckoPigeonUtils.createConnectionError(channelName)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||||
interface GeckoSuggestionApi {
|
interface GeckoSuggestionApi {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class GeckoAddonService extends GeckoAddonEvents {
|
|||||||
final _browserIconSubject = ReplaySubject<ExtensionIconEvent>();
|
final _browserIconSubject = ReplaySubject<ExtensionIconEvent>();
|
||||||
final _pageIconSubject = ReplaySubject<ExtensionIconEvent>();
|
final _pageIconSubject = ReplaySubject<ExtensionIconEvent>();
|
||||||
final _popupSubject = PublishSubject<ExtensionPopupEvent>();
|
final _popupSubject = PublishSubject<ExtensionPopupEvent>();
|
||||||
|
final _openAddonSettingsSubject = PublishSubject<String>();
|
||||||
|
|
||||||
Stream<ExtensionDataEvent> get browserExtensionStream =>
|
Stream<ExtensionDataEvent> get browserExtensionStream =>
|
||||||
_browserExtensionSubject.stream;
|
_browserExtensionSubject.stream;
|
||||||
@@ -36,6 +37,8 @@ class GeckoAddonService extends GeckoAddonEvents {
|
|||||||
_browserIconSubject.stream;
|
_browserIconSubject.stream;
|
||||||
Stream<ExtensionIconEvent> get pageIconStream => _pageIconSubject.stream;
|
Stream<ExtensionIconEvent> get pageIconStream => _pageIconSubject.stream;
|
||||||
Stream<ExtensionPopupEvent> get popupStream => _popupSubject.stream;
|
Stream<ExtensionPopupEvent> get popupStream => _popupSubject.stream;
|
||||||
|
Stream<String> get openAddonSettingsStream =>
|
||||||
|
_openAddonSettingsSubject.stream;
|
||||||
|
|
||||||
Future<List<AddonInfo>> getAddons({bool allowCache = true}) {
|
Future<List<AddonInfo>> getAddons({bool allowCache = true}) {
|
||||||
return _api.getAddons(allowCache);
|
return _api.getAddons(allowCache);
|
||||||
@@ -189,6 +192,11 @@ class GeckoAddonService extends GeckoAddonEvents {
|
|||||||
_popupSubject.add((extensionId: extensionId, extensionName: extensionName));
|
_popupSubject.add((extensionId: extensionId, extensionName: extensionName));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onOpenAddonSettingsRequested(String addonId) {
|
||||||
|
_openAddonSettingsSubject.add(addonId);
|
||||||
|
}
|
||||||
|
|
||||||
GeckoAddonService.setUp({
|
GeckoAddonService.setUp({
|
||||||
BinaryMessenger? binaryMessenger,
|
BinaryMessenger? binaryMessenger,
|
||||||
GeckoAddonsApi? api,
|
GeckoAddonsApi? api,
|
||||||
@@ -207,5 +215,6 @@ class GeckoAddonService extends GeckoAddonEvents {
|
|||||||
await _browserIconSubject.close();
|
await _browserIconSubject.close();
|
||||||
await _pageIconSubject.close();
|
await _pageIconSubject.close();
|
||||||
await _popupSubject.close();
|
await _popupSubject.close();
|
||||||
|
await _openAddonSettingsSubject.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1962,10 +1962,7 @@ abstract class GeckoAddonsApi {
|
|||||||
);
|
);
|
||||||
|
|
||||||
@async
|
@async
|
||||||
List<AddonListing> getFeaturedAddonListings(
|
List<AddonListing> getFeaturedAddonListings(AddonStoreApp app, int pageSize);
|
||||||
AddonStoreApp app,
|
|
||||||
int pageSize,
|
|
||||||
);
|
|
||||||
|
|
||||||
void invokeAddonAction(String extensionId, WebExtensionActionType actionType);
|
void invokeAddonAction(String extensionId, WebExtensionActionType actionType);
|
||||||
|
|
||||||
@@ -2026,6 +2023,8 @@ abstract class GeckoAddonEvents {
|
|||||||
);
|
);
|
||||||
|
|
||||||
void onWebExtensionPopupRequested(String extensionId, String extensionName);
|
void onWebExtensionPopupRequested(String extensionId, String extensionName);
|
||||||
|
|
||||||
|
void onOpenAddonSettingsRequested(String addonId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostApi()
|
@HostApi()
|
||||||
|
|||||||
Reference in New Issue
Block a user