mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-06 07:48:38 +09:00
pt 1 in reviving the android build (copied from pm 28a1, wish me luck)
This commit is contained in:
parent
efa9662725
commit
d7788a6d6d
4249 changed files with 468189 additions and 0 deletions
|
|
@ -0,0 +1,324 @@
|
|||
/* -*- 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.notifications;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.app.NotificationManagerCompat;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
import org.mozilla.gecko.AppConstants;
|
||||
import org.mozilla.gecko.GeckoApp;
|
||||
import org.mozilla.gecko.GeckoAppShell;
|
||||
import org.mozilla.gecko.GeckoService;
|
||||
import org.mozilla.gecko.NotificationListener;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.gfx.BitmapUtils;
|
||||
|
||||
/**
|
||||
* Client for posting notifications.
|
||||
*/
|
||||
public final class NotificationClient implements NotificationListener {
|
||||
private static final String LOGTAG = "GeckoNotificationClient";
|
||||
/* package */ static final String CLICK_ACTION = AppConstants.ANDROID_PACKAGE_NAME + ".NOTIFICATION_CLICK";
|
||||
/* package */ static final String CLOSE_ACTION = AppConstants.ANDROID_PACKAGE_NAME + ".NOTIFICATION_CLOSE";
|
||||
/* package */ static final String PERSISTENT_INTENT_EXTRA = "persistentIntent";
|
||||
|
||||
private final Context mContext;
|
||||
private final NotificationManagerCompat mNotificationManager;
|
||||
|
||||
private final HashMap<String, Notification> mNotifications = new HashMap<>();
|
||||
|
||||
/**
|
||||
* 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 String mForegroundNotification;
|
||||
|
||||
public NotificationClient(Context context) {
|
||||
mContext = context.getApplicationContext();
|
||||
mNotificationManager = NotificationManagerCompat.from(mContext);
|
||||
}
|
||||
|
||||
@Override // NotificationListener
|
||||
public void showNotification(String name, String cookie, String title,
|
||||
String text, String host, String imageUrl) {
|
||||
showNotification(name, cookie, title, text, host, imageUrl, /* data */ null);
|
||||
}
|
||||
|
||||
@Override // NotificationListener
|
||||
public void showPersistentNotification(String name, String cookie, String title,
|
||||
String text, String host, String imageUrl,
|
||||
String data) {
|
||||
showNotification(name, cookie, title, text, host, imageUrl, data != null ? data : "");
|
||||
}
|
||||
|
||||
private void showNotification(String name, String cookie, String title,
|
||||
String text, String host, String imageUrl,
|
||||
String persistentData) {
|
||||
// Put the strings into the intent as an URI
|
||||
// "alert:?name=<name>&cookie=<cookie>"
|
||||
String packageName = AppConstants.ANDROID_PACKAGE_NAME;
|
||||
String className = AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS;
|
||||
if (GeckoAppShell.getGeckoInterface() != null) {
|
||||
final ComponentName comp = GeckoAppShell.getGeckoInterface()
|
||||
.getActivity().getComponentName();
|
||||
packageName = comp.getPackageName();
|
||||
className = comp.getClassName();
|
||||
}
|
||||
final Uri dataUri = (new Uri.Builder())
|
||||
.scheme("moz-notification")
|
||||
.authority(packageName)
|
||||
.path(className)
|
||||
.appendQueryParameter("name", name)
|
||||
.appendQueryParameter("cookie", cookie)
|
||||
.build();
|
||||
|
||||
final Intent clickIntent = new Intent(CLICK_ACTION);
|
||||
clickIntent.setClass(mContext, NotificationReceiver.class);
|
||||
clickIntent.setData(dataUri);
|
||||
|
||||
if (persistentData != null) {
|
||||
final Intent persistentIntent = GeckoService.getIntentToCreateServices(
|
||||
mContext, "persistent-notification-click", persistentData);
|
||||
clickIntent.putExtra(PERSISTENT_INTENT_EXTRA, persistentIntent);
|
||||
}
|
||||
|
||||
final PendingIntent clickPendingIntent = PendingIntent.getBroadcast(
|
||||
mContext, 0, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
final Intent closeIntent = new Intent(CLOSE_ACTION);
|
||||
closeIntent.setClass(mContext, NotificationReceiver.class);
|
||||
closeIntent.setData(dataUri);
|
||||
|
||||
if (persistentData != null) {
|
||||
final Intent persistentIntent = GeckoService.getIntentToCreateServices(
|
||||
mContext, "persistent-notification-close", persistentData);
|
||||
closeIntent.putExtra(PERSISTENT_INTENT_EXTRA, persistentIntent);
|
||||
}
|
||||
|
||||
final PendingIntent closePendingIntent = PendingIntent.getBroadcast(
|
||||
mContext, 0, closeIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
add(name, imageUrl, host, title, text, clickPendingIntent, closePendingIntent);
|
||||
GeckoAppShell.onNotificationShow(name, cookie);
|
||||
}
|
||||
|
||||
@Override // NotificationListener
|
||||
public void closeNotification(String name)
|
||||
{
|
||||
remove(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a notification; used for web notifications.
|
||||
*
|
||||
* @param name the unique name of the notification
|
||||
* @param imageUrl URL of the image to use
|
||||
* @param alertTitle title of the notification
|
||||
* @param alertText text of the notification
|
||||
* @param contentIntent Intent used when the notification is clicked
|
||||
* @param deleteIntent Intent used when the notification is closed
|
||||
*/
|
||||
private void add(final String name, final String imageUrl, final String host,
|
||||
final String alertTitle, final String alertText,
|
||||
final PendingIntent contentIntent, final PendingIntent deleteIntent) {
|
||||
final NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext)
|
||||
.setContentTitle(alertTitle)
|
||||
.setContentText(alertText)
|
||||
.setSmallIcon(R.drawable.ic_status_logo)
|
||||
.setContentIntent(contentIntent)
|
||||
.setDeleteIntent(deleteIntent)
|
||||
.setAutoCancel(true)
|
||||
.setStyle(new NotificationCompat.BigTextStyle()
|
||||
.bigText(alertText)
|
||||
.setSummaryText(host));
|
||||
|
||||
// Fetch icon.
|
||||
if (!imageUrl.isEmpty()) {
|
||||
final Bitmap image = BitmapUtils.decodeUrl(imageUrl);
|
||||
builder.setLargeIcon(image);
|
||||
}
|
||||
|
||||
builder.setWhen(System.currentTimeMillis());
|
||||
final Notification notification = builder.build();
|
||||
|
||||
synchronized (this) {
|
||||
mNotifications.put(name, notification);
|
||||
}
|
||||
|
||||
mNotificationManager.notify(name, 0, notification);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a notification; used for Fennec app notifications.
|
||||
*
|
||||
* @param name the unique name of the notification
|
||||
* @param notification the Notification to add
|
||||
*/
|
||||
public synchronized void add(final String name, final Notification notification) {
|
||||
final boolean ongoing = isOngoing(notification);
|
||||
|
||||
if (ongoing != isOngoing(mNotifications.get(name))) {
|
||||
// In order to change notification from ongoing to non-ongoing, or vice versa,
|
||||
// we have to remove the previous notification, because ongoing notifications
|
||||
// use a different id value than non-ongoing notifications.
|
||||
onNotificationClose(name);
|
||||
}
|
||||
|
||||
mNotifications.put(name, notification);
|
||||
|
||||
if (!ongoing) {
|
||||
mNotificationManager.notify(name, 0, notification);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ongoing
|
||||
if (mForegroundNotification == null) {
|
||||
setForegroundNotificationLocked(name, notification);
|
||||
} else if (mForegroundNotification.equals(name)) {
|
||||
// Shortcut to update the current foreground notification, instead of
|
||||
// going through the service.
|
||||
mNotificationManager.notify(R.id.foregroundNotification, notification);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a notification.
|
||||
*
|
||||
* @param name Name of existing notification
|
||||
* @param progress progress of item being updated
|
||||
* @param progressMax max progress of item being updated
|
||||
* @param alertText text of the notification
|
||||
*/
|
||||
public void update(final String name, final long progress,
|
||||
final long progressMax, final String alertText) {
|
||||
Notification notification;
|
||||
synchronized (this) {
|
||||
notification = mNotifications.get(name);
|
||||
}
|
||||
if (notification == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
notification = new NotificationCompat.Builder(mContext)
|
||||
.setContentText(alertText)
|
||||
.setSmallIcon(notification.icon)
|
||||
.setWhen(notification.when)
|
||||
.setContentIntent(notification.contentIntent)
|
||||
.setProgress((int) progressMax, (int) progress, false)
|
||||
.build();
|
||||
|
||||
add(name, notification);
|
||||
}
|
||||
|
||||
/* package */ synchronized Notification onNotificationClose(final String name) {
|
||||
mNotificationManager.cancel(name, 0);
|
||||
|
||||
final Notification notification = mNotifications.remove(name);
|
||||
if (notification != null) {
|
||||
updateForegroundNotificationLocked(name);
|
||||
}
|
||||
return notification;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a notification.
|
||||
*
|
||||
* @param name Name of existing notification
|
||||
*/
|
||||
public synchronized void remove(final String name) {
|
||||
final Notification notification = onNotificationClose(name);
|
||||
if (notification == null || notification.deleteIntent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Canceling the notification doesn't trigger the delete intent, so we
|
||||
// have to trigger it manually.
|
||||
try {
|
||||
notification.deleteIntent.send();
|
||||
} catch (final PendingIntent.CanceledException e) {
|
||||
// Ignore.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 synchronized boolean isDone() {
|
||||
return mNotifications.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a notification should hold a foreground service to keep Gecko alive
|
||||
*
|
||||
* @param name the name of the notification to check
|
||||
* @return whether the notification is ongoing
|
||||
*/
|
||||
public synchronized boolean isOngoing(final String name) {
|
||||
return isOngoing(mNotifications.get(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a notification should hold a foreground service to keep Gecko alive
|
||||
*
|
||||
* @param notification the notification to check
|
||||
* @return whether the notification is ongoing
|
||||
*/
|
||||
public boolean isOngoing(final Notification notification) {
|
||||
if (notification != null && (notification.flags & Notification.FLAG_ONGOING_EVENT) != 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void setForegroundNotificationLocked(final String name,
|
||||
final Notification notification) {
|
||||
mForegroundNotification = name;
|
||||
|
||||
final Intent intent = new Intent(mContext, NotificationService.class);
|
||||
intent.putExtra(NotificationService.EXTRA_NOTIFICATION, notification);
|
||||
mContext.startService(intent);
|
||||
}
|
||||
|
||||
private void updateForegroundNotificationLocked(final String oldName) {
|
||||
if (mForegroundNotification == null || !mForegroundNotification.equals(oldName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're removing the notification associated with the
|
||||
// foreground, we need to pick another active notification to act
|
||||
// as the foreground notification.
|
||||
for (final String name : mNotifications.keySet()) {
|
||||
final Notification notification = mNotifications.get(name);
|
||||
if (isOngoing(notification)) {
|
||||
setForegroundNotificationLocked(name, notification);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setForegroundNotificationLocked(null, null);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,366 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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.notifications;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.gecko.AppConstants;
|
||||
import org.mozilla.gecko.EventDispatcher;
|
||||
import org.mozilla.gecko.GeckoAppShell;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.gfx.BitmapUtils;
|
||||
import org.mozilla.gecko.mozglue.SafeIntent;
|
||||
import org.mozilla.gecko.util.GeckoEventListener;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.util.Log;
|
||||
|
||||
public final class NotificationHelper implements GeckoEventListener {
|
||||
public static final String HELPER_BROADCAST_ACTION = AppConstants.ANDROID_PACKAGE_NAME + ".helperBroadcastAction";
|
||||
|
||||
public static final String NOTIFICATION_ID = "NotificationHelper_ID";
|
||||
private static final String LOGTAG = "GeckoNotificationHelper";
|
||||
private static final String HELPER_NOTIFICATION = "helperNotif";
|
||||
|
||||
// Attributes mandatory to be used while sending a notification from js.
|
||||
private static final String TITLE_ATTR = "title";
|
||||
private static final String TEXT_ATTR = "text";
|
||||
/* package */ static final String ID_ATTR = "id";
|
||||
private static final String SMALLICON_ATTR = "smallIcon";
|
||||
|
||||
// Attributes that can be used while sending a notification from js.
|
||||
private static final String PROGRESS_VALUE_ATTR = "progress_value";
|
||||
private static final String PROGRESS_MAX_ATTR = "progress_max";
|
||||
private static final String PROGRESS_INDETERMINATE_ATTR = "progress_indeterminate";
|
||||
private static final String LIGHT_ATTR = "light";
|
||||
private static final String ONGOING_ATTR = "ongoing";
|
||||
private static final String WHEN_ATTR = "when";
|
||||
private static final String PRIORITY_ATTR = "priority";
|
||||
private static final String LARGE_ICON_ATTR = "largeIcon";
|
||||
private static final String ACTIONS_ATTR = "actions";
|
||||
private static final String ACTION_ID_ATTR = "buttonId";
|
||||
private static final String ACTION_TITLE_ATTR = "title";
|
||||
private static final String ACTION_ICON_ATTR = "icon";
|
||||
private static final String PERSISTENT_ATTR = "persistent";
|
||||
private static final String HANDLER_ATTR = "handlerKey";
|
||||
private static final String COOKIE_ATTR = "cookie";
|
||||
static final String EVENT_TYPE_ATTR = "eventType";
|
||||
|
||||
private static final String NOTIFICATION_SCHEME = "moz-notification";
|
||||
|
||||
private static final String BUTTON_EVENT = "notification-button-clicked";
|
||||
private static final String CLICK_EVENT = "notification-clicked";
|
||||
static final String CLEARED_EVENT = "notification-cleared";
|
||||
|
||||
static final String ORIGINAL_EXTRA_COMPONENT = "originalComponent";
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
// Holds a list of notifications that should be cleared if the Fennec Activity is shut down.
|
||||
// Will not include ongoing or persistent notifications that are tied to Gecko's lifecycle.
|
||||
private HashMap<String, String> mClearableNotifications;
|
||||
|
||||
private boolean mInitialized;
|
||||
private static NotificationHelper sInstance;
|
||||
|
||||
private NotificationHelper(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
public void init() {
|
||||
if (mInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
mClearableNotifications = new HashMap<String, String>();
|
||||
EventDispatcher.getInstance().registerGeckoThreadListener(this,
|
||||
"Notification:Show",
|
||||
"Notification:Hide");
|
||||
mInitialized = true;
|
||||
}
|
||||
|
||||
public static NotificationHelper getInstance(Context context) {
|
||||
// If someone else created this singleton, but didn't initialize it, something has gone wrong.
|
||||
if (sInstance != null && !sInstance.mInitialized) {
|
||||
throw new IllegalStateException("NotificationHelper was created by someone else but not initialized");
|
||||
}
|
||||
|
||||
if (sInstance == null) {
|
||||
sInstance = new NotificationHelper(context.getApplicationContext());
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(String event, JSONObject message) {
|
||||
if (event.equals("Notification:Show")) {
|
||||
showNotification(message);
|
||||
} else if (event.equals("Notification:Hide")) {
|
||||
hideNotification(message);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isHelperIntent(Intent i) {
|
||||
return i.getBooleanExtra(HELPER_NOTIFICATION, false);
|
||||
}
|
||||
|
||||
public static void getArgsAndSendNotificationIntent(SafeIntent intent) {
|
||||
final JSONObject args = new JSONObject();
|
||||
final Uri data = intent.getData();
|
||||
|
||||
final String notificationType = data.getQueryParameter(EVENT_TYPE_ATTR);
|
||||
|
||||
try {
|
||||
args.put(ID_ATTR, data.getQueryParameter(ID_ATTR));
|
||||
args.put(EVENT_TYPE_ATTR, notificationType);
|
||||
args.put(HANDLER_ATTR, data.getQueryParameter(HANDLER_ATTR));
|
||||
args.put(COOKIE_ATTR, intent.getStringExtra(COOKIE_ATTR));
|
||||
|
||||
if (BUTTON_EVENT.equals(notificationType)) {
|
||||
final String actionName = data.getQueryParameter(ACTION_ID_ATTR);
|
||||
args.put(ACTION_ID_ATTR, actionName);
|
||||
}
|
||||
|
||||
Log.i(LOGTAG, "Send " + args.toString());
|
||||
GeckoAppShell.notifyObservers("Notification:Event", args.toString());
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "Error building JSON notification arguments.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void handleNotificationIntent(SafeIntent i) {
|
||||
final Uri data = i.getData();
|
||||
final String notificationType = data.getQueryParameter(EVENT_TYPE_ATTR);
|
||||
final String id = data.getQueryParameter(ID_ATTR);
|
||||
if (id == null || notificationType == null) {
|
||||
Log.e(LOGTAG, "handleNotificationEvent: invalid intent parameters");
|
||||
return;
|
||||
}
|
||||
|
||||
getArgsAndSendNotificationIntent(i);
|
||||
|
||||
// If the notification was clicked, we are closing it. This must be executed after
|
||||
// sending the event to js side because when the notification is canceled no event can be
|
||||
// handled.
|
||||
if (CLICK_EVENT.equals(notificationType) && !i.getBooleanExtra(ONGOING_ATTR, false)) {
|
||||
// The handler and cookie parameters are optional.
|
||||
final String handler = data.getQueryParameter(HANDLER_ATTR);
|
||||
final String cookie = i.getStringExtra(COOKIE_ATTR);
|
||||
hideNotification(id, handler, cookie);
|
||||
}
|
||||
}
|
||||
|
||||
private Uri.Builder getNotificationBuilder(JSONObject message, String type) {
|
||||
Uri.Builder b = new Uri.Builder();
|
||||
b.scheme(NOTIFICATION_SCHEME).appendQueryParameter(EVENT_TYPE_ATTR, type);
|
||||
|
||||
try {
|
||||
final String id = message.getString(ID_ATTR);
|
||||
b.appendQueryParameter(ID_ATTR, id);
|
||||
} catch (JSONException ex) {
|
||||
Log.i(LOGTAG, "buildNotificationPendingIntent, error parsing", ex);
|
||||
}
|
||||
|
||||
try {
|
||||
final String id = message.getString(HANDLER_ATTR);
|
||||
b.appendQueryParameter(HANDLER_ATTR, id);
|
||||
} catch (JSONException ex) {
|
||||
Log.i(LOGTAG, "Notification doesn't have a handler");
|
||||
}
|
||||
|
||||
return b;
|
||||
}
|
||||
|
||||
private Intent buildNotificationIntent(JSONObject message, Uri.Builder builder) {
|
||||
Intent notificationIntent = new Intent(HELPER_BROADCAST_ACTION);
|
||||
final boolean ongoing = message.optBoolean(ONGOING_ATTR);
|
||||
notificationIntent.putExtra(ONGOING_ATTR, ongoing);
|
||||
|
||||
final Uri dataUri = builder.build();
|
||||
notificationIntent.setData(dataUri);
|
||||
notificationIntent.putExtra(HELPER_NOTIFICATION, true);
|
||||
notificationIntent.putExtra(COOKIE_ATTR, message.optString(COOKIE_ATTR));
|
||||
|
||||
// All intents get routed through the notificationReceiver. That lets us bail if we don't want to start Gecko
|
||||
final ComponentName name = new ComponentName(mContext, GeckoAppShell.getGeckoInterface().getActivity().getClass());
|
||||
notificationIntent.putExtra(ORIGINAL_EXTRA_COMPONENT, name);
|
||||
|
||||
return notificationIntent;
|
||||
}
|
||||
|
||||
private PendingIntent buildNotificationPendingIntent(JSONObject message, String type) {
|
||||
Uri.Builder builder = getNotificationBuilder(message, type);
|
||||
final Intent notificationIntent = buildNotificationIntent(message, builder);
|
||||
return PendingIntent.getBroadcast(mContext, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
private PendingIntent buildButtonClickPendingIntent(JSONObject message, JSONObject action) {
|
||||
Uri.Builder builder = getNotificationBuilder(message, BUTTON_EVENT);
|
||||
try {
|
||||
// Action name must be in query uri, otherwise buttons pending intents
|
||||
// would be collapsed.
|
||||
if (action.has(ACTION_ID_ATTR)) {
|
||||
builder.appendQueryParameter(ACTION_ID_ATTR, action.getString(ACTION_ID_ATTR));
|
||||
} else {
|
||||
Log.i(LOGTAG, "button event with no name");
|
||||
}
|
||||
} catch (JSONException ex) {
|
||||
Log.i(LOGTAG, "buildNotificationPendingIntent, error parsing", ex);
|
||||
}
|
||||
final Intent notificationIntent = buildNotificationIntent(message, builder);
|
||||
PendingIntent res = PendingIntent.getBroadcast(mContext, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
return res;
|
||||
}
|
||||
|
||||
private void showNotification(JSONObject message) {
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext);
|
||||
|
||||
// These attributes are required
|
||||
final String id;
|
||||
try {
|
||||
builder.setContentTitle(message.getString(TITLE_ATTR));
|
||||
builder.setContentText(message.getString(TEXT_ATTR));
|
||||
id = message.getString(ID_ATTR);
|
||||
} catch (JSONException ex) {
|
||||
Log.i(LOGTAG, "Error parsing", ex);
|
||||
return;
|
||||
}
|
||||
|
||||
Uri imageUri = Uri.parse(message.optString(SMALLICON_ATTR));
|
||||
builder.setSmallIcon(BitmapUtils.getResource(mContext, imageUri));
|
||||
|
||||
JSONArray light = message.optJSONArray(LIGHT_ATTR);
|
||||
if (light != null && light.length() == 3) {
|
||||
try {
|
||||
builder.setLights(light.getInt(0),
|
||||
light.getInt(1),
|
||||
light.getInt(2));
|
||||
} catch (JSONException ex) {
|
||||
Log.i(LOGTAG, "Error parsing", ex);
|
||||
}
|
||||
}
|
||||
|
||||
boolean ongoing = message.optBoolean(ONGOING_ATTR);
|
||||
builder.setOngoing(ongoing);
|
||||
|
||||
if (message.has(WHEN_ATTR)) {
|
||||
long when = message.optLong(WHEN_ATTR);
|
||||
builder.setWhen(when);
|
||||
}
|
||||
|
||||
if (message.has(PRIORITY_ATTR)) {
|
||||
int priority = message.optInt(PRIORITY_ATTR);
|
||||
builder.setPriority(priority);
|
||||
}
|
||||
|
||||
if (message.has(LARGE_ICON_ATTR)) {
|
||||
Bitmap b = BitmapUtils.getBitmapFromDataURI(message.optString(LARGE_ICON_ATTR));
|
||||
builder.setLargeIcon(b);
|
||||
}
|
||||
|
||||
if (message.has(PROGRESS_VALUE_ATTR) &&
|
||||
message.has(PROGRESS_MAX_ATTR) &&
|
||||
message.has(PROGRESS_INDETERMINATE_ATTR)) {
|
||||
try {
|
||||
final int progress = message.getInt(PROGRESS_VALUE_ATTR);
|
||||
final int progressMax = message.getInt(PROGRESS_MAX_ATTR);
|
||||
final boolean progressIndeterminate = message.getBoolean(PROGRESS_INDETERMINATE_ATTR);
|
||||
builder.setProgress(progressMax, progress, progressIndeterminate);
|
||||
} catch (JSONException ex) {
|
||||
Log.i(LOGTAG, "Error parsing", ex);
|
||||
}
|
||||
}
|
||||
|
||||
JSONArray actions = message.optJSONArray(ACTIONS_ATTR);
|
||||
if (actions != null) {
|
||||
try {
|
||||
for (int i = 0; i < actions.length(); i++) {
|
||||
JSONObject action = actions.getJSONObject(i);
|
||||
final PendingIntent pending = buildButtonClickPendingIntent(message, action);
|
||||
final String actionTitle = action.getString(ACTION_TITLE_ATTR);
|
||||
final Uri actionImage = Uri.parse(action.optString(ACTION_ICON_ATTR));
|
||||
builder.addAction(BitmapUtils.getResource(mContext, actionImage),
|
||||
actionTitle,
|
||||
pending);
|
||||
}
|
||||
} catch (JSONException ex) {
|
||||
Log.i(LOGTAG, "Error parsing", ex);
|
||||
}
|
||||
}
|
||||
|
||||
PendingIntent pi = buildNotificationPendingIntent(message, CLICK_EVENT);
|
||||
builder.setContentIntent(pi);
|
||||
PendingIntent deletePendingIntent = buildNotificationPendingIntent(message, CLEARED_EVENT);
|
||||
builder.setDeleteIntent(deletePendingIntent);
|
||||
|
||||
((NotificationClient) GeckoAppShell.getNotificationListener()).add(id, builder.build());
|
||||
|
||||
boolean persistent = message.optBoolean(PERSISTENT_ATTR);
|
||||
// We add only not persistent notifications to the list since we want to purge only
|
||||
// them when geckoapp is destroyed.
|
||||
if (!persistent && !mClearableNotifications.containsKey(id)) {
|
||||
mClearableNotifications.put(id, message.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private void hideNotification(JSONObject message) {
|
||||
final String id;
|
||||
final String handler;
|
||||
final String cookie;
|
||||
try {
|
||||
id = message.getString("id");
|
||||
handler = message.optString("handlerKey");
|
||||
cookie = message.optString("cookie");
|
||||
} catch (JSONException ex) {
|
||||
Log.i(LOGTAG, "Error parsing", ex);
|
||||
return;
|
||||
}
|
||||
|
||||
hideNotification(id, handler, cookie);
|
||||
}
|
||||
|
||||
private void closeNotification(String id, String handlerKey, String cookie) {
|
||||
((NotificationClient) GeckoAppShell.getNotificationListener()).remove(id);
|
||||
}
|
||||
|
||||
public void hideNotification(String id, String handlerKey, String cookie) {
|
||||
mClearableNotifications.remove(id);
|
||||
closeNotification(id, handlerKey, cookie);
|
||||
}
|
||||
|
||||
private void clearAll() {
|
||||
for (Iterator<String> i = mClearableNotifications.keySet().iterator(); i.hasNext();) {
|
||||
final String id = i.next();
|
||||
final String json = mClearableNotifications.get(id);
|
||||
i.remove();
|
||||
|
||||
JSONObject obj;
|
||||
try {
|
||||
obj = new JSONObject(json);
|
||||
} catch (JSONException ex) {
|
||||
obj = new JSONObject();
|
||||
}
|
||||
|
||||
closeNotification(id, obj.optString(HANDLER_ATTR), obj.optString(COOKIE_ATTR));
|
||||
}
|
||||
}
|
||||
|
||||
public static void destroy() {
|
||||
if (sInstance != null) {
|
||||
sInstance.clearAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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.notifications;
|
||||
|
||||
import org.mozilla.gecko.GeckoApp;
|
||||
import org.mozilla.gecko.GeckoAppShell;
|
||||
import org.mozilla.gecko.GeckoThread;
|
||||
import org.mozilla.gecko.mozglue.SafeIntent;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Broadcast receiver for Notifications. Will forward them to GeckoApp (and start Gecko) if they're clicked.
|
||||
* If they're being dismissed, it will not start Gecko, but may forward them to JS if Gecko is running.
|
||||
* This is also the only entry point for notification intents.
|
||||
*/
|
||||
public class NotificationReceiver extends BroadcastReceiver {
|
||||
private static final String LOGTAG = "GeckoNotificationReceiver";
|
||||
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
final Uri data = intent.getData();
|
||||
if (data == null) {
|
||||
Log.e(LOGTAG, "handleNotificationEvent: empty data");
|
||||
return;
|
||||
}
|
||||
|
||||
final String action = intent.getAction();
|
||||
if (NotificationClient.CLICK_ACTION.equals(action) ||
|
||||
NotificationClient.CLOSE_ACTION.equals(action)) {
|
||||
onNotificationClientAction(context, action, data, intent);
|
||||
return;
|
||||
}
|
||||
|
||||
final String notificationType = data.getQueryParameter(NotificationHelper.EVENT_TYPE_ATTR);
|
||||
if (notificationType == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// In case the user swiped out the notification, we empty the id set.
|
||||
if (NotificationHelper.CLEARED_EVENT.equals(notificationType)) {
|
||||
// If Gecko isn't running, we throw away events where the notification was cancelled.
|
||||
// i.e. Don't bug the user if they're just closing a bunch of notifications.
|
||||
if (GeckoThread.isRunning()) {
|
||||
NotificationHelper.getArgsAndSendNotificationIntent(new SafeIntent(intent));
|
||||
}
|
||||
|
||||
final NotificationClient client = (NotificationClient)
|
||||
GeckoAppShell.getNotificationListener();
|
||||
client.onNotificationClose(data.getQueryParameter(NotificationHelper.ID_ATTR));
|
||||
return;
|
||||
}
|
||||
|
||||
forwardMessageToActivity(intent, context);
|
||||
}
|
||||
|
||||
private void forwardMessageToActivity(final Intent intent, final Context context) {
|
||||
final ComponentName name = intent.getExtras().getParcelable(NotificationHelper.ORIGINAL_EXTRA_COMPONENT);
|
||||
intent.setComponent(name);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
private void onNotificationClientAction(final Context context, final String action,
|
||||
final Uri data, final Intent intent) {
|
||||
final String name = data.getQueryParameter("name");
|
||||
final String cookie = data.getQueryParameter("cookie");
|
||||
final Intent persistentIntent = (Intent)
|
||||
intent.getParcelableExtra(NotificationClient.PERSISTENT_INTENT_EXTRA);
|
||||
|
||||
if (persistentIntent != null) {
|
||||
// Go through GeckoService for persistent notifications.
|
||||
context.startService(persistentIntent);
|
||||
}
|
||||
|
||||
if (NotificationClient.CLICK_ACTION.equals(action)) {
|
||||
GeckoAppShell.onNotificationClick(name, cookie);
|
||||
|
||||
if (persistentIntent != null) {
|
||||
// Don't launch GeckoApp if it's a background persistent notification.
|
||||
return;
|
||||
}
|
||||
|
||||
final Intent appIntent = new Intent(GeckoApp.ACTION_ALERT_CALLBACK);
|
||||
appIntent.setComponent(new ComponentName(
|
||||
data.getAuthority(), data.getPath().substring(1))); // exclude leading slash.
|
||||
appIntent.setData(data);
|
||||
appIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(appIntent);
|
||||
|
||||
} else if (NotificationClient.CLOSE_ACTION.equals(action)) {
|
||||
GeckoAppShell.onNotificationClose(name, cookie);
|
||||
|
||||
final NotificationClient client = (NotificationClient)
|
||||
GeckoAppShell.getNotificationListener();
|
||||
client.onNotificationClose(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/* -*- 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.notifications;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
|
||||
import org.mozilla.gecko.R;
|
||||
|
||||
public final class NotificationService extends Service {
|
||||
public static final String EXTRA_NOTIFICATION = "notification";
|
||||
|
||||
@Override // Service
|
||||
public int onStartCommand(final Intent intent, final int flags, final int startId) {
|
||||
final Notification notification = intent.getParcelableExtra(EXTRA_NOTIFICATION);
|
||||
if (notification != null) {
|
||||
// Start foreground notification.
|
||||
startForeground(R.id.foregroundNotification, notification);
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
// Stop foreground notification
|
||||
stopForeground(true);
|
||||
stopSelfResult(startId);
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
@Override // Service
|
||||
public IBinder onBind(final Intent intent) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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.notifications;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.keepsafe.switchboard.SwitchBoard;
|
||||
import org.mozilla.gecko.AppConstants;
|
||||
import org.mozilla.gecko.GeckoSharedPrefs;
|
||||
import org.mozilla.gecko.Locales;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.Telemetry;
|
||||
import org.mozilla.gecko.TelemetryContract;
|
||||
import org.mozilla.gecko.preferences.GeckoPreferences;
|
||||
import org.mozilla.gecko.Experiments;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class WhatsNewReceiver extends BroadcastReceiver {
|
||||
|
||||
public static final String EXTRA_WHATSNEW_NOTIFICATION = "whatsnew_notification";
|
||||
private static final String ACTION_NOTIFICATION_CANCELLED = "notification_cancelled";
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (ACTION_NOTIFICATION_CANCELLED.equals(intent.getAction())) {
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, TelemetryContract.Method.NOTIFICATION, EXTRA_WHATSNEW_NOTIFICATION);
|
||||
return;
|
||||
}
|
||||
|
||||
final String dataString = intent.getDataString();
|
||||
if (TextUtils.isEmpty(dataString) || !dataString.contains(AppConstants.ANDROID_PACKAGE_NAME)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!SwitchBoard.isInExperiment(context, Experiments.WHATSNEW_NOTIFICATION)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isPreferenceEnabled(context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
showWhatsNewNotification(context);
|
||||
}
|
||||
|
||||
private boolean isPreferenceEnabled(Context context) {
|
||||
return GeckoSharedPrefs.forApp(context).getBoolean(GeckoPreferences.PREFS_NOTIFICATIONS_WHATS_NEW, true);
|
||||
}
|
||||
|
||||
private void showWhatsNewNotification(Context context) {
|
||||
final Notification notification = new NotificationCompat.Builder(context)
|
||||
.setContentTitle(context.getString(R.string.whatsnew_notification_title))
|
||||
.setContentText(context.getString(R.string.whatsnew_notification_summary))
|
||||
.setSmallIcon(R.drawable.ic_status_logo)
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(getContentIntent(context))
|
||||
.setDeleteIntent(getDeleteIntent(context))
|
||||
.build();
|
||||
|
||||
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
final int notificationID = EXTRA_WHATSNEW_NOTIFICATION.hashCode();
|
||||
notificationManager.notify(notificationID, notification);
|
||||
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.NOTIFICATION, EXTRA_WHATSNEW_NOTIFICATION);
|
||||
}
|
||||
|
||||
private PendingIntent getContentIntent(Context context) {
|
||||
final String link = context.getString(R.string.whatsnew_notification_url,
|
||||
AppConstants.MOZ_APP_VERSION,
|
||||
AppConstants.OS_TARGET,
|
||||
Locales.getLanguageTag(Locale.getDefault()));
|
||||
|
||||
final Intent i = new Intent(Intent.ACTION_VIEW);
|
||||
i.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
|
||||
i.setData(Uri.parse(link));
|
||||
i.putExtra(EXTRA_WHATSNEW_NOTIFICATION, true);
|
||||
|
||||
return PendingIntent.getActivity(context, 0, i, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
private PendingIntent getDeleteIntent(Context context) {
|
||||
final Intent i = new Intent(context, WhatsNewReceiver.class);
|
||||
i.setAction(ACTION_NOTIFICATION_CANCELLED);
|
||||
|
||||
return PendingIntent.getBroadcast(context, 0, i, PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue