Bug 1240423 - part1 : implement the remote media-control on Fennec. r=ahunt

MozReview-Commit-ID: GjkSCy5ecbQ

--HG--
extra : rebase_source : d0f3c81e5f42d556a19e3283b8b0dfdc0e3e7381
This commit is contained in:
Alastor Wu 2016-06-01 10:26:01 +08:00
parent e149d9fa0d
commit ae6fa1e22d
6 changed files with 320 additions and 9 deletions

View File

@ -924,6 +924,9 @@ pref("dom.presentation.enabled", true);
pref("dom.presentation.discovery.enabled", true);
pref("dom.audiochannel.audioCompeting", true);
// TODO : turn this pref default on in bug1264901
pref("dom.audiochannel.mediaControl", false);
// TODO : remove it after landing bug1242874 because now it's the only way to
// suspend the MediaElement.
pref("media.useAudioChannelAPI", true);

View File

@ -234,6 +234,10 @@
android:process="@MANGLED_ANDROID_PACKAGE_NAME@.Restarter">
</service>
<service android:name="org.mozilla.gecko.media.MediaControlService"
android:exported="false">
</service>
<receiver android:name="org.mozilla.gecko.AlarmReceiver" >
</receiver>

View File

@ -6,6 +6,7 @@ import org.mozilla.gecko.EventDispatcher;
import org.mozilla.gecko.GeckoAppShell;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.media.AudioManager.OnAudioFocusChangeListener;
import android.util.Log;
@ -17,7 +18,11 @@ public class AudioFocusAgent {
private AudioManager mAudioManager;
private OnAudioFocusChangeListener mAfChangeListener;
private boolean mIsOwningAudioFocus = false;
public static final String OWN_FOCUS = "own_focus";
public static final String LOST_FOCUS = "lost_focus";
public static final String LOST_FOCUS_TRANSIENT = "lost_focus_transient";
private String mAudioFocusState = LOST_FOCUS;
@WrapForJNI
public static void notifyStartedPlaying() {
@ -51,15 +56,20 @@ public class AudioFocusAgent {
case AudioManager.AUDIOFOCUS_LOSS:
Log.d(LOGTAG, "onAudioFocusChange, AUDIOFOCUS_LOSS");
notifyObservers("AudioFocusChanged", "lostAudioFocus");
// TODO : to dispatch audio-stop from gecko to trigger abandonAudioFocusIfNeeded
notifyMediaControlService(MediaControlService.ACTION_PAUSE);
mAudioFocusState = LOST_FOCUS;
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
Log.d(LOGTAG, "onAudioFocusChange, AUDIOFOCUS_LOSS_TRANSIENT");
notifyObservers("AudioFocusChanged", "lostAudioFocusTransiently");
notifyMediaControlService(MediaControlService.ACTION_PAUSE);
mAudioFocusState = LOST_FOCUS_TRANSIENT;
break;
case AudioManager.AUDIOFOCUS_GAIN:
Log.d(LOGTAG, "onAudioFocusChange, AUDIOFOCUS_GAIN");
notifyObservers("AudioFocusChanged", "gainAudioFocus");
notifyMediaControlService(MediaControlService.ACTION_PLAY);
mAudioFocusState = OWN_FOCUS;
break;
default:
}
@ -87,7 +97,7 @@ public class AudioFocusAgent {
private AudioFocusAgent() {}
private void requestAudioFocusIfNeeded() {
if (mIsOwningAudioFocus) {
if (mAudioFocusState.equals(OWN_FOCUS)) {
return;
}
@ -98,19 +108,26 @@ public class AudioFocusAgent {
String focusMsg = (result == AudioManager.AUDIOFOCUS_GAIN) ?
"AudioFocus request granted" : "AudioFoucs request failed";
Log.d(LOGTAG, focusMsg);
// TODO : Enable media control when get the AudioFocus, see bug1240423.
if (result == AudioManager.AUDIOFOCUS_GAIN) {
mIsOwningAudioFocus = true;
mAudioFocusState = OWN_FOCUS;
notifyMediaControlService(MediaControlService.ACTION_START);
}
}
private void abandonAudioFocusIfNeeded() {
if (!mIsOwningAudioFocus) {
if (!mAudioFocusState.equals(OWN_FOCUS)) {
return;
}
Log.d(LOGTAG, "Abandon AudioFocus");
mAudioManager.abandonAudioFocus(mAfChangeListener);
mIsOwningAudioFocus = false;
mAudioFocusState = LOST_FOCUS;
notifyMediaControlService(MediaControlService.ACTION_STOP);
}
private void notifyMediaControlService(String action) {
Intent intent = new Intent(mContext, MediaControlService.class);
intent.setAction(action);
mContext.startService(intent);
}
}

View File

@ -0,0 +1,284 @@
package org.mozilla.gecko.media;
import org.mozilla.gecko.BrowserApp;
import org.mozilla.gecko.EventDispatcher;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoEvent;
import org.mozilla.gecko.PrefsHelper;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.media.session.MediaController;
import android.media.session.MediaSession;
import android.media.session.MediaSessionManager;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import android.R;
public class MediaControlService extends Service {
private static final String LOGTAG = "MediaControlService";
public static final String ACTION_START = "action_start";
public static final String ACTION_PLAY = "action_play";
public static final String ACTION_PAUSE = "action_pause";
public static final String ACTION_STOP = "action_stop";
public static final String ACTION_REMOVE_CONTROL = "action_remove_control";
private static final int MEDIA_CONTROL_ID = 1;
private static final String MEDIA_CONTROL_PREF = "dom.audiochannel.mediaControl";
private String mActionState = ACTION_STOP;
private MediaSession mSession;
private MediaController mController;
private PrefsHelper.PrefHandler mPrefsObserver;
private final String[] mPrefs = { MEDIA_CONTROL_PREF };
private boolean mIsInitMediaSession = false;
private boolean mIsMediaControlPrefOn = true;
@Override
public void onCreate() {
getGeckoPreference();
initMediaSession();
}
@Override
public void onDestroy() {
notifyControlInterfaceChanged(ACTION_REMOVE_CONTROL);
PrefsHelper.removeObserver(mPrefsObserver);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
handleIntent(intent);
return super.onStartCommand(intent, flags, startId);
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public boolean onUnbind(Intent intent) {
mSession.release();
return super.onUnbind(intent);
}
@Override
public void onTaskRemoved(Intent rootIntent) {
stopSelf();
}
private boolean isAndroidVersionLollopopOrHigher() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
}
private void handleIntent(Intent intent) {
if(intent == null || intent.getAction() == null ||
!mIsInitMediaSession) {
return;
}
Log.d(LOGTAG, "HandleIntent, action = " + intent.getAction() + ", actionState = " + mActionState);
switch (intent.getAction()) {
case ACTION_START :
mController.getTransportControls().sendCustomAction(ACTION_START, null);
break;
case ACTION_PLAY :
mController.getTransportControls().play();
break;
case ACTION_PAUSE :
mController.getTransportControls().pause();
break;
case ACTION_STOP :
if (!mActionState.equals(ACTION_PLAY)) {
return;
}
mController.getTransportControls().stop();
break;
case ACTION_REMOVE_CONTROL :
mController.getTransportControls().stop();
break;
}
}
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 (mActionState.equals(ACTION_PLAY)) {
notifyControlInterfaceChanged(mIsMediaControlPrefOn ?
ACTION_PAUSE : ACTION_REMOVE_CONTROL);
}
// If turn off pref during pausing, except removing media
// interface, we also need to stop the service and notify
// gecko about that.
if (mActionState.equals(ACTION_PAUSE) &&
!mIsMediaControlPrefOn) {
Intent intent = new Intent(getApplicationContext(), MediaControlService.class);
intent.setAction(ACTION_REMOVE_CONTROL);
handleIntent(intent);
}
}
}
};
PrefsHelper.addObserver(mPrefs, mPrefsObserver);
}
private void initMediaSession() {
if (!isAndroidVersionLollopopOrHigher() || mIsInitMediaSession) {
return;
}
// Android MediaSession is introduced since version L.
mSession = new MediaSession(getApplicationContext(),
"fennec media session");
mController = new MediaController(getApplicationContext(),
mSession.getSessionToken());
mSession.setCallback(new MediaSession.Callback() {
@Override
public void onCustomAction(String action, Bundle extras) {
if (action.equals(ACTION_START)) {
Log.d(LOGTAG, "Controller, onStart");
notifyControlInterfaceChanged(ACTION_PAUSE);
mActionState = ACTION_PLAY;
}
}
@Override
public void onPlay() {
Log.d(LOGTAG, "Controller, onPlay");
super.onPlay();
notifyControlInterfaceChanged(ACTION_PAUSE);
notifyObservers("MediaControl", "resumeMedia");
mActionState = ACTION_PLAY;
}
@Override
public void onPause() {
Log.d(LOGTAG, "Controller, onPause");
super.onPause();
notifyControlInterfaceChanged(ACTION_PLAY);
notifyObservers("MediaControl", "mediaControlPaused");
mActionState = ACTION_PAUSE;
}
@Override
public void onStop() {
Log.d(LOGTAG, "Controller, onStop");
super.onStop();
notifyControlInterfaceChanged(ACTION_STOP);
notifyObservers("MediaControl", "mediaControlStopped");
mActionState = ACTION_STOP;
stopSelf();
}
});
mIsInitMediaSession = true;
}
private void notifyObservers(String topic, String data) {
GeckoAppShell.notifyObservers(topic, data);
}
private boolean isNeedToRemoveControlInterface(String action) {
return (action.equals(ACTION_STOP) ||
action.equals(ACTION_REMOVE_CONTROL));
}
private void notifyControlInterfaceChanged(String action) {
Log.d(LOGTAG, "notifyControlInterfaceChanged, action = " + action);
NotificationManager notificationManager = (NotificationManager)
getSystemService(Context.NOTIFICATION_SERVICE);
if (isNeedToRemoveControlInterface(action)) {
notificationManager.cancel(MEDIA_CONTROL_ID);
return;
}
if (!mIsMediaControlPrefOn) {
return;
}
notificationManager.notify(MEDIA_CONTROL_ID, getNotification(action));
}
private Notification getNotification(String action) {
// TODO : use website name, content and favicon in bug1264901.
return new Notification.Builder(this)
.setSmallIcon(android.R.drawable.ic_media_play)
.setContentTitle("Media Title")
.setContentText("Media Artist")
.setDeleteIntent(getDeletePendingIntent())
.setContentIntent(getClickPendingIntent())
.setStyle(getMediaStyle())
.addAction(getAction(action))
.setOngoing(action.equals(ACTION_PAUSE))
.build();
}
private Notification.Action getAction(String action) {
int icon = getActionIcon(action);
String title = getActionTitle(action);
Intent intent = new Intent(getApplicationContext(), MediaControlService.class);
intent.setAction(action);
PendingIntent pendingIntent = PendingIntent.getService(getApplicationContext(), 1, intent, 0);
return new Notification.Action.Builder(icon, title, pendingIntent).build();
}
private int getActionIcon(String action) {
switch (action) {
case ACTION_PLAY :
return android.R.drawable.ic_media_play;
case ACTION_PAUSE :
return android.R.drawable.ic_media_pause;
default:
return 0;
}
}
private String getActionTitle(String action) {
switch (action) {
case ACTION_PLAY :
return "Play";
case ACTION_PAUSE :
return "Pause";
default:
return null;
}
}
private PendingIntent getDeletePendingIntent() {
Intent intent = new Intent(getApplicationContext(), MediaControlService.class);
intent.setAction(ACTION_REMOVE_CONTROL);
return PendingIntent.getService(getApplicationContext(), 1, intent, 0);
}
private PendingIntent getClickPendingIntent() {
Intent intent = new Intent(getApplicationContext(), BrowserApp.class);
return PendingIntent.getActivity(getApplicationContext(), 0, intent, 0);
}
private Notification.MediaStyle getMediaStyle() {
Notification.MediaStyle style = new Notification.MediaStyle();
style.setShowActionsInCompactView(0);
return style;
}
}

View File

@ -455,6 +455,7 @@ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
'lwt/LightweightThemeDrawable.java',
'mdns/MulticastDNSManager.java',
'media/AudioFocusAgent.java',
'media/MediaControlService.java',
'MediaCastingBar.java',
'MemoryMonitor.java',
'menu/GeckoMenu.java',

View File

@ -749,6 +749,7 @@ var AudioPlaybackListener = {
init() {
Services.obs.addObserver(this, "audio-playback", false);
Services.obs.addObserver(this, "AudioFocusChanged", false);
Services.obs.addObserver(this, "MediaControl", false);
addMessageListener("AudioPlayback", this);
addEventListener("unload", () => {
@ -759,6 +760,7 @@ var AudioPlaybackListener = {
uninit() {
Services.obs.removeObserver(this, "audio-playback");
Services.obs.removeObserver(this, "AudioFocusChanged");
Services.obs.removeObserver(this, "MediaControl");
removeMessageListener("AudioPlayback", this);
},
@ -775,7 +777,7 @@ var AudioPlaybackListener = {
utils.audioMuted = false;
break;
case "lostAudioFocus":
utils.mediaSuspend = suspendTypes.SUSPENDED_STOP_DISPOSABLE;
utils.mediaSuspend = suspendTypes.SUSPENDED_PAUSE_DISPOSABLE;
break;
case "lostAudioFocusTransiently":
utils.mediaSuspend = suspendTypes.SUSPENDED_PAUSE;
@ -808,7 +810,7 @@ var AudioPlaybackListener = {
name += (data === "active") ? "Start" : "Stop";
sendAsyncMessage(name);
}
} else if (topic == "AudioFocusChanged") {
} else if (topic == "AudioFocusChanged" || topic == "MediaControl") {
this.handleMediaControlMessage(data);
}
},