diff options
Diffstat (limited to 'AddressBook/GTMABAddressBook.m')
-rw-r--r-- | AddressBook/GTMABAddressBook.m | 1186 |
1 files changed, 1186 insertions, 0 deletions
diff --git a/AddressBook/GTMABAddressBook.m b/AddressBook/GTMABAddressBook.m new file mode 100644 index 0000000..b7920fe --- /dev/null +++ b/AddressBook/GTMABAddressBook.m @@ -0,0 +1,1186 @@ +// +// GTMAddressBook.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 "GTMGarbageCollection.h" + +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 + __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; +#if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 +- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state + objects:(id *)stackbuf + count:(NSUInteger)len; +#endif // MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 +@end + +@implementation GTMABAddressBook ++ (GTMABAddressBook *)addressBook { + return [[[self alloc] init] autorelease]; +} + +- (id)init { + if ((self = [super init])) { +#if GTM_IPHONE_SDK + addressBook_ = ABAddressBookCreate(); +#else // GTM_IPHONE_SDK + addressBook_ = ABGetSharedAddressBook(); +#endif // GTM_IPHONE_SDK + if (!addressBook_) { + // COV_NF_START + [self release]; + self = nil; + // COV_NF_END + } + } + return self; +} + +- (void)dealloc { +#if GTM_IPHONE_SDK + if (addressBook_) { + CFRelease(addressBook_); + } +#endif // GTM_IPHONE_SDK + [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; + GTM_FOREACH_OBJECT(person, 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; + GTM_FOREACH_OBJECT(group, 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 { + // TODO(dmaclach): Change over to recordsMatchingSearchElement as an + // optimization? + NSArray *people = [self people]; + NSMutableArray *foundPeople = [NSMutableArray array]; + GTMABPerson *person; + GTM_FOREACH_OBJECT(person, people) { + NSString *compositeName = [person compositeName]; + NSRange range = [compositeName rangeOfString:prefix + options:(NSCaseInsensitiveSearch + | NSAnchoredSearch)]; + if (range.location != NSNotFound) { + [foundPeople addObject:person]; + } + } + return foundPeople; +} + +// 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; + GTM_FOREACH_OBJECT(group, groups) { + NSString *compositeName = [group compositeName]; + NSRange range = [compositeName rangeOfString:prefix + options:(NSCaseInsensitiveSearch + | 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 { +#ifdef 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 release]; + 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) { + wasGood = [[[NSImage alloc] initWithData:data] autorelease] != 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 { + return [NSString stringWithFormat:@"%@ %@ %@ %d", + [self class], + [self valueForProperty:kGTMABPersonFirstNameProperty], + [self valueForProperty:kGTMABPersonLastNameProperty], + [self recordID]]; +} + ++ (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 + return ABPersonGetCompositeNameFormat(); +#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 release]; + 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; + GTM_FOREACH_OBJECT(person, 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 { + return [NSString stringWithFormat:@"%@ %@ %d", + [self class], + [self valueForProperty:kABGroupNameProperty], + [self recordID]]; +} +@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 = [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]; + id objValue = [(GTMABMultiValue*)object 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. + [value retain]; + } + } + 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]; +} + +#if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 +- (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; +} +#endif // MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 + +- (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 + |