From f880b41bf8969e78b85a88390a44e162d4977278 Mon Sep 17 00:00:00 2001 From: Brian Nicholson Date: Thu, 21 Mar 2013 13:32:11 -0700 Subject: [PATCH] Bug 850693 - Create a NotificationHandler for each Fennec instance. r=kats --HG-- rename : mobile/android/base/NotificationServiceClient.java => mobile/android/base/NotificationClient.java rename : mobile/android/base/NotificationService.java => mobile/android/base/NotificationHandler.java extra : rebase_source : 861bb500d9f1cd7cbc98a0c3628ddf7894e1e976 --- .../android/base/AppNotificationClient.java | 25 +++ mobile/android/base/BrowserApp.java | 7 + mobile/android/base/GeckoApp.java | 8 + mobile/android/base/GeckoAppShell.java | 6 +- mobile/android/base/GeckoApplication.java | 1 - mobile/android/base/Makefile.in | 5 +- ...iceClient.java => NotificationClient.java} | 96 ++++------- mobile/android/base/NotificationHandler.java | 157 ++++++++++++++++++ mobile/android/base/NotificationService.java | 157 ++---------------- .../base/ServiceNotificationClient.java | 71 ++++++++ 10 files changed, 318 insertions(+), 215 deletions(-) create mode 100644 mobile/android/base/AppNotificationClient.java rename mobile/android/base/{NotificationServiceClient.java => NotificationClient.java} (58%) create mode 100644 mobile/android/base/NotificationHandler.java create mode 100644 mobile/android/base/ServiceNotificationClient.java diff --git a/mobile/android/base/AppNotificationClient.java b/mobile/android/base/AppNotificationClient.java new file mode 100644 index 000000000000..471aeaa0cd8a --- /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)); + } +} diff --git a/mobile/android/base/BrowserApp.java b/mobile/android/base/BrowserApp.java index 6bd3081124dc..f662ea548385 100644 --- a/mobile/android/base/BrowserApp.java +++ b/mobile/android/base/BrowserApp.java @@ -1634,6 +1634,13 @@ abstract public class BrowserApp extends GeckoApp }).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 diff --git a/mobile/android/base/GeckoApp.java b/mobile/android/base/GeckoApp.java index e84104b0122b..a17aecbdfb0d 100644 --- a/mobile/android/base/GeckoApp.java +++ b/mobile/android/base/GeckoApp.java @@ -1457,6 +1457,8 @@ abstract public class GeckoApp editor.commit(); } }); + + GeckoAppShell.setNotificationClient(makeNotificationClient()); } protected void initializeChrome(String uri, boolean isExternalURL) { @@ -2636,4 +2638,10 @@ abstract public class GeckoApp } return false; } + + protected NotificationClient makeNotificationClient() { + // Don't use a notification service; we may be killed in the background + // during downloads. + return new AppNotificationClient(getApplicationContext()); + } } diff --git a/mobile/android/base/GeckoAppShell.java b/mobile/android/base/GeckoAppShell.java index 3cfe87183667..6cee880a63b7 100644 --- a/mobile/android/base/GeckoAppShell.java +++ b/mobile/android/base/GeckoAppShell.java @@ -151,7 +151,7 @@ public class GeckoAppShell 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 */ @@ -1170,11 +1170,11 @@ public class GeckoAppShell }}); } - 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"); } } diff --git a/mobile/android/base/GeckoApplication.java b/mobile/android/base/GeckoApplication.java index 01a54e78e8af..c57fc94a70d0 100644 --- a/mobile/android/base/GeckoApplication.java +++ b/mobile/android/base/GeckoApplication.java @@ -31,7 +31,6 @@ public class GeckoApplication extends Application { GeckoBatteryManager.getInstance().start(); GeckoNetworkManager.getInstance().init(getApplicationContext()); MemoryMonitor.getInstance().init(getApplicationContext()); - GeckoAppShell.setNotificationClient(new NotificationServiceClient(getApplicationContext())); mInited = true; } diff --git a/mobile/android/base/Makefile.in b/mobile/android/base/Makefile.in index 13c9a034852d..3ccd0d2840e1 100644 --- a/mobile/android/base/Makefile.in +++ b/mobile/android/base/Makefile.in @@ -52,6 +52,7 @@ FENNEC_JAVA_FILES = \ AndroidImportPreference.java \ AnimatorProxy.java \ AnimatedHeightLayout.java \ + AppNotificationClient.java \ AwesomeBar.java \ AwesomebarResultHandler.java \ AwesomeBarTabs.java \ @@ -119,8 +120,9 @@ FENNEC_JAVA_FILES = \ MenuPanel.java \ MenuPopup.java \ MultiChoicePreference.java \ + NotificationClient.java \ + NotificationHandler.java \ NotificationService.java \ - NotificationServiceClient.java \ NSSBridge.java \ CustomEditText.java \ OnInterceptTouchListener.java \ @@ -137,6 +139,7 @@ FENNEC_JAVA_FILES = \ ReaderModeUtils.java \ RemoteTabs.java \ RobocopAPI.java \ + ServiceNotificationClient.java \ SessionParser.java \ SetupScreen.java \ ShapedButton.java \ diff --git a/mobile/android/base/NotificationServiceClient.java b/mobile/android/base/NotificationClient.java similarity index 58% rename from mobile/android/base/NotificationServiceClient.java rename to mobile/android/base/NotificationClient.java index 691e38dba7c5..a79796321f75 100644 --- a/mobile/android/base/NotificationServiceClient.java +++ b/mobile/android/base/NotificationClient.java @@ -9,32 +9,21 @@ 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 mTaskQueue = new LinkedList(); private final ConcurrentHashMap mUpdatesMap = new ConcurrentHashMap(); - public NotificationServiceClient(Context context) { - mContext = context; - } - /** * Runnable that is reused between update notifications. * @@ -75,26 +64,26 @@ public class NotificationServiceClient { 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(); } } @@ -102,7 +91,7 @@ public class NotificationServiceClient { /** * 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) { @@ -120,7 +109,7 @@ public class NotificationServiceClient { } synchronized (this) { - if (mBound) { + if (mReady) { mTaskQueue.add(runnable); notify(); } @@ -130,17 +119,17 @@ public class NotificationServiceClient { /** * 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); } }); @@ -150,47 +139,28 @@ public class NotificationServiceClient { /** * 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); + protected void bind() { + mReady = true; } - private void unbind() { - if (mBound) { - mBound = false; - mContext.unbindService(mConnection); - mUpdatesMap.clear(); - } + protected void unbind() { + mReady = false; + mUpdatesMap.clear(); } - 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(); - } - - @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; @@ -198,20 +168,14 @@ public class NotificationServiceClient { 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(); } } diff --git a/mobile/android/base/NotificationHandler.java b/mobile/android/base/NotificationHandler.java new file mode 100644 index 000000000000..aa5391179709 --- /dev/null +++ b/mobile/android/base/NotificationHandler.java @@ -0,0 +1,157 @@ +/* -*- 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.content.Context; +import android.net.Uri; + +public class NotificationHandler { + private final ConcurrentHashMap + mAlertNotifications = new ConcurrentHashMap(); + 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 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 + * @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 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(mContext, notificationID, + icon, aAlertTitle, aAlertText, System.currentTimeMillis(), imageUri); + + notification.setLatestEventInfo(mContext, 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(); + } + + protected void setForegroundNotification(AlertNotification notification) { + mForegroundNotification = 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); + } + } +} diff --git a/mobile/android/base/NotificationService.java b/mobile/android/base/NotificationService.java index f6f58977401c..73e7647fa83e 100644 --- a/mobile/android/base/NotificationService.java +++ b/mobile/android/base/NotificationService.java @@ -5,33 +5,25 @@ 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 NotificationHandler mHandler = new NotificationHandler(this) { + @Override + protected void setForegroundNotification(AlertNotification notification) { + super.setForegroundNotification(notification); - private final ConcurrentHashMap - mAlertNotifications = new ConcurrentHashMap(); - - /** - * 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() { @@ -45,130 +37,7 @@ public class NotificationService extends Service { 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 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; } } diff --git a/mobile/android/base/ServiceNotificationClient.java b/mobile/android/base/ServiceNotificationClient.java new file mode 100644 index 000000000000..16b9e30fdf95 --- /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()); + } + } +}