Bug 1384866 - Refactored MediaControlService logic to GeckoMediaControlAgent. r?sdaswani
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
--- 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");