/* * PhoneGap is available under *either* the terms of the modified BSD license *or* the * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text. * * Copyright (c) 2005-2010, Nitobi Software Inc. * Copyright (c) 2010, IBM Corporation */ #import #import #import #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* com_phonegap_contacts_W3CtoAB = nil; static NSDictionary* com_phonegap_contacts_ABtoW3C = nil; static NSSet* com_phonegap_contacts_W3CtoNull = nil; static NSDictionary* com_phonegap_contacts_objectAndProperties = nil; static NSDictionary* com_phonegap_contacts_defaultFields = nil; @implementation PGContact : NSObject @synthesize record; @synthesize returnFields; - (id) init { if ((self = [super init]) != nil) self.record = ABPersonCreate(); return self; } - (id) initFromABRecord:(ABRecordRef)aRecord { if ((self = [super init]) != nil) self.record = CFRetain(aRecord); return self; } /* 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 (com_phonegap_contacts_ABtoW3C == nil) { com_phonegap_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]; // these dictionaries get invalidated without the retain, although running with GuardMalloc shows no memory overwrites??? [com_phonegap_contacts_ABtoW3C retain]; } return com_phonegap_contacts_ABtoW3C; } +(NSDictionary*) defaultW3CtoAB { if (com_phonegap_contacts_W3CtoAB == nil){ com_phonegap_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 mulitple times*/ nil]; [com_phonegap_contacts_W3CtoAB retain]; } return com_phonegap_contacts_W3CtoAB; } +(NSSet*) defaultW3CtoNull { // these are values that have no AddressBook Equivalent OR have not been implemented yet if (com_phonegap_contacts_W3CtoNull == nil){ com_phonegap_contacts_W3CtoNull = [NSSet setWithObjects: kW3ContactDisplayName, kW3ContactCategories, kW3ContactFormattedName, nil]; [com_phonegap_contacts_W3CtoNull retain]; } return com_phonegap_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 (com_phonegap_contacts_objectAndProperties == nil){ com_phonegap_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]; [com_phonegap_contacts_objectAndProperties retain]; } return com_phonegap_contacts_objectAndProperties; } +(NSDictionary*) defaultFields { if (com_phonegap_contacts_defaultFields == nil){ com_phonegap_contacts_defaultFields = [NSDictionary dictionaryWithObjectsAndKeys: [[PGContact defaultObjectAndProperties] objectForKey: kW3ContactName], kW3ContactName, [NSNull null], kW3ContactNickname, [[PGContact defaultObjectAndProperties] objectForKey: kW3ContactAddresses], kW3ContactAddresses, [[PGContact defaultObjectAndProperties] objectForKey: kW3ContactOrganizations], kW3ContactOrganizations, [[PGContact defaultObjectAndProperties] objectForKey: kW3ContactPhoneNumbers], kW3ContactPhoneNumbers, [[PGContact defaultObjectAndProperties] objectForKey: kW3ContactEmails], kW3ContactEmails, [[PGContact defaultObjectAndProperties] objectForKey: kW3ContactIms], kW3ContactIms, [[PGContact defaultObjectAndProperties] objectForKey: kW3ContactPhotos], kW3ContactPhotos, [[PGContact defaultObjectAndProperties] objectForKey: kW3ContactUrls], kW3ContactUrls, [NSNull null],kW3ContactBirthday, [NSNull null],kW3ContactNote, nil]; [com_phonegap_contacts_defaultFields retain]; } return com_phonegap_contacts_defaultFields; } +(void) releaseDefaults { // ugly but it works if (com_phonegap_contacts_ABtoW3C != nil) { [com_phonegap_contacts_ABtoW3C release]; if ([com_phonegap_contacts_ABtoW3C retainCount] == 1) com_phonegap_contacts_ABtoW3C = nil; } if (com_phonegap_contacts_W3CtoAB != nil) { [com_phonegap_contacts_W3CtoAB release]; if ([com_phonegap_contacts_W3CtoAB retainCount] == 1) com_phonegap_contacts_W3CtoAB = nil; } if (com_phonegap_contacts_W3CtoNull != nil) { [com_phonegap_contacts_W3CtoNull release]; if ([com_phonegap_contacts_W3CtoNull retainCount] == 1) com_phonegap_contacts_W3CtoNull = nil; }if (com_phonegap_contacts_objectAndProperties != nil) { [com_phonegap_contacts_objectAndProperties release]; if ([com_phonegap_contacts_objectAndProperties retainCount] == 1) com_phonegap_contacts_objectAndProperties = nil; } if (com_phonegap_contacts_defaultFields != nil) { [com_phonegap_contacts_defaultFields release]; if ([com_phonegap_contacts_defaultFields retainCount] == 1) com_phonegap_contacts_defaultFields = nil; } } /* 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:(NSMutableDictionary*) 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; NSMutableDictionary* dict = [aContact valueForKey:kW3ContactName]; if ([dict isKindOfClass:[NSDictionary class]]){ bName = true; NSArray* propArray = [[PGContact 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*)[[PGContact 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"); array = [aContact valueForKey:kW3ContactOrganizations]; // iOS only supports one organization - use first one if ([array isKindOfClass:[NSArray class]]){ NSDictionary* dict = [array objectAtIndex:0]; if ([dict isKindOfClass:[NSDictionary class]]){ [self setValue: [dict valueForKey:@"name"] forProperty: kABPersonOrganizationProperty inRecord: person asUpdate: bUpdate]; [self setValue: [dict valueForKey:kW3ContactTitle] forProperty: kABPersonJobTitleProperty inRecord: person asUpdate: bUpdate]; [self setValue: [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 // modifiction 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, (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 enry * 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 necessay??? bSuccess = ABRecordSetValue(aRecord, aProperty, 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 %@: %@", 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)){ NSString* label = (NSString*)[PGContact convertContactTypeToPropertyLabel:[dict valueForKey:kW3ContactFieldType]]; bSuccess = ABMultiValueAddValueAndLabel(multi, value,(CFStringRef)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: %@", 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: * emtpy 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 = (NSString*)[PGContact 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 = [(NSString*)ABMultiValueCopyValueAtIndex(multi, i) autorelease]; NSString* labelAB = [(NSString*)ABMultiValueCopyLabelAtIndex(multi, i) autorelease]; if (valueAB == nil || ![val isEqualToString: valueAB]){ ABMultiValueReplaceValueAtIndex(multi, val, i); } if (labelAB == nil || ![label isEqualToString:labelAB]){ ABMultiValueReplaceLabelAtIndex(multi, (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 NSObject* typeValue = ((prop == kABPersonInstantMessageProperty) ? (NSObject*)kABOtherLabel : (NSString*)[dict 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: 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 = [[PGContact defaultObjectAndProperties] valueForKey: [[PGContact 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 ([PGContact needsConversion: key]){ // IM types must be converted setValue = (NSString*)[PGContact 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*)[[PGContact 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: * emtpy 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 = [[PGContact defaultObjectAndProperties] valueForKey: [[PGContact 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); NSString* existingABLabel = [(NSString*)ABMultiValueCopyLabelAtIndex(multi, idx) autorelease]; CFStringRef w3cLabel = [PGContact convertContactTypeToPropertyLabel:[field valueForKey: kW3ContactFieldType]]; if (w3cLabel && ![existingABLabel isEqualToString:(NSString*)w3cLabel]){ //replace the label ABMultiValueReplaceLabelAtIndex(multi, w3cLabel,idx); bUpdateLabel = YES; } 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 = [PGContact needsConversion:(NSString*)k] ? (NSString*)[PGContact convertContactTypeToPropertyLabel:value] : value; [dict setObject:setValue forKey: (NSString*)[[PGContact 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 = [existingDictionary valueForKey:[[PGContact defaultW3CtoAB] valueForKey:k]]; if (valueAB != nil){ [dict setValue:valueAB forKey:[[PGContact defaultW3CtoAB] valueForKey:k]]; } } // else if value == "" it will not be added into updated dict and thus removed if ([dict count] > 0){ // something was added into new dict, ABMultiValueReplaceValueAtIndex(multi, 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) ? (NSObject*)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 { type = kABOtherLabel; } return type; } +(NSString*) convertPropertyLabelToContactType: (NSString*)label { NSString* type = nil; if (label != nil){ // improve effieciency...... 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 { type = kW3ContactOtherLabel; } } 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 PhoneGap will at least return id) return nc; } if ([self.returnFields objectForKey:kW3ContactDisplayName]){ // diplayname 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 = [(NSString*)ABRecordCopyValue(self.record, kABPersonNicknameProperty) autorelease]; [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 = [(NSString*)ABRecordCopyCompositeName(self.record) autorelease]; if (tryName != nil){ [nc setObject:tryName forKey:kW3ContactDisplayName]; } else { // use nickname or empty string value = [(NSString*)ABRecordCopyValue(self.record, kABPersonNicknameProperty) autorelease]; [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:[self extractAddresses] 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){ [(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); NSString* value; // all values should be CFStringRefs / NSString* bool bFound; if ([fields containsObject: kW3ContactFieldValue]){ // value = user name bFound = CFDictionaryGetValueIfPresent(dict, kABPersonInstantMessageUsernameKey, (void *)&value); [newDict setObject: (bFound && value != NULL) ? (id)value : [NSNull null] forKey: kW3ContactFieldValue]; } if ([fields containsObject: kW3ContactFieldType]){ bFound = CFDictionaryGetValueIfPresent(dict, kABPersonInstantMessageServiceKey, (void *)&value); [newDict setObject: (bFound && value != NULL) ? (id)[[PGContact class ]convertPropertyLabelToContactType: value] : [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 Dictionaris 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 = [NSMutableArray arrayWithCapacity:1]; NSMutableDictionary* newDict = [NSMutableDictionary dictionaryWithCapacity:5]; id value; for (id i in fields){ id key = [[PGContact defaultW3CtoAB] valueForKey:i]; if (key && [key isKindOfClass:[NSNumber class]]){ value = [(NSString *)ABRecordCopyValue(self.record, (ABPropertyID)[[[PGContact defaultW3CtoAB] valueForKey:i] intValue]) autorelease]; [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) { // 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]; [(NSMutableArray*)array addObject:newDict]; } 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)){ CFDataRef photoData = ABPersonCopyImageData(self.record); NSData* data = (NSData*)photoData; // write to temp directory and store URI in photos array // get the temp directory path NSString* docsPath = [NSTemporaryDirectory() stringByStandardizingPath]; NSError* err = nil; NSFileManager* fileMgr = [[NSFileManager alloc] init]; // generate unique file name NSString* filePath; int i=1; do { filePath = [NSString stringWithFormat:@"%@/photo_%03d.jpg", docsPath, i++]; } while([fileMgr fileExistsAtPath: filePath]); // save file if ([data writeToFile: filePath options: NSAtomicWrite error: &err]){ 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]; } [fileMgr release]; 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 [PGContact defaultFields]; // return all fields } for (id i in fieldsArray){ 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 = [[PGContact 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 = [[PGContact defaultW3CtoAB] objectForKey:property]; if (abEquiv || [[PGContact 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 = [[PGContact defaultW3CtoAB] objectForKey:name]; if (valid || [[PGContact 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; } /* * 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 ([searchFields valueForKeyIsArray: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 = [(NSString*)ABRecordCopyCompositeName(self.record) autorelease]; 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 && [searchFields valueForKeyIsArray:kW3ContactPhoneNumbers]){ bFound = [self searchContactFields: (NSArray*) [searchFields valueForKey: kW3ContactPhoneNumbers] forMVStringProperty: kABPersonPhoneProperty withValue: testValue]; } if (!bFound && [searchFields valueForKeyIsArray: kW3ContactEmails]){ bFound = [self searchContactFields: (NSArray*) [searchFields valueForKey: kW3ContactEmails] forMVStringProperty: kABPersonEmailProperty withValue: testValue]; } if (!bFound && [searchFields valueForKeyIsArray: kW3ContactAddresses]){ bFound = [self searchContactFields: [searchFields valueForKey:kW3ContactAddresses] forMVDictionaryProperty: kABPersonAddressProperty withValue: testValue]; } if (!bFound && [searchFields valueForKeyIsArray: kW3ContactIms]){ bFound = [self searchContactFields: [searchFields valueForKey:kW3ContactIms] forMVDictionaryProperty: kABPersonInstantMessageProperty withValue: testValue]; } if (!bFound && [searchFields valueForKeyIsArray: 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 && [searchFields valueForKeyIsArray: kW3ContactUrls]){ bFound = [self searchContactFields: (NSArray*) [searchFields valueForKey: kW3ContactUrls] forMVStringProperty: kABPersonURLProperty withValue: testValue]; } return bFound; } /* * 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 ([[PGContact defaultW3CtoAB] valueForKeyIsNumber: property ]) { ABPropertyID propId = [[[PGContact defaultW3CtoAB] objectForKey: property] intValue]; if(ABPersonGetTypeOfProperty(propId) == kABStringPropertyType){ NSString* propValue = [(NSString*)ABRecordCopyValue(self.record, propId) autorelease]; 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 ([[PGContact defaultW3CtoAB] valueForKeyIsNumber: property ]) { ABPropertyID propId = [[[PGContact defaultW3CtoAB] objectForKey: property] intValue]; if(ABPersonGetTypeOfProperty(propId) == kABDateTimePropertyType){ NSDate* date = [(NSString*)ABRecordCopyValue(self.record, propId) autorelease]; 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 ([PGContact isValidW3ContactType: testValue]){ // only search types if the filter string is a valid ContactField.type testString = (NSString*)[PGContact 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 */ - (NSArray *) valuesForProperty: (ABPropertyID) propId inRecord: (ABRecordRef) aRecord { ABMultiValueRef multi = ABRecordCopyValue(aRecord, propId); NSArray *values = (NSArray *)ABMultiValueCopyArrayOfAllValues(multi); CFRelease(multi); return [values autorelease]; } /* * 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 = (NSString *)ABMultiValueCopyLabelAtIndex(multi, i); if (label){ [labels addObject:label]; [label release]; } } 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 dictionaty * (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) // for ims dictionary contains with service (w3C type) and username (W3c value) // for addresses dictionary contains street, city, state, zip, country for(id dict in values){ for(NSString* member in fields){ NSString* abKey = [[PGContact defaultW3CtoAB] valueForKey:member]; // im and address fields are all strings NSString* abValue = nil; if (abKey){ NSString* testString = nil; if ([member isEqualToString:kW3ContactImType]){ if ([PGContact isValidW3ContactType: testValue]){ // only search service/types if the filter string is a valid ContactField.type testString = (NSString*)[PGContact convertContactTypeToPropertyLabel:testValue]; } } else { testString = testValue; } if(testString != nil){ BOOL bExists = CFDictionaryGetValueIfPresent((CFDictionaryRef)dict, abKey, (void *)&abValue); if(bExists) { NSPredicate *containPred = [NSPredicate predicateWithFormat:@"SELF contains[cd] %@", testString]; bFound = [containPred evaluateWithObject:abValue]; } } } if (bFound == YES) { break; } } // end of for each member in fields if (bFound == YES) { break; } } // end of for each dictionary return bFound; } - (void) dealloc { if (self.record){ CFRelease(self.record); } [super dealloc]; } @end