Bug 1384866 - refactored MediaControlService, updated NotificationService for Oreo. r?sdaswani draft
authorVlad Baicu <vlad.baicu@softvision.ro>
Thu, 24 May 2018 19:48:04 +0300
changeset 799417 4953217e2dfb952802cb760332178048b987ebf9
parent 797566 000309d44abb81084276c9fa977c57aee7126053
push id111047
push uservbaicu@mozilla.com
push dateThu, 24 May 2018 16:48:59 +0000
reviewerssdaswani
bugs1384866
milestone62.0a1
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
mobile/android/base/java/org/mozilla/gecko/media/AudioFocusAgent.java
mobile/android/base/java/org/mozilla/gecko/media/MediaControlService.java
mobile/android/base/java/org/mozilla/gecko/notifications/NotificationClient.java
mobile/android/base/java/org/mozilla/gecko/notifications/NotificationService.java
--- 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;
     }
 }