Backed out 4 changesets (bug 1624410, bug 1658937) for gv-junit failures CLOSED TREE
authorBogdan Tara <btara@mozilla.com>
Wed, 23 Sep 2020 21:04:19 +0300
changeset 550056 e73fdac6425430dc9316756a3880c92505139042
parent 550055 49e60e08ca4c41b35d3dcd685ca8de11c2c82fbb
child 550057 0e8c8e02db69ca8a3d0fe7f171de9ac005f0a7d1
push id127052
push userbtara@mozilla.com
push dateWed, 23 Sep 2020 18:07:46 +0000
treeherderautoland@e73fdac64254 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1624410, 1658937
milestone83.0a1
backs out48126ab6653f9d4ca78ab2be1c8fcba298abce09
eef492b1405c58e2f1c425f236f3680b5741c253
d00dc8d2956ceab88edcdd23abaa211f709c35cd
525ef78d1586f77d6ab94419acfb2afd8cb464bd
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
Backed out 4 changesets (bug 1624410, bug 1658937) for gv-junit failures CLOSED TREE Backed out changeset 48126ab6653f (bug 1624410) Backed out changeset eef492b1405c (bug 1658937) Backed out changeset d00dc8d2956c (bug 1658937) Backed out changeset 525ef78d1586 (bug 1658937)
mobile/android/geckoview/api.txt
mobile/android/geckoview/src/androidTest/assets/www/media_session_dom1.html
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/MediaSessionTest.kt
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/MediaSession.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtension.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md
mobile/android/modules/geckoview/GeckoViewMediaControl.jsm
mobile/android/modules/geckoview/GeckoViewWebExtension.jsm
widget/android/moz.build
widget/android/nsWindow.cpp
widget/android/nsWindow.h
--- a/mobile/android/geckoview/api.txt
+++ b/mobile/android/geckoview/api.txt
@@ -1368,16 +1368,17 @@ package org.mozilla.geckoview {
 
   @UiThread public static interface MediaSession.Delegate {
     method default public void onActivated(@NonNull GeckoSession, @NonNull MediaSession);
     method default public void onDeactivated(@NonNull GeckoSession, @NonNull MediaSession);
     method default public void onFeatures(@NonNull GeckoSession, @NonNull MediaSession, long);
     method default public void onFullscreen(@NonNull GeckoSession, @NonNull MediaSession, boolean, @Nullable MediaSession.ElementMetadata);
     method default public void onMetadata(@NonNull GeckoSession, @NonNull MediaSession, @NonNull MediaSession.Metadata);
     method default public void onPause(@NonNull GeckoSession, @NonNull MediaSession);
+    method default public void onPictureInPicture(@NonNull GeckoSession, @NonNull MediaSession, boolean);
     method default public void onPlay(@NonNull GeckoSession, @NonNull MediaSession);
     method default public void onPositionState(@NonNull GeckoSession, @NonNull MediaSession, @NonNull MediaSession.PositionState);
     method default public void onStop(@NonNull GeckoSession, @NonNull MediaSession);
   }
 
   public static class MediaSession.ElementMetadata {
     ctor public ElementMetadata(@Nullable String, double, long, long, int, int);
     field public final int audioTrackCount;
@@ -1678,17 +1679,16 @@ package org.mozilla.geckoview {
     field @NonNull public final Image icon;
     field public final boolean isRecommended;
     field @Nullable public final String name;
     field public final boolean openOptionsPageInTab;
     field @Nullable public final String optionsPageUrl;
     field @NonNull public final String[] origins;
     field @NonNull public final String[] permissions;
     field public final int signedState;
-    field public final boolean temporary;
     field @NonNull public final String version;
   }
 
   @UiThread public static class WebExtension.Port {
     ctor protected Port();
     method public void disconnect();
     method public void postMessage(@NonNull JSONObject);
     method public void setDelegate(@Nullable WebExtension.PortDelegate);
--- a/mobile/android/geckoview/src/androidTest/assets/www/media_session_dom1.html
+++ b/mobile/android/geckoview/src/androidTest/assets/www/media_session_dom1.html
@@ -1,99 +1,99 @@
 <html>
   <head><title>MediaSessionDOMTest1</title></head>
   <body>
     <script>
       function updatePositionState(event) {
-        if (event.target != active) {
-          return;
-        }
         navigator.mediaSession.setPositionState({
           duration: parseFloat(event.target.duration),
           position: parseFloat(event.target.currentTime),
           playbackRate: 1,
         });
       }
 
-      function updateMetadata() {
+      function updateMetadata(event) {
+        active.removeEventListener("timeupdate", updatePositionState);
+        active = event.target;
+        updatePositionState(event);
+        active.addEventListener("timeupdate", updatePositionState);
         navigator.mediaSession.metadata = active.metadata;
       }
 
       function getTrack(offset) {
         console.log("" + active.id + " " + offset);
         const nextId = Math.min(tracks.length - 1,
             Math.max(0, parseInt(active.id) + offset));
         return tracks[nextId];
       }
 
-      navigator.mediaSession.setActionHandler("play", async () => {
-        await active.play();
-        updateMetadata();
+      navigator.mediaSession.setActionHandler("play", () => {
+        active.play();
       });
 
       navigator.mediaSession.setActionHandler("pause", () => {
         active.pause();
       });
 
       navigator.mediaSession.setActionHandler("previoustrack", () => {
-        active = getTrack(-1);
+        active.pause();
+        getTrack(-1).play();
       });
 
       navigator.mediaSession.setActionHandler("nexttrack", () => {
-        active = getTrack(1);
+        active.pause();
+        getTrack(1).play();
       });
 
       const audio1 = document.createElement("audio");
       audio1.src = "audio/owl.mp3";
-      audio1.addEventListener("timeupdate", updatePositionState);
+      audio1.addEventListener("play", updateMetadata);
       audio1.metadata = new window.MediaMetadata({
         title: "hoot",
         artist: "owl",
         album: "hoots",
         artwork: [{
           src: "images/test.gif",
           type: "image/gif",
           sizes: "265x199"
         }]
       });
       audio1.id = 0;
 
       const audio2 = document.createElement("audio");
       audio2.src = "audio/owl.mp3";
-      audio2.addEventListener("timeupdate", updatePositionState);
+      audio2.addEventListener("play", updateMetadata);
       audio2.metadata = new window.MediaMetadata({
         title: "hoot2",
         artist: "stillowl",
         album: "dahoots",
         artwork: [{
           src: "images/test.gif",
           type: "image/gif",
           sizes: "265x199"
         }]
       });
       audio2.id = 1;
 
       const audio3 = document.createElement("audio");
       audio3.src = "audio/owl.mp3";
-      audio3.addEventListener("timeupdate", updatePositionState);
+      audio3.addEventListener("play", updateMetadata);
       audio3.metadata = new window.MediaMetadata({
         title: "hoot3",
         artist: "immaowl",
         album: "mahoots",
         artwork: [{
           src: "images/test.gif",
           type: "image/gif",
           sizes: "265x199"
         }]
       });
       audio3.id = 2;
 
       const tracks = [audio1, audio2, audio3];
       let active = audio1;
 
-      window.onload = async () => {
-        active = getTrack(0);
-        await active.play();
-        updateMetadata();
+      window.onload = () => {
+        getTrack(0).play();
       };
     </script>
   </body>
 </html>
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/MediaSessionTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/MediaSessionTest.kt
@@ -56,16 +56,23 @@ class MediaSessionTest : BaseSessionTest
                 Metadata(
                         DOM_TEST_TITLE2,
                         DOM_TEST_ARTIST2,
                         DOM_TEST_ALBUM2),
                 Metadata(
                         DOM_TEST_TITLE3,
                         DOM_TEST_ARTIST3,
                         DOM_TEST_ALBUM3))
+
+        val DEFAULT_META = arrayOf(
+                Metadata(
+                        DEFAULT_TEST_TITLE1,
+                        // TODO: enforce null for empty strings?
+                        "",
+                        ""))
     }
 
     @Before
     fun setup() {
         sessionRule.setPrefsUntilTestEnd(mapOf(
             "media.mediacontrol.stopcontrol.aftermediaends" to false,
             "dom.media.mediasession.enabled" to true))
     }
@@ -111,49 +118,46 @@ class MediaSessionTest : BaseSessionTest
         //    a. Ensure onPause (1) is called.
         val completedStep3 = GeckoResult.allOf(
                 onPauseCalled[0])
 
         // 4. Resume playback (1).
         //    a. Ensure onMetadata (1) is called.
         //    b. Ensure onPlay (1) is called.
         val completedStep4 = GeckoResult.allOf(
-                onPlayCalled[1],
-                onMetadataCalled[1])
+                onPlayCalled[1])
 
         // 5. Wait for track 1 end.
         //    a. Ensure onPause (1) is called.
         val completedStep5 = GeckoResult.allOf(
                 onPauseCalled[1])
 
         // 6. Play next track (2).
         //    a. Ensure onMetadata (2) is called.
         //    b. Ensure onPlay (2) is called.
         val completedStep6 = GeckoResult.allOf(
-                onMetadataCalled[2],
+                onMetadataCalled[1],
                 onPlayCalled[2])
 
         // 7. Play next track (3).
         //    a. Ensure onPause (2) is called.
         //    b. Ensure onMetadata (3) is called.
         //    c. Ensure onPlay (3) is called.
         val completedStep7 = GeckoResult.allOf(
                 onPauseCalled[2],
-                onMetadataCalled[3],
+                onMetadataCalled[2],
                 onPlayCalled[3])
 
         // 8. Play previous track (2).
         //    a. Ensure onPause (3) is called.
         //    b. Ensure onMetadata (2) is called.
         //    c. Ensure onPlay (2) is called.
-        val completedStep8a = GeckoResult.allOf(
-                onPauseCalled[3])
-        // Without the split, this seems to race and we don't get the pause event.
-        val completedStep8b = GeckoResult.allOf(
-                onMetadataCalled[4],
+        val completedStep8 = GeckoResult.allOf(
+                onPauseCalled[3],
+                onMetadataCalled[3],
                 onPlayCalled[4])
 
         // 9. Wait for track 2 end.
         //    a. Ensure onPause (2) is called.
         val completedStep9 = GeckoResult.allOf(
                 onPauseCalled[4])
 
         val path = MEDIA_SESSION_DOM1_PATH
@@ -167,22 +171,16 @@ class MediaSessionTest : BaseSessionTest
             @AssertCalled(count = 1)
             override fun onActivated(
                     session: GeckoSession,
                     mediaSession: MediaSession) {
                 onActivatedCalled[0].complete(null)
                 mediaSession1 = mediaSession
             }
 
-            @AssertCalled(false)
-            override fun onDeactivated(
-                    session: GeckoSession,
-                    mediaSession: MediaSession) {
-            }
-
             @AssertCalled
             override fun onFeatures(
                     session: GeckoSession,
                     mediaSession: MediaSession,
                     features: Long) {
 
                 val play = (features and MediaSession.Feature.PLAY) != 0L
                 val pause = (features and MediaSession.Feature.PAUSE) != 0L
@@ -279,39 +277,37 @@ class MediaSessionTest : BaseSessionTest
         sessionRule.waitForResult(completedStep2)
         mediaSession1!!.pause()
 
         sessionRule.waitForResult(completedStep3)
         mediaSession1!!.play()
 
         sessionRule.waitForResult(completedStep4)
         sessionRule.waitForResult(completedStep5)
-        mediaSession1!!.pause()
         mediaSession1!!.nextTrack()
-        mediaSession1!!.play()
 
         sessionRule.waitForResult(completedStep6)
-        mediaSession1!!.pause()
         mediaSession1!!.nextTrack()
-        mediaSession1!!.play()
 
         sessionRule.waitForResult(completedStep7)
-        mediaSession1!!.pause()
+        mediaSession1!!.previousTrack()
 
-        sessionRule.waitForResult(completedStep8a)
-        mediaSession1!!.previousTrack()
-        mediaSession1!!.play()
-
-        sessionRule.waitForResult(completedStep8b)
+        sessionRule.waitForResult(completedStep8)
         sessionRule.waitForResult(completedStep9)
     }
 
     @Test
     fun defaultMetadataPlayback() {
         val onActivatedCalled = arrayOf(GeckoResult<Void>())
+        val onMetadataCalled = arrayOf(
+                GeckoResult<Void>(),
+                GeckoResult<Void>(),
+                GeckoResult<Void>(),
+                GeckoResult<Void>(),
+                GeckoResult<Void>())
         val onPlayCalled = arrayOf(GeckoResult<Void>(),
                 GeckoResult<Void>(),
                 GeckoResult<Void>(),
                 GeckoResult<Void>(),
                 GeckoResult<Void>(),
                 GeckoResult<Void>())
         val onPauseCalled = arrayOf(GeckoResult<Void>(),
                 GeckoResult<Void>(),
@@ -319,19 +315,21 @@ class MediaSessionTest : BaseSessionTest
                 GeckoResult<Void>(),
                 GeckoResult<Void>(),
                 GeckoResult<Void>())
 
         // Test:
         // 1. Load Media Session page which contains 1 audio track.
         // 2. Track 1 is played on page load.
         //    a. Ensure onActivated is called.
+        //    a. Ensure onMetadata (1) is called.
         //    b. Ensure onPlay (1) is called.
         val completedStep2 = GeckoResult.allOf(
                 onActivatedCalled[0],
+                onMetadataCalled[0],
                 onPlayCalled[0])
 
         // 3. Pause playback of track 1.
         //    a. Ensure onPause (1) is called.
         val completedStep3 = GeckoResult.allOf(
                 onPauseCalled[0])
 
         // 4. Resume playback (1).
@@ -355,16 +353,57 @@ class MediaSessionTest : BaseSessionTest
             @AssertCalled(count = 1)
             override fun onActivated(
                     session: GeckoSession,
                     mediaSession: MediaSession) {
                 onActivatedCalled[0].complete(null)
                 mediaSession1 = mediaSession
             }
 
+            /*
+            TODO: currently not called for non-media-session content.
+            @AssertCalled
+            override fun onFeatures(
+                    session: GeckoSession,
+                    mediaSession: MediaSession,
+                    features: Long) {
+
+                val play = (features and MediaSession.Feature.PLAY) != 0L
+                val pause = (features and MediaSession.Feature.PAUSE) != 0L
+                val stop = (features and MediaSession.Feature.PAUSE) != 0L
+
+                assertThat(
+                        "Core playback constrols should be supported",
+                        play && pause && stop,
+                        equalTo(true))
+            }
+            */
+
+            @AssertCalled(count = 1)
+            override fun onMetadata(
+                    session: GeckoSession,
+                    mediaSession: MediaSession,
+                    meta: MediaSession.Metadata) {
+                assertThat(
+                        "Title should match",
+                        meta.title,
+                        equalTo(DEFAULT_META[0].title))
+                assertThat(
+                        "Artist should match",
+                        meta.artist,
+                        equalTo(DEFAULT_META[0].artist))
+                assertThat(
+                        "Album should match",
+                        meta.album,
+                        equalTo(DEFAULT_META[0].album))
+
+                onMetadataCalled[sessionRule.currentCall.counter - 1]
+                        .complete(null)
+            }
+
             @AssertCalled(count = 2)
             override fun onPlay(
                     session: GeckoSession,
                     mediaSession: MediaSession) {
                 onPlayCalled[sessionRule.currentCall.counter - 1]
                         .complete(null)
             }
 
@@ -655,15 +694,13 @@ class MediaSessionTest : BaseSessionTest
         sessionRule.waitForResult(completedStep3)
 
         session2.loadTestPath(path)
         sessionRule.waitForResult(completedStep5)
 
         mediaSession2!!.pause()
         sessionRule.waitForResult(completedStep6)
 
-        mediaSession1!!.pause()
         mediaSession1!!.nextTrack()
-        mediaSession1!!.play()
         sessionRule.waitForResult(completedStep7)
         sessionRule.waitForResult(completedStep8)
     }
 }
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
@@ -1120,16 +1120,24 @@ public class GeckoSession {
                                            GeckoBundle initData);
 
         @WrapForJNI(dispatchTo = "proxy")
         public native void attachEditable(IGeckoEditableParent parent);
 
         @WrapForJNI(dispatchTo = "proxy")
         public native void attachAccessibility(SessionAccessibility.NativeProvider sessionAccessibility);
 
+        @WrapForJNI(dispatchTo = "proxy")
+        public native void attachMediaSessionController(
+            final MediaSession.Controller controller, final long id);
+
+        @WrapForJNI(dispatchTo = "proxy")
+        public native void detachMediaSessionController(
+            final MediaSession.Controller controller);
+
         @WrapForJNI(calledFrom = "gecko")
         private synchronized void onReady(final @Nullable NativeQueue queue) {
             // onReady is called the first time the Gecko window is ready, with a null queue
             // argument. In this case, we simply set the current queue to ready state.
             //
             // After the initial call, onReady is called again every time Window.transfer()
             // is called, with a non-null queue argument. In this case, we only set the
             // current queue to ready state _if_ the current queue matches the given queue,
@@ -2605,16 +2613,71 @@ public class GeckoSession {
      * Get the media session delegate.
      * @return The current media session delegate.
      */
     @AnyThread
     public @Nullable MediaSession.Delegate getMediaSessionDelegate() {
         return mMediaSessionHandler.getDelegate();
     }
 
+    @UiThread
+    /* package */ void attachMediaSessionController(
+            final MediaSession.Controller controller) {
+        ThreadUtils.assertOnUiThread();
+
+        if (DEBUG) {
+            Log.d(LOGTAG,
+                    "attachMediaSessionController" +
+                    " isOpen=" + isOpen() +
+                    ", isEnabled=" + mMediaSessionHandler.isEnabled());
+        }
+
+        if (!isOpen() || !mMediaSessionHandler.isEnabled()) {
+            return;
+        }
+
+        if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+            mWindow.attachMediaSessionController(controller, controller.getId());
+        } else {
+            GeckoThread.queueNativeCallUntil(
+                    GeckoThread.State.PROFILE_READY,
+                    mWindow, "attachMediaSessionController",
+                    MediaSession.Controller.class,
+                    controller,
+                    controller.getId());
+        }
+    }
+
+    @UiThread
+    /* package */ void detachMediaSessionController(
+            final MediaSession.Controller controller) {
+        ThreadUtils.assertOnUiThread();
+
+        if (DEBUG) {
+            Log.d(LOGTAG,
+                    "detachMediaSessionController" +
+                    " isOpen=" + isOpen() +
+                    ", isEnabled=" + mMediaSessionHandler.isEnabled());
+        }
+
+        if (!isOpen() || !mMediaSessionHandler.isEnabled()) {
+            return;
+        }
+
+        if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+            mWindow.detachMediaSessionController(controller);
+        } else {
+            GeckoThread.queueNativeCallUntil(
+                    GeckoThread.State.PROFILE_READY,
+                    mWindow, "detachMediaSessionController",
+                    MediaSession.Controller.class,
+                    controller);
+        }
+    }
+
     /**
      * Get the current selection action delegate for this GeckoSession.
      *
      * @return SelectionActionDelegate instance or null if not set.
      */
     @AnyThread
     public @Nullable SelectionActionDelegate getSelectionActionDelegate() {
         return mSelectionActionDelegate.getDelegate();
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/MediaSession.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/MediaSession.java
@@ -14,178 +14,298 @@ import androidx.annotation.LongDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 import android.util.Log;
 
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.ImageResource;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.mozglue.JNIObject;
 
 /**
  * The MediaSession API provides media controls and events for a GeckoSession.
  * This includes support for the DOM Media Session API and regular HTML media
  * content.
  *
  * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaSession">Media Session API</a>
  */
 @UiThread
 public class MediaSession {
     private static final String LOGTAG = "MediaSession";
     private static final boolean DEBUG = false;
 
     private final GeckoSession mSession;
-    private boolean mIsActive;
+    private Controller mController;
+
+    private static final String ATTACHED_EVENT =
+        "GeckoView:MediaSession:Attached";
+    private boolean mControllerAttached;
 
     protected MediaSession(final GeckoSession session) {
         mSession = session;
     }
 
+    /* package */ final class Controller extends JNIObject {
+        private final long mId;
+
+        /* package */ Controller(final long id) {
+            mId = id;
+        }
+
+        public long getId() {
+            return mId;
+        }
+
+        @Override // JNIObject
+        public void disposeNative() {
+            // Dispose in native code.
+            throw new UnsupportedOperationException();
+        }
+
+        @WrapForJNI(calledFrom = "ui")
+        /* package */ void onAttached() {
+            MediaSession.this.onControllerAttached();
+        }
+
+        @WrapForJNI(calledFrom = "ui")
+        /* package */ void onDetached() {
+            MediaSession.this.onControllerDetached();
+        }
+
+        @WrapForJNI(dispatchTo = "gecko")
+        public native void pause();
+
+        @WrapForJNI(dispatchTo = "gecko")
+        public native void stop();
+
+        @WrapForJNI(dispatchTo = "gecko")
+        public native void play();
+
+        @WrapForJNI(dispatchTo = "gecko")
+        public native void skipAd();
+
+        @WrapForJNI(dispatchTo = "gecko")
+        public native void focus();
+
+        @WrapForJNI(dispatchTo = "gecko")
+        public native void seekTo(double time, boolean fast);
+
+        @WrapForJNI(dispatchTo = "gecko")
+        public native void seekForward(double offset);
+
+        @WrapForJNI(dispatchTo = "gecko")
+        public native void seekBackward(double offset);
+
+        @WrapForJNI(dispatchTo = "gecko")
+        public native void nextTrack();
+
+        @WrapForJNI(dispatchTo = "gecko")
+        public native void previousTrack();
+
+        @WrapForJNI(dispatchTo = "gecko")
+        public native void muteAudio(boolean mute);
+    }
+
+    /* package */ Controller getController() {
+        return mController;
+    }
+
     /**
      * Get whether the media session is active.
      * Only active media sessions can be controlled.
      * Inactive media session may receive state events since some state events
      * may be dispatched before the media session becomes active.
      *
      * Changes in the active state are notified via {@link Delegate#onActivated}
      * and {@link Delegate#onDeactivated} respectively.
      *
      * @see MediaSession.Delegate#onActivated
      * @see MediaSession.Delegate#onDeactivated
      *
      * @return True if this media session is active, false otherwise.
      */
     public boolean isActive() {
-        return mIsActive;
+        return mControllerAttached;
+    }
+
+    /* package */ void attachController(final long id) {
+        mController = new Controller(id);
+        mSession.attachMediaSessionController(mController);
+    }
+
+    void onControllerAttached() {
+        mControllerAttached = true;
+        // TODO: Remove temp workaround once we move to webidl (bug 1658937).
+        mSession.getEventDispatcher().dispatch(ATTACHED_EVENT, null);
     }
 
-    /* package */ void setActive(final boolean active) {
-        mIsActive = active;
+    void onControllerDetached() {
+        if (!mControllerAttached) {
+            return;
+        }
+        mControllerAttached = false;
+        // TODO: Remove temp workaround once we move to webidl (bug 1658937).
+        mSession.getEventDispatcher().dispatch(DEACTIVATED_EVENT, null);
+    }
+
+    /* package */ void detachController() {
+        if (mControllerAttached) {
+            return;
+        }
+        mSession.detachMediaSessionController(mController);
+        mControllerAttached = false;
+        mController = null;
     }
 
     /**
      * Pause playback for the media session.
      */
     public void pause() {
+        if (!mControllerAttached) {
+            return;
+        }
         if (DEBUG) {
             Log.d(LOGTAG, "pause");
         }
-        mSession.getEventDispatcher().dispatch(PAUSE_EVENT, null);
+        mController.pause();
     }
 
     /**
      * Stop playback for the media session.
      */
     public void stop() {
+        if (!mControllerAttached) {
+            return;
+        }
         if (DEBUG) {
             Log.d(LOGTAG, "stop");
         }
-        mSession.getEventDispatcher().dispatch(STOP_EVENT, null);
+        mController.stop();
     }
 
     /**
      * Start playback for the media session.
      */
     public void play() {
+        if (!mControllerAttached) {
+            return;
+        }
         if (DEBUG) {
             Log.d(LOGTAG, "play");
         }
-        mSession.getEventDispatcher().dispatch(PLAY_EVENT, null);
+        mController.play();
     }
 
     /**
      * Seek to a specific time.
      * Prefer using fast seeking when calling this in a sequence.
      * Don't use fast seeking for the last or only call in a sequence.
      *
      * @param time The time in seconds to move the playback time to.
      * @param fast Whether fast seeking should be used.
      */
     public void seekTo(final double time, final boolean fast) {
+        if (!mControllerAttached) {
+            return;
+        }
         if (DEBUG) {
             Log.d(LOGTAG, "seekTo: time=" + time + ", fast=" + fast);
         }
-        final GeckoBundle bundle = new GeckoBundle(2);
-        bundle.putDouble("time", time);
-        bundle.putBoolean("fast", fast);
-        mSession.getEventDispatcher().dispatch(SEEK_TO_EVENT, bundle);
+        mController.seekTo(time, fast);
     }
 
     /**
      * Seek forward by a sensible number of seconds.
      */
     public void seekForward() {
+        if (!mControllerAttached) {
+            return;
+        }
         if (DEBUG) {
             Log.d(LOGTAG, "seekForward");
         }
-        final GeckoBundle bundle = new GeckoBundle(1);
-        bundle.putDouble("offset", 0.0);
-        mSession.getEventDispatcher().dispatch(SEEK_FORWARD_EVENT, bundle);
+        mController.seekForward(0.0);
     }
 
     /**
      * Seek backward by a sensible number of seconds.
      */
     public void seekBackward() {
+        if (!mControllerAttached) {
+            return;
+        }
         if (DEBUG) {
             Log.d(LOGTAG, "seekBackward");
         }
-        final GeckoBundle bundle = new GeckoBundle(1);
-        bundle.putDouble("offset", 0.0);
-        mSession.getEventDispatcher().dispatch(SEEK_BACKWARD_EVENT, bundle);
+        mController.seekBackward(0.0);
     }
 
     /**
      * Select and play the next track.
      * Move playback to the next item in the playlist when supported.
      */
     public void nextTrack() {
+        if (!mControllerAttached) {
+            return;
+        }
         if (DEBUG) {
             Log.d(LOGTAG, "nextTrack");
         }
-        mSession.getEventDispatcher().dispatch(NEXT_TRACK_EVENT, null);
+        mController.nextTrack();
     }
 
     /**
      * Select and play the previous track.
      * Move playback to the previous item in the playlist when supported.
      */
     public void previousTrack() {
+        if (!mControllerAttached) {
+            return;
+        }
         if (DEBUG) {
             Log.d(LOGTAG, "previousTrack");
         }
-        mSession.getEventDispatcher().dispatch(PREV_TRACK_EVENT, null);
+        mController.previousTrack();
     }
 
     /**
      * Skip the advertisement that is currently playing.
      */
     public void skipAd() {
+        if (!mControllerAttached) {
+            return;
+        }
         if (DEBUG) {
             Log.d(LOGTAG, "skipAd");
         }
-        mSession.getEventDispatcher().dispatch(SKIP_AD_EVENT, null);
+        mController.skipAd();
     }
 
     /**
      * Set whether audio should be muted.
      * Muting audio is supported by default and does not require the media
      * session to be active.
      *
      * @param mute True if audio for this media session should be muted.
      */
     public void muteAudio(final boolean mute) {
+        if (!mControllerAttached) {
+            return;
+        }
         if (DEBUG) {
             Log.d(LOGTAG, "muteAudio=" + mute);
         }
-        final GeckoBundle bundle = new GeckoBundle(1);
-        bundle.putBoolean("mute", mute);
-        mSession.getEventDispatcher().dispatch(MUTE_AUDIO_EVENT, bundle);
+        mController.muteAudio(mute);
     }
 
+    // TODO: Not sure if we want it.
+    // public void focus() {}
+
     /**
      * Implement this delegate to receive media session events.
      */
     @UiThread
     public interface Delegate {
         /**
          * Notify that the given media session has become active.
          *
@@ -286,16 +406,29 @@ public class MediaSession {
          * @param enabled True when this media session in in fullscreen mode.
          * @param meta An instance of {@link ElementMetadata}, if enabled.
          */
         default void onFullscreen(
                 @NonNull GeckoSession session,
                 @NonNull MediaSession mediaSession,
                 boolean enabled,
                 @Nullable ElementMetadata meta) {}
+
+        /**
+         * Notify on changed picture-in-picture mode state.
+         *
+         * @param session The associated GeckoSession.
+         * @param mediaSession The media session for the given GeckoSession.
+         * @param enabled True when this media session in in picture-in-picture
+         *                mode.
+         */
+        default void onPictureInPicture(
+                @NonNull GeckoSession session,
+                @NonNull MediaSession mediaSession,
+                boolean enabled) {}
     }
 
 
     /**
      * The representation of a media element's metadata.
      */
     public static class ElementMetadata {
         /**
@@ -539,18 +672,18 @@ public class MediaSession {
     }
 
     @Retention(RetentionPolicy.SOURCE)
     @LongDef(flag = true,
              value = {
                  Feature.NONE, Feature.PLAY, Feature.PAUSE, Feature.STOP,
                  Feature.SEEK_TO, Feature.SEEK_FORWARD, Feature.SEEK_BACKWARD,
                  Feature.SKIP_AD, Feature.NEXT_TRACK, Feature.PREVIOUS_TRACK,
-                 //Feature.SET_VIDEO_SURFACE
-             })
+                 //Feature.SET_VIDEO_SURFACE,
+                 Feature.FOCUS })
     /* package */ @interface MSFeature {}
 
     /**
      * Flags for supported media session features.
      */
     public static class Feature {
         public static final long NONE = 0;
 
@@ -634,59 +767,42 @@ public class MediaSession {
     private static final String METADATA_EVENT =
         "GeckoView:MediaSession:Metadata";
     private static final String POSITION_STATE_EVENT =
         "GeckoView:MediaSession:PositionState";
     private static final String FEATURES_EVENT =
         "GeckoView:MediaSession:Features";
     private static final String FULLSCREEN_EVENT =
         "GeckoView:MediaSession:Fullscreen";
+    private static final String PICTURE_IN_PICTURE_EVENT =
+        "GeckoView:MediaSession:PictureInPicture";
     private static final String PLAYBACK_NONE_EVENT =
         "GeckoView:MediaSession:Playback:None";
     private static final String PLAYBACK_PAUSED_EVENT =
         "GeckoView:MediaSession:Playback:Paused";
     private static final String PLAYBACK_PLAYING_EVENT =
         "GeckoView:MediaSession:Playback:Playing";
 
-    private static final String PLAY_EVENT =
-        "GeckoView:MediaSession:Play";
-    private static final String PAUSE_EVENT =
-        "GeckoView:MediaSession:Pause";
-    private static final String STOP_EVENT =
-        "GeckoView:MediaSession:Stop";
-    private static final String NEXT_TRACK_EVENT =
-        "GeckoView:MediaSession:NextTrack";
-    private static final String PREV_TRACK_EVENT =
-        "GeckoView:MediaSession:PrevTrack";
-    private static final String SEEK_FORWARD_EVENT =
-        "GeckoView:MediaSession:SeekForward";
-    private static final String SEEK_BACKWARD_EVENT =
-        "GeckoView:MediaSession:SeekBackward";
-    private static final String SKIP_AD_EVENT =
-        "GeckoView:MediaSession:SkipAd";
-    private static final String SEEK_TO_EVENT =
-        "GeckoView:MediaSession:SeekTo";
-    private static final String MUTE_AUDIO_EVENT =
-        "GeckoView:MediaSession:MuteAudio";
-
     /* package */ static class Handler
             extends GeckoSessionHandler<MediaSession.Delegate> {
 
         private final GeckoSession mSession;
         private final MediaSession mMediaSession;
 
         public Handler(final GeckoSession session) {
             super(
                 "GeckoViewMediaControl",
                 session,
                 new String[]{
+                    ATTACHED_EVENT,
                     ACTIVATED_EVENT,
                     DEACTIVATED_EVENT,
                     METADATA_EVENT,
                     FULLSCREEN_EVENT,
+                    PICTURE_IN_PICTURE_EVENT,
                     POSITION_STATE_EVENT,
                     PLAYBACK_NONE_EVENT,
                     PLAYBACK_PAUSED_EVENT,
                     PLAYBACK_PLAYING_EVENT,
                     FEATURES_EVENT,
                 });
             mSession = session;
             mMediaSession = new MediaSession(session);
@@ -697,25 +813,27 @@ public class MediaSession {
                 final Delegate delegate,
                 final String event,
                 final GeckoBundle message,
                 final EventCallback callback) {
             if (DEBUG) {
                 Log.d(LOGTAG, "handleMessage " + event);
             }
 
-            if (ACTIVATED_EVENT.equals(event)) {
-                mMediaSession.setActive(true);
+            if (ATTACHED_EVENT.equals(event)) {
                 delegate.onActivated(mSession, mMediaSession);
+            } else if (ACTIVATED_EVENT.equals(event)) {
+                mMediaSession.attachController(message.getLong("id"));
+                // TODO: We can call this direclty, once we move to webidl.
+                // delegate.onActivated(mSession, mMediaSession);
             } else if (DEACTIVATED_EVENT.equals(event)) {
-                mMediaSession.setActive(false);
+                mMediaSession.detachController();
                 delegate.onDeactivated(mSession, mMediaSession);
             } else if (METADATA_EVENT.equals(event)) {
-                final Metadata meta =
-                        Metadata.fromBundle(message.getBundle("metadata"));
+                final Metadata meta = Metadata.fromBundle(message);
                 delegate.onMetadata(mSession, mMediaSession, meta);
             } else if (POSITION_STATE_EVENT.equals(event)) {
                 final PositionState state =
                         PositionState.fromBundle(message.getBundle("state"));
                 delegate.onPositionState(mSession, mMediaSession, state);
             } else if (PLAYBACK_NONE_EVENT.equals(event)) {
                 delegate.onStop(mSession, mMediaSession);
             } else if (PLAYBACK_PAUSED_EVENT.equals(event)) {
@@ -727,12 +845,15 @@ public class MediaSession {
                         message.getBundle("features"));
                 delegate.onFeatures(mSession, mMediaSession, features);
             } else if (FULLSCREEN_EVENT.equals(event)) {
                 final boolean enabled = message.getBoolean("enabled");
                 final ElementMetadata meta =
                         ElementMetadata.fromBundle(
                                 message.getBundle("metadata"));
                 delegate.onFullscreen(mSession, mMediaSession, enabled, meta);
+            } else if (PICTURE_IN_PICTURE_EVENT.equals(event)) {
+                final boolean enabled = message.getBoolean("enabled");
+                delegate.onPictureInPicture(mSession, mMediaSession, enabled);
             }
         }
     }
 }
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtension.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtension.java
@@ -1653,22 +1653,16 @@ public class WebExtension {
          */
         public final boolean allowedInPrivateBrowsing;
 
         /**
          * Whether this extension is enabled or not.
          */
         public final boolean enabled;
 
-        /**
-         * Whether this extension is temporary or not. Temporary extensions are not retained
-         * and will be uninstalled when the browser exits.
-         */
-        public final boolean temporary;
-
         /** Override for testing. */
         protected MetaData() {
             icon = null;
             permissions = null;
             origins = null;
             name = null;
             description = null;
             version = null;
@@ -1677,17 +1671,16 @@ public class WebExtension {
             homepageUrl = null;
             optionsPageUrl = null;
             openOptionsPageInTab = false;
             isRecommended = false;
             blocklistState = BlocklistStateFlags.NOT_BLOCKED;
             signedState = SignedStateFlags.UNKNOWN;
             disabledFlags = 0;
             enabled = true;
-            temporary = false;
             baseUrl = null;
             allowedInPrivateBrowsing = false;
         }
 
         /* package */ MetaData(final GeckoBundle bundle) {
             // We only expose permissions that the embedder should prompt for
             permissions = bundle.getStringArray("promptPermissions");
             origins = bundle.getStringArray("origins");
@@ -1697,17 +1690,16 @@ public class WebExtension {
             creatorUrl = bundle.getString("creatorURL");
             homepageUrl = bundle.getString("homepageURL");
             name = bundle.getString("name");
             optionsPageUrl = bundle.getString("optionsPageURL");
             openOptionsPageInTab = bundle.getBoolean("openOptionsPageInTab");
             isRecommended = bundle.getBoolean("isRecommended");
             blocklistState = bundle.getInt("blocklistState", BlocklistStateFlags.NOT_BLOCKED);
             enabled = bundle.getBoolean("enabled", false);
-            temporary = bundle.getBoolean("temporary", false);
             baseUrl = bundle.getString("baseURL");
             allowedInPrivateBrowsing = bundle.getBoolean("privateBrowsingAllowed", false);
 
             int signedState = bundle.getInt("signedState", SignedStateFlags.UNKNOWN);
             if (signedState <= SignedStateFlags.LAST) {
                 this.signedState = signedState;
             } else {
                 Log.e(LOGTAG, "Unrecognized signed state: " + signedState);
--- 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
@@ -9,26 +9,16 @@ exclude: true
 {% capture javadoc_uri %}{{ site.url }}{{ site.baseurl}}/javadoc/mozilla-central/org/mozilla/geckoview{% endcapture %}
 {% capture bugzilla %}https://bugzilla.mozilla.org/show_bug.cgi?id={% endcapture %}
 
 # GeckoView API Changelog.
 
 ⚠️  breaking change and deprecation notices
 
 ## v83
-- ⚠️  Removing unsupported `MediaSession.Delegate.onPictureInPicture` for now.
-  Also, [`MediaSession.Delegate.onMetadata`][83.1] is no longer dispatched for
-  plain media elements.
-  ([bug 1658937]({{bugzilla}}1658937))
-- Added [`WebExtension.MetaData.temporary`][83.2] which exposes whether an extension
-  has been installed temporarily, e.g. when using web-ext.
-  ([bug 1624410]({{bugzilla}}1624410))
-
-[83.1]: {{javadoc_uri}}/MediaSession.Delegate.html#onMetadata-org.mozilla.geckoview.GeckoSession-org.mozilla.geckoview.MediaSession-org.mozilla.geckoview.MediaSession.Metadata-
-[83.2]: {{javadoc_uri}}/WebExtension.MetaData.html#temporary
 
 ## v82
 - ⚠️  [`WebNotification.source`][79.2] is now `@Nullable` to account for
   WebExtension notifications which don't have a `source` field.
 - ⚠️ Deprecated [`ContentDelegate#onExternalResponse(GeckoSession, WebResponseInfo)`][82.1] with the intention of removing
   them in GeckoView v85. 
   ([bug 1530022]({{bugzilla}}1530022))
 - Added [`ContentDelegate#onExternalResponse(GeckoSession, WebResponse)`][82.2] to eliminate the need
@@ -808,9 +798,9 @@ to allow adding gecko profiler markers.
 [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]: 01fdaf45cfe4dc5974de314cbdb79584781eb282
+[api-version]: e62341ace2541ae9fbd4f08f7c2d03ba28bf416a
--- a/mobile/android/modules/geckoview/GeckoViewMediaControl.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewMediaControl.jsm
@@ -26,99 +26,42 @@ class GeckoViewMediaControl extends Geck
       mozSystemGroup: true,
       capture: false,
     };
 
     this.controller.addEventListener("activated", this, options);
     this.controller.addEventListener("deactivated", this, options);
     this.controller.addEventListener("supportedkeyschange", this, options);
     this.controller.addEventListener("positionstatechange", this, options);
-    this.controller.addEventListener("metadatachange", this, options);
-    this.controller.addEventListener("playbackstatechange", this, options);
+    // TODO: Move other events to webidl once supported.
 
     this.messageManager.addMessageListener(
       "GeckoView:MediaControl:Fullscreen",
       this
     );
-
-    this.registerListener([
-      "GeckoView:MediaSession:Play",
-      "GeckoView:MediaSession:Pause",
-      "GeckoView:MediaSession:Stop",
-      "GeckoView:MediaSession:NextTrack",
-      "GeckoView:MediaSession:PrevTrack",
-      "GeckoView:MediaSession:SeekForward",
-      "GeckoView:MediaSession:SeekBackward",
-      "GeckoView:MediaSession:SkipAd",
-      "GeckoView:MediaSession:SeekTo",
-      "GeckoView:MediaSession:MuteAudio",
-    ]);
   }
 
   onDisable() {
     debug`onDisable`;
 
     this.controller.removeEventListener("activated", this);
     this.controller.removeEventListener("deactivated", this);
     this.controller.removeEventListener("supportedkeyschange", this);
     this.controller.removeEventListener("positionstatechange", this);
-    this.controller.removeEventListener("metadatachange", this);
-    this.controller.removeEventListener("playbackstatechange", this);
 
     this.messageManager.removeMessageListener(
       "GeckoView:MediaControl:Fullscreen",
       this
     );
   }
 
   get controller() {
     return this.browser.browsingContext.mediaController;
   }
 
-  onEvent(aEvent, aData, aCallback) {
-    debug`onEvent: event=${aEvent}, data=${aData}`;
-
-    switch (aEvent) {
-      case "GeckoView:MediaSession:Play":
-        this.controller.play();
-        break;
-      case "GeckoView:MediaSession:Pause":
-        this.controller.pause();
-        break;
-      case "GeckoView:MediaSession:Stop":
-        this.controller.stop();
-        break;
-      case "GeckoView:MediaSession:NextTrack":
-        this.controller.nextTrack();
-        break;
-      case "GeckoView:MediaSession:PrevTrack":
-        this.controller.prevTrack();
-        break;
-      case "GeckoView:MediaSession:SeekForward":
-        this.controller.seekForward();
-        break;
-      case "GeckoView:MediaSession:SeekBackward":
-        this.controller.seekBackward();
-        break;
-      case "GeckoView:MediaSession:SkipAd":
-        this.controller.skipAd();
-        break;
-      case "GeckoView:MediaSession:SeekTo":
-        this.controller.seekTo(aData.time, aData.fast);
-        break;
-      case "GeckoView:MediaSession:MuteAudio":
-        if (aData.mute) {
-          this.browser.mute();
-        } else {
-          this.browser.unmute();
-        }
-        break;
-    }
-  }
-
   receiveMessage(aMsg) {
     debug`receiveMessage: name=${aMsg.name}, data=${aMsg.data}`;
 
     switch (aMsg.name) {
       case "GeckoView:MediaControl:Fullscreen":
         this.handleFullscreenChanged(aMsg.data);
         break;
       default:
@@ -139,59 +82,57 @@ class GeckoViewMediaControl extends Geck
         this.handleDeactivated();
         break;
       case "supportedkeyschange":
         this.handleSupportedKeysChanged();
         break;
       case "positionstatechange":
         this.handlePositionStateChanged(aEvent);
         break;
-      case "metadatachange":
-        this.handleMetadataChanged();
-        break;
-      case "playbackstatechange":
-        this.handlePlaybackStateChanged();
-        break;
       default:
         warn`Unknown event type ${aEvent.type}`;
         break;
     }
   }
 
   handleFullscreenChanged(aData) {
     debug`handleFullscreenChanged ${aData.enabled}`;
 
     this.eventDispatcher.sendRequest({
       type: "GeckoView:MediaSession:Fullscreen",
+      id: this.controller.id,
       enabled: aData.enabled,
       metadata: aData.metadata,
     });
   }
 
   handleActivated() {
     debug`handleActivated`;
 
     this.eventDispatcher.sendRequest({
       type: "GeckoView:MediaSession:Activated",
+      id: this.controller.id,
     });
   }
 
   handleDeactivated() {
     debug`handleDeactivated`;
 
     this.eventDispatcher.sendRequest({
       type: "GeckoView:MediaSession:Deactivated",
+      id: this.controller.id,
     });
   }
 
   handlePositionStateChanged(aEvent) {
     debug`handlePositionStateChanged`;
 
     this.eventDispatcher.sendRequest({
       type: "GeckoView:MediaSession:PositionState",
+      id: this.controller.id,
       state: {
         duration: aEvent.duration,
         playbackRate: aEvent.playbackRate,
         position: aEvent.position,
       },
     });
   }
 
@@ -204,60 +145,17 @@ class GeckoViewMediaControl extends Geck
     // implementation for now.
     const features = new Map();
     supported.forEach(key => {
       features[key] = true;
     });
 
     this.eventDispatcher.sendRequest({
       type: "GeckoView:MediaSession:Features",
+      id: this.controller.id,
       features,
     });
   }
-
-  handleMetadataChanged() {
-    let metadata = null;
-    try {
-      metadata = this.controller.getMetadata();
-    } catch (e) {
-      warn`Metadata not available`;
-    }
-    debug`handleMetadataChanged ${metadata}`;
-
-    if (metadata) {
-      this.eventDispatcher.sendRequest({
-        type: "GeckoView:MediaSession:Metadata",
-        metadata,
-      });
-    }
-  }
-
-  handlePlaybackStateChanged() {
-    const state = this.controller.playbackState;
-    let type = null;
-
-    debug`handlePlaybackStateChanged ${state}`;
-
-    switch (state) {
-      case "none":
-        type = "GeckoView:MediaSession:Playback:None";
-        break;
-      case "paused":
-        type = "GeckoView:MediaSession:Playback:Paused";
-        break;
-      case "playing":
-        type = "GeckoView:MediaSession:Playback:Playing";
-        break;
-    }
-
-    if (!type) {
-      return;
-    }
-
-    this.eventDispatcher.sendRequest({
-      type,
-    });
-  }
 }
 
 const { debug, warn } = GeckoViewMediaControl.initLogging(
   "GeckoViewMediaControl"
 );
--- a/mobile/android/modules/geckoview/GeckoViewWebExtension.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewWebExtension.jsm
@@ -277,17 +277,16 @@ async function exportExtension(aAddon, a
     icons,
     version,
     optionsURL,
     optionsType,
     isRecommended,
     blocklistState,
     userDisabled,
     embedderDisabled,
-    temporarilyInstalled,
     isActive,
     isBuiltin,
     id,
   } = aAddon;
   let creatorName = null;
   let creatorURL = null;
   if (creator) {
     const { name, url } = creator;
@@ -315,17 +314,16 @@ async function exportExtension(aAddon, a
     locationURI: aSourceURI != null ? aSourceURI.spec : "",
     isBuiltIn: isBuiltin,
     webExtensionFlags: exportFlags(policy),
     metaData: {
       origins: aPermissions ? aPermissions.origins : [],
       promptPermissions,
       description,
       enabled: isActive,
-      temporary: temporarilyInstalled,
       disabledFlags,
       version,
       creatorName,
       creatorURL,
       homepageURL,
       name,
       optionsPageURL: optionsURL,
       openOptionsPageInTab,
--- a/widget/android/moz.build
+++ b/widget/android/moz.build
@@ -57,16 +57,17 @@ classes_with_WrapForJNI = [
     'GeckoSystemStateListener',
     'GeckoThread',
     'GeckoVRManager',
     'GeckoVideoInfo',
     'GeckoWebExecutor',
     'HardwareCodecCapabilityUtils',
     'ImageDecoder',
     'MediaDrmProxy',
+    'MediaSession',
     'PanZoomController',
     'PrefsHelper',
     'RuntimeTelemetry',
     'Sample',
     'SampleBuffer',
     'ScreenManagerHelper',
     'ServiceAllocator',
     'SessionAccessibility',
--- a/widget/android/nsWindow.cpp
+++ b/widget/android/nsWindow.cpp
@@ -24,16 +24,17 @@
 
 #include "mozilla/Preferences.h"
 #include "mozilla/Unused.h"
 #include "mozilla/a11y/SessionAccessibility.h"
 #include "mozilla/dom/BrowsingContext.h"
 #include "mozilla/dom/CanonicalBrowsingContext.h"
 #include "mozilla/dom/ContentChild.h"
 #include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/MediaControlService.h"
 #include "mozilla/dom/MouseEventBinding.h"
 #include "mozilla/gfx/2D.h"
 #include "mozilla/gfx/DataSurfaceHelpers.h"
 #include "mozilla/gfx/Types.h"
 #include "mozilla/layers/RenderTrace.h"
 #include <algorithm>
 
 using mozilla::Unused;
@@ -92,16 +93,17 @@ using mozilla::gfx::SurfaceFormat;
 #include "KeyEvent.h"
 #include "MotionEvent.h"
 #include "mozilla/java/EventDispatcherWrappers.h"
 #include "mozilla/java/GeckoAppShellWrappers.h"
 #include "mozilla/java/GeckoEditableChildWrappers.h"
 #include "mozilla/java/GeckoResultWrappers.h"
 #include "mozilla/java/GeckoSessionNatives.h"
 #include "mozilla/java/GeckoSystemStateListenerWrappers.h"
+#include "mozilla/java/MediaSessionNatives.h"
 #include "mozilla/java/PanZoomControllerNatives.h"
 #include "mozilla/java/SessionAccessibilityWrappers.h"
 #include "ScreenHelperAndroid.h"
 
 #include "GeckoProfiler.h"  // For AUTO_PROFILER_LABEL
 #include "nsPrintfCString.h"
 #include "nsString.h"
 
@@ -168,16 +170,365 @@ bool DispatchToUiThread(const char* aNam
 }
 }  // namespace
 
 namespace mozilla {
 namespace widget {
 
 using WindowPtr = jni::NativeWeakPtr<GeckoViewSupport>;
 
+class MediaSessionSupport final
+    : public mozilla::java::MediaSession::Controller::Natives<
+          MediaSessionSupport> {
+  using MediaKeysArray = nsTArray<MediaControlKey>;
+
+  typedef RefPtr<mozilla::dom::MediaController> ControllerPtr;
+
+  WindowPtr mWindow;
+  mozilla::java::MediaSession::Controller::WeakRef mJavaController;
+  ControllerPtr mMediaController;
+  MediaEventListener mMetadataChangedListener;
+  MediaEventListener mPlaybackChangedListener;
+
+ public:
+  typedef java::MediaSession::Controller::Natives<MediaSessionSupport> Base;
+
+  using Base::AttachNative;
+  using Base::DisposeNative;
+
+  MediaSessionSupport(
+      WindowPtr aWindow,
+      const java::MediaSession::Controller::LocalRef& aController)
+      : mWindow(aWindow),
+        mJavaController(aController),
+        mMediaController(nullptr) {
+#if defined(DEBUG)
+    auto win(mWindow.Access());
+    MOZ_ASSERT(!!win);
+#endif  // defined(DEBUG)
+  }
+
+  bool Dispatch(const char16_t aType[],
+                java::GeckoBundle::Param aBundle = nullptr) {
+    auto win = mWindow.Access();
+    if (!win) {
+      return false;
+    }
+
+    nsWindow* gkWindow = win->GetNsWindow();
+    if (!gkWindow) {
+      return false;
+    }
+
+    widget::EventDispatcher* dispatcher = gkWindow->GetEventDispatcher();
+    if (!dispatcher) {
+      return false;
+    }
+
+    dispatcher->Dispatch(aType, aBundle);
+
+    return true;
+  }
+
+  void PipChanged(const bool aEnabled) {
+    const size_t kBundleSize = 1;
+
+    AutoTArray<jni::String::LocalRef, kBundleSize> keys;
+    AutoTArray<jni::Object::LocalRef, kBundleSize> values;
+
+    keys.AppendElement(
+        jni::StringParam(NS_LITERAL_STRING_FROM_CSTRING("enabled")));
+    values.AppendElement(aEnabled ? java::sdk::Boolean::TRUE()
+                                  : java::sdk::Boolean::FALSE());
+
+    MOZ_ASSERT(kBundleSize == keys.Length());
+    MOZ_ASSERT(kBundleSize == values.Length());
+
+    auto bundleKeys = jni::ObjectArray::New<jni::String>(kBundleSize);
+    auto bundleValues = jni::ObjectArray::New<jni::Object>(kBundleSize);
+
+    for (size_t i = 0; i < kBundleSize; ++i) {
+      bundleKeys->SetElement(i, keys[i]);
+      bundleValues->SetElement(i, values[i]);
+    }
+    auto bundle = java::GeckoBundle::New(bundleKeys, bundleValues);
+
+    const char16_t kPictureInPicture[] =
+        u"GeckoView:MediaSession:PictureInPicture";
+    Dispatch(kPictureInPicture, bundle);
+  }
+
+  void MetadataChanged(const dom::MediaMetadataBase& aMetadata) {
+    MOZ_ASSERT(NS_IsMainThread());
+
+    const size_t kBundleSize = 4;
+
+    AutoTArray<jni::String::LocalRef, kBundleSize> keys;
+    AutoTArray<jni::Object::LocalRef, kBundleSize> values;
+
+    keys.AppendElement(
+        jni::StringParam(NS_LITERAL_STRING_FROM_CSTRING("title")));
+    values.AppendElement(jni::StringParam(aMetadata.mTitle));
+
+    keys.AppendElement(
+        jni::StringParam(NS_LITERAL_STRING_FROM_CSTRING("artist")));
+    values.AppendElement(jni::StringParam(aMetadata.mArtist));
+
+    keys.AppendElement(
+        jni::StringParam(NS_LITERAL_STRING_FROM_CSTRING("album")));
+    values.AppendElement(jni::StringParam(aMetadata.mAlbum));
+
+    auto images =
+        jni::ObjectArray::New<java::GeckoBundle>(aMetadata.mArtwork.Length());
+
+    for (size_t i = 0; i < aMetadata.mArtwork.Length(); ++i) {
+      const auto& image = aMetadata.mArtwork[i];
+
+      const size_t kImageBundleSize = 3;
+      auto imageKeys = jni::ObjectArray::New<jni::String>(kImageBundleSize);
+      auto imageValues = jni::ObjectArray::New<jni::String>(kImageBundleSize);
+
+      imageKeys->SetElement(
+          0, jni::StringParam(NS_LITERAL_STRING_FROM_CSTRING("src")));
+      imageValues->SetElement(0, jni::StringParam(image.mSrc));
+
+      imageKeys->SetElement(
+          1, jni::StringParam(NS_LITERAL_STRING_FROM_CSTRING("type")));
+      imageValues->SetElement(1, jni::StringParam(image.mType));
+
+      imageKeys->SetElement(
+          2, jni::StringParam(NS_LITERAL_STRING_FROM_CSTRING("sizes")));
+      imageValues->SetElement(2, jni::StringParam(image.mSizes));
+
+      images->SetElement(i, java::GeckoBundle::New(imageKeys, imageValues));
+    }
+
+    keys.AppendElement(
+        jni::StringParam(NS_LITERAL_STRING_FROM_CSTRING("artwork")));
+    values.AppendElement(images);
+
+    MOZ_ASSERT(kBundleSize == keys.Length());
+    MOZ_ASSERT(kBundleSize == values.Length());
+
+    auto bundleKeys = jni::ObjectArray::New<jni::String>(kBundleSize);
+    auto bundleValues = jni::ObjectArray::New<jni::Object>(kBundleSize);
+
+    for (size_t i = 0; i < kBundleSize; ++i) {
+      bundleKeys->SetElement(i, keys[i]);
+      bundleValues->SetElement(i, values[i]);
+    }
+    auto bundle = java::GeckoBundle::New(bundleKeys, bundleValues);
+
+    const char16_t kMetadata[] = u"GeckoView:MediaSession:Metadata";
+    Dispatch(kMetadata, bundle);
+  }
+
+  void PlaybackChanged(const MediaSessionPlaybackState& aState) {
+    MOZ_ASSERT(NS_IsMainThread());
+
+    const char16_t kPlaybackNone[] = u"GeckoView:MediaSession:Playback:None";
+    const char16_t kPlaybackPaused[] =
+        u"GeckoView:MediaSession:Playback:Paused";
+    const char16_t kPlaybackPlaying[] =
+        u"GeckoView:MediaSession:Playback:Playing";
+
+    switch (aState) {
+      case MediaSessionPlaybackState::None:
+        Dispatch(kPlaybackNone);
+        break;
+      case MediaSessionPlaybackState::Paused:
+        Dispatch(kPlaybackPaused);
+        break;
+      case MediaSessionPlaybackState::Playing:
+        Dispatch(kPlaybackPlaying);
+        break;
+      default:
+        MOZ_ASSERT_UNREACHABLE("Invalid MediaSessionPlaybackState");
+        break;
+    }
+  }
+
+  void OnWeakNonIntrusiveDetach(already_AddRefed<Runnable> aDisposer) {
+    MOZ_ASSERT(NS_IsMainThread());
+
+    RefPtr<Runnable> disposer = aDisposer;
+
+    SetNativeController(nullptr);
+
+    if (RefPtr<nsThread> uiThread = GetAndroidUiThread()) {
+      auto controller =
+          java::MediaSession::Controller::GlobalRef(mJavaController);
+      if (!controller) {
+        return;
+      }
+
+      uiThread->Dispatch(
+          NS_NewRunnableFunction("MEdiaSessionSupport::OnDetach",
+                                 [controller, disposer = std::move(disposer)] {
+                                   controller->OnDetached();
+                                   disposer->Run();
+                                 }));
+    }
+  }
+
+  const java::MediaSession::Controller::Ref& GetJavaController() const {
+    return mJavaController;
+  }
+
+  void SetNativeController(mozilla::dom::MediaController* aController) {
+    MOZ_ASSERT(NS_IsMainThread());
+
+    if (mMediaController == aController) {
+      return;
+    }
+
+    MOZ_ASSERT(!mMediaController || !aController);
+
+    if (mMediaController) {
+      UnregisterControllerListeners();
+    }
+
+    mMediaController = aController;
+
+    if (mMediaController) {
+      MetadataChanged(mMediaController->GetCurrentMediaMetadata());
+      PlaybackChanged(mMediaController->PlaybackState());
+
+      RegisterControllerListeners();
+    }
+  }
+
+  void RegisterControllerListeners() {
+    mMetadataChangedListener = mMediaController->MetadataChangedEvent().Connect(
+        AbstractThread::MainThread(), this,
+        &MediaSessionSupport::MetadataChanged);
+
+    mPlaybackChangedListener = mMediaController->PlaybackChangedEvent().Connect(
+        AbstractThread::MainThread(), this,
+        &MediaSessionSupport::PlaybackChanged);
+  }
+
+  void UnregisterControllerListeners() {
+    mMetadataChangedListener.DisconnectIfExists();
+    mPlaybackChangedListener.DisconnectIfExists();
+  }
+
+  bool IsActive() const {
+    MOZ_ASSERT(NS_IsMainThread());
+
+    return mMediaController && mMediaController->IsActive();
+  }
+
+  void Pause() {
+    MOZ_ASSERT(NS_IsMainThread());
+
+    if (!IsActive()) {
+      return;
+    }
+    mMediaController->Pause();
+  }
+
+  void Stop() {
+    MOZ_ASSERT(NS_IsMainThread());
+
+    if (!IsActive()) {
+      return;
+    }
+    mMediaController->Stop();
+  }
+
+  void Play() {
+    MOZ_ASSERT(NS_IsMainThread());
+
+    if (!IsActive()) {
+      return;
+    }
+    mMediaController->Play();
+  }
+
+  void Focus() {
+    MOZ_ASSERT(NS_IsMainThread());
+
+    if (!IsActive()) {
+      return;
+    }
+    mMediaController->Focus();
+  }
+
+  void NextTrack() {
+    MOZ_ASSERT(NS_IsMainThread());
+
+    if (!IsActive()) {
+      return;
+    }
+    mMediaController->NextTrack();
+  }
+
+  void PreviousTrack() {
+    MOZ_ASSERT(NS_IsMainThread());
+
+    if (!IsActive()) {
+      return;
+    }
+    mMediaController->PrevTrack();
+  }
+
+  void SeekTo(double aTime, bool aFast) {
+    MOZ_ASSERT(NS_IsMainThread());
+
+    if (!IsActive()) {
+      return;
+    }
+    mMediaController->SeekTo(aTime, aFast);
+  }
+
+  void SeekForward(double aOffset) {
+    MOZ_ASSERT(NS_IsMainThread());
+
+    if (!IsActive()) {
+      return;
+    }
+    mMediaController->SeekForward();
+  }
+
+  void SeekBackward(double aOffset) {
+    MOZ_ASSERT(NS_IsMainThread());
+
+    if (!IsActive()) {
+      return;
+    }
+    mMediaController->SeekBackward();
+  }
+
+  void SkipAd() {
+    MOZ_ASSERT(NS_IsMainThread());
+
+    if (!IsActive()) {
+      return;
+    }
+    mMediaController->SkipAd();
+  }
+
+  void MuteAudio(bool aMute) {
+    MOZ_ASSERT(NS_IsMainThread());
+
+    if (!IsActive()) {
+      return;
+    }
+
+    RefPtr<dom::BrowsingContext> bc =
+        dom::BrowsingContext::Get(mMediaController->Id());
+    if (!bc) {
+      return;
+    }
+
+    Unused << bc->SetMuted(aMute);
+  }
+};
+
 /**
  * PanZoomController handles its native calls on the UI thread, so make
  * it separate from GeckoViewSupport.
  */
 class NPZCSupport final
     : public java::PanZoomController::NativeProvider::Natives<NPZCSupport> {
   WindowPtr mWindow;
   java::PanZoomController::NativeProvider::WeakRef mNPZC;
@@ -1417,35 +1768,67 @@ void GeckoViewSupport::PassExternalRespo
 
   auto response = java::WebResponse::GlobalRef(aResponse);
 
   DispatchToUiThread("GeckoViewSupport::PassExternalResponse",
                      [window = java::GeckoSession::Window::GlobalRef(window),
                       response] { window->PassExternalWebResponse(response); });
 }
 
+void GeckoViewSupport::AttachMediaSessionController(
+    const GeckoSession::Window::LocalRef& inst, jni::Object::Param aController,
+    const int64_t aId) {
+  auto controller = java::MediaSession::Controller::LocalRef(
+      jni::GetGeckoThreadEnv(),
+      java::MediaSession::Controller::Ref::From(aController));
+  mWindow->mMediaSessionSupport =
+      jni::NativeWeakPtrHolder<MediaSessionSupport>::Attach(
+          controller, mWindow->mGeckoViewSupport, controller);
+
+  RefPtr<BrowsingContext> bc = BrowsingContext::Get(aId);
+  RefPtr<dom::MediaController> nativeController =
+      bc->Canonical()->GetMediaController();
+  MOZ_ASSERT(nativeController);
+
+  if (auto acc = mWindow->mMediaSessionSupport.Access()) {
+    acc->SetNativeController(nativeController);
+  }
+
+  DispatchToUiThread("GeckoViewSupport::AttachMediaSessionController",
+                     [controller = java::MediaSession::Controller::GlobalRef(
+                          controller)] { controller->OnAttached(); });
+}
+
+void GeckoViewSupport::DetachMediaSessionController(
+    const GeckoSession::Window::LocalRef& inst,
+    jni::Object::Param aController) {
+  mWindow->mMediaSessionSupport.Detach();
+}
+
 }  // namespace widget
 }  // namespace mozilla
 
 void nsWindow::InitNatives() {
   jni::InitConversionStatics();
   mozilla::widget::GeckoViewSupport::Base::Init();
   mozilla::widget::LayerViewSupport::Init();
+  mozilla::widget::MediaSessionSupport::Init();
   mozilla::widget::NPZCSupport::Init();
 
   mozilla::widget::GeckoEditableSupport::Init();
   a11y::SessionAccessibility::Init();
 }
 
 void nsWindow::DetachNatives() {
   MOZ_ASSERT(NS_IsMainThread());
   mEditableSupport.Detach();
   mNPZCSupport.Detach();
   mLayerViewSupport.Detach();
   mSessionAccessibility.Detach();
+  mMediaSessionSupport.Detach();
 }
 
 /* static */
 already_AddRefed<nsWindow> nsWindow::From(nsPIDOMWindowOuter* aDOMWindow) {
   nsCOMPtr<nsIWidget> widget = WidgetUtils::DOMWindowToWidget(aDOMWindow);
   return From(widget);
 }
 
--- a/widget/android/nsWindow.h
+++ b/widget/android/nsWindow.h
@@ -33,16 +33,17 @@ class APZCTreeManager;
 class UiCompositorControllerChild;
 }  // namespace layers
 
 namespace widget {
 class AndroidView;
 class GeckoEditableSupport;
 class GeckoViewSupport;
 class LayerViewSupport;
+class MediaSessionSupport;
 class NPZCSupport;
 }  // namespace widget
 
 namespace ipc {
 class Shmem;
 }  // namespace ipc
 
 namespace a11y {
@@ -90,16 +91,19 @@ class nsWindow final : public nsBaseWidg
       mEditableSupport;
   mozilla::jni::Object::GlobalRef mEditableParent;
 
   // Object that implements native SessionAccessibility calls.
   // Strong referenced by the Java instance.
   mozilla::jni::NativeWeakPtr<mozilla::a11y::SessionAccessibility>
       mSessionAccessibility;
 
+  mozilla::jni::NativeWeakPtr<mozilla::widget::MediaSessionSupport>
+      mMediaSessionSupport;
+
   // Object that implements native GeckoView calls and associated states.
   // nullptr for nsWindows that were not opened from GeckoView.
   mozilla::jni::NativeWeakPtr<mozilla::widget::GeckoViewSupport>
       mGeckoViewSupport;
 
   mozilla::Atomic<bool, mozilla::ReleaseAcquire> mContentDocumentDisplayed;
 
  public: