Bug 823285 - Use foreground notification service for downloads. r=kats

--HG--
extra : rebase_source : 004e31f0379819c5f72420223b24ed0ceccb2e2e
This commit is contained in:
Brian Nicholson 2013-03-11 11:59:23 -07:00
parent c4d3dc87a3
commit 7243a415b3
8 changed files with 491 additions and 107 deletions

View File

@ -28,26 +28,50 @@ public class AlertNotification
private final String mText;
private final NotificationManager mNotificationManager;
private boolean mProgressStyle; // = false
private boolean mProgressStyle;
private double mPrevPercent = -1;
private String mPrevAlertText = "";
private static final double UPDATE_THRESHOLD = .01;
private Context mContext;
public AlertNotification(Context aContext, int aNotificationId, int aIcon,
String aTitle, String aText, long aWhen) {
String aTitle, String aText, long aWhen, Uri aIconUri) {
super(aIcon, (aText.length() > 0) ? aText : aTitle, aWhen);
mIcon = aIcon;
mTitle = aTitle;
mText = aText;
mId = aNotificationId;
mContext = aContext;
mNotificationManager = (NotificationManager)
aContext.getSystemService(Context.NOTIFICATION_SERVICE);
mNotificationManager = (NotificationManager) aContext.getSystemService(Context.NOTIFICATION_SERVICE);
if (aIconUri == null || aIconUri.getScheme() == null)
return;
// Custom view
int layout = R.layout.notification_icon_text;
RemoteViews view = new RemoteViews(mContext.getPackageName(), layout);
try {
URL url = new URL(aIconUri.toString());
Bitmap bm = BitmapFactory.decodeStream(url.openStream());
view.setImageViewBitmap(R.id.notification_image, bm);
view.setTextViewText(R.id.notification_title, mTitle);
if (mText.length() > 0) {
view.setTextViewText(R.id.notification_text, mText);
}
contentView = view;
} catch (Exception e) {
Log.e(LOGTAG, "failed to create bitmap", e);
}
}
public boolean isProgressStyle() {
public int getId() {
return mId;
}
public synchronized boolean isProgressStyle() {
return mProgressStyle;
}
@ -59,34 +83,12 @@ public class AlertNotification
mNotificationManager.cancel(mId);
}
public void setCustomIcon(Uri aIconUri) {
if (aIconUri == null || aIconUri.getScheme() == null)
return;
// Custom view
int layout = R.layout.notification_icon_text;
RemoteViews view = new RemoteViews(GeckoApp.mAppContext.getPackageName(), layout);
try {
URL url = new URL(aIconUri.toString());
Bitmap bm = BitmapFactory.decodeStream(url.openStream());
view.setImageViewBitmap(R.id.notification_image, bm);
view.setTextViewText(R.id.notification_title, mTitle);
if (mText.length() > 0) {
view.setTextViewText(R.id.notification_text, mText);
}
contentView = view;
mNotificationManager.notify(mId, this);
} catch(Exception ex) {
Log.e(LOGTAG, "failed to create bitmap", ex);
}
}
public void updateProgress(String aAlertText, long aProgress, long aProgressMax) {
public synchronized void updateProgress(String aAlertText, long aProgress, long aProgressMax) {
if (!mProgressStyle) {
// Custom view
int layout = aAlertText.length() > 0 ? R.layout.notification_progress_text : R.layout.notification_progress;
RemoteViews view = new RemoteViews(GeckoApp.mAppContext.getPackageName(), layout);
RemoteViews view = new RemoteViews(mContext.getPackageName(), layout);
view.setImageViewResource(R.id.notification_image, mIcon);
view.setTextViewText(R.id.notification_title, mTitle);
contentView = view;

View File

@ -273,6 +273,11 @@
android:process="@MANGLED_ANDROID_PACKAGE_NAME@.UpdateService">
</service>
<service
android:exported="false"
android:name="org.mozilla.gecko.NotificationService">
</service>
#include ../services/manifests/AnnouncementsAndroidManifest_services.xml.in
#include ../services/manifests/SyncAndroidManifest_services.xml.in

View File

@ -1094,7 +1094,7 @@ abstract public class GeckoApp
final String fileName = aSrc.substring(aSrc.lastIndexOf("/") + 1);
final PendingIntent emptyIntent = PendingIntent.getActivity(mAppContext, 0, new Intent(), 0);
final AlertNotification notification = new AlertNotification(mAppContext, fileName.hashCode(),
R.drawable.alert_download, fileName, progText, System.currentTimeMillis() );
R.drawable.alert_download, fileName, progText, System.currentTimeMillis(), null);
notification.setLatestEventInfo(mAppContext, fileName, progText, emptyIntent );
notification.flags |= Notification.FLAG_ONGOING_EVENT;
notification.show();
@ -1677,8 +1677,6 @@ abstract public class GeckoApp
}
});
final GeckoApp self = this;
// End of the startup of our Java App
mJavaUiStartupTimer.stop();
@ -1731,8 +1729,9 @@ abstract public class GeckoApp
} else {
onSearchRequested();
}
} else if (ACTION_ALERT_CALLBACK.equals(action)) {
processAlertCallback(intent);
}
}
public GeckoProfile getProfile() {
@ -1820,6 +1819,21 @@ abstract public class GeckoApp
}
}
private void processAlertCallback(Intent intent) {
String alertName = "";
String alertCookie = "";
Uri data = intent.getData();
if (data != null) {
alertName = data.getQueryParameter("name");
if (alertName == null)
alertName = "";
alertCookie = data.getQueryParameter("cookie");
if (alertCookie == null)
alertCookie = "";
}
handleNotification(ACTION_ALERT_CALLBACK, alertName, alertCookie);
}
@Override
protected void onNewIntent(Intent intent) {
if (GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoExiting)) {
@ -1860,18 +1874,7 @@ abstract public class GeckoApp
String uri = getURIFromIntent(intent);
GeckoAppShell.sendEventToGecko(GeckoEvent.createURILoadEvent(uri));
} else if (ACTION_ALERT_CALLBACK.equals(action)) {
String alertName = "";
String alertCookie = "";
Uri data = intent.getData();
if (data != null) {
alertName = data.getQueryParameter("name");
if (alertName == null)
alertName = "";
alertCookie = data.getQueryParameter("cookie");
if (alertCookie == null)
alertCookie = "";
}
handleNotification(ACTION_ALERT_CALLBACK, alertName, alertCookie);
processAlertCallback(intent);
} else if (ACTION_WIDGET.equals(action)) {
addTab();
}
@ -2012,11 +2015,6 @@ abstract public class GeckoApp
@Override
public void onDestroy()
{
// Tell Gecko to shutting down; we'll end up calling System.exit()
// in onXreExit.
if (isFinishing())
GeckoAppShell.sendEventToGecko(GeckoEvent.createShutdownEvent());
unregisterEventListener("log");
unregisterEventListener("Reader:Added");
unregisterEventListener("Reader:Removed");
@ -2173,7 +2171,12 @@ abstract public class GeckoApp
}
public void handleNotification(String action, String alertName, String alertCookie) {
GeckoAppShell.handleNotification(action, alertName, alertCookie);
// If Gecko isn't running yet, we ignore the notification. Note that
// even if Gecko is running but it was restarted since the notification
// was created, the notification won't be handled (bug 849653).
if (GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) {
GeckoAppShell.handleNotification(action, alertName, alertCookie);
}
}
private void checkMigrateProfile() {

View File

@ -80,10 +80,8 @@ import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
@ -111,9 +109,6 @@ public class GeckoAppShell
static private GeckoEditableListener mEditableListener = null;
static private final HashMap<Integer, AlertNotification>
mAlertNotifications = new HashMap<Integer, AlertNotification>();
/* Keep in sync with constants found here:
http://mxr.mozilla.org/mozilla-central/source/uriloader/base/nsIWebProgressListener.idl
*/
@ -157,6 +152,7 @@ public class GeckoAppShell
private static Handler sGeckoHandler;
static ActivityHandlerHelper sActivityHelper = new ActivityHandlerHelper();
static NotificationServiceClient sNotificationClient;
/* The Android-side API: API methods that Android calls */
@ -1185,6 +1181,14 @@ public class GeckoAppShell
}});
}
public static void setNotificationClient(NotificationServiceClient client) {
if (sNotificationClient == null) {
sNotificationClient = client;
} else {
Log.w(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" +
@ -1194,72 +1198,41 @@ public class GeckoAppShell
"- cookie = '" + aAlertCookie +"'\n" +
"- name = '" + aAlertName + "'");
int icon = R.drawable.ic_status_logo;
Uri imageUri = Uri.parse(aImageUrl);
String scheme = imageUri.getScheme();
if ("drawable".equals(scheme)) {
String resource = imageUri.getSchemeSpecificPart();
resource = resource.substring(resource.lastIndexOf('/') + 1);
try {
Class<R.drawable> drawableClass = R.drawable.class;
Field f = drawableClass.getField(resource);
icon = f.getInt(null);
} catch (Exception e) {} // just means the resource doesn't exist
imageUri = null;
}
int notificationID = aAlertName.hashCode();
// Remove the old notification with the same ID, if any
removeNotification(notificationID);
AlertNotification notification =
new AlertNotification(GeckoApp.mAppContext,notificationID, icon,
aAlertTitle, aAlertText,
System.currentTimeMillis());
// The intent to launch when the user clicks the expanded notification
Intent notificationIntent = new Intent(GeckoApp.ACTION_ALERT_CLICK);
notificationIntent.setClassName(GeckoApp.mAppContext,
GeckoApp.mAppContext.getPackageName() + ".NotificationHandler");
int notificationID = aAlertName.hashCode();
// Put the strings into the intent as an URI "alert:?name=<alertName>&app=<appName>&cookie=<cookie>"
Uri.Builder b = new Uri.Builder();
String app = GeckoApp.mAppContext.getClass().getName();
Uri dataUri = b.scheme("alert")
.path(Integer.toString(notificationID))
.appendQueryParameter("name", aAlertName)
.appendQueryParameter("app", app)
.appendQueryParameter("cookie", aAlertCookie)
.build();
Uri dataUri = b.scheme("alert").path(Integer.toString(notificationID))
.appendQueryParameter("name", aAlertName)
.appendQueryParameter("app", app)
.appendQueryParameter("cookie", aAlertCookie)
.build();
notificationIntent.setData(dataUri);
PendingIntent contentIntent = PendingIntent.getBroadcast(GeckoApp.mAppContext, 0, notificationIntent, 0);
notification.setLatestEventInfo(GeckoApp.mAppContext, aAlertTitle, aAlertText, contentIntent);
notification.setCustomIcon(imageUri);
// The intent to execute when the status entry is deleted by the user with the "Clear All Notifications" button
Intent clearNotificationIntent = new Intent(GeckoApp.ACTION_ALERT_CLEAR);
clearNotificationIntent.setClassName(GeckoApp.mAppContext,
GeckoApp.mAppContext.getPackageName() + ".NotificationHandler");
clearNotificationIntent.setData(dataUri);
notification.deleteIntent = PendingIntent.getBroadcast(GeckoApp.mAppContext, 0, clearNotificationIntent, 0);
PendingIntent clearIntent = PendingIntent.getBroadcast(GeckoApp.mAppContext, 0, clearNotificationIntent, 0);
mAlertNotifications.put(notificationID, notification);
notification.show();
sNotificationClient.add(notificationID, aImageUrl, aAlertTitle, aAlertText, contentIntent, clearIntent);
}
public static void alertsProgressListener_OnProgress(String aAlertName, long aProgress, long aProgressMax, String aAlertText) {
final int notificationID = aAlertName.hashCode();
AlertNotification notification = mAlertNotifications.get(notificationID);
if (notification != null)
notification.updateProgress(aAlertText, aProgress, aProgressMax);
int notificationID = aAlertName.hashCode();
sNotificationClient.update(notificationID, aProgress, aProgressMax, aAlertText);
if (aProgress == aProgressMax) {
// Hide the notification at 100%
removeObserver(aAlertName);
removeNotification(notificationID);
}
}
@ -1276,9 +1249,8 @@ public class GeckoAppShell
if (GeckoApp.ACTION_ALERT_CALLBACK.equals(aAction)) {
callObserver(aAlertName, "alertclickcallback", aAlertCookie);
AlertNotification notification = mAlertNotifications.get(notificationID);
if (notification != null && notification.isProgressStyle()) {
// When clicked, keep the notification, if it displays a progress
if (sNotificationClient.isProgressStyle(notificationID)) {
// When clicked, keep the notification if it displays progress
return;
}
}
@ -1295,11 +1267,7 @@ public class GeckoAppShell
}
private static void removeNotification(int notificationID) {
mAlertNotifications.remove(notificationID);
NotificationManager notificationManager = (NotificationManager)
GeckoApp.mAppContext.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(notificationID);
sNotificationClient.remove(notificationID);
}
public static int getDpi() {

View File

@ -31,6 +31,8 @@ public class GeckoApplication extends Application {
GeckoBatteryManager.getInstance().start();
GeckoNetworkManager.getInstance().init(getApplicationContext());
MemoryMonitor.getInstance().init(getApplicationContext());
GeckoAppShell.setNotificationClient(new NotificationServiceClient(getApplicationContext()));
mInited = true;
}

View File

@ -117,6 +117,8 @@ FENNEC_JAVA_FILES = \
MenuPanel.java \
MenuPopup.java \
MultiChoicePreference.java \
NotificationService.java \
NotificationServiceClient.java \
NSSBridge.java \
CustomEditText.java \
OnInterceptTouchListener.java \

View File

@ -0,0 +1,175 @@
/* -*- 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>();
/**
* 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;
}
/**
* 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, PendingIntent clearIntent) {
// 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.deleteIntent = clearIntent;
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);
}
}
}

View File

@ -0,0 +1,227 @@
/* -*- 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.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.
*/
public class NotificationServiceClient {
private static final String LOGTAG = "GeckoNotificationServiceClient";
private volatile NotificationService mService;
private final ServiceConnection mConnection = new NotificationServiceConnection();
private boolean mBound;
private final Context mContext;
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;
private String mAlertText;
final private int mNotificationID;
public UpdateRunnable(int notificationID) {
mNotificationID = notificationID;
}
public synchronized boolean updateProgress(long progress, long progressMax, String alertText) {
if (progress == mProgress
&& mProgressMax == progressMax
&& TextUtils.equals(mAlertText, alertText)) {
return false;
}
mProgress = progress;
mProgressMax = progressMax;
mAlertText = alertText;
return true;
}
@Override
public void run() {
long progress;
long progressMax;
String alertText;
synchronized (this) {
progress = mProgress;
progressMax = mProgressMax;
alertText = mAlertText;
}
mService.update(mNotificationID, progress, progressMax, alertText);
}
};
/**
* Adds a notification.
*
* @see NotificationService#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,
final PendingIntent clearIntent) {
mTaskQueue.add(new Runnable() {
@Override
public void run() {
mService.add(notificationID, aImageUrl, aAlertTitle, aAlertText,
contentIntent, clearIntent);
}
});
notify();
if (!mBound) {
bind();
}
}
/**
* Updates a notification.
*
* @see NotificationService#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);
}
// 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) {
mTaskQueue.add(runnable);
notify();
}
}
}
/**
* Removes a notification.
*
* @see NotificationService#remove(int)
*/
public synchronized void remove(final int notificationID) {
if (!mBound) {
return;
}
mTaskQueue.add(new Runnable() {
@Override
public void run() {
mService.remove(notificationID);
mUpdatesMap.remove(notificationID);
}
});
notify();
}
/**
* Determines whether a notification is showing progress.
*
* @see NotificationService#isProgressStyle(int)
*/
public boolean isProgressStyle(int notificationID) {
final NotificationService service = mService;
return service != null && service.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();
}
}
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());
}
@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) {
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.
unbind();
return;
}
NotificationServiceClient.this.wait();
r = mTaskQueue.poll();
}
}
r.run();
}
} catch (InterruptedException e) {
Log.e(LOGTAG, "Notification task queue processing interrupted", e);
}
}
}
}