dom/contacts/fallback/ContactDB.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/dom/contacts/fallback/ContactDB.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1402 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +"use strict";
     1.9 +
    1.10 +// Everything but "ContactDB" is only exported here for testing.
    1.11 +this.EXPORTED_SYMBOLS = ["ContactDB", "DB_NAME", "STORE_NAME", "SAVED_GETALL_STORE_NAME",
    1.12 +                         "REVISION_STORE", "DB_VERSION"];
    1.13 +
    1.14 +const DEBUG = false;
    1.15 +function debug(s) { dump("-*- ContactDB component: " + s + "\n"); }
    1.16 +
    1.17 +const Cu = Components.utils;
    1.18 +const Cc = Components.classes;
    1.19 +const Ci = Components.interfaces;
    1.20 +
    1.21 +Cu.import("resource://gre/modules/Services.jsm");
    1.22 +Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
    1.23 +Cu.import("resource://gre/modules/PhoneNumberUtils.jsm");
    1.24 +Cu.importGlobalProperties(["indexedDB"]);
    1.25 +
    1.26 +/* all exported symbols need to be bound to this on B2G - Bug 961777 */
    1.27 +this.DB_NAME = "contacts";
    1.28 +this.DB_VERSION = 20;
    1.29 +this.STORE_NAME = "contacts";
    1.30 +this.SAVED_GETALL_STORE_NAME = "getallcache";
    1.31 +const CHUNK_SIZE = 20;
    1.32 +this.REVISION_STORE = "revision";
    1.33 +const REVISION_KEY = "revision";
    1.34 +
    1.35 +function exportContact(aRecord) {
    1.36 +  if (aRecord) {
    1.37 +    delete aRecord.search;
    1.38 +  }
    1.39 +  return aRecord;
    1.40 +}
    1.41 +
    1.42 +function ContactDispatcher(aContacts, aFullContacts, aCallback, aNewTxn, aClearDispatcher, aFailureCb) {
    1.43 +  let nextIndex = 0;
    1.44 +
    1.45 +  let sendChunk;
    1.46 +  let count = 0;
    1.47 +  if (aFullContacts) {
    1.48 +    sendChunk = function() {
    1.49 +      try {
    1.50 +        let chunk = aContacts.splice(0, CHUNK_SIZE);
    1.51 +        if (chunk.length > 0) {
    1.52 +          aCallback(chunk);
    1.53 +        }
    1.54 +        if (aContacts.length === 0) {
    1.55 +          aCallback(null);
    1.56 +          aClearDispatcher();
    1.57 +        }
    1.58 +      } catch (e) {
    1.59 +        aClearDispatcher();
    1.60 +      }
    1.61 +    }
    1.62 +  } else {
    1.63 +    sendChunk = function() {
    1.64 +      try {
    1.65 +        let start = nextIndex;
    1.66 +        nextIndex += CHUNK_SIZE;
    1.67 +        let chunk = [];
    1.68 +        aNewTxn("readonly", STORE_NAME, function(txn, store) {
    1.69 +          for (let i = start; i < Math.min(start+CHUNK_SIZE, aContacts.length); ++i) {
    1.70 +            store.get(aContacts[i]).onsuccess = function(e) {
    1.71 +              chunk.push(exportContact(e.target.result));
    1.72 +              count++;
    1.73 +              if (count === aContacts.length) {
    1.74 +                aCallback(chunk);
    1.75 +                aCallback(null);
    1.76 +                aClearDispatcher();
    1.77 +              } else if (chunk.length === CHUNK_SIZE) {
    1.78 +                aCallback(chunk);
    1.79 +                chunk.length = 0;
    1.80 +              }
    1.81 +            }
    1.82 +          }
    1.83 +        }, null, function(errorMsg) {
    1.84 +          aFailureCb(errorMsg);
    1.85 +        });
    1.86 +      } catch (e) {
    1.87 +        aClearDispatcher();
    1.88 +      }
    1.89 +    }
    1.90 +  }
    1.91 +
    1.92 +  return {
    1.93 +    sendNow: function() {
    1.94 +      sendChunk();
    1.95 +    }
    1.96 +  };
    1.97 +}
    1.98 +
    1.99 +this.ContactDB = function ContactDB() {
   1.100 +  if (DEBUG) debug("Constructor");
   1.101 +};
   1.102 +
   1.103 +ContactDB.prototype = {
   1.104 +  __proto__: IndexedDBHelper.prototype,
   1.105 +
   1.106 +  _dispatcher: {},
   1.107 +
   1.108 +  useFastUpgrade: true,
   1.109 +
   1.110 +  upgradeSchema: function upgradeSchema(aTransaction, aDb, aOldVersion, aNewVersion) {
   1.111 +    let loadInitialContacts = function() {
   1.112 +      // Add default contacts
   1.113 +      let jsm = {};
   1.114 +      Cu.import("resource://gre/modules/FileUtils.jsm", jsm);
   1.115 +      Cu.import("resource://gre/modules/NetUtil.jsm", jsm);
   1.116 +      // Loading resource://app/defaults/contacts.json doesn't work because
   1.117 +      // contacts.json is not in the omnijar.
   1.118 +      // So we look for the app dir instead and go from here...
   1.119 +      let contactsFile = jsm.FileUtils.getFile("DefRt", ["contacts.json"], false);
   1.120 +      if (!contactsFile || (contactsFile && !contactsFile.exists())) {
   1.121 +        // For b2g desktop
   1.122 +        contactsFile = jsm.FileUtils.getFile("ProfD", ["contacts.json"], false);
   1.123 +        if (!contactsFile || (contactsFile && !contactsFile.exists())) {
   1.124 +          return;
   1.125 +        }
   1.126 +      }
   1.127 +
   1.128 +      let chan = jsm.NetUtil.newChannel(contactsFile);
   1.129 +      let stream = chan.open();
   1.130 +      // Obtain a converter to read from a UTF-8 encoded input stream.
   1.131 +      let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
   1.132 +                      .createInstance(Ci.nsIScriptableUnicodeConverter);
   1.133 +      converter.charset = "UTF-8";
   1.134 +      let rawstr = converter.ConvertToUnicode(jsm.NetUtil.readInputStreamToString(
   1.135 +                                              stream,
   1.136 +                                              stream.available()) || "");
   1.137 +      stream.close();
   1.138 +      let contacts;
   1.139 +      try {
   1.140 +        contacts = JSON.parse(rawstr);
   1.141 +      } catch(e) {
   1.142 +        if (DEBUG) debug("Error parsing " + contactsFile.path + " : " + e);
   1.143 +        return;
   1.144 +      }
   1.145 +
   1.146 +      let idService = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
   1.147 +      objectStore = aTransaction.objectStore(STORE_NAME);
   1.148 +
   1.149 +      for (let i = 0; i < contacts.length; i++) {
   1.150 +        let contact = {};
   1.151 +        contact.properties = contacts[i];
   1.152 +        contact.id = idService.generateUUID().toString().replace(/[{}-]/g, "");
   1.153 +        contact = this.makeImport(contact);
   1.154 +        this.updateRecordMetadata(contact);
   1.155 +        if (DEBUG) debug("import: " + JSON.stringify(contact));
   1.156 +        objectStore.put(contact);
   1.157 +      }
   1.158 +    }.bind(this);
   1.159 +
   1.160 +    function createFinalSchema() {
   1.161 +      if (DEBUG) debug("creating final schema");
   1.162 +      let objectStore = aDb.createObjectStore(STORE_NAME, {keyPath: "id"});
   1.163 +      objectStore.createIndex("familyName", "properties.familyName", { multiEntry: true });
   1.164 +      objectStore.createIndex("givenName",  "properties.givenName",  { multiEntry: true });
   1.165 +      objectStore.createIndex("name",      "properties.name",        { multiEntry: true });
   1.166 +      objectStore.createIndex("familyNameLowerCase", "search.familyName", { multiEntry: true });
   1.167 +      objectStore.createIndex("givenNameLowerCase",  "search.givenName",  { multiEntry: true });
   1.168 +      objectStore.createIndex("nameLowerCase",       "search.name",       { multiEntry: true });
   1.169 +      objectStore.createIndex("telLowerCase",        "search.tel",        { multiEntry: true });
   1.170 +      objectStore.createIndex("emailLowerCase",      "search.email",      { multiEntry: true });
   1.171 +      objectStore.createIndex("tel", "search.exactTel", { multiEntry: true });
   1.172 +      objectStore.createIndex("category", "properties.category", { multiEntry: true });
   1.173 +      objectStore.createIndex("email", "search.email", { multiEntry: true });
   1.174 +      objectStore.createIndex("telMatch", "search.parsedTel", {multiEntry: true});
   1.175 +      objectStore.createIndex("phoneticFamilyName", "properties.phoneticFamilyName", { multiEntry: true });
   1.176 +      objectStore.createIndex("phoneticGivenName", "properties.phoneticGivenName", { multiEntry: true });
   1.177 +      objectStore.createIndex("phoneticFamilyNameLowerCase", "search.phoneticFamilyName", { multiEntry: true });
   1.178 +      objectStore.createIndex("phoneticGivenNameLowerCase",  "search.phoneticGivenName",  { multiEntry: true });
   1.179 +      aDb.createObjectStore(SAVED_GETALL_STORE_NAME);
   1.180 +      aDb.createObjectStore(REVISION_STORE).put(0, REVISION_KEY);
   1.181 +    }
   1.182 +
   1.183 +    let valueUpgradeSteps = [];
   1.184 +
   1.185 +    function scheduleValueUpgrade(upgradeFunc) {
   1.186 +      var length = valueUpgradeSteps.push(upgradeFunc);
   1.187 +      if (DEBUG) debug("Scheduled a value upgrade function, index " + (length - 1));
   1.188 +    }
   1.189 +
   1.190 +    // We always output this debug line because it's useful and the noise ratio
   1.191 +    // very low.
   1.192 +    debug("upgrade schema from: " + aOldVersion + " to " + aNewVersion + " called!");
   1.193 +    let db = aDb;
   1.194 +    let objectStore;
   1.195 +
   1.196 +    if (aOldVersion === 0 && this.useFastUpgrade) {
   1.197 +      createFinalSchema();
   1.198 +      loadInitialContacts();
   1.199 +      return;
   1.200 +    }
   1.201 +
   1.202 +    let steps = [
   1.203 +      function upgrade0to1() {
   1.204 +        /**
   1.205 +         * Create the initial database schema.
   1.206 +         *
   1.207 +         * The schema of records stored is as follows:
   1.208 +         *
   1.209 +         * {id:            "...",       // UUID
   1.210 +         *  published:     Date(...),   // First published date.
   1.211 +         *  updated:       Date(...),   // Last updated date.
   1.212 +         *  properties:    {...}        // Object holding the ContactProperties
   1.213 +         * }
   1.214 +         */
   1.215 +        if (DEBUG) debug("create schema");
   1.216 +        objectStore = db.createObjectStore(STORE_NAME, {keyPath: "id"});
   1.217 +
   1.218 +        // Properties indexes
   1.219 +        objectStore.createIndex("familyName", "properties.familyName", { multiEntry: true });
   1.220 +        objectStore.createIndex("givenName",  "properties.givenName",  { multiEntry: true });
   1.221 +
   1.222 +        objectStore.createIndex("familyNameLowerCase", "search.familyName", { multiEntry: true });
   1.223 +        objectStore.createIndex("givenNameLowerCase",  "search.givenName",  { multiEntry: true });
   1.224 +        objectStore.createIndex("telLowerCase",        "search.tel",        { multiEntry: true });
   1.225 +        objectStore.createIndex("emailLowerCase",      "search.email",      { multiEntry: true });
   1.226 +        next();
   1.227 +      },
   1.228 +      function upgrade1to2() {
   1.229 +        if (DEBUG) debug("upgrade 1");
   1.230 +
   1.231 +        // Create a new scheme for the tel field. We move from an array of tel-numbers to an array of
   1.232 +        // ContactTelephone.
   1.233 +        if (!objectStore) {
   1.234 +          objectStore = aTransaction.objectStore(STORE_NAME);
   1.235 +        }
   1.236 +        // Delete old tel index.
   1.237 +        if (objectStore.indexNames.contains("tel")) {
   1.238 +          objectStore.deleteIndex("tel");
   1.239 +        }
   1.240 +
   1.241 +        // Upgrade existing tel field in the DB.
   1.242 +        objectStore.openCursor().onsuccess = function(event) {
   1.243 +          let cursor = event.target.result;
   1.244 +          if (cursor) {
   1.245 +            if (DEBUG) debug("upgrade tel1: " + JSON.stringify(cursor.value));
   1.246 +            for (let number in cursor.value.properties.tel) {
   1.247 +              cursor.value.properties.tel[number] = {number: number};
   1.248 +            }
   1.249 +            cursor.update(cursor.value);
   1.250 +            if (DEBUG) debug("upgrade tel2: " + JSON.stringify(cursor.value));
   1.251 +            cursor.continue();
   1.252 +          } else {
   1.253 +            next();
   1.254 +          }
   1.255 +        };
   1.256 +
   1.257 +        // Create new searchable indexes.
   1.258 +        objectStore.createIndex("tel", "search.tel", { multiEntry: true });
   1.259 +        objectStore.createIndex("category", "properties.category", { multiEntry: true });
   1.260 +      },
   1.261 +      function upgrade2to3() {
   1.262 +        if (DEBUG) debug("upgrade 2");
   1.263 +        // Create a new scheme for the email field. We move from an array of emailaddresses to an array of
   1.264 +        // ContactEmail.
   1.265 +        if (!objectStore) {
   1.266 +          objectStore = aTransaction.objectStore(STORE_NAME);
   1.267 +        }
   1.268 +
   1.269 +        // Delete old email index.
   1.270 +        if (objectStore.indexNames.contains("email")) {
   1.271 +          objectStore.deleteIndex("email");
   1.272 +        }
   1.273 +
   1.274 +        // Upgrade existing email field in the DB.
   1.275 +        objectStore.openCursor().onsuccess = function(event) {
   1.276 +          let cursor = event.target.result;
   1.277 +          if (cursor) {
   1.278 +            if (cursor.value.properties.email) {
   1.279 +              if (DEBUG) debug("upgrade email1: " + JSON.stringify(cursor.value));
   1.280 +              cursor.value.properties.email =
   1.281 +                cursor.value.properties.email.map(function(address) { return { address: address }; });
   1.282 +              cursor.update(cursor.value);
   1.283 +              if (DEBUG) debug("upgrade email2: " + JSON.stringify(cursor.value));
   1.284 +            }
   1.285 +            cursor.continue();
   1.286 +          } else {
   1.287 +            next();
   1.288 +          }
   1.289 +        };
   1.290 +
   1.291 +        // Create new searchable indexes.
   1.292 +        objectStore.createIndex("email", "search.email", { multiEntry: true });
   1.293 +      },
   1.294 +      function upgrade3to4() {
   1.295 +        if (DEBUG) debug("upgrade 3");
   1.296 +
   1.297 +        if (!objectStore) {
   1.298 +          objectStore = aTransaction.objectStore(STORE_NAME);
   1.299 +        }
   1.300 +
   1.301 +        // Upgrade existing impp field in the DB.
   1.302 +        objectStore.openCursor().onsuccess = function(event) {
   1.303 +          let cursor = event.target.result;
   1.304 +          if (cursor) {
   1.305 +            if (cursor.value.properties.impp) {
   1.306 +              if (DEBUG) debug("upgrade impp1: " + JSON.stringify(cursor.value));
   1.307 +              cursor.value.properties.impp =
   1.308 +                cursor.value.properties.impp.map(function(value) { return { value: value }; });
   1.309 +              cursor.update(cursor.value);
   1.310 +              if (DEBUG) debug("upgrade impp2: " + JSON.stringify(cursor.value));
   1.311 +            }
   1.312 +            cursor.continue();
   1.313 +          }
   1.314 +        };
   1.315 +        // Upgrade existing url field in the DB.
   1.316 +        objectStore.openCursor().onsuccess = function(event) {
   1.317 +          let cursor = event.target.result;
   1.318 +          if (cursor) {
   1.319 +            if (cursor.value.properties.url) {
   1.320 +              if (DEBUG) debug("upgrade url1: " + JSON.stringify(cursor.value));
   1.321 +              cursor.value.properties.url =
   1.322 +                cursor.value.properties.url.map(function(value) { return { value: value }; });
   1.323 +              cursor.update(cursor.value);
   1.324 +              if (DEBUG) debug("upgrade impp2: " + JSON.stringify(cursor.value));
   1.325 +            }
   1.326 +            cursor.continue();
   1.327 +          } else {
   1.328 +            next();
   1.329 +          }
   1.330 +        };
   1.331 +      },
   1.332 +      function upgrade4to5() {
   1.333 +        if (DEBUG) debug("Add international phone numbers upgrade");
   1.334 +        if (!objectStore) {
   1.335 +          objectStore = aTransaction.objectStore(STORE_NAME);
   1.336 +        }
   1.337 +
   1.338 +        objectStore.openCursor().onsuccess = function(event) {
   1.339 +          let cursor = event.target.result;
   1.340 +          if (cursor) {
   1.341 +            if (cursor.value.properties.tel) {
   1.342 +              if (DEBUG) debug("upgrade : " + JSON.stringify(cursor.value));
   1.343 +              cursor.value.properties.tel.forEach(
   1.344 +                function(duple) {
   1.345 +                  let parsedNumber = PhoneNumberUtils.parse(duple.value.toString());
   1.346 +                  if (parsedNumber) {
   1.347 +                    if (DEBUG) {
   1.348 +                      debug("InternationalFormat: " + parsedNumber.internationalFormat);
   1.349 +                      debug("InternationalNumber: " + parsedNumber.internationalNumber);
   1.350 +                      debug("NationalNumber: " + parsedNumber.nationalNumber);
   1.351 +                      debug("NationalFormat: " + parsedNumber.nationalFormat);
   1.352 +                    }
   1.353 +                    if (duple.value.toString() !== parsedNumber.internationalNumber) {
   1.354 +                      cursor.value.search.tel.push(parsedNumber.internationalNumber);
   1.355 +                    }
   1.356 +                  } else {
   1.357 +                    dump("Warning: No international number found for " + duple.value + "\n");
   1.358 +                  }
   1.359 +                }
   1.360 +              )
   1.361 +              cursor.update(cursor.value);
   1.362 +            }
   1.363 +            if (DEBUG) debug("upgrade2 : " + JSON.stringify(cursor.value));
   1.364 +            cursor.continue();
   1.365 +          } else {
   1.366 +            next();
   1.367 +          }
   1.368 +        };
   1.369 +      },
   1.370 +      function upgrade5to6() {
   1.371 +        if (DEBUG) debug("Add index for equals tel searches");
   1.372 +        if (!objectStore) {
   1.373 +          objectStore = aTransaction.objectStore(STORE_NAME);
   1.374 +        }
   1.375 +
   1.376 +        // Delete old tel index (not on the right field).
   1.377 +        if (objectStore.indexNames.contains("tel")) {
   1.378 +          objectStore.deleteIndex("tel");
   1.379 +        }
   1.380 +
   1.381 +        // Create new index for "equals" searches
   1.382 +        objectStore.createIndex("tel", "search.exactTel", { multiEntry: true });
   1.383 +
   1.384 +        objectStore.openCursor().onsuccess = function(event) {
   1.385 +          let cursor = event.target.result;
   1.386 +          if (cursor) {
   1.387 +            if (cursor.value.properties.tel) {
   1.388 +              if (DEBUG) debug("upgrade : " + JSON.stringify(cursor.value));
   1.389 +              cursor.value.properties.tel.forEach(
   1.390 +                function(duple) {
   1.391 +                  let number = duple.value.toString();
   1.392 +                  let parsedNumber = PhoneNumberUtils.parse(number);
   1.393 +
   1.394 +                  cursor.value.search.exactTel = [number];
   1.395 +                  if (parsedNumber &&
   1.396 +                      parsedNumber.internationalNumber &&
   1.397 +                      number !== parsedNumber.internationalNumber) {
   1.398 +                    cursor.value.search.exactTel.push(parsedNumber.internationalNumber);
   1.399 +                  }
   1.400 +                }
   1.401 +              )
   1.402 +              cursor.update(cursor.value);
   1.403 +            }
   1.404 +            if (DEBUG) debug("upgrade : " + JSON.stringify(cursor.value));
   1.405 +            cursor.continue();
   1.406 +          } else {
   1.407 +            next();
   1.408 +          }
   1.409 +        };
   1.410 +      },
   1.411 +      function upgrade6to7() {
   1.412 +        if (!objectStore) {
   1.413 +          objectStore = aTransaction.objectStore(STORE_NAME);
   1.414 +        }
   1.415 +        let names = objectStore.indexNames;
   1.416 +        let whiteList = ["tel", "familyName", "givenName",  "familyNameLowerCase",
   1.417 +                         "givenNameLowerCase", "telLowerCase", "category", "email",
   1.418 +                         "emailLowerCase"];
   1.419 +        for (var i = 0; i < names.length; i++) {
   1.420 +          if (whiteList.indexOf(names[i]) < 0) {
   1.421 +            objectStore.deleteIndex(names[i]);
   1.422 +          }
   1.423 +        }
   1.424 +        next();
   1.425 +      },
   1.426 +      function upgrade7to8() {
   1.427 +        if (DEBUG) debug("Adding object store for cached searches");
   1.428 +        db.createObjectStore(SAVED_GETALL_STORE_NAME);
   1.429 +        next();
   1.430 +      },
   1.431 +      function upgrade8to9() {
   1.432 +        if (DEBUG) debug("Make exactTel only contain the value entered by the user");
   1.433 +        if (!objectStore) {
   1.434 +          objectStore = aTransaction.objectStore(STORE_NAME);
   1.435 +        }
   1.436 +
   1.437 +        objectStore.openCursor().onsuccess = function(event) {
   1.438 +          let cursor = event.target.result;
   1.439 +          if (cursor) {
   1.440 +            if (cursor.value.properties.tel) {
   1.441 +              cursor.value.search.exactTel = [];
   1.442 +              cursor.value.properties.tel.forEach(
   1.443 +                function(tel) {
   1.444 +                  let normalized = PhoneNumberUtils.normalize(tel.value.toString());
   1.445 +                  cursor.value.search.exactTel.push(normalized);
   1.446 +                }
   1.447 +              );
   1.448 +              cursor.update(cursor.value);
   1.449 +            }
   1.450 +            cursor.continue();
   1.451 +          } else {
   1.452 +            next();
   1.453 +          }
   1.454 +        };
   1.455 +      },
   1.456 +      function upgrade9to10() {
   1.457 +        // no-op, see https://bugzilla.mozilla.org/show_bug.cgi?id=883770#c16
   1.458 +        next();
   1.459 +      },
   1.460 +      function upgrade10to11() {
   1.461 +        if (DEBUG) debug("Adding object store for database revision");
   1.462 +        db.createObjectStore(REVISION_STORE).put(0, REVISION_KEY);
   1.463 +        next();
   1.464 +      },
   1.465 +      function upgrade11to12() {
   1.466 +        if (DEBUG) debug("Add a telMatch index with national and international numbers");
   1.467 +        if (!objectStore) {
   1.468 +          objectStore = aTransaction.objectStore(STORE_NAME);
   1.469 +        }
   1.470 +        if (!objectStore.indexNames.contains("telMatch")) {
   1.471 +          objectStore.createIndex("telMatch", "search.parsedTel", {multiEntry: true});
   1.472 +        }
   1.473 +        objectStore.openCursor().onsuccess = function(event) {
   1.474 +          let cursor = event.target.result;
   1.475 +          if (cursor) {
   1.476 +            if (cursor.value.properties.tel) {
   1.477 +              cursor.value.search.parsedTel = [];
   1.478 +              cursor.value.properties.tel.forEach(
   1.479 +                function(tel) {
   1.480 +                  let parsed = PhoneNumberUtils.parse(tel.value.toString());
   1.481 +                  if (parsed) {
   1.482 +                    cursor.value.search.parsedTel.push(parsed.nationalNumber);
   1.483 +                    cursor.value.search.parsedTel.push(PhoneNumberUtils.normalize(parsed.nationalFormat));
   1.484 +                    cursor.value.search.parsedTel.push(parsed.internationalNumber);
   1.485 +                    cursor.value.search.parsedTel.push(PhoneNumberUtils.normalize(parsed.internationalFormat));
   1.486 +                  }
   1.487 +                  cursor.value.search.parsedTel.push(PhoneNumberUtils.normalize(tel.value.toString()));
   1.488 +                }
   1.489 +              );
   1.490 +              cursor.update(cursor.value);
   1.491 +            }
   1.492 +            cursor.continue();
   1.493 +          } else {
   1.494 +            next();
   1.495 +          }
   1.496 +        };
   1.497 +      },
   1.498 +      function upgrade12to13() {
   1.499 +        if (DEBUG) debug("Add phone substring to the search index if appropriate for country");
   1.500 +        if (this.substringMatching) {
   1.501 +          scheduleValueUpgrade(function upgradeValue12to13(value) {
   1.502 +            if (value.properties.tel) {
   1.503 +              value.search.parsedTel = value.search.parsedTel || [];
   1.504 +              value.properties.tel.forEach(
   1.505 +                function(tel) {
   1.506 +                  let normalized = PhoneNumberUtils.normalize(tel.value.toString());
   1.507 +                  if (normalized) {
   1.508 +                    if (this.substringMatching && normalized.length > this.substringMatching) {
   1.509 +                      let sub = normalized.slice(-this.substringMatching);
   1.510 +                      if (value.search.parsedTel.indexOf(sub) === -1) {
   1.511 +                        if (DEBUG) debug("Adding substring index: " + tel + ", " + sub);
   1.512 +                        value.search.parsedTel.push(sub);
   1.513 +                      }
   1.514 +                    }
   1.515 +                  }
   1.516 +                }.bind(this)
   1.517 +              );
   1.518 +              return true;
   1.519 +            } else {
   1.520 +              return false;
   1.521 +            }
   1.522 +          }.bind(this));
   1.523 +        }
   1.524 +        next();
   1.525 +      },
   1.526 +      function upgrade13to14() {
   1.527 +        if (DEBUG) debug("Cleaning up empty substring entries in telMatch index");
   1.528 +        scheduleValueUpgrade(function upgradeValue13to14(value) {
   1.529 +          function removeEmptyStrings(value) {
   1.530 +            if (value) {
   1.531 +              const oldLength = value.length;
   1.532 +              for (let i = 0; i < value.length; ++i) {
   1.533 +                if (!value[i] || value[i] == "null") {
   1.534 +                  value.splice(i, 1);
   1.535 +                }
   1.536 +              }
   1.537 +              return oldLength !== value.length;
   1.538 +            }
   1.539 +          }
   1.540 +
   1.541 +          let modified = removeEmptyStrings(value.search.parsedTel);
   1.542 +          let modified2 = removeEmptyStrings(value.search.tel);
   1.543 +          return (modified || modified2);
   1.544 +        });
   1.545 +
   1.546 +        next();
   1.547 +      },
   1.548 +      function upgrade14to15() {
   1.549 +        if (DEBUG) debug("Fix array properties saved as scalars");
   1.550 +        const ARRAY_PROPERTIES = ["photo", "adr", "email", "url", "impp", "tel",
   1.551 +                                 "name", "honorificPrefix", "givenName",
   1.552 +                                 "additionalName", "familyName", "honorificSuffix",
   1.553 +                                 "nickname", "category", "org", "jobTitle",
   1.554 +                                 "note", "key"];
   1.555 +        const PROPERTIES_WITH_TYPE = ["adr", "email", "url", "impp", "tel"];
   1.556 +
   1.557 +        scheduleValueUpgrade(function upgradeValue14to15(value) {
   1.558 +          let changed = false;
   1.559 +
   1.560 +          let props = value.properties;
   1.561 +          for (let prop of ARRAY_PROPERTIES) {
   1.562 +            if (props[prop]) {
   1.563 +              if (!Array.isArray(props[prop])) {
   1.564 +                value.properties[prop] = [props[prop]];
   1.565 +                changed = true;
   1.566 +              }
   1.567 +              if (PROPERTIES_WITH_TYPE.indexOf(prop) !== -1) {
   1.568 +                let subprop = value.properties[prop];
   1.569 +                for (let i = 0; i < subprop.length; ++i) {
   1.570 +                  if (!Array.isArray(subprop[i].type)) {
   1.571 +                    value.properties[prop][i].type = [subprop[i].type];
   1.572 +                    changed = true;
   1.573 +                  }
   1.574 +                }
   1.575 +              }
   1.576 +            }
   1.577 +          }
   1.578 +
   1.579 +          return changed;
   1.580 +        });
   1.581 +
   1.582 +        next();
   1.583 +      },
   1.584 +      function upgrade15to16() {
   1.585 +        if (DEBUG) debug("Fix Date properties");
   1.586 +        const DATE_PROPERTIES = ["bday", "anniversary"];
   1.587 +
   1.588 +        scheduleValueUpgrade(function upgradeValue15to16(value) {
   1.589 +          let changed = false;
   1.590 +          let props = value.properties;
   1.591 +          for (let prop of DATE_PROPERTIES) {
   1.592 +            if (props[prop] && !(props[prop] instanceof Date)) {
   1.593 +              value.properties[prop] = new Date(props[prop]);
   1.594 +              changed = true;
   1.595 +            }
   1.596 +          }
   1.597 +
   1.598 +          return changed;
   1.599 +        });
   1.600 +
   1.601 +        next();
   1.602 +      },
   1.603 +      function upgrade16to17() {
   1.604 +        if (DEBUG) debug("Fix array with null values");
   1.605 +        const ARRAY_PROPERTIES = ["photo", "adr", "email", "url", "impp", "tel",
   1.606 +                                 "name", "honorificPrefix", "givenName",
   1.607 +                                 "additionalName", "familyName", "honorificSuffix",
   1.608 +                                 "nickname", "category", "org", "jobTitle",
   1.609 +                                 "note", "key"];
   1.610 +
   1.611 +        const PROPERTIES_WITH_TYPE = ["adr", "email", "url", "impp", "tel"];
   1.612 +
   1.613 +        const DATE_PROPERTIES = ["bday", "anniversary"];
   1.614 +
   1.615 +        scheduleValueUpgrade(function upgradeValue16to17(value) {
   1.616 +          let changed;
   1.617 +
   1.618 +          function filterInvalidValues(val) {
   1.619 +            let shouldKeep = val != null; // null or undefined
   1.620 +            if (!shouldKeep) {
   1.621 +              changed = true;
   1.622 +            }
   1.623 +            return shouldKeep;
   1.624 +          }
   1.625 +
   1.626 +          function filteredArray(array) {
   1.627 +            return array.filter(filterInvalidValues);
   1.628 +          }
   1.629 +
   1.630 +          let props = value.properties;
   1.631 +
   1.632 +          for (let prop of ARRAY_PROPERTIES) {
   1.633 +
   1.634 +            // properties that were empty strings weren't converted to arrays
   1.635 +            // in upgrade14to15
   1.636 +            if (props[prop] != null && !Array.isArray(props[prop])) {
   1.637 +              props[prop] = [props[prop]];
   1.638 +              changed = true;
   1.639 +            }
   1.640 +
   1.641 +            if (props[prop] && props[prop].length) {
   1.642 +              props[prop] = filteredArray(props[prop]);
   1.643 +
   1.644 +              if (PROPERTIES_WITH_TYPE.indexOf(prop) !== -1) {
   1.645 +                let subprop = props[prop];
   1.646 +
   1.647 +                for (let i = 0; i < subprop.length; ++i) {
   1.648 +                  let curSubprop = subprop[i];
   1.649 +                  // upgrade14to15 transformed type props into an array
   1.650 +                  // without checking invalid values
   1.651 +                  if (curSubprop.type) {
   1.652 +                    curSubprop.type = filteredArray(curSubprop.type);
   1.653 +                  }
   1.654 +                }
   1.655 +              }
   1.656 +            }
   1.657 +          }
   1.658 +
   1.659 +          for (let prop of DATE_PROPERTIES) {
   1.660 +            if (props[prop] != null && !(props[prop] instanceof Date)) {
   1.661 +              // props[prop] is probably '' and wasn't converted
   1.662 +              // in upgrade15to16
   1.663 +              props[prop] = null;
   1.664 +              changed = true;
   1.665 +            }
   1.666 +          }
   1.667 +
   1.668 +          if (changed) {
   1.669 +            value.properties = props;
   1.670 +            return true;
   1.671 +          } else {
   1.672 +            return false;
   1.673 +          }
   1.674 +        });
   1.675 +
   1.676 +        next();
   1.677 +      },
   1.678 +      function upgrade17to18() {
   1.679 +        // this upgrade function has been moved to the next upgrade path because
   1.680 +        // a previous version of it had a bug
   1.681 +        next();
   1.682 +      },
   1.683 +      function upgrade18to19() {
   1.684 +        if (DEBUG) {
   1.685 +          debug("Adding the name index");
   1.686 +        }
   1.687 +
   1.688 +        if (!objectStore) {
   1.689 +          objectStore = aTransaction.objectStore(STORE_NAME);
   1.690 +        }
   1.691 +
   1.692 +        // an earlier version of this code could have run, so checking whether
   1.693 +        // the index exists
   1.694 +        if (!objectStore.indexNames.contains("name")) {
   1.695 +          objectStore.createIndex("name", "properties.name", { multiEntry: true });
   1.696 +          objectStore.createIndex("nameLowerCase", "search.name", { multiEntry: true });
   1.697 +        }
   1.698 +
   1.699 +        scheduleValueUpgrade(function upgradeValue18to19(value) {
   1.700 +          value.search.name = [];
   1.701 +          if (value.properties.name) {
   1.702 +            value.properties.name.forEach(function addNameIndex(name) {
   1.703 +              var lowerName = name.toLowerCase();
   1.704 +              // an earlier version of this code could have added it already
   1.705 +              if (value.search.name.indexOf(lowerName) === -1) {
   1.706 +                value.search.name.push(lowerName);
   1.707 +              }
   1.708 +            });
   1.709 +          }
   1.710 +          return true;
   1.711 +        });
   1.712 +
   1.713 +        next();
   1.714 +      },
   1.715 +      function upgrade19to20() {
   1.716 +        if (DEBUG) debug("upgrade19to20 create schema(phonetic)");
   1.717 +        if (!objectStore) {
   1.718 +          objectStore = aTransaction.objectStore(STORE_NAME);
   1.719 +        }
   1.720 +        objectStore.createIndex("phoneticFamilyName", "properties.phoneticFamilyName", { multiEntry: true });
   1.721 +        objectStore.createIndex("phoneticGivenName", "properties.phoneticGivenName", { multiEntry: true });
   1.722 +        objectStore.createIndex("phoneticFamilyNameLowerCase", "search.phoneticFamilyName", { multiEntry: true });
   1.723 +        objectStore.createIndex("phoneticGivenNameLowerCase",  "search.phoneticGivenName",  { multiEntry: true });
   1.724 +        next();
   1.725 +      },
   1.726 +    ];
   1.727 +
   1.728 +    let index = aOldVersion;
   1.729 +    let outer = this;
   1.730 +
   1.731 +    /* This function runs all upgrade functions that are in the
   1.732 +     * valueUpgradeSteps array. These functions have the following properties:
   1.733 +     * - they must be synchronous
   1.734 +     * - they must take the value as parameter and modify it directly. They
   1.735 +     *   must not create a new object.
   1.736 +     * - they must return a boolean true/false; true if the value was actually
   1.737 +     *   changed
   1.738 +     */
   1.739 +    function runValueUpgradeSteps(done) {
   1.740 +      if (DEBUG) debug("Running the value upgrade functions.");
   1.741 +      if (!objectStore) {
   1.742 +        objectStore = aTransaction.objectStore(STORE_NAME);
   1.743 +      }
   1.744 +      objectStore.openCursor().onsuccess = function(event) {
   1.745 +        let cursor = event.target.result;
   1.746 +        if (cursor) {
   1.747 +          let changed = false;
   1.748 +          let oldValue;
   1.749 +          let value = cursor.value;
   1.750 +          if (DEBUG) {
   1.751 +            oldValue = JSON.stringify(value);
   1.752 +          }
   1.753 +          valueUpgradeSteps.forEach(function(upgradeFunc, i) {
   1.754 +            if (DEBUG) debug("Running upgrade function " + i);
   1.755 +            changed = upgradeFunc(value) || changed;
   1.756 +          });
   1.757 +
   1.758 +          if (changed) {
   1.759 +            cursor.update(value);
   1.760 +          } else if (DEBUG) {
   1.761 +            let newValue = JSON.stringify(value);
   1.762 +            if (newValue !== oldValue) {
   1.763 +              // oops something went wrong
   1.764 +              debug("upgrade: `changed` was false and still the value changed! Aborting.");
   1.765 +              aTransaction.abort();
   1.766 +              return;
   1.767 +            }
   1.768 +          }
   1.769 +          cursor.continue();
   1.770 +        } else {
   1.771 +          done();
   1.772 +        }
   1.773 +      };
   1.774 +    }
   1.775 +
   1.776 +    function finish() {
   1.777 +      // We always output this debug line because it's useful and the noise ratio
   1.778 +      // very low.
   1.779 +      debug("Upgrade finished");
   1.780 +
   1.781 +      outer.incrementRevision(aTransaction);
   1.782 +    }
   1.783 +
   1.784 +    function next() {
   1.785 +      if (index == aNewVersion) {
   1.786 +        runValueUpgradeSteps(finish);
   1.787 +        return;
   1.788 +      }
   1.789 +
   1.790 +      try {
   1.791 +        var i = index++;
   1.792 +        if (DEBUG) debug("Upgrade step: " + i + "\n");
   1.793 +        steps[i].call(outer);
   1.794 +      } catch(ex) {
   1.795 +        dump("Caught exception" + ex);
   1.796 +        aTransaction.abort();
   1.797 +        return;
   1.798 +      }
   1.799 +    }
   1.800 +
   1.801 +    function fail(why) {
   1.802 +      why = why || "";
   1.803 +      if (this.error) {
   1.804 +        why += " (root cause: " + this.error.name + ")";
   1.805 +      }
   1.806 +
   1.807 +      debug("Contacts DB upgrade error: " + why);
   1.808 +      aTransaction.abort();
   1.809 +    }
   1.810 +
   1.811 +    if (aNewVersion > steps.length) {
   1.812 +      fail("No migration steps for the new version!");
   1.813 +    }
   1.814 +
   1.815 +    this.cpuLock = Cc["@mozilla.org/power/powermanagerservice;1"]
   1.816 +                     .getService(Ci.nsIPowerManagerService)
   1.817 +                     .newWakeLock("cpu");
   1.818 +
   1.819 +    function unlockCPU() {
   1.820 +      if (outer.cpuLock) {
   1.821 +        if (DEBUG) debug("unlocking cpu wakelock");
   1.822 +        outer.cpuLock.unlock();
   1.823 +        outer.cpuLock = null;
   1.824 +      }
   1.825 +    }
   1.826 +
   1.827 +    aTransaction.addEventListener("complete", unlockCPU);
   1.828 +    aTransaction.addEventListener("abort", unlockCPU);
   1.829 +
   1.830 +    next();
   1.831 +  },
   1.832 +
   1.833 +  makeImport: function makeImport(aContact) {
   1.834 +    let contact = {properties: {}};
   1.835 +
   1.836 +    contact.search = {
   1.837 +      name:            [],
   1.838 +      givenName:       [],
   1.839 +      familyName:      [],
   1.840 +      email:           [],
   1.841 +      category:        [],
   1.842 +      tel:             [],
   1.843 +      exactTel:        [],
   1.844 +      parsedTel:       [],
   1.845 +      phoneticFamilyName:   [],
   1.846 +      phoneticGivenName:    [],
   1.847 +    };
   1.848 +
   1.849 +    for (let field in aContact.properties) {
   1.850 +      contact.properties[field] = aContact.properties[field];
   1.851 +      // Add search fields
   1.852 +      if (aContact.properties[field] && contact.search[field]) {
   1.853 +        for (let i = 0; i <= aContact.properties[field].length; i++) {
   1.854 +          if (aContact.properties[field][i]) {
   1.855 +            if (field == "tel" && aContact.properties[field][i].value) {
   1.856 +              let number = aContact.properties.tel[i].value.toString();
   1.857 +              let normalized = PhoneNumberUtils.normalize(number);
   1.858 +              // We use an object here to avoid duplicates
   1.859 +              let containsSearch = {};
   1.860 +              let matchSearch = {};
   1.861 +
   1.862 +              if (normalized) {
   1.863 +                // exactTel holds normalized version of entered phone number.
   1.864 +                // normalized: +1 (949) 123 - 4567 -> +19491234567
   1.865 +                contact.search.exactTel.push(normalized);
   1.866 +                // matchSearch holds normalized version of entered phone number,
   1.867 +                // nationalNumber, nationalFormat, internationalNumber, internationalFormat
   1.868 +                matchSearch[normalized] = 1;
   1.869 +                let parsedNumber = PhoneNumberUtils.parse(number);
   1.870 +                if (parsedNumber) {
   1.871 +                  if (DEBUG) {
   1.872 +                    debug("InternationalFormat: " + parsedNumber.internationalFormat);
   1.873 +                    debug("InternationalNumber: " + parsedNumber.internationalNumber);
   1.874 +                    debug("NationalNumber: " + parsedNumber.nationalNumber);
   1.875 +                    debug("NationalFormat: " + parsedNumber.nationalFormat);
   1.876 +                    debug("NationalMatchingFormat: " + parsedNumber.nationalMatchingFormat);
   1.877 +                  }
   1.878 +                  matchSearch[parsedNumber.nationalNumber] = 1;
   1.879 +                  matchSearch[parsedNumber.internationalNumber] = 1;
   1.880 +                  matchSearch[PhoneNumberUtils.normalize(parsedNumber.nationalFormat)] = 1;
   1.881 +                  matchSearch[PhoneNumberUtils.normalize(parsedNumber.internationalFormat)] = 1;
   1.882 +                  matchSearch[PhoneNumberUtils.normalize(parsedNumber.nationalMatchingFormat)] = 1;
   1.883 +                } else if (this.substringMatching && normalized.length > this.substringMatching) {
   1.884 +                  matchSearch[normalized.slice(-this.substringMatching)] = 1;
   1.885 +                }
   1.886 +
   1.887 +                // containsSearch holds incremental search values for:
   1.888 +                // normalized number and national format
   1.889 +                for (let i = 0; i < normalized.length; i++) {
   1.890 +                  containsSearch[normalized.substring(i, normalized.length)] = 1;
   1.891 +                }
   1.892 +                if (parsedNumber && parsedNumber.nationalFormat) {
   1.893 +                  let number = PhoneNumberUtils.normalize(parsedNumber.nationalFormat);
   1.894 +                  for (let i = 0; i < number.length; i++) {
   1.895 +                    containsSearch[number.substring(i, number.length)] = 1;
   1.896 +                  }
   1.897 +                }
   1.898 +              }
   1.899 +              for (let num in containsSearch) {
   1.900 +                if (num && num != "null") {
   1.901 +                  contact.search.tel.push(num);
   1.902 +                }
   1.903 +              }
   1.904 +              for (let num in matchSearch) {
   1.905 +                if (num && num != "null") {
   1.906 +                  contact.search.parsedTel.push(num);
   1.907 +                }
   1.908 +              }
   1.909 +            } else if ((field == "impp" || field == "email") && aContact.properties[field][i].value) {
   1.910 +              let value = aContact.properties[field][i].value;
   1.911 +              if (value && typeof value == "string") {
   1.912 +                contact.search[field].push(value.toLowerCase());
   1.913 +              }
   1.914 +            } else {
   1.915 +              let val = aContact.properties[field][i];
   1.916 +              if (typeof val == "string") {
   1.917 +                contact.search[field].push(val.toLowerCase());
   1.918 +              }
   1.919 +            }
   1.920 +          }
   1.921 +        }
   1.922 +      }
   1.923 +    }
   1.924 +
   1.925 +    contact.updated = aContact.updated;
   1.926 +    contact.published = aContact.published;
   1.927 +    contact.id = aContact.id;
   1.928 +
   1.929 +    return contact;
   1.930 +  },
   1.931 +
   1.932 +  updateRecordMetadata: function updateRecordMetadata(record) {
   1.933 +    if (!record.id) {
   1.934 +      Cu.reportError("Contact without ID");
   1.935 +    }
   1.936 +    if (!record.published) {
   1.937 +      record.published = new Date();
   1.938 +    }
   1.939 +    record.updated = new Date();
   1.940 +  },
   1.941 +
   1.942 +  removeObjectFromCache: function CDB_removeObjectFromCache(aObjectId, aCallback, aFailureCb) {
   1.943 +    if (DEBUG) debug("removeObjectFromCache: " + aObjectId);
   1.944 +    if (!aObjectId) {
   1.945 +      if (DEBUG) debug("No object ID passed");
   1.946 +      return;
   1.947 +    }
   1.948 +    this.newTxn("readwrite", SAVED_GETALL_STORE_NAME, function(txn, store) {
   1.949 +      store.openCursor().onsuccess = function(e) {
   1.950 +        let cursor = e.target.result;
   1.951 +        if (cursor) {
   1.952 +          for (let i = 0; i < cursor.value.length; ++i) {
   1.953 +            if (cursor.value[i] == aObjectId) {
   1.954 +              if (DEBUG) debug("id matches cache");
   1.955 +              cursor.value.splice(i, 1);
   1.956 +              cursor.update(cursor.value);
   1.957 +              break;
   1.958 +            }
   1.959 +          }
   1.960 +          cursor.continue();
   1.961 +        } else {
   1.962 +          aCallback();
   1.963 +        }
   1.964 +      }.bind(this);
   1.965 +    }.bind(this), null,
   1.966 +    function(errorMsg) {
   1.967 +      aFailureCb(errorMsg);
   1.968 +    });
   1.969 +  },
   1.970 +
   1.971 +  // Invalidate the entire cache. It will be incrementally regenerated on demand
   1.972 +  // See getCacheForQuery
   1.973 +  invalidateCache: function CDB_invalidateCache(aErrorCb) {
   1.974 +    if (DEBUG) debug("invalidate cache");
   1.975 +    this.newTxn("readwrite", SAVED_GETALL_STORE_NAME, function (txn, store) {
   1.976 +      store.clear();
   1.977 +    }, aErrorCb);
   1.978 +  },
   1.979 +
   1.980 +  incrementRevision: function CDB_incrementRevision(txn) {
   1.981 +    let revStore = txn.objectStore(REVISION_STORE);
   1.982 +    revStore.get(REVISION_KEY).onsuccess = function(e) {
   1.983 +      revStore.put(parseInt(e.target.result, 10) + 1, REVISION_KEY);
   1.984 +    };
   1.985 +  },
   1.986 +
   1.987 +  saveContact: function CDB_saveContact(aContact, successCb, errorCb) {
   1.988 +    let contact = this.makeImport(aContact);
   1.989 +    this.newTxn("readwrite", STORE_NAME, function (txn, store) {
   1.990 +      if (DEBUG) debug("Going to update" + JSON.stringify(contact));
   1.991 +
   1.992 +      // Look up the existing record and compare the update timestamp.
   1.993 +      // If no record exists, just add the new entry.
   1.994 +      let newRequest = store.get(contact.id);
   1.995 +      newRequest.onsuccess = function (event) {
   1.996 +        if (!event.target.result) {
   1.997 +          if (DEBUG) debug("new record!")
   1.998 +          this.updateRecordMetadata(contact);
   1.999 +          store.put(contact);
  1.1000 +        } else {
  1.1001 +          if (DEBUG) debug("old record!")
  1.1002 +          if (new Date(typeof contact.updated === "undefined" ? 0 : contact.updated) < new Date(event.target.result.updated)) {
  1.1003 +            if (DEBUG) debug("rev check fail!");
  1.1004 +            txn.abort();
  1.1005 +            return;
  1.1006 +          } else {
  1.1007 +            if (DEBUG) debug("rev check OK");
  1.1008 +            contact.published = event.target.result.published;
  1.1009 +            contact.updated = new Date();
  1.1010 +            store.put(contact);
  1.1011 +          }
  1.1012 +        }
  1.1013 +        this.invalidateCache(errorCb);
  1.1014 +      }.bind(this);
  1.1015 +
  1.1016 +      this.incrementRevision(txn);
  1.1017 +    }.bind(this), successCb, errorCb);
  1.1018 +  },
  1.1019 +
  1.1020 +  removeContact: function removeContact(aId, aSuccessCb, aErrorCb) {
  1.1021 +    if (DEBUG) debug("removeContact: " + aId);
  1.1022 +    this.removeObjectFromCache(aId, function() {
  1.1023 +      this.newTxn("readwrite", STORE_NAME, function(txn, store) {
  1.1024 +        store.delete(aId).onsuccess = function() {
  1.1025 +          aSuccessCb();
  1.1026 +        };
  1.1027 +        this.incrementRevision(txn);
  1.1028 +      }.bind(this), null, aErrorCb);
  1.1029 +    }.bind(this), aErrorCb);
  1.1030 +  },
  1.1031 +
  1.1032 +  clear: function clear(aSuccessCb, aErrorCb) {
  1.1033 +    this.newTxn("readwrite", STORE_NAME, function (txn, store) {
  1.1034 +      if (DEBUG) debug("Going to clear all!");
  1.1035 +      store.clear();
  1.1036 +      this.incrementRevision(txn);
  1.1037 +    }.bind(this), aSuccessCb, aErrorCb);
  1.1038 +  },
  1.1039 +
  1.1040 +  createCacheForQuery: function CDB_createCacheForQuery(aQuery, aSuccessCb, aFailureCb) {
  1.1041 +    this.find(function (aContacts) {
  1.1042 +      if (aContacts) {
  1.1043 +        let contactsArray = [];
  1.1044 +        for (let i in aContacts) {
  1.1045 +          contactsArray.push(aContacts[i]);
  1.1046 +        }
  1.1047 +
  1.1048 +        // save contact ids in cache
  1.1049 +        this.newTxn("readwrite", SAVED_GETALL_STORE_NAME, function(txn, store) {
  1.1050 +          store.put(contactsArray.map(function(el) el.id), aQuery);
  1.1051 +        }, null, aFailureCb);
  1.1052 +
  1.1053 +        // send full contacts
  1.1054 +        aSuccessCb(contactsArray, true);
  1.1055 +      } else {
  1.1056 +        aSuccessCb([], true);
  1.1057 +      }
  1.1058 +    }.bind(this),
  1.1059 +    function (aErrorMsg) { aFailureCb(aErrorMsg); },
  1.1060 +    JSON.parse(aQuery));
  1.1061 +  },
  1.1062 +
  1.1063 +  getCacheForQuery: function CDB_getCacheForQuery(aQuery, aSuccessCb, aFailureCb) {
  1.1064 +    if (DEBUG) debug("getCacheForQuery");
  1.1065 +    // Here we try to get the cached results for query `aQuery'. If they don't
  1.1066 +    // exist, it means the cache was invalidated and needs to be recreated, so
  1.1067 +    // we do that. Otherwise, we just return the existing cache.
  1.1068 +    this.newTxn("readonly", SAVED_GETALL_STORE_NAME, function(txn, store) {
  1.1069 +      let req = store.get(aQuery);
  1.1070 +      req.onsuccess = function(e) {
  1.1071 +        if (e.target.result) {
  1.1072 +          if (DEBUG) debug("cache exists");
  1.1073 +          aSuccessCb(e.target.result, false);
  1.1074 +        } else {
  1.1075 +          if (DEBUG) debug("creating cache for query " + aQuery);
  1.1076 +          this.createCacheForQuery(aQuery, aSuccessCb);
  1.1077 +        }
  1.1078 +      }.bind(this);
  1.1079 +      req.onerror = function(e) {
  1.1080 +        aFailureCb(e.target.errorMessage);
  1.1081 +      };
  1.1082 +    }.bind(this), null, aFailureCb);
  1.1083 +  },
  1.1084 +
  1.1085 +  sendNow: function CDB_sendNow(aCursorId) {
  1.1086 +    if (aCursorId in this._dispatcher) {
  1.1087 +      this._dispatcher[aCursorId].sendNow();
  1.1088 +    }
  1.1089 +  },
  1.1090 +
  1.1091 +  clearDispatcher: function CDB_clearDispatcher(aCursorId) {
  1.1092 +    if (DEBUG) debug("clearDispatcher: " + aCursorId);
  1.1093 +    if (aCursorId in this._dispatcher) {
  1.1094 +      delete this._dispatcher[aCursorId];
  1.1095 +    }
  1.1096 +  },
  1.1097 +
  1.1098 +  getAll: function CDB_getAll(aSuccessCb, aFailureCb, aOptions, aCursorId) {
  1.1099 +    if (DEBUG) debug("getAll")
  1.1100 +    let optionStr = JSON.stringify(aOptions);
  1.1101 +    this.getCacheForQuery(optionStr, function(aCachedResults, aFullContacts) {
  1.1102 +      // aFullContacts is true if the cache didn't exist and had to be created.
  1.1103 +      // In that case, we receive the full contacts since we already have them
  1.1104 +      // in memory to create the cache. This allows us to avoid accessing the
  1.1105 +      // object store again.
  1.1106 +      if (aCachedResults && aCachedResults.length > 0) {
  1.1107 +        let newTxnFn = this.newTxn.bind(this);
  1.1108 +        let clearDispatcherFn = this.clearDispatcher.bind(this, aCursorId);
  1.1109 +        this._dispatcher[aCursorId] = new ContactDispatcher(aCachedResults, aFullContacts,
  1.1110 +                                                            aSuccessCb, newTxnFn,
  1.1111 +                                                            clearDispatcherFn, aFailureCb);
  1.1112 +        this._dispatcher[aCursorId].sendNow();
  1.1113 +      } else { // no contacts
  1.1114 +        if (DEBUG) debug("query returned no contacts");
  1.1115 +        aSuccessCb(null);
  1.1116 +      }
  1.1117 +    }.bind(this), aFailureCb);
  1.1118 +  },
  1.1119 +
  1.1120 +  getRevision: function CDB_getRevision(aSuccessCb, aErrorCb) {
  1.1121 +    if (DEBUG) debug("getRevision");
  1.1122 +    this.newTxn("readonly", REVISION_STORE, function (txn, store) {
  1.1123 +      store.get(REVISION_KEY).onsuccess = function (e) {
  1.1124 +        aSuccessCb(e.target.result);
  1.1125 +      };
  1.1126 +    },null, aErrorCb);
  1.1127 +  },
  1.1128 +
  1.1129 +  getCount: function CDB_getCount(aSuccessCb, aErrorCb) {
  1.1130 +    if (DEBUG) debug("getCount");
  1.1131 +    this.newTxn("readonly", STORE_NAME, function (txn, store) {
  1.1132 +      store.count().onsuccess = function (e) {
  1.1133 +        aSuccessCb(e.target.result);
  1.1134 +      };
  1.1135 +    }, null, aErrorCb);
  1.1136 +  },
  1.1137 +
  1.1138 +  getSortByParam: function CDB_getSortByParam(aFindOptions) {
  1.1139 +    switch (aFindOptions.sortBy) {
  1.1140 +      case "familyName":
  1.1141 +        return [ "familyName", "givenName" ];
  1.1142 +      case "givenName":
  1.1143 +        return [ "givenName" , "familyName" ];
  1.1144 +      case "phoneticFamilyName":
  1.1145 +        return [ "phoneticFamilyName" , "phoneticGivenName" ];
  1.1146 +      case "phoneticGivenName":
  1.1147 +        return [ "phoneticGivenName" , "phoneticFamilyName" ];
  1.1148 +      default:
  1.1149 +        return [ "givenName" , "familyName" ];
  1.1150 +    }
  1.1151 +  },
  1.1152 +
  1.1153 +  /*
  1.1154 +   * Sorting the contacts by sortBy field. aSortBy can either be familyName or givenName.
  1.1155 +   * If 2 entries have the same sortyBy field or no sortBy field is present, we continue
  1.1156 +   * sorting with the other sortyBy field.
  1.1157 +   */
  1.1158 +  sortResults: function CDB_sortResults(aResults, aFindOptions) {
  1.1159 +    if (!aFindOptions)
  1.1160 +      return;
  1.1161 +    if (aFindOptions.sortBy != "undefined") {
  1.1162 +      const sortOrder = aFindOptions.sortOrder;
  1.1163 +      const sortBy = this.getSortByParam(aFindOptions);
  1.1164 +
  1.1165 +      aResults.sort(function (a, b) {
  1.1166 +        let x, y;
  1.1167 +        let result = 0;
  1.1168 +        let xIndex = 0;
  1.1169 +        let yIndex = 0;
  1.1170 +
  1.1171 +        do {
  1.1172 +          while (xIndex < sortBy.length && !x) {
  1.1173 +            x = a.properties[sortBy[xIndex]];
  1.1174 +            if (x) {
  1.1175 +              x = x.join("").toLowerCase();
  1.1176 +            }
  1.1177 +            xIndex++;
  1.1178 +          }
  1.1179 +          while (yIndex < sortBy.length && !y) {
  1.1180 +            y = b.properties[sortBy[yIndex]];
  1.1181 +            if (y) {
  1.1182 +              y = y.join("").toLowerCase();
  1.1183 +            }
  1.1184 +            yIndex++;
  1.1185 +          }
  1.1186 +          if (!x) {
  1.1187 +            if (!y) {
  1.1188 +              let px, py;
  1.1189 +              px = JSON.stringify(a.published);
  1.1190 +              py = JSON.stringify(b.published);
  1.1191 +              if (px && py) {
  1.1192 +                return px.localeCompare(py);
  1.1193 +              }
  1.1194 +            } else {
  1.1195 +              return sortOrder == 'descending' ? 1 : -1;
  1.1196 +            }
  1.1197 +          }
  1.1198 +          if (!y) {
  1.1199 +            return sortOrder == "ascending" ? 1 : -1;
  1.1200 +          }
  1.1201 +
  1.1202 +          result = x.localeCompare(y);
  1.1203 +          x = null;
  1.1204 +          y = null;
  1.1205 +        } while (result == 0);
  1.1206 +
  1.1207 +        return sortOrder == "ascending" ? result : -result;
  1.1208 +      });
  1.1209 +    }
  1.1210 +    if (aFindOptions.filterLimit && aFindOptions.filterLimit != 0) {
  1.1211 +      if (DEBUG) debug("filterLimit is set: " + aFindOptions.filterLimit);
  1.1212 +      aResults.splice(aFindOptions.filterLimit, aResults.length);
  1.1213 +    }
  1.1214 +  },
  1.1215 +
  1.1216 +  /**
  1.1217 +   * @param successCb
  1.1218 +   *        Callback function to invoke with result array.
  1.1219 +   * @param failureCb [optional]
  1.1220 +   *        Callback function to invoke when there was an error.
  1.1221 +   * @param options [optional]
  1.1222 +   *        Object specifying search options. Possible attributes:
  1.1223 +   *        - filterBy
  1.1224 +   *        - filterOp
  1.1225 +   *        - filterValue
  1.1226 +   *        - count
  1.1227 +   */
  1.1228 +  find: function find(aSuccessCb, aFailureCb, aOptions) {
  1.1229 +    if (DEBUG) debug("ContactDB:find val:" + aOptions.filterValue + " by: " + aOptions.filterBy + " op: " + aOptions.filterOp);
  1.1230 +    let self = this;
  1.1231 +    this.newTxn("readonly", STORE_NAME, function (txn, store) {
  1.1232 +      let filterOps = ["equals", "contains", "match", "startsWith"];
  1.1233 +      if (aOptions && (filterOps.indexOf(aOptions.filterOp) >= 0)) {
  1.1234 +        self._findWithIndex(txn, store, aOptions);
  1.1235 +      } else {
  1.1236 +        self._findAll(txn, store, aOptions);
  1.1237 +      }
  1.1238 +    }, aSuccessCb, aFailureCb);
  1.1239 +  },
  1.1240 +
  1.1241 +  _findWithIndex: function _findWithIndex(txn, store, options) {
  1.1242 +    if (DEBUG) debug("_findWithIndex: " + options.filterValue +" " + options.filterOp + " " + options.filterBy + " ");
  1.1243 +    let fields = options.filterBy;
  1.1244 +    for (let key in fields) {
  1.1245 +      if (DEBUG) debug("key: " + fields[key]);
  1.1246 +      if (!store.indexNames.contains(fields[key]) && fields[key] != "id") {
  1.1247 +        if (DEBUG) debug("Key not valid!" + fields[key] + ", " + JSON.stringify(store.indexNames));
  1.1248 +        txn.abort();
  1.1249 +        return;
  1.1250 +      }
  1.1251 +    }
  1.1252 +
  1.1253 +    // lookup for all keys
  1.1254 +    if (options.filterBy.length == 0) {
  1.1255 +      if (DEBUG) debug("search in all fields!" + JSON.stringify(store.indexNames));
  1.1256 +      for(let myIndex = 0; myIndex < store.indexNames.length; myIndex++) {
  1.1257 +        fields = Array.concat(fields, store.indexNames[myIndex])
  1.1258 +      }
  1.1259 +    }
  1.1260 +
  1.1261 +    // Sorting functions takes care of limit if set.
  1.1262 +    let limit = options.sortBy === 'undefined' ? options.filterLimit : null;
  1.1263 +
  1.1264 +    let filter_keys = fields.slice();
  1.1265 +    for (let key = filter_keys.shift(); key; key = filter_keys.shift()) {
  1.1266 +      let request;
  1.1267 +      let substringResult = {};
  1.1268 +      if (key == "id") {
  1.1269 +        // store.get would return an object and not an array
  1.1270 +        request = store.mozGetAll(options.filterValue);
  1.1271 +      } else if (key == "category") {
  1.1272 +        let index = store.index(key);
  1.1273 +        request = index.mozGetAll(options.filterValue, limit);
  1.1274 +      } else if (options.filterOp == "equals") {
  1.1275 +        if (DEBUG) debug("Getting index: " + key);
  1.1276 +        // case sensitive
  1.1277 +        let index = store.index(key);
  1.1278 +        let filterValue = options.filterValue;
  1.1279 +        if (key == "tel") {
  1.1280 +          filterValue = PhoneNumberUtils.normalize(filterValue,
  1.1281 +                                                   /*numbersOnly*/ true);
  1.1282 +        }
  1.1283 +        request = index.mozGetAll(filterValue, limit);
  1.1284 +      } else if (options.filterOp == "match") {
  1.1285 +        if (DEBUG) debug("match");
  1.1286 +        if (key != "tel") {
  1.1287 +          dump("ContactDB: 'match' filterOp only works on tel\n");
  1.1288 +          return txn.abort();
  1.1289 +        }
  1.1290 +
  1.1291 +        let index = store.index("telMatch");
  1.1292 +        let normalized = PhoneNumberUtils.normalize(options.filterValue,
  1.1293 +                                                    /*numbersOnly*/ true);
  1.1294 +
  1.1295 +        if (!normalized.length) {
  1.1296 +          dump("ContactDB: normalized filterValue is empty, can't perform match search.\n");
  1.1297 +          return txn.abort();
  1.1298 +        }
  1.1299 +
  1.1300 +        // Some countries need special handling for number matching. Bug 877302
  1.1301 +        if (this.substringMatching && normalized.length > this.substringMatching) {
  1.1302 +          let substring = normalized.slice(-this.substringMatching);
  1.1303 +          if (DEBUG) debug("Substring: " + substring);
  1.1304 +
  1.1305 +          let substringRequest = index.mozGetAll(substring, limit);
  1.1306 +
  1.1307 +          substringRequest.onsuccess = function (event) {
  1.1308 +            if (DEBUG) debug("Request successful. Record count: " + event.target.result.length);
  1.1309 +            for (let i in event.target.result) {
  1.1310 +              substringResult[event.target.result[i].id] = event.target.result[i];
  1.1311 +            }
  1.1312 +          }.bind(this);
  1.1313 +        } else if (normalized[0] !== "+") {
  1.1314 +          // We might have an international prefix like '00'
  1.1315 +          let parsed = PhoneNumberUtils.parse(normalized);
  1.1316 +          if (parsed && parsed.internationalNumber &&
  1.1317 +              parsed.nationalNumber  &&
  1.1318 +              parsed.nationalNumber !== normalized &&
  1.1319 +              parsed.internationalNumber !== normalized) {
  1.1320 +            if (DEBUG) debug("Search with " + parsed.internationalNumber);
  1.1321 +            let prefixRequest = index.mozGetAll(parsed.internationalNumber, limit);
  1.1322 +
  1.1323 +            prefixRequest.onsuccess = function (event) {
  1.1324 +              if (DEBUG) debug("Request successful. Record count: " + event.target.result.length);
  1.1325 +              for (let i in event.target.result) {
  1.1326 +                substringResult[event.target.result[i].id] = event.target.result[i];
  1.1327 +              }
  1.1328 +            }.bind(this);
  1.1329 +          }
  1.1330 +        }
  1.1331 +
  1.1332 +        request = index.mozGetAll(normalized, limit);
  1.1333 +      } else {
  1.1334 +        // XXX: "contains" should be handled separately, this is "startsWith"
  1.1335 +        if (options.filterOp === 'contains' && key !== 'tel') {
  1.1336 +          dump("ContactDB: 'contains' only works for 'tel'. Falling back " +
  1.1337 +               "to 'startsWith'.\n");
  1.1338 +        }
  1.1339 +        // not case sensitive
  1.1340 +        let lowerCase = options.filterValue.toString().toLowerCase();
  1.1341 +        if (key === "tel") {
  1.1342 +          let origLength = lowerCase.length;
  1.1343 +          let tmp = PhoneNumberUtils.normalize(lowerCase, /*numbersOnly*/ true);
  1.1344 +          if (tmp.length != origLength) {
  1.1345 +            let NON_SEARCHABLE_CHARS = /[^#+\*\d\s()-]/;
  1.1346 +            // e.g. number "123". find with "(123)" but not with "123a"
  1.1347 +            if (tmp === "" || NON_SEARCHABLE_CHARS.test(lowerCase)) {
  1.1348 +              if (DEBUG) debug("Call continue!");
  1.1349 +              continue;
  1.1350 +            }
  1.1351 +            lowerCase = tmp;
  1.1352 +          }
  1.1353 +        }
  1.1354 +        if (DEBUG) debug("lowerCase: " + lowerCase);
  1.1355 +        let range = IDBKeyRange.bound(lowerCase, lowerCase + "\uFFFF");
  1.1356 +        let index = store.index(key + "LowerCase");
  1.1357 +        request = index.mozGetAll(range, limit);
  1.1358 +      }
  1.1359 +      if (!txn.result)
  1.1360 +        txn.result = {};
  1.1361 +
  1.1362 +      request.onsuccess = function (event) {
  1.1363 +        if (DEBUG) debug("Request successful. Record count: " + event.target.result.length);
  1.1364 +        if (Object.keys(substringResult).length > 0) {
  1.1365 +          for (let attrname in substringResult) {
  1.1366 +            event.target.result[attrname] = substringResult[attrname];
  1.1367 +          }
  1.1368 +        }
  1.1369 +        this.sortResults(event.target.result, options);
  1.1370 +        for (let i in event.target.result)
  1.1371 +          txn.result[event.target.result[i].id] = exportContact(event.target.result[i]);
  1.1372 +      }.bind(this);
  1.1373 +    }
  1.1374 +  },
  1.1375 +
  1.1376 +  _findAll: function _findAll(txn, store, options) {
  1.1377 +    if (DEBUG) debug("ContactDB:_findAll:  " + JSON.stringify(options));
  1.1378 +    if (!txn.result)
  1.1379 +      txn.result = {};
  1.1380 +    // Sorting functions takes care of limit if set.
  1.1381 +    let limit = options.sortBy === 'undefined' ? options.filterLimit : null;
  1.1382 +    store.mozGetAll(null, limit).onsuccess = function (event) {
  1.1383 +      if (DEBUG) debug("Request successful. Record count:" + event.target.result.length);
  1.1384 +      this.sortResults(event.target.result, options);
  1.1385 +      for (let i in event.target.result) {
  1.1386 +        txn.result[event.target.result[i].id] = exportContact(event.target.result[i]);
  1.1387 +      }
  1.1388 +    }.bind(this);
  1.1389 +  },
  1.1390 +
  1.1391 +  // Enable special phone number substring matching. Does not update existing DB entries.
  1.1392 +  enableSubstringMatching: function enableSubstringMatching(aDigits) {
  1.1393 +    if (DEBUG) debug("MCC enabling substring matching " + aDigits);
  1.1394 +    this.substringMatching = aDigits;
  1.1395 +  },
  1.1396 +
  1.1397 +  disableSubstringMatching: function disableSubstringMatching() {
  1.1398 +    if (DEBUG) debug("MCC disabling substring matching");
  1.1399 +    delete this.substringMatching;
  1.1400 +  },
  1.1401 +
  1.1402 +  init: function init() {
  1.1403 +    this.initDBHelper(DB_NAME, DB_VERSION, [STORE_NAME, SAVED_GETALL_STORE_NAME, REVISION_STORE]);
  1.1404 +  }
  1.1405 +};

mercurial