Bug 1587552 - Update GeckoView content blocking exception API and add better support for removing exceptions. r=esawin,Ehsan,geckoview-reviewers,snorp
☠☠ backed out by 12d8255184b1 ☠ ☠
authorDylan Roeh <droeh@mozilla.com>
Tue, 14 Jan 2020 16:40:46 +0000
changeset 510216 db42fce46a0b19a742e42d00b58d6267d740c157
parent 510215 805e50a7dd6d5008b25c9fd629096729778ce753
child 510217 c9d67e672cf04f79d8523f7f93a1dee0763b16f9
push id105109
push userdroeh@mozilla.com
push dateTue, 14 Jan 2020 16:42:10 +0000
treeherderautoland@db42fce46a0b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersesawin, Ehsan, geckoview-reviewers, snorp
bugs1587552
milestone74.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1587552 - Update GeckoView content blocking exception API and add better support for removing exceptions. r=esawin,Ehsan,geckoview-reviewers,snorp Differential Revision: https://phabricator.services.mozilla.com/D58828
mobile/android/components/geckoview/GeckoViewStartup.js
mobile/android/geckoview/api.txt
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentBlockingControllerTest.kt
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/ContentBlockingController.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md
mobile/android/modules/geckoview/GeckoViewContentBlockingController.jsm
toolkit/components/antitracking/ContentBlockingAllowList.jsm
--- a/mobile/android/components/geckoview/GeckoViewStartup.js
+++ b/mobile/android/components/geckoview/GeckoViewStartup.js
@@ -105,16 +105,17 @@ GeckoViewStartup.prototype = {
           this,
           "GeckoViewContentBlockingController",
           {
             module:
               "resource://gre/modules/GeckoViewContentBlockingController.jsm",
             ged: [
               "ContentBlocking:AddException",
               "ContentBlocking:RemoveException",
+              "ContentBlocking:RemoveExceptionByPrincipal",
               "ContentBlocking:CheckException",
               "ContentBlocking:SaveList",
               "ContentBlocking:RestoreList",
               "ContentBlocking:ClearList",
             ],
           }
         );
 
--- a/mobile/android/geckoview/api.txt
+++ b/mobile/android/geckoview/api.txt
@@ -35,16 +35,17 @@ import android.view.inputmethod.Extracte
 import android.view.inputmethod.ExtractedTextRequest;
 import android.view.inputmethod.InputConnection;
 import android.widget.FrameLayout;
 import java.io.File;
 import java.io.InputStream;
 import java.lang.Boolean;
 import java.lang.CharSequence;
 import java.lang.Class;
+import java.lang.Deprecated;
 import java.lang.Exception;
 import java.lang.Float;
 import java.lang.Integer;
 import java.lang.Long;
 import java.lang.Object;
 import java.lang.Runnable;
 import java.lang.RuntimeException;
 import java.lang.String;
@@ -291,18 +292,26 @@ package org.mozilla.geckoview {
 
   @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 @NonNull public GeckoResult<List<ContentBlockingController.LogEntry>> getLog(@NonNull GeckoSession);
     method @UiThread public void removeException(@NonNull GeckoSession);
-    method @UiThread public void restoreExceptionList(@NonNull ContentBlockingController.ExceptionList);
-    method @UiThread @NonNull public GeckoResult<ContentBlockingController.ExceptionList> saveExceptionList();
+    method @AnyThread public void removeException(@NonNull ContentBlockingController.ContentBlockingException);
+    method @Deprecated @UiThread public void restoreExceptionList(@NonNull ContentBlockingController.ExceptionList);
+    method @AnyThread public void restoreExceptionList(@NonNull List<ContentBlockingController.ContentBlockingException>);
+    method @UiThread @NonNull public GeckoResult<List<ContentBlockingController.ContentBlockingException>> saveExceptionList();
+  }
+
+  @AnyThread public static class ContentBlockingController.ContentBlockingException {
+    method @NonNull public static ContentBlockingController.ContentBlockingException fromJson(@NonNull JSONObject);
+    method @NonNull public JSONObject toJson();
+    field @NonNull public final String uri;
   }
 
   public static class ContentBlockingController.Event {
     ctor protected Event();
     field public static final int BLOCKED_CRYPTOMINING_CONTENT = 2048;
     field public static final int BLOCKED_FINGERPRINTING_CONTENT = 64;
     field public static final int BLOCKED_SOCIALTRACKING_CONTENT = 65536;
     field public static final int BLOCKED_TRACKING_CONTENT = 4096;
@@ -318,17 +327,17 @@ package org.mozilla.geckoview {
     field public static final int COOKIES_PARTITIONED_FOREIGN = -2147483648;
     field public static final int LOADED_CRYPTOMINING_CONTENT = 2097152;
     field public static final int LOADED_FINGERPRINTING_CONTENT = 1024;
     field public static final int LOADED_LEVEL_1_TRACKING_CONTENT = 8192;
     field public static final int LOADED_LEVEL_2_TRACKING_CONTENT = 1048576;
     field public static final int LOADED_SOCIALTRACKING_CONTENT = 131072;
   }
 
-  @AnyThread public class ContentBlockingController.ExceptionList {
+  @Deprecated @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();
   }
 
   @AnyThread public static class ContentBlockingController.LogEntry {
     ctor protected LogEntry();
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentBlockingControllerTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentBlockingControllerTest.kt
@@ -8,16 +8,17 @@ import androidx.test.filters.MediumTest
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import org.hamcrest.Matchers
 import org.hamcrest.Matchers.equalTo
 import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mozilla.geckoview.ContentBlocking
 import org.mozilla.geckoview.ContentBlockingController
+import org.mozilla.geckoview.ContentBlockingController.ContentBlockingException
 import org.mozilla.geckoview.GeckoSession
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
 import org.mozilla.geckoview.test.util.Callbacks
 import org.junit.Assume.assumeThat
 
 @RunWith(AndroidJUnit4::class)
 @MediumTest
@@ -39,42 +40,110 @@ class ContentBlockingControllerTest : Ba
                         assertThat("Category should be set",
                                 event.antiTrackingCategory,
                                 Matchers.equalTo(category))
                         assertThat("URI should not be null", event.uri, Matchers.notNullValue())
                         assertThat("URI should match", event.uri, Matchers.endsWith("tracker.js"))
                     }
                 })
 
+        // Add exception for this site.
         sessionRule.runtime.contentBlockingController.addException(sessionRule.session)
 
         sessionRule.runtime.contentBlockingController.checkException(sessionRule.session).accept {
             assertThat("Site should be on exceptions list", it, Matchers.equalTo(true))
         }
 
         var list = sessionRule.waitForResult(sessionRule.runtime.contentBlockingController.saveExceptionList())
         assertThat("Exceptions list should not be null", list, Matchers.notNullValue())
-        assertThat("Exceptions list should have one entry", list.uris.size, Matchers.equalTo(1))
+        assertThat("Exceptions list should have one entry", list.size, Matchers.equalTo(1))
 
         sessionRule.session.reload()
         sessionRule.waitForPageStop()
 
         sessionRule.forCallbacksDuringWait(
                 object : Callbacks.ContentBlockingDelegate {
                     @GeckoSessionTestRule.AssertCalled(false)
                     override fun onContentBlocked(session: GeckoSession,
                                                   event: ContentBlocking.BlockEvent) {
                     }
                 })
 
+        // Remove exception for this site by passing GeckoSession.
         sessionRule.runtime.contentBlockingController.removeException(sessionRule.session)
 
         list = sessionRule.waitForResult(sessionRule.runtime.contentBlockingController.saveExceptionList())
         assertThat("Exceptions list should not be null", list, Matchers.notNullValue())
-        assertThat("Exceptions list should have one entry", list.uris.size, Matchers.equalTo(0))
+        assertThat("Exceptions list should have one entry", list.size, Matchers.equalTo(0))
+
+        sessionRule.session.reload()
+
+        sessionRule.waitUntilCalled(
+                object : Callbacks.ContentBlockingDelegate {
+                    @GeckoSessionTestRule.AssertCalled(count=3)
+                    override fun onContentBlocked(session: GeckoSession,
+                                                  event: ContentBlocking.BlockEvent) {
+                        assertThat("Category should be set",
+                                event.antiTrackingCategory,
+                                Matchers.equalTo(category))
+                        assertThat("URI should not be null", event.uri, Matchers.notNullValue())
+                        assertThat("URI should match", event.uri, Matchers.endsWith("tracker.js"))
+                    }
+                })
+    }
+
+// disable test on debug for frequently failing #Bug 1580223
+    @Test
+    fun trackingProtectionExceptionRemoveByException() {
+        assumeThat(sessionRule.env.isDebugBuild, equalTo(false))
+        val category = ContentBlocking.AntiTracking.TEST
+        sessionRule.runtime.settings.contentBlocking.setAntiTracking(category)
+        sessionRule.session.loadTestPath(TRACKERS_PATH)
+
+        sessionRule.waitUntilCalled(
+                object : Callbacks.ContentBlockingDelegate {
+                    @GeckoSessionTestRule.AssertCalled(count=3)
+                    override fun onContentBlocked(session: GeckoSession,
+                                                  event: ContentBlocking.BlockEvent) {
+                        assertThat("Category should be set",
+                                event.antiTrackingCategory,
+                                Matchers.equalTo(category))
+                        assertThat("URI should not be null", event.uri, Matchers.notNullValue())
+                        assertThat("URI should match", event.uri, Matchers.endsWith("tracker.js"))
+                    }
+                })
+
+        // Add exception for this site.
+        sessionRule.runtime.contentBlockingController.addException(sessionRule.session)
+
+        sessionRule.runtime.contentBlockingController.checkException(sessionRule.session).accept {
+            assertThat("Site should be on exceptions list", it, Matchers.equalTo(true))
+        }
+
+        var list = sessionRule.waitForResult(sessionRule.runtime.contentBlockingController.saveExceptionList())
+        assertThat("Exceptions list should not be null", list, Matchers.notNullValue())
+        assertThat("Exceptions list should have one entry", list.size, Matchers.equalTo(1))
+
+        sessionRule.session.reload()
+        sessionRule.waitForPageStop()
+
+        sessionRule.forCallbacksDuringWait(
+                object : Callbacks.ContentBlockingDelegate {
+                    @GeckoSessionTestRule.AssertCalled(false)
+                    override fun onContentBlocked(session: GeckoSession,
+                                                  event: ContentBlocking.BlockEvent) {
+                    }
+                })
+
+        // Remove exception for this site by passing ContentBlockingException.
+        sessionRule.runtime.contentBlockingController.removeException(list.get(0))
+
+        list = sessionRule.waitForResult(sessionRule.runtime.contentBlockingController.saveExceptionList())
+        assertThat("Exceptions list should not be null", list, Matchers.notNullValue())
+        assertThat("Exceptions list should have one entry", list.size, Matchers.equalTo(0))
 
         sessionRule.session.reload()
 
         sessionRule.waitUntilCalled(
                 object : Callbacks.ContentBlockingDelegate {
                     @GeckoSessionTestRule.AssertCalled(count=3)
                     override fun onContentBlocked(session: GeckoSession,
                                                   event: ContentBlocking.BlockEvent) {
@@ -98,55 +167,36 @@ class ContentBlockingControllerTest : Ba
 
         sessionRule.waitForPageStop()
 
         sessionRule.runtime.contentBlockingController.addException(sessionRule.session)
 
         var export = sessionRule.waitForResult(sessionRule.runtime.contentBlockingController
                 .saveExceptionList())
         assertThat("Exported list must not be null", export, Matchers.notNullValue())
-        assertThat("Exported list must contain one entry", export.uris.size, Matchers.equalTo(1))
+        assertThat("Exported list must contain one entry", export.size, Matchers.equalTo(1))
 
-        val exportStr = export.toString()
-        assertThat("Exported string must not be null", exportStr, Matchers.notNullValue())
-
-        val exportJson = export.toJson()
+        val exportJson = export.get(0).toJson()
         assertThat("Exported JSON must not be null", exportJson, Matchers.notNullValue())
 
         // Wipe
         sessionRule.runtime.contentBlockingController.clearExceptionList()
         export = sessionRule.waitForResult(sessionRule.runtime.contentBlockingController
                 .saveExceptionList())
         assertThat("Exported list must not be null", export, Matchers.notNullValue())
-        assertThat("Exported list must contain zero entries", export.uris.size, Matchers.equalTo(0))
-
-        // Restore from String
-        val importStr = sessionRule.runtime.contentBlockingController.ExceptionList(exportStr)
-        sessionRule.runtime.contentBlockingController.restoreExceptionList(importStr)
-
-        export = sessionRule.waitForResult(sessionRule.runtime.contentBlockingController
-                .saveExceptionList())
-        assertThat("Exported list must not be null", export, Matchers.notNullValue())
-        assertThat("Exported list must contain one entry", export.uris.size, Matchers.equalTo(1))
-
-        // Wipe
-        sessionRule.runtime.contentBlockingController.clearExceptionList()
-        export = sessionRule.waitForResult(sessionRule.runtime.contentBlockingController
-                .saveExceptionList())
-        assertThat("Exported list must not be null", export, Matchers.notNullValue())
-        assertThat("Exported list must contain zero entries", export.uris.size, Matchers.equalTo(0))
+        assertThat("Exported list must contain zero entries", export.size, Matchers.equalTo(0))
 
         // Restore from JSON
-        val importJson = sessionRule.runtime.contentBlockingController.ExceptionList(exportJson)
+        val importJson = listOf(ContentBlockingException.fromJson(exportJson))
         sessionRule.runtime.contentBlockingController.restoreExceptionList(importJson)
 
         export = sessionRule.waitForResult(sessionRule.runtime.contentBlockingController
                 .saveExceptionList())
         assertThat("Exported list must not be null", export, Matchers.notNullValue())
-        assertThat("Exported list must contain one entry", export.uris.size, Matchers.equalTo(1))
+        assertThat("Exported list must contain one entry", export.size, Matchers.equalTo(1))
 
         // Wipe so as not to break other tests.
         sessionRule.runtime.contentBlockingController.clearExceptionList()
     }
 
     @Test
     fun getLog() {
         val category = ContentBlocking.AntiTracking.TEST
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/ContentBlockingController.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/ContentBlockingController.java
@@ -28,21 +28,67 @@ import java.util.List;
 /**
  * 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";
 
+    @AnyThread
+    public static class ContentBlockingException {
+        private final @NonNull String mEncodedPrincipal;
+
+        /**
+         * A String representing the URI of this content blocking exception.
+         */
+        public final @NonNull String uri;
+
+        /* package */ ContentBlockingException(final @NonNull String encodedPrincipal,
+                                               final @NonNull String uri) {
+            mEncodedPrincipal = encodedPrincipal;
+            this.uri = uri;
+        }
+
+        /**
+         * Returns a JSONObject representation of the content blocking exception.
+         *
+         * @return A JSONObject representing the exception.
+         *
+         * @throws JSONException if conversion to JSONObject fails.
+         */
+        public @NonNull JSONObject toJson() throws JSONException {
+            final JSONObject res = new JSONObject();
+            res.put("principal", mEncodedPrincipal);
+            res.put("uri", uri);
+            return res;
+        }
+
+        /**
+         *
+         * Returns a ContentBlockingException reconstructed from JSON.
+         *
+         * @param savedException A JSONObject representation of a saved exception; should be the output of
+         *                       {@link toJson}.
+         *
+         * @return A ContentBlockingException reconstructed from the supplied JSONObject.
+         *
+         * @throws JSONException if the JSONObject cannot be converted for any reason.
+         */
+        public static @NonNull ContentBlockingException fromJson(final @NonNull JSONObject savedException) throws JSONException {
+            return new ContentBlockingException(savedException.getString("principal"), savedException.getString("uri"));
+        }
+    }
+
     /**
      * 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.
      */
+    @Deprecated
     @AnyThread
     public class ExceptionList {
         private final @NonNull GeckoBundle mBundle;
 
         /* package */ ExceptionList(final @NonNull GeckoBundle bundle) {
             mBundle = new GeckoBundle(bundle);
         }
 
@@ -139,16 +185,31 @@ public class ContentBlockingController {
     @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);
     }
 
     /**
+     * Remove the exception specified by the supplied {@link ContentBlockingException} from
+     * the content blocking exception list, if it is present. If there is no such exception,
+     * this is a no-op.
+     *
+     * @param exception A {@link ContentBlockingException} which will be removed from the
+     *                  content blocking exception list.
+     */
+    @AnyThread
+    public void removeException(final @NonNull ContentBlockingException exception) {
+        final GeckoBundle msg = new GeckoBundle(1);
+        msg.putString("principal", exception.mEncodedPrincipal);
+        EventDispatcher.getInstance().dispatch("ContentBlocking:RemoveExceptionByPrincipal", 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.
@@ -163,44 +224,81 @@ public class ContentBlockingController {
         };
         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}.
+     * Save the current content blocking exception list as a List of {@link ContentBlockingException}.
      *
-     * @return An {@link ExceptionList} which can be used to restore the current
-     *         exception list.
+     * @return A List of {@link ContentBlockingException} which can be used to restore or
+     *         inspect the current exception list.
      */
     @UiThread
-    public @NonNull GeckoResult<ExceptionList> saveExceptionList() {
-        final CallbackResult<ExceptionList> result = new CallbackResult<ExceptionList>() {
+    public @NonNull GeckoResult<List<ContentBlockingException>> saveExceptionList() {
+        final CallbackResult<List<ContentBlockingException>> result = new CallbackResult<List<ContentBlockingException>>() {
             @Override
             public void sendSuccess(final Object value) {
-                complete(new ExceptionList((GeckoBundle) value));
+                final String[] principals = ((GeckoBundle) value).getStringArray("principals");
+                final String[] uris = ((GeckoBundle) value).getStringArray("uris");
+
+                if (principals == null || uris == null) {
+                    completeExceptionally(new RuntimeException("Received invalid content blocking exception list"));
+                    return;
+                }
+
+                final ArrayList<ContentBlockingException> res = new ArrayList<ContentBlockingException>(principals.length);
+
+                for (int i = 0; i < principals.length; i++) {
+                    res.add(new ContentBlockingException(principals[i], uris[i]));
+                }
+
+                complete(Collections.unmodifiableList(res));
             }
         };
         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}.
      */
+    @Deprecated
     @UiThread
     public void restoreExceptionList(final @NonNull ExceptionList list) {
         EventDispatcher.getInstance().dispatch("ContentBlocking:RestoreList", list.getBundle());
     }
 
     /**
+     * Restore the supplied List of {@link ContentBlockingException}, overwriting the existing exception list.
+     *
+     * @param list A List of {@link ContentBlockingException} originally created by {@link saveExceptionList}.
+     */
+    @AnyThread
+    public void restoreExceptionList(final @NonNull List<ContentBlockingException> list) {
+        final GeckoBundle bundle = new GeckoBundle(2);
+        final String[] principals = new String[list.size()];
+        final String[] uris = new String[list.size()];
+
+        for (int i = 0; i < list.size(); i++) {
+            principals[i] = list.get(i).mEncodedPrincipal;
+            uris[i] = list.get(i).uri;
+        }
+
+        bundle.putStringArray("principals", principals);
+        bundle.putStringArray("uris", uris);
+
+        EventDispatcher.getInstance().dispatch("ContentBlocking:RestoreList", bundle);
+    }
+
+    /**
      * Clear the content blocking exception list entirely.
      */
     @UiThread
     public void clearExceptionList() {
         EventDispatcher.getInstance().dispatch("ContentBlocking:ClearList", null);
     }
 
     public static class Event {
--- 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
@@ -24,23 +24,30 @@ exclude: true
   delivered over a secure connection.
   ([bug 1508730]({{bugzilla}}1508730))
 - Added ['WebResponse#certificate'][74.5], which is the server certificate used for the
   response, if any.
   ([bug 1508730]({{bugzilla}}1508730))
 - Added ['WebRequestError#certificate'][74.6], which is the server certificate used in the
   failed request, if any.
   ([bug 1508730]({{bugzilla}}1508730))
+- ⚠️ Updated [`ContentBlockingController`][74.7] to use new representation for content blocking
+  exceptions and to add better support for removing exceptions. This deprecates [`ExceptionList`][74.8]
+  and [`restoreExceptionList`][74.9] with the intent to remove them in 76.
+  ([bug 1587552]({{bugzilla}}1587552))
 
 [74.1]: {{javadoc_uri}}/WebExtensionController.html#enable-org.mozilla.geckoview.WebExtension-int-
 [74.2]: {{javadoc_uri}}/WebExtensionController.html#disable-org.mozilla.geckoview.WebExtension-int-
 [74.3]: {{javadoc_uri}}/GeckoSession.ProgressDelegate.SecurityInformation.html#certificate
 [74.4]: {{javadoc_uri}}/WebResponse.html#isSecure
 [74.5]: {{javadoc_uri}}/WebResponse.html#certificate
 [74.6]: {{javadoc_uri}}/WebRequestError.html#certificate
+[74.7]: {{javadoc_uri}}/ContentBlockingController.html
+[74.8]: {{javadoc_uri}}/ContentBlockingController.ExceptionList.html
+[74.9]: {{javadoc_uri}}/ContentBlockingController.html#restoreExceptionList-org.mozilla.geckoview.ContentBlockingController.ExceptionList-
 
 ## v73
 - Added [`WebExtensionController.install`][73.1] and [`uninstall`][73.2] to
   manage installed extensions
 - ⚠️ Renamed `ScreenLength.VIEWPORT_WIDTH`, `ScreenLength.VIEWPORT_HEIGHT`,
   `ScreenLength.fromViewportWidth` and `ScreenLength.fromViewportHeight` to
   [`ScreenLength.VISUAL_VIEWPORT_WIDTH`][73.3],
   [`ScreenLength.VISUAL_VIEWPORT_HEIGHT`][73.4],
@@ -541,9 +548,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]: 34255117d007cf40a19b289285f00206f55ea792
+[api-version]: e89f3bb79d0c5cc936812de4c2d8f4d30923afb9
--- a/mobile/android/modules/geckoview/GeckoViewContentBlockingController.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewContentBlockingController.jsm
@@ -49,16 +49,22 @@ const GeckoViewContentBlockingController
         const sessionWindow = Services.ww.getWindowByName(
           aData.sessionId,
           null
         );
         ContentBlockingAllowList.remove(sessionWindow.browser);
         break;
       }
 
+      case "ContentBlocking:RemoveExceptionByPrincipal": {
+        const principal = E10SUtils.deserializePrincipal(aData.principal);
+        ContentBlockingAllowList.removeByPrincipal(principal);
+        break;
+      }
+
       case "ContentBlocking:CheckException": {
         const sessionWindow = Services.ww.getWindowByName(
           aData.sessionId,
           null
         );
         const res = ContentBlockingAllowList.includes(sessionWindow.browser);
         aCallback.onSuccess(res);
         break;
--- a/toolkit/components/antitracking/ContentBlockingAllowList.jsm
+++ b/toolkit/components/antitracking/ContentBlockingAllowList.jsm
@@ -96,16 +96,24 @@ const ContentBlockingAllowList = {
    */
   remove(browser) {
     let prin = this._basePrincipalForAntiTrackingCommon(browser);
     let type = this._permissionTypeFor(browser);
     Services.perms.removeFromPrincipal(prin, type);
   },
 
   /**
+   * Remove the given principal from the Content Blocking allow list if present.
+   */
+  removeByPrincipal(principal) {
+    Services.perms.removeFromPrincipal(principal, "trackingprotection");
+    Services.perms.removeFromPrincipal(principal, "trackingprotection-pb");
+  },
+
+  /**
    * Returns true if the current browser has loaded a document that is on the
    * Content Blocking allow list.
    */
   includes(browser) {
     let prin = this._basePrincipalForAntiTrackingCommon(browser);
     let type = this._permissionTypeFor(browser);
     return (
       Services.perms.testExactPermissionFromPrincipal(prin, type) ==