diff --git a/mobile/android/app/mobile.js b/mobile/android/app/mobile.js
index 8056e8b478a5..c391a4c93f31 100644
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -230,6 +230,8 @@ pref("extensions.compatability.locales.buildid", "0");
/* Don't let XPIProvider install distribution add-ons; we do our own thing on mobile. */
pref("extensions.installDistroAddons", false);
+pref("extensions.webextPermissionPrompts", true);
+
// Add-on content security policies.
pref("extensions.webextensions.base-content-security-policy", "script-src 'self' https://* moz-extension: blob: filesystem: 'unsafe-eval' 'unsafe-inline'; object-src 'self' https://* moz-extension: blob: filesystem:;");
pref("extensions.webextensions.default-content-security-policy", "script-src 'self'; object-src 'self';");
diff --git a/mobile/android/app/src/main/res/drawable/ic_extension.xml b/mobile/android/app/src/main/res/drawable/ic_extension.xml
new file mode 100644
index 000000000000..ad192170b456
--- /dev/null
+++ b/mobile/android/app/src/main/res/drawable/ic_extension.xml
@@ -0,0 +1,4 @@
+
+
+
diff --git a/mobile/android/app/src/main/res/layout/extension_permissions_dialog.xml b/mobile/android/app/src/main/res/layout/extension_permissions_dialog.xml
new file mode 100644
index 000000000000..06949b88e3d0
--- /dev/null
+++ b/mobile/android/app/src/main/res/layout/extension_permissions_dialog.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
index 857029896da9..7598b7a1cb20 100644
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -89,6 +89,7 @@ import org.mozilla.gecko.delegates.ScreenshotDelegate;
import org.mozilla.gecko.distribution.Distribution;
import org.mozilla.gecko.distribution.DistributionStoreCallback;
import org.mozilla.gecko.dlc.DownloadContentService;
+import org.mozilla.gecko.extensions.ExtensionPermissionsHelper;
import org.mozilla.gecko.firstrun.FirstrunAnimationContainer;
import org.mozilla.gecko.gfx.DynamicToolbarAnimator;
import org.mozilla.gecko.gfx.DynamicToolbarAnimator.PinReason;
@@ -315,6 +316,8 @@ public class BrowserApp extends GeckoApp
private AccountsHelper mAccountsHelper;
+ private ExtensionPermissionsHelper mExtensionPermissionsHelper;
+
// The tab to be selected on editing mode exit.
private Integer mTargetTabForEditingMode;
@@ -818,6 +821,7 @@ public class BrowserApp extends GeckoApp
mSharedPreferencesHelper = new SharedPreferencesHelper(appContext);
mReadingListHelper = new ReadingListHelper(appContext, profile);
mAccountsHelper = new AccountsHelper(appContext, profile);
+ mExtensionPermissionsHelper = new ExtensionPermissionsHelper(this);
if (AppConstants.MOZ_ANDROID_BEAM) {
NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this);
@@ -1536,6 +1540,11 @@ public class BrowserApp extends GeckoApp
mAccountsHelper = null;
}
+ if (mExtensionPermissionsHelper != null) {
+ mExtensionPermissionsHelper.uninit();
+ mExtensionPermissionsHelper = null;
+ }
+
mSearchEngineManager.unregisterListeners();
EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
diff --git a/mobile/android/base/java/org/mozilla/gecko/extensions/ExtensionPermissionsHelper.java b/mobile/android/base/java/org/mozilla/gecko/extensions/ExtensionPermissionsHelper.java
new file mode 100644
index 000000000000..5fe483997e02
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/extensions/ExtensionPermissionsHelper.java
@@ -0,0 +1,85 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.extensions;
+
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.util.BundleEventListener;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.GeckoBundle;
+import org.mozilla.gecko.util.ResourceDrawableUtils;
+import org.mozilla.gecko.R;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+public class ExtensionPermissionsHelper implements BundleEventListener {
+ private final Context mContext;
+
+ public ExtensionPermissionsHelper(Context context) {
+ mContext = context;
+
+ EventDispatcher.getInstance().registerUiThreadListener(this,
+ "Extension:PermissionPrompt");
+ }
+
+ public void uninit() {
+ EventDispatcher.getInstance().unregisterUiThreadListener(this,
+ "Extension:PermissionPrompt");
+ }
+
+ @Override // BundleEventListener
+ public void handleMessage(final String event, final GeckoBundle message,
+ final EventCallback callback) {
+ if ("Extension:PermissionPrompt".equals(event)) {
+ final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
+
+ final View view = LayoutInflater.from(mContext)
+ .inflate(R.layout.extension_permissions_dialog, null);
+ builder.setView(view);
+
+ final TextView headerText = (TextView) view.findViewById(R.id.extension_permission_header);
+ headerText.setText(message.getString("header"));
+
+ final TextView bodyText = (TextView) view.findViewById(R.id.extension_permission_body);
+ bodyText.setText(message.getString("message"));
+
+ builder.setPositiveButton(message.getString("acceptText"), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ callback.sendSuccess(true);
+ }
+ });
+ builder.setNegativeButton(message.getString("cancelText"), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ callback.sendSuccess(false);
+ }
+ });
+
+ final String iconUrl = message.getString("icon");
+ if ("DEFAULT".equals(iconUrl)) {
+ headerText.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_extension, 0, 0, 0);
+ } else {
+ ResourceDrawableUtils.getDrawable(mContext, iconUrl, new ResourceDrawableUtils.BitmapLoader() {
+ @Override
+ public void onBitmapFound(final Drawable d) {
+ headerText.setCompoundDrawablesWithIntrinsicBounds(d, null, null, null);
+ }
+ });
+ }
+
+ final AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+ }
+}
diff --git a/mobile/android/base/moz.build b/mobile/android/base/moz.build
index dabe3328da17..9bc4ac74ea7f 100644
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -628,6 +628,7 @@ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
'DynamicToolbar.java',
'EditBookmarkDialog.java',
'Experiments.java',
+ 'extensions/ExtensionPermissionsHelper.java',
'FilePicker.java',
'FilePickerResultHandler.java',
'FindInPageBar.java',
diff --git a/mobile/android/chrome/content/ExtensionPermissions.js b/mobile/android/chrome/content/ExtensionPermissions.js
new file mode 100644
index 000000000000..6c358f2532f6
--- /dev/null
+++ b/mobile/android/chrome/content/ExtensionPermissions.js
@@ -0,0 +1,68 @@
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionData",
+ "resource://gre/modules/Extension.jsm");
+
+var ExtensionPermissions = {
+ // Prepare the strings needed for a permission notification.
+ _prepareStrings(info) {
+ let appName = Strings.brand.GetStringFromName("brandShortName");
+ let info2 = Object.assign({appName, addonName: info.addon.name}, info);
+ let strings = ExtensionData.formatPermissionStrings(info2, Strings.browser);
+
+ // We dump the main body of the dialog into a big android
+ // TextView. Build a big string with the full contents here.
+ let message = "";
+ if (strings.msgs.length > 0) {
+ message = [strings.listIntro, ...strings.msgs.map(s => `\u2022 ${s}`)].join("\n");
+ }
+
+ return {
+ header: strings.header || strings.text,
+ message,
+ acceptText: strings.acceptText,
+ cancelText: strings.cancelText,
+ };
+ },
+
+ // Prepare an icon for a permission notification
+ _prepareIcon(iconURL) {
+ // We can render pngs with ResourceDrawableUtils
+ if (iconURL.endsWith(".png")) {
+ return iconURL;
+ }
+
+ // If we can't render an icon, show the default
+ return "DEFAULT";
+ },
+
+ async observe(subject, topic, data) {
+ switch (topic) {
+ case "webextension-permission-prompt": {
+ let {target, info} = subject.wrappedJSObject;
+
+ let details = this._prepareStrings(info);
+ details.icon = this._prepareIcon(info.icon);
+ details.type = "Extension:PermissionPrompt";
+ let accepted = await EventDispatcher.instance.sendRequestForResult(details);
+
+ if (accepted) {
+ info.resolve();
+ } else {
+ info.reject();
+ }
+ break;
+ }
+
+ case "webextension-update-permissions":
+ // To be implemented in bug 1391579, just auto-approve until then
+ subject.wrappedJSObject.resolve();
+ break;
+
+ case "webextension-optional-permission-prompt":
+ // To be implemented in bug 1392176, just auto-approve until then
+ subject.wrappedJSObject.resolve(true);
+ break;
+ }
+ },
+};
diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js
index 286d7b2f4b1e..d1d6906e2a07 100644
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -151,6 +151,10 @@ lazilyLoadedBrowserScripts.forEach(function (aScript) {
var lazilyLoadedObserverScripts = [
["MemoryObserver", ["memory-pressure", "Memory:Dump"], "chrome://browser/content/MemoryObserver.js"],
["ConsoleAPI", ["console-api-log-event"], "chrome://browser/content/ConsoleAPI.js"],
+ ["ExtensionPermissions", ["webextension-permission-prompt",
+ "webextension-update-permissions",
+ "webextension-optional-permission-prompt"],
+ "chrome://browser/content/ExtensionPermissions.js"],
];
lazilyLoadedObserverScripts.forEach(function (aScript) {
diff --git a/mobile/android/chrome/jar.mn b/mobile/android/chrome/jar.mn
index 791f0ec94992..5a433ab8dccb 100644
--- a/mobile/android/chrome/jar.mn
+++ b/mobile/android/chrome/jar.mn
@@ -60,6 +60,7 @@ chrome.jar:
#ifndef RELEASE_OR_BETA
content/WebcompatReporter.js (content/WebcompatReporter.js)
#endif
+ content/ExtensionPermissions.js (content/ExtensionPermissions.js)
% content branding %content/branding/
diff --git a/mobile/android/locales/en-US/chrome/browser.properties b/mobile/android/locales/en-US/chrome/browser.properties
index cb4ca4c0af1d..2a0b2abf56fa 100644
--- a/mobile/android/locales/en-US/chrome/browser.properties
+++ b/mobile/android/locales/en-US/chrome/browser.properties
@@ -109,6 +109,69 @@ xpinstallDisabledMessageLocked=Software installation has been disabled by your s
xpinstallDisabledMessage2=Software installation is currently disabled. Press Enable and try again.
xpinstallDisabledButton=Enable
+# LOCALIZATION NOTE (webextPerms.header)
+# This string is used as a header in the webextension permissions dialog,
+# %S is replaced with the localized name of the extension being installed.
+# See https://bug1308309.bmoattachments.org/attachment.cgi?id=8814612
+# for an example of the full dialog.
+# Note, this string will be used as raw markup. Avoid characters like <, >, &
+webextPerms.header=Add %S?
+
+# LOCALIZATION NOTE (webextPerms.listIntro)
+# This string will be followed by a list of permissions requested
+# by the webextension.
+webextPerms.listIntro=It requires your permission to:
+webextPerms.add.label=Add
+webextPerms.add.accessKey=A
+webextPerms.cancel.label=Cancel
+webextPerms.cancel.accessKey=C
+
+webextPerms.description.bookmarks=Read and modify bookmarks
+webextPerms.description.browserSettings=Read and modify browser settings
+webextPerms.description.clipboardRead=Get data from the clipboard
+webextPerms.description.clipboardWrite=Input data to the clipboard
+webextPerms.description.downloads=Download files and read and modify the browser’s download history
+webextPerms.description.geolocation=Access your location
+webextPerms.description.history=Access browsing history
+webextPerms.description.management=Monitor extension usage and manage themes
+# LOCALIZATION NOTE (webextPerms.description.nativeMessaging)
+# %S will be replaced with the name of the application
+webextPerms.description.nativeMessaging=Exchange messages with programs other than %S
+webextPerms.description.notifications=Display notifications to you
+webextPerms.description.privacy=Read and modify privacy settings
+webextPerms.description.sessions=Access recently closed tabs
+webextPerms.description.tabs=Access browser tabs
+webextPerms.description.topSites=Access browsing history
+webextPerms.description.unlimitedStorage=Store unlimited amount of client-side data
+webextPerms.description.webNavigation=Access browser activity during navigation
+
+webextPerms.hostDescription.allUrls=Access your data for all websites
+
+# LOCALIZATION NOTE (webextPerms.hostDescription.wildcard)
+# %S will be replaced by the DNS domain for which a webextension
+# is requesting access (e.g., mozilla.org)
+webextPerms.hostDescription.wildcard=Access your data for sites in the %S domain
+
+# LOCALIZATION NOTE (webextPerms.hostDescription.tooManyWildcards):
+# Semi-colon list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 will be replaced by an integer indicating the number of additional
+# domains for which this webextension is requesting permission.
+webextPerms.hostDescription.tooManyWildcards=Access your data in #1 other domain;Access your data in #1 other domains
+
+# LOCALIZATION NOTE (webextPerms.hostDescription.oneSite)
+# %S will be replaced by the DNS host name for which a webextension
+# is requesting access (e.g., www.mozilla.org)
+webextPerms.hostDescription.oneSite=Access your data for %S
+
+# LOCALIZATION NOTE (webextPerms.hostDescription.tooManySites)
+# Semi-colon list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 will be replaced by an integer indicating the number of additional
+# hosts for which this webextension is requesting permission.
+webextPerms.hostDescription.tooManySites=Access your data on #1 other site;Access your data on #1 other sites
+
+
# Site Identity
identity.identified.verifier=Verified by: %S
identity.identified.verified_by_you=You have added a security exception for this site