diff --git a/js/ui/cordova/plugins/cordova-plugin-contacts/src/windows/ContactProxy.js b/js/ui/cordova/plugins/cordova-plugin-contacts/src/windows/ContactProxy.js new file mode 100644 index 0000000..61f1d7a --- /dev/null +++ b/js/ui/cordova/plugins/cordova-plugin-contacts/src/windows/ContactProxy.js @@ -0,0 +1,423 @@ +/* + * + * 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. + * + */ + +var ContactField = require('./ContactField'), + ContactAddress = require('./ContactAddress'), + ContactOrganization = require('./ContactOrganization'), + ContactName = require('./ContactName'), + ContactError = require('./ContactError'), + Contact = require('./Contact'); + + +function convertToContact(windowsContact) { + var contact = new Contact(); + + // displayName & nickname + contact.displayName = windowsContact.displayName || windowsContact.name; + contact.nickname = windowsContact.name; + contact.id = windowsContact.id; + + // name + // Additional fields like lastName, middleName etc. available on windows8.1/wp8.1 only + contact.name = new ContactName( + windowsContact.displayName || windowsContact.name, + windowsContact.lastName, + windowsContact.firstName || windowsContact.name, + windowsContact.middleName, + windowsContact.honorificNamePrefix || windowsContact.honorificPrefix, + windowsContact.honorificNameSuffix || windowsContact.honorificSuffix); + + // phoneNumbers + contact.phoneNumbers = []; + var phoneSource = windowsContact.phoneNumbers || windowsContact.phones; + for (var i = 0; i < phoneSource.size; i++) { + var rawPhone = phoneSource[i]; + var phone = new ContactField(rawPhone.category || rawPhone.kind, rawPhone.value || rawPhone.number); + contact.phoneNumbers.push(phone); + } + + // emails + contact.emails = []; + var emailSource = windowsContact.emails; + for (var i = 0; i < emailSource.size; i++) { + var rawEmail = emailSource[i]; + var email = new ContactField(rawEmail.category || rawEmail.kind, rawEmail.value || rawEmail.address); + contact.emails.push(email); + } + + // addressres + contact.addresses = []; + var addressSource = windowsContact.locations || windowsContact.addresses; + for (var i = 0; i < addressSource.size; i++) { + var rawAddress = addressSource[i]; + var address = new ContactAddress( + null, + rawAddress.category || rawAddress.kind, + rawAddress.unstructuredAddress, + rawAddress.street || rawAddress.streetAddress, + rawAddress.city || rawAddress.locality, + rawAddress.region, + rawAddress.postalCode, + rawAddress.country); + contact.addresses.push(address); + } + + // ims + contact.ims = []; + var imSource = windowsContact.instantMessages || windowsContact.connectedServiceAccounts; + for (var i = 0; i < imSource.size; i++) { + var rawIm = imSource[i]; + var im = new ContactField(rawIm.category || rawIm.serviceName, rawIm.userName || rawIm.id); + contact.ims.push(im); + } + + // jobInfo field available on Windows 8.1/WP8.1 only + var jobInfo = windowsContact.jobInfo; + if (jobInfo) { + contact.organizations = []; + for (var j = 0; j < jobInfo.size; j++) { + var rawJob = jobInfo[i]; + contact.organizations.push(new ContactOrganization(false, null, + rawJob.companyName, rawJob.department, rawJob.title)); + } + } + + // note field available on Windows 8.1/WP8.1 only + var contactNotes = windowsContact.notes; + if (contactNotes) { + contact.note = contactNotes; + } + + // returned is a file, a blob url can be made + var contactPhoto = windowsContact.thumbnail; + if (contactPhoto && contactPhoto.path) { + contact.photos = [new ContactField('url', URL.createObjectURL(contactPhoto) , false)]; + } + + return contact; +} + +// Win API Contacts namespace +var contactsNS = Windows.ApplicationModel.Contacts; + +function cdvContactToWindowsContact(contact) { + var result = new contactsNS.Contact(); + + // name first + if (contact.name) { + result.displayNameOverride = contact.name.formatted; + result.firstName = contact.name.givenName; + result.middleName = contact.name.middleName; + result.lastName = contact.name.familyName; + result.honorificNamePrefix = contact.name.honorificPrefix; + result.honorificNameSuffix = contact.name.honorificSuffix; + } + + result.nickname = contact.nickname; + + // phone numbers + if (contact.phoneNumbers) { + contact.phoneNumbers.forEach(function(contactPhone) { + var resultPhone = new contactsNS.ContactPhone(); + resultPhone.description = contactPhone.type; + resultPhone.number = contactPhone.value; + result.phones.push(resultPhone); + }); + } + + // emails + if (contact.emails) { + contact.emails.forEach(function(contactEmail) { + var resultEmail = new contactsNS.ContactEmail(); + resultEmail.description = contactEmail.type; + resultEmail.address = contactEmail.value; + result.emails.push(resultEmail); + }); + } + + // Addresses + if (contact.addresses) { + contact.addresses.forEach(function(contactAddress) { + var address = new contactsNS.ContactAddress(); + address.description = contactAddress.type; + address.streetAddress = contactAddress.streetAddress; + address.locality = contactAddress.locality; + address.region = contactAddress.region; + address.postalCode = contactAddress.postalCode; + address.country = contactAddress.country; + result.addresses.push(address); + }); + } + + // IMs + if (contact.ims) { + contact.ims.forEach(function(contactIM) { + var acct = new contactsNS.ContactConnectedServiceAccount(); + acct.serviceName = contactIM.type; + acct.id = contactIM.value; + result.connectedServiceAccounts.push(acct); + }); + } + + // JobInfo + if (contact.organizations) { + contact.organizations.forEach(function(contactOrg) { + var job = new contactsNS.ContactJobInfo(); + job.companyName = contactOrg.name; + job.department = contactOrg.department; + job.description = contactOrg.type; + job.title = contactOrg.title; + result.jobInfo.push(job); + }); + } + + result.notes = contact.note; + + if (contact.photos) { + var eligiblePhotos = contact.photos.filter(function(photo) { + return typeof photo.value !== 'undefined'; + }); + if (eligiblePhotos.length > 0) { + var supportedPhoto = eligiblePhotos[0]; + var path = supportedPhoto.value; + + try { + var streamRef; + if (/^([a-z][a-z0-9+\-.]*):\/\//i.test(path)) { + streamRef = Windows.Storage.Streams.RandomAccessStreamReference.createFromUri(new Windows.Foundation.Uri(path)); + } else { + streamRef = Windows.Storage.Streams.RandomAccessStreamReference.createFromFile(path); + } + result.thumbnail = streamRef; + } + catch (e) { + // incompatible reference to the photo + } + } + } + + return result; +} + +module.exports = { + + pickContact: function (win, fail, args) { + + // ContactPicker class works differently on Windows8/8.1 and Windows Phone 8.1 + // so we need to detect when we are running on phone + var runningOnPhone = navigator.userAgent.indexOf('Windows Phone') !== -1; + + var picker = new contactsNS.ContactPicker(); + if (runningOnPhone) { + // TODO: Windows Phone 8.1 requires this specification. This should be noted in quirks + // See http://msdn.microsoft.com/en-us/library/windows/apps/windows.applicationmodel.contacts.contactpicker.desiredfieldswithcontactfieldtype.aspx for details + // Multiple ContactFieldType items, appended to array causes `Request not suported` error. + picker.desiredFieldsWithContactFieldType.append(Windows.ApplicationModel.Contacts.ContactFieldType.phoneNumber); + } + + // pickContactAsync is available on Windows 8.1 or later, instead of + // pickSingleContactAsync, which is deprecated in Windows 8.1, + // so try to use newer method, if available. + // see http://msdn.microsoft.com/en-us/library/windows/apps/windows.applicationmodel.contacts.contactpicker.picksinglecontactasync.aspx + + var pickRequest = picker.pickContactAsync ? picker.pickContactAsync() : picker.pickSingleContactAsync(); + pickRequest.done(function (contact) { + // if contact was not picked + if (!contact) { + var cancelledError = new ContactError(ContactError.OPERATION_CANCELLED_ERROR); + cancelledError.message = "User did not pick a contact."; + fail(cancelledError); + return; + } + // If we are on desktop, just send em back + if (!runningOnPhone) { + win(convertToContact(contact)); + return; + } + // On WP8.1 fields set in resulted Contact object depends on desiredFieldsWithContactFieldType property of contact picker + // so we retrieve full contact by its' Id + contactsNS.ContactManager.requestStoreAsync().done(function (contactStore) { + contactStore.getContactAsync(contact.id).done(function(con) { + win(convertToContact(con)); + }, function() { + fail(new ContactError(ContactError.UNKNOWN_ERROR)); + }); + }, function () { + fail(new ContactError(ContactError.UNKNOWN_ERROR)); + }); + }); + }, + + save: function (win, fail, args) { + if (typeof contactsNS.ContactList === 'undefined') { + // Not supported yet since WinJS API do not provide methods to manage contactStore + // On Windows Phone 8.1 this can be implemented using native class library + // See Windows.Phone.PersonalInformation namespace + // http://msdn.microsoft.com/en-us/library/windows/apps/xaml/windows.phone.personalinformation.aspx + + //We don't need to create Error object here since it will be created at navigator.contacts.find() method + fail && fail(ContactError.NOT_SUPPORTED_ERROR); + return; + } + + var winContact = cdvContactToWindowsContact(args[0]); + + contactsNS.ContactManager.requestStoreAsync(contactsNS.ContactStoreAccessType.appContactsReadWrite).then(function(store) { + return store.findContactListsAsync().then(function(lists) { + if (lists.length > 0) { + return lists[0]; + } else { + return store.createContactListAsync(''); + } + }, function(error) { + return store.createContactListAsync(''); + }); + }).then(function(list) { + return list.saveContactAsync(winContact); + }).done(function(result) { + win(convertToContact(winContact)); + }, function(error) { + fail(error); + }); + }, + + remove: function(win, fail, args) { + if (typeof contactsNS.ContactList === 'undefined') { + // Not supported yet since WinJS API do not provide methods to manage contactStore + // On Windows Phone 8.1 this can be implemented using native class library + // See Windows.Phone.PersonalInformation namespace + // http://msdn.microsoft.com/en-us/library/windows/apps/xaml/windows.phone.personalinformation.aspx + + //We don't need to create Error object here since it will be created at navigator.contacts.find() method + fail && fail(ContactError.NOT_SUPPORTED_ERROR); + return; + } + + // This is a complicated scenario because in Win10, there is a notion of 'app contacts' vs 'global contacts'. + // search() returns all global contacts, which are "aggregate contacts", so the IDs of contacts that Cordova + // creates never match the IDs of the contacts returned from search(). + // In order to work around this, we need to: + // - Get two Stores: one that is read-write to the app-contacts list, one which is read-only for global contacts + // - Read the app-local store to see if a contact with the passed-in ID matches + // - Grab the global aggregate contact manager, then ask it for raw contacts (app-local ACM returns access denied) + // - Find my app-list of contacts + // - Enumerate the raw contacts and see if there is a raw contact whose parent list matches the app-list + // - If so, remove the raw contact from the app-list + // - If any of this fails, the operation fails + WinJS.Promise.join([contactsNS.ContactManager.requestStoreAsync(contactsNS.ContactStoreAccessType.appContactsReadWrite), + contactsNS.ContactManager.requestStoreAsync(contactsNS.ContactStoreAccessType.allContactsReadOnly)]).then(function(stores) { + var readOnlyStore = stores[1]; + var writableStore = stores[0]; + + var storeReader = writableStore.getContactReader(); + return storeReader.readBatchAsync().then(function(batch) { + if (batch.status !== contactsNS.ContactBatchStatus.success) { + // Couldn't read contacts store + throw new ContactError(ContactError.IO_ERROR); + } + + var candidates = batch.contacts.filter(function(testContact) { + return testContact.id === args[0]; + }); + + if (candidates.length === 0) { + // No matching contact from aggregate store + throw new ContactError(ContactError.IO_ERROR); + } + + return candidates[0]; + }).then(function(contactToDelete) { + return readOnlyStore.aggregateContactManager.findRawContactsAsync(contactToDelete); + }).then(function(rawContacts) { + return writableStore.findContactListsAsync().then(function(lists) { + var deleteList = null; + var deleteContact = null; + var matched = lists.some(function(list) { + for (var i = 0; i < rawContacts.length; i++) { + if (rawContacts[i].contactListId === list.id) { + deleteList = list; + deleteContact = rawContacts[i]; + return true; + } + } + return false; + }); + + if (!matched) { + throw new ContactError(ContactError.IO_ERROR); + } + + return deleteList.deleteContactAsync(deleteContact); + }); + }); + }).done(function() { + win(); + }, function(error) { + fail(error); + }); + }, + + search: function (win, fail, options) { + + // searchFields is not supported yet due to WP8.1 API limitations. + // findContactsAsync(String) method will attempt to match the name, email address, or phone number of a contact. + // see http://msdn.microsoft.com/en-us/library/windows/apps/dn624861.aspx for details + var searchFields = options[0], + searchOptions = options[1], + searchFilter = searchOptions.filter, + searchMultiple = searchOptions && searchOptions.multiple; + + // Check if necessary API is available. + // If not available, we are running on desktop, which doesn't support searching for contacts + if (!(contactsNS.ContactManager && contactsNS.ContactManager.requestStoreAsync)) { + fail(new ContactError(ContactError.NOT_SUPPORTED_ERROR)); + return; + } + + // Retrieve contact store instance + var contactStoreRequest = contactsNS.ContactManager.requestStoreAsync(); + + // When contact store retrieved + contactStoreRequest.done(function (contactStore) { + // determine, which function we use depending on whether searchOptions.filter specified or not + var contactsRequest = searchFilter ? contactStore.findContactsAsync(searchFilter) : contactStore.findContactsAsync(); + // request contacts and resolve either with success or error callback + contactsRequest.done(function (contacts) { + var result = []; + if (contacts.size !== 0) { + // Depending on searchOptions we should return all contacts found or only first + var outputContactsArray = searchMultiple ? contacts : [contacts[0]]; + outputContactsArray.forEach(function (contact) { + // Convert windows contacts to plugin's contact objects + result.push(convertToContact(contact)); + }); + } + win(result); + }, function() { + fail(new ContactError(ContactError.UNKNOWN_ERROR)); + }); + }, function() { + fail(new ContactError(ContactError.UNKNOWN_ERROR)); + }); + } +}; + +require("cordova/exec/proxy").add("Contacts", module.exports);