Bug 1384866 - refactored MediaControlService, updated NotificationService for Oreo. r?sdaswani
Refactored the MediaControlService to a bound service. Using stopService instead of calling startService with a null notification in order to stop NotificationService.
MozReview-Commit-ID: DDhvqdhAwtr
--- 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,44 @@ 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 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 mServiceBound = false;
+ 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 +210,46 @@ 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;
}
- Intent intent = new Intent(mContext, MediaControlService.class);
- intent.setAction(action);
- mContext.startService(intent);
+ if (mServiceBound) {
+ mediaControlService.handleAction(action);
+ } else {
+ Intent intent = new Intent(mContext, MediaControlService.class);
+ intent.setAction(action);
+ mContext.startService(intent);
+ mContext.bindService(intent, this, Context.BIND_AUTO_CREATE);
+ }
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ mServiceBound = true;
+ MediaControlService.Binder mBinder = (MediaControlService.Binder) service;
+ mediaControlService = mBinder.getService();
+ mBinder.setListener(new MediaControlService.TaskFinishedListener() {
+ @Override
+ public void onFinish() {
+ if (mServiceBound) {
+ mContext.unbindService(AudioFocusAgent.this);
+ mServiceBound = false;
+ }
+ mContext.stopService(new Intent(mContext, MediaControlService.class));
+ }
+ });
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ mServiceBound = false;
}
@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,17 +67,20 @@ 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;
- protected enum State {
+ private IBinder mBinder = new Binder();
+ private TaskFinishedListener listener;
+
+ public enum State {
PLAYING,
PAUSED,
STOPPED
}
@Override
public void onCreate() {
initialize();
@@ -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() {
@@ -336,17 +342,16 @@ public class MediaControlService extends
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;
}
@@ -385,23 +390,17 @@ public class MediaControlService extends
.setStyle(style)
.addAction(createNotificationAction())
.setOngoing(isPlaying)
.setShowWhen(false)
.setWhen(0)
.setVisibility(visibility)
.build();
- if (isPlaying) {
- startForeground(R.id.mediaControlNotification, notification);
- } else {
- stopForeground(false);
- NotificationManagerCompat.from(this)
- .notify(R.id.mediaControlNotification, notification);
- }
+ 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);
@@ -480,17 +479,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();
+ }
}
--- a/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationClient.java
+++ b/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationClient.java
@@ -1,32 +1,30 @@
/* -*- 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.notifications;
+import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Notification;
import android.app.PendingIntent;
-import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
-import android.util.Log;
import java.util.HashMap;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.GeckoActivityMonitor;
-import org.mozilla.gecko.GeckoApp;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoService;
import org.mozilla.gecko.NotificationListener;
import org.mozilla.gecko.R;
import org.mozilla.gecko.util.BitmapUtils;
/**
* Client for posting notifications.
@@ -294,23 +292,32 @@ public final class NotificationClient im
*/
public boolean isOngoing(final Notification notification) {
if (notification != null && (notification.flags & Notification.FLAG_ONGOING_EVENT) != 0) {
return true;
}
return false;
}
+ @SuppressLint("NewApi")
private void setForegroundNotificationLocked(final String name,
final Notification notification) {
mForegroundNotification = name;
final Intent intent = new Intent(mContext, NotificationService.class);
intent.putExtra(NotificationService.EXTRA_NOTIFICATION, notification);
- mContext.startService(intent);
+ if (AppConstants.Versions.preO) {
+ mContext.startService(intent);
+ } else {
+ mContext.startForegroundService(intent);
+ }
+ }
+
+ private void setForegroundNotificationClosed() {
+ mContext.stopService(new Intent(mContext, NotificationService.class));
}
private void updateForegroundNotificationLocked(final String oldName) {
if (mForegroundNotification == null || !mForegroundNotification.equals(oldName)) {
return;
}
// If we're removing the notification associated with the
@@ -323,11 +330,11 @@ public final class NotificationClient im
// uses a special ID, so we need to close its old instantiation and then
// re-add it with the new ID through the NotificationService.
onNotificationClose(name);
setForegroundNotificationLocked(name, notification);
return;
}
}
- setForegroundNotificationLocked(null, null);
+ setForegroundNotificationClosed();
}
}
--- a/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationService.java
@@ -13,25 +13,18 @@ import android.os.IBinder;
import org.mozilla.gecko.R;
public final class NotificationService extends Service {
public static final String EXTRA_NOTIFICATION = "notification";
@Override // Service
public int onStartCommand(final Intent intent, final int flags, final int startId) {
final Notification notification = intent.getParcelableExtra(EXTRA_NOTIFICATION);
- if (notification != null) {
- // Start foreground notification.
- startForeground(R.id.foregroundNotification, notification);
- return START_NOT_STICKY;
- }
-
- // Stop foreground notification
- stopForeground(true);
- stopSelfResult(startId);
+ // Start foreground notification.
+ startForeground(R.id.foregroundNotification, notification);
return START_NOT_STICKY;
}
@Override // Service
public IBinder onBind(final Intent intent) {
return null;
}
}