Bug 1379833 Part 2: Display permissions dialog during extension install on Android draft
authorAndrew Swan <aswan@mozilla.com>
Tue, 15 Aug 2017 08:35:11 -0700
changeset 661108 7d722024b371747c41d3f2e2c0fbdf136e1609aa
parent 661107 02e748210100e2f3b0a08b77746d0171f4085149
child 730449 848383488050d5e18e0653803e56711c41aa8820
push id78628
push useraswan@mozilla.com
push dateThu, 07 Sep 2017 22:35:00 +0000
bugs1379833
milestone57.0a1
Bug 1379833 Part 2: Display permissions dialog during extension install on Android MozReview-Commit-ID: 721i0B0lC4n
mobile/android/app/mobile.js
mobile/android/app/src/main/res/drawable/ic_extension.xml
mobile/android/app/src/main/res/layout/extension_permissions_dialog.xml
mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
mobile/android/base/java/org/mozilla/gecko/extensions/ExtensionPermissionsHelper.java
mobile/android/base/moz.build
mobile/android/chrome/content/ExtensionPermissions.js
mobile/android/chrome/content/browser.js
mobile/android/chrome/jar.mn
mobile/android/locales/en-US/chrome/browser.properties
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -225,16 +225,18 @@ pref("extensions.getAddons.getWithPerfor
 
 /* preference for the locale picker */
 pref("extensions.getLocales.get.url", "");
 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';");
 
 /* block popups by default, and notify the user about blocked popups */
 pref("dom.disable_open_during_load", true);
 pref("privacy.popups.showBrowserMessage", true);
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/app/src/main/res/drawable/ic_extension.xml
@@ -0,0 +1,4 @@
+<vector android:height="24dp" android:viewportHeight="64.0"
+    android:viewportWidth="64.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#FF66CC52" android:pathData="M42,62c2.2,0 4,-1.8 4,-4l0,-14.2c0,0 0.4,-3.7 2.8,-3.7c2.4,0 2.2,3.9 6.7,3.9c2.3,0 6.2,-1.2 6.2,-8.2c0,-7 -3.9,-7.9 -6.2,-7.9c-4.5,0 -4.3,3.7 -6.7,3.7c-2.4,0 -2.8,-3.8 -2.8,-3.8V22c0,-2.2 -1.8,-4 -4,-4H31.5c0,0 -3.4,-0.6 -3.4,-3c0,-2.4 3.8,-2.6 3.8,-7.1c0,-2.3 -1.3,-5.9 -8.3,-5.9s-8,3.6 -8,5.9c0,4.5 3.4,4.7 3.4,7.1c0,2.4 -3.4,3 -3.4,3H6c-2.2,0 -4,1.8 -4,4l0,7.8c0,0 -0.4,6 4.4,6c3.1,0 3.2,-4.1 7.3,-4.1c2,0 4,1.9 4,6c0,4.2 -2,6.3 -4,6.3c-4,0 -4.2,-4.1 -7.3,-4.1c-4.8,0 -4.4,5.8 -4.4,5.8L2,58c0,2.2 1.8,4 4,4H19c0,0 6.3,0.4 6.3,-4.4c0,-3.1 -4,-3.6 -4,-7.7c0,-2 2.2,-4.5 6.4,-4.5c4.2,0 6.6,2.5 6.6,4.5c0,4 -3.9,4.6 -3.9,7.7c0,4.9 6.3,4.4 6.3,4.4H42z"/>
+</vector>
new file mode 100644
--- /dev/null
+++ b/mobile/android/app/src/main/res/layout/extension_permissions_dialog.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:orientation="vertical"
+              android:paddingLeft="10dp"
+              android:paddingRight="10dp">
+
+    <LinearLayout android:layout_width="match_parent"
+                  android:layout_height="wrap_content"
+                  android:orientation="horizontal">
+        <ImageView android:id="@+id/extension_permission_icon"
+                   android:layout_width="60dp"
+                   android:layout_height="60dp"
+                   android:layout_gravity="center_horizontal"
+                   android:padding="@dimen/doorhanger_section_padding_small"/>
+        <TextView android:id="@+id/extension_permission_header"
+                  android:layout_width="match_parent"
+                  android:layout_height="wrap_content"
+                  android:layout_gravity="center_vertical"/>
+    </LinearLayout>
+
+    <TextView android:id="@+id/extension_permission_body"
+              android:layout_width="match_parent"
+              android:layout_height="wrap_content"/>
+</LinearLayout>
+
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -84,16 +84,17 @@ import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.SuggestedSites;
 import org.mozilla.gecko.delegates.BookmarkStateChangeDelegate;
 import org.mozilla.gecko.delegates.BrowserAppDelegate;
 import org.mozilla.gecko.delegates.OfflineTabStatusDelegate;
 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;
 import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
 import org.mozilla.gecko.home.BrowserSearch;
 import org.mozilla.gecko.home.HomeBanner;
 import org.mozilla.gecko.home.HomeConfig;
 import org.mozilla.gecko.home.HomeConfig.PanelType;
@@ -310,16 +311,18 @@ public class BrowserApp extends GeckoApp
     private int mToolbarHeight;
 
     private SharedPreferencesHelper mSharedPreferencesHelper;
 
     private ReadingListHelper mReadingListHelper;
 
     private AccountsHelper mAccountsHelper;
 
+    private ExtensionPermissionsHelper mExtensionPermissionsHelper;
+
     // The tab to be selected on editing mode exit.
     private Integer mTargetTabForEditingMode;
 
     private final TabEditingState mLastTabEditingState = new TabEditingState();
 
     // The animator used to toggle HomePager visibility has a race where if the HomePager is shown
     // (starting the animation), the HomePager is hidden, and the HomePager animation completes,
     // both the web content and the HomePager will be hidden. This flag is used to prevent the
@@ -813,16 +816,17 @@ public class BrowserApp extends GeckoApp
         // Init suggested sites engine in BrowserDB.
         final SuggestedSites suggestedSites = new SuggestedSites(appContext, distribution);
         final BrowserDB db = BrowserDB.from(profile);
         db.setSuggestedSites(suggestedSites);
 
         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);
             if (nfc != null) {
                 nfc.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() {
                     @Override
                     public NdefMessage createNdefMessage(NfcEvent event) {
                         Tab tab = Tabs.getInstance().getSelectedTab();
@@ -1531,16 +1535,21 @@ public class BrowserApp extends GeckoApp
             mReadingListHelper = null;
         }
 
         if (mAccountsHelper != null) {
             mAccountsHelper.uninit();
             mAccountsHelper = null;
         }
 
+        if (mExtensionPermissionsHelper != null) {
+            mExtensionPermissionsHelper.uninit();
+            mExtensionPermissionsHelper = null;
+        }
+
         mSearchEngineManager.unregisterListeners();
 
         EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
             "Search:Keyword",
             null);
 
         EventDispatcher.getInstance().unregisterUiThreadListener(this,
             "Accessibility:Enabled",
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/extensions/ExtensionPermissionsHelper.java
@@ -0,0 +1,80 @@
+/* -*- 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 static final String LOGTAG = "GeckoAddonManager";
+    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);
+            ((TextView) view.findViewById(R.id.extension_permission_header)).setText(message.getString("header"));
+            ((TextView) view.findViewById(R.id.extension_permission_body)).setText(message.getString("message"));
+
+            final String iconUrl = message.getString("icon");
+            final ImageView iconView = (ImageView) view.findViewById(R.id.extension_permission_icon);
+
+            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);
+                }
+            });
+
+            ResourceDrawableUtils.getDrawable(mContext, iconUrl, new ResourceDrawableUtils.BitmapLoader() {
+                    @Override
+                    public void onBitmapFound(final Drawable d) {
+                        iconView.setImageDrawable(d);
+                    }
+                });
+
+            final AlertDialog dialog = builder.create();
+            dialog.show();
+        }
+    }
+}
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -623,16 +623,17 @@ gbjar.sources += ['java/org/mozilla/geck
     'dlc/VerifyAction.java',
     'DoorHangerPopup.java',
     'DownloadsIntegration.java',
     'drawable/DrawableWrapper.java',
     'drawable/ShiftDrawable.java',
     'DynamicToolbar.java',
     'EditBookmarkDialog.java',
     'Experiments.java',
+    'extensions/ExtensionPermissionsHelper.java',
     'FilePicker.java',
     'FilePickerResultHandler.java',
     'FindInPageBar.java',
     'firstrun/DataPanel.java',
     'firstrun/FirstrunAnimationContainer.java',
     'firstrun/FirstrunPager.java',
     'firstrun/FirstrunPagerConfig.java',
     'firstrun/FirstrunPanel.java',
new file mode 100644
--- /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 "drawable://ic_extension";
+  },
+
+  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;
+    }
+  },
+};
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -146,16 +146,20 @@ lazilyLoadedBrowserScripts.forEach(funct
     Services.scriptloader.loadSubScript(script, sandbox);
     return sandbox[name];
   });
 });
 
 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) {
   let [name, notifications, script] = aScript;
   XPCOMUtils.defineLazyGetter(window, name, function() {
     let sandbox = {};
     Services.scriptloader.loadSubScript(script, sandbox);
     return sandbox[name];
--- a/mobile/android/chrome/jar.mn
+++ b/mobile/android/chrome/jar.mn
@@ -55,16 +55,17 @@ chrome.jar:
 #endif
   content/aboutAccounts.xhtml          (content/aboutAccounts.xhtml)
   content/aboutAccounts.js             (content/aboutAccounts.js)
   content/aboutLogins.xhtml            (content/aboutLogins.xhtml)
   content/aboutLogins.js               (content/aboutLogins.js)
 #ifndef RELEASE_OR_BETA
   content/WebcompatReporter.js         (content/WebcompatReporter.js)
 #endif
+  content/ExtensionPermissions.js      (content/ExtensionPermissions.js)
 
 % content branding %content/branding/
 
 % override chrome://global/content/config.xul chrome://browser/content/config.xhtml
 % override chrome://global/content/netError.xhtml chrome://browser/content/netError.xhtml
 % override chrome://mozapps/content/extensions/extensions.xul chrome://browser/content/aboutAddons.xhtml
 
 # L10n resource overrides.
--- a/mobile/android/locales/en-US/chrome/browser.properties
+++ b/mobile/android/locales/en-US/chrome/browser.properties
@@ -104,16 +104,79 @@ blockPopups.label2=Popups
 xpinstallPromptWarning2=%S prevented this site (%S) from asking you to install software on your device.
 xpinstallPromptWarningLocal=%S prevented this add-on (%S) from installing on your device.
 xpinstallPromptWarningDirect=%S prevented an add-on from installing on your device.
 xpinstallPromptAllowButton=Allow
 xpinstallDisabledMessageLocked=Software installation has been disabled by your system administrator.
 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
 identity.identified.state_and_country=%S, %S
 identity.identified.title_with_country=%S (%S)
 
 # Geolocation UI
 geolocation.allow=Share