blob: d92cb083ddc16be4cceae5765d31d83ed868714a [file] [log] [blame]
//
// 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 <UIKit/UIKit.h>
#else // GTM_IPHONE_SDK
#import <Cocoa/Cocoa.h>
#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