// // GTMABAddressBook.m // // Copyright 2008 Google Inc. // // Licensed 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 "GTMABAddressBook.h" #import "GTMTypeCasting.h" #if GTM_IPHONE_SDK #import #else // GTM_IPHONE_SDK #import #endif // GTM_IPHONE_SDK NSString *const kGTMABUnknownPropertyName = @"UNKNOWN_PROPERTY"; typedef struct { GTMABPropertyType pType; Class class; } TypeClassNameMap; @interface GTMABMultiValue () - (unsigned long*)mutations; @end @interface GTMABMutableMultiValue () // Checks to see if a value is a valid type to be stored in this multivalue - (BOOL)checkValueType:(id)value; @end @interface GTMABMultiValueEnumerator : NSEnumerator { @private GTM_WEAK ABMultiValueRef ref_; // ref_ cached from enumeree_ GTMABMultiValue *enumeree_; unsigned long mutations_; NSUInteger count_; NSUInteger index_; BOOL useLabels_; } + (id)valueEnumeratorFor:(GTMABMultiValue*)enumeree; + (id)labelEnumeratorFor:(GTMABMultiValue*)enumeree; - (id)initWithEnumeree:(GTMABMultiValue*)enumeree useLabels:(BOOL)useLabels; - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len; @end @implementation GTMABAddressBook + (GTMABAddressBook *)addressBook { return [[[self alloc] init] autorelease]; } - (id)init { if ((self = [super init])) { #if GTM_IPHONE_SDK CFErrorRef error = nil; addressBook_ = ABAddressBookCreateWithOptions(NULL, &error); if (error) { _GTMDevLog(@"ABAddressBookCreate: %@", error); CFRelease(error); } #else // GTM_IPHONE_SDK addressBook_ = ABGetSharedAddressBook(); CFRetain(addressBook_); #endif // GTM_IPHONE_SDK if (!addressBook_) { // COV_NF_START [self release]; self = nil; // COV_NF_END } } return self; } - (void)dealloc { if (addressBook_) { CFRelease(addressBook_); } [super dealloc]; } - (BOOL)save { #if GTM_IPHONE_SDK CFErrorRef cfError = NULL; bool wasGood = ABAddressBookSave(addressBook_, &cfError); if (!wasGood) { _GTMDevLog(@"Error in [%@ %@]: %@", [self class], NSStringFromSelector(_cmd), cfError); CFRelease(cfError); } #else // GTM_IPHONE_SDK bool wasGood = ABSave(addressBook_); #endif // GTM_IPHONE_SDK return wasGood ? YES : NO; } - (BOOL)hasUnsavedChanges { bool hasUnsavedChanges; #if GTM_IPHONE_SDK hasUnsavedChanges = ABAddressBookHasUnsavedChanges(addressBook_); #else // GTM_IPHONE_SDK hasUnsavedChanges = ABHasUnsavedChanges(addressBook_); #endif // GTM_IPHONE_SDK return hasUnsavedChanges ? YES : NO; } - (BOOL)addRecord:(GTMABRecord *)record { // Note: we check for bad data here because of radar // 6201258 Adding a NULL record using ABAddressBookAddRecord crashes if (!record) return NO; #if GTM_IPHONE_SDK CFErrorRef cfError = NULL; bool wasGood = ABAddressBookAddRecord(addressBook_, [record recordRef], &cfError); if (cfError) { // COV_NF_START _GTMDevLog(@"Error in [%@ %@]: %@", [self class], NSStringFromSelector(_cmd), cfError); CFRelease(cfError); // COV_NF_END } #else // GTM_IPHONE_SDK bool wasGood = ABAddRecord(addressBook_, [record recordRef]); #endif // GTM_IPHONE_SDK return wasGood ? YES : NO; } - (BOOL)removeRecord:(GTMABRecord *)record { // Note: we check for bad data here because of radar // 6201276 Removing a NULL record using ABAddressBookRemoveRecord crashes if (!record) return NO; #if GTM_IPHONE_SDK CFErrorRef cfError = NULL; bool wasGood = ABAddressBookRemoveRecord(addressBook_, [record recordRef], &cfError); if (cfError) { // COV_NF_START _GTMDevLog(@"Error in [%@ %@]: %@", [self class], NSStringFromSelector(_cmd), cfError); CFRelease(cfError); // COV_NF_END } #else // GTM_IPHONE_SDK GTMABRecordID recID = [record recordID]; ABRecordRef ref = ABCopyRecordForUniqueId(addressBook_, (CFStringRef)recID); bool wasGood = NO; if (ref) { wasGood = ABRemoveRecord(addressBook_, [record recordRef]); CFRelease(ref); } #endif // GTM_IPHONE_SDK return wasGood ? YES : NO; } - (NSArray *)people { #if GTM_IPHONE_SDK NSArray *people = GTMCFAutorelease(ABAddressBookCopyArrayOfAllPeople(addressBook_)); #else // GTM_IPHONE_SDK NSArray *people = GTMCFAutorelease(ABCopyArrayOfAllPeople(addressBook_)); #endif // GTM_IPHONE_SDK NSMutableArray *result = [NSMutableArray arrayWithCapacity:[people count]]; id person; for (person in people) { [result addObject:[GTMABPerson recordWithRecord:person]]; } return result; } - (NSArray *)groups { #if GTM_IPHONE_SDK NSArray *groups = GTMCFAutorelease(ABAddressBookCopyArrayOfAllGroups(addressBook_)); #else // GTM_IPHONE_SDK NSArray *groups = GTMCFAutorelease(ABCopyArrayOfAllGroups(addressBook_)); #endif // GTM_IPHONE_SDK NSMutableArray *result = [NSMutableArray arrayWithCapacity:[groups count]]; id group; for (group in groups) { [result addObject:[GTMABGroup recordWithRecord:group]]; } return result; } - (ABAddressBookRef)addressBookRef { return addressBook_; } - (GTMABPerson *)personForId:(GTMABRecordID)uniqueId { GTMABPerson *person = nil; #if GTM_IPHONE_SDK ABRecordRef ref = ABAddressBookGetPersonWithRecordID(addressBook_, uniqueId); #else // GTM_IPHONE_SDK ABRecordRef ref = ABCopyRecordForUniqueId(addressBook_, (CFStringRef)uniqueId); #endif // GTM_IPHONE_SDK if (ref) { person = [GTMABPerson recordWithRecord:ref]; } return person; } - (GTMABGroup *)groupForId:(GTMABRecordID)uniqueId { GTMABGroup *group = nil; #if GTM_IPHONE_SDK ABRecordRef ref = ABAddressBookGetGroupWithRecordID(addressBook_, uniqueId); #else // GTM_IPHONE_SDK ABRecordRef ref = ABCopyRecordForUniqueId(addressBook_, (CFStringRef)uniqueId); #endif // GTM_IPHONE_SDK if (ref) { group = [GTMABGroup recordWithRecord:ref]; } return group; } // Performs a prefix search on the composite names of people in an address book // and returns an array of persons that match the search criteria. - (NSArray *)peopleWithCompositeNameWithPrefix:(NSString *)prefix { #if GTM_IPHONE_SDK NSArray *people = GTMCFAutorelease(ABAddressBookCopyPeopleWithName(addressBook_, (CFStringRef)prefix)); NSMutableArray *gtmPeople = [NSMutableArray arrayWithCapacity:[people count]]; id person; for (person in people) { GTMABPerson *gtmPerson = [GTMABPerson recordWithRecord:person]; [gtmPeople addObject:gtmPerson]; } return gtmPeople; #else // TODO(dmaclach): Change over to recordsMatchingSearchElement as an // optimization? // TODO(dmaclach): Make this match the way that the iPhone does it (by // checking both first and last names) and adding unittests for all this. NSArray *people = [self people]; NSMutableArray *foundPeople = [NSMutableArray array]; GTMABPerson *person; for (person in people) { NSString *compositeName = [person compositeName]; NSRange range = [compositeName rangeOfString:prefix options:(NSCaseInsensitiveSearch | NSDiacriticInsensitiveSearch | NSWidthInsensitiveSearch | NSAnchoredSearch)]; if (range.location != NSNotFound) { [foundPeople addObject:person]; } } return foundPeople; #endif } // Performs a prefix search on the composite names of groups in an address book // and returns an array of groups that match the search criteria. - (NSArray *)groupsWithCompositeNameWithPrefix:(NSString *)prefix { NSArray *groups = [self groups]; NSMutableArray *foundGroups = [NSMutableArray array]; GTMABGroup *group; for (group in groups) { NSString *compositeName = [group compositeName]; NSRange range = [compositeName rangeOfString:prefix options:(NSCaseInsensitiveSearch | NSDiacriticInsensitiveSearch | NSWidthInsensitiveSearch | NSAnchoredSearch)]; if (range.location != NSNotFound) { [foundGroups addObject:group]; } } return foundGroups; } + (NSString *)localizedLabel:(NSString *)label { #if GTM_IPHONE_SDK return GTMCFAutorelease(ABAddressBookCopyLocalizedLabel((CFStringRef)label)); #else // GTM_IPHONE_SDK return GTMCFAutorelease(ABCopyLocalizedPropertyOrLabel((CFStringRef)label)); #endif // GTM_IPHONE_SDK } @end @implementation GTMABRecord + (id)recordWithRecord:(ABRecordRef)record { return [[[self alloc] initWithRecord:record] autorelease]; } - (id)initWithRecord:(ABRecordRef)record { if ((self = [super init])) { if ([self class] == [GTMABRecord class]) { [self autorelease]; [self doesNotRecognizeSelector:_cmd]; } if (!record) { [self release]; self = nil; } else { record_ = (ABRecordRef)CFRetain(record); } } return self; } - (NSUInteger)hash { // This really isn't completely valid due to // 6203836 ABRecords hash to their address // but it's the best we can do without knowing what properties // are in a record, and we don't have an API for that. return CFHash(record_); } - (BOOL)isEqual:(id)object { // This really isn't completely valid due to // 6203836 ABRecords hash to their address // but it's the best we can do without knowing what properties // are in a record, and we don't have an API for that. return [object respondsToSelector:@selector(recordRef)] && CFEqual(record_, [object recordRef]); } - (void)dealloc { if (record_) { CFRelease(record_); } [super dealloc]; } - (ABRecordRef)recordRef { return record_; } - (GTMABRecordID)recordID { #if GTM_IPHONE_SDK return ABRecordGetRecordID(record_); #else // GTM_IPHONE_SDK return GTMCFAutorelease(ABRecordCopyUniqueId(record_)); #endif // GTM_IPHONE_SDK } - (id)valueForProperty:(GTMABPropertyID)property { #if GTM_IPHONE_SDK id value = GTMCFAutorelease(ABRecordCopyValue(record_, property)); #else // GTM_IPHONE_SDK id value = GTMCFAutorelease(ABRecordCopyValue(record_, (CFStringRef)property)); #endif // GTM_IPHONE_SDK if (value) { if ([[self class] typeOfProperty:property] & kABMultiValueMask) { value = [[[GTMABMultiValue alloc] initWithMultiValue:(ABMultiValueRef)value] autorelease]; } } return value; } - (BOOL)setValue:(id)value forProperty:(GTMABPropertyID)property { if (!value) return NO; // We check the type here because of // Radar 6201046 ABRecordSetValue returns true even if you pass in a bad type // for a value TypeClassNameMap fullTypeMap[] = { { kGTMABStringPropertyType, [NSString class] }, { kGTMABIntegerPropertyType, [NSNumber class] }, { kGTMABRealPropertyType, [NSNumber class] }, { kGTMABDateTimePropertyType, [NSDate class] }, { kGTMABDictionaryPropertyType, [NSDictionary class] }, { kGTMABMultiStringPropertyType, [GTMABMultiValue class] }, { kGTMABMultiRealPropertyType, [GTMABMultiValue class] }, { kGTMABMultiDateTimePropertyType, [GTMABMultiValue class] }, { kGTMABMultiDictionaryPropertyType, [GTMABMultiValue class] } }; GTMABPropertyType type = [[self class] typeOfProperty:property]; BOOL wasFound = NO; for (size_t i = 0; i < sizeof(fullTypeMap) / sizeof(TypeClassNameMap); ++i) { if (fullTypeMap[i].pType == type) { wasFound = YES; if (![[value class] isSubclassOfClass:fullTypeMap[i].class]) { return NO; } } } if (!wasFound) { return NO; } if (type & kABMultiValueMask) { value = (id)[value multiValueRef]; } #if GTM_IPHONE_SDK CFErrorRef cfError = nil; bool wasGood = ABRecordSetValue(record_, property, (CFTypeRef)value, &cfError); if (cfError) { // COV_NF_START _GTMDevLog(@"Error in [%@ %@]: %@", [self class], NSStringFromSelector(_cmd), cfError); CFRelease(cfError); // COV_NF_END } #else // GTM_IPHONE_SDK bool wasGood = ABRecordSetValue(record_, (CFStringRef)property, (CFTypeRef)value); #endif // GTM_IPHONE_SDK return wasGood ? YES : NO; } - (BOOL)removeValueForProperty:(GTMABPropertyID)property { #if GTM_IPHONE_SDK CFErrorRef cfError = nil; // We check to see if the value is in the property because of: // Radar 6201005 ABRecordRemoveValue returns true for value that aren't // in the record id value = [self valueForProperty:property]; bool wasGood = value && ABRecordRemoveValue(record_, property, &cfError); if (cfError) { // COV_NF_START _GTMDevLog(@"Error in [%@ %@]: %@", [self class], NSStringFromSelector(_cmd), cfError); CFRelease(cfError); // COV_NF_END } #else // GTM_IPHONE_SDK id value = [self valueForProperty:property]; bool wasGood = value && ABRecordRemoveValue(record_, (CFStringRef)property); #endif // GTM_IPHONE_SDK return wasGood ? YES : NO; } // COV_NF_START // All of these methods are to be overridden by their subclasses - (NSString *)compositeName { [self doesNotRecognizeSelector:_cmd]; return nil; } + (GTMABPropertyType)typeOfProperty:(GTMABPropertyID)property { [self doesNotRecognizeSelector:_cmd]; return kGTMABInvalidPropertyType; } + (NSString *)localizedPropertyName:(GTMABPropertyID)property { [self doesNotRecognizeSelector:_cmd]; return nil; } // COV_NF_END @end @implementation GTMABPerson + (GTMABPerson *)personWithFirstName:(NSString *)first lastName:(NSString *)last { GTMABPerson *person = [[[self alloc] init] autorelease]; if (person) { BOOL isGood = YES; if (first) { isGood = [person setValue:first forProperty:kGTMABPersonFirstNameProperty]; } if (isGood && last) { isGood = [person setValue:last forProperty:kGTMABPersonLastNameProperty]; } if (!isGood) { // COV_NF_START // Marked as NF because I don't know how to force an error person = nil; // COV_NF_END } } return person; } - (id)init { ABRecordRef person = ABPersonCreate(); self = [super initWithRecord:person]; if (person) { CFRelease(person); } return self; } - (BOOL)setImageData:(NSData *)data { #if GTM_IPHONE_SDK CFErrorRef cfError = NULL; bool wasGood = NO; if (!data) { wasGood = ABPersonRemoveImageData([self recordRef], &cfError); } else { // We verify that the data is good because of: // Radar 6202868 ABPersonSetImageData should validate image data UIImage *image = [UIImage imageWithData:data]; wasGood = image && ABPersonSetImageData([self recordRef], (CFDataRef)data, &cfError); } if (cfError) { // COV_NF_START _GTMDevLog(@"Error in [%@ %@]: %@", [self class], NSStringFromSelector(_cmd), cfError); CFRelease(cfError); // COV_NF_END } #else // GTM_IPHONE_SDK bool wasGood = YES; if (data) { NSImage *image = [[[NSImage alloc] initWithData:data] autorelease]; wasGood = image != nil; } wasGood = wasGood && ABPersonSetImageData([self recordRef], (CFDataRef)data); #endif // GTM_IPHONE_SDK return wasGood ? YES : NO; } - (GTMABImage *)image { NSData *data = [self imageData]; #if GTM_IPHONE_SDK return [UIImage imageWithData:data]; #else // GTM_IPHONE_SDK return [[[NSImage alloc] initWithData:data] autorelease]; #endif // GTM_IPHONE_SDK } - (BOOL)setImage:(GTMABImage *)image { #if GTM_IPHONE_SDK NSData *data = UIImagePNGRepresentation(image); #else // GTM_IPHONE_SDK NSData *data = [image TIFFRepresentation]; #endif // GTM_IPHONE_SDK return [self setImageData:data]; } - (NSData *)imageData { return GTMCFAutorelease(ABPersonCopyImageData([self recordRef])); } - (NSString *)compositeName { #if GTM_IPHONE_SDK return GTMCFAutorelease(ABRecordCopyCompositeName([self recordRef])); #else // GTM_IPHONE_SDK NSNumber *nsFlags = [self valueForProperty:kABPersonFlags]; NSInteger flags = [nsFlags longValue]; NSString *compositeName = nil; if (flags & kABShowAsCompany) { compositeName = [self valueForProperty:kABOrganizationProperty]; } else { NSString *firstName = [self valueForProperty:kGTMABPersonFirstNameProperty]; NSString *lastName = [self valueForProperty:kGTMABPersonLastNameProperty]; if (firstName && lastName) { GTMABPersonCompositeNameFormat format; if (flags & kABFirstNameFirst) { format = kABPersonCompositeNameFormatFirstNameFirst; } else if (flags & kABLastNameFirst) { format = kABPersonCompositeNameFormatLastNameFirst; } else { format = [[self class] compositeNameFormat]; } if (format == kABPersonCompositeNameFormatLastNameFirst) { NSString *tempStr = lastName; lastName = firstName; firstName = tempStr; } compositeName = [NSString stringWithFormat:@"%@ %@", firstName, lastName]; } else if (firstName) { compositeName = firstName; } else if (lastName) { compositeName = lastName; } else { compositeName = @""; } } return compositeName; #endif // GTM_IPHONE_SDK } - (NSString *)description { #if GTM_IPHONE_SDK return [NSString stringWithFormat:@"%@ %@ %@ %d", [self class], [self valueForProperty:kGTMABPersonFirstNameProperty], [self valueForProperty:kGTMABPersonLastNameProperty], [self recordID]]; #else // GTM_IPHONE_SDK return [NSString stringWithFormat:@"%@ %@ %@ %@", [self class], [self valueForProperty:kGTMABPersonFirstNameProperty], [self valueForProperty:kGTMABPersonLastNameProperty], [self recordID]]; #endif // GTM_IPHONE_SDK } + (NSString *)localizedPropertyName:(GTMABPropertyID)property { #if GTM_IPHONE_SDK return GTMCFAutorelease(ABPersonCopyLocalizedPropertyName(property)); #else // GTM_IPHONE_SDK return ABLocalizedPropertyOrLabel(property); #endif // GTM_IPHONE_SDK } + (GTMABPersonCompositeNameFormat)compositeNameFormat { #if GTM_IPHONE_SDK #if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_7_0 return ABPersonGetCompositeNameFormat(); #else return ABPersonGetCompositeNameFormatForRecord(NULL); #endif // __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_7_0 #else // GTM_IPHONE_SDK NSInteger nameOrdering = [[ABAddressBook sharedAddressBook] defaultNameOrdering]; return nameOrdering == kABFirstNameFirst ? kABPersonCompositeNameFormatFirstNameFirst : kABPersonCompositeNameFormatLastNameFirst; #endif // GTM_IPHONE_SDK } + (GTMABPropertyType)typeOfProperty:(GTMABPropertyID)property { #if GTM_IPHONE_SDK return ABPersonGetTypeOfProperty(property); #else // GTM_IPHONE_SDK return ABTypeOfProperty([[GTMABAddressBook addressBook] addressBookRef], (CFStringRef)kABPersonRecordType, (CFStringRef)property); #endif // GTM_IPHONE_SDK } @end @implementation GTMABGroup + (GTMABGroup *)groupNamed:(NSString *)name { GTMABGroup *group = [[[self alloc] init] autorelease]; if (group) { if (![group setValue:name forProperty:kABGroupNameProperty]) { // COV_NF_START // Can't get setValue to fail for me group = nil; // COV_NF_END } } return group; } - (id)init { ABRecordRef group = ABGroupCreate(); self = [super initWithRecord:group]; if (group) { CFRelease(group); } return self; } - (NSArray *)members { NSArray *people = GTMCFAutorelease(ABGroupCopyArrayOfAllMembers([self recordRef])); NSMutableArray *gtmPeople = [NSMutableArray arrayWithCapacity:[people count]]; id person; for (person in people) { [gtmPeople addObject:[GTMABPerson recordWithRecord:(ABRecordRef)person]]; } return gtmPeople; } - (BOOL)addMember:(GTMABPerson *)person { #if GTM_IPHONE_SDK CFErrorRef cfError = nil; // We check for person because of // Radar 6202860 Passing nil person into ABGroupAddMember crashes bool wasGood = person && ABGroupAddMember([self recordRef], [person recordRef], &cfError); if (cfError) { // COV_NF_START _GTMDevLog(@"Error in [%@ %@]: %@", [self class], NSStringFromSelector(_cmd), cfError); CFRelease(cfError); // COV_NF_END } #else // GTM_IPHONE_SDK bool wasGood = person && ABGroupAddMember([self recordRef], [person recordRef]); #endif // GTM_IPHONE_SDK return wasGood ? YES : NO; } - (BOOL)removeMember:(GTMABPerson *)person { #if GTM_IPHONE_SDK CFErrorRef cfError = nil; // We check for person because of // Radar 6202860 Passing nil person into ABGroupAddMember crashes // (I know this is remove, but it crashes there too) bool wasGood = person && ABGroupRemoveMember([self recordRef], [person recordRef], &cfError); if (cfError) { // COV_NF_START _GTMDevLog(@"Error in [%@ %@]: %@", [self class], NSStringFromSelector(_cmd), cfError); CFRelease(cfError); // COV_NF_END } #else // GTM_IPHONE_SDK bool wasGood = person != nil; if (wasGood) { NSArray *array = GTMCFAutorelease(ABPersonCopyParentGroups([person recordRef])); if ([array containsObject:[self recordRef]]) { wasGood = ABGroupRemoveMember([self recordRef], [person recordRef]); } else { wasGood = NO; } } #endif // GTM_IPHONE_SDK return wasGood ? YES : NO; } - (NSString *)compositeName { #if GTM_IPHONE_SDK return GTMCFAutorelease(ABRecordCopyCompositeName([self recordRef])); #else // GTM_IPHONE_SDK return [self valueForProperty:kGTMABGroupNameProperty]; #endif // GTM_IPHONE_SDK } + (GTMABPropertyType)typeOfProperty:(GTMABPropertyID)property { GTMABPropertyType type = kGTMABInvalidPropertyType; if (property == kABGroupNameProperty) { type = kGTMABStringPropertyType; } return type; } + (NSString *)localizedPropertyName:(GTMABPropertyID)property { NSString *name = kGTMABUnknownPropertyName; if (property == kABGroupNameProperty) { name = NSLocalizedStringFromTable(@"Name", @"GTMABAddressBook", @"name property"); } return name; } - (NSString *)description { #if GTM_IPHONE_SDK return [NSString stringWithFormat:@"%@ %@ %d", [self class], [self valueForProperty:kABGroupNameProperty], [self recordID]]; #else // GTM_IPHONE_SDK return [NSString stringWithFormat:@"%@ %@ %@", [self class], [self valueForProperty:kABGroupNameProperty], [self recordID]]; #endif // GTM_IPHONE_SDK } @end @implementation GTMABMultiValue - (id)init { // Call super init and release so we don't leak [[super init] autorelease]; [self doesNotRecognizeSelector:_cmd]; return nil; // COV_NF_LINE } - (id)initWithMultiValue:(ABMultiValueRef)multiValue { if ((self = [super init])) { if (!multiValue) { [self release]; self = nil; } else { multiValue_ = CFRetain(multiValue); } } return self; } - (id)copyWithZone:(NSZone *)zone { return [[GTMABMultiValue alloc] initWithMultiValue:multiValue_]; } - (id)mutableCopyWithZone:(NSZone *)zone { return [[GTMABMutableMultiValue alloc] initWithMultiValue:multiValue_]; } - (NSUInteger)hash { // I'm implementing hash instead of using CFHash(multiValue_) because // 6203854 ABMultiValues hash to their address NSUInteger count = [self count]; NSUInteger hash = 0; for (NSUInteger i = 0; i < count; ++i) { NSString *label = [self labelAtIndex:i]; id value = [self valueAtIndex:i]; hash += [label hash]; hash += [value hash]; } return hash; } - (BOOL)isEqual:(id)object { // I'm implementing isEqual instea of using CFEquals(multiValue,...) because // 6203854 ABMultiValues hash to their address // and it appears CFEquals just calls through to hash to compare them. BOOL isEqual = NO; if ([object respondsToSelector:@selector(multiValueRef)]) { isEqual = multiValue_ == [object multiValueRef]; if (!isEqual) { NSUInteger count = [self count]; NSUInteger objCount = [(GTMABMultiValue *)object count]; isEqual = count == objCount; for (NSUInteger i = 0; isEqual && i < count; ++i) { NSString *label = [self labelAtIndex:i]; NSString *objLabel = [object labelAtIndex:i]; isEqual = [label isEqual:objLabel]; if (isEqual) { id value = [self valueAtIndex:i]; GTMABMultiValue *multiValueObject = GTM_STATIC_CAST(GTMABMultiValue, object); id objValue = [multiValueObject valueAtIndex:i]; isEqual = [value isEqual:objValue]; } } } } return isEqual; } - (void)dealloc { if (multiValue_) { CFRelease(multiValue_); } [super dealloc]; } - (ABMultiValueRef)multiValueRef { return multiValue_; } - (NSUInteger)count { #if GTM_IPHONE_SDK return ABMultiValueGetCount(multiValue_); #else // GTM_IPHONE_SDK return ABMultiValueCount(multiValue_); #endif // GTM_IPHONE_SDK } - (id)valueAtIndex:(NSUInteger)idx { id value = nil; if (idx < [self count]) { value = GTMCFAutorelease(ABMultiValueCopyValueAtIndex(multiValue_, idx)); ABPropertyType type = [self propertyType]; if (type == kGTMABIntegerPropertyType || type == kGTMABRealPropertyType || type == kGTMABDictionaryPropertyType) { // This is because of // 6208390 Integer and real values don't work in ABMultiValueRefs // Apparently they forget to add a ref count on int, real and // dictionary values in ABMultiValueCopyValueAtIndex, although they do // remember them for all other types. // Once they fix this, this will lead to a leak, but I figure the leak // is better than the crash. Our unittests will test to make sure that // this is the case, and once we find a system that has this fixed, we // can conditionalize this code. Look for testRadar6208390 in // GTMABAddressBookTest.m // Also, search for 6208390 below and fix the fast enumerator to actually // be somewhat performant when this is fixed. #ifndef __clang_analyzer__ [value retain]; #endif // __clang_analyzer__ } } return value; } - (NSString *)labelAtIndex:(NSUInteger)idx { NSString *label = nil; if (idx < [self count]) { label = GTMCFAutorelease(ABMultiValueCopyLabelAtIndex(multiValue_, idx)); } return label; } - (GTMABMultiValueIdentifier)identifierAtIndex:(NSUInteger)idx { GTMABMultiValueIdentifier identifier = kGTMABMultiValueInvalidIdentifier; if (idx < [self count]) { #if GTM_IPHONE_SDK identifier = ABMultiValueGetIdentifierAtIndex(multiValue_, idx); #else // GTM_IPHONE_SDK identifier = GTMCFAutorelease(ABMultiValueCopyIdentifierAtIndex(multiValue_, idx)); #endif // GTM_IPHONE_SDK } return identifier; } - (NSUInteger)indexForIdentifier:(GTMABMultiValueIdentifier)identifier { #if GTM_IPHONE_SDK NSUInteger idx = ABMultiValueGetIndexForIdentifier(multiValue_, identifier); #else // GTM_IPHONE_SDK NSUInteger idx = ABMultiValueIndexForIdentifier(multiValue_, (CFStringRef)identifier); #endif // GTM_IPHONE_SDK return idx == (NSUInteger)kCFNotFound ? (NSUInteger)NSNotFound : idx; } - (GTMABPropertyType)propertyType { #if GTM_IPHONE_SDK return ABMultiValueGetPropertyType(multiValue_); #else // GTM_IPHONE_SDK return ABMultiValuePropertyType(multiValue_); #endif // GTM_IPHONE_SDK } - (id)valueForIdentifier:(GTMABMultiValueIdentifier)identifier { return [self valueAtIndex:[self indexForIdentifier:identifier]]; } - (NSString *)labelForIdentifier:(GTMABMultiValueIdentifier)identifier { return [self labelAtIndex:[self indexForIdentifier:identifier]]; } - (unsigned long*)mutations { // We just need some constant non-zero value here so fast enumeration works. // Dereferencing self should give us the isa which will stay constant // over the enumeration. return (unsigned long*)self; } - (NSEnumerator *)valueEnumerator { return [GTMABMultiValueEnumerator valueEnumeratorFor:self]; } - (NSEnumerator *)labelEnumerator { return [GTMABMultiValueEnumerator labelEnumeratorFor:self]; } @end @implementation GTMABMutableMultiValue + (id)valueWithPropertyType:(GTMABPropertyType)type { return [[[self alloc] initWithPropertyType:type] autorelease]; } - (id)initWithPropertyType:(GTMABPropertyType)type { ABMutableMultiValueRef ref = nil; if (type != kGTMABInvalidPropertyType) { #if GTM_IPHONE_SDK ref = ABMultiValueCreateMutable(type); #else // GTM_IPHONE_SDK ref = ABMultiValueCreateMutable(); #endif // GTM_IPHONE_SDK } self = [super initWithMultiValue:ref]; if (ref) { CFRelease(ref); } return self; } - (id)initWithMultiValue:(ABMultiValueRef)multiValue { ABMutableMultiValueRef ref = nil; if (multiValue) { ref = ABMultiValueCreateMutableCopy(multiValue); } self = [super initWithMultiValue:ref]; if (ref) { CFRelease(ref); } return self; } - (id)initWithMutableMultiValue:(ABMutableMultiValueRef)multiValue { return [super initWithMultiValue:multiValue]; } - (BOOL)checkValueType:(id)value { BOOL isGood = NO; if (value) { TypeClassNameMap singleValueTypeMap[] = { { kGTMABStringPropertyType, [NSString class] }, { kGTMABIntegerPropertyType, [NSNumber class] }, { kGTMABRealPropertyType, [NSNumber class] }, { kGTMABDateTimePropertyType, [NSDate class] }, { kGTMABDictionaryPropertyType, [NSDictionary class] }, }; GTMABPropertyType type = [self propertyType] & ~kABMultiValueMask; #if GTM_MACOS_SDK // Since on the desktop mutables don't have a type UNTIL they have // something in them, return YES if it's empty. if ((type == 0) && ([self count] == 0)) return YES; #endif // GTM_MACOS_SDK for (size_t i = 0; i < sizeof(singleValueTypeMap) / sizeof(TypeClassNameMap); ++i) { if (singleValueTypeMap[i].pType == type) { if ([[value class] isSubclassOfClass:singleValueTypeMap[i].class]) { isGood = YES; break; } } } } return isGood; } - (GTMABMultiValueIdentifier)addValue:(id)value withLabel:(CFStringRef)label { GTMABMultiValueIdentifier identifier = kGTMABMultiValueInvalidIdentifier; // We check label and value here because of // radar 6202827 Passing nil info ABMultiValueAddValueAndLabel causes crash bool wasGood = label && [self checkValueType:value]; if (wasGood) { #if GTM_IPHONE_SDK wasGood = ABMultiValueAddValueAndLabel(multiValue_, value, label, &identifier); #else // GTM_IPHONE_SDK wasGood = ABMultiValueAdd((ABMutableMultiValueRef)multiValue_, value, label, (CFStringRef *)&identifier); #endif // GTM_IPHONE_SDK } if (!wasGood) { identifier = kGTMABMultiValueInvalidIdentifier; } else { mutations_++; } return identifier; } - (GTMABMultiValueIdentifier)insertValue:(id)value withLabel:(CFStringRef)label atIndex:(NSUInteger)idx { GTMABMultiValueIdentifier identifier = kGTMABMultiValueInvalidIdentifier; // We perform a check here to ensure that we don't get bitten by // Radar 6202807 ABMultiValueInsertValueAndLabelAtIndex allows you to insert // values past end NSUInteger count = [self count]; // We check label and value here because of // radar 6202827 Passing nil info ABMultiValueAddValueAndLabel causes crash bool wasGood = idx <= count && label && [self checkValueType:value]; if (wasGood) { #if GTM_IPHONE_SDK wasGood = ABMultiValueInsertValueAndLabelAtIndex(multiValue_, value, label, idx, &identifier); #else // GTM_IPHONE_SDK wasGood = ABMultiValueInsert((ABMutableMultiValueRef)multiValue_, value, label, idx, (CFStringRef *)&identifier); #endif // GTM_IPHONE_SDK } if (!wasGood) { identifier = kGTMABMultiValueInvalidIdentifier; } else { mutations_++; } return identifier; } - (BOOL)removeValueAndLabelAtIndex:(NSUInteger)idx { BOOL isGood = NO; NSUInteger count = [self count]; if (idx < count) { #if GTM_IPHONE_SDK bool wasGood = ABMultiValueRemoveValueAndLabelAtIndex(multiValue_, idx); #else // GTM_IPHONE_SDK bool wasGood = ABMultiValueRemove((ABMutableMultiValueRef)multiValue_, idx); #endif // GTM_IPHONE_SDK if (wasGood) { mutations_++; isGood = YES; } } return isGood; } - (BOOL)replaceValueAtIndex:(NSUInteger)idx withValue:(id)value { BOOL isGood = NO; NSUInteger count = [self count]; if (idx < count && [self checkValueType:value]) { #if GTM_IPHONE_SDK bool goodReplace = ABMultiValueReplaceValueAtIndex(multiValue_, value, idx); #else // GTM_IPHONE_SDK bool goodReplace = ABMultiValueReplaceValue((ABMutableMultiValueRef)multiValue_, (CFTypeRef)value, idx); #endif // GTM_IPHONE_SDK if (goodReplace) { mutations_++; isGood = YES; } } return isGood; } - (BOOL)replaceLabelAtIndex:(NSUInteger)idx withLabel:(CFStringRef)label { BOOL isGood = NO; NSUInteger count = [self count]; if (idx < count) { #if GTM_IPHONE_SDK bool goodReplace = ABMultiValueReplaceLabelAtIndex(multiValue_, label, idx); #else // GTM_IPHONE_SDK bool goodReplace = ABMultiValueReplaceLabel((ABMutableMultiValueRef)multiValue_, (CFTypeRef)label, idx); #endif // GTM_IPHONE_SDK if (goodReplace) { mutations_++; isGood = YES; } } return isGood; } - (unsigned long*)mutations { return &mutations_; } @end @implementation GTMABMultiValueEnumerator + (id)valueEnumeratorFor:(GTMABMultiValue*)enumeree { return [[[self alloc] initWithEnumeree:enumeree useLabels:NO] autorelease]; } + (id)labelEnumeratorFor:(GTMABMultiValue*)enumeree { return [[[self alloc] initWithEnumeree:enumeree useLabels:YES] autorelease]; } - (id)initWithEnumeree:(GTMABMultiValue*)enumeree useLabels:(BOOL)useLabels { if ((self = [super init])) { if (enumeree) { enumeree_ = [enumeree retain]; useLabels_ = useLabels; } else { // COV_NF_START // Since this is a private class where the enumeree creates us // there is no way we should ever get here. [self release]; self = nil; // COV_NF_END } } return self; } - (void)dealloc { [enumeree_ release]; [super dealloc]; } - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len { NSUInteger i; if (!ref_) { count_ = [enumeree_ count]; ref_ = [enumeree_ multiValueRef]; } for (i = 0; state->state < count_ && i < len; ++i, ++state->state) { if (useLabels_) { stackbuf[i] = GTMCFAutorelease(ABMultiValueCopyLabelAtIndex(ref_, state->state)); } else { // TODO(dmaclach) Check this on Mac Desktop and use fast path if we can // Yes this is slow, but necessary in light of radar 6208390 // Once this is fixed we can go to something similar to the label // case which should speed stuff up again. Hopefully anybody who wants // real performance is willing to move down to the C API anyways. stackbuf[i] = [enumeree_ valueAtIndex:state->state]; } } state->itemsPtr = stackbuf; state->mutationsPtr = [enumeree_ mutations]; return i; } - (id)nextObject { id value = nil; if (!ref_) { count_ = [enumeree_ count]; mutations_ = *[enumeree_ mutations]; ref_ = [enumeree_ multiValueRef]; } if (mutations_ != *[enumeree_ mutations]) { NSString *reason = [NSString stringWithFormat:@"*** Collection <%@> was " "mutated while being enumerated", enumeree_]; [[NSException exceptionWithName:NSGenericException reason:reason userInfo:nil] raise]; } if (index_ < count_) { if (useLabels_) { value = GTMCFAutorelease(ABMultiValueCopyLabelAtIndex(ref_, index_)); } else { // TODO(dmaclach) Check this on Mac Desktop and use fast path if we can // Yes this is slow, but necessary in light of radar 6208390 // Once this is fixed we can go to something similar to the label // case which should speed stuff up again. Hopefully anybody who wants // real performance is willing to move down to the C API anyways. value = [enumeree_ valueAtIndex:index_]; } index_ += 1; } return value; } @end