diff --git a/mobile/android/b2gdroid/app/Makefile.in b/mobile/android/b2gdroid/app/Makefile.in
index a67d13692cf..c6075e1f909 100644
--- a/mobile/android/b2gdroid/app/Makefile.in
+++ b/mobile/android/b2gdroid/app/Makefile.in
@@ -9,6 +9,9 @@ JAVAFILES := \
src/main/java/org/mozilla/b2gdroid/Launcher.java \
src/main/java/org/mozilla/b2gdroid/ScreenStateObserver.java \
src/main/java/org/mozilla/b2gdroid/SettingsMapper.java \
+ src/main/java/org/mozilla/b2gdroid/GeckoEventReceiver.java \
+ src/main/java/org/mozilla/b2gdroid/NotificationObserver.java \
+ src/main/java/org/mozilla/b2gdroid/RemoteGeckoEventProxy.java \
$(NULL)
# The GeckoView consuming APK depends on the GeckoView JAR files. There are two
diff --git a/mobile/android/b2gdroid/app/src/main/AndroidManifest.xml b/mobile/android/b2gdroid/app/src/main/AndroidManifest.xml
index 5ace06c132d..6f3b10bb2b9 100644
--- a/mobile/android/b2gdroid/app/src/main/AndroidManifest.xml
+++ b/mobile/android/b2gdroid/app/src/main/AndroidManifest.xml
@@ -121,6 +121,15 @@
+
+
+
+
+
+
mCurrentNotifications = new HashMap();
+
+ @Override
+ public void onCreate() {
+ Log.i(LOGTAG, "onCreate()");
+ super.onCreate();
+
+ RemoteGeckoEventProxy.registerRemoteGeckoThreadListener(this, this, "Android:NotificationOpened", "Android:NotificationClosed");
+ }
+
+
+ @Override
+ public void onNotificationPosted(StatusBarNotification aNotification, RankingMap aRanking) {
+ Log.i(LOGTAG, "onNotificationPosted(aNotification, aRanking)");
+ this.onNotificationPosted(aNotification);
+ }
+
+ @Override
+ public void onNotificationPosted(StatusBarNotification aNotification) {
+ Log.i(LOGTAG, "onNotificationPosted(aNotification, aRanking)");
+
+ Notification notification = aNotification.getNotification();
+
+ CharSequence title = notification.extras.getCharSequence(Notification.EXTRA_TITLE);
+ CharSequence extraText = notification.extras.getCharSequence(Notification.EXTRA_TEXT);
+
+ String notificationKey = getNotificationKey(aNotification);
+
+ JSONObject notificationData = new JSONObject();
+ JSONObject behaviors = new JSONObject();
+
+ try {
+ notificationData.put("_action", "post");
+ notificationData.put("id", notificationKey);
+ notificationData.put("title", title.toString());
+ notificationData.put("text", extraText.toString());
+ notificationData.put("manifestURL", "android://" + aNotification.getPackageName().toLowerCase() + "/manifest.webapp");
+ notificationData.put("icon", getBase64Icon(aNotification));
+ notificationData.put("timestamp", aNotification.getPostTime());
+
+ if ((Notification.FLAG_ONGOING_EVENT & notification.flags) != 0) {
+ notificationData.put("noNotify", true);
+ }
+
+ behaviors.put("vibrationPattern", Arrays.asList(notification.vibrate));
+
+ if ((Notification.FLAG_NO_CLEAR & notification.flags) != 0) {
+ behaviors.put("noclear", true);
+ }
+
+ notificationData.put("mozbehavior", behaviors);
+
+ } catch(Exception ex) {
+ Log.wtf(LOGTAG, "Error building android notification message " + ex.getMessage());
+ return;
+ }
+
+ mCurrentNotifications.put(notificationKey, aNotification);
+ sendBroadcast(GeckoEventReceiver.createBroadcastEventIntent("Android:Notification", notificationData.toString()));
+ }
+
+ @Override
+ public void onNotificationRemoved(StatusBarNotification aNotification, RankingMap aRanking) {
+ Log.i(LOGTAG, "onNotifciationRemoved(aNotification, aRanking)");
+ onNotificationRemoved(aNotification);
+ }
+
+ @Override
+ public void onNotificationRemoved(StatusBarNotification aNotification) {
+ Log.i(LOGTAG, "onNotificationRemoved(aNotification)");
+
+ String notificationKey = getNotificationKey(aNotification);
+ removeNotificationFromGaia(notificationKey);
+ }
+
+ private void removeNotificationFromGaia(String aNotificationKey) {
+ Log.i(LOGTAG, "removeNotificationFromGaia(aNotificationKey=" + aNotificationKey + ")");
+ JSONObject notificationData = new JSONObject();
+
+ mCurrentNotifications.remove(aNotificationKey);
+
+ try {
+ notificationData.put("_action", "remove");
+ notificationData.put("id", aNotificationKey);
+ } catch(Exception ex) {
+ Log.wtf(LOGTAG, "Error building android notification message " + ex.getMessage());
+ return;
+ }
+
+ sendBroadcast(GeckoEventReceiver.createBroadcastEventIntent("Android:Notification", notificationData.toString()));
+ }
+
+ private String getNotificationKey(StatusBarNotification aNotification) {
+ // API Level 20 has a 'getKey' method, however for backwards we
+ // implement this getNotificationKey
+ String notificationTag = aNotification.getTag();
+ if (notificationTag == null) {
+ notificationTag = aNotification.getPackageName();
+ }
+
+ return notificationTag + "#" + Integer.toString(aNotification.getId());
+ }
+
+ private String getBase64Icon(StatusBarNotification aStatusBarNotification) {
+ Notification notification = aStatusBarNotification.getNotification();
+
+ PackageManager manager = getPackageManager();
+ Resources notifyingPackageResources;
+ Bitmap bitmap = null;
+
+ try {
+ notifyingPackageResources = manager.getResourcesForApplication(aStatusBarNotification.getPackageName());
+ int resourceId = notification.icon;
+ bitmap = BitmapFactory.decodeResource(notifyingPackageResources, resourceId);
+ } catch(NameNotFoundException ex) {
+ Log.i(LOGTAG, "Could not find package name: " + ex.getMessage());
+ }
+
+ if (bitmap == null) {
+ // Try and get the icon directly from the notification
+ if (notification.largeIcon != null) {
+ bitmap = notification.largeIcon;
+ } else {
+ Log.i(LOGTAG, "No image found for notification");
+ return "";
+ }
+ }
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);
+ return "data:image/png;base64," + Base64.encodeToString(baos.toByteArray(), Base64.NO_WRAP);
+ }
+
+ /* GeckoEventListener */
+ public void handleMessage(String aEvent, JSONObject message) {
+ Log.i(LOGTAG, "handleMessage(aEvent=" + aEvent + ")");
+
+ String notificationId;
+ try {
+ notificationId = message.getString("id");
+ } catch(Exception ex) {
+ Log.i(LOGTAG, "Malformed Android:NotificationOpened JSON - missing ID");
+ return;
+ }
+
+ StatusBarNotification sbNotification = mCurrentNotifications.get(notificationId);
+
+ if (sbNotification == null) {
+ Log.w(LOGTAG, "No notification found for ID: " + notificationId);
+ return;
+ }
+
+ if ("Android:NotificationOpened".equals(aEvent)) {
+ PendingIntent contentIntent = sbNotification.getNotification().contentIntent;
+
+ try {
+ contentIntent.send();
+ } catch(Exception ex) {
+ Log.i(LOGTAG, "Intent was cancelled");
+ }
+
+ removeNotificationFromGaia(notificationId);
+ return;
+ } else if("Android:NotificationClose".equals(aEvent)) {
+ NotificationManager notificationManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
+
+ notificationManager.cancel(sbNotification.getTag(), sbNotification.getId());
+ mCurrentNotifications.remove(notificationId);
+ }
+ }
+
+ public static void registerForNativeNotifications(Context context) {
+ Intent intent = createNotificationIntent(context, ACTION_REGISTER_FOR_NOTIFICATIONS);
+ context.startService(intent);
+ }
+
+ private static Intent createNotificationIntent(Context context, String action) {
+ return new Intent(action, /* URI */ null, context, NotificationObserver.class);
+ }
+}
diff --git a/mobile/android/b2gdroid/app/src/main/java/org/mozilla/b2gdroid/RemoteGeckoEventProxy.java b/mobile/android/b2gdroid/app/src/main/java/org/mozilla/b2gdroid/RemoteGeckoEventProxy.java
new file mode 100644
index 00000000000..66675738d90
--- /dev/null
+++ b/mobile/android/b2gdroid/app/src/main/java/org/mozilla/b2gdroid/RemoteGeckoEventProxy.java
@@ -0,0 +1,96 @@
+/* vim: set ts=4 sw=4 tw=78 et :
+ * 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.b2gdroid;
+
+import android.util.Log;
+
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.Context;
+import android.content.BroadcastReceiver;
+
+import org.json.JSONObject;
+
+import org.mozilla.gecko.util.GeckoEventListener;
+import org.mozilla.gecko.AppConstants;
+
+/**
+ * Proxy GeckoEvents from the Java UI Thread to other processes owned by this
+ * app - deferring processing to those processes.
+ *
+ * Usage:
+ *
+ * // In remote process `context` given GeckoEventListener `listener`
+ * RemoteGeckoEventProxy
+ * .registerRemoteGeckoThreadListener(context, listener, "Message:Message");
+ *
+ * // In UI Thread (Main Activity `context`)
+ * RemoteGeckoEventProxy mGeckoEventProxy = new RemoteGeckoEventProxy(context);
+ * EventDispatcher
+ * .getInstance()
+ * .registerGeckoThreadListener(mGeckoEventProxy, "Message:Message");
+ */
+class RemoteGeckoEventProxy implements GeckoEventListener {
+
+ private static final String LOGTAG = "RemoteGeckoEventProxy";
+
+ private static final String RECEIVE_GECKO_EVENT = AppConstants.ANDROID_PACKAGE_NAME + ".RECEIVE_GECKO_EVENT";
+
+ private static final String EXTRA_DATA = "DATA";
+ private static final String EXTRA_SUBJECT = "SUBJECT";
+
+ private Context mSenderContext;
+
+ public static void registerRemoteGeckoThreadListener(Context aContext, GeckoEventListener mListener, String... aMessages) {
+ for (String message : aMessages) {
+ IntentFilter intentFilter = new IntentFilter(RECEIVE_GECKO_EVENT + "." + message);
+ aContext.registerReceiver(new GeckoEventProxyReceiver(mListener), intentFilter);
+ }
+ }
+
+ public RemoteGeckoEventProxy(Context aSenderContext) {
+ mSenderContext = aSenderContext;
+ }
+
+ public void handleMessage(String aEvent, JSONObject aMessage) {
+ Intent broadcastIntent = createBroadcastIntent(aEvent, aMessage.toString());
+ mSenderContext.sendBroadcast(broadcastIntent);
+ }
+
+ private static Intent createBroadcastIntent(String aEvent, String aMessage) {
+ Intent intent = new Intent(RECEIVE_GECKO_EVENT + "." + aEvent);
+ intent.putExtra(EXTRA_SUBJECT, aEvent);
+ intent.putExtra(EXTRA_DATA, aMessage);
+ return intent;
+ }
+
+ static class GeckoEventProxyReceiver extends BroadcastReceiver {
+
+ private GeckoEventListener mGeckoEventListener;
+
+ public GeckoEventProxyReceiver(GeckoEventListener aEventListener) {
+ mGeckoEventListener = aEventListener;
+ }
+
+ @Override
+ public void onReceive(Context aContext, Intent aIntent) {
+ String geckoEventSubject = aIntent.getStringExtra(EXTRA_SUBJECT);
+ String geckoEventJSON = aIntent.getStringExtra(EXTRA_DATA);
+
+ // Unwrap the message data and invoke the GeckoEventListener
+ JSONObject obj;
+ try {
+ obj = new JSONObject(geckoEventJSON);
+ obj = obj.getJSONObject("value");
+ } catch(Exception ex) {
+ Log.wtf(RemoteGeckoEventProxy.LOGTAG, "Could not decode JSON payload for message: " + geckoEventSubject);
+ return;
+ }
+
+ mGeckoEventListener.handleMessage(geckoEventSubject, obj);
+ }
+ }
+}
diff --git a/mobile/android/b2gdroid/components/MessagesBridge.jsm b/mobile/android/b2gdroid/components/MessagesBridge.jsm
index d0e4ff8c6d0..3572cdf5c33 100644
--- a/mobile/android/b2gdroid/components/MessagesBridge.jsm
+++ b/mobile/android/b2gdroid/components/MessagesBridge.jsm
@@ -10,6 +10,11 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/SystemAppProxy.jsm");
Cu.import("resource://gre/modules/Messaging.jsm");
+Cu.import("resource://gre/modules/AppsUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "appsService",
+ "@mozilla.org/AppsService;1",
+ "nsIAppsService");
XPCOMUtils.defineLazyServiceGetter(this, "settings",
"@mozilla.org/settingsService;1",
@@ -36,8 +41,11 @@ this.MessagesBridge = {
Services.obs.addObserver(this.onAndroidMessage, "Android:Launcher", false);
Services.obs.addObserver(this.onAndroidSetting, "Android:Setting", false);
Services.obs.addObserver(this.onSettingChange, "mozsettings-changed", false);
+ Services.obs.addObserver(this.onAndroidNotification, "Android:Notification", false);
Services.obs.addObserver(this, "xpcom-shutdown", false);
+ SystemAppProxy.addEventListener("mozContentNotificationEvent", this);
+
// Send a request to get the device's IMEI.
Messaging.sendRequestForResult({ type: "Android:GetIMEI" })
.then(aData => {
@@ -47,6 +55,38 @@ this.MessagesBridge = {
});
},
+ handleEvent: function(evt) {
+ let detail = evt.detail;
+
+ switch(detail.type) {
+ case "desktop-notification-click":
+ debug("Sending Android:NotificationOpened");
+ Messaging.sendRequest({ type: "Android:NotificationOpened", value: { id: detail.id }});
+ break;
+ case "desktop-notification-close":
+ // On receipt of a notification close, send the id to the Android layer
+ // so it can be removed from the Android NotificationManager.
+ debug("Sending Android:NotificationClosed");
+ Messaging.sendRequest({ type: "Android:NotificationClosed", value: { id: detail.id }});
+ break;
+ }
+ },
+
+ onAndroidNotification: function(aSubject, aTopic, aData) {
+ let data = JSON.parse(aData);
+ debug("Got android notification: " + data._action);
+ switch(data._action) {
+ case "post":
+ debug("showNotification(id=" + data.id + ")");
+ showNotification(data);
+ break;
+ case "remove":
+ debug("removeNotification(id=" + data.id + ")");
+ removeNotification(data);
+ break;
+ }
+ },
+
onAndroidMessage: function(aSubject, aTopic, aData) {
let data = JSON.parse(aData);
debug(`Got Android:Launcher message ${data.action}`);
@@ -122,9 +162,50 @@ this.MessagesBridge = {
Services.obs.removeObserver(this.onAndroidMessage, "Android:Launcher");
Services.obs.removeObserver(this.onAndroidSetting, "Android:Setting");
Services.obs.removeObserver(this.onSettingChange, "mozsettings-changed");
+ Services.obs.removeObserver(this.onAndroidNotification, "Android:Notification");
Services.obs.removeObserver(this, "xpcom-shutdown");
}
}
}
+function removeNotification(aDetail) {
+ // Remove the notification
+ SystemAppProxy._sendCustomEvent("mozChromeNotificationEvent", {
+ type: "desktop-notification-close",
+ id: aDetail.id
+ });
+}
+
+function showNotification(aDetail) {
+ const manifestURL = aDetail.manifestURL;
+
+ function send(appName) {
+ aDetail.type = "desktop-notification";
+ aDetail.appName = appName;
+
+ SystemAppProxy._sendCustomEvent("mozChromeNotificationEvent", aDetail);
+ }
+
+ if (!manifestURL|| !manifestURL.length) {
+ send(null);
+ return;
+ }
+
+ // If we have a manifest URL, get the app title from the manifest
+ // to prevent spoofing.
+ appsService.getManifestFor(manifestURL).then((manifest) => {
+ let app = appsService.getAppByManifestURL(manifestURL);
+
+ // Sometimes an android notification may be created by a service rather
+ // than an app. In this case, don't use the appName.
+ if (!app) {
+ send(null);
+ return;
+ }
+
+ let helper = new ManifestHelper(manifest, app.origin, manifestURL);
+ send(helper.name);
+ });
+}
+
this.MessagesBridge.init();