diff --git a/js/ui/cordova/plugins/cordova-plugin-contacts/src/firefoxos/ContactsProxy.js b/js/ui/cordova/plugins/cordova-plugin-contacts/src/firefoxos/ContactsProxy.js new file mode 100644 index 0000000..6be8d8c --- /dev/null +++ b/js/ui/cordova/plugins/cordova-plugin-contacts/src/firefoxos/ContactsProxy.js @@ -0,0 +1,464 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +// Cordova contact definition: +// http://cordova.apache.org/docs/en/2.5.0/cordova_contacts_contacts.md.html#Contact +// FxOS contact definition: +// https://developer.mozilla.org/en-US/docs/Web/API/mozContact + + +var Contact = require('./Contact'); +var ContactField = require('./ContactField'); +var ContactAddress = require('./ContactAddress'); +var ContactName = require('./ContactName'); + +// XXX: a hack to check if id is "empty". Cordova inserts a +// string "this string is supposed to be a unique identifier that will +// never show up on a device" if id is empty +function _hasId(id) { + if (!id || id.indexOf(' ') >= 0) { + return false; + } + return true; +} + +// Extend mozContact prototype to provide update from Cordova +function updateFromCordova(contact, fromContact) { + + function exportContactFieldArray(contactFieldArray, key) { + if (!key) { + key = 'value'; + } + var arr = []; + for (var i=0; i < contactFieldArray.length; i++) { + arr.push(contactFieldArray[i][key]); + }; + return arr; + } + + function exportAddress(addresses) { + // TODO: check moz address format + var arr = []; + + for (var i=0; i < addresses.length; i++) { + var addr = {}; + for (var key in addresses[i]) { + if (key == 'formatted' || key == 'id') { + continue; + } else if (key == 'type') { + addr[key] = [addresses[i][key]]; + } else if (key == 'country') { + addr['countryName'] = addresses[i][key]; + } else { + addr[key] = addresses[i][key]; + } + } + arr.push(addr); + } + return arr; + } + + function exportContactField(data) { + var contactFields = []; + for (var i=0; i < data.length; i++) { + var item = data[i]; + if (item.value) { + var itemData = {value: item.value}; + if (item.type) { + itemData.type = [item.type]; + } + if (item.pref) { + itemData.pref = item.pref; + } + contactFields.push(itemData); + } + } + return contactFields; + } + // adding simple fields [contactField, eventualMozContactField] + var nameFields = [['givenName'], ['familyName'], + ['honorificPrefix'], ['honorificSuffix'], + ['middleName', 'additionalName']]; + var baseArrayFields = [['displayName', 'name'], ['nickname']]; + var baseStringFields = []; + var j = 0; while(field = nameFields[j++]) { + if (fromContact.name[field[0]]) { + contact[field[1] || field[0]] = fromContact.name[field[0]].split(' '); + } + } + j = 0; while(field = baseArrayFields[j++]) { + if (fromContact[field[0]]) { + contact[field[1] || field[0]] = fromContact[field[0]].split(' '); + } + } + j = 0; while(field = baseStringFields[j++]) { + if (fromContact[field[0]]) { + contact[field[1] || field[0]] = fromContact[field[0]]; + } + } + if (fromContact.birthday) { + contact.bday = new Date(fromContact.birthday); + } + if (fromContact.emails) { + var emails = exportContactField(fromContact.emails) + contact.email = emails; + } + if (fromContact.categories) { + contact.category = exportContactFieldArray(fromContact.categories); + } + if (fromContact.addresses) { + contact.adr = exportAddress(fromContact.addresses); + } + if (fromContact.phoneNumbers) { + contact.tel = exportContactField(fromContact.phoneNumbers); + } + if (fromContact.organizations) { + // XXX: organizations are saved in 2 arrays - org and jobTitle + // depending on the usecase it might generate issues + // where wrong title will be added to an organization + contact.org = exportContactFieldArray(fromContact.organizations, 'name'); + contact.jobTitle = exportContactFieldArray(fromContact.organizations, 'title'); + } + if (fromContact.note) { + contact.note = [fromContact.note]; + } +} + + +// Extend Cordova Contact prototype to provide update from FFOS contact +Contact.prototype.updateFromMozilla = function(moz) { + function exportContactField(data) { + var contactFields = []; + for (var i=0; i < data.length; i++) { + var item = data[i]; + var itemData = new ContactField(item.type, item.value, item.pref); + contactFields.push(itemData); + } + return contactFields; + } + + function makeContactFieldFromArray(data) { + var contactFields = []; + for (var i=0; i < data.length; i++) { + var itemData = new ContactField(null, data[i]); + contactFields.push(itemData); + } + return contactFields; + } + + function exportAddresses(addresses) { + // TODO: check moz address format + var arr = []; + + for (var i=0; i < addresses.length; i++) { + var addr = {}; + for (var key in addresses[i]) { + if (key == 'countryName') { + addr['country'] = addresses[i][key]; + } else if (key == 'type') { + addr[key] = addresses[i][key].join(' '); + } else { + addr[key] = addresses[i][key]; + } + } + arr.push(addr); + } + return arr; + } + + function createOrganizations(orgs, jobs) { + orgs = (orgs) ? orgs : []; + jobs = (jobs) ? jobs : []; + var max_length = Math.max(orgs.length, jobs.length); + var organizations = []; + for (var i=0; i < max_length; i++) { + organizations.push(new ContactOrganization( + null, null, orgs[i] || null, null, jobs[i] || null)); + } + return organizations; + } + + function createFormatted(name) { + var fields = ['honorificPrefix', 'givenName', 'middleName', + 'familyName', 'honorificSuffix']; + var f = ''; + for (var i = 0; i < fields.length; i++) { + if (name[fields[i]]) { + if (f) { + f += ' '; + } + f += name[fields[i]]; + } + } + return f; + } + + + if (moz.id) { + this.id = moz.id; + } + var nameFields = [['givenName'], ['familyName'], + ['honorificPrefix'], ['honorificSuffix'], + ['additionalName', 'middleName']]; + var baseArrayFields = [['name', 'displayName'], 'nickname', ['note']]; + var baseStringFields = []; + var name = new ContactName(); + var j = 0; while(field = nameFields[j++]) { + if (moz[field[0]]) { + name[field[1] || field[0]] = moz[field[0]].join(' '); + } + } + this.name = name; + j = 0; while(field = baseArrayFields[j++]) { + if (moz[field[0]]) { + this[field[1] || field[0]] = moz[field[0]].join(' '); + } + } + j = 0; while(field = baseStringFields[j++]) { + if (moz[field[0]]) { + this[field[1] || field[0]] = moz[field[0]]; + } + } + // emails + if (moz.email) { + this.emails = exportContactField(moz.email); + } + // categories + if (moz.category) { + this.categories = makeContactFieldFromArray(moz.category); + } + + // addresses + if (moz.adr) { + this.addresses = exportAddresses(moz.adr); + } + + // phoneNumbers + if (moz.tel) { + this.phoneNumbers = exportContactField(moz.tel); + } + // birthday + if (moz.bday) { + this.birthday = Date.parse(moz.bday); + } + // organizations + if (moz.org || moz.jobTitle) { + // XXX: organizations array is created from org and jobTitle + this.organizations = createOrganizations(moz.org, moz.jobTitle); + } + // construct a read-only formatted value + this.name.formatted = createFormatted(this.name); + + /* Find out how to translate these parameters + // photo: Blob + // url: Array with metadata (?) + // impp: exportIM(contact.ims), TODO: find the moz impp definition + // anniversary + // sex + // genderIdentity + // key + */ +} + + +function createMozillaFromCordova(successCB, errorCB, contact) { + var moz; + // get contact if exists + if (_hasId(contact.id)) { + var search = navigator.mozContacts.find({ + filterBy: ['id'], filterValue: contact.id, filterOp: 'equals'}); + search.onsuccess = function() { + moz = search.result[0]; + updateFromCordova(moz, contact); + successCB(moz); + }; + search.onerror = errorCB; + return; + } + + // create empty contact + moz = new mozContact(); + // if ('init' in moz) { + // 1.2 and below compatibility + // moz.init(); + // } + updateFromCordova(moz, contact); + successCB(moz); +} + + +function createCordovaFromMozilla(moz) { + var contact = new Contact(); + contact.updateFromMozilla(moz); + return contact; +} + + +// However API requires the ability to save multiple contacts, it is +// used to save only one element array +function saveContacts(successCB, errorCB, contacts) { + // a closure which is holding the right moz contact + function makeSaveSuccessCB(moz) { + return function(result) { + // create contact from FXOS contact (might be different than + // the original one due to differences in API) + var contact = createCordovaFromMozilla(moz); + // call callback + successCB(contact); + } + } + var i=0; + var contact; + while(contact = contacts[i++]){ + var moz = createMozillaFromCordova(function(moz) { + var request = navigator.mozContacts.save(moz); + // success and/or fail will be called every time a contact is saved + request.onsuccess = makeSaveSuccessCB(moz); + request.onerror = errorCB; + }, function() {}, contact); + } +} + + +// API provides a list of ids to be removed +function remove(successCB, errorCB, ids) { + var i=0; + var id; + for (var i=0; i < ids.length; i++){ + // throw an error if no id provided + if (!_hasId(ids[i])) { + console.error('FFOS: Attempt to remove unsaved contact'); + errorCB(0); + return; + } + // check if provided id actually exists + var search = navigator.mozContacts.find({ + filterBy: ['id'], filterValue: ids[i], filterOp: 'equals'}); + search.onsuccess = function() { + if (search.result.length === 0) { + console.error('FFOS: Attempt to remove a non existing contact'); + errorCB(0); + return; + } + var moz = search.result[0]; + var request = navigator.mozContacts.remove(moz); + request.onsuccess = successCB; + request.onerror = errorCB; + }; + search.onerror = errorCB; + } +} + + +var mozContactSearchFields = [['name', 'displayName'], ['givenName'], + ['familyName'], ['email'], ['tel'], ['jobTitle'], ['note'], + ['tel', 'phoneNumbers'], ['email', 'emails']]; +// Searching by nickname and additionalName is forbidden in 1.3 and below +// Searching by name is forbidden in 1.2 and below + +// finds if a key is allowed and returns FFOS name if different +function getMozSearchField(key) { + if (mozContactSearchFields.indexOf([key]) >= 0) { + return key; + } + for (var i=0; i < mozContactSearchFields.length; i++) { + if (mozContactSearchFields[i].length > 1) { + if (mozContactSearchFields[i][1] === key) { + return mozContactSearchFields[i][0]; + } + } + } + return false; +} + + +function _getAll(successCB, errorCB, params) { + // [contactField, eventualMozContactField] + var getall = navigator.mozContacts.getAll({}); + var contacts = []; + + getall.onsuccess = function() { + if (getall.result) { + contacts.push(createCordovaFromMozilla(getall.result)); + getall.continue(); + } else { + successCB(contacts); + } + }; + getall.onerror = errorCB; +} + + +function search(successCB, errorCB, params) { + var options = params[1] || {}; + if (!options.filter) { + return _getAll(successCB, errorCB, params); + } + var filterBy = []; + // filter and translate fields + for (var i=0; i < params[0].length; i++) { + var searchField = params[0][i]; + var mozField = getMozSearchField(searchField); + if (searchField === 'name') { + // Cordova uses name for search by all name fields. + filterBy.push('givenName'); + filterBy.push('familyName'); + continue; + } + if (searchField === 'displayName' && 'init' in new mozContact()) { + // ``init`` in ``mozContact`` indicates FFOS version 1.2 or below + // Searching by name (in moz) is then forbidden + console.log('FFOS ContactProxy: Unable to search by displayName on FFOS 1.2'); + continue; + } + if (mozField) { + filterBy.push(mozField); + } else { + console.log('FXOS ContactProxy: inallowed field passed to search filtered out: ' + searchField); + } + } + + var mozOptions = {filterBy: filterBy, filterOp: 'startsWith'}; + if (!options.multiple) { + mozOptions.filterLimit = 1; + } + mozOptions.filterValue = options.filter; + var request = navigator.mozContacts.find(mozOptions); + request.onsuccess = function() { + var contacts = []; + var mozContacts = request.result; + var moz = mozContacts[0]; + for (var i=0; i < mozContacts.length; i++) { + contacts.push(createCordovaFromMozilla(mozContacts[i])); + } + successCB(contacts); + }; + request.onerror = errorCB; +} + + +module.exports = { + save: saveContacts, + remove: remove, + search: search +}; + +require("cordova/exec/proxy").add("Contacts", module.exports); \ No newline at end of file