Bug 781061 - System notifications should call back to the process that created them. r=blassey
authorWes Johnston <wjohnston@mozilla.com>
Wed, 22 Aug 2012 08:37:08 -0700
changeset 105067 1c14ca0ce21fef10640fe6e633c025cc4d96333e
parent 105066 8ce5250fe80f401eb955fade014cab9f9bb885ec
child 105068 2e06b3299a5e7e1325062d4bdb65a94705900ffb
push id55
push usershu@rfrn.org
push dateThu, 30 Aug 2012 01:33:09 +0000
reviewersblassey
bugs781061
milestone17.0a1
Bug 781061 - System notifications should call back to the process that created them. r=blassey
mobile/android/base/AndroidManifest.xml.in
mobile/android/base/GeckoApp.java
mobile/android/base/GeckoAppShell.java
mobile/android/base/NotificationHandler.java.in
mobile/android/base/WebAppManifestFragment.xml.frag
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -63,16 +63,20 @@
                   android:windowSoftInputMode="stateUnspecified|adjustResize"
                   android:launchMode="singleTask"
                   android:theme="@style/Gecko.App">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
             <intent-filter>
+                <action android:name="org.mozilla.gecko.ACTION_ALERT_CALLBACK" />
+            </intent-filter>
+
+            <intent-filter>
                 <action android:name="org.mozilla.gecko.UPDATE"/>
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
 
             <!-- Default browser intents -->
             <intent-filter>
                 <action android:name="android.intent.action.VIEW" />
                 <category android:name="android.intent.category.DEFAULT" />
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -117,16 +117,17 @@ abstract public class GeckoApp
     public static enum StartupMode {
         NORMAL,
         NEW_VERSION,
         NEW_PROFILE
     }
 
     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 final String ACTION_ALERT_CALLBACK = "org.mozilla.gecko.ACTION_ALERT_CALLBACK";
     public static final String ACTION_WEBAPP_PREFIX = "org.mozilla.gecko.WEBAPP";
     public static final String ACTION_DEBUG         = "org.mozilla.gecko.DEBUG";
     public static final String ACTION_BOOKMARK      = "org.mozilla.gecko.BOOKMARK";
     public static final String ACTION_LOAD          = "org.mozilla.gecko.LOAD";
     public static final String ACTION_UPDATE        = "org.mozilla.gecko.UPDATE";
     public static final String ACTION_INIT_PW       = "org.mozilla.gecko.INIT_PW";
     public static final String SAVED_STATE_TITLE         = "title";
     public static final String SAVED_STATE_IN_BACKGROUND = "inBackground";
@@ -1910,16 +1911,30 @@ abstract public class GeckoApp
             GeckoAppShell.sendEventToGecko(GeckoEvent.createWebappLoadEvent(uri));
             Log.i(LOGTAG,"Intent : WEBAPP (" + action + ") - " + uri);
         }
         else if (ACTION_BOOKMARK.equals(action)) {
             String uri = getURIFromIntent(intent);
             GeckoAppShell.sendEventToGecko(GeckoEvent.createBookmarkLoadEvent(uri));
             Log.i(LOGTAG,"Intent : BOOKMARK - " + 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);
+        }
     }
 
     /*
      * Handles getting a uri from and intent in a way that is backwards
      * compatable with our previous implementations
      */
     private String getURIFromIntent(Intent intent) {
         String uri = intent.getDataString();
@@ -2247,17 +2262,17 @@ abstract public class GeckoApp
                                 getPackageName() + ".Restarter");
             /* TODO: addEnvToIntent(intent); */
             intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
                             Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
             Log.i(LOGTAG, intent.toString());
             GeckoAppShell.killAnyZombies();
             startActivity(intent);
         } catch (Exception e) {
-            Log.i(LOGTAG, "error doing restart", e);
+            Log.e(LOGTAG, "error doing restart", e);
         }
         finish();
         // Give the restart process time to start before we die
         GeckoAppShell.waitForAnotherGeckoProc();
     }
 
     public void handleNotification(String action, String alertName, String alertCookie) {
         GeckoAppShell.handleNotification(action, alertName, alertCookie);
--- a/mobile/android/base/GeckoAppShell.java
+++ b/mobile/android/base/GeckoAppShell.java
@@ -126,16 +126,18 @@ public class GeckoAppShell
     static public final int WPL_STATE_START = 0x00000001;
     static public final int WPL_STATE_STOP = 0x00000010;
     static public final int WPL_STATE_IS_DOCUMENT = 0x00020000;
     static public final int WPL_STATE_IS_NETWORK = 0x00040000;
 
     public static final String SHORTCUT_TYPE_WEBAPP = "webapp";
     public static final String SHORTCUT_TYPE_BOOKMARK = "bookmark";
 
+    static private final boolean LOGGING = false;
+
     static public final int RESTORE_NONE = 0;
     static public final int RESTORE_OOM = 1;
     static public final int RESTORE_CRASH = 2;
 
     static private File sCacheFile = null;
     static private int sFreeSpace = -1;
     static File sHomeDir = null;
     static private int sDensityDpi = 0;
@@ -1323,18 +1325,25 @@ public class GeckoAppShell
                                   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");
 
-        // Put the strings into the intent as an URI "alert:<name>#<cookie>"
-        Uri dataUri = Uri.fromParts("alert", aAlertName, aAlertCookie);
+        // 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();
         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,
@@ -1371,31 +1380,36 @@ public class GeckoAppShell
 
         removeObserver(aAlertName);
 
         int notificationID = aAlertName.hashCode();
         removeNotification(notificationID);
     }
 
     public static void handleNotification(String aAction, String aAlertName, String aAlertCookie) {
+        if (LOGGING) Log.i(LOGTAG, "handleNotification " + aAction + " " + aAlertName + " " + aAlertCookie);
         int notificationID = aAlertName.hashCode();
 
-        if (GeckoApp.ACTION_ALERT_CLICK.equals(aAction)) {
-            Log.i(LOGTAG, "GeckoAppShell.handleNotification: callObserver(alertclickcallback)");
+        if (GeckoApp.ACTION_ALERT_CALLBACK.equals(aAction)) {
+            if (LOGGING) Log.i(LOGTAG, "GeckoAppShell.handleNotification: callObserver(alertclickcallback)");
             callObserver(aAlertName, "alertclickcallback", aAlertCookie);
 
             AlertNotification notification = mAlertNotifications.get(notificationID);
             if (notification != null && notification.isProgressStyle()) {
                 // When clicked, keep the notification, if it displays a progress
                 return;
             }
         }
 
         callObserver(aAlertName, "alertfinished", aAlertCookie);
-
+        // Also send a notification to the observer service
+        // New listeners should register for these notifications since they will be called even if
+        // Gecko has been killed and restared between when your notification was shown and when the
+        // user clicked on it.
+        sendEventToGecko(GeckoEvent.createBroadcastEvent("Notification:Clicked", aAlertCookie));
         removeObserver(aAlertName);
 
         removeNotification(notificationID);
     }
 
     private static void removeNotification(int notificationID) {
         mAlertNotifications.remove(notificationID);
 
--- a/mobile/android/base/NotificationHandler.java.in
+++ b/mobile/android/base/NotificationHandler.java.in
@@ -1,73 +1,69 @@
 /* -*- 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/. */
 
 #filter substitution
 package @ANDROID_PACKAGE_NAME@;
 
+import android.app.ActivityManager;
+import android.app.ActivityManager.RunningAppProcessInfo;
 import android.app.NotificationManager;
 import android.content.ActivityNotFoundException;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
+import android.os.Process;
+import android.text.TextUtils;
 import android.util.Log;
 
+import java.util.List;
+
+import org.mozilla.gecko.GeckoAppShell;
+
 public class NotificationHandler extends BroadcastReceiver {
     private static final String LOGTAG = "GeckoNotificationHandler";
 
     @Override
     public void onReceive(Context context, Intent intent) {
         if (intent != null)
             handleIntent(context, intent);
     }
 
     protected void handleIntent(Context context, Intent notificationIntent) {
-        String action = notificationIntent.getAction();
-        String alertName = "";
-        String alertCookie = "";
+        String appName = "";
         Uri data = notificationIntent.getData();
         if (data != null) {
-            alertName = data.getSchemeSpecificPart();
-            alertCookie = data.getFragment();
-            if (alertCookie == null)
-                alertCookie = "";
+            appName = data.getQueryParameter("app");
+            if (appName == null)
+              appName = "@ANDROID_PACKAGE_NAME@.App";
         }
 
-        Log.i(LOGTAG, "NotificationHandler.handleIntent\n" +
-              "- action = '" + action + "'\n" +
-              "- alertName = '" + alertName + "'\n" +
-              "- alertCookie = '" + alertCookie + "'");
+        sendIntent(context, App.ACTION_ALERT_CALLBACK,
+                            appName,
+                            data);
+    }
 
-        int notificationID = alertName.hashCode();
-
-        Log.i(LOGTAG, "Handle notification ID " + notificationID);
+    private void sendIntent(Context context, String action, String className, Uri data) {
+        Intent appIntent = new Intent(action);
 
-        if (App.ACTION_ALERT_CLICK.equals(action)) {
-            // Start or bring to front the main activity
-            Intent appIntent = new Intent(Intent.ACTION_MAIN);
-            appIntent.setClassName(context, "@ANDROID_PACKAGE_NAME@.App");
-            appIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-            appIntent.putExtra("args", "-alert " + alertName + (alertCookie.length() > 0 ? "#" + alertCookie : ""));
-            try {
-                Log.i(LOGTAG, "startActivity with intent: Action='" + appIntent.getAction() + "'" +
-                      ", args='" + appIntent.getStringExtra("args") + "'" );
-                context.startActivity(appIntent);
-            } catch (ActivityNotFoundException e) {
-                Log.e(LOGTAG, "NotificationHandler Exception: ", e);
-            }
-        }
+        if (TextUtils.isEmpty(className))
+          appIntent.setClassName(context, "@ANDROID_PACKAGE_NAME@.App");
+        else
+          appIntent.setClassName(context, className);
+
+        appIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
 
-        // Call observers after we open the app in order to avoid race conditions with
-        // notification handlers that launch third party apps or webapps
-        if (App.mAppContext != null) {
-            // This should call the observer, if any
-            App.mAppContext.handleNotification(action, alertName, alertCookie);
-        } else {
-            // The app is not running, just cancel this notification
-            NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
-            notificationManager.cancel(notificationID);
+        if (data != null)
+            appIntent.setData(data);
+
+        try {
+            Log.i(LOGTAG, "startActivity with intent: Action='" + appIntent.getAction() +
+                          " appName='" + className + "'");
+            context.startActivity(appIntent);
+        } catch (ActivityNotFoundException e) {
+            Log.e(LOGTAG, "NotificationHandler Exception: ", e);
         }
     }
 }
--- a/mobile/android/base/WebAppManifestFragment.xml.frag
+++ b/mobile/android/base/WebAppManifestFragment.xml.frag
@@ -3,10 +3,13 @@
                   android:configChanges="keyboard|keyboardHidden|mcc|mnc|orientation|screenSize"
                   android:windowSoftInputMode="stateUnspecified|adjustResize"
                   android:launchMode="singleInstance"
                   android:process=":@ANDROID_PACKAGE_NAME@.WebApp@APPNUM@"
                   android:theme="@style/Gecko.NoActionBar">
             <intent-filter>
                 <action android:name="org.mozilla.gecko.WEBAPP@APPNUM@" />
             </intent-filter>
+            <intent-filter>
+                <action android:name="org.mozilla.gecko.ACTION_ALERT_CALLBACK" />
+            </intent-filter>
         </activity>