diff --git a/js/ui/cordova/plugins/cordova-plugin-contacts/src/ios/CDVContact.m b/js/ui/cordova/plugins/cordova-plugin-contacts/src/ios/CDVContact.m new file mode 100644 index 0000000..a1dd85d --- /dev/null +++ b/js/ui/cordova/plugins/cordova-plugin-contacts/src/ios/CDVContact.m @@ -0,0 +1,1781 @@ +/* + 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. + */ + +#import "CDVContact.h" + +#define DATE_OR_NULL(dateObj) ((aDate != nil) ? (id)([aDate descriptionWithLocale:[NSLocale currentLocale]]) : (id)([NSNull null])) +#define IS_VALID_VALUE(value) ((value != nil) && (![value isKindOfClass:[NSNull class]])) + +static NSDictionary* org_apache_cordova_contacts_W3CtoAB = nil; +static NSDictionary* org_apache_cordova_contacts_ABtoW3C = nil; +static NSSet* org_apache_cordova_contacts_W3CtoNull = nil; +static NSDictionary* org_apache_cordova_contacts_objectAndProperties = nil; +static NSDictionary* org_apache_cordova_contacts_defaultFields = nil; + +@implementation CDVContact : NSObject + + @synthesize returnFields; + +- (id)init +{ + if ((self = [super init]) != nil) { + ABRecordRef rec = ABPersonCreate(); + self.record = rec; + if (rec) { + CFRelease(rec); + } + } + return self; +} + +- (id)initFromABRecord:(ABRecordRef)aRecord +{ + if ((self = [super init]) != nil) { + self.record = aRecord; + } + return self; +} + +/* synthesize 'record' ourselves to have retain properties for CF types */ + +- (void)setRecord:(ABRecordRef)aRecord +{ + if (record != NULL) { + CFRelease(record); + } + if (aRecord != NULL) { + record = CFRetain(aRecord); + } +} + +- (ABRecordRef)record +{ + return record; +} + +/* Rather than creating getters and setters for each AddressBook (AB) Property, generic methods are used to deal with + * simple properties, MultiValue properties( phone numbers and emails) and MultiValueDictionary properties (Ims and addresses). + * The dictionaries below are used to translate between the W3C identifiers and the AB properties. Using the dictionaries, + * allows looping through sets of properties to extract from or set into the W3C dictionary to/from the ABRecord. + */ + +/* The two following dictionaries translate between W3C properties and AB properties. It currently mixes both + * Properties (kABPersonAddressProperty for example) and Strings (kABPersonAddressStreetKey) so users should be aware of + * what types of values are expected. + * a bit. +*/ ++ (NSDictionary*)defaultABtoW3C +{ + if (org_apache_cordova_contacts_ABtoW3C == nil) { + org_apache_cordova_contacts_ABtoW3C = [NSDictionary dictionaryWithObjectsAndKeys: + kW3ContactNickname, [NSNumber numberWithInt:kABPersonNicknameProperty], + kW3ContactGivenName, [NSNumber numberWithInt:kABPersonFirstNameProperty], + kW3ContactFamilyName, [NSNumber numberWithInt:kABPersonLastNameProperty], + kW3ContactMiddleName, [NSNumber numberWithInt:kABPersonMiddleNameProperty], + kW3ContactHonorificPrefix, [NSNumber numberWithInt:kABPersonPrefixProperty], + kW3ContactHonorificSuffix, [NSNumber numberWithInt:kABPersonSuffixProperty], + kW3ContactPhoneNumbers, [NSNumber numberWithInt:kABPersonPhoneProperty], + kW3ContactAddresses, [NSNumber numberWithInt:kABPersonAddressProperty], + kW3ContactStreetAddress, kABPersonAddressStreetKey, + kW3ContactLocality, kABPersonAddressCityKey, + kW3ContactRegion, kABPersonAddressStateKey, + kW3ContactPostalCode, kABPersonAddressZIPKey, + kW3ContactCountry, kABPersonAddressCountryKey, + kW3ContactEmails, [NSNumber numberWithInt:kABPersonEmailProperty], + kW3ContactIms, [NSNumber numberWithInt:kABPersonInstantMessageProperty], + kW3ContactOrganizations, [NSNumber numberWithInt:kABPersonOrganizationProperty], + kW3ContactOrganizationName, [NSNumber numberWithInt:kABPersonOrganizationProperty], + kW3ContactTitle, [NSNumber numberWithInt:kABPersonJobTitleProperty], + kW3ContactDepartment, [NSNumber numberWithInt:kABPersonDepartmentProperty], + kW3ContactBirthday, [NSNumber numberWithInt:kABPersonBirthdayProperty], + kW3ContactUrls, [NSNumber numberWithInt:kABPersonURLProperty], + kW3ContactNote, [NSNumber numberWithInt:kABPersonNoteProperty], + nil]; + } + + return org_apache_cordova_contacts_ABtoW3C; +} + ++ (NSDictionary*)defaultW3CtoAB +{ + if (org_apache_cordova_contacts_W3CtoAB == nil) { + org_apache_cordova_contacts_W3CtoAB = [NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithInt:kABPersonNicknameProperty], kW3ContactNickname, + [NSNumber numberWithInt:kABPersonFirstNameProperty], kW3ContactGivenName, + [NSNumber numberWithInt:kABPersonLastNameProperty], kW3ContactFamilyName, + [NSNumber numberWithInt:kABPersonMiddleNameProperty], kW3ContactMiddleName, + [NSNumber numberWithInt:kABPersonPrefixProperty], kW3ContactHonorificPrefix, + [NSNumber numberWithInt:kABPersonSuffixProperty], kW3ContactHonorificSuffix, + [NSNumber numberWithInt:kABPersonPhoneProperty], kW3ContactPhoneNumbers, + [NSNumber numberWithInt:kABPersonAddressProperty], kW3ContactAddresses, + kABPersonAddressStreetKey, kW3ContactStreetAddress, + kABPersonAddressCityKey, kW3ContactLocality, + kABPersonAddressStateKey, kW3ContactRegion, + kABPersonAddressZIPKey, kW3ContactPostalCode, + kABPersonAddressCountryKey, kW3ContactCountry, + [NSNumber numberWithInt:kABPersonEmailProperty], kW3ContactEmails, + [NSNumber numberWithInt:kABPersonInstantMessageProperty], kW3ContactIms, + [NSNumber numberWithInt:kABPersonOrganizationProperty], kW3ContactOrganizations, + [NSNumber numberWithInt:kABPersonJobTitleProperty], kW3ContactTitle, + [NSNumber numberWithInt:kABPersonDepartmentProperty], kW3ContactDepartment, + [NSNumber numberWithInt:kABPersonBirthdayProperty], kW3ContactBirthday, + [NSNumber numberWithInt:kABPersonNoteProperty], kW3ContactNote, + [NSNumber numberWithInt:kABPersonURLProperty], kW3ContactUrls, + kABPersonInstantMessageUsernameKey, kW3ContactImValue, + kABPersonInstantMessageServiceKey, kW3ContactImType, + [NSNull null], kW3ContactFieldType, /* include entries in dictionary to indicate ContactField properties */ + [NSNull null], kW3ContactFieldValue, + [NSNull null], kW3ContactFieldPrimary, + [NSNull null], kW3ContactFieldId, + [NSNumber numberWithInt:kABPersonOrganizationProperty], kW3ContactOrganizationName, /* careful, name is used multiple times*/ + nil]; + } + return org_apache_cordova_contacts_W3CtoAB; +} + ++ (NSSet*)defaultW3CtoNull +{ + // these are values that have no AddressBook Equivalent OR have not been implemented yet + if (org_apache_cordova_contacts_W3CtoNull == nil) { + org_apache_cordova_contacts_W3CtoNull = [NSSet setWithObjects:kW3ContactDisplayName, + kW3ContactCategories, kW3ContactFormattedName, nil]; + } + return org_apache_cordova_contacts_W3CtoNull; +} + +/* + * The objectAndProperties dictionary contains the all of the properties of the W3C Contact Objects specified by the key + * Used in calcReturnFields, and various extract methods + */ ++ (NSDictionary*)defaultObjectAndProperties +{ + if (org_apache_cordova_contacts_objectAndProperties == nil) { + org_apache_cordova_contacts_objectAndProperties = [NSDictionary dictionaryWithObjectsAndKeys: + [NSArray arrayWithObjects:kW3ContactGivenName, kW3ContactFamilyName, + kW3ContactMiddleName, kW3ContactHonorificPrefix, kW3ContactHonorificSuffix, kW3ContactFormattedName, nil], kW3ContactName, + [NSArray arrayWithObjects:kW3ContactStreetAddress, kW3ContactLocality, kW3ContactRegion, + kW3ContactPostalCode, kW3ContactCountry, /*kW3ContactAddressFormatted,*/ nil], kW3ContactAddresses, + [NSArray arrayWithObjects:kW3ContactOrganizationName, kW3ContactTitle, kW3ContactDepartment, nil], kW3ContactOrganizations, + [NSArray arrayWithObjects:kW3ContactFieldType, kW3ContactFieldValue, kW3ContactFieldPrimary, nil], kW3ContactPhoneNumbers, + [NSArray arrayWithObjects:kW3ContactFieldType, kW3ContactFieldValue, kW3ContactFieldPrimary, nil], kW3ContactEmails, + [NSArray arrayWithObjects:kW3ContactFieldType, kW3ContactFieldValue, kW3ContactFieldPrimary, nil], kW3ContactPhotos, + [NSArray arrayWithObjects:kW3ContactFieldType, kW3ContactFieldValue, kW3ContactFieldPrimary, nil], kW3ContactUrls, + [NSArray arrayWithObjects:kW3ContactImValue, kW3ContactImType, nil], kW3ContactIms, + nil]; + } + return org_apache_cordova_contacts_objectAndProperties; +} + ++ (NSDictionary*)defaultFields +{ + if (org_apache_cordova_contacts_defaultFields == nil) { + org_apache_cordova_contacts_defaultFields = [NSDictionary dictionaryWithObjectsAndKeys: + [[CDVContact defaultObjectAndProperties] objectForKey:kW3ContactName], kW3ContactName, + [NSNull null], kW3ContactNickname, + [[CDVContact defaultObjectAndProperties] objectForKey:kW3ContactAddresses], kW3ContactAddresses, + [[CDVContact defaultObjectAndProperties] objectForKey:kW3ContactOrganizations], kW3ContactOrganizations, + [[CDVContact defaultObjectAndProperties] objectForKey:kW3ContactPhoneNumbers], kW3ContactPhoneNumbers, + [[CDVContact defaultObjectAndProperties] objectForKey:kW3ContactEmails], kW3ContactEmails, + [[CDVContact defaultObjectAndProperties] objectForKey:kW3ContactIms], kW3ContactIms, + [[CDVContact defaultObjectAndProperties] objectForKey:kW3ContactPhotos], kW3ContactPhotos, + [[CDVContact defaultObjectAndProperties] objectForKey:kW3ContactUrls], kW3ContactUrls, + [NSNull null], kW3ContactBirthday, + [NSNull null], kW3ContactNote, + nil]; + } + return org_apache_cordova_contacts_defaultFields; +} + +/* Translate W3C Contact data into ABRecordRef + * + * New contact information comes in as a NSMutableDictionary. All Null entries in Contact object are set + * as [NSNull null] in the dictionary when translating from the JSON input string of Contact data. However, if + * user did not set a value within a Contact object or sub-object (by not using the object constructor) some data + * may not exist. + * bUpdate = YES indicates this is a save of an existing record + */ +- (bool)setFromContactDict:(NSDictionary*)aContact asUpdate:(BOOL)bUpdate +{ + if (![aContact isKindOfClass:[NSDictionary class]]) { + return FALSE; // can't do anything if no dictionary! + } + + ABRecordRef person = self.record; + bool bSuccess = TRUE; + CFErrorRef error; + + // set name info + // iOS doesn't have displayName - might have to pull parts from it to create name + bool bName = false; + NSDictionary* dict = [aContact valueForKey:kW3ContactName]; + if ([dict isKindOfClass:[NSDictionary class]]) { + bName = true; + NSArray* propArray = [[CDVContact defaultObjectAndProperties] objectForKey:kW3ContactName]; + + for (id i in propArray) { + if (![(NSString*)i isEqualToString : kW3ContactFormattedName]) { // kW3ContactFormattedName is generated from ABRecordCopyCompositeName() and can't be set + [self setValue:[dict valueForKey:i] forProperty:(ABPropertyID)[(NSNumber*)[[CDVContact defaultW3CtoAB] objectForKey:i] intValue] + inRecord:person asUpdate:bUpdate]; + } + } + } + + id nn = [aContact valueForKey:kW3ContactNickname]; + if (![nn isKindOfClass:[NSNull class]]) { + bName = true; + [self setValue:nn forProperty:kABPersonNicknameProperty inRecord:person asUpdate:bUpdate]; + } + if (!bName) { + // if no name or nickname - try and use displayName as W3Contact must have displayName or ContactName + [self setValue:[aContact valueForKey:kW3ContactDisplayName] forProperty:kABPersonNicknameProperty + inRecord:person asUpdate:bUpdate]; + } + + // set phoneNumbers + // NSLog(@"setting phoneNumbers"); + NSArray* array = [aContact valueForKey:kW3ContactPhoneNumbers]; + if ([array isKindOfClass:[NSArray class]]) { + [self setMultiValueStrings:array forProperty:kABPersonPhoneProperty inRecord:person asUpdate:bUpdate]; + } + // set Emails + // NSLog(@"setting emails"); + array = [aContact valueForKey:kW3ContactEmails]; + if ([array isKindOfClass:[NSArray class]]) { + [self setMultiValueStrings:array forProperty:kABPersonEmailProperty inRecord:person asUpdate:bUpdate]; + } + // set Urls + // NSLog(@"setting urls"); + array = [aContact valueForKey:kW3ContactUrls]; + if ([array isKindOfClass:[NSArray class]]) { + [self setMultiValueStrings:array forProperty:kABPersonURLProperty inRecord:person asUpdate:bUpdate]; + } + + // set multivalue dictionary properties + // set addresses: streetAddress, locality, region, postalCode, country + // set ims: value = username, type = servicetype + // iOS addresses and im are a MultiValue Properties with label, value=dictionary of info, and id + // NSLog(@"setting addresses"); + error = nil; + array = [aContact valueForKey:kW3ContactAddresses]; + if ([array isKindOfClass:[NSArray class]]) { + [self setMultiValueDictionary:array forProperty:kABPersonAddressProperty inRecord:person asUpdate:bUpdate]; + } + // ims + // NSLog(@"setting ims"); + array = [aContact valueForKey:kW3ContactIms]; + if ([array isKindOfClass:[NSArray class]]) { + [self setMultiValueDictionary:array forProperty:kABPersonInstantMessageProperty inRecord:person asUpdate:bUpdate]; + } + + // organizations + // W3C ContactOrganization has pref, type, name, title, department + // iOS only supports name, title, department + // NSLog(@"setting organizations"); + // TODO this may need work - should Organization information be removed when array is empty?? + array = [aContact valueForKey:kW3ContactOrganizations]; // iOS only supports one organization - use first one + if ([array isKindOfClass:[NSArray class]]) { + BOOL bRemove = NO; + NSDictionary* dict = nil; + if ([array count] > 0) { + dict = [array objectAtIndex:0]; + } else { + // remove the organization info entirely + bRemove = YES; + } + if ([dict isKindOfClass:[NSDictionary class]] || (bRemove == YES)) { + [self setValue:(bRemove ? @"" : [dict valueForKey:@"name"]) forProperty:kABPersonOrganizationProperty inRecord:person asUpdate:bUpdate]; + [self setValue:(bRemove ? @"" : [dict valueForKey:kW3ContactTitle]) forProperty:kABPersonJobTitleProperty inRecord:person asUpdate:bUpdate]; + [self setValue:(bRemove ? @"" : [dict valueForKey:kW3ContactDepartment]) forProperty:kABPersonDepartmentProperty inRecord:person asUpdate:bUpdate]; + } + } + // add dates + // Dates come in as milliseconds in NSNumber Object + id ms = [aContact valueForKey:kW3ContactBirthday]; + NSDate* aDate = nil; + if (ms && [ms isKindOfClass:[NSNumber class]]) { + double msValue = [ms doubleValue]; + msValue = msValue / 1000; + aDate = [NSDate dateWithTimeIntervalSince1970:msValue]; + } + if ((aDate != nil) || [ms isKindOfClass:[NSString class]]) { + [self setValue:aDate != nil ? aDate:ms forProperty:kABPersonBirthdayProperty inRecord:person asUpdate:bUpdate]; + } + // don't update creation date + // modification date will get updated when save + // anniversary is removed from W3C Contact api Dec 9, 2010 spec - don't waste time on it yet + + // kABPersonDateProperty + + // kABPersonAnniversaryLabel + + // iOS doesn't have gender - ignore + // note + [self setValue:[aContact valueForKey:kW3ContactNote] forProperty:kABPersonNoteProperty inRecord:person asUpdate:bUpdate]; + + // iOS doesn't have preferredName- ignore + + // photo + array = [aContact valueForKey:kW3ContactPhotos]; + if ([array isKindOfClass:[NSArray class]]) { + if (bUpdate && ([array count] == 0)) { + // remove photo + bSuccess = ABPersonRemoveImageData(person, &error); + } else if ([array count] > 0) { + NSDictionary* dict = [array objectAtIndex:0]; // currently only support one photo + if ([dict isKindOfClass:[NSDictionary class]]) { + id value = [dict objectForKey:kW3ContactFieldValue]; + if ([value isKindOfClass:[NSString class]]) { + if (bUpdate && ([value length] == 0)) { + // remove the current image + bSuccess = ABPersonRemoveImageData(person, &error); + } else { + // use this image + // don't know if string is encoded or not so first unencode it then encode it again + NSString* cleanPath = [value stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + NSURL* photoUrl = [NSURL URLWithString:[cleanPath stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; + // caller is responsible for checking for a connection, if no connection this will fail + NSError* err = nil; + NSData* data = nil; + if (photoUrl) { + data = [NSData dataWithContentsOfURL:photoUrl options:NSDataReadingUncached error:&err]; + } + if (data && ([data length] > 0)) { + bSuccess = ABPersonSetImageData(person, (__bridge CFDataRef)data, &error); + } + if (!data || !bSuccess) { + NSLog(@"error setting contact image: %@", (err != nil ? [err localizedDescription] : @"")); + } + } + } + } + } + } + + // TODO WebURLs + + // TODO timezone + + return bSuccess; +} + +/* Set item into an AddressBook Record for the specified property. + * aValue - the value to set into the address book (code checks for null or [NSNull null] + * aProperty - AddressBook property ID + * aRecord - the record to update + * bUpdate - whether this is a possible update vs a new entry + * RETURN + * true - property was set (or input value as null) + * false - property was not set + */ +- (bool)setValue:(id)aValue forProperty:(ABPropertyID)aProperty inRecord:(ABRecordRef)aRecord asUpdate:(BOOL)bUpdate +{ + bool bSuccess = true; // if property was null, just ignore and return success + CFErrorRef error; + + if (aValue && ![aValue isKindOfClass:[NSNull class]]) { + if (bUpdate && ([aValue isKindOfClass:[NSString class]] && ([aValue length] == 0))) { // if updating, empty string means to delete + aValue = NULL; + } // really only need to set if different - more efficient to just update value or compare and only set if necessary??? + bSuccess = ABRecordSetValue(aRecord, aProperty, (__bridge CFTypeRef)aValue, &error); + if (!bSuccess) { + NSLog(@"error setting %d property", aProperty); + } + } + + return bSuccess; +} + +- (bool)removeProperty:(ABPropertyID)aProperty inRecord:(ABRecordRef)aRecord +{ + CFErrorRef err; + bool bSuccess = ABRecordRemoveValue(aRecord, aProperty, &err); + + if (!bSuccess) { + CFStringRef errDescription = CFErrorCopyDescription(err); + NSLog(@"Unable to remove property %d: %@", aProperty, errDescription); + CFRelease(errDescription); + } + return bSuccess; +} + +- (bool)addToMultiValue:(ABMultiValueRef)multi fromDictionary:dict +{ + bool bSuccess = FALSE; + id value = [dict valueForKey:kW3ContactFieldValue]; + + if (IS_VALID_VALUE(value)) { + CFStringRef label = [CDVContact convertContactTypeToPropertyLabel:[dict valueForKey:kW3ContactFieldType]]; + bSuccess = ABMultiValueAddValueAndLabel(multi, (__bridge CFTypeRef)value, label, NULL); + if (!bSuccess) { + NSLog(@"Error setting Value: %@ and label: %@", value, label); + } + } + return bSuccess; +} + +- (ABMultiValueRef)allocStringMultiValueFromArray:array +{ + ABMutableMultiValueRef multi = ABMultiValueCreateMutable(kABMultiStringPropertyType); + + for (NSDictionary* dict in array) { + [self addToMultiValue:multi fromDictionary:dict]; + } + + return multi; // caller is responsible for releasing multi +} + +- (bool)setValue:(CFTypeRef)value forProperty:(ABPropertyID)prop inRecord:(ABRecordRef)person +{ + CFErrorRef error; + bool bSuccess = ABRecordSetValue(person, prop, value, &error); + + if (!bSuccess) { + NSLog(@"Error setting value for property: %d", prop); + } + return bSuccess; +} + +/* Set MultiValue string properties into Address Book Record. + * NSArray* fieldArray - array of dictionaries containing W3C properties to be set into record + * ABPropertyID prop - the property to be set (generally used for phones and emails) + * ABRecordRef person - the record to set values into + * BOOL bUpdate - whether or not to update date or set as new. + * When updating: + * empty array indicates to remove entire property + * empty string indicates to remove + * [NSNull null] do not modify (keep existing record value) + * RETURNS + * bool false indicates error + * + * used for phones and emails + */ +- (bool)setMultiValueStrings:(NSArray*)fieldArray forProperty:(ABPropertyID)prop inRecord:(ABRecordRef)person asUpdate:(BOOL)bUpdate +{ + bool bSuccess = TRUE; + ABMutableMultiValueRef multi = nil; + + if (!bUpdate) { + multi = [self allocStringMultiValueFromArray:fieldArray]; + bSuccess = [self setValue:multi forProperty:prop inRecord:person]; + } else if (bUpdate && ([fieldArray count] == 0)) { + // remove entire property + bSuccess = [self removeProperty:prop inRecord:person]; + } else { // check for and apply changes + ABMultiValueRef copy = ABRecordCopyValue(person, prop); + if (copy != nil) { + multi = ABMultiValueCreateMutableCopy(copy); + CFRelease(copy); + + for (NSDictionary* dict in fieldArray) { + id val; + NSString* label = nil; + val = [dict valueForKey:kW3ContactFieldValue]; + label = (__bridge NSString*)[CDVContact convertContactTypeToPropertyLabel:[dict valueForKey:kW3ContactFieldType]]; + if (IS_VALID_VALUE(val)) { + // is an update, find index of entry with matching id, if values are different, update. + id idValue = [dict valueForKey:kW3ContactFieldId]; + int identifier = [idValue isKindOfClass:[NSNumber class]] ? [idValue intValue] : -1; + CFIndex i = identifier >= 0 ? ABMultiValueGetIndexForIdentifier(multi, identifier) : kCFNotFound; + if (i != kCFNotFound) { + if ([val length] == 0) { + // remove both value and label + ABMultiValueRemoveValueAndLabelAtIndex(multi, i); + } else { + NSString* valueAB = (__bridge_transfer NSString*)ABMultiValueCopyValueAtIndex(multi, i); + NSString* labelAB = (__bridge_transfer NSString*)ABMultiValueCopyLabelAtIndex(multi, i); + if ((valueAB == nil) || ![val isEqualToString:valueAB]) { + ABMultiValueReplaceValueAtIndex(multi, (__bridge CFTypeRef)val, i); + } + if ((labelAB == nil) || ![label isEqualToString:labelAB]) { + ABMultiValueReplaceLabelAtIndex(multi, (__bridge CFStringRef)label, i); + } + } + } else { + // is a new value - insert + [self addToMultiValue:multi fromDictionary:dict]; + } + } // end of if value + } // end of for + } else { // adding all new value(s) + multi = [self allocStringMultiValueFromArray:fieldArray]; + } + // set the (updated) copy as the new value + bSuccess = [self setValue:multi forProperty:prop inRecord:person]; + } + + if (multi) { + CFRelease(multi); + } + + return bSuccess; +} + +// used for ims and addresses +- (ABMultiValueRef)allocDictMultiValueFromArray:array forProperty:(ABPropertyID)prop +{ + ABMutableMultiValueRef multi = ABMultiValueCreateMutable(kABMultiDictionaryPropertyType); + NSMutableDictionary* newDict; + NSMutableDictionary* addDict; + + for (NSDictionary* dict in array) { + newDict = [self translateW3Dict:dict forProperty:prop]; + addDict = [NSMutableDictionary dictionaryWithCapacity:2]; + if (newDict) { // create a new dictionary with a Label and Value, value is the dictionary previously created + // June, 2011 W3C Contact spec adds type into ContactAddress book + // get the type out of the original dictionary for address + NSString* addrType = (NSString*)[dict valueForKey:kW3ContactFieldType]; + if (!addrType) { + addrType = (NSString*)kABOtherLabel; + } + NSObject* typeValue = ((prop == kABPersonInstantMessageProperty) ? (NSString*)kABOtherLabel : addrType); + // NSLog(@"typeValue: %@", typeValue); + [addDict setObject:typeValue forKey:kW3ContactFieldType]; // im labels will be set as Other and address labels as type from dictionary + [addDict setObject:newDict forKey:kW3ContactFieldValue]; + [self addToMultiValue:multi fromDictionary:addDict]; + } + } + + return multi; // caller is responsible for releasing +} + +// used for ims and addresses to convert W3 dictionary of values to AB Dictionary +// got messier when June, 2011 W3C Contact spec added type field into ContactAddress +- (NSMutableDictionary*)translateW3Dict:(NSDictionary*)dict forProperty:(ABPropertyID)prop +{ + NSArray* propArray = [[CDVContact defaultObjectAndProperties] valueForKey:[[CDVContact defaultABtoW3C] objectForKey:[NSNumber numberWithInt:prop]]]; + + NSMutableDictionary* newDict = [NSMutableDictionary dictionaryWithCapacity:1]; + id value; + + for (NSString* key in propArray) { // for each W3 Contact key get the value + if (((value = [dict valueForKey:key]) != nil) && ![value isKindOfClass:[NSNull class]]) { + // if necessary convert the W3 value to AB Property label + NSString* setValue = value; + if ([CDVContact needsConversion:key]) { // IM types must be converted + setValue = (NSString*)[CDVContact convertContactTypeToPropertyLabel:value]; + // IMs must have a valid AB value! + if ((prop == kABPersonInstantMessageProperty) && [setValue isEqualToString:(NSString*)kABOtherLabel]) { + setValue = @""; // try empty string + } + } + // set the AB value into the dictionary + [newDict setObject:setValue forKey:(NSString*)[[CDVContact defaultW3CtoAB] valueForKey:(NSString*)key]]; + } + } + + if ([newDict count] == 0) { + newDict = nil; // no items added + } + return newDict; +} + +/* set multivalue dictionary properties into an AddressBook Record + * NSArray* array - array of dictionaries containing the W3C properties to set into the record + * ABPropertyID prop - the property id for the multivalue dictionary (addresses and ims) + * ABRecordRef person - the record to set the values into + * BOOL bUpdate - YES if this is an update to an existing record + * When updating: + * empty array indicates to remove entire property + * value/label == "" indicates to remove + * value/label == [NSNull null] do not modify (keep existing record value) + * RETURN + * bool false indicates fatal error + * + * iOS addresses and im are a MultiValue Properties with label, value=dictionary of info, and id + * set addresses: streetAddress, locality, region, postalCode, country + * set ims: value = username, type = servicetype + * there are some special cases in here for ims - needs cleanup / simplification + * + */ +- (bool)setMultiValueDictionary:(NSArray*)array forProperty:(ABPropertyID)prop inRecord:(ABRecordRef)person asUpdate:(BOOL)bUpdate +{ + bool bSuccess = FALSE; + ABMutableMultiValueRef multi = nil; + + if (!bUpdate) { + multi = [self allocDictMultiValueFromArray:array forProperty:prop]; + bSuccess = [self setValue:multi forProperty:prop inRecord:person]; + } else if (bUpdate && ([array count] == 0)) { + // remove property + bSuccess = [self removeProperty:prop inRecord:person]; + } else { // check for and apply changes + ABMultiValueRef copy = ABRecordCopyValue(person, prop); + if (copy) { + multi = ABMultiValueCreateMutableCopy(copy); + CFRelease(copy); + // get the W3C values for this property + NSArray* propArray = [[CDVContact defaultObjectAndProperties] valueForKey:[[CDVContact defaultABtoW3C] objectForKey:[NSNumber numberWithInt:prop]]]; + id value; + id valueAB; + + for (NSDictionary* field in array) { + NSMutableDictionary* dict; + // find the index for the current property + id idValue = [field valueForKey:kW3ContactFieldId]; + int identifier = [idValue isKindOfClass:[NSNumber class]] ? [idValue intValue] : -1; + CFIndex idx = identifier >= 0 ? ABMultiValueGetIndexForIdentifier(multi, identifier) : kCFNotFound; + BOOL bUpdateLabel = NO; + if (idx != kCFNotFound) { + dict = [NSMutableDictionary dictionaryWithCapacity:1]; + // NSDictionary* existingDictionary = (NSDictionary*)ABMultiValueCopyValueAtIndex(multi, idx); + CFTypeRef existingDictionary = ABMultiValueCopyValueAtIndex(multi, idx); + NSString* existingABLabel = (__bridge_transfer NSString*)ABMultiValueCopyLabelAtIndex(multi, idx); + NSString* testLabel = [field valueForKey:kW3ContactFieldType]; + // fixes cb-143 where setting empty label could cause address to not be removed + // (because empty label would become 'other' in convertContactTypeToPropertyLabel + // which may not have matched existing label thus resulting in an incorrect updating of the label + // and the address not getting removed at the end of the for loop) + if (testLabel && [testLabel isKindOfClass:[NSString class]] && ([testLabel length] > 0)) { + CFStringRef w3cLabel = [CDVContact convertContactTypeToPropertyLabel:testLabel]; + if (w3cLabel && ![existingABLabel isEqualToString:(__bridge NSString*)w3cLabel]) { + // replace the label + ABMultiValueReplaceLabelAtIndex(multi, w3cLabel, idx); + bUpdateLabel = YES; + } + } // else was invalid or empty label string so do not update + + for (id k in propArray) { + value = [field valueForKey:k]; + bool bSet = (value != nil && ![value isKindOfClass:[NSNull class]] && ([value isKindOfClass:[NSString class]] && [value length] > 0)); + // if there is a contact value, put it into dictionary + if (bSet) { + NSString* setValue = [CDVContact needsConversion:(NSString*)k] ? (NSString*)[CDVContact convertContactTypeToPropertyLabel:value] : value; + [dict setObject:setValue forKey:(NSString*)[[CDVContact defaultW3CtoAB] valueForKey:(NSString*)k]]; + } else if ((value == nil) || ([value isKindOfClass:[NSString class]] && ([value length] != 0))) { + // value not provided in contact dictionary - if prop exists in AB dictionary, preserve it + valueAB = [(__bridge NSDictionary*)existingDictionary valueForKey : [[CDVContact defaultW3CtoAB] valueForKey:k]]; + if (valueAB != nil) { + [dict setValue:valueAB forKey:[[CDVContact defaultW3CtoAB] valueForKey:k]]; + } + } // else if value == "" it will not be added into updated dict and thus removed + } // end of for loop (moving here fixes cb-143, need to end for loop before replacing or removing multivalue) + + if ([dict count] > 0) { + // something was added into new dict, + ABMultiValueReplaceValueAtIndex(multi, (__bridge CFTypeRef)dict, idx); + } else if (!bUpdateLabel) { + // nothing added into new dict and no label change so remove this property entry + ABMultiValueRemoveValueAndLabelAtIndex(multi, idx); + } + + CFRelease(existingDictionary); + } else { + // not found in multivalue so add it + dict = [self translateW3Dict:field forProperty:prop]; + if (dict) { + NSMutableDictionary* addDict = [NSMutableDictionary dictionaryWithCapacity:2]; + // get the type out of the original dictionary for address + NSObject* typeValue = ((prop == kABPersonInstantMessageProperty) ? (NSString*)kABOtherLabel : (NSString*)[field valueForKey:kW3ContactFieldType]); + // NSLog(@"typeValue: %@", typeValue); + [addDict setObject:typeValue forKey:kW3ContactFieldType]; // im labels will be set as Other and address labels as type from dictionary + [addDict setObject:dict forKey:kW3ContactFieldValue]; + [self addToMultiValue:multi fromDictionary:addDict]; + } + } + } // end of looping through dictionaries + + // set the (updated) copy as the new value + bSuccess = [self setValue:multi forProperty:prop inRecord:person]; + } + } // end of copy and apply changes + if (multi) { + CFRelease(multi); + } + + return bSuccess; +} + +/* Determine which W3C labels need to be converted + */ ++ (BOOL)needsConversion:(NSString*)W3Label +{ + BOOL bConvert = NO; + + if ([W3Label isEqualToString:kW3ContactFieldType] || [W3Label isEqualToString:kW3ContactImType]) { + bConvert = YES; + } + return bConvert; +} + +/* Translation of property type labels contact API ---> iPhone + * + * phone: work, home, other, mobile, fax, pager --> + * kABWorkLabel, kABHomeLabel, kABOtherLabel, kABPersonPhoneMobileLabel, kABPersonHomeFAXLabel || kABPersonHomeFAXLabel, kABPersonPhonePagerLabel + * emails: work, home, other ---> kABWorkLabel, kABHomeLabel, kABOtherLabel + * ims: aim, gtalk, icq, xmpp, msn, skype, qq, yahoo --> kABPersonInstantMessageService + (AIM, ICG, MSN, Yahoo). No support for gtalk, xmpp, skype, qq + * addresses: work, home, other --> kABWorkLabel, kABHomeLabel, kABOtherLabel + * + * + */ ++ (CFStringRef)convertContactTypeToPropertyLabel:(NSString*)label +{ + CFStringRef type; + + if ([label isKindOfClass:[NSNull class]] || ![label isKindOfClass:[NSString class]]) { + type = NULL; // no label + } else if ([label caseInsensitiveCompare:kW3ContactWorkLabel] == NSOrderedSame) { + type = kABWorkLabel; + } else if ([label caseInsensitiveCompare:kW3ContactHomeLabel] == NSOrderedSame) { + type = kABHomeLabel; + } else if ([label caseInsensitiveCompare:kW3ContactOtherLabel] == NSOrderedSame) { + type = kABOtherLabel; + } else if ([label caseInsensitiveCompare:kW3ContactPhoneMobileLabel] == NSOrderedSame) { + type = kABPersonPhoneMobileLabel; + } else if ([label caseInsensitiveCompare:kW3ContactPhonePagerLabel] == NSOrderedSame) { + type = kABPersonPhonePagerLabel; + } else if ([label caseInsensitiveCompare:kW3ContactImAIMLabel] == NSOrderedSame) { + type = kABPersonInstantMessageServiceAIM; + } else if ([label caseInsensitiveCompare:kW3ContactImICQLabel] == NSOrderedSame) { + type = kABPersonInstantMessageServiceICQ; + } else if ([label caseInsensitiveCompare:kW3ContactImMSNLabel] == NSOrderedSame) { + type = kABPersonInstantMessageServiceMSN; + } else if ([label caseInsensitiveCompare:kW3ContactImYahooLabel] == NSOrderedSame) { + type = kABPersonInstantMessageServiceYahoo; + } else if ([label caseInsensitiveCompare:kW3ContactUrlProfile] == NSOrderedSame) { + type = kABPersonHomePageLabel; + } else { + // CB-3950 If label is not one of kW3*Label constants, threat it as custom label, + // otherwise fetching contact and then saving it will break this label in address book. + type = (__bridge CFStringRef)(label); + } + + return type; +} + ++ (NSString*)convertPropertyLabelToContactType:(NSString*)label +{ + NSString* type = nil; + + if (label != nil) { // improve efficiency...... + if ([label isEqualToString:(NSString*)kABPersonPhoneMobileLabel]) { + type = kW3ContactPhoneMobileLabel; + } else if ([label isEqualToString:(NSString*)kABPersonPhoneHomeFAXLabel] || + [label isEqualToString:(NSString*)kABPersonPhoneWorkFAXLabel]) { + type = kW3ContactPhoneFaxLabel; + } else if ([label isEqualToString:(NSString*)kABPersonPhonePagerLabel]) { + type = kW3ContactPhonePagerLabel; + } else if ([label isEqualToString:(NSString*)kABHomeLabel]) { + type = kW3ContactHomeLabel; + } else if ([label isEqualToString:(NSString*)kABWorkLabel]) { + type = kW3ContactWorkLabel; + } else if ([label isEqualToString:(NSString*)kABOtherLabel]) { + type = kW3ContactOtherLabel; + } else if ([label isEqualToString:(NSString*)kABPersonInstantMessageServiceAIM]) { + type = kW3ContactImAIMLabel; + } else if ([label isEqualToString:(NSString*)kABPersonInstantMessageServiceICQ]) { + type = kW3ContactImICQLabel; + } else if ([label isEqualToString:(NSString*)kABPersonInstantMessageServiceJabber]) { + type = kW3ContactOtherLabel; + } else if ([label isEqualToString:(NSString*)kABPersonInstantMessageServiceMSN]) { + type = kW3ContactImMSNLabel; + } else if ([label isEqualToString:(NSString*)kABPersonInstantMessageServiceYahoo]) { + type = kW3ContactImYahooLabel; + } else if ([label isEqualToString:(NSString*)kABPersonHomePageLabel]) { + type = kW3ContactUrlProfile; + } else { + // CB-3950 If label is not one of kW3*Label constants, threat it as custom label, + // otherwise fetching contact and then saving it will break this label in address book. + type = label; + } + } + return type; +} + +/* Check if the input label is a valid W3C ContactField.type. This is used when searching, + * only search field types if the search string is a valid type. If we converted any search + * string to a ABPropertyLabel it could convert to kABOtherLabel which is probably not want + * the user wanted to search for and could skew the results. + */ ++ (BOOL)isValidW3ContactType:(NSString*)label +{ + BOOL isValid = NO; + + if ([label isKindOfClass:[NSNull class]] || ![label isKindOfClass:[NSString class]]) { + isValid = NO; // no label + } else if ([label caseInsensitiveCompare:kW3ContactWorkLabel] == NSOrderedSame) { + isValid = YES; + } else if ([label caseInsensitiveCompare:kW3ContactHomeLabel] == NSOrderedSame) { + isValid = YES; + } else if ([label caseInsensitiveCompare:kW3ContactOtherLabel] == NSOrderedSame) { + isValid = YES; + } else if ([label caseInsensitiveCompare:kW3ContactPhoneMobileLabel] == NSOrderedSame) { + isValid = YES; + } else if ([label caseInsensitiveCompare:kW3ContactPhonePagerLabel] == NSOrderedSame) { + isValid = YES; + } else if ([label caseInsensitiveCompare:kW3ContactImAIMLabel] == NSOrderedSame) { + isValid = YES; + } else if ([label caseInsensitiveCompare:kW3ContactImICQLabel] == NSOrderedSame) { + isValid = YES; + } else if ([label caseInsensitiveCompare:kW3ContactImMSNLabel] == NSOrderedSame) { + isValid = YES; + } else if ([label caseInsensitiveCompare:kW3ContactImYahooLabel] == NSOrderedSame) { + isValid = YES; + } else { + isValid = NO; + } + + return isValid; +} + +/* Create a new Contact Dictionary object from an ABRecordRef that contains information in a format such that + * it can be returned to JavaScript callback as JSON object string. + * Uses: + * ABRecordRef set into Contact Object + * NSDictionary withFields indicates which fields to return from the AddressBook Record + * + * JavaScript Contact: + * @param {DOMString} id unique identifier + * @param {DOMString} displayName + * @param {ContactName} name + * @param {DOMString} nickname + * @param {ContactField[]} phoneNumbers array of phone numbers + * @param {ContactField[]} emails array of email addresses + * @param {ContactAddress[]} addresses array of addresses + * @param {ContactField[]} ims instant messaging user ids + * @param {ContactOrganization[]} organizations + * @param {DOMString} published date contact was first created + * @param {DOMString} updated date contact was last updated + * @param {DOMString} birthday contact's birthday + * @param (DOMString} anniversary contact's anniversary + * @param {DOMString} gender contact's gender + * @param {DOMString} note user notes about contact + * @param {DOMString} preferredUsername + * @param {ContactField[]} photos + * @param {ContactField[]} tags + * @param {ContactField[]} relationships + * @param {ContactField[]} urls contact's web sites + * @param {ContactAccounts[]} accounts contact's online accounts + * @param {DOMString} timezone UTC time zone offset + * @param {DOMString} connected + */ + +- (NSDictionary*)toDictionary:(NSDictionary*)withFields +{ + // if not a person type record bail out for now + if (ABRecordGetRecordType(self.record) != kABPersonType) { + return NULL; + } + id value = nil; + self.returnFields = withFields; + + NSMutableDictionary* nc = [NSMutableDictionary dictionaryWithCapacity:1]; // new contact dictionary to fill in from ABRecordRef + // id + [nc setObject:[NSNumber numberWithInt:ABRecordGetRecordID(self.record)] forKey:kW3ContactId]; + if (self.returnFields == nil) { + // if no returnFields specified, W3C says to return empty contact (but Cordova will at least return id) + return nc; + } + if ([self.returnFields objectForKey:kW3ContactDisplayName]) { + // displayname requested - iOS doesn't have so return null + [nc setObject:[NSNull null] forKey:kW3ContactDisplayName]; + // may overwrite below if requested ContactName and there are no values + } + // nickname + if ([self.returnFields valueForKey:kW3ContactNickname]) { + value = (__bridge_transfer NSString*)ABRecordCopyValue(self.record, kABPersonNicknameProperty); + [nc setObject:(value != nil) ? value:[NSNull null] forKey:kW3ContactNickname]; + } + + // name dictionary + // NSLog(@"getting name info"); + NSObject* data = [self extractName]; + if (data != nil) { + [nc setObject:data forKey:kW3ContactName]; + } + if ([self.returnFields objectForKey:kW3ContactDisplayName] && ((data == nil) || ([(NSDictionary*)data objectForKey : kW3ContactFormattedName] == [NSNull null]))) { + // user asked for displayName which iOS doesn't support but there is no other name data being returned + // try and use Composite Name so some name is returned + id tryName = (__bridge_transfer NSString*)ABRecordCopyCompositeName(self.record); + if (tryName != nil) { + [nc setObject:tryName forKey:kW3ContactDisplayName]; + } else { + // use nickname or empty string + value = (__bridge_transfer NSString*)ABRecordCopyValue(self.record, kABPersonNicknameProperty); + [nc setObject:(value != nil) ? value:@"" forKey:kW3ContactDisplayName]; + } + } + // phoneNumbers array + // NSLog(@"getting phoneNumbers"); + value = [self extractMultiValue:kW3ContactPhoneNumbers]; + if (value != nil) { + [nc setObject:value forKey:kW3ContactPhoneNumbers]; + } + // emails array + // NSLog(@"getting emails"); + value = [self extractMultiValue:kW3ContactEmails]; + if (value != nil) { + [nc setObject:value forKey:kW3ContactEmails]; + } + // urls array + value = [self extractMultiValue:kW3ContactUrls]; + if (value != nil) { + [nc setObject:value forKey:kW3ContactUrls]; + } + // addresses array + // NSLog(@"getting addresses"); + value = [self extractAddresses]; + if (value != nil) { + [nc setObject:value forKey:kW3ContactAddresses]; + } + // im array + // NSLog(@"getting ims"); + value = [self extractIms]; + if (value != nil) { + [nc setObject:value forKey:kW3ContactIms]; + } + // organization array (only info for one organization in iOS) + // NSLog(@"getting organizations"); + value = [self extractOrganizations]; + if (value != nil) { + [nc setObject:value forKey:kW3ContactOrganizations]; + } + + // for simple properties, could make this a bit more efficient by storing all simple properties in a single + // array in the returnFields dictionary and setting them via a for loop through the array + + // add dates + // NSLog(@"getting dates"); + NSNumber* ms; + + /** Contact Revision field removed from June 16, 2011 version of specification + + if ([self.returnFields valueForKey:kW3ContactUpdated]){ + ms = [self getDateAsNumber: kABPersonModificationDateProperty]; + if (!ms){ + // try and get published date + ms = [self getDateAsNumber: kABPersonCreationDateProperty]; + } + if (ms){ + [nc setObject: ms forKey:kW3ContactUpdated]; + } + + } + */ + + if ([self.returnFields valueForKey:kW3ContactBirthday]) { + ms = [self getDateAsNumber:kABPersonBirthdayProperty]; + if (ms) { + [nc setObject:ms forKey:kW3ContactBirthday]; + } + } + + /* Anniversary removed from 12-09-2010 W3C Contacts api spec + if ([self.returnFields valueForKey:kW3ContactAnniversary]){ + // Anniversary date is stored in a multivalue property + ABMultiValueRef multi = ABRecordCopyValue(self.record, kABPersonDateProperty); + if (multi){ + CFStringRef label = nil; + CFIndex count = ABMultiValueGetCount(multi); + // see if contains an Anniversary date + for(CFIndex i=0; i 0) { // ?? this will always be true since we set id,label,primary field?? + [(NSMutableArray*)addresses addObject : newAddress]; + } + CFRelease(dict); + } // end of loop through addresses + } else { + addresses = [NSNull null]; + } + if (multi) { + CFRelease(multi); + } + + return addresses; +} + +/* Create array of Dictionaries to match JavaScript ContactField object for ims + * type one of [aim, gtalk, icq, xmpp, msn, skype, qq, yahoo] needs other as well + * value + * (bool) primary + * id + * + * iOS IMs are a MultiValue Properties with label, value=dictionary of IM details (service, username), and id + */ +- (NSObject*)extractIms +{ + NSArray* fields = [self.returnFields objectForKey:kW3ContactIms]; + + if (fields == nil) { // no name fields requested + return nil; + } + NSObject* imArray; + ABMultiValueRef multi = ABRecordCopyValue(self.record, kABPersonInstantMessageProperty); + CFIndex count = multi ? ABMultiValueGetCount(multi) : 0; + if (count) { + imArray = [NSMutableArray arrayWithCapacity:count]; + + for (CFIndex i = 0; i < ABMultiValueGetCount(multi); i++) { + NSMutableDictionary* newDict = [NSMutableDictionary dictionaryWithCapacity:3]; + // iOS has label property (work, home, other) for each IM but W3C contact API doesn't use + CFDictionaryRef dict = (CFDictionaryRef)ABMultiValueCopyValueAtIndex(multi, i); + CFStringRef value; // all values should be CFStringRefs / NSString* + bool bFound; + if ([fields containsObject:kW3ContactFieldValue]) { + // value = user name + bFound = CFDictionaryGetValueIfPresent(dict, kABPersonInstantMessageUsernameKey, (void*)&value); + if (bFound && (value != NULL)) { + CFRetain(value); + [newDict setObject:(__bridge id)value forKey:kW3ContactFieldValue]; + CFRelease(value); + } else { + [newDict setObject:[NSNull null] forKey:kW3ContactFieldValue]; + } + } + if ([fields containsObject:kW3ContactFieldType]) { + bFound = CFDictionaryGetValueIfPresent(dict, kABPersonInstantMessageServiceKey, (void*)&value); + if (bFound && (value != NULL)) { + CFRetain(value); + [newDict setObject:(id)[[CDVContact class] convertPropertyLabelToContactType : (__bridge NSString*)value] forKey:kW3ContactFieldType]; + CFRelease(value); + } else { + [newDict setObject:[NSNull null] forKey:kW3ContactFieldType]; + } + } + // always set ID + id identifier = [NSNumber numberWithUnsignedInt:ABMultiValueGetIdentifierAtIndex(multi, i)]; + [newDict setObject:(identifier != nil) ? identifier:[NSNull null] forKey:kW3ContactFieldId]; + + [(NSMutableArray*)imArray addObject : newDict]; + CFRelease(dict); + } + } else { + imArray = [NSNull null]; + } + + if (multi) { + CFRelease(multi); + } + return imArray; +} + +/* Create array of Dictionaries to match JavaScript ContactOrganization object + * pref - not supported in iOS + * type - not supported in iOS + * name + * department + * title + */ + +- (NSObject*)extractOrganizations +{ + NSArray* fields = [self.returnFields objectForKey:kW3ContactOrganizations]; + + if (fields == nil) { // no name fields requested + return nil; + } + NSObject* array = nil; + NSMutableDictionary* newDict = [NSMutableDictionary dictionaryWithCapacity:5]; + id value; + int validValueCount = 0; + + for (id i in fields) { + id key = [[CDVContact defaultW3CtoAB] valueForKey:i]; + if (key && [key isKindOfClass:[NSNumber class]]) { + value = (__bridge_transfer NSString*)ABRecordCopyValue(self.record, (ABPropertyID)[[[CDVContact defaultW3CtoAB] valueForKey:i] intValue]); + if (value != nil) { + // if there are no organization values we should return null for organization + // this counter keeps indicates if any organization values have been set + validValueCount++; + } + [newDict setObject:(value != nil) ? value:[NSNull null] forKey:i]; + } else { // not a key iOS supports, set to null + [newDict setObject:[NSNull null] forKey:i]; + } + } + + if (([newDict count] > 0) && (validValueCount > 0)) { + // add pref and type + // they are not supported by iOS and thus these values never change + [newDict setObject:@"false" forKey:kW3ContactFieldPrimary]; + [newDict setObject:[NSNull null] forKey:kW3ContactFieldType]; + array = [NSMutableArray arrayWithCapacity:1]; + [(NSMutableArray*)array addObject : newDict]; + } else { + array = [NSNull null]; + } + return array; +} + +// W3C Contacts expects an array of photos. Can return photos in more than one format, currently +// just returning the default format +// Save the photo data into tmp directory and return FileURI - temp directory is deleted upon application exit +- (NSObject*)extractPhotos +{ + NSMutableArray* photos = nil; + + if (ABPersonHasImageData(self.record)) { + CFIndex photoId = ABRecordGetRecordID(self.record); + CFDataRef photoData = ABPersonCopyImageData(self.record); + if (!photoData) { + return nil; + } + + NSData* data = (__bridge NSData*)photoData; + // write to temp directory and store URI in photos array + // get the temp directory path + NSString* docsPath = [NSTemporaryDirectory()stringByStandardizingPath]; + NSString* filePath = [NSString stringWithFormat:@"%@/contact_photo_%ld", docsPath, photoId]; + + // save file + if ([data writeToFile:filePath atomically:YES]) { + photos = [NSMutableArray arrayWithCapacity:1]; + NSMutableDictionary* newDict = [NSMutableDictionary dictionaryWithCapacity:2]; + [newDict setObject:filePath forKey:kW3ContactFieldValue]; + [newDict setObject:@"url" forKey:kW3ContactFieldType]; + [newDict setObject:@"false" forKey:kW3ContactFieldPrimary]; + [photos addObject:newDict]; + } + + CFRelease(photoData); + } + return photos; +} + +/** + * given an array of W3C Contact field names, create a dictionary of field names to extract + * if field name represents an object, return all properties for that object: "name" - returns all properties in ContactName + * if field name is an explicit property, return only those properties: "name.givenName - returns a ContactName with only ContactName.givenName + * if field contains ONLY ["*"] return all fields + * dictionary format: + * key is W3Contact #define + * value is NSMutableArray* for complex keys: name,addresses,organizations, phone, emails, ims + * value is [NSNull null] for simple keys +*/ ++ (NSDictionary*)calcReturnFields:(NSArray*)fieldsArray // NSLog(@"getting self.returnFields"); +{ + NSMutableDictionary* d = [NSMutableDictionary dictionaryWithCapacity:1]; + + if ((fieldsArray != nil) && [fieldsArray isKindOfClass:[NSArray class]]) { + if (([fieldsArray count] == 1) && [[fieldsArray objectAtIndex:0] isEqualToString:@"*"]) { + return [CDVContact defaultFields]; // return all fields + } + + for (id i in fieldsArray) { + + // CB-7906 ignore NULL desired fields to avoid fatal exception + if ([i isKindOfClass:[NSNull class]]) continue; + + NSMutableArray* keys = nil; + NSString* fieldStr = nil; + if ([i isKindOfClass:[NSNumber class]]) { + fieldStr = [i stringValue]; + } else { + fieldStr = i; + } + + // see if this is specific property request in object - object.property + NSArray* parts = [fieldStr componentsSeparatedByString:@"."]; // returns original string if no separator found + NSString* name = [parts objectAtIndex:0]; + NSString* property = nil; + if ([parts count] > 1) { + property = [parts objectAtIndex:1]; + } + // see if this is a complex field by looking for its array of properties in objectAndProperties dictionary + id fields = [[CDVContact defaultObjectAndProperties] objectForKey:name]; + + // if find complex name (name,addresses,organizations, phone, emails, ims) in fields, add name as key + // with array of associated properties as the value + if ((fields != nil) && (property == nil)) { // request was for full object + keys = [NSMutableArray arrayWithArray:fields]; + if (keys != nil) { + [d setObject:keys forKey:name]; // will replace if prop array already exists + } + } else if ((fields != nil) && (property != nil)) { + // found an individual property request in form of name.property + // verify is real property name by using it as key in W3CtoAB + id abEquiv = [[CDVContact defaultW3CtoAB] objectForKey:property]; + if (abEquiv || [[CDVContact defaultW3CtoNull] containsObject:property]) { + // if existing array add to it + if ((keys = [d objectForKey:name]) != nil) { + [keys addObject:property]; + } else { + keys = [NSMutableArray arrayWithObject:property]; + [d setObject:keys forKey:name]; + } + } else { + NSLog(@"Contacts.find -- request for invalid property ignored: %@.%@", name, property); + } + } else { // is an individual property, verify is real property name by using it as key in W3CtoAB + id valid = [[CDVContact defaultW3CtoAB] objectForKey:name]; + if (valid || [[CDVContact defaultW3CtoNull] containsObject:name]) { + [d setObject:[NSNull null] forKey:name]; + } + } + } + } + if ([d count] == 0) { + // no array or nothing in the array. W3C spec says to return nothing + return nil; // [Contact defaultFields]; + } + return d; +} + + +- (BOOL)valueForKeyIsArray:(NSDictionary*)dict key:(NSString*)key +{ + BOOL bArray = NO; + NSObject* value = [dict objectForKey:key]; + + if (value) { + bArray = [value isKindOfClass:[NSArray class]]; + } + return bArray; +} + + +/* + * Search for the specified value in each of the fields specified in the searchFields dictionary. + * NSString* value - the string value to search for (need clarification from W3C on how to search for dates) + * NSDictionary* searchFields - a dictionary created via calcReturnFields where the key is the top level W3C + * object and the object is the array of specific fields within that object or null if it is a single property + * RETURNS + * YES as soon as a match is found in any of the fields + * NO - the specified value does not exist in any of the fields in this contact + * + * Note: I'm not a fan of returning in the middle of methods but have done it some in this method in order to + * keep the code simpler. bgibson + */ +- (BOOL)foundValue:(NSString*)testValue inFields:(NSDictionary*)searchFields +{ + BOOL bFound = NO; + + if ((testValue == nil) || ![testValue isKindOfClass:[NSString class]] || ([testValue length] == 0)) { + // nothing to find so return NO + return NO; + } + NSInteger valueAsInt = [testValue integerValue]; + + // per W3C spec, always include id in search + int recordId = ABRecordGetRecordID(self.record); + if (valueAsInt && (recordId == valueAsInt)) { + return YES; + } + + if (searchFields == nil) { + // no fields to search + return NO; + } + + if ([searchFields valueForKey:kW3ContactNickname]) { + bFound = [self testStringValue:testValue forW3CProperty:kW3ContactNickname]; + if (bFound == YES) { + return bFound; + } + } + + if ([self valueForKeyIsArray:searchFields key:kW3ContactName]) { + // test name fields. All are string properties obtained via ABRecordCopyValue except kW3ContactFormattedName + NSArray* fields = [searchFields valueForKey:kW3ContactName]; + + for (NSString* testItem in fields) { + if ([testItem isEqualToString:kW3ContactFormattedName]) { + NSString* propValue = (__bridge_transfer NSString*)ABRecordCopyCompositeName(self.record); + if ((propValue != nil) && ([propValue length] > 0)) { + NSRange range = [propValue rangeOfString:testValue options:NSCaseInsensitiveSearch]; + bFound = (range.location != NSNotFound); + propValue = nil; + } + } else { + bFound = [self testStringValue:testValue forW3CProperty:testItem]; + } + + if (bFound) { + break; + } + } + } + if (!bFound && [self valueForKeyIsArray:searchFields key:kW3ContactPhoneNumbers]) { + bFound = [self searchContactFields:(NSArray*)[searchFields valueForKey:kW3ContactPhoneNumbers] + forMVStringProperty:kABPersonPhoneProperty withValue:testValue]; + } + if (!bFound && [self valueForKeyIsArray:searchFields key:kW3ContactEmails]) { + bFound = [self searchContactFields:(NSArray*)[searchFields valueForKey:kW3ContactEmails] + forMVStringProperty:kABPersonEmailProperty withValue:testValue]; + } + + if (!bFound && [self valueForKeyIsArray:searchFields key:kW3ContactAddresses]) { + bFound = [self searchContactFields:[searchFields valueForKey:kW3ContactAddresses] + forMVDictionaryProperty:kABPersonAddressProperty withValue:testValue]; + } + + if (!bFound && [self valueForKeyIsArray:searchFields key:kW3ContactIms]) { + bFound = [self searchContactFields:[searchFields valueForKey:kW3ContactIms] + forMVDictionaryProperty:kABPersonInstantMessageProperty withValue:testValue]; + } + + if (!bFound && [self valueForKeyIsArray:searchFields key:kW3ContactOrganizations]) { + NSArray* fields = [searchFields valueForKey:kW3ContactOrganizations]; + + for (NSString* testItem in fields) { + bFound = [self testStringValue:testValue forW3CProperty:testItem]; + if (bFound == YES) { + break; + } + } + } + if (!bFound && [searchFields valueForKey:kW3ContactNote]) { + bFound = [self testStringValue:testValue forW3CProperty:kW3ContactNote]; + } + + // if searching for a date field is requested, get the date field as a localized string then look for match against testValue in date string + // searching for photos is not supported + if (!bFound && [searchFields valueForKey:kW3ContactBirthday]) { + bFound = [self testDateValue:testValue forW3CProperty:kW3ContactBirthday]; + } + if (!bFound && [self valueForKeyIsArray:searchFields key:kW3ContactUrls]) { + bFound = [self searchContactFields:(NSArray*)[searchFields valueForKey:kW3ContactUrls] + forMVStringProperty:kABPersonURLProperty withValue:testValue]; + } + + return bFound; +} + +- (BOOL)valueForKeyIsNumber:(NSDictionary*)dict key:(NSString*)key +{ + BOOL bNumber = NO; + NSObject* value = [dict objectForKey:key]; + + if (value) { + bNumber = [value isKindOfClass:[NSNumber class]]; + } + return bNumber; +} + +/* + * Test for the existence of a given string within the value of a ABPersonRecord string property based on the W3c property name. + * + * IN: + * NSString* testValue - the value to find - search is case insensitive + * NSString* property - the W3c property string + * OUT: + * BOOL YES if the given string was found within the property value + * NO if the testValue was not found, W3C property string was invalid or the AddressBook property was not a string + */ +- (BOOL)testStringValue:(NSString*)testValue forW3CProperty:(NSString*)property +{ + BOOL bFound = NO; + + if ([self valueForKeyIsNumber:[CDVContact defaultW3CtoAB] key:property]) { + ABPropertyID propId = [[[CDVContact defaultW3CtoAB] objectForKey:property] intValue]; + if (ABPersonGetTypeOfProperty(propId) == kABStringPropertyType) { + NSString* propValue = (__bridge_transfer NSString*)ABRecordCopyValue(self.record, propId); + if ((propValue != nil) && ([propValue length] > 0)) { + NSPredicate* containPred = [NSPredicate predicateWithFormat:@"SELF contains[cd] %@", testValue]; + bFound = [containPred evaluateWithObject:propValue]; + // NSRange range = [propValue rangeOfString:testValue options: NSCaseInsensitiveSearch]; + // bFound = (range.location != NSNotFound); + } + } + } + return bFound; +} + +/* + * Test for the existence of a given Date string within the value of a ABPersonRecord datetime property based on the W3c property name. + * + * IN: + * NSString* testValue - the value to find - search is case insensitive + * NSString* property - the W3c property string + * OUT: + * BOOL YES if the given string was found within the localized date string value + * NO if the testValue was not found, W3C property string was invalid or the AddressBook property was not a DateTime + */ +- (BOOL)testDateValue:(NSString*)testValue forW3CProperty:(NSString*)property +{ + BOOL bFound = NO; + + if ([self valueForKeyIsNumber:[CDVContact defaultW3CtoAB] key:property]) { + ABPropertyID propId = [[[CDVContact defaultW3CtoAB] objectForKey:property] intValue]; + if (ABPersonGetTypeOfProperty(propId) == kABDateTimePropertyType) { + NSDate* date = (__bridge_transfer NSDate*)ABRecordCopyValue(self.record, propId); + if (date != nil) { + NSString* dateString = [date descriptionWithLocale:[NSLocale currentLocale]]; + NSPredicate* containPred = [NSPredicate predicateWithFormat:@"SELF contains[cd] %@", testValue]; + bFound = [containPred evaluateWithObject:dateString]; + } + } + } + return bFound; +} + +/* + * Search the specified fields within an AddressBook multivalue string property for the specified test value. + * Used for phoneNumbers, emails and urls. + * IN: + * NSArray* fields - the fields to search for within the multistring property (value and/or type) + * ABPropertyID - the property to search + * NSString* testValue - the value to search for. Will convert between W3C types and AB types. Will only + * search for types if the testValue is a valid ContactField type. + * OUT: + * YES if the test value was found in one of the specified fields + * NO if the test value was not found + */ +- (BOOL)searchContactFields:(NSArray*)fields forMVStringProperty:(ABPropertyID)propId withValue:testValue +{ + BOOL bFound = NO; + + for (NSString* type in fields) { + NSString* testString = nil; + if ([type isEqualToString:kW3ContactFieldType]) { + if ([CDVContact isValidW3ContactType:testValue]) { + // only search types if the filter string is a valid ContactField.type + testString = (NSString*)[CDVContact convertContactTypeToPropertyLabel:testValue]; + } + } else { + testString = testValue; + } + + if (testString != nil) { + bFound = [self testMultiValueStrings:testString forProperty:propId ofType:type]; + } + if (bFound == YES) { + break; + } + } + + return bFound; +} + +/* + * Searches a multiString value of the specified type for the specified test value. + * + * IN: + * NSString* testValue - the value to test for + * ABPropertyID propId - the property id of the multivalue property to search + * NSString* type - the W3C contact type to search for (value or type) + * OUT: + * YES is the test value was found + * NO if the test value was not found + */ +- (BOOL)testMultiValueStrings:(NSString*)testValue forProperty:(ABPropertyID)propId ofType:(NSString*)type +{ + BOOL bFound = NO; + + if (ABPersonGetTypeOfProperty(propId) == kABMultiStringPropertyType) { + NSArray* valueArray = nil; + if ([type isEqualToString:kW3ContactFieldType]) { + valueArray = [self labelsForProperty:propId inRecord:self.record]; + } else if ([type isEqualToString:kW3ContactFieldValue]) { + valueArray = [self valuesForProperty:propId inRecord:self.record]; + } + if (valueArray) { + NSString* valuesAsString = [valueArray componentsJoinedByString:@" "]; + NSPredicate* containPred = [NSPredicate predicateWithFormat:@"SELF contains[cd] %@", testValue]; + bFound = [containPred evaluateWithObject:valuesAsString]; + } + } + return bFound; +} + +/* + * Returns the array of values for a multivalue string property of the specified property id + */ +- (__autoreleasing NSArray*)valuesForProperty:(ABPropertyID)propId inRecord:(ABRecordRef)aRecord +{ + ABMultiValueRef multi = ABRecordCopyValue(aRecord, propId); + NSArray* values = (__bridge_transfer NSArray*)ABMultiValueCopyArrayOfAllValues(multi); + + CFRelease(multi); + return values; +} + +/* + * Returns the array of labels for a multivalue string property of the specified property id + */ +- (NSArray*)labelsForProperty:(ABPropertyID)propId inRecord:(ABRecordRef)aRecord +{ + ABMultiValueRef multi = ABRecordCopyValue(aRecord, propId); + CFIndex count = ABMultiValueGetCount(multi); + NSMutableArray* labels = [NSMutableArray arrayWithCapacity:count]; + + for (int i = 0; i < count; i++) { + NSString* label = (__bridge_transfer NSString*)ABMultiValueCopyLabelAtIndex(multi, i); + if (label) { + [labels addObject:label]; + } + } + + CFRelease(multi); + return labels; +} + +/* search for values within MultiValue Dictionary properties Address or IM property + * IN: + * (NSArray*) fields - the array of W3C field names to search within + * (ABPropertyID) propId - the AddressBook property that returns a multivalue dictionary + * (NSString*) testValue - the string to search for within the specified fields + * + */ +- (BOOL)searchContactFields:(NSArray*)fields forMVDictionaryProperty:(ABPropertyID)propId withValue:(NSString*)testValue +{ + BOOL bFound = NO; + + NSArray* values = [self valuesForProperty:propId inRecord:self.record]; // array of dictionaries (as CFDictionaryRef) + NSUInteger dictCount = [values count]; + + // for ims dictionary contains with service (w3C type) and username (W3c value) + // for addresses dictionary contains street, city, state, zip, country + for (int i = 0; i < dictCount; i++) { + CFDictionaryRef dict = (__bridge CFDictionaryRef)[values objectAtIndex:i]; + + for (NSString* member in fields) { + NSString* abKey = [[CDVContact defaultW3CtoAB] valueForKey:member]; // im and address fields are all strings + CFStringRef abValue = nil; + if (abKey) { + NSString* testString = nil; + if ([member isEqualToString:kW3ContactImType]) { + if ([CDVContact isValidW3ContactType:testValue]) { + // only search service/types if the filter string is a valid ContactField.type + testString = (NSString*)[CDVContact convertContactTypeToPropertyLabel:testValue]; + } + } else { + testString = testValue; + } + if (testString != nil) { + BOOL bExists = CFDictionaryGetValueIfPresent(dict, (__bridge const void*)abKey, (void*)&abValue); + if (bExists) { + CFRetain(abValue); + NSPredicate* containPred = [NSPredicate predicateWithFormat:@"SELF contains[cd] %@", testString]; + bFound = [containPred evaluateWithObject:(__bridge id)abValue]; + CFRelease(abValue); + } + } + } + if (bFound == YES) { + break; + } + } // end of for each member in fields + + if (bFound == YES) { + break; + } + } // end of for each dictionary + + return bFound; +} + +@end