Bug 569402 - Show notifications in the Status Bar on Android r=blassey a=blocking-fennec
authorAlex Pakhotin <alexp@mozilla.com>
Mon, 14 Jun 2010 19:17:37 -0700
changeset 51723 8e6af865ffdb7200db3ef9f21df180179c754f8d
parent 51722 3ec56ec77529977a390dee9effbb23f9cedab232
child 51724 b68354fbfb713ad6ff7ced7c63a43469d96e8955
push idunknown
push userunknown
push dateunknown
reviewersblassey, blocking-fennec
bugs569402
milestone2.0b5pre
Bug 569402 - Show notifications in the Status Bar on Android r=blassey a=blocking-fennec
embedding/android/AndroidManifest.xml.in
embedding/android/GeckoApp.java
embedding/android/GeckoAppShell.java
embedding/android/Makefile.in
embedding/android/NotificationHandler.java.in
toolkit/components/alerts/public/nsIAlertsService.idl
toolkit/components/alerts/src/nsAlertsService.cpp
widget/src/android/AndroidBridge.cpp
widget/src/android/AndroidBridge.h
widget/src/android/AndroidJNI.cpp
widget/src/android/AndroidJavaWrappers.cpp
widget/src/android/AndroidJavaWrappers.h
widget/src/android/nsAppShell.cpp
widget/src/android/nsAppShell.h
--- a/embedding/android/AndroidManifest.xml.in
+++ b/embedding/android/AndroidManifest.xml.in
@@ -56,15 +56,25 @@
             </intent-filter>
 
             <!-- For debugging -->
             <intent-filter>
                 <action android:name="org.mozilla.gecko.DEBUG" />
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
-	<receiver android:enabled="true" android:name="Restarter">
-	  <intent-filter>
+
+        <activity android:name="NotificationHandler"
+                  android:label="@MOZ_APP_DISPLAYNAME@ Notification"
+                  android:theme="@android:style/Theme.NoTitleBar">
+            <intent-filter>
+                <action android:name="org.mozilla.gecko.ACTION_ALERT_CLICK" />
+                <action android:name="org.mozilla.gecko.ACTION_ALERT_CLEAR" />
+            </intent-filter>
+        </activity>
+
+        <receiver android:enabled="true" android:name="Restarter">
+          <intent-filter>
             <action android:name="org.mozilla.gecko.restart@MOZ_APP_NAME@" />
-	  </intent-filter>
-	</receiver>
+          </intent-filter>
+        </receiver>
     </application>
 </manifest> 
--- a/embedding/android/GeckoApp.java
+++ b/embedding/android/GeckoApp.java
@@ -52,16 +52,19 @@ import android.graphics.*;
 import android.widget.*;
 import android.hardware.*;
 
 import android.util.*;
 
 abstract public class GeckoApp
     extends Activity
 {
+    public static final String ACTION_ALERT_CLICK = "org.mozilla.gecko.ACTION_ALERT_CLICK";
+    public static final String ACTION_ALERT_CLEAR = "org.mozilla.gecko.ACTION_ALERT_CLEAR";
+
     public static FrameLayout mainLayout;
     public static GeckoSurfaceView surfaceView;
     public static GeckoApp mAppContext;
     ProgressDialog mProgressDialog;
 
     void launch()
     {
         // unpack files in the components directory
@@ -426,9 +429,13 @@ abstract public class GeckoApp
             String amCmd = "/system/bin/am broadcast -a " + action + getEnvString() + " -n org.mozilla." + getAppName() + "/org.mozilla." + getAppName() + ".Restarter";
             Log.i("GeckoAppJava", amCmd);
             Runtime.getRuntime().exec(amCmd);
         } catch (Exception e) {
             Log.i("GeckoAppJava", e.toString());
         }
         System.exit(0);
     }
+
+    public void handleNotification(String action, String alertName, String alertCookie) {
+        GeckoAppShell.handleNotification(action, alertName, alertCookie);
+    }
 }
--- a/embedding/android/GeckoAppShell.java
+++ b/embedding/android/GeckoAppShell.java
@@ -86,16 +86,18 @@ class GeckoAppShell
     public static native void nativeInit();
     public static native void nativeRun(String args);
 
     // helper methods
     public static native void setInitialSize(int width, int height);
     public static native void setSurfaceView(GeckoSurfaceView sv);
     public static native void putenv(String map);
     public static native void onResume();
+    public static native void callObserver(String observerKey, String topic, String data);
+    public static native void removeObserver(String observerKey);
 
     // java-side stuff
     public static void loadGeckoLibs() {
         // The package data lib directory isn't placed in ld.so's
         // search path, so we have to manually load libraries that
         // libxul will depend on.  Not ideal.
 
         // MozAlloc
@@ -434,9 +436,74 @@ class GeckoAppShell
     }
 
     static void setClipboardText(String text) {
         Context context = GeckoApp.surfaceView.getContext();
         ClipboardManager cm = (ClipboardManager)
             context.getSystemService(Context.CLIPBOARD_SERVICE);
         cm.setText(text);
     }
+
+    static void showAlertNotification(String imageUrl, String alertTitle, String alertText,
+                                      String alertCookie, String alertName) {
+        Log.i("GeckoAppJava", "GeckoAppShell.showAlertNotification\n" +
+              "- image = '" + imageUrl + "'\n" +
+              "- title = '" + alertTitle + "'\n" +
+              "- text = '" + alertText +"'\n" +
+              "- cookie = '" + alertCookie +"'\n" +
+              "- name = '" + alertName + "'");
+
+        int icon = R.drawable.icon; // Just use the app icon by default
+
+        Uri imageUri = Uri.parse(imageUrl);
+        String scheme = imageUri.getScheme();
+
+        if ("drawable".equals(scheme)) {
+            String resource = imageUri.getSchemeSpecificPart();
+            if ("//alertdownloads".equals(resource))
+                icon = R.drawable.alertdownloads;
+            else if ("//alertaddons".equals(resource))
+                icon = R.drawable.alertaddons;
+        }
+
+        int notificationID = alertName.hashCode();
+
+        Notification notification = new Notification(icon, alertTitle, 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,
+                                        "org.mozilla." + GeckoApp.mAppContext.getAppName() + ".NotificationHandler");
+
+        // Put the strings into the intent as an URI "alert:<name>#<cookie>"
+        Uri dataUri = Uri.fromParts("alert", alertName, alertCookie);
+        notificationIntent.setData(dataUri);
+
+        PendingIntent contentIntent = PendingIntent.getActivity(GeckoApp.mAppContext, 0, notificationIntent, 0);
+        notification.setLatestEventInfo(GeckoApp.mAppContext, alertTitle, alertText, contentIntent);
+
+        // 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,
+                                        "org.mozilla." + GeckoApp.mAppContext.getAppName() + ".NotificationHandler");
+        clearNotificationIntent.setData(dataUri);
+
+        PendingIntent pendingClearIntent = PendingIntent.getActivity(GeckoApp.mAppContext, 0, clearNotificationIntent, 0);
+        notification.deleteIntent = pendingClearIntent;
+
+        // Show the notification
+        NotificationManager notificationManager = (NotificationManager)
+            GeckoApp.mAppContext.getSystemService(Context.NOTIFICATION_SERVICE);
+        notificationManager.notify(notificationID, notification);
+        Log.i("GeckoAppJava", "Created notification ID " + notificationID);
+    }
+
+    public static void handleNotification(String action, String alertName, String alertCookie) {
+        if (GeckoApp.ACTION_ALERT_CLICK.equals(action)) {
+            Log.i("GeckoAppJava", "GeckoAppShell.handleNotification: callObserver(alertclickcallback)");
+            callObserver(alertName, "alertclickcallback", alertCookie);
+        }
+
+        Log.i("GeckoAppJava", "GeckoAppShell.handleNotification: callObserver(alertfinished)");
+        callObserver(alertName, "alertfinished", alertCookie);
+        removeObserver(alertName);
+    }
 }
--- a/embedding/android/Makefile.in
+++ b/embedding/android/Makefile.in
@@ -46,45 +46,60 @@ include $(topsrcdir)/ipc/app/defs.mk
 JAVAFILES = \
   GeckoApp.java \
   GeckoAppShell.java \
   GeckoEvent.java \
   GeckoSurfaceView.java \
   GeckoInputConnection.java \
   $(NULL)
 
+PROCESSEDJAVAFILES = \
+  App.java \
+  Restarter.java \
+  NotificationHandler.java \
+  $(NULL)
+
 DEFINES += \
   -DMOZ_APP_DISPLAYNAME=$(MOZ_APP_DISPLAYNAME) \
   -DMOZ_APP_NAME=$(MOZ_APP_NAME) \
   -DMOZ_CHILD_PROCESS_NAME=$(MOZ_CHILD_PROCESS_NAME)
 
 GARBAGE += \
   AndroidManifest.xml  \
   classes.dex  \
   $(MOZ_APP_NAME).apk  \
-  App.java \
-  Restarter.java \
+  $(PROCESSEDJAVAFILES) \
   gecko.ap_  \
   gecko-unaligned.apk  \
   gecko-unsigned-unaligned.apk \
   $(NULL)
 
 GARBAGE_DIRS += res libs dist classes
 
 DIRS = utils
 
 # Bug 567884 - Need a way to find appropriate icons during packaging
 ifeq ($(MOZ_APP_NAME),fennec)
 ICON_PATH = $(topsrcdir)/$(MOZ_BRANDING_DIRECTORY)/content/fennec_48x48.png
-ICON_PATH_HI = $(topsrcdir)/$(MOZ_BRANDING_DIRECTORY)/content/fennec_72x72.png
+ICON_PATH_HDPI = $(topsrcdir)/$(MOZ_BRANDING_DIRECTORY)/content/fennec_72x72.png
 else
 ICON_PATH = $(topsrcdir)/$(MOZ_BRANDING_DIRECTORY)/content/icon48.png
-ICON_PATH_HI = $(topsrcdir)/$(MOZ_BRANDING_DIRECTORY)/content/icon64.png
+ICON_PATH_HDPI = $(topsrcdir)/$(MOZ_BRANDING_DIRECTORY)/content/icon64.png
 endif
 
+RES_DRAWABLE = \
+  res/drawable/alertaddons.png \
+  res/drawable/alertdownloads.png \
+  $(NULL)
+
+RES_DRAWABLE_HDPI = \
+  res/drawable-hdpi/alertaddons.png \
+  res/drawable-hdpi/alertdownloads.png \
+  $(NULL)
+
 NATIVE_LIBS = $(shell cat $(DIST)/bin/dependentlibs.list) libxpcom.so libnssckbi.so libfreebl3.so libmozutils.so
 FULL_LIBS = $(addprefix libs/armeabi/,$(NATIVE_LIBS))
 
 # We'll strip all the libs by default, due to size, but we might
 # want to have a non-stripped version for debugging.  We should
 # really pull out debuginfo and stuff.
 ifdef NO_STRIP
 DO_STRIP=echo not stripping
@@ -115,34 +130,45 @@ include $(topsrcdir)/config/rules.mk
 
 # Override the Java settings with some specific android settings
 include $(topsrcdir)/config/android-common.mk
 
 tools:: $(MOZ_APP_NAME).apk
 
 # Note that we're going to set up a dependency directly between embed_android.dex and the java files
 # Instead of on the .class files, since more than one .class file might be produced per .java file
-classes.dex: $(JAVAFILES) App.java Restarter.java
+classes.dex: $(JAVAFILES) $(PROCESSEDJAVAFILES) R.java
 	$(NSINSTALL) -D classes
-	$(JAVAC) $(JAVAC_FLAGS) -d classes  $(addprefix $(srcdir)/,$(JAVAFILES)) App.java Restarter.java
+	$(JAVAC) $(JAVAC_FLAGS) -d classes  $(addprefix $(srcdir)/,$(JAVAFILES)) $(PROCESSEDJAVAFILES) R.java
 	$(DX) --dex --output=$@ classes
 
-AndroidManifest.xml App.java Restarter.java : % : %.in
+AndroidManifest.xml $(PROCESSEDJAVAFILES): % : %.in
 	$(PYTHON) $(topsrcdir)/config/Preprocessor.py \
              $(AUTOMATION_PPARGS) $(DEFINES) $(ACDEFINES) $< > $@
 
 res/drawable/icon.png: $(MOZ_APP_ICON)
 	$(NSINSTALL) -D res/drawable
 	cp $(ICON_PATH) $@
 
 res/drawable-hdpi/icon.png: $(MOZ_APP_ICON)
 	$(NSINSTALL) -D res/drawable-hdpi
-	cp $(ICON_PATH_HI) $@
+	cp $(ICON_PATH_HDPI) $@
+
+$(RES_DRAWABLE):
+	$(NSINSTALL) -D res/drawable
+	cp $(topsrcdir)/mobile/app/android/drawable/* res/drawable/
 
-gecko.ap_: AndroidManifest.xml res/drawable/icon.png res/drawable-hdpi/icon.png
+$(RES_DRAWABLE_HDPI):
+	$(NSINSTALL) -D res/drawable-hdpi
+	cp $(topsrcdir)/mobile/app/android/drawable-hdpi/* res/drawable-hdpi/
+
+R.java: $(MOZ_APP_ICON) $(RES_DRAWABLE) $(RES_DRAWABLE_HDPI)
+	$(AAPT) package -f -M AndroidManifest.xml -I $(ANDROID_SDK)/android.jar -S res -J . --custom-package org.mozilla.gecko
+
+gecko.ap_: AndroidManifest.xml res/drawable/icon.png res/drawable-hdpi/icon.png $(RES_DRAWABLE) $(RES_DRAWABLE_HDPI)
 	$(AAPT) package -f -M AndroidManifest.xml -I $(ANDROID_SDK)/android.jar  -S res -F $@
 
 libs/armeabi/%: $(DIST)/lib/%
 	@$(NSINSTALL) -D libs/armeabi
 	@cp -L -v $< $@
 	@$(DO_STRIP) $@
 
 # Bug 567873 - Android packaging should use standard packaging code
new file mode 100644
--- /dev/null
+++ b/embedding/android/NotificationHandler.java.in
@@ -0,0 +1,104 @@
+/* -*- Mode: Java; tab-width: 20; indent-tabs-mode: nil; -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Android code.
+ *
+ * The Initial Developer of the Original Code is Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Alex Pakhotin <alexp@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#filter substitution
+package org.mozilla.@MOZ_APP_NAME@;
+
+import android.app.Activity;
+import android.app.NotificationManager;
+import android.content.Intent;
+import android.content.ActivityNotFoundException;
+import android.os.Bundle;
+import android.util.Log;
+import android.net.Uri;
+
+public class NotificationHandler
+    extends Activity
+{
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        Log.i("GeckoAppJava", "NotificationHandler.onCreate");
+
+        Intent intent = getIntent();
+        if (intent != null)
+            handleIntent(intent);
+
+        finish();
+    }
+
+    protected void handleIntent(Intent notificationIntent) {
+        String action = notificationIntent.getAction();
+        String alertName = "";
+        String alertCookie = "";
+        Uri data = notificationIntent.getData();
+        if (data != null) {
+            alertName = data.getSchemeSpecificPart();
+            alertCookie = data.getFragment();
+            if (alertCookie == null)
+                alertCookie = "";
+        }
+
+        Log.i("GeckoAppJava", "NotificationHandler.handleIntent\n" +
+              "- action = '" + action + "'\n" +
+              "- alertName = '" + alertName + "'\n" +
+              "- alertCookie = '" + alertCookie + "'");
+
+        int notificationID = alertName.hashCode();
+
+        Log.i("GeckoAppJava", "Handle notification ID " + notificationID);
+
+        NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+        notificationManager.cancel(notificationID);
+
+        if (App.mAppContext != null) {
+            // This should call the observer, if any
+            App.mAppContext.handleNotification(action, alertName, alertCookie);
+        }
+
+        if (App.ACTION_ALERT_CLICK.equals(action)) {
+            // Start or bring to front the main activity
+            Intent appIntent = new Intent(Intent.ACTION_MAIN);
+            appIntent.setClassName(this, "org.mozilla.@MOZ_APP_NAME@.App");
+            try {
+                startActivity(appIntent);
+            } catch (ActivityNotFoundException e) {
+                Log.e("GeckoAppJava", "NotificationHandler Exception: " + e.getMessage());
+            }
+        }
+    }
+}
--- a/toolkit/components/alerts/public/nsIAlertsService.idl
+++ b/toolkit/components/alerts/public/nsIAlertsService.idl
@@ -51,19 +51,21 @@ interface nsIAlertsService : nsISupports
     * @param text           The contents of the alert.
     * @param textClickable  If true, causes the alert text to look like a link
     *                       and notifies the listener when user attempts to 
     *                       click the alert text.
     * @param cookie         A blind cookie the alert will pass back to the 
     *                       consumer during the alert listener callbacks.
     * @param alertListener  Used for callbacks. May be null if the caller 
     *                       doesn't care about callbacks.
-    * @param name           The name of the notification.  This is currently
-    *                       only used on OS X with Growl.  On OS X with Growl,
-    *                       users can disable notifications with a given name.
+    * @param name           The name of the notification. This is currently
+    *                       only used on OS X with Growl and Android.
+    *                       On OS X with Growl, users can disable notifications
+    *                       with a given name. On Android the name is hashed
+    *                       and used as a notification ID.
     *
     * @throws NS_ERROR_NOT_AVAILABLE If the notification cannot be displayed.
     *
     * The following arguments will be passed to the alertListener's observe() 
     * method:
     *   subject - null
     *   topic   - "alertfinished" when the alert goes away
     *             "alertclickcallback" when the text is clicked
--- a/toolkit/components/alerts/src/nsAlertsService.cpp
+++ b/toolkit/components/alerts/src/nsAlertsService.cpp
@@ -17,47 +17,55 @@
  * The Initial Developer of the Original Code is
  * Netscape Communications Corporation.
  * Portions created by the Initial Developer are Copyright (C) 2002
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
  *   Scott MacGregor <mscott@netscape.com>
  *   Jens Bannmann <jens.b@web.de>
+ *   Alex Pakhotin <alexp@mozilla.com>
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
  * decision by deleting the provisions above and replace them with the notice
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 #include "nsAlertsService.h"
+
+#ifdef ANDROID
+#include "AndroidBridge.h"
+#else
+
 #include "nsISupportsArray.h"
 #include "nsXPCOM.h"
 #include "nsISupportsPrimitives.h"
 #include "nsIServiceManager.h"
 #include "nsIDOMWindowInternal.h"
 #include "nsIWindowWatcher.h"
 #include "nsDependentString.h"
 #include "nsWidgetsCID.h"
 #include "nsILookAndFeel.h"
 #include "nsToolkitCompsCID.h"
 
 static NS_DEFINE_CID(kLookAndFeelCID, NS_LOOKANDFEEL_CID);
 
 #define ALERT_CHROME_URL "chrome://global/content/alerts/alert.xul"
 
+#endif // !ANDROID
+
 NS_IMPL_THREADSAFE_ADDREF(nsAlertsService)
 NS_IMPL_THREADSAFE_RELEASE(nsAlertsService)
 
 NS_INTERFACE_MAP_BEGIN(nsAlertsService)
    NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIAlertsService)
    NS_INTERFACE_MAP_ENTRY(nsIAlertsService)
 NS_INTERFACE_MAP_END_THREADSAFE
 
@@ -69,16 +77,21 @@ nsAlertsService::~nsAlertsService()
 {}
 
 NS_IMETHODIMP nsAlertsService::ShowAlertNotification(const nsAString & aImageUrl, const nsAString & aAlertTitle, 
                                                      const nsAString & aAlertText, PRBool aAlertTextClickable,
                                                      const nsAString & aAlertCookie,
                                                      nsIObserver * aAlertListener,
                                                      const nsAString & aAlertName)
 {
+#ifdef ANDROID
+  mozilla::AndroidBridge::Bridge()->ShowAlertNotification(aImageUrl, aAlertTitle, aAlertText, aAlertCookie,
+                                                          aAlertListener, aAlertName);
+  return NS_OK;
+#else
   // Check if there is an optional service that handles system-level notifications
   nsCOMPtr<nsIAlertsService> sysAlerts(do_GetService(NS_SYSTEMALERTSERVICE_CONTRACTID));
   nsresult rv;
   if (sysAlerts) {
     rv = sysAlerts->ShowAlertNotification(aImageUrl, aAlertTitle, aAlertText, aAlertTextClickable,
                                           aAlertCookie, aAlertListener, aAlertName);
     if (NS_SUCCEEDED(rv))
       return rv;
@@ -151,9 +164,10 @@ NS_IMETHODIMP nsAlertsService::ShowAlert
     rv = argsArray->AppendElement(ifptr);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   rv = wwatch->OpenWindow(0, ALERT_CHROME_URL, "_blank",
                  "chrome,dialog=yes,titlebar=no,popup=yes", argsArray,
                  getter_AddRefs(newWindow));
   return rv;
+#endif // !ANDROID
 }
--- a/widget/src/android/AndroidBridge.cpp
+++ b/widget/src/android/AndroidBridge.cpp
@@ -41,16 +41,17 @@
 #include "mozilla/dom/ContentChild.h"
 #include "nsXULAppAPI.h"
 #endif
 #include <pthread.h>
 #include <prthread.h>
 #include "nsXPCOMStrings.h"
 
 #include "AndroidBridge.h"
+#include "nsAppShell.h"
 
 using namespace mozilla;
 
 static PRUintn sJavaEnvThreadIndex = 0;
 
 AndroidBridge *AndroidBridge::sBridge = 0;
 
 static void
@@ -105,16 +106,17 @@ AndroidBridge::Init(JNIEnv *jEnv,
     jNotifyXreExit = (jmethodID) jEnv->GetStaticMethodID(jGeckoAppShellClass, "onXreExit", "()V");
     jGetHandlersForMimeType = (jmethodID) jEnv->GetStaticMethodID(jGeckoAppShellClass, "getHandlersForMimeType", "(Ljava/lang/String;)[Ljava/lang/String;");
     jGetHandlersForProtocol = (jmethodID) jEnv->GetStaticMethodID(jGeckoAppShellClass, "getHandlersForProtocol", "(Ljava/lang/String;)[Ljava/lang/String;");
     jOpenUriExternal = (jmethodID) jEnv->GetStaticMethodID(jGeckoAppShellClass, "openUriExternal", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Z");
     jGetMimeTypeFromExtension = (jmethodID) jEnv->GetStaticMethodID(jGeckoAppShellClass, "getMimeTypeFromExtension", "(Ljava/lang/String;)Ljava/lang/String;");
     jMoveTaskToBack = (jmethodID) jEnv->GetStaticMethodID(jGeckoAppShellClass, "moveTaskToBack", "()V");
     jGetClipboardText = (jmethodID) jEnv->GetStaticMethodID(jGeckoAppShellClass, "getClipboardText", "()Ljava/lang/String;");
     jSetClipboardText = (jmethodID) jEnv->GetStaticMethodID(jGeckoAppShellClass, "setClipboardText", "(Ljava/lang/String;)V");
+    jShowAlertNotification = (jmethodID) jEnv->GetStaticMethodID(jGeckoAppShellClass, "showAlertNotification", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
 
 
     jEGLContextClass = (jclass) jEnv->NewGlobalRef(jEnv->FindClass("javax/microedition/khronos/egl/EGLContext"));
     jEGL10Class = (jclass) jEnv->NewGlobalRef(jEnv->FindClass("javax/microedition/khronos/egl/EGL10"));
     jEGLSurfaceImplClass = (jclass) jEnv->NewGlobalRef(jEnv->FindClass("com/google/android/gles_jni/EGLSurfaceImpl"));
     jEGLContextImplClass = (jclass) jEnv->NewGlobalRef(jEnv->FindClass("com/google/android/gles_jni/EGLContextImpl"));
     jEGLConfigImplClass = (jclass) jEnv->NewGlobalRef(jEnv->FindClass("com/google/android/gles_jni/EGLConfigImpl"));
     jEGLDisplayImplClass = (jclass) jEnv->NewGlobalRef(jEnv->FindClass("com/google/android/gles_jni/EGLDisplayImpl"));
@@ -399,16 +401,40 @@ AndroidBridge::ClipboardHasText()
 
 void
 AndroidBridge::EmptyClipboard()
 {
     mJNIEnv->CallStaticObjectMethod(mGeckoAppShellClass, jSetClipboardText, nsnull);
 }
 
 void
+AndroidBridge::ShowAlertNotification(const nsAString& aImageUrl,
+                                     const nsAString& aAlertTitle,
+                                     const nsAString& aAlertText,
+                                     const nsAString& aAlertCookie,
+                                     nsIObserver *aAlertListener,
+                                     const nsAString& aAlertName)
+{
+    ALOG("ShowAlertNotification");
+
+    AutoLocalJNIFrame jniFrame;
+
+    if (nsAppShell::gAppShell && aAlertListener)
+        nsAppShell::gAppShell->AddObserver(aAlertName, aAlertListener);
+
+    jvalue args[5];
+    args[0].l = mJNIEnv->NewString(nsPromiseFlatString(aImageUrl).get(), aImageUrl.Length());
+    args[1].l = mJNIEnv->NewString(nsPromiseFlatString(aAlertTitle).get(), aAlertTitle.Length());
+    args[2].l = mJNIEnv->NewString(nsPromiseFlatString(aAlertText).get(), aAlertText.Length());
+    args[3].l = mJNIEnv->NewString(nsPromiseFlatString(aAlertCookie).get(), aAlertCookie.Length());
+    args[4].l = mJNIEnv->NewString(nsPromiseFlatString(aAlertName).get(), aAlertName.Length());
+    mJNIEnv->CallStaticVoidMethodA(mGeckoAppShellClass, jShowAlertNotification, args);
+}
+
+void
 AndroidBridge::SetSurfaceView(jobject obj)
 {
     mSurfaceView.Init(obj);
 }
 
 void *
 AndroidBridge::CallEglCreateWindowSurface(void *dpy, void *config, AndroidGeckoSurfaceView &sview)
 {
--- a/widget/src/android/AndroidBridge.h
+++ b/widget/src/android/AndroidBridge.h
@@ -38,16 +38,17 @@
 #ifndef AndroidBridge_h__
 #define AndroidBridge_h__
 
 #include <jni.h>
 #include <android/log.h>
 
 #include "nsCOMPtr.h"
 #include "nsIRunnable.h"
+#include "nsIObserver.h"
 
 #include "AndroidJavaWrappers.h"
 
 #include "nsVoidArray.h"
 
 // Some debug #defines
 // #define ANDROID_DEBUG_EVENTS
 // #define ANDROID_DEBUG_WIDGET
@@ -129,16 +130,23 @@ public:
     bool GetClipboardText(nsAString& aText);
 
     void SetClipboardText(const nsAString& aText);
     
     void EmptyClipboard();
 
     bool ClipboardHasText();
 
+    void ShowAlertNotification(const nsAString& aImageUrl,
+                               const nsAString& aAlertTitle,
+                               const nsAString& aAlertText,
+                               const nsAString& aAlertData,
+                               nsIObserver *aAlertListener,
+                               const nsAString& aAlertName);
+
     struct AutoLocalJNIFrame {
         AutoLocalJNIFrame(int nEntries = 128) : mEntries(nEntries) {
             AndroidBridge::Bridge()->JNI()->PushLocalFrame(mEntries);
         }
         // Note! Calling Purge makes all previous local refs created in
         // the AutoLocalJNIFrame's scope INVALID; be sure that you locked down
         // any local refs that you need to keep around in global refs!
         void Purge() {
@@ -186,16 +194,17 @@ protected:
     jmethodID jGetOutstandingDrawEvents;
     jmethodID jGetHandlersForMimeType;
     jmethodID jGetHandlersForProtocol;
     jmethodID jOpenUriExternal;
     jmethodID jGetMimeTypeFromExtension;
     jmethodID jMoveTaskToBack;
     jmethodID jGetClipboardText;
     jmethodID jSetClipboardText;
+    jmethodID jShowAlertNotification;
 
     // stuff we need for CallEglCreateWindowSurface
     jclass jEGLSurfaceImplClass;
     jclass jEGLContextImplClass;
     jclass jEGLConfigImplClass;
     jclass jEGLDisplayImplClass;
     jclass jEGLContextClass;
     jclass jEGL10Class;
--- a/widget/src/android/AndroidJNI.cpp
+++ b/widget/src/android/AndroidJNI.cpp
@@ -31,16 +31,17 @@
  * decision by deleting the provisions above and replace them with the notice
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 #include "nsILocalFile.h"
+#include "nsString.h"
 
 #include "AndroidBridge.h"
 
 #include <jni.h>
 #include <pthread.h>
 #include <dlfcn.h>
 
 #include "nsAppShell.h"
@@ -52,16 +53,18 @@ using namespace mozilla;
 /* Forward declare all the JNI methods as extern "C" */
 
 extern "C" {
     NS_EXPORT void JNICALL Java_org_mozilla_gecko_GeckoAppShell_nativeInit(JNIEnv *, jclass);
     NS_EXPORT void JNICALL Java_org_mozilla_gecko_GeckoAppShell_notifyGeckoOfEvent(JNIEnv *, jclass, jobject event);
     NS_EXPORT void JNICALL Java_org_mozilla_gecko_GeckoAppShell_setSurfaceView(JNIEnv *jenv, jclass, jobject sv);
     NS_EXPORT void JNICALL Java_org_mozilla_gecko_GeckoAppShell_setInitialSize(JNIEnv *jenv, jclass, int width, int height);
     NS_EXPORT void JNICALL Java_org_mozilla_gecko_GeckoAppShell_onResume(JNIEnv *, jclass);
+    NS_EXPORT void JNICALL Java_org_mozilla_gecko_GeckoAppShell_callObserver(JNIEnv *, jclass, jstring observerKey, jstring topic, jstring data);
+    NS_EXPORT void JNICALL Java_org_mozilla_gecko_GeckoAppShell_removeObserver(JNIEnv *jenv, jclass, jstring jObserverKey);
 }
 
 
 /*
  * Incoming JNI methods
  */
 
 NS_EXPORT void JNICALL
@@ -93,8 +96,35 @@ Java_org_mozilla_gecko_GeckoAppShell_set
 }
 
 NS_EXPORT void JNICALL
 Java_org_mozilla_gecko_GeckoAppShell_onResume(JNIEnv *jenv, jclass jc)
 {
     if (nsAppShell::gAppShell)
         nsAppShell::gAppShell->OnResume();
 }
+
+NS_EXPORT void JNICALL
+Java_org_mozilla_gecko_GeckoAppShell_callObserver(JNIEnv *jenv, jclass, jstring jObserverKey, jstring jTopic, jstring jData)
+{
+    if (!nsAppShell::gAppShell)
+        return;
+
+    nsJNIString sObserverKey(jObserverKey, jenv);
+    nsJNIString sTopic(jTopic, jenv);
+    nsJNIString sData(jData, jenv);
+
+    nsAppShell::gAppShell->CallObserver(sObserverKey, sTopic, sData);
+}
+
+NS_EXPORT void JNICALL
+Java_org_mozilla_gecko_GeckoAppShell_removeObserver(JNIEnv *jenv, jclass, jstring jObserverKey)
+{
+    if (!nsAppShell::gAppShell)
+        return;
+
+    const jchar *observerKey = jenv->GetStringChars(jObserverKey, NULL);
+    nsString sObserverKey(observerKey);
+    sObserverKey.SetLength(jenv->GetStringLength(jObserverKey));
+    jenv->ReleaseStringChars(jObserverKey, observerKey);
+
+    nsAppShell::gAppShell->RemoveObserver(sObserverKey);
+}
--- a/widget/src/android/AndroidJavaWrappers.cpp
+++ b/widget/src/android/AndroidJavaWrappers.cpp
@@ -436,21 +436,22 @@ AndroidRect::Init(JNIEnv *jenv, jobject 
     } else {
         mTop = 0;
         mLeft = 0;
         mRight = 0;
         mBottom = 0;
     }
 }
 
-nsJNIString::nsJNIString(jstring jstr)
+nsJNIString::nsJNIString(jstring jstr, JNIEnv *jenv)
 {
     if (!jstr) {
         SetIsVoid(PR_TRUE);
         return;
     }
-    const jchar* jCharPtr = JNI()->GetStringChars(jstr, false);
-    int len = JNI()->GetStringLength(jstr);
-    nsresult rv;
+    JNIEnv *jni = jenv;
+    if (!jni)
+        jni = JNI();
+    const jchar* jCharPtr = jni->GetStringChars(jstr, false);
+    int len = jni->GetStringLength(jstr);
     Assign(jCharPtr, len);
-    JNI()->ReleaseStringChars(jstr, jCharPtr);
-
+    jni->ReleaseStringChars(jstr, jCharPtr);
 }
--- a/widget/src/android/AndroidJavaWrappers.h
+++ b/widget/src/android/AndroidJavaWrappers.h
@@ -463,14 +463,14 @@ public:
         IME_GET_SELECTION = 6,
         IME_ADD_RANGE = 7
     };
 };
 
 class nsJNIString : public nsString
 {
 public:
-    nsJNIString(jstring jstr);
+    nsJNIString(jstring jstr, JNIEnv *jenv = NULL);
 };
 
 }
 
 #endif
--- a/widget/src/android/nsAppShell.cpp
+++ b/widget/src/android/nsAppShell.cpp
@@ -113,16 +113,18 @@ nsAppShell::Init()
 #endif
 
     mQueueLock = PR_NewLock();
     mCondLock = PR_NewLock();
     mPausedLock = PR_NewLock();
     mQueueCond = PR_NewCondVar(mCondLock);
     mPaused = PR_NewCondVar(mPausedLock);
 
+    mObserversHash.Init();
+
     return nsBaseAppShell::Init();
 }
 
 
 void
 nsAppShell::ScheduleNativeEventCallback()
 {
     EVLOG("nsAppShell::ScheduleNativeEventCallback pth: %p thread: %p main: %d", (void*) pthread_self(), (void*) NS_GetCurrentThread(), NS_IsMainThread());
@@ -362,16 +364,77 @@ void
 nsAppShell::OnResume()
 {
     PR_Lock(mPausedLock);
     PR_NotifyCondVar(mPaused);
     PR_Unlock(mPausedLock);
 
 }
 
+nsresult
+nsAppShell::AddObserver(const nsAString &aObserverKey, nsIObserver *aObserver)
+{
+    NS_ASSERTION(aObserver != nsnull, "nsAppShell::AddObserver: aObserver is null!");
+    return mObserversHash.Put(aObserverKey, aObserver) ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
+}
+
+/**
+ * The XPCOM event that will call the observer on the main thread.
+ */
+class ObserverCaller : public nsRunnable {
+public:
+    ObserverCaller(nsIObserver *aObserver, const char *aTopic, const PRUnichar *aData) :
+        mObserver(aObserver), mTopic(aTopic), mData(aData) {
+        NS_ASSERTION(aObserver != nsnull, "ObserverCaller: aObserver is null!");
+    }
+
+    NS_IMETHOD Run() {
+        ALOG("ObserverCaller::Run: observer = %p, topic = '%s')",
+             (nsIObserver*)mObserver, mTopic.get());
+        mObserver->Observe(nsnull, mTopic.get(), mData.get());
+        return NS_OK;
+    }
+
+private:
+    nsCOMPtr<nsIObserver> mObserver;
+    nsCString mTopic;
+    nsString mData;
+};
+
+void
+nsAppShell::CallObserver(const nsAString &aObserverKey, const nsAString &aTopic, const nsAString &aData)
+{
+    nsCOMPtr<nsIObserver> observer;
+    mObserversHash.Get(aObserverKey, getter_AddRefs(observer));
+
+    if (!observer) {
+        ALOG("nsAppShell::CallObserver: Observer was not found!");
+        return;
+    }
+
+    const NS_ConvertUTF16toUTF8 sTopic(aTopic);
+    const nsPromiseFlatString& sData = PromiseFlatString(aData);
+    
+    if (NS_IsMainThread()) {
+        // This branch will unlikely be hit, have it just in case
+        observer->Observe(nsnull, sTopic.get(), sData.get());
+    } else {
+        // Java is not running on main thread, so we have to use NS_DispatchToMainThread
+        nsCOMPtr<nsIRunnable> observerCaller = new ObserverCaller(observer, sTopic.get(), sData.get());
+        nsresult rv = NS_DispatchToMainThread(observerCaller);
+        ALOG("NS_DispatchToMainThread result: %d", rv);
+    }
+}
+
+void
+nsAppShell::RemoveObserver(const nsAString &aObserverKey)
+{
+    mObserversHash.Remove(aObserverKey);
+}
+
 // Used by IPC code
 namespace mozilla {
 
 bool ProcessNextEvent()
 {
     return nsAppShell::gAppShell->ProcessNextNativeEvent(PR_TRUE) ? true : false;
 }
 
--- a/widget/src/android/nsAppShell.h
+++ b/widget/src/android/nsAppShell.h
@@ -37,16 +37,17 @@
  * ***** END LICENSE BLOCK ***** */
 
 #ifndef nsAppShell_h__
 #define nsAppShell_h__
 
 #include "nsBaseAppShell.h"
 #include "nsCOMPtr.h"
 #include "nsTArray.h"
+#include "nsInterfaceHashtable.h"
 
 #include "prcvar.h"
 
 namespace mozilla {
 class AndroidGeckoEvent;
 bool ProcessNextEvent();
 void NotifyEvent();
 }
@@ -65,26 +66,31 @@ public:
     void NotifyNativeEvent();
 
     virtual PRBool ProcessNextNativeEvent(PRBool mayWait);
 
     void PostEvent(mozilla::AndroidGeckoEvent *event);
     void RemoveNextEvent();
     void OnResume();
 
+    nsresult AddObserver(const nsAString &aObserverKey, nsIObserver *aObserver);
+    void CallObserver(const nsAString &aObserverKey, const nsAString &aTopic, const nsAString &aData);
+    void RemoveObserver(const nsAString &aObserverKey);
+
 protected:
     virtual void ScheduleNativeEventCallback();
     virtual ~nsAppShell();
 
     int mNumDraws;
     PRLock *mQueueLock;
     PRLock *mCondLock;
     PRLock *mPausedLock;
     PRCondVar *mQueueCond;
     PRCondVar *mPaused;
     nsTArray<mozilla::AndroidGeckoEvent *> mEventQueue;
+    nsInterfaceHashtable<nsStringHashKey, nsIObserver> mObserversHash;
 
     mozilla::AndroidGeckoEvent *GetNextEvent();
     mozilla::AndroidGeckoEvent *PeekNextEvent();
 };
 
 #endif // nsAppShell_h__