Bug 1384866 - Refactored MediaControlService logic to GeckoMediaControlAgent. r=jchen
authorVlad Baicu <vlad.baicu@softvision.ro>
Fri, 22 Jun 2018 19:02:54 +0300
changeset 818972 65677fe92b47b6a48984c204899803283f1c44e6
parent 818971 2d4bb9aa2e342902f53ce80ac155e97cffde34bc
child 818973 1022b1a54372b5a06732313ada9ef598e87edb80
push id116413
push userbgrinstead@mozilla.com
push dateMon, 16 Jul 2018 22:40:17 +0000
reviewersjchen
bugs1384866
milestone63.0a1
Bug 1384866 - Refactored MediaControlService logic to GeckoMediaControlAgent. r=jchen Moved the logic ouf of MediaControlService to a new singleton GeckoMediaControlAgent, which delegates all media-related actions.Currently, MediaControlService is used for the foreground notification and to retrieve actions from the notification pending intents. Removed redundant test cases. MozReview-Commit-ID: KukAmpnn33S
mobile/android/app/lint.xml
mobile/android/app/src/test/java/org/mozilla/gecko/media/TestGeckoMediaControlAgent.java
mobile/android/app/src/test/java/org/mozilla/gecko/media/TestMediaControlService.java
mobile/android/base/java/org/mozilla/gecko/media/AudioFocusAgent.java
mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaControlAgent.java
mobile/android/base/java/org/mozilla/gecko/media/MediaControlService.java
mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/MediaPlaybackTest.java
mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testMediaControl.java
--- a/mobile/android/app/lint.xml
+++ b/mobile/android/app/lint.xml
@@ -44,17 +44,17 @@
 
     <!-- Fixes are in progress but we would like to block future candidates.
          The ** in the path are wildcards. We need these wildcards to not change our code structure.
          See: http://stackoverflow.com/questions/43994420/what-path-is-the-issue-ignore-path-element-in-lint-xml-relative-to -->
     <issue id="NewApi" severity="error">
         <ignore path="**/mobile/android/base/java/org/mozilla/gecko/activitystream/ActivityStreamPreference.java"/>
         <ignore path="**/mobile/android/base/java/org/mozilla/gecko/dlc/DownloadContentTelemetry.java"/>
         <ignore path="**/mobile/android/base/java/org/mozilla/gecko/preferences/LocaleListPreference.java"/>
-        <ignore path="**/mobile/android/base/java/org/mozilla/gecko/media/MediaControlService.java"/>
+        <ignore path="**/mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaControlAgent.java"/>
         <ignore path="**/mobile/android/base/java/org/mozilla/gecko/PrintHelper.java"/>
         <ignore path="**/mobile/android/base/java/org/mozilla/gecko/animation/PropertyAnimator.java"/>
         <ignore path="**/mobile/android/base/java/org/mozilla/gecko/RemotePresentationService.java"/>
         <ignore path="**/mobile/android/base/java/org/mozilla/gecko/VirtualPresentation.java"/>
         <ignore path="**/mobile/android/base/java/org/mozilla/gecko/db/SharedBrowserDatabaseProvider.java"/>
         <ignore path="**/mobile/android/base/java/org/mozilla/gecko/tabs/TabPanelBackButton.java"/>
         <ignore path="**/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarEditText.java"/>
         <ignore path="**/mobile/android/base/java/org/mozilla/gecko/util/ViewUtil.java"/>
rename from mobile/android/app/src/test/java/org/mozilla/gecko/media/TestMediaControlService.java
rename to mobile/android/app/src/test/java/org/mozilla/gecko/media/TestGeckoMediaControlAgent.java
--- a/mobile/android/app/src/test/java/org/mozilla/gecko/media/TestMediaControlService.java
+++ b/mobile/android/app/src/test/java/org/mozilla/gecko/media/TestGeckoMediaControlAgent.java
@@ -6,83 +6,41 @@ package org.mozilla.gecko.media;
 import android.content.Context;
 import android.content.Intent;
 
 import junit.framework.Assert;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
 import org.mockito.internal.util.reflection.Whitebox;
-import org.mozilla.gecko.Tab;
-import org.mozilla.gecko.Tabs;
-import org.mozilla.gecko.media.MediaControlService.State;
-import org.robolectric.Robolectric;
 import org.robolectric.RobolectricTestRunner;
 
-import java.lang.ref.WeakReference;
-
-import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
 
 @RunWith(RobolectricTestRunner.class)
-public class TestMediaControlService {
+public class TestGeckoMediaControlAgent {
 
-    private MediaControlService mSpyService;
-    private AudioFocusAgent mSpyAudioAgent;
-    private Context mMockContext;
-    private Tab mMockTab;
+    private GeckoMediaControlAgent mSpyMediaAgent;
 
     @Before
     public void setUp() {
-        MediaControlService service = Robolectric.buildService(MediaControlService.class).get();
-        mSpyService = spy(service);
-        mSpyAudioAgent = spy(AudioFocusAgent.getInstance());
-        mMockContext = mock(Context.class);
-        mMockTab = mock(Tab.class);
+        Context mMockContext = mock(Context.class);
+        mSpyMediaAgent = spy(GeckoMediaControlAgent.getInstance());
         // We should use White-box as less as possible. But this is not avoidable so far.
-        Whitebox.setInternalState(mSpyService, "mInitialize", true);
-        Whitebox.setInternalState(mSpyAudioAgent,"mContext", mMockContext);
-    }
-
-    @Test
-    public void testTabPlayingMedia() throws Exception {
-        // If the tab is playing media and we got another MEDIA_PLAYING_CHANGE,
-        // we should notify the service that its state should be PLAYING.
-        Whitebox.setInternalState(mSpyAudioAgent, "mTabReference", new WeakReference<>(mMockTab));
-        doReturn(true).when(mMockTab).isMediaPlaying();
-
-        mSpyAudioAgent.onTabChanged(mMockTab, Tabs.TabEvents.MEDIA_PLAYING_CHANGE, "");
-        ArgumentCaptor<Intent> serviceIntent = ArgumentCaptor.forClass(Intent.class);
-        verify(mMockContext).startService(serviceIntent.capture());
-        Assert.assertEquals(MediaControlService.ACTION_TAB_STATE_PLAYING, serviceIntent.getValue().getAction());
-    }
-
-    @Test
-    public void testTabNotPlayingMedia() throws Exception {
-        // If the tab is not playing media and we got another MEDIA_PLAYING_CHANGE,
-        // we should notify the service that its state should be STOPPED.
-        Whitebox.setInternalState(mSpyAudioAgent, "mTabReference", new WeakReference<>(mMockTab));
-        doReturn(false).when(mMockTab).isMediaPlaying();
-
-        mSpyAudioAgent.onTabChanged(mMockTab, Tabs.TabEvents.MEDIA_PLAYING_CHANGE, "");
-        ArgumentCaptor<Intent> serviceIntent = ArgumentCaptor.forClass(Intent.class);
-        verify(mMockContext).startService(serviceIntent.capture());
-        Assert.assertEquals(MediaControlService.ACTION_TAB_STATE_STOPPED, serviceIntent.getValue().getAction());
+        Whitebox.setInternalState(mSpyMediaAgent, "mContext", mMockContext);
     }
 
     @Test
     public void testIntentForPlayingState() throws Exception {
         // For PLAYING state, should create an PAUSE intent for notification
-        Intent intent = mSpyService.createIntentUponState(State.PLAYING);
-        Assert.assertEquals(intent.getAction(), MediaControlService.ACTION_PAUSE);
+        Intent intent = mSpyMediaAgent.createIntentUponState(GeckoMediaControlAgent.State.PLAYING);
+        Assert.assertEquals(intent.getAction(), GeckoMediaControlAgent.ACTION_PAUSE);
     }
 
     @Test
     public void testIntentForPausedState() throws Exception {
         // For PAUSED state, should create an RESUME intent for notification
-        Intent intent = mSpyService.createIntentUponState(State.PAUSED);
-        Assert.assertEquals(intent.getAction(), MediaControlService.ACTION_RESUME);
+        Intent intent = mSpyMediaAgent.createIntentUponState(GeckoMediaControlAgent.State.PAUSED);
+        Assert.assertEquals(intent.getAction(), GeckoMediaControlAgent.ACTION_RESUME);
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/media/AudioFocusAgent.java
+++ b/mobile/android/base/java/org/mozilla/gecko/media/AudioFocusAgent.java
@@ -1,39 +1,44 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 package org.mozilla.gecko.media;
 
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.AudioManager.OnAudioFocusChangeListener;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+
+import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.annotation.RobocopTarget;
 import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.GeckoAppShell;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.content.Intent;
-import android.media.AudioManager;
-import android.media.AudioManager.OnAudioFocusChangeListener;
-import android.support.annotation.VisibleForTesting;
-import android.util.Log;
 
 import java.lang.ref.WeakReference;
 
 import static org.mozilla.gecko.AppConstants.Versions;
 
 public class AudioFocusAgent implements Tabs.OnTabsChangedListener {
     private static final String LOGTAG = "AudioFocusAgent";
 
     // We're referencing the *application* context, so this is in fact okay.
     @SuppressLint("StaticFieldLeak")
     private static Context mContext;
     private AudioManager mAudioManager;
     private OnAudioFocusChangeListener mAfChangeListener;
 
     private WeakReference<Tab> mTabReference = new WeakReference<>(null);
 
+    private GeckoMediaControlAgent geckoMediaControlAgent = GeckoMediaControlAgent.getInstance();
+
     public enum State {
         OWN_FOCUS,
         LOST_FOCUS,
         LOST_FOCUS_TRANSIENT,
         LOST_FOCUS_TRANSIENT_CAN_DUCK
     }
 
     private State mAudioFocusState = State.LOST_FOCUS;
@@ -57,49 +62,50 @@ public class AudioFocusAgent implements 
     }
 
     public synchronized void attachToContext(Context context) {
         if (isAttachedToContext()) {
             return;
         }
 
         mContext = context.getApplicationContext();
+        geckoMediaControlAgent.attachToContext(mContext);
         mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
         Tabs.registerOnTabsChangedListener(this);
 
         mAfChangeListener = new OnAudioFocusChangeListener() {
             public void onAudioFocusChange(int focusChange) {
                 switch (focusChange) {
                     case AudioManager.AUDIOFOCUS_LOSS:
                         Log.d(LOGTAG, "onAudioFocusChange, AUDIOFOCUS_LOSS");
                         mAudioFocusState = State.LOST_FOCUS;
                         notifyObservers("audioFocusChanged", "lostAudioFocus");
-                        notifyMediaControlService(MediaControlService.ACTION_PAUSE_BY_AUDIO_FOCUS);
+                        notifyMediaControlAgent(GeckoMediaControlAgent.ACTION_PAUSE_BY_AUDIO_FOCUS);
                         break;
                     case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                         Log.d(LOGTAG, "onAudioFocusChange, AUDIOFOCUS_LOSS_TRANSIENT");
                         mAudioFocusState = State.LOST_FOCUS_TRANSIENT;
                         notifyObservers("audioFocusChanged", "lostAudioFocusTransiently");
-                        notifyMediaControlService(MediaControlService.ACTION_PAUSE_BY_AUDIO_FOCUS);
+                        notifyMediaControlAgent(GeckoMediaControlAgent.ACTION_PAUSE_BY_AUDIO_FOCUS);
                         break;
                     case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                         Log.d(LOGTAG, "onAudioFocusChange, AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK");
                         mAudioFocusState = State.LOST_FOCUS_TRANSIENT_CAN_DUCK;
-                        notifyMediaControlService(MediaControlService.ACTION_START_AUDIO_DUCK);
+                        notifyMediaControlAgent(GeckoMediaControlAgent.ACTION_START_AUDIO_DUCK);
                         break;
                     case AudioManager.AUDIOFOCUS_GAIN:
                         State state = mAudioFocusState;
                         mAudioFocusState = State.OWN_FOCUS;
                         if (state.equals(State.LOST_FOCUS_TRANSIENT_CAN_DUCK)) {
                             Log.d(LOGTAG, "onAudioFocusChange, AUDIOFOCUS_GAIN (from DUCKING)");
-                            notifyMediaControlService(MediaControlService.ACTION_STOP_AUDIO_DUCK);
+                            notifyMediaControlAgent(GeckoMediaControlAgent.ACTION_STOP_AUDIO_DUCK);
                         } else if (state.equals(State.LOST_FOCUS_TRANSIENT)) {
                             Log.d(LOGTAG, "onAudioFocusChange, AUDIOFOCUS_GAIN");
                             notifyObservers("audioFocusChanged", "gainAudioFocus");
-                            notifyMediaControlService(MediaControlService.ACTION_RESUME_BY_AUDIO_FOCUS);
+                            notifyMediaControlAgent(GeckoMediaControlAgent.ACTION_RESUME_BY_AUDIO_FOCUS);
                         }
                         break;
                     default:
                 }
             }
         };
     }
 
@@ -167,56 +173,54 @@ public class AudioFocusAgent implements 
 
         final Tab playingTab = mTabReference.get();
         switch (msg) {
             case MEDIA_PLAYING_CHANGE:
                 // The 'MEDIA_PLAYING_CHANGE' would only be received when the
                 // media starts or ends.
                 if (playingTab != tab && tab.isMediaPlaying()) {
                     mTabReference = new WeakReference<>(tab);
-                    notifyMediaControlService(MediaControlService.ACTION_TAB_STATE_PLAYING);
+                    notifyMediaControlAgent(GeckoMediaControlAgent.ACTION_TAB_STATE_PLAYING);
                 } else if (playingTab == tab) {
                     mTabReference = new WeakReference<>(tab.isMediaPlaying() ? tab : null);
                     final String action = tab.isMediaPlaying()
-                            ? MediaControlService.ACTION_TAB_STATE_PLAYING
-                            : MediaControlService.ACTION_TAB_STATE_STOPPED;
-                    notifyMediaControlService(action);
+                            ? GeckoMediaControlAgent.ACTION_TAB_STATE_PLAYING
+                            : GeckoMediaControlAgent.ACTION_TAB_STATE_STOPPED;
+                    notifyMediaControlAgent(action);
                 }
                 break;
             case MEDIA_PLAYING_RESUME:
                 // user resume the paused-by-control media from page so that we
                 // should make the control interface consistent.
                 if (playingTab == tab) {
-                    notifyMediaControlService(MediaControlService.ACTION_TAB_STATE_RESUMED);
+                    notifyMediaControlAgent(GeckoMediaControlAgent.ACTION_TAB_STATE_RESUMED);
                 }
                 break;
             case CLOSED:
                 if (playingTab == null || playingTab == tab) {
                     // Remove the controls when the playing tab disappeared or was closed.
-                    notifyMediaControlService(MediaControlService.ACTION_TAB_STATE_STOPPED);
+                    notifyMediaControlAgent(GeckoMediaControlAgent.ACTION_TAB_STATE_STOPPED);
                 }
                 break;
             case FAVICON:
                 if (playingTab == tab) {
-                    notifyMediaControlService(MediaControlService.ACTION_TAB_STATE_FAVICON);
+                    notifyMediaControlAgent(GeckoMediaControlAgent.ACTION_TAB_STATE_FAVICON);
                 }
                 break;
         }
     }
 
-    private void notifyMediaControlService(String action) {
+    private void notifyMediaControlAgent(String action) {
         if (Versions.preLollipop) {
             // The notification only works from Lollipop onwards (at least until we try using
             // the support library version), so there's no point in starting the service.
             return;
         }
 
-        Intent intent = new Intent(mContext, MediaControlService.class);
-        intent.setAction(action);
-        mContext.startService(intent);
+        geckoMediaControlAgent.handleAction(action);
     }
 
     @VisibleForTesting
     @RobocopTarget
     public State getAudioFocusState() {
         return mAudioFocusState;
     }
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaControlAgent.java
@@ -0,0 +1,537 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+package org.mozilla.gecko.media;
+
+import android.annotation.SuppressLint;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.media.AudioManager;
+import android.media.VolumeProvider;
+import android.media.session.MediaController;
+import android.media.session.MediaSession;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.CheckResult;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.app.NotificationManagerCompat;
+import android.util.Log;
+
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoApplication;
+import org.mozilla.gecko.IntentHelper;
+import org.mozilla.gecko.PrefsHelper;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.Tab;
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import static org.mozilla.gecko.BuildConfig.DEBUG;
+
+public class GeckoMediaControlAgent {
+    private static final String LOGTAG = "GeckoMediaControlAgent";
+
+    @SuppressLint("StaticFieldLeak")
+    private static GeckoMediaControlAgent instance;
+    private Context mContext;
+
+    public static final String ACTION_RESUME         = "action_resume";
+    public static final String ACTION_PAUSE          = "action_pause";
+    public static final String ACTION_STOP           = "action_stop";
+    /* package */ static final String ACTION_RESUME_BY_AUDIO_FOCUS = "action_resume_audio_focus";
+    /* package */ static final String ACTION_PAUSE_BY_AUDIO_FOCUS  = "action_pause_audio_focus";
+    /* package */ static final String ACTION_START_AUDIO_DUCK      = "action_start_audio_duck";
+    /* package */ static final String ACTION_STOP_AUDIO_DUCK       = "action_stop_audio_duck";
+    /* package */ static final String ACTION_TAB_STATE_PLAYING = "action_tab_state_playing";
+    /* package */ static final String ACTION_TAB_STATE_STOPPED = "action_tab_state_stopped";
+    /* package */ static final String ACTION_TAB_STATE_RESUMED = "action_tab_state_resumed";
+    /* package */ static final String ACTION_TAB_STATE_FAVICON = "action_tab_state_favicon";
+    private static final String MEDIA_CONTROL_PREF = "dom.audiochannel.mediaControl";
+
+    // This is maximum volume level difference when audio ducking. The number is arbitrary.
+    private static final int AUDIO_DUCK_MAX_STEPS = 3;
+    private enum AudioDucking { START, STOP }
+    private boolean mSupportsDucking = false;
+    private int mAudioDuckCurrentSteps = 0;
+
+    private MediaSession mSession;
+    private MediaController mController;
+    private HeadSetStateReceiver mHeadSetStateReceiver;
+
+    private PrefsHelper.PrefHandler mPrefsObserver;
+    private final String[] mPrefs = { MEDIA_CONTROL_PREF };
+
+    private boolean mInitialized = false;
+    private boolean mIsMediaControlPrefOn = true;
+
+    private int minCoverSize;
+    private int coverSize;
+
+    private Notification currentNotification;
+
+    /**
+     * Internal state of MediaControlService, to indicate it is playing media, or paused...etc.
+     */
+    private State mMediaState = State.STOPPED;
+
+    protected enum State {
+        PLAYING,
+        PAUSED,
+        STOPPED
+    }
+
+    @RobocopTarget
+    public static GeckoMediaControlAgent getInstance() {
+        if (instance == null) {
+            instance = new GeckoMediaControlAgent();
+        }
+
+        return instance;
+    }
+
+    private GeckoMediaControlAgent() {}
+
+    public void attachToContext(Context context) {
+        if (isAttachedToContext()) {
+            return;
+        }
+
+        mContext = context;
+        initialize();
+    }
+
+    private boolean isAttachedToContext() {
+        return (mContext != null);
+    }
+
+    private void initialize() {
+        if (mInitialized) {
+            return;
+        }
+
+        if (!isAndroidVersionLollipopOrHigher()) {
+            return;
+        }
+
+        Log.d(LOGTAG, "initialize");
+        getGeckoPreference();
+        if (!initMediaSession()) {
+            if (DEBUG) {
+                Log.e(LOGTAG, "initialization fail!");
+            }
+            return;
+        }
+
+        coverSize = (int) mContext.getResources().getDimension(R.dimen.notification_media_cover);
+        minCoverSize = mContext.getResources().getDimensionPixelSize(R.dimen.favicon_bg);
+
+        mHeadSetStateReceiver = new HeadSetStateReceiver().registerReceiver(mContext);
+
+        mInitialized = true;
+    }
+
+    private boolean isAndroidVersionLollipopOrHigher() {
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
+    }
+
+    private void getGeckoPreference() {
+        mPrefsObserver = new PrefsHelper.PrefHandlerBase() {
+            @Override
+            public void prefValue(String pref, boolean value) {
+                if (pref.equals(MEDIA_CONTROL_PREF)) {
+                    mIsMediaControlPrefOn = value;
+
+                    // If media is playing, we just need to create or remove
+                    // the media control interface.
+                    if (mMediaState.equals(State.PLAYING)) {
+                        setState(mIsMediaControlPrefOn ? State.PLAYING : State.STOPPED);
+                    }
+
+                    // If turn off pref during pausing, except removing media
+                    // interface, we also need to stop the service and notify
+                    // gecko about that.
+                    if (mMediaState.equals(State.PAUSED) &&
+                            !mIsMediaControlPrefOn) {
+                        handleAction(ACTION_STOP);
+                    }
+                }
+            }
+        };
+        PrefsHelper.addObserver(mPrefs, mPrefsObserver);
+    }
+
+    private boolean initMediaSession() {
+        // Android MediaSession is introduced since version L.
+        try {
+            mSession = new MediaSession(mContext,
+                    "fennec media session");
+            mController = new MediaController(mContext,
+                    mSession.getSessionToken());
+        } catch (IllegalStateException e) {
+            if (DEBUG) {
+                Log.e(LOGTAG, "can't create MediaSession and MediaController!");
+            }
+            return false;
+        }
+
+        int volumeControl = mController.getPlaybackInfo().getVolumeControl();
+        if (volumeControl == VolumeProvider.VOLUME_CONTROL_ABSOLUTE ||
+                volumeControl == VolumeProvider.VOLUME_CONTROL_RELATIVE) {
+            mSupportsDucking = true;
+        } else {
+            if (DEBUG) {
+                Log.w(LOGTAG, "initMediaSession, Session does not support volume absolute or relative volume control");
+            }
+        }
+
+        mSession.setCallback(new MediaSession.Callback() {
+            @Override
+            public void onCustomAction(@NonNull String action, Bundle extras) {
+                if (action.equals(ACTION_PAUSE_BY_AUDIO_FOCUS)) {
+                    Log.d(LOGTAG, "Controller, pause by audio focus changed");
+                    setState(State.PAUSED);
+                } else if (action.equals(ACTION_RESUME_BY_AUDIO_FOCUS)) {
+                    Log.d(LOGTAG, "Controller, resume by audio focus changed");
+                    setState(State.PLAYING);
+                }
+            }
+
+            @Override
+            public void onPlay() {
+                Log.d(LOGTAG, "Controller, onPlay");
+                super.onPlay();
+                setState(State.PLAYING);
+                notifyObservers("mediaControl", "resumeMedia");
+            }
+
+            @Override
+            public void onPause() {
+                Log.d(LOGTAG, "Controller, onPause");
+                super.onPause();
+                setState(State.PAUSED);
+                notifyObservers("mediaControl", "mediaControlPaused");
+            }
+
+            @Override
+            public void onStop() {
+                Log.d(LOGTAG, "Controller, onStop");
+                super.onStop();
+                setState(State.STOPPED);
+                notifyObservers("mediaControl", "mediaControlStopped");
+                AudioFocusAgent.getInstance().clearActiveMediaTab();
+            }
+        });
+        mSession.setActive(true);
+        return true;
+    }
+
+    private void notifyObservers(String topic, String data) {
+        GeckoAppShell.notifyObservers(topic, data);
+    }
+
+    private boolean isNeedToRemoveControlInterface(State state) {
+        return state.equals(State.STOPPED);
+    }
+
+    private void setState(State newState) {
+        mMediaState = newState;
+        setMediaStateForTab(mMediaState.equals(State.PLAYING));
+        onStateChanged();
+    }
+
+    private void setMediaStateForTab(boolean isTabPlaying) {
+        final Tab tab = AudioFocusAgent.getInstance().getActiveMediaTab();
+        if (tab == null) {
+            return;
+        }
+        tab.setIsMediaPlaying(isTabPlaying);
+    }
+
+    private void onStateChanged() {
+        if (!mInitialized) {
+            return;
+        }
+
+        Log.d(LOGTAG, "onStateChanged, state = " + mMediaState);
+
+        if (isNeedToRemoveControlInterface(mMediaState)) {
+            stopForegroundService();
+            NotificationManagerCompat.from(mContext).cancel(R.id.mediaControlNotification);
+            release();
+            return;
+        }
+
+        if (!mIsMediaControlPrefOn) {
+            return;
+        }
+
+        final Tab tab = AudioFocusAgent.getInstance().getActiveMediaTab();
+
+        if (tab == null || tab.isPrivate()) {
+            return;
+        }
+
+        ThreadUtils.postToBackgroundThread(new Runnable() {
+            @Override
+            public void run() {
+                updateNotification(tab);
+            }
+        });
+    }
+
+    private boolean isMediaPlaying() {
+        return mMediaState.equals(State.PLAYING);
+    }
+
+    public void handleAction(String action) {
+        if (action == null) {
+            return;
+        }
+
+        if (!mInitialized && action.equals(ACTION_TAB_STATE_PLAYING)) {
+            initialize();
+        }
+
+        if (!mInitialized) {
+            return;
+        }
+
+        Log.d(LOGTAG, "HandleAction, action = " + action + ", mediaState = " + mMediaState);
+        switch (action) {
+            case ACTION_RESUME :
+                mController.getTransportControls().play();
+                break;
+            case ACTION_PAUSE :
+                mController.getTransportControls().pause();
+                break;
+            case ACTION_STOP :
+                mController.getTransportControls().stop();
+                break;
+            case ACTION_PAUSE_BY_AUDIO_FOCUS :
+                mController.getTransportControls().sendCustomAction(ACTION_PAUSE_BY_AUDIO_FOCUS, null);
+                break;
+            case ACTION_RESUME_BY_AUDIO_FOCUS :
+                mController.getTransportControls().sendCustomAction(ACTION_RESUME_BY_AUDIO_FOCUS, null);
+                break;
+            case ACTION_START_AUDIO_DUCK :
+                handleAudioDucking(AudioDucking.START);
+                break;
+            case ACTION_STOP_AUDIO_DUCK :
+                handleAudioDucking(AudioDucking.STOP);
+                break;
+            case ACTION_TAB_STATE_PLAYING :
+                setState(State.PLAYING);
+                break;
+            case ACTION_TAB_STATE_STOPPED :
+                setState(State.STOPPED);
+                break;
+            case ACTION_TAB_STATE_RESUMED :
+                if (!isMediaPlaying()) {
+                    setState(State.PLAYING);
+                }
+                break;
+            case ACTION_TAB_STATE_FAVICON :
+                setState(isMediaPlaying() ? State.PLAYING : State.PAUSED);
+                break;
+        }
+    }
+
+    private void handleAudioDucking(AudioDucking audioDucking) {
+        if (!mInitialized || !mSupportsDucking) {
+            return;
+        }
+
+        int currentVolume = mController.getPlaybackInfo().getCurrentVolume();
+        int maxVolume = mController.getPlaybackInfo().getMaxVolume();
+
+        int adjustDirection;
+        if (audioDucking == AudioDucking.START) {
+            mAudioDuckCurrentSteps = Math.min(AUDIO_DUCK_MAX_STEPS, currentVolume);
+            adjustDirection = AudioManager.ADJUST_LOWER;
+        } else {
+            mAudioDuckCurrentSteps = Math.min(mAudioDuckCurrentSteps, maxVolume - currentVolume);
+            adjustDirection = AudioManager.ADJUST_RAISE;
+        }
+
+        for (int i = 0; i < mAudioDuckCurrentSteps; i++) {
+            mController.adjustVolume(adjustDirection, 0);
+        }
+    }
+
+    @SuppressLint("NewApi")
+    private void setCurrentNotification(Tab tab, boolean onGoing, int visibility) {
+        final Notification.MediaStyle style = new Notification.MediaStyle();
+        style.setShowActionsInCompactView(0);
+
+        final Notification.Builder notificationBuilder = new Notification.Builder(mContext)
+                .setSmallIcon(R.drawable.ic_status_logo)
+                .setLargeIcon(generateCoverArt(tab))
+                .setContentTitle(tab.getTitle())
+                .setContentText(tab.getURL())
+                .setContentIntent(createContentIntent(tab))
+                .setDeleteIntent(createDeleteIntent())
+                .setStyle(style)
+                .addAction(createNotificationAction())
+                .setOngoing(onGoing)
+                .setShowWhen(false)
+                .setWhen(0)
+                .setVisibility(visibility);
+
+        if (!AppConstants.Versions.preO) {
+            notificationBuilder.setChannelId(GeckoApplication.getDefaultNotificationChannel().getId());
+        }
+
+        currentNotification = notificationBuilder.build();
+    }
+
+    /* package */ Notification getCurrentNotification() {
+        return currentNotification;
+    }
+
+    private void updateNotification(Tab tab) {
+        ThreadUtils.assertNotOnUiThread();
+
+        final boolean isPlaying = isMediaPlaying();
+        final int visibility = tab.isPrivate() ? Notification.VISIBILITY_PRIVATE : Notification.VISIBILITY_PUBLIC;
+        setCurrentNotification(tab, isPlaying, visibility);
+
+        if (isPlaying) {
+            startForegroundService();
+        } else {
+            stopForegroundService();
+            NotificationManagerCompat.from(mContext).notify(R.id.mediaControlNotification, getCurrentNotification());
+        }
+    }
+
+    private Notification.Action createNotificationAction() {
+        final Intent intent = createIntentUponState(mMediaState);
+        boolean isPlayAction = intent.getAction().equals(ACTION_RESUME);
+
+        int icon = isPlayAction ? R.drawable.ic_media_play : R.drawable.ic_media_pause;
+        String title = mContext.getString(isPlayAction ? R.string.media_play : R.string.media_pause);
+
+        final PendingIntent pendingIntent = PendingIntent.getService(mContext, 1, intent, 0);
+
+        //noinspection deprecation - The new constructor is only for API > 23
+        return new Notification.Action.Builder(icon, title, pendingIntent).build();
+    }
+
+    /**
+     * This method encapsulated UI logic. For PLAYING state, UI should display a PAUSE icon.
+     * @param state The expected current state of MediaControlService
+     * @return corresponding Intent to be used for Notification
+     */
+    @VisibleForTesting
+    Intent createIntentUponState(State state) {
+        String action = state.equals(State.PLAYING) ? ACTION_PAUSE : ACTION_RESUME;
+        final Intent intent = new Intent(mContext, MediaControlService.class);
+        intent.setAction(action);
+        return intent;
+    }
+
+    private PendingIntent createContentIntent(Tab tab) {
+        Intent intent = IntentHelper.getTabSwitchIntent(tab);
+        return PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+    }
+
+    private PendingIntent createDeleteIntent() {
+        Intent intent = new Intent(mContext, MediaControlService.class);
+        intent.setAction(ACTION_STOP);
+        return PendingIntent.getService(mContext, 1, intent, 0);
+    }
+
+    private Bitmap generateCoverArt(Tab tab) {
+        final Bitmap favicon = tab.getFavicon();
+
+        // If we do not have a favicon or if it's smaller than 72 pixels then just use the default icon.
+        if (favicon == null || favicon.getWidth() < minCoverSize || favicon.getHeight() < minCoverSize) {
+            // Use the launcher icon as fallback
+            return BitmapFactory.decodeResource(mContext.getResources(), R.drawable.notification_media);
+        }
+
+        // Favicon should at least have half of the size of the cover
+        int width = Math.max(favicon.getWidth(), coverSize / 2);
+        int height = Math.max(favicon.getHeight(), coverSize / 2);
+
+        final Bitmap coverArt = Bitmap.createBitmap(coverSize, coverSize, Bitmap.Config.ARGB_8888);
+        final Canvas canvas = new Canvas(coverArt);
+        canvas.drawColor(0xFF777777);
+
+        int left = Math.max(0, (coverArt.getWidth() / 2) - (width / 2));
+        int right = Math.min(coverSize, left + width);
+        int top = Math.max(0, (coverArt.getHeight() / 2) - (height / 2));
+        int bottom = Math.min(coverSize, top + height);
+
+        final Paint paint = new Paint();
+        paint.setAntiAlias(true);
+
+        canvas.drawBitmap(favicon,
+                new Rect(0, 0, favicon.getWidth(), favicon.getHeight()),
+                new Rect(left, top, right, bottom),
+                paint);
+
+        return coverArt;
+    }
+
+    @SuppressLint("NewApi")
+    private void startForegroundService() {
+        Intent intent = new Intent(mContext, MediaControlService.class);
+
+        if (AppConstants.Versions.preO) {
+            mContext.startService(intent);
+        } else {
+            mContext.startForegroundService(intent);
+        }
+    }
+
+    private void stopForegroundService() {
+        mContext.stopService(new Intent(mContext, MediaControlService.class));
+    }
+
+    private void release() {
+        if (!mInitialized) {
+            return;
+        }
+        mInitialized = false;
+
+        Log.d(LOGTAG, "release");
+        if (!mMediaState.equals(State.STOPPED)) {
+            setState(State.STOPPED);
+        }
+        PrefsHelper.removeObserver(mPrefsObserver);
+        mHeadSetStateReceiver.unregisterReceiver(mContext);
+        mSession.release();
+    }
+
+    private class HeadSetStateReceiver extends BroadcastReceiver {
+        @CheckResult(suggest = "new HeadSetStateReceiver().registerReceiver(Context)")
+        HeadSetStateReceiver registerReceiver(Context context) {
+            IntentFilter intentFilter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
+            context.registerReceiver(this, intentFilter);
+            return this;
+        }
+
+        void unregisterReceiver(Context context) {
+            context.unregisterReceiver(HeadSetStateReceiver.this);
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (isMediaPlaying()) {
+                handleAction(ACTION_PAUSE);
+            }
+        }
+    }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/media/MediaControlService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/media/MediaControlService.java
@@ -1,504 +1,29 @@
 package org.mozilla.gecko.media;
 
-import android.app.Notification;
-import android.app.PendingIntent;
 import android.app.Service;
-import android.content.BroadcastReceiver;
-import android.content.Context;
 import android.content.Intent;
-import android.content.IntentFilter;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.media.AudioManager;
-import android.media.VolumeProvider;
-import android.media.session.MediaController;
-import android.media.session.MediaSession;
-import android.os.Build;
-import android.os.Bundle;
 import android.os.IBinder;
-import android.support.annotation.CheckResult;
-import android.support.annotation.NonNull;
-import android.support.annotation.VisibleForTesting;
-import android.support.v4.app.NotificationManagerCompat;
 import android.util.Log;
 
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoApplication;
-import org.mozilla.gecko.IntentHelper;
-import org.mozilla.gecko.PrefsHelper;
 import org.mozilla.gecko.R;
-import org.mozilla.gecko.Tab;
-import org.mozilla.gecko.util.ThreadUtils;
 
 public class MediaControlService extends Service {
     private static final String LOGTAG = "MediaControlService";
 
-    public static final String ACTION_RESUME         = "action_resume";
-    public static final String ACTION_PAUSE          = "action_pause";
-    public static final String ACTION_STOP           = "action_stop";
-    public static final String ACTION_RESUME_BY_AUDIO_FOCUS = "action_resume_audio_focus";
-    public static final String ACTION_PAUSE_BY_AUDIO_FOCUS  = "action_pause_audio_focus";
-    public static final String ACTION_START_AUDIO_DUCK      = "action_start_audio_duck";
-    public static final String ACTION_STOP_AUDIO_DUCK       = "action_stop_audio_duck";
-    /* package */ static final String ACTION_TAB_STATE_PLAYING = "action_tab_state_playing";
-    /* package */ static final String ACTION_TAB_STATE_STOPPED = "action_tab_state_stopped";
-    /* package */ static final String ACTION_TAB_STATE_RESUMED = "action_tab_state_resumed";
-    /* package */ static final String ACTION_TAB_STATE_FAVICON = "action_tab_state_favicon";
-    private static final String MEDIA_CONTROL_PREF = "dom.audiochannel.mediaControl";
-
-    // This is maximum volume level difference when audio ducking. The number is arbitrary.
-    private static final int AUDIO_DUCK_MAX_STEPS = 3;
-    private enum AudioDucking { START, STOP }
-    private boolean mSupportsDucking = false;
-    private int mAudioDuckCurrentSteps = 0;
-
-    private MediaSession mSession;
-    private MediaController mController;
-    private HeadSetStateReceiver mHeadSetStateReceiver;
-
-    private PrefsHelper.PrefHandler mPrefsObserver;
-    private final String[] mPrefs = { MEDIA_CONTROL_PREF };
-
-    private boolean mInitialize = false;
-    private boolean mIsMediaControlPrefOn = true;
-
-    private int minCoverSize;
-    private int coverSize;
-
-    /**
-     * Internal state of MediaControlService, to indicate it is playing media, or paused...etc.
-     */
-    private State mMediaState = State.STOPPED;
-
-    protected enum State {
-        PLAYING,
-        PAUSED,
-        STOPPED
-    }
-
-    @Override
-    public void onCreate() {
-        initialize();
-    }
-
-    @Override
-    public void onDestroy() {
-        shutdown();
-    }
-
     @Override
     public int onStartCommand(Intent intent, int flags, int startId) {
-        handleIntent(intent);
+        Log.d(LOGTAG, "onStartCommand");
+        startForeground(R.id.mediaControlNotification, GeckoMediaControlAgent.getInstance().getCurrentNotification());
+
+        if (intent != null && intent.getAction() != null) {
+            GeckoMediaControlAgent.getInstance().handleAction(intent.getAction());
+        }
+
         return START_NOT_STICKY;
     }
 
     @Override
     public IBinder onBind(Intent intent) {
         return null;
     }
-
-    @Override
-    public void onTaskRemoved(Intent rootIntent) {
-        shutdown();
-    }
-
-    private boolean isMediaPlaying() {
-        return mMediaState.equals(State.PLAYING);
-    }
-
-    private void initialize() {
-        if (mInitialize) {
-            return;
-        }
-
-        if (!isAndroidVersionLollipopOrHigher()) {
-            stopSelf();
-            return;
-        }
-
-        Log.d(LOGTAG, "initialize");
-        getGeckoPreference();
-        if (!initMediaSession()) {
-             Log.e(LOGTAG, "initialization fail!");
-             stopSelf();
-             return;
-        }
-
-        coverSize = (int) getResources().getDimension(R.dimen.notification_media_cover);
-        minCoverSize = getResources().getDimensionPixelSize(R.dimen.favicon_bg);
-
-        mHeadSetStateReceiver = new HeadSetStateReceiver().registerReceiver(getApplicationContext());
-
-        mInitialize = true;
-    }
-
-    private void shutdown() {
-        if (!mInitialize) {
-            return;
-        }
-        mInitialize = false;
-
-        Log.d(LOGTAG, "shutdown");
-        if (!mMediaState.equals(State.STOPPED)) {
-            setState(State.STOPPED);
-        }
-        PrefsHelper.removeObserver(mPrefsObserver);
-        mHeadSetStateReceiver.unregisterReceiver(getApplicationContext());
-        mSession.release();
-
-        stopSelf();
-    }
-
-    private boolean isAndroidVersionLollipopOrHigher() {
-        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
-    }
-
-    private void handleIntent(Intent intent) {
-        if (intent == null || intent.getAction() == null || !mInitialize) {
-            return;
-        }
-
-        Log.d(LOGTAG, "HandleIntent, action = " + intent.getAction() + ", mediaState = " + mMediaState);
-        switch (intent.getAction()) {
-            case ACTION_RESUME :
-                mController.getTransportControls().play();
-                break;
-            case ACTION_PAUSE :
-                mController.getTransportControls().pause();
-                break;
-            case ACTION_STOP :
-                mController.getTransportControls().stop();
-                break;
-            case ACTION_PAUSE_BY_AUDIO_FOCUS :
-                mController.getTransportControls().sendCustomAction(ACTION_PAUSE_BY_AUDIO_FOCUS, null);
-                break;
-            case ACTION_RESUME_BY_AUDIO_FOCUS :
-                mController.getTransportControls().sendCustomAction(ACTION_RESUME_BY_AUDIO_FOCUS, null);
-                break;
-            case ACTION_START_AUDIO_DUCK :
-                handleAudioDucking(AudioDucking.START);
-                break;
-            case ACTION_STOP_AUDIO_DUCK :
-                handleAudioDucking(AudioDucking.STOP);
-                break;
-            case ACTION_TAB_STATE_PLAYING :
-                setState(State.PLAYING);
-                break;
-            case ACTION_TAB_STATE_STOPPED :
-                setState(State.STOPPED);
-                break;
-            case ACTION_TAB_STATE_RESUMED :
-                if (!isMediaPlaying()) {
-                    setState(State.PLAYING);
-                }
-                break;
-            case ACTION_TAB_STATE_FAVICON :
-                setState(isMediaPlaying() ? State.PLAYING : State.PAUSED);
-                break;
-        }
-    }
-
-    private void handleAudioDucking(AudioDucking audioDucking) {
-        if (!mInitialize || !mSupportsDucking) {
-            return;
-        }
-
-        int currentVolume = mController.getPlaybackInfo().getCurrentVolume();
-        int maxVolume = mController.getPlaybackInfo().getMaxVolume();
-
-        int adjustDirection;
-        if (audioDucking == AudioDucking.START) {
-            mAudioDuckCurrentSteps = Math.min(AUDIO_DUCK_MAX_STEPS, currentVolume);
-            adjustDirection = AudioManager.ADJUST_LOWER;
-        } else {
-            mAudioDuckCurrentSteps = Math.min(mAudioDuckCurrentSteps, maxVolume - currentVolume);
-            adjustDirection = AudioManager.ADJUST_RAISE;
-        }
-
-        for (int i = 0; i < mAudioDuckCurrentSteps; i++) {
-            mController.adjustVolume(adjustDirection, 0);
-        }
-    }
-
-    private void getGeckoPreference() {
-        mPrefsObserver = new PrefsHelper.PrefHandlerBase() {
-            @Override
-            public void prefValue(String pref, boolean value) {
-                if (pref.equals(MEDIA_CONTROL_PREF)) {
-                    mIsMediaControlPrefOn = value;
-
-                    // If media is playing, we just need to create or remove
-                    // the media control interface.
-                    if (mMediaState.equals(State.PLAYING)) {
-                        setState(mIsMediaControlPrefOn ? State.PLAYING : State.STOPPED);
-                    }
-
-                    // If turn off pref during pausing, except removing media
-                    // interface, we also need to stop the service and notify
-                    // gecko about that.
-                    if (mMediaState.equals(State.PAUSED) &&
-                        !mIsMediaControlPrefOn) {
-                        Intent intent = new Intent(getApplicationContext(), MediaControlService.class);
-                        intent.setAction(ACTION_STOP);
-                        handleIntent(intent);
-                    }
-                }
-            }
-        };
-        PrefsHelper.addObserver(mPrefs, mPrefsObserver);
-    }
-
-    private boolean initMediaSession() {
-        // Android MediaSession is introduced since version L.
-        try {
-            mSession = new MediaSession(getApplicationContext(),
-                                        "fennec media session");
-            mController = new MediaController(getApplicationContext(),
-                                              mSession.getSessionToken());
-        } catch (IllegalStateException e) {
-            Log.e(LOGTAG, "can't create MediaSession and MediaController!");
-            return false;
-        }
-
-        int volumeControl = mController.getPlaybackInfo().getVolumeControl();
-        if (volumeControl == VolumeProvider.VOLUME_CONTROL_ABSOLUTE ||
-                volumeControl == VolumeProvider.VOLUME_CONTROL_RELATIVE) {
-            mSupportsDucking = true;
-        } else {
-            Log.w(LOGTAG, "initMediaSession, Session does not support volume absolute or relative volume control");
-        }
-
-        mSession.setCallback(new MediaSession.Callback() {
-            @Override
-            public void onCustomAction(@NonNull String action, Bundle extras) {
-                if (action.equals(ACTION_PAUSE_BY_AUDIO_FOCUS)) {
-                    Log.d(LOGTAG, "Controller, pause by audio focus changed");
-                    setState(State.PAUSED);
-                } else if (action.equals(ACTION_RESUME_BY_AUDIO_FOCUS)) {
-                    Log.d(LOGTAG, "Controller, resume by audio focus changed");
-                    setState(State.PLAYING);
-                }
-            }
-
-            @Override
-            public void onPlay() {
-                Log.d(LOGTAG, "Controller, onPlay");
-                super.onPlay();
-                setState(State.PLAYING);
-                notifyObservers("mediaControl", "resumeMedia");
-            }
-
-            @Override
-            public void onPause() {
-                Log.d(LOGTAG, "Controller, onPause");
-                super.onPause();
-                setState(State.PAUSED);
-                notifyObservers("mediaControl", "mediaControlPaused");
-            }
-
-            @Override
-            public void onStop() {
-                Log.d(LOGTAG, "Controller, onStop");
-                super.onStop();
-                setState(State.STOPPED);
-                notifyObservers("mediaControl", "mediaControlStopped");
-                AudioFocusAgent.getInstance().clearActiveMediaTab();
-            }
-        });
-        mSession.setActive(true);
-        return true;
-    }
-
-    private void setMediaStateForTab(boolean isTabPlaying) {
-        final Tab tab = AudioFocusAgent.getInstance().getActiveMediaTab();
-        if (tab == null) {
-            return;
-        }
-        tab.setIsMediaPlaying(isTabPlaying);
-    }
-
-    private void notifyObservers(String topic, String data) {
-        GeckoAppShell.notifyObservers(topic, data);
-    }
-
-    private boolean isNeedToRemoveControlInterface(State state) {
-        return state.equals(State.STOPPED);
-    }
-
-    private void setState(State newState) {
-        mMediaState = newState;
-        setMediaStateForTab(mMediaState.equals(State.PLAYING));
-        onStateChanged();
-    }
-
-    private void onStateChanged() {
-        if (!mInitialize) {
-            return;
-        }
-
-        Log.d(LOGTAG, "onStateChanged, state = " + mMediaState);
-
-        if (isNeedToRemoveControlInterface(mMediaState)) {
-            stopForeground(false);
-            NotificationManagerCompat.from(this).cancel(R.id.mediaControlNotification);
-            shutdown();
-            return;
-        }
-
-        if (!mIsMediaControlPrefOn) {
-            return;
-        }
-
-        final Tab tab = AudioFocusAgent.getInstance().getActiveMediaTab();
-
-        if (tab == null || tab.isPrivate()) {
-            return;
-        }
-
-        ThreadUtils.postToBackgroundThread(new Runnable() {
-            @Override
-            public void run() {
-                updateNotification(tab);
-            }
-        });
-    }
-
-    @SuppressWarnings("NewApi")
-    protected void updateNotification(Tab tab) {
-        ThreadUtils.assertNotOnUiThread();
-
-        final Notification.MediaStyle style = new Notification.MediaStyle();
-        style.setShowActionsInCompactView(0);
-
-        final boolean isPlaying = isMediaPlaying();
-        final int visibility = tab.isPrivate() ?
-            Notification.VISIBILITY_PRIVATE : Notification.VISIBILITY_PUBLIC;
-
-        final Notification.Builder notificationBuilder = new Notification.Builder(this)
-            .setSmallIcon(R.drawable.ic_status_logo)
-            .setLargeIcon(generateCoverArt(tab))
-            .setContentTitle(tab.getTitle())
-            .setContentText(tab.getURL())
-            .setContentIntent(createContentIntent(tab))
-            .setDeleteIntent(createDeleteIntent())
-            .setStyle(style)
-            .addAction(createNotificationAction())
-            .setOngoing(isPlaying)
-            .setShowWhen(false)
-            .setWhen(0)
-            .setVisibility(visibility);
-
-        if (!AppConstants.Versions.preO) {
-            notificationBuilder.setChannelId(GeckoApplication.getDefaultNotificationChannel().getId());
-        }
-
-        final Notification notification = notificationBuilder.build();
-
-        if (isPlaying) {
-            startForeground(R.id.mediaControlNotification, notification);
-        } else {
-            stopForeground(false);
-            NotificationManagerCompat.from(this)
-                .notify(R.id.mediaControlNotification, notification);
-        }
-    }
-
-    private Notification.Action createNotificationAction() {
-        final Intent intent = createIntentUponState(mMediaState);
-        boolean isPlayAction = intent.getAction().equals(ACTION_RESUME);
-
-        int icon = isPlayAction ? R.drawable.ic_media_play : R.drawable.ic_media_pause;
-        String title = getString(isPlayAction ? R.string.media_play : R.string.media_pause);
-
-        final PendingIntent pendingIntent = PendingIntent.getService(getApplicationContext(), 1, intent, 0);
-
-        //noinspection deprecation - The new constructor is only for API > 23
-        return new Notification.Action.Builder(icon, title, pendingIntent).build();
-    }
-
-    /**
-     * This method encapsulated UI logic. For PLAYING state, UI should display a PAUSE icon.
-     * @param state The expected current state of MediaControlService
-     * @return corresponding Intent to be used for Notification
-     */
-    @VisibleForTesting
-    protected Intent createIntentUponState(State state) {
-        String action = state.equals(State.PLAYING) ? ACTION_PAUSE : ACTION_RESUME;
-        final Intent intent = new Intent(getApplicationContext(), MediaControlService.class);
-        intent.setAction(action);
-        return intent;
-    }
-
-    private PendingIntent createContentIntent(Tab tab) {
-        Intent intent = IntentHelper.getTabSwitchIntent(tab);
-        return PendingIntent.getActivity(getApplicationContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
-    }
-
-    private PendingIntent createDeleteIntent() {
-        Intent intent = new Intent(getApplicationContext(), MediaControlService.class);
-        intent.setAction(ACTION_STOP);
-        return  PendingIntent.getService(getApplicationContext(), 1, intent, 0);
-    }
-
-    private Bitmap generateCoverArt(Tab tab) {
-        final Bitmap favicon = tab.getFavicon();
-
-        // If we do not have a favicon or if it's smaller than 72 pixels then just use the default icon.
-        if (favicon == null || favicon.getWidth() < minCoverSize || favicon.getHeight() < minCoverSize) {
-            // Use the launcher icon as fallback
-            return BitmapFactory.decodeResource(getResources(), R.drawable.notification_media);
-        }
-
-        // Favicon should at least have half of the size of the cover
-        int width = Math.max(favicon.getWidth(), coverSize / 2);
-        int height = Math.max(favicon.getHeight(), coverSize / 2);
-
-        final Bitmap coverArt = Bitmap.createBitmap(coverSize, coverSize, Bitmap.Config.ARGB_8888);
-        final Canvas canvas = new Canvas(coverArt);
-        canvas.drawColor(0xFF777777);
-
-        int left = Math.max(0, (coverArt.getWidth() / 2) - (width / 2));
-        int right = Math.min(coverSize, left + width);
-        int top = Math.max(0, (coverArt.getHeight() / 2) - (height / 2));
-        int bottom = Math.min(coverSize, top + height);
-
-        final Paint paint = new Paint();
-        paint.setAntiAlias(true);
-
-        canvas.drawBitmap(favicon,
-                new Rect(0, 0, favicon.getWidth(), favicon.getHeight()),
-                new Rect(left, top, right, bottom),
-                paint);
-
-        return coverArt;
-    }
-
-    private class HeadSetStateReceiver extends BroadcastReceiver {
-
-        @CheckResult(suggest = "new HeadSetStateReceiver().registerReceiver(Context)")
-        HeadSetStateReceiver registerReceiver(Context context) {
-            IntentFilter intentFilter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
-            context.registerReceiver(this, intentFilter);
-            return this;
-        }
-
-        void unregisterReceiver(Context context) {
-            context.unregisterReceiver(HeadSetStateReceiver.this);
-        }
-
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            if (isMediaPlaying()) {
-                Intent pauseIntent = new Intent(getApplicationContext(), MediaControlService.class);
-                pauseIntent.setAction(ACTION_PAUSE);
-                handleIntent(pauseIntent);
-            }
-        }
-
-    }
-
 }
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/MediaPlaybackTest.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/MediaPlaybackTest.java
@@ -1,32 +1,30 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.tests;
 
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.os.Build;
+import android.service.notification.StatusBarNotification;
+
+import com.robotium.solo.Condition;
+
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.media.AudioFocusAgent;
 import org.mozilla.gecko.media.AudioFocusAgent.State;
-import org.mozilla.gecko.media.MediaControlService;
+import org.mozilla.gecko.media.GeckoMediaControlAgent;
 import org.mozilla.gecko.tests.helpers.JavascriptBridge;
 
-import android.content.Intent;
-import android.content.Context;
-
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.os.Build;
-import android.service.notification.StatusBarNotification;
-
-import com.robotium.solo.Condition;
-
 abstract class MediaPlaybackTest extends OldBaseTest {
     private Context mContext;
     private int mPrevIcon = 0;
     protected String mPrevURL = "";
     private JavascriptBridge mJs;
 
     private static final int UI_CHANGED_WAIT_MS = 6000;
     private static final int MEDIA_PLAYBACK_CHANGED_WAIT_MS = 30000;
@@ -140,20 +138,20 @@ abstract class MediaPlaybackTest extends
                      "Checking the audio playing state of tab, isTabPlaying = " + isTabPlaying,
                      "Tab's audio playing state is correct.");
     }
 
     /**
      * Since we can't testing media control via clicking the media control, we
      * directly send intent to service to simulate the behavior.
      */
-    protected final void notifyMediaControlService(String action) {
-        Intent intent = new Intent(getContext(), MediaControlService.class);
-        intent.setAction(action);
-        getContext().startService(intent);
+    protected final void notifyMediaControlAgent(String action) {
+        GeckoMediaControlAgent geckoMediaControlAgent = GeckoMediaControlAgent.getInstance();
+        geckoMediaControlAgent.attachToContext(getContext());
+        geckoMediaControlAgent.handleAction(action);
     }
 
     /**
      * Use these methods when both media control and audio focus state should
      * be changed and you want to check whether the changing are correct or not.
      * Checking selected tab is default option.
      */
     protected final void checkIfMediaPlayingSuccess(boolean isTabPlaying) {
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testMediaControl.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testMediaControl.java
@@ -1,19 +1,19 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.tests;
 
+import android.media.AudioManager;
+
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
-import org.mozilla.gecko.media.MediaControlService;
-
-import android.media.AudioManager;
+import org.mozilla.gecko.media.GeckoMediaControlAgent;
 
 public class testMediaControl extends MediaPlaybackTest {
     public void testMediaControl() {
         info("- ensure the test is running on correct Android version -");
         checkAndroidVersionForMediaControlTest();
 
         info("- wait for gecko ready -");
         blockForGeckoReady();
@@ -56,25 +56,25 @@ public class testMediaControl extends Me
         info("- load URL -");
         final String MEDIA_URL = getAbsoluteUrl(mStringHelper.ROBOCOP_MEDIA_PLAYBACK_LOOP_URL);
         loadUrlAndWait(MEDIA_URL);
 
         info("- check whether media starts playing -");
         checkIfMediaPlayingSuccess(true /* playing */);
 
         info("- simulate media control pause -");
-        notifyMediaControlService(MediaControlService.ACTION_PAUSE);
+        notifyMediaControlAgent(GeckoMediaControlAgent.ACTION_PAUSE);
         checkIfMediaPlayingSuccess(false /* paused */);
 
         info("- simulate media control resume -");
-        notifyMediaControlService(MediaControlService.ACTION_RESUME);
+        notifyMediaControlAgent(GeckoMediaControlAgent.ACTION_RESUME);
         checkIfMediaPlayingSuccess(true /* playing */);
 
         info("- simulate media control stop -");
-        notifyMediaControlService(MediaControlService.ACTION_STOP);
+        notifyMediaControlAgent(GeckoMediaControlAgent.ACTION_STOP);
         checkIfMediaPlayingSuccess(false /* paused */, true /* clear notification */);
 
         info("- close tab -");
         closeAllTabs();
     }
 
     private void testNavigateOutThePage() {
         info("- load URL -");
@@ -174,17 +174,17 @@ public class testMediaControl extends Me
 
         info("- play media -");
         getJS().syncCall("play_audio");
 
         info("- check whether media starts playing -");
         checkIfMediaPlayingSuccess(true /* playing */);
 
         info("- simulate media control pause -");
-        notifyMediaControlService(MediaControlService.ACTION_PAUSE);
+        notifyMediaControlAgent(GeckoMediaControlAgent.ACTION_PAUSE);
         checkIfMediaPlayingSuccess(false /* paused */);
 
         info("- resume media from page -");
         getJS().syncCall("play_audio");
         checkIfMediaPlayingSuccess(true /* playing */);
 
         info("- pause media from page -");
         getJS().syncCall("pause_audio");