Bug 877302 - [Dialer] when searching for a number in contacts, display partial matches (matching the end of the number). r=bent, reuben
authorGregor Wagner <anygregor@gmail.com>
Thu, 13 Jun 2013 15:00:23 -0700
changeset 146517 ab100d18b1002b569d7233dc534801e85103fc09
parent 146516 b5d440b71e595c24f017dcc525688f4c8528f840
child 146518 c3fef5de1868d1c38b83a03c32326d0b2d962bb4
push id2697
push userbbajaj@mozilla.com
push dateMon, 05 Aug 2013 18:49:53 +0000
treeherdermozilla-beta@dfec938c7b63 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbent, reuben
bugs877302
milestone24.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 877302 - [Dialer] when searching for a number in contacts, display partial matches (matching the end of the number). r=bent, reuben
b2g/app/b2g.js
dom/contacts/fallback/ContactDB.jsm
dom/contacts/fallback/ContactService.jsm
dom/contacts/tests/Makefile.in
dom/contacts/tests/test_contacts_international.html
dom/contacts/tests/test_contacts_substringmatching.html
dom/phonenumberutils/PhoneNumberUtils.jsm
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -372,26 +372,32 @@ pref("browser.link.open_newwindow.restri
 pref("dom.mozBrowserFramesEnabled", true);
 
 // Enable a (virtually) unlimited number of mozbrowser processes.
 // We'll run out of PIDs on UNIX-y systems before we hit this limit.
 pref("dom.ipc.processCount", 100000);
 
 pref("dom.ipc.browser_frames.oop_by_default", false);
 
-// Temporary permission hack for WebSMS
+// WebSMS
 pref("dom.sms.enabled", true);
 pref("dom.sms.strict7BitEncoding", false); // Disabled by default.
 pref("dom.sms.requestStatusReport", true); // Enabled by default.
 
-// Temporary permission hack for WebContacts
+// WebContacts
 pref("dom.mozContacts.enabled", true);
 pref("dom.navigator-property.disable.mozContacts", false);
 pref("dom.global-constructor.disable.mozContact", false);
 
+// Shortnumber matching needed for e.g. Brazil:
+// 01187654321 can be found with 87654321
+pref("dom.phonenumber.substringmatching.BR", 8);
+pref("dom.phonenumber.substringmatching.CO", 10);
+pref("dom.phonenumber.substringmatching.VE", 7);
+
 // WebAlarms
 pref("dom.mozAlarms.enabled", true);
 
 // SimplePush
 pref("services.push.enabled", true);
 // serverURL to be assigned by services team
 pref("services.push.serverURL", "");
 pref("services.push.userAgentID", "");
--- a/dom/contacts/fallback/ContactDB.jsm
+++ b/dom/contacts/fallback/ContactDB.jsm
@@ -477,17 +477,21 @@ ContactDB.prototype = {
                     debug("InternationalFormat: " + parsedNumber.internationalFormat);
                     debug("InternationalNumber: " + parsedNumber.internationalNumber);
                     debug("NationalNumber: " + parsedNumber.nationalNumber);
                     debug("NationalFormat: " + parsedNumber.nationalFormat);
                   }
                   matchSearch[parsedNumber.nationalNumber] = 1;
                   matchSearch[parsedNumber.internationalNumber] = 1;
                   matchSearch[PhoneNumberUtils.normalize(parsedNumber.nationalFormat)] = 1;
-                  matchSearch[PhoneNumberUtils.normalize(parsedNumber.internationalFormat)] = 1
+                  matchSearch[PhoneNumberUtils.normalize(parsedNumber.internationalFormat)] = 1;
+
+                  if (this.substringMatching && normalized.length > this.substringMatching) {
+                    matchSearch[normalized.slice(-this.substringMatching)] = 1;
+                  }
                 }
 
                 // containsSearch holds incremental search values for:
                 // normalized number and national format
                 for (let i = 0; i < normalized.length; i++) {
                   containsSearch[normalized.substring(i, normalized.length)] = 1;
                 }
                 if (parsedNumber && parsedNumber.nationalFormat) {
@@ -872,16 +876,21 @@ ContactDB.prototype = {
         if (key != "tel") {
           dump("ContactDB: 'match' filterOp only works on tel\n");
           return txn.abort();
         }
 
         let index = store.index("telMatch");
         let normalized = PhoneNumberUtils.normalize(options.filterValue,
                                                     /*numbersOnly*/ true);
+
+        // Some countries need special handling for number matching. Bug 877302
+        if (this.substringMatching && normalized.length > this.substringMatching) {
+          normalized = normalized.slice(-this.substringMatching);
+        }
         request = index.mozGetAll(normalized, limit);
       } else {
         // XXX: "contains" should be handled separately, this is "startsWith"
         if (options.filterOp === 'contains' && key !== 'tel') {
           console.warn("ContactDB: 'contains' only works for 'tel'. " +
                        "Falling back to 'startsWith'.");
         }
         // not case sensitive
@@ -926,12 +935,17 @@ ContactDB.prototype = {
       if (DEBUG) debug("Request successful. Record count:" + event.target.result.length);
       this.sortResults(event.target.result, options);
       for (let i in event.target.result) {
         txn.result[event.target.result[i].id] = this.makeExport(event.target.result[i]);
       }
     }.bind(this);
   },
 
+  // Enable special phone number substring matching. Does not update existing DB entries.
+  enableSubstringMatching: function enableSubstringMatching(aDigits) {
+    this.substringMatching = aDigits;
+  },
+
   init: function init(aGlobal) {
-      this.initDBHelper(DB_NAME, DB_VERSION, [STORE_NAME, SAVED_GETALL_STORE_NAME, REVISION_STORE], aGlobal);
+    this.initDBHelper(DB_NAME, DB_VERSION, [STORE_NAME, SAVED_GETALL_STORE_NAME, REVISION_STORE], aGlobal);
   }
 };
--- a/dom/contacts/fallback/ContactService.jsm
+++ b/dom/contacts/fallback/ContactService.jsm
@@ -11,16 +11,17 @@ const Cu = Components.utils;
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 
 this.EXPORTED_SYMBOLS = [];
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/ContactDB.jsm");
+Cu.import("resource://gre/modules/PhoneNumberUtils.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
                                    "@mozilla.org/parentprocessmessagemanager;1",
                                    "nsIMessageListenerManager");
 
 let myGlobal = this;
 
 let ContactService = {
@@ -37,32 +38,54 @@ let ContactService = {
       ppmm.addMessageListener(msgName, this);
     }.bind(this));
 
     var idbManager = Components.classes["@mozilla.org/dom/indexeddb/manager;1"].getService(Ci.nsIIndexedDatabaseManager);
     idbManager.initWindowless(myGlobal);
     this._db = new ContactDB(myGlobal);
     this._db.init(myGlobal);
 
+    let countryName = PhoneNumberUtils.getCountryName();
+    if (Services.prefs.getPrefType("dom.phonenumber.substringmatching." + countryName) == Ci.nsIPrefBranch.PREF_INT) {
+      if (DEBUG) debug("Enable Substring Matching for Phone Numbers: " + countryName);
+      let val = Services.prefs.getIntPref("dom.phonenumber.substringmatching." + countryName);
+      if (val && val > 0) {
+        this._db.enableSubstringMatching(val);
+      }
+    }
+
     Services.obs.addObserver(this, "profile-before-change", false);
+    Services.prefs.addObserver("dom.phonenumber.substringmatching", this, false);
   },
 
   observe: function(aSubject, aTopic, aData) {
-    myGlobal = null;
-    this._messages.forEach(function(msgName) {
-      ppmm.removeMessageListener(msgName, this);
-    }.bind(this));
-    Services.obs.removeObserver(this, "profile-before-change");
-    ppmm = null;
-    this._messages = null;
-    if (this._db)
-      this._db.close();
-    this._db = null;
-    this._children = null;
-    this._cursors = null;
+    if (aTopic === 'profile-before-change') {
+      myGlobal = null;
+      this._messages.forEach(function(msgName) {
+        ppmm.removeMessageListener(msgName, this);
+      }.bind(this));
+      Services.obs.removeObserver(this, "profile-before-change");
+      Services.prefs.removeObserver("dom.phonenumber.substringmatching", this);
+      ppmm = null;
+      this._messages = null;
+      if (this._db)
+        this._db.close();
+      this._db = null;
+      this._children = null;
+      this._cursors = null;
+    } else if (aTopic === 'nsPref:changed' && aData.contains("dom.phonenumber.substringmatching")) {
+      // We don't fully support changing substringMatching during runtime. This is mostly for testing.
+      let countryName = PhoneNumberUtils.getCountryName();
+      if (Services.prefs.getPrefType("dom.phonenumber.substringmatching." + countryName) == Ci.nsIPrefBranch.PREF_INT) {
+        let val = Services.prefs.getIntPref("dom.phonenumber.substringmatching." + countryName);
+        if (val && val > 0) {
+          this._db.enableSubstringMatching(val);
+        }
+      }
+    }
   },
 
   assertPermission: function(aMessage, aPerm) {
     if (!aMessage.target.assertPermission(aPerm)) {
       Cu.reportError("Contacts message " + aMessage.name +
                      " from a content process with no" + aPerm + " privileges.");
       return false;
     }
--- a/dom/contacts/tests/Makefile.in
+++ b/dom/contacts/tests/Makefile.in
@@ -8,16 +8,17 @@ srcdir           = @srcdir@
 VPATH            = @srcdir@
 
 relativesrcdir   = @relativesrcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 MOCHITEST_FILES = \
   test_contacts_basics.html \
+  test_contacts_substringmatching.html \
   test_contacts_events.html \
   test_contacts_blobs.html \
   test_contacts_international.html \
   test_contacts_getall.html \
   $(NULL)
 
 include $(topsrcdir)/config/rules.mk
 
--- a/dom/contacts/tests/test_contacts_international.html
+++ b/dom/contacts/tests/test_contacts_international.html
@@ -214,16 +214,25 @@ var steps = [
     req = mozContacts.find(options);
     req.onsuccess = function () {
       ok(req.result.length == 0, "Found exactly 1 contact.");
       next();
     };
     req.onerror = onFailure;
   },
   function () {
+    ok(true, "Deleting database");
+    req = mozContacts.clear()
+    req.onsuccess = function () {
+      ok(true, "Deleted the database");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
     ok(true, "all done!\n");
     SimpleTest.finish();
   }
 ];
 
 function next() {
   ok(true, "Begin!");
   if (index >= steps.length) {
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/test_contacts_substringmatching.html
@@ -0,0 +1,261 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=877302
+-->
+<head>
+  <title>Test for Bug 877302 substring matching for WebContacts</title>
+  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=877302">Mozilla Bug 877302</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+"use strict";
+
+if (SpecialPowers.isMainProcess()) {
+  SpecialPowers.Cu.import("resource://gre/modules/ContactService.jsm");
+}
+
+var substringLength = 8;
+SpecialPowers.setIntPref("dom.phonenumber.substringmatching.BR", substringLength);
+
+SpecialPowers.addPermission("contacts-write", true, document);
+SpecialPowers.addPermission("contacts-read", true, document);
+SpecialPowers.addPermission("contacts-create", true, document);
+
+var sample_id1;
+var createResult1;
+var findResult1;
+
+function onFailure() {
+  ok(false, "in on Failure!");
+}
+
+var prop = {
+  tel: [{value: "7932012345" }, {value: "7932012346"}]
+};
+
+var prop2 = {
+  tel: [{value: "01187654321" }]
+};
+
+var req;
+var index = 0;
+
+var mozContacts = window.navigator.mozContacts;
+ok(mozContacts, "mozContacts exists");
+ok("mozContact" in window, "mozContact exists");
+var steps = [
+  function () {
+    ok(true, "Adding contact");
+    createResult1 = new mozContact();
+    createResult1.init(prop);
+    req = navigator.mozContacts.save(createResult1);
+    req.onsuccess = function () {
+      ok(createResult1.id, "The contact now has an ID.");
+      sample_id1 = createResult1.id;
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving all contacts");
+    req = mozContacts.find({});
+    req.onsuccess = function () {
+      is(req.result.length, 1, "One contact.");
+      findResult1 = req.result[0];
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by substring 1");
+    var length = prop.tel[0].value.length;
+    var num = prop.tel[0].value.substring(length - substringLength, length);
+    var options = {filterBy: ["tel"],
+                   filterOp: "match",
+                   filterValue: num};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      ok(findResult1.id == sample_id1, "Same ID");
+      is(findResult1.tel[0].value, "7932012345", "Same Value");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by substring 2");
+    var length = prop.tel[1].value.length;
+    var num = prop.tel[1].value.substring(length - substringLength, length);
+    var options = {filterBy: ["tel"],
+                   filterOp: "match",
+                   filterValue: num};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      ok(findResult1.id == sample_id1, "Same ID");
+      is(findResult1.tel[0].value, "7932012345", "Same Value");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by substring 3");
+    var length = prop.tel[0].value.length;
+    var num = prop.tel[0].value.substring(length - substringLength + 1, length);
+    var options = {filterBy: ["tel"],
+                   filterOp: "match",
+                   filterValue: num};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 0, "Found exactly 0 contacts.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by substring 4");
+    var length = prop.tel[0].value.length;
+    var num = prop.tel[0].value.substring(length - substringLength - 1, length);
+    var options = {filterBy: ["tel"],
+                   filterOp: "match",
+                   filterValue: num};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contacts.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Adding contact");
+    createResult1 = new mozContact();
+    createResult1.init(prop2);
+    req = navigator.mozContacts.save(createResult1);
+    req.onsuccess = function () {
+      ok(createResult1.id, "The contact now has an ID.");
+      sample_id1 = createResult1.id;
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by substring 5");
+    var options = {filterBy: ["tel"],
+                   filterOp: "match",
+                   filterValue: "87654321"};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contacts.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by substring 6");
+    var options = {filterBy: ["tel"],
+                   filterOp: "match",
+                   filterValue: "01187654321"};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contacts.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by substring 7");
+    var options = {filterBy: ["tel"],
+                   filterOp: "match",
+                   filterValue: "909087654321"};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contacts.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by substring 8");
+    var options = {filterBy: ["tel"],
+                   filterOp: "match",
+                   filterValue: "0411187654321"};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contacts.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by substring 9");
+    var options = {filterBy: ["tel"],
+                   filterOp: "match",
+                   filterValue: "90411187654321"};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contacts.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving by substring 10");
+    var options = {filterBy: ["tel"],
+                   filterOp: "match",
+                   filterValue: "+551187654321"};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      is(req.result.length, 1, "Found exactly 1 contacts.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Deleting database");
+    req = mozContacts.clear()
+    req.onsuccess = function () {
+      ok(true, "Deleted the database");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "all done!\n");
+    SpecialPowers.setIntPref("dom.phonenumber.substringmatching.BR", -1);
+    SimpleTest.finish();
+  }
+];
+
+function next() {
+  ok(true, "Begin!");
+  if (index >= steps.length) {
+    ok(false, "Shouldn't get here!");
+    return;
+  }
+  try {
+    steps[index]();
+  } catch(ex) {
+    ok(false, "Caught exception", ex);
+  }
+  index += 1;
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(next);
+</script>
+</pre>
+</body>
+</html>
\ No newline at end of file
--- a/dom/phonenumberutils/PhoneNumberUtils.jsm
+++ b/dom/phonenumberutils/PhoneNumberUtils.jsm
@@ -25,17 +25,17 @@ this.PhoneNumberUtils = {
   //  1. See whether we have a network mcc
   //  2. If we don't have that, look for the simcard mcc
   //  3. TODO: If we don't have that or its 0 (not activated), pick up the last used mcc
   //  4. If we don't have, default to some mcc
 
   // mcc for Brasil
   _mcc: '724',
 
-  _getCountryName: function() {
+  getCountryName: function getCountryName() {
     let mcc;
     let countryName;
 
 #ifdef MOZ_B2G_RIL
     // Get network mcc
     if (mobileConnection.voiceConnectionInfo &&
         mobileConnection.voiceConnectionInfo.network) {
       mcc = mobileConnection.voiceConnectionInfo.network.mcc;
@@ -61,17 +61,17 @@ this.PhoneNumberUtils = {
 
     countryName = MCC_ISO3166_TABLE[mcc];
     if (DEBUG) debug("MCC: " + mcc + "countryName: " + countryName);
     return countryName;
   },
 
   parse: function(aNumber) {
     if (DEBUG) debug("call parse: " + aNumber);
-    let result = PhoneNumber.Parse(aNumber, this._getCountryName());
+    let result = PhoneNumber.Parse(aNumber, this.getCountryName());
     if (DEBUG) {
       if (result) {
         debug("InternationalFormat: " + result.internationalFormat);
         debug("InternationalNumber: " + result.internationalNumber);
         debug("NationalNumber: " + result.nationalNumber);
         debug("NationalFormat: " + result.nationalFormat);
       } else {
         debug("No result!\n");