Bug 850693 - Create a NotificationHandler for each Fennec instance. r=kats
authorBrian Nicholson <bnicholson@mozilla.com>
Thu, 21 Mar 2013 13:32:11 -0700
changeset 125839 440ee0b03c9d848ae1b1d1ecd85be360e738a9c7
parent 125838 6609ab08172ebe9e5c10b71692fb7c0c42808653
child 125840 c70ae394f0be234651b5c6d295dcae1efc9bde91
push id25118
push userbnicholson@mozilla.com
push dateThu, 21 Mar 2013 20:32:50 +0000
treeherdermozilla-inbound@440ee0b03c9d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskats
bugs850693
milestone22.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 850693 - Create a NotificationHandler for each Fennec instance. r=kats
mobile/android/base/AppNotificationClient.java
mobile/android/base/BrowserApp.java
mobile/android/base/GeckoApp.java
mobile/android/base/GeckoAppShell.java
mobile/android/base/GeckoApplication.java
mobile/android/base/Makefile.in
mobile/android/base/NotificationClient.java
mobile/android/base/NotificationHandler.java
mobile/android/base/NotificationService.java
mobile/android/base/NotificationServiceClient.java
mobile/android/base/ServiceNotificationClient.java
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/AppNotificationClient.java
@@ -0,0 +1,25 @@
+/* -*- 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;
+
+import android.content.Context;
+
+/**
+ * Client for posting notifications in the application.
+ */
+public class AppNotificationClient extends NotificationClient {
+    private final Context mContext;
+
+    public AppNotificationClient(Context context) {
+        mContext = context;
+    }
+
+    @Override
+    protected void bind() {
+        super.bind();
+        connectHandler(new NotificationHandler(mContext));
+    }
+}
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -1629,16 +1629,23 @@ abstract public class BrowserApp extends
             @Override
             public void onPostExecute(Boolean shouldShowFeedbackPage) {
                 if (shouldShowFeedbackPage)
                     Tabs.getInstance().loadUrlInTab("about:feedback");
             }
         }).execute();
     }
 
+    @Override
+    protected NotificationClient makeNotificationClient() {
+        // The service is local to Fennec, so we can use it to keep
+        // Fennec alive during downloads.
+        return new ServiceNotificationClient(getApplicationContext());
+    }
+
     private void resetFeedbackLaunchCount() {
         ThreadUtils.postToBackgroundThread(new Runnable() {
             @Override
             public synchronized void run() {
                 SharedPreferences settings = getPreferences(Activity.MODE_PRIVATE);
                 settings.edit().putInt(getPackageName() + ".feedback_launch_count", 0).commit();
             }
         });
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -1452,16 +1452,18 @@ abstract public class GeckoApp
                 editor.putBoolean(GeckoApp.PREFS_OOM_EXCEPTION, false);
 
                 // put a flag to check if we got a normal onSaveInstaceState
                 // on exit, or if we were suddenly killed (crash or native OOM)
                 editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, false);
                 editor.commit();
             }
         });
+
+        GeckoAppShell.setNotificationClient(makeNotificationClient());
     }
 
     protected void initializeChrome(String uri, boolean isExternalURL) {
         mDoorHangerPopup = new DoorHangerPopup(this, null);
         mPluginContainer = (AbsoluteLayout) findViewById(R.id.plugin_container);
         mFormAssistPopup = (FormAssistPopup) findViewById(R.id.form_assist_popup);
 
         if (cameraView == null) {
@@ -2631,9 +2633,15 @@ abstract public class GeckoApp
                         GeckoAppShell.createShortcut(title, url, url, favicon == null ? null : favicon, "");
                     }
                 }
                 return true;
             }
         }
         return false;
     }
+
+    protected NotificationClient makeNotificationClient() {
+        // Don't use a notification service; we may be killed in the background
+        // during downloads.
+        return new AppNotificationClient(getApplicationContext());
+    }
 }
--- a/mobile/android/base/GeckoAppShell.java
+++ b/mobile/android/base/GeckoAppShell.java
@@ -146,17 +146,17 @@ public class GeckoAppShell
     private static Sensor gGyroscopeSensor = null;
     private static Sensor gOrientationSensor = null;
     private static Sensor gProximitySensor = null;
     private static Sensor gLightSensor = null;
 
     private static boolean mLocationHighAccuracy = false;
 
     static ActivityHandlerHelper sActivityHelper = new ActivityHandlerHelper();
-    static NotificationServiceClient sNotificationClient;
+    static NotificationClient sNotificationClient;
 
     /* The Android-side API: API methods that Android calls */
 
     // Initialization methods
     public static native void nativeInit();
 
     // helper methods
     //    public static native void setSurfaceView(GeckoSurfaceView sv);
@@ -1165,21 +1165,21 @@ public class GeckoAppShell
                 } else {
                     android.text.ClipboardManager cm = (android.text.ClipboardManager)
                         context.getSystemService(Context.CLIPBOARD_SERVICE);
                     cm.setText(text);
                 }
             }});
     }
 
-    public static void setNotificationClient(NotificationServiceClient client) {
+    public static void setNotificationClient(NotificationClient client) {
         if (sNotificationClient == null) {
             sNotificationClient = client;
         } else {
-            Log.w(LOGTAG, "Notification client already set");
+            Log.d(LOGTAG, "Notification client already set");
         }
     }
 
     public static void showAlertNotification(String aImageUrl, String aAlertTitle, String aAlertText,
                                              String aAlertCookie, String aAlertName) {
         Log.d(LOGTAG, "GeckoAppShell.showAlertNotification\n" +
             "- image = '" + aImageUrl + "'\n" +
             "- title = '" + aAlertTitle + "'\n" +
--- a/mobile/android/base/GeckoApplication.java
+++ b/mobile/android/base/GeckoApplication.java
@@ -26,17 +26,16 @@ public class GeckoApplication extends Ap
 
         mLightweightTheme = new LightweightTheme(this);
 
         GeckoConnectivityReceiver.getInstance().init(getApplicationContext());
         GeckoBatteryManager.getInstance().init(getApplicationContext());
         GeckoBatteryManager.getInstance().start();
         GeckoNetworkManager.getInstance().init(getApplicationContext());
         MemoryMonitor.getInstance().init(getApplicationContext());
-        GeckoAppShell.setNotificationClient(new NotificationServiceClient(getApplicationContext()));
 
         mInited = true;
     }
 
     protected void onActivityPause(GeckoActivityStatus activity) {
         mInBackground = true;
 
         if ((activity.isFinishing() == false) &&
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -47,16 +47,17 @@ FENNEC_JAVA_FILES = \
   AboutHomeSection.java \
   ActivityHandlerHelper.java \
   AlertNotification.java \
   AllCapsTextView.java \
   AndroidImport.java \
   AndroidImportPreference.java \
   AnimatorProxy.java \
   AnimatedHeightLayout.java \
+  AppNotificationClient.java \
   AwesomeBar.java \
   AwesomebarResultHandler.java \
   AwesomeBarTabs.java \
   awesomebar/AwesomeBarTab.java \
   awesomebar/AllPagesTab.java \
   awesomebar/BookmarksTab.java \
   awesomebar/HistoryTab.java \
   BackButton.java \
@@ -114,34 +115,36 @@ FENNEC_JAVA_FILES = \
   LinkTextView.java \
   MemoryMonitor.java \
   MenuButton.java \
   MenuItemActionBar.java \
   MenuItemDefault.java \
   MenuPanel.java \
   MenuPopup.java \
   MultiChoicePreference.java \
+  NotificationClient.java \
+  NotificationHandler.java \
   NotificationService.java \
-  NotificationServiceClient.java \
   NSSBridge.java \
   CustomEditText.java \
   OnInterceptTouchListener.java \
   PrefsHelper.java \
   PrivateDataPreference.java \
   PrivateTab.java \
   PropertyAnimator.java \
   ProfileMigrator.java \
   PromptService.java \
   sqlite/ByteBufferInputStream.java \
   sqlite/MatrixBlobCursor.java \
   sqlite/SQLiteBridge.java \
   sqlite/SQLiteBridgeException.java \
   ReaderModeUtils.java \
   RemoteTabs.java \
   RobocopAPI.java \
+  ServiceNotificationClient.java \
   SessionParser.java \
   SetupScreen.java \
   ShapedButton.java \
   SiteIdentityPopup.java \
   SuggestClient.java \
   SurfaceBits.java \
   Tab.java \
   Tabs.java \
rename from mobile/android/base/NotificationServiceClient.java
rename to mobile/android/base/NotificationClient.java
--- a/mobile/android/base/NotificationServiceClient.java
+++ b/mobile/android/base/NotificationClient.java
@@ -4,42 +4,31 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
 import java.util.LinkedList;
 import java.util.concurrent.ConcurrentHashMap;
 
 import android.app.PendingIntent;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.os.IBinder;
 import android.text.TextUtils;
 import android.util.Log;
 
 /**
- * Client for posting notifications through the NotificationService.
+ * Client for posting notifications through a NotificationHandler.
  */
-public class NotificationServiceClient {
-    private static final String LOGTAG = "GeckoNotificationServiceClient";
+public abstract class NotificationClient {
+    private static final String LOGTAG = "GeckoNotificationClient";
 
-    private volatile NotificationService mService;
-    private final ServiceConnection mConnection = new NotificationServiceConnection();
-    private boolean mBound;
-    private final Context mContext;
+    private volatile NotificationHandler mHandler;
+    private boolean mReady;
     private final LinkedList<Runnable> mTaskQueue = new LinkedList<Runnable>();
     private final ConcurrentHashMap<Integer, UpdateRunnable> mUpdatesMap =
             new ConcurrentHashMap<Integer, UpdateRunnable>();
 
-    public NotificationServiceClient(Context context) {
-        mContext = context;
-    }
-
     /**
      * Runnable that is reused between update notifications.
      *
      * Updates happen frequently, so reusing Runnables prevents frequent dynamic allocation.
      */
     private class UpdateRunnable implements Runnable {
         private long mProgress;
         private long mProgressMax;
@@ -70,44 +59,44 @@ public class NotificationServiceClient {
             String alertText;
 
             synchronized (this) {
                 progress = mProgress;
                 progressMax = mProgressMax;
                 alertText = mAlertText;
             }
 
-            mService.update(mNotificationID, progress, progressMax, alertText);
+            mHandler.update(mNotificationID, progress, progressMax, alertText);
         }
     };
 
     /**
      * Adds a notification.
      *
-     * @see NotificationService#add(int, String, String, String, PendingIntent, PendingIntent)
+     * @see NotificationHandler#add(int, String, String, String, PendingIntent, PendingIntent)
      */
     public synchronized void add(final int notificationID, final String aImageUrl,
             final String aAlertTitle, final String aAlertText, final PendingIntent contentIntent) {
         mTaskQueue.add(new Runnable() {
             @Override
             public void run() {
-                mService.add(notificationID, aImageUrl, aAlertTitle, aAlertText, contentIntent);
+                mHandler.add(notificationID, aImageUrl, aAlertTitle, aAlertText, contentIntent);
             }
         });
         notify();
 
-        if (!mBound) {
+        if (!mReady) {
             bind();
         }
     }
 
     /**
      * Updates a notification.
      *
-     * @see NotificationService#update(int, long, long, String)
+     * @see NotificationHandler#update(int, long, long, String)
      */
     public void update(final int notificationID, final long aProgress, final long aProgressMax,
             final String aAlertText) {
         UpdateRunnable runnable = mUpdatesMap.get(notificationID);
 
         if (runnable == null) {
             runnable = new UpdateRunnable(notificationID);
             mUpdatesMap.put(notificationID, runnable);
@@ -115,108 +104,83 @@ public class NotificationServiceClient {
 
         // If we've already posted an update with these values, there's no
         // need to do it again.
         if (!runnable.updateProgress(aProgress, aProgressMax, aAlertText)) {
             return;
         }
 
         synchronized (this) {
-            if (mBound) {
+            if (mReady) {
                 mTaskQueue.add(runnable);
                 notify();
             }
         }
     }
 
     /**
      * Removes a notification.
      *
-     * @see NotificationService#remove(int)
+     * @see NotificationHandler#remove(int)
      */
     public synchronized void remove(final int notificationID) {
-        if (!mBound) {
+        if (!mReady) {
             return;
         }
 
         mTaskQueue.add(new Runnable() {
             @Override
             public void run() {
-                mService.remove(notificationID);
+                mHandler.remove(notificationID);
                 mUpdatesMap.remove(notificationID);
             }
         });
         notify();
     }
 
     /**
      * Determines whether a notification is showing progress.
      *
-     * @see NotificationService#isProgressStyle(int)
+     * @see NotificationHandler#isProgressStyle(int)
      */
     public boolean isProgressStyle(int notificationID) {
-        final NotificationService service = mService;
-        return service != null && service.isProgressStyle(notificationID);
+        final NotificationHandler handler = mHandler;
+        return handler != null && handler.isProgressStyle(notificationID);
     }
 
-    private void bind() {
-        mBound = true;
-        final Intent intent = new Intent(mContext, NotificationService.class);
-        mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
-    }
-
-    private void unbind() {
-        if (mBound) {
-            mBound = false;
-            mContext.unbindService(mConnection);
-            mUpdatesMap.clear();
-        }
+    protected void bind() {
+        mReady = true;
     }
 
-    class NotificationServiceConnection implements ServiceConnection, Runnable {
-        @Override
-        public void onServiceConnected(ComponentName className, IBinder service) {
-            final NotificationService.NotificationBinder binder =
-                    (NotificationService.NotificationBinder) service;
-            mService = binder.getService();
-
-            new Thread(this).start();
-        }
+    protected void unbind() {
+        mReady = false;
+        mUpdatesMap.clear();
+    }
 
-        @Override
-        public void onServiceDisconnected(ComponentName componentName) {
-            // This is called when the connection with the service has been
-            // unexpectedly disconnected -- that is, its process crashed.
-            // Because it is running in our same process, we should never
-            // see this happen, and the correctness of this class relies on
-            // this never happening.
-            Log.e(LOGTAG, "Notification service disconnected", new Exception());
-        }
+    protected void connectHandler(NotificationHandler handler) {
+        mHandler = handler;
+        new Thread(new NotificationRunnable()).start();
+    }
 
+    private class NotificationRunnable implements Runnable {
         @Override
         public void run() {
             Runnable r;
             try {
                 while (true) {
                     // Synchronize polls to prevent tasks from being added to the queue
                     // during the isDone check.
-                    synchronized (NotificationServiceClient.this) {
+                    synchronized (NotificationClient.this) {
                         r = mTaskQueue.poll();
                         while (r == null) {
-                            if (mService.isDone()) {
-                                // If there are no more tasks and no notifications being
-                                // displayed, the service is disconnected. Unfortunately,
-                                // since completed download notifications are shown by
-                                // removing the progress notification and creating a new
-                                // static one, this will cause the service to be unbound
-                                // and immediately rebound when a download completes.
+                            if (mHandler.isDone()) {
                                 unbind();
                                 return;
                             }
-                            NotificationServiceClient.this.wait();
+                            NotificationClient.this.wait();
                             r = mTaskQueue.poll();
                         }
                     }
                     r.run();
                 }
             } catch (InterruptedException e) {
                 Log.e(LOGTAG, "Notification task queue processing interrupted", e);
             }
copy from mobile/android/base/NotificationService.java
copy to mobile/android/base/NotificationHandler.java
--- a/mobile/android/base/NotificationService.java
+++ b/mobile/android/base/NotificationHandler.java
@@ -4,50 +4,38 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
 import java.lang.reflect.Field;
 import java.util.concurrent.ConcurrentHashMap;
 
 import android.app.PendingIntent;
-import android.app.Service;
-import android.content.Intent;
+import android.content.Context;
 import android.net.Uri;
-import android.os.Binder;
-import android.os.IBinder;
 
-public class NotificationService extends Service {
-    private final IBinder mBinder = new NotificationBinder();
-
+public class NotificationHandler {
     private final ConcurrentHashMap<Integer, AlertNotification>
             mAlertNotifications = new ConcurrentHashMap<Integer, AlertNotification>();
+    private final Context mContext;
 
     /**
      * Notification associated with this service's foreground state.
      *
      * {@link android.app.Service#startForeground(int, android.app.Notification)}
      * associates the foreground with exactly one notification from the service.
      * To keep Fennec alive during downloads (and to make sure it can be killed
      * once downloads are complete), we make sure that the foreground is always
      * associated with an active progress notification if and only if at least
      * one download is in progress.
      */
     private AlertNotification mForegroundNotification;
 
-    public class NotificationBinder extends Binder {
-        NotificationService getService() {
-            // Return this instance of NotificationService so clients can call public methods
-            return NotificationService.this;
-        }
-    }
-
-    @Override
-    public IBinder onBind(Intent intent) {
-        return mBinder;
+    public NotificationHandler(Context context) {
+        mContext = context;
     }
 
     /**
      * Adds a notification.
      *
      * @param notificationID the unique ID of the notification
      * @param aImageUrl      URL of the image to use
      * @param aAlertTitle    title of the notification
@@ -70,20 +58,20 @@ public class NotificationService extends
             try {
                 final Class<R.drawable> drawableClass = R.drawable.class;
                 final Field f = drawableClass.getField(resource);
                 icon = f.getInt(null);
             } catch (final Exception e) {} // just means the resource doesn't exist
             imageUri = null;
         }
 
-        final AlertNotification notification = new AlertNotification(this, notificationID,
+        final AlertNotification notification = new AlertNotification(mContext, notificationID,
                 icon, aAlertTitle, aAlertText, System.currentTimeMillis(), imageUri);
 
-        notification.setLatestEventInfo(this, aAlertTitle, aAlertText, contentIntent);
+        notification.setLatestEventInfo(mContext, aAlertTitle, aAlertText, contentIntent);
 
         notification.show();
         mAlertNotifications.put(notification.getId(), notification);
     }
 
     /**
      * Updates a notification.
      *
@@ -141,23 +129,18 @@ public class NotificationService extends
      * @param notificationID the notification to check
      * @return               whether the notification is progress style
      */
     public boolean isProgressStyle(int notificationID) {
         final AlertNotification notification = mAlertNotifications.get(notificationID);
         return notification != null && notification.isProgressStyle();
     }
 
-    private void setForegroundNotification(AlertNotification notification) {
+    protected void setForegroundNotification(AlertNotification notification) {
         mForegroundNotification = notification;
-        if (notification == null) {
-            stopForeground(true);
-        } else {
-            startForeground(notification.getId(), notification);
-        }
     }
 
     private void updateForegroundNotification(AlertNotification oldNotification) {
         if (mForegroundNotification == oldNotification) {
             // If we're removing the notification associated with the
             // foreground, we need to pick another active notification to act
             // as the foreground notification.
             AlertNotification foregroundNotification = null;
--- a/mobile/android/base/NotificationService.java
+++ b/mobile/android/base/NotificationService.java
@@ -1,174 +1,43 @@
 /* -*- 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;
 
-import java.lang.reflect.Field;
-import java.util.concurrent.ConcurrentHashMap;
-
-import android.app.PendingIntent;
 import android.app.Service;
 import android.content.Intent;
-import android.net.Uri;
 import android.os.Binder;
 import android.os.IBinder;
 
 public class NotificationService extends Service {
     private final IBinder mBinder = new NotificationBinder();
-
-    private final ConcurrentHashMap<Integer, AlertNotification>
-            mAlertNotifications = new ConcurrentHashMap<Integer, AlertNotification>();
+    private final NotificationHandler mHandler = new NotificationHandler(this) {
+        @Override
+        protected void setForegroundNotification(AlertNotification notification) {
+            super.setForegroundNotification(notification);
 
-    /**
-     * Notification associated with this service's foreground state.
-     *
-     * {@link android.app.Service#startForeground(int, android.app.Notification)}
-     * associates the foreground with exactly one notification from the service.
-     * To keep Fennec alive during downloads (and to make sure it can be killed
-     * once downloads are complete), we make sure that the foreground is always
-     * associated with an active progress notification if and only if at least
-     * one download is in progress.
-     */
-    private AlertNotification mForegroundNotification;
+            if (notification == null) {
+                stopForeground(true);
+            } else {
+                startForeground(notification.getId(), notification);
+            }
+        }
+    };
 
     public class NotificationBinder extends Binder {
         NotificationService getService() {
             // Return this instance of NotificationService so clients can call public methods
             return NotificationService.this;
         }
     }
 
     @Override
     public IBinder onBind(Intent intent) {
         return mBinder;
     }
 
-    /**
-     * Adds a notification.
-     *
-     * @param notificationID the unique ID of the notification
-     * @param aImageUrl      URL of the image to use
-     * @param aAlertTitle    title of the notification
-     * @param aAlertText     text of the notification
-     * @param contentIntent  Intent used when the notification is clicked
-     * @param clearIntent    Intent used when the notification is removed
-     */
-    public void add(int notificationID, String aImageUrl, String aAlertTitle,
-                    String aAlertText, PendingIntent contentIntent) {
-        // Remove the old notification with the same ID, if any
-        remove(notificationID);
-
-        int icon = R.drawable.ic_status_logo;
-
-        Uri imageUri = Uri.parse(aImageUrl);
-        final String scheme = imageUri.getScheme();
-        if ("drawable".equals(scheme)) {
-            String resource = imageUri.getSchemeSpecificPart();
-            resource = resource.substring(resource.lastIndexOf('/') + 1);
-            try {
-                final Class<R.drawable> drawableClass = R.drawable.class;
-                final Field f = drawableClass.getField(resource);
-                icon = f.getInt(null);
-            } catch (final Exception e) {} // just means the resource doesn't exist
-            imageUri = null;
-        }
-
-        final AlertNotification notification = new AlertNotification(this, notificationID,
-                icon, aAlertTitle, aAlertText, System.currentTimeMillis(), imageUri);
-
-        notification.setLatestEventInfo(this, aAlertTitle, aAlertText, contentIntent);
-
-        notification.show();
-        mAlertNotifications.put(notification.getId(), notification);
-    }
-
-    /**
-     * Updates a notification.
-     *
-     * @param notificationID ID of existing notification
-     * @param aProgress      progress of item being updated
-     * @param aProgressMax   max progress of item being updated
-     * @param aAlertText     text of the notification
-     */
-    public void update(int notificationID, long aProgress, long aProgressMax, String aAlertText) {
-        final AlertNotification notification = mAlertNotifications.get(notificationID);
-        if (notification == null) {
-            return;
-        }
-
-        notification.updateProgress(aAlertText, aProgress, aProgressMax);
-
-        if (mForegroundNotification == null && notification.isProgressStyle()) {
-            setForegroundNotification(notification);
-        }
-
-        // Hide the notification at 100%
-        if (aProgress == aProgressMax) {
-            remove(notificationID);
-        }
-    }
-
-    /**
-     * Removes a notification.
-     *
-     * @param notificationID ID of existing notification
-     */
-    public void remove(int notificationID) {
-        final AlertNotification notification = mAlertNotifications.remove(notificationID);
-        if (notification != null) {
-            updateForegroundNotification(notification);
-            notification.cancel();
-        }
-    }
-
-    /**
-     * Determines whether the service is done.
-     *
-     * The service is considered finished when all notifications have been
-     * removed.
-     *
-     * @return whether all notifications have been removed
-     */
-    public boolean isDone() {
-        return mAlertNotifications.isEmpty();
-    }
-
-    /**
-     * Determines whether a notification is showing progress.
-     *
-     * @param notificationID the notification to check
-     * @return               whether the notification is progress style
-     */
-    public boolean isProgressStyle(int notificationID) {
-        final AlertNotification notification = mAlertNotifications.get(notificationID);
-        return notification != null && notification.isProgressStyle();
-    }
-
-    private void setForegroundNotification(AlertNotification notification) {
-        mForegroundNotification = notification;
-        if (notification == null) {
-            stopForeground(true);
-        } else {
-            startForeground(notification.getId(), notification);
-        }
-    }
-
-    private void updateForegroundNotification(AlertNotification oldNotification) {
-        if (mForegroundNotification == oldNotification) {
-            // If we're removing the notification associated with the
-            // foreground, we need to pick another active notification to act
-            // as the foreground notification.
-            AlertNotification foregroundNotification = null;
-            for (final AlertNotification notification : mAlertNotifications.values()) {
-                if (notification.isProgressStyle()) {
-                    foregroundNotification = notification;
-                    break;
-                }
-            }
-
-            setForegroundNotification(foregroundNotification);
-        }
+    public NotificationHandler getNotificationHandler() {
+        return mHandler;
     }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/ServiceNotificationClient.java
@@ -0,0 +1,71 @@
+/* -*- 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;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.util.Log;
+
+/**
+ * Client for posting notifications through the NotificationService.
+ */
+public class ServiceNotificationClient extends NotificationClient {
+    private static final String LOGTAG = "GeckoServiceNotificationClient";
+
+    private final ServiceConnection mConnection = new NotificationServiceConnection();
+    private boolean mBound;
+    private final Context mContext;
+
+    public ServiceNotificationClient(Context context) {
+        mContext = context;
+    }
+
+    @Override
+    protected void bind() {
+        super.bind();
+        final Intent intent = new Intent(mContext, NotificationService.class);
+        mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+    }
+
+    @Override
+    protected void unbind() {
+        // If there are no more tasks and no notifications being
+        // displayed, the service is disconnected. Unfortunately,
+        // since completed download notifications are shown by
+        // removing the progress notification and creating a new
+        // static one, this will cause the service to be unbound
+        // and immediately rebound when a download completes.
+        super.unbind();
+
+        if (mBound) {
+            mBound = false;
+            mContext.unbindService(mConnection);
+        }
+    }
+
+    class NotificationServiceConnection implements ServiceConnection {
+        @Override
+        public void onServiceConnected(ComponentName className, IBinder service) {
+            final NotificationService.NotificationBinder binder =
+                    (NotificationService.NotificationBinder) service;
+            connectHandler(binder.getService().getNotificationHandler());
+            mBound = true;
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName componentName) {
+            // This is called when the connection with the service has been
+            // unexpectedly disconnected -- that is, its process crashed.
+            // Because it is running in our same process, we should never
+            // see this happen, and the correctness of this class relies on
+            // this never happening.
+            Log.e(LOGTAG, "Notification service disconnected", new Exception());
+        }
+    }
+}