Bug 1384866 - Refactored MediaControlService. r?sdaswani
Refactored the MediaControlService to a bound service.
MozReview-Commit-ID: CYNJ0xjbCjh
--- a/mobile/android/base/java/org/mozilla/gecko/media/AudioFocusAgent.java
+++ b/mobile/android/base/java/org/mozilla/gecko/media/AudioFocusAgent.java
@@ -2,38 +2,48 @@ package org.mozilla.gecko.media;
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.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.ServiceConnection;
import android.media.AudioManager;
import android.media.AudioManager.OnAudioFocusChangeListener;
+import android.os.IBinder;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
import static org.mozilla.gecko.AppConstants.Versions;
-public class AudioFocusAgent implements Tabs.OnTabsChangedListener {
+public class AudioFocusAgent implements Tabs.OnTabsChangedListener, ServiceConnection {
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 boolean isServiceBounded = false;
+ private boolean isAttemptingToBind = false;
+ private List<String> actionsQueue = new ArrayList<>();
+ private MediaControlService mediaControlService;
+
public enum State {
OWN_FOCUS,
LOST_FOCUS,
LOST_FOCUS_TRANSIENT,
LOST_FOCUS_TRANSIENT_CAN_DUCK
}
private State mAudioFocusState = State.LOST_FOCUS;
@@ -204,19 +214,77 @@ public class AudioFocusAgent implements
private void notifyMediaControlService(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;
}
+ if (isServiceBounded) {
+ mediaControlService.handleAction(action);
+ } else {
+ if (isAttemptingToBind) {
+ queueAction(action);
+ } else {
+ bindService(action);
+ }
+ }
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ isServiceBounded = true;
+ isAttemptingToBind = false;
+ MediaControlService.Binder mBinder = (MediaControlService.Binder) service;
+ mediaControlService = mBinder.getService();
+ checkActionQueue();
+ mBinder.setListener(new MediaControlService.TaskFinishedListener() {
+ @Override
+ public void onFinish() {
+ unbindService();
+ }
+ });
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ isServiceBounded = false;
+ }
+
+ private void bindService(String action) {
+ isAttemptingToBind = true;
Intent intent = new Intent(mContext, MediaControlService.class);
intent.setAction(action);
mContext.startService(intent);
+ mContext.bindService(intent, this, Context.BIND_ALLOW_OOM_MANAGEMENT);
+ }
+
+ private void unbindService() {
+ isAttemptingToBind = false;
+ if (isServiceBounded) {
+ mContext.unbindService(AudioFocusAgent.this);
+ isServiceBounded = false;
+ }
+ mContext.stopService(new Intent(mContext, MediaControlService.class));
+ }
+
+ private void queueAction(String action) {
+ actionsQueue.add(action);
+ }
+
+ private void checkActionQueue() {
+ if (actionsQueue.isEmpty()) {
+ return;
+ }
+
+ for (String action : actionsQueue) {
+ mediaControlService.handleAction(action);
+ }
+ actionsQueue.clear();
}
@VisibleForTesting
@RobocopTarget
public State getAudioFocusState() {
return mAudioFocusState;
}
--- a/mobile/android/base/java/org/mozilla/gecko/media/MediaControlService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/media/MediaControlService.java
@@ -67,16 +67,19 @@ public class MediaControlService extends
private int minCoverSize;
private int coverSize;
/**
* Internal state of MediaControlService, to indicate it is playing media, or paused...etc.
*/
private State mMediaState = State.STOPPED;
+ private IBinder mBinder = new Binder();
+ private TaskFinishedListener listener;
+
protected enum State {
PLAYING,
PAUSED,
STOPPED
}
@Override
public void onCreate() {
@@ -85,23 +88,28 @@ public class MediaControlService extends
@Override
public void onDestroy() {
shutdown();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
- handleIntent(intent);
+ handleAction(intent.getAction());
return START_NOT_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
- return null;
+ return mBinder;
+ }
+
+ @Override
+ public boolean onUnbind(Intent intent) {
+ return true;
}
@Override
public void onTaskRemoved(Intent rootIntent) {
shutdown();
}
private boolean isMediaPlaying() {
@@ -109,25 +117,25 @@ public class MediaControlService extends
}
private void initialize() {
if (mInitialize) {
return;
}
if (!isAndroidVersionLollipopOrHigher()) {
- stopSelf();
+ stopService();
return;
}
Log.d(LOGTAG, "initialize");
getGeckoPreference();
if (!initMediaSession()) {
Log.e(LOGTAG, "initialization fail!");
- stopSelf();
+ stopService();
return;
}
coverSize = (int) getResources().getDimension(R.dimen.notification_media_cover);
minCoverSize = getResources().getDimensionPixelSize(R.dimen.favicon_bg);
mHeadSetStateReceiver = new HeadSetStateReceiver().registerReceiver(getApplicationContext());
@@ -143,30 +151,30 @@ public class MediaControlService extends
Log.d(LOGTAG, "shutdown");
if (!mMediaState.equals(State.STOPPED)) {
setState(State.STOPPED);
}
PrefsHelper.removeObserver(mPrefsObserver);
mHeadSetStateReceiver.unregisterReceiver(getApplicationContext());
mSession.release();
- stopSelf();
+ stopService();
}
private boolean isAndroidVersionLollipopOrHigher() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
}
- private void handleIntent(Intent intent) {
- if (intent == null || intent.getAction() == null || !mInitialize) {
+ public void handleAction(String action) {
+ if (action == null || !mInitialize) {
return;
}
- Log.d(LOGTAG, "HandleIntent, action = " + intent.getAction() + ", mediaState = " + mMediaState);
- switch (intent.getAction()) {
+ Log.d(LOGTAG, "HandleIntent, 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();
@@ -235,19 +243,17 @@ public class MediaControlService extends
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);
+ handleAction(ACTION_STOP);
}
}
}
};
PrefsHelper.addObserver(mPrefs, mPrefsObserver);
}
private boolean initMediaSession() {
@@ -390,17 +396,17 @@ public class MediaControlService extends
.setVisibility(visibility)
.build();
if (isPlaying) {
startForeground(R.id.mediaControlNotification, notification);
} else {
stopForeground(false);
NotificationManagerCompat.from(this)
- .notify(R.id.mediaControlNotification, notification);
+ .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;
@@ -480,17 +486,40 @@ public class MediaControlService extends
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);
+ handleAction(ACTION_PAUSE);
}
}
}
+ /**
+ * If our listener is not null we can safely assume that our service is bound and we must inform the client to unbind and
+ * stop the service, otherwise we can call stopSelf directly.
+ */
+ private void stopService() {
+ if (listener != null) {
+ listener.onFinish();
+ } else {
+ stopSelf();
+ }
+ }
+
+ public class Binder extends android.os.Binder {
+ MediaControlService getService() {
+ return MediaControlService.this;
+ }
+
+ void setListener(TaskFinishedListener listener) {
+ MediaControlService.this.listener = listener;
+ }
+ }
+
+ public interface TaskFinishedListener {
+ void onFinish();
+ }
}