Bug 1557009 - Add ContentBlockingController to GeckoSession to allow managing exceptions list for content blocking. r=snorp!,ehsan!,#geckoview-reviewers! a=lizzard
authorDylan Roeh <droeh@mozilla.com>
Fri, 13 Sep 2019 14:58:56 -0500
changeset 555014 6c496f7af7076b770955b70548ce35944192bfdd
parent 555013 915a44db40d9bded936e67ae9b2d0f1e39f8c555
child 555015 7ee95bd56010b7559ab2155e0bfe4bf88be78fbb
push id2165
push userffxbld-merge
push dateMon, 14 Oct 2019 16:30:58 +0000
treeherdermozilla-release@0eae18af659f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssnorp, lizzard
bugs1557009
milestone70.0
Bug 1557009 - Add ContentBlockingController to GeckoSession to allow managing exceptions list for content blocking. r=snorp!,ehsan!,#geckoview-reviewers! a=lizzard
mobile/android/components/geckoview/GeckoViewStartup.js
mobile/android/geckoview/api.txt
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/ContentBlockingController.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntime.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md
mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
mobile/android/geckoview_example/src/main/res/menu/actions.xml
mobile/android/geckoview_example/src/main/res/values/strings.xml
mobile/android/modules/geckoview/GeckoViewContentBlockingController.jsm
mobile/android/modules/geckoview/moz.build
toolkit/components/antitracking/ContentBlockingAllowList.jsm
--- a/mobile/android/components/geckoview/GeckoViewStartup.js
+++ b/mobile/android/components/geckoview/GeckoViewStartup.js
@@ -67,16 +67,33 @@ GeckoViewStartup.prototype = {
           module: "resource://gre/modules/GeckoViewStorageController.jsm",
           ged: [
             "GeckoView:ClearData",
             "GeckoView:ClearSessionContextData",
             "GeckoView:ClearHostData",
           ],
         });
 
+        GeckoViewUtils.addLazyGetter(
+          this,
+          "GeckoViewContentBlockingController",
+          {
+            module:
+              "resource://gre/modules/GeckoViewContentBlockingController.jsm",
+            ged: [
+              "ContentBlocking:AddException",
+              "ContentBlocking:RemoveException",
+              "ContentBlocking:CheckException",
+              "ContentBlocking:SaveList",
+              "ContentBlocking:RestoreList",
+              "ContentBlocking:ClearList",
+            ],
+          }
+        );
+
         GeckoViewUtils.addLazyPrefObserver(
           {
             name: "geckoview.console.enabled",
             default: false,
           },
           {
             handler: _ => this.GeckoViewConsole,
           }
--- a/mobile/android/geckoview/api.txt
+++ b/mobile/android/geckoview/api.txt
@@ -54,16 +54,17 @@ import java.net.InetAddress;
 import java.nio.ByteBuffer;
 import java.util.AbstractSequentialList;
 import java.util.List;
 import java.util.Map;
 import org.json.JSONObject;
 import org.mozilla.geckoview.AllowOrDeny;
 import org.mozilla.geckoview.CompositorController;
 import org.mozilla.geckoview.ContentBlocking;
+import org.mozilla.geckoview.ContentBlockingController;
 import org.mozilla.geckoview.DynamicToolbarAnimator;
 import org.mozilla.geckoview.GeckoDisplay;
 import org.mozilla.geckoview.GeckoResponse;
 import org.mozilla.geckoview.GeckoResult;
 import org.mozilla.geckoview.GeckoRuntime;
 import org.mozilla.geckoview.GeckoRuntimeSettings;
 import org.mozilla.geckoview.GeckoSession;
 import org.mozilla.geckoview.GeckoSessionSettings;
@@ -207,16 +208,33 @@ package org.mozilla.geckoview {
     ctor public Builder();
     method @NonNull public ContentBlocking.Settings.Builder antiTracking(int);
     method @NonNull public ContentBlocking.Settings.Builder cookieBehavior(int);
     method @NonNull public ContentBlocking.Settings.Builder cookieLifetime(int);
     method @NonNull public ContentBlocking.Settings.Builder safeBrowsing(int);
     method @NonNull protected ContentBlocking.Settings newSettings(@Nullable ContentBlocking.Settings);
   }
 
+  @AnyThread public class ContentBlockingController {
+    ctor public ContentBlockingController();
+    method @UiThread public void addException(@NonNull GeckoSession);
+    method @UiThread @NonNull public GeckoResult<Boolean> checkException(@NonNull GeckoSession);
+    method @UiThread public void clearExceptionList();
+    method @UiThread public void removeException(@NonNull GeckoSession);
+    method @UiThread public void restoreExceptionList(@NonNull ContentBlockingController.ExceptionList);
+    method @UiThread @NonNull public GeckoResult<ContentBlockingController.ExceptionList> saveExceptionList();
+  }
+
+  @AnyThread public class ContentBlockingController.ExceptionList {
+    ctor public ExceptionList(@NonNull String);
+    ctor public ExceptionList(@NonNull JSONObject);
+    method @NonNull public String[] getUris();
+    method @NonNull public JSONObject toJson();
+  }
+
   public class CrashReporter {
     ctor public CrashReporter();
     method @AnyThread @NonNull public static GeckoResult<String> sendCrashReport(@NonNull Context, @NonNull Intent, @NonNull String);
     method @AnyThread @NonNull public static GeckoResult<String> sendCrashReport(@NonNull Context, @NonNull Bundle, @NonNull String);
     method @AnyThread @NonNull public static GeckoResult<String> sendCrashReport(@NonNull Context, @NonNull File, @NonNull File, @NonNull String);
     method @AnyThread @NonNull public static GeckoResult<String> sendCrashReport(@NonNull Context, @NonNull File, @NonNull Map<String,String>, @NonNull String);
   }
 
@@ -302,16 +320,17 @@ package org.mozilla.geckoview {
     ctor public UncaughtException(Throwable);
   }
 
   public final class GeckoRuntime implements Parcelable {
     method @UiThread public void attachTo(@NonNull Context);
     method @UiThread public void configurationChanged(@NonNull Configuration);
     method @UiThread @NonNull public static GeckoRuntime create(@NonNull Context);
     method @UiThread @NonNull public static GeckoRuntime create(@NonNull Context, @NonNull GeckoRuntimeSettings);
+    method @UiThread @NonNull public ContentBlockingController getContentBlockingController();
     method @UiThread @NonNull public static synchronized GeckoRuntime getDefault(@NonNull Context);
     method @UiThread @Nullable public GeckoRuntime.Delegate getDelegate();
     method @UiThread @Nullable public File getProfileDir();
     method @AnyThread @NonNull public GeckoRuntimeSettings getSettings();
     method @UiThread @NonNull public StorageController getStorageController();
     method @UiThread @NonNull public RuntimeTelemetry getTelemetry();
     method @UiThread @NonNull public WebExtensionController getWebExtensionController();
     method @UiThread @Nullable public WebNotificationDelegate getWebNotificationDelegate();
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/ContentBlockingController.java
@@ -0,0 +1,198 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * vim: ts=4 sw=4 expandtab:
+ * 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.geckoview;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.util.GeckoBundle;
+
+import android.support.annotation.AnyThread;
+import android.support.annotation.Nullable;
+import android.support.annotation.NonNull;
+import android.support.annotation.UiThread;
+import android.util.Log;
+
+/**
+ * ContentBlockingController is used to manage and modify the content
+ * blocking exception list. This list is shared across all sessions.
+ */
+@AnyThread
+public class ContentBlockingController {
+    private static final String LOGTAG = "GeckoContentBlocking";
+
+    /**
+     * ExceptionList represents a content blocking exception list exported
+     * from Gecko. It can be used to persist the list or to inspect the URIs
+     * present in the list.
+     */
+    @AnyThread
+    public class ExceptionList {
+        private final @NonNull GeckoBundle mBundle;
+
+        /* package */ ExceptionList(final @NonNull GeckoBundle bundle) {
+            mBundle = new GeckoBundle(bundle);
+        }
+
+        /**
+         * Returns the URIs currently on the content blocking exception list.
+         *
+         * @return A string array containing the URIs.
+         */
+        public @NonNull String[] getUris() {
+            return mBundle.getStringArray("uris");
+        }
+
+        /**
+         * Returns a string representation of the content blocking exception list.
+         * May be null if the JSON to string conversion fails for any reason.
+         *
+         * @return A string representing the exception list.
+         */
+        @Override
+        public @Nullable String toString() {
+            String res;
+            try {
+                res = mBundle.toJSONObject().toString();
+            } catch (JSONException e) {
+                Log.e(LOGTAG, "Could not convert session state to string.");
+                res = null;
+            }
+
+            return res;
+        }
+
+        /**
+         * Returns a JSONObject representation of the content blocking exception list.
+         *
+         * @return A JSONObject representing the exception list.
+         *
+         * @throws JSONException if conversion to JSONObject fails.
+         */
+        public @NonNull JSONObject toJson() throws JSONException {
+            return mBundle.toJSONObject();
+        }
+
+        /**
+         * Creates a new exception list from a string. The string should be valid
+         * output from {@link toString}.
+         *
+         * @param savedList A string representation of a saved exception list.
+         *
+         * @throws JSONException if the string representation no longer represents valid JSON.
+         */
+        public ExceptionList(final @NonNull String savedList) throws JSONException {
+            mBundle = GeckoBundle.fromJSONObject(new JSONObject(savedList));
+        }
+
+        /**
+         * Creates a new exception list from a JSONObject. The JSONObject should be valid
+         * output from {@link toJson}.
+         *
+         * @param savedList A JSONObject representation of a saved exception list.
+         *
+         * @throws JSONException if the JSONObject cannot be converted for any reason.
+         */
+        public ExceptionList(final @NonNull JSONObject savedList) throws JSONException {
+            mBundle = GeckoBundle.fromJSONObject(savedList);
+        }
+
+        /* package */ GeckoBundle getBundle() {
+            return mBundle;
+        }
+    }
+
+    /**
+     * Add a content blocking exception for the site currently loaded by the supplied
+     * {@link GeckoSession}.
+     *
+     * @param session A {@link GeckoSession} whose site will be added to the content
+     *                blocking exceptions list.
+     */
+    @UiThread
+    public void addException(final @NonNull GeckoSession session) {
+        final GeckoBundle msg = new GeckoBundle(1);
+        msg.putString("sessionId", session.getId());
+        EventDispatcher.getInstance().dispatch("ContentBlocking:AddException", msg);
+    }
+
+    /**
+     * Remove an exception for the site currently loaded by the supplied {@link GeckoSession}
+     * from the content blocking exception list, if there is such an exception. If there is no
+     * such exception, this is a no-op.
+     *
+     * @param session A {@link GeckoSession} whose site will be removed from the content
+     *                blocking exceptions list.
+     */
+    @UiThread
+    public void removeException(final @NonNull GeckoSession session) {
+        final GeckoBundle msg = new GeckoBundle(1);
+        msg.putString("sessionId", session.getId());
+        EventDispatcher.getInstance().dispatch("ContentBlocking:RemoveException", msg);
+    }
+
+    /**
+     * Check whether or not there is an exception for the site currently loaded by the
+     * supplied {@link GeckoSession}.
+     *
+     * @param session A {@link GeckoSession} whose site will be checked against the content
+     *                blocking exceptions list.
+     *
+     * @return A {@link GeckoResult} which resolves to a Boolean indicating whether or
+     *         not the current site is on the exception list.
+     */
+    @UiThread
+    public @NonNull GeckoResult<Boolean> checkException(final @NonNull GeckoSession session) {
+        final CallbackResult<Boolean> result = new CallbackResult<Boolean>() {
+            @Override
+            public void sendSuccess(final Object value) {
+                complete((Boolean) value);
+            }
+        };
+        final GeckoBundle msg = new GeckoBundle(1);
+        msg.putString("sessionId", session.getId());
+        EventDispatcher.getInstance().dispatch("ContentBlocking:CheckException", msg, result);
+        return result;
+    }
+
+    /**
+     * Save the current content blocking exception list as an {@link ExceptionList}.
+     *
+     * @return An {@link ExceptionList} which can be used to restore the current
+     *         exception list.
+     */
+    @UiThread
+    public @NonNull GeckoResult<ExceptionList> saveExceptionList() {
+        final CallbackResult<ExceptionList> result = new CallbackResult<ExceptionList>() {
+            @Override
+            public void sendSuccess(final Object value) {
+                complete(new ExceptionList((GeckoBundle) value));
+            }
+        };
+        EventDispatcher.getInstance().dispatch("ContentBlocking:SaveList", null, result);
+        return result;
+    }
+
+    /**
+     * Restore the supplied {@link ExceptionList}, overwriting the existing exception list.
+     *
+     * @param list An {@link ExceptionList} originally created by {@link saveExceptionList}.
+     */
+    @UiThread
+    public void restoreExceptionList(final @NonNull ExceptionList list) {
+        EventDispatcher.getInstance().dispatch("ContentBlocking:RestoreList", list.getBundle());
+    }
+
+    /**
+     * Clear the content blocking exception list entirely.
+     */
+    @UiThread
+    public void clearExceptionList() {
+        EventDispatcher.getInstance().dispatch("ContentBlocking:ClearList", null);
+    }
+}
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntime.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntime.java
@@ -160,20 +160,22 @@ public final class GeckoRuntime implemen
     private static GeckoRuntime sRuntime;
     private GeckoRuntimeSettings mSettings;
     private Delegate mDelegate;
     private WebNotificationDelegate mNotificationDelegate;
     private RuntimeTelemetry mTelemetry;
     private final WebExtensionEventDispatcher mWebExtensionDispatcher;
     private StorageController mStorageController;
     private final WebExtensionController mWebExtensionController;
+    private final ContentBlockingController mContentBlockingController;
 
     private GeckoRuntime() {
         mWebExtensionDispatcher = new WebExtensionEventDispatcher();
         mWebExtensionController = new WebExtensionController(this, mWebExtensionDispatcher);
+        mContentBlockingController = new ContentBlockingController();
         if (sRuntime != null) {
             throw new IllegalStateException("Only one GeckoRuntime instance is allowed");
         }
         sRuntime = this;
     }
 
     @WrapForJNI
     @UiThread
@@ -351,16 +353,26 @@ public final class GeckoRuntime implemen
      * @return an instance of {@link WebExtensionController}.
      */
     @UiThread
     public @NonNull WebExtensionController getWebExtensionController() {
         return mWebExtensionController;
     }
 
     /**
+     * Returns the ContentBlockingController for this GeckoRuntime.
+     *
+     * @return An instance of {@link ContentBlockingController}.
+     */
+    @UiThread
+    public @NonNull ContentBlockingController getContentBlockingController() {
+        return mContentBlockingController;
+    }
+
+    /**
      * Register a {@link WebExtension} that will be run with this GeckoRuntime.
      *
      * <p>At this time, WebExtensions don't have access to any UI element and
      * cannot communicate with the application. Any UI element will be
      * ignored.</p>
      *
      * Example:
      * <pre><code>
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md
@@ -51,16 +51,18 @@ exclude: true
 - Added [`WebExtensionController`][70.18] and [`WebExtensionController.TabDelegate`][70.19] to handle
   [`browser.tabs.create`][70.20] calls by WebExtensions.
   ([bug 1539144]({{bugzilla}}1539144))
 - Added [`onCloseTab`][70.21] to [`WebExtensionController.TabDelegate`][70.19] to handle
   [`browser.tabs.remove`][70.22] calls by WebExtensions.
   ([bug 1565782]({{bugzilla}}1565782))
 - Added onSlowScript to [`ContentDelegate`][70.23] which allows handling of slow and hung scripts.
   ([bug 1621094]({{bugzilla}}1621094))
+- Added [`ContentBlockingController`][70.24], accessible via [`GeckoRuntime.getContentBlockingController`][70.25]
+  to allow modification and inspection of a content blocking exception list.
 
 [70.1]: {{javadoc_uri}}/GeckoSessionSettings.Builder.html#contextId-java.lang.String-
 [70.2]: {{javadoc_uri}}/StorageController.html#clearDataForSessionContext-java.lang.String-
 [70.3]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport-android.content.Context-java.io.File-java.io.File-java.lang.String-
 [70.4]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport-android.content.Context-java.io.File-java.util.Map-java.lang.String-
 [70.5]: {{javadoc_uri}}/GeckoView.html
 [70.6]: {{javadoc_uri}}/GeckoSession.html
 [70.7]: {{javadoc_uri}}/GeckoSession.PromptDelegate.html#CAPTURE_TYPE_NONE
@@ -75,16 +77,18 @@ exclude: true
 [70.16]: {{javadoc_uri}}/WebNotificationDelegate.html
 [70.17]: {{javadoc_uri}}/ContentBlocking.html
 [70.18]: {{javadoc_uri}}/WebExtensionController.html
 [70.19]: {{javadoc_uri}}/WebExtensionController.TabDelegate.html
 [70.20]: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/create
 [70.21]: {{javadoc_uri}}/WebExtensionController.TabDelegate.html#onCloseTab-org.mozilla.geckoview.WebExtension-org.mozilla.geckoview.GeckoSession-
 [70.22]: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/remove
 [70.23]: {{javadoc_uri}}/GeckoSession.ContentDelegate.html
+[70.24]: {{javadoc_uri}}/ContentBlockingController.html
+[70.25]: {{javadoc_uri}}/GeckoRuntime.html#getContentBlockingController--
 
 ## v69
 - Modified behavior of ['setAutomaticFontSizeAdjustment'][69.1] so that it no 
   longer has any effect on ['setFontInflationEnabled'][69.2]
 - Add [GeckoSession.LOAD_FLAGS_FORCE_ALLOW_DATA_URI][69.14]
 - Added [`GeckoResult.accept`][69.3] for consuming a result without
   transforming it.
 - [`GeckoSession.setMessageDelegate`][69.13] callers must now specify the
@@ -318,9 +322,9 @@ exclude: true
 [65.19]: {{javadoc_uri}}/GeckoSession.NavigationDelegate.LoadRequest.html#isRedirect
 [65.20]: {{javadoc_uri}}/GeckoSession.html#LOAD_FLAGS_BYPASS_CLASSIFIER    
 [65.21]: {{javadoc_uri}}/GeckoSession.ContentDelegate.ContextElement.html
 [65.22]: {{javadoc_uri}}/GeckoSession.ContentDelegate.html#onContextMenu-org.mozilla.geckoview.GeckoSession-int-int-org.mozilla.geckoview.GeckoSession.ContentDelegate.ContextElement-
 [65.23]: {{javadoc_uri}}/GeckoSession.FinderResult.html
 [65.24]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport-android.content.Context-android.os.Bundle-java.lang.String-
 [65.25]: {{javadoc_uri}}/GeckoResult.html
 
-[api-version]: 4199a7a889674d4402926366a7f4fb2f93de5c46
+[api-version]: f2d89facd57470b760be9a867173d02bf9040c75
--- a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
@@ -422,16 +422,28 @@ public class GeckoViewActivity extends A
                 mUseMultiprocess = !mUseMultiprocess;
                 recreateSession();
                 break;
             case R.id.action_tp:
                 mUseTrackingProtection = !mUseTrackingProtection;
                 updateTrackingProtection(session);
                 session.reload();
                 break;
+            case R.id.action_tpe:
+                sGeckoRuntime.getContentBlockingController().checkException(session).accept(value -> {
+                    if (value.booleanValue()) {
+                        sGeckoRuntime.getContentBlockingController().removeException(session);
+                        item.setTitle(R.string.tracking_protection_ex);
+                    } else {
+                        sGeckoRuntime.getContentBlockingController().addException(session);
+                        item.setTitle(R.string.tracking_protection_ex2);
+                    }
+                    session.reload();
+                });
+                break;
             case R.id.desktop_mode:
                 mDesktopMode = !mDesktopMode;
                 updateDesktopMode(session);
                 session.reload();
                 break;
             case R.id.action_pb:
                 mUsePrivateBrowsing = !mUsePrivateBrowsing;
                 recreateSession();
--- a/mobile/android/geckoview_example/src/main/res/menu/actions.xml
+++ b/mobile/android/geckoview_example/src/main/res/menu/actions.xml
@@ -1,14 +1,15 @@
 <?xml version="1.0" encoding="utf-8"?>
 <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
     <item android:title="@string/multiprocess" android:id="@+id/action_e10s" android:checkable="true"
         app:showAsAction="never"/>
     <item android:title="@string/tracking_protection" android:id="@+id/action_tp" app:showAsAction="never"
           android:checkable="true"/>
+    <item android:title="@string/tracking_protection_ex" android:id="@+id/action_tpe" app:showAsAction="never" />
     <item android:title="@string/desktop_mode" android:id="@+id/desktop_mode" android:checkable="true"
           app:showAsAction="never" />
     <item android:title="@string/private_browsing" android:checkable="true" android:id="@+id/action_pb"/>
     <item android:title="@string/remote_debugging" android:checkable="true" android:id="@+id/action_remote_debugging"/>
     <item android:title="@string/new_tab" android:id="@+id/action_new_tab"/>
     <item android:title="@string/close_tab" android:id="@+id/action_close_tab"/>
     <item android:title="@string/forward" android:id="@+id/action_forward"/>
     <item android:title="@string/reload" android:id="@+id/action_reload"/>
--- a/mobile/android/geckoview_example/src/main/res/values/strings.xml
+++ b/mobile/android/geckoview_example/src/main/res/values/strings.xml
@@ -16,16 +16,18 @@
     <string name="media_microphone">Microphone</string>
     <string name="media_other">Unknown source</string>
 
     <string name="crash_native">Native</string>
     <string name="crash_java">Java</string>
     <string name="crash_content_native">Content (Native)</string>
     <string name="multiprocess">Multiprocess</string>
     <string name="tracking_protection">Tracking Protection</string>
+    <string name="tracking_protection_ex">Disable TP</string>
+    <string name="tracking_protection_ex2">Reenable TP</string>
     <string name="private_browsing">Private Browsing</string>
     <string name="remote_debugging">Remote Debugging</string>
     <string name="forward">Forward</string>
     <string name="reload">Reload</string>
     <string name="crashed_title">GeckoView Example Crashed</string>
     <string name="crashed_text">Tap to report to Mozilla.</string>
     <string name="crashed_ignore">Ignore</string>
     <string name="device_sharing_microphone">Microphone is on</string>
new file mode 100644
--- /dev/null
+++ b/mobile/android/modules/geckoview/GeckoViewContentBlockingController.jsm
@@ -0,0 +1,93 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["GeckoViewContentBlockingController"];
+
+const { GeckoViewUtils } = ChromeUtils.import(
+  "resource://gre/modules/GeckoViewUtils.jsm"
+);
+const { XPCOMUtils } = ChromeUtils.import(
+  "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+ChromeUtils.defineModuleGetter(
+  this,
+  "E10SUtils",
+  "resource://gre/modules/E10SUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  ContentBlockingAllowList:
+    "resource://gre/modules/ContentBlockingAllowList.jsm",
+  Services: "resource://gre/modules/Services.jsm",
+});
+
+// eslint-disable-next-line no-unused-vars
+const { debug, warn } = GeckoViewUtils.initLogging(
+  "GeckoViewContentBlockingController"
+);
+
+const GeckoViewContentBlockingController = {
+  // Bundle event handler.
+  onEvent(aEvent, aData, aCallback) {
+    debug`onEvent: event=${aEvent}, data=${aData}`;
+
+    switch (aEvent) {
+      case "ContentBlocking:AddException": {
+        const sessionWindow = Services.ww.getWindowByName(
+          aData.sessionId,
+          this.window
+        );
+        ContentBlockingAllowList.add(sessionWindow.browser);
+        break;
+      }
+
+      case "ContentBlocking:RemoveException": {
+        const sessionWindow = Services.ww.getWindowByName(
+          aData.sessionId,
+          this.window
+        );
+        ContentBlockingAllowList.remove(sessionWindow.browser);
+        break;
+      }
+
+      case "ContentBlocking:CheckException": {
+        const sessionWindow = Services.ww.getWindowByName(
+          aData.sessionId,
+          this.window
+        );
+        const res = ContentBlockingAllowList.includes(sessionWindow.browser);
+        aCallback.onSuccess(res);
+        break;
+      }
+
+      case "ContentBlocking:SaveList": {
+        const list = ContentBlockingAllowList.getAllowListedPrincipals();
+        const principals = list.map(p => E10SUtils.serializePrincipal(p));
+        const uris = list.map(p => (p.URI ? p.URI.displaySpec : null));
+        aCallback.onSuccess({
+          principals,
+          uris,
+        });
+        break;
+      }
+
+      case "ContentBlocking:RestoreList": {
+        const principals = aData.principals.map(p =>
+          E10SUtils.deserializePrincipal(p)
+        );
+        ContentBlockingAllowList.wipeLists();
+        ContentBlockingAllowList.addAllowListPrincipals(principals);
+        break;
+      }
+
+      case "ContentBlocking:ClearList": {
+        ContentBlockingAllowList.wipeLists();
+        break;
+      }
+    }
+  },
+};
--- a/mobile/android/modules/geckoview/moz.build
+++ b/mobile/android/modules/geckoview/moz.build
@@ -8,16 +8,17 @@ EXTRA_JS_MODULES += [
     'AndroidLog.jsm',
     'ContentCrashHandler.jsm',
     'DelayedInit.jsm',
     'GeckoViewAccessibility.jsm',
     'GeckoViewAutoFill.jsm',
     'GeckoViewChildModule.jsm',
     'GeckoViewConsole.jsm',
     'GeckoViewContent.jsm',
+    'GeckoViewContentBlockingController.jsm',
     'GeckoViewMedia.jsm',
     'GeckoViewModule.jsm',
     'GeckoViewNavigation.jsm',
     'GeckoViewProcessHangMonitor.jsm',
     'GeckoViewProgress.jsm',
     'GeckoViewRemoteDebugger.jsm',
     'GeckoViewSettings.jsm',
     'GeckoViewStorageController.jsm',
--- a/toolkit/components/antitracking/ContentBlockingAllowList.jsm
+++ b/toolkit/components/antitracking/ContentBlockingAllowList.jsm
@@ -107,9 +107,46 @@ const ContentBlockingAllowList = {
   includes(browser) {
     let prin = this._basePrincipalForAntiTrackingCommon(browser);
     let type = this._permissionTypeFor(browser);
     return (
       Services.perms.testExactPermissionFromPrincipal(prin, type) ==
       Services.perms.ALLOW_ACTION
     );
   },
+
+  /**
+   * Returns a list of all non-private browsing principals that are on the
+   * content blocking allow list.
+   */
+  getAllowListedPrincipals() {
+    const exceptions = Services.perms
+      .getAllWithTypePrefix("trackingprotection")
+      .filter(
+        // Only export non-private exceptions for security reasons.
+        p => p.type == "trackingprotection"
+      );
+    return exceptions.map(e => e.principal);
+  },
+
+  /**
+   * Takes a list of nsIPrincipals and uses it to update the content blocking allow
+   * list.
+   */
+  addAllowListPrincipals(principals) {
+    principals.forEach(p =>
+      Services.perms.addFromPrincipal(
+        p,
+        "trackingprotection",
+        Services.perms.ALLOW_ACTION,
+        Ci.nsIPermissionManager.EXPIRE_SESSION
+      )
+    );
+  },
+
+  /**
+   * Removes all content blocking exceptions.
+   */
+  wipeLists() {
+    Services.perms.removeByType("trackingprotection");
+    Services.perms.removeByType("trackingprotection-pb");
+  },
 };