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 132272 440ee0b03c9d848ae1b1d1ecd85be360e738a9c7
parent 132271 6609ab08172ebe9e5c10b71692fb7c0c42808653
child 132273 c70ae394f0be234651b5c6d295dcae1efc9bde91
push idunknown
push userunknown
push dateunknown
reviewerskats
bugs850693
milestone22.0a1
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());
+        }
+    }
+}