Bug 1082443 - B2G NFC: event fallback to System app if the foreground app cannot handle it. r=smaug, dimi
authorYoshi Huang <allstars.chh@mozilla.com>
Thu, 11 Dec 2014 17:42:35 +0800
changeset 239474 37aec6d48423712f60f15667e87ab1c00fbc1d87
parent 239473 c41bf6d0171561b7309481effbb4cda6fc57b3fa
child 239475 9daf364f18c0f16c3c9fcab8faaaea4c756f5569
push id500
push userjoshua.m.grant@gmail.com
push dateThu, 29 Jan 2015 01:48:36 +0000
reviewerssmaug, dimi
bugs1082443
milestone38.0a1
Bug 1082443 - B2G NFC: event fallback to System app if the foreground app cannot handle it. r=smaug, dimi
dom/nfc/NfcContentHelper.js
dom/nfc/gonk/Nfc.js
dom/nfc/nsINfcContentHelper.idl
dom/nfc/nsNfc.js
dom/webidl/MozNFC.webidl
--- a/dom/nfc/NfcContentHelper.js
+++ b/dom/nfc/NfcContentHelper.js
@@ -142,16 +142,20 @@ NfcContentHelper.prototype = {
     }
   },
 
   queryRFState: function queryRFState() {
     return this._rfState;
   },
 
   encodeNDEFRecords: function encodeNDEFRecords(records) {
+    if (!Array.isArray(records)) {
+      return null;
+    }
+
     let encodedRecords = [];
     for (let i = 0; i < records.length; i++) {
       let record = records[i];
       encodedRecords.push({
         tnf: record.tnf,
         type: record.type || undefined,
         id: record.id || undefined,
         payload: record.payload || undefined,
@@ -273,16 +277,26 @@ NfcContentHelper.prototype = {
     let requestId = callback.getCallbackId();
     this._requestMap[requestId] = callback;
 
     cpmm.sendAsyncMessage("NFC:ChangeRFState",
                           {requestId: requestId,
                            rfState: rfState});
   },
 
+  callDefaultFoundHandler: function callDefaultFoundHandler(sessionToken,
+                                                            isP2P,
+                                                            records) {
+    let encodedRecords = this.encodeNDEFRecords(records);
+    cpmm.sendAsyncMessage("NFC:CallDefaultFoundHandler",
+                          {sessionToken: sessionToken,
+                           isP2P: isP2P,
+                           records: encodedRecords});
+  },
+
   // nsIObserver
   observe: function observe(subject, topic, data) {
     if (topic == "xpcom-shutdown") {
       this.destroyDOMRequestHelper();
       Services.obs.removeObserver(this, NFC.TOPIC_MOZSETTINGS_CHANGED);
       Services.obs.removeObserver(this, "xpcom-shutdown");
       cpmm = null;
     } else if (topic == NFC.TOPIC_MOZSETTINGS_CHANGED) {
--- a/dom/nfc/gonk/Nfc.js
+++ b/dom/nfc/gonk/Nfc.js
@@ -52,17 +52,18 @@ updateDebug();
 
 const NFC_CONTRACTID = "@mozilla.org/nfc;1";
 const NFC_CID =
   Components.ID("{2ff24790-5e74-11e1-b86c-0800200c9a66}");
 
 const NFC_IPC_MSG_ENTRIES = [
   { permission: null,
     messages: ["NFC:AddEventListener",
-               "NFC:QueryInfo"] },
+               "NFC:QueryInfo",
+               "NFC:CallDefaultFoundHandler"] },
 
   { permission: "nfc",
     messages: ["NFC:ReadNDEF",
                "NFC:WriteNDEF",
                "NFC:MakeReadOnly",
                "NFC:Format",
                "NFC:Transceive"] },
 
@@ -222,16 +223,23 @@ XPCOMUtils.defineLazyGetter(this, "gMess
         debug("Peer already lost or " + appId + " is not a registered PeerReadytarget");
         return;
       }
 
       this.notifyDOMEvent(target, {event: NFC.PEER_EVENT_READY,
                                    sessionToken: sessionToken});
     },
 
+    callDefaultFoundHandler: function callDefaultFoundHandler(message) {
+      let sysMsg = new NfcTechDiscoveredSysMsg(message.sessionToken,
+                                               message.isP2P,
+                                               message.records || null);
+      gSystemMessenger.broadcastMessage("nfc-manager-tech-discovered", sysMsg);
+    },
+
     onTagFound: function onTagFound(message) {
       let target = this.eventListeners[this.focusApp] ||
                    this.eventListeners[NFC.SYSTEM_APP_ID];
 
       message.event = NFC.TAG_EVENT_FOUND;
 
       this.notifyDOMEvent(target, message);
 
@@ -312,16 +320,19 @@ XPCOMUtils.defineLazyGetter(this, "gMess
           // to appropriate content process.
           message.data.type = "NotifySendFileStatusResponse";
           if (message.data.status) {
             message.data.errorMsg =
               this.nfc.getErrorMessage(NFC.NFC_GECKO_ERROR_SEND_FILE_FAILED);
           }
           this.nfc.sendNfcResponse(message.data);
           return null;
+        case "NFC:CallDefaultFoundHandler":
+          this.callDefaultFoundHandler(message.data);
+          return null;
         default:
           return this.nfc.receiveMessage(message);
       }
     },
 
     /**
      * nsIObserver interface methods.
      */
@@ -497,21 +508,16 @@ Nfc.prototype = {
           if (message.records) {
             // TODO: Bug 1082493.
           } else {
             gMessageManager.onPeerEvent(NFC.PEER_EVENT_FOUND, message.sessionToken);
           }
         } else {
           gMessageManager.onTagFound(message);
         }
-
-        let sysMsg = new NfcTechDiscoveredSysMsg(message.sessionToken,
-                                                 message.isP2P,
-                                                 message.records || null);
-        gSystemMessenger.broadcastMessage("nfc-manager-tech-discovered", sysMsg);
         break;
       case "TechLostNotification":
         message.type = "techLost";
 
         // Update the upper layers with a session token (alias)
         message.sessionToken = SessionHelper.getToken(message.sessionId);
         if (SessionHelper.isP2PSession(message.sessionId)) {
           gMessageManager.onPeerEvent(NFC.PEER_EVENT_LOST, message.sessionToken);
--- a/dom/nfc/nsINfcContentHelper.idl
+++ b/dom/nfc/nsINfcContentHelper.idl
@@ -106,17 +106,17 @@ interface nsINfcRequestCallback : nsISup
 interface nsINfcBrowserAPI : nsISupports
 {
   const int32_t SYSTEM_APP_ID = -1;
 
   void setFocusApp(in uint64_t tabId,
                    in boolean isFocus);
 };
 
-[scriptable, uuid(b5194ae8-d5d5-482f-a73f-dd0d755a1972)]
+[scriptable, uuid(b35f4bf5-e1b8-45f4-b5d3-2ae9b6d5871e)]
 interface nsINfcContentHelper : nsISupports
 {
   void init(in nsIDOMWindow window);
 
   /**
    * Read current NDEF data on the tag.
    *
    * @param sessionToken
@@ -273,9 +273,24 @@ interface nsINfcContentHelper : nsISuppo
    *
    * @param rfState. Possible values are 'idle', 'listen' and 'discovery'.
    *
    * @param callback
    *        Called when request is finished
    */
   void changeRFState(in DOMString rfState,
                      in nsINfcRequestCallback callback);
+
+  /**
+   * Notify parent process to call the default tagfound or peerfound event
+   * handler.
+   *
+   * @param sessionToken
+   *        Session token of this event.
+   * @param isP2P
+   *        Is this a P2P Session.
+   * @param records
+   *        NDEF Records.
+   */
+  void callDefaultFoundHandler(in DOMString sessionToken,
+                               in boolean isP2P,
+                               in nsIVariant records);
 };
--- a/dom/nfc/nsNfc.js
+++ b/dom/nfc/nsNfc.js
@@ -426,28 +426,39 @@ MozNFCImpl.prototype = {
       return;
     }
 
     let appId = this._window.document.nodePrincipal.appId;
     this._nfcContentHelper.unregisterTargetForPeerReady(appId);
   },
 
   notifyTagFound: function notifyTagFound(sessionToken, tagInfo, ndefInfo, records) {
+    if (!this.handleTagFound(sessionToken, tagInfo, ndefInfo, records)) {
+      this._nfcContentHelper.callDefaultFoundHandler(sessionToken, false, records);
+    };
+  },
+
+  /**
+   * Handles Tag Found event.
+   *
+   * returns true if the app could process this event, false otherwise.
+   */
+  handleTagFound: function handleTagFound(sessionToken, tagInfo, ndefInfo, records) {
     if (this.hasDeadWrapper()) {
       dump("this._window or this.__DOM_IMPL__ is a dead wrapper.");
-      return;
+      return false;
     }
 
     if (!this.eventService.hasListenersFor(this.__DOM_IMPL__, "tagfound")) {
       debug("ontagfound is not registered.");
-      return;
+      return false;
     }
 
     if (!this.checkPermissions(["nfc"])) {
-      return;
+      return false;
     }
 
     this.eventService.addSystemEventListener(this._window, "visibilitychange",
       this, /* useCapture */false);
 
     let tagImpl = new MozNFCTagImpl(this._window, sessionToken, tagInfo, ndefInfo);
     let tag = this._window.MozNFCTag._create(this._window, tagImpl);
 
@@ -460,23 +471,33 @@ MozNFCImpl.prototype = {
       let record = records[i];
       ndefRecords.push(new this._window.MozNDEFRecord({tnf: record.tnf,
                                                        type: record.type,
                                                        id: record.id,
                                                        payload: record.payload}));
     }
 
     let eventData = {
+      "cancelable": true,
       "tag": tag,
       "ndefRecords": ndefRecords
     };
 
     debug("fire ontagfound " + sessionToken);
     let tagEvent = new this._window.MozNFCTagEvent("tagfound", eventData);
     this.__DOM_IMPL__.dispatchEvent(tagEvent);
+
+    // If defaultPrevented is false, means we need to take the default action
+    // for this event - redirect this event to System app. Before redirecting to
+    // System app, we need revoke the tag object first.
+    if (!tagEvent.defaultPrevented) {
+      this.notifyTagLost(sessionToken);
+    }
+
+    return tagEvent.defaultPrevented;
   },
 
   notifyTagLost: function notifyTagLost(sessionToken) {
     if (this.hasDeadWrapper()) {
       dump("this._window or this.__DOM_IMPL__ is a dead wrapper.");
       return;
     }
 
@@ -499,43 +520,78 @@ MozNFCImpl.prototype = {
     this.nfcTag = null;
 
     debug("fire ontaglost " + sessionToken);
     let event = new this._window.Event("taglost");
     this.__DOM_IMPL__.dispatchEvent(event);
   },
 
   notifyPeerFound: function notifyPeerFound(sessionToken, isPeerReady) {
+    if (!this.handlePeerFound(sessionToken, isPeerReady)) {
+      this._nfcContentHelper.callDefaultFoundHandler(sessionToken, true, null);
+    }
+  },
+
+  /**
+   * Handles Peer Found/Peer Ready event.
+   *
+   * returns true if the app could process this event, false otherwise.
+   */
+  handlePeerFound: function handlePeerFound(sessionToken, isPeerReady) {
     if (this.hasDeadWrapper()) {
       dump("this._window or this.__DOM_IMPL__ is a dead wrapper.");
-      return;
+      return false;
     }
 
     if (!isPeerReady &&
         !this.eventService.hasListenersFor(this.__DOM_IMPL__, "peerfound")) {
       debug("onpeerfound is not registered.");
-      return;
+      return false;
     }
 
     let perm = isPeerReady ? ["nfc-share"] : ["nfc"];
     if (!this.checkPermissions(perm)) {
-      return;
+      return false;
     }
 
     this.eventService.addSystemEventListener(this._window, "visibilitychange",
       this, /* useCapture */false);
 
     let peerImpl = new MozNFCPeerImpl(this._window, sessionToken);
     this.nfcPeer = this._window.MozNFCPeer._create(this._window, peerImpl);
-    let eventData = { "peer": this.nfcPeer };
-    let type = (isPeerReady) ? "peerready" : "peerfound";
+
+    let eventType;
+    let eventData = {
+      "peer": this.nfcPeer
+    };
+
+    if (isPeerReady) {
+      eventType = "peerready";
+    } else {
+      eventData.cancelable = true;
+      eventType = "peerfound";
+    }
 
-    debug("fire on" + type + " " + sessionToken);
-    let event = new this._window.MozNFCPeerEvent(type, eventData);
+    debug("fire on" + eventType + " " + sessionToken);
+    let event = new this._window.MozNFCPeerEvent(eventType, eventData);
     this.__DOM_IMPL__.dispatchEvent(event);
+
+    // For peerready we don't take the default action.
+    if (isPeerReady) {
+      return true;
+    }
+
+    // If defaultPrevented is false, means we need to take the default action
+    // for this event - redirect this event to System app. Before redirecting to
+    // System app, we need revoke the peer object first.
+    if (!event.defaultPrevented) {
+      this.notifyPeerLost(sessionToken);
+    }
+
+    return event.defaultPrevented;
   },
 
   notifyPeerLost: function notifyPeerLost(sessionToken) {
     if (this.hasDeadWrapper()) {
       dump("this._window or this.__DOM_IMPL__ is a dead wrapper.");
       return;
     }
 
--- a/dom/webidl/MozNFC.webidl
+++ b/dom/webidl/MozNFC.webidl
@@ -85,28 +85,43 @@ interface MozNFC : EventTarget {
    * This event will be fired when another NFCPeer is detected, and user confirms
    * to share data to the NFCPeer object by calling mozNFC.notifyUserAcceptedP2P.
    * The event will be type of NFCPeerEvent.
    */
   [CheckPermissions="nfc-share", AvailableIn=CertifiedApps]
   attribute EventHandler onpeerready;
 
   /**
-   * This event will be fired when a NFCPeer is detected.
+   * This event will be fired when a NFCPeer is detected. The application has to
+   * be running on the foreground (decided by System app) to receive this event.
+   *
+   * The default action of this event is to dispatch the event in System app
+   * again, and System app will run the default UX behavior (like vibration).
+   * So if the application would like to cancel the event, the application
+   * should call event.preventDefault() or return false in this event handler.
    */
   attribute EventHandler onpeerfound;
 
   /**
    * This event will be fired when NFCPeer, earlier detected in onpeerready
    * or onpeerfound, moves out of range.
    */
   attribute EventHandler onpeerlost;
 
   /**
-   * Ths event will be fired when a NFCTag is detected.
+   * This event will be fired when a NFCTag is detected. The application has to
+   * be running on the foreground (decided by System app) to receive this event.
+   *
+   * The default action of this event is to dispatch the event in System app
+   * again, and System app will run the default UX behavior (like vibration) and
+   * launch MozActivity to handle the content of the tag. (For example, System
+   * app will launch Browser if the tag contains URL). So if the application
+   * would like to cancel the event, i.e. in the above example, the application
+   * would process the URL by itself without launching Browser, the application
+   * should call event.preventDefault() or return false in this event handler.
    */
   attribute EventHandler ontagfound;
 
   /**
    * This event will be fired if the tag detected in ontagfound has been removed.
    */
   attribute EventHandler ontaglost;
 };