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
This commit is contained in:
Brian Nicholson 2013-03-21 13:32:11 -07:00
Родитель 67a657cb5c
Коммит f880b41bf8
10 изменённых файлов: 318 добавлений и 215 удалений

Просмотреть файл

@ -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));
}
}

Просмотреть файл

@ -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

Просмотреть файл

@ -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());
}
}

Просмотреть файл

@ -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");
}
}

Просмотреть файл

@ -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;
}

Просмотреть файл

@ -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 \

Просмотреть файл

@ -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<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.
*
@ -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);
protected void unbind() {
mReady = false;
mUpdatesMap.clear();
}
protected void connectHandler(NotificationHandler handler) {
mHandler = handler;
new Thread(new NotificationRunnable()).start();
}
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());
}
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();
}
}

Просмотреть файл

@ -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<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 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<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(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);
}
}
}

Просмотреть файл

@ -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<Integer, AlertNotification>
mAlertNotifications = new ConcurrentHashMap<Integer, AlertNotification>();
/**
* 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<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;
}
}

Просмотреть файл

@ -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());
}
}
}