From e9fe951720e98ca768e2c51d05fa257ecfd6b894 Mon Sep 17 00:00:00 2001 From: "gtm.daemon" Date: Wed, 6 May 2009 17:30:40 +0000 Subject: [Author: dmaclach] Made GTMABAddressBook work on both the iPhone and the Desktop, giving you a single interface to both. DELTA=2539 (2173 added, 79 deleted, 287 changed) R=thomasvl --- AddressBook/GTMABAddressBook.h | 403 ++++++++++++ AddressBook/GTMABAddressBook.m | 1186 ++++++++++++++++++++++++++++++++++ AddressBook/GTMABAddressBook.strings | Bin 0 -> 1428 bytes AddressBook/GTMABAddressBookTest.m | 689 ++++++++++++++++++++ AddressBook/TestData/phone.png | Bin 0 -> 3242 bytes 5 files changed, 2278 insertions(+) create mode 100644 AddressBook/GTMABAddressBook.h create mode 100644 AddressBook/GTMABAddressBook.m create mode 100644 AddressBook/GTMABAddressBook.strings create mode 100644 AddressBook/GTMABAddressBookTest.m create mode 100644 AddressBook/TestData/phone.png (limited to 'AddressBook') diff --git a/AddressBook/GTMABAddressBook.h b/AddressBook/GTMABAddressBook.h new file mode 100644 index 0000000..a3dfba3 --- /dev/null +++ b/AddressBook/GTMABAddressBook.h @@ -0,0 +1,403 @@ +// +// GTMABAddressBook.h +// +// 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. +// + +// These classes wrap up the iPhone AddressBook 'C' API in a manner very +// similar to that found on Mac OS X. They differ only in that none of these +// routines throws, and some of the types are different as necessitated by +// the APIs that they wrap. These wrappers also protect you from a number +// of issues in the AddressBook API (as of iPhone SDK 2.0/2.1) +// +// Note that there is a strings file that you may want to localize +// (GTMABAddressBook.strings). +// +// If things seem strange, it may be due to one of the following radars: +// 6240394 AddressBook framework constants not initialized until +// ABCreateAddressBook called +// -- CLOSED as designed +// 6208390 Integer and real values don't work in ABMultiValueRefs +// (and this isn't part of the title, but dictionaries don't work +// either) +// 6207605 RecordIDs for people and groups are not unique in AddressBook +// -- CLOSED as designed +// 6204021 kABGroupNameProperty and kABPersonFirstNameProperty have the same +// value +// 6203982 ABPersonCopyLocalizedPropertyName returns name for +// kABGroupNameProperty +// 6203961 ABPersonGetTypeOfProperty returns a type for kABGroupNameProperty +// 6203854 ABMultiValues hash to their address +// 6203836 ABRecords hash to their address +// -- CLOSED behaves correctly +// 6203606 Need CFTypeIDs for AddressBook CFTypes +// 6202868 ABPersonSetImageData should validate image data +// 6202860 Passing nil person into ABGroupAddMember crashes +// -- CLOSED behaves correctly +// 6202827 Passing nil info ABMultiValueAddValueAndLabel causes crash +// -- CLOSED behaves correctly +// 6202807 ABMultiValueInsertValueAndLabelAtIndex allows you to insert values +// past end +// 6201276 Removing a NULL record using ABAddressBookRemoveRecord crashes +// -- CLOSED behaves correctly +// 6201258 Adding a NULL record using ABAddressBookAddRecord crashes +// -- CLOSED behaves correctly +// 6201046 ABRecordSetValue returns true even if you pass in a bad type for a +// value +// 6201005 ABRecordRemoveValue returns true for value that aren't in the record +// -- CLOSED behaves correctly +// 6200703 ABAddressBookAddRecord doesn't add an item to the people array until +// it's saved +// 6200638 ABAddressBookHasUnsavedChanges doesn't work + +#import "GTMDefines.h" +#import + +#if GTM_IPHONE_SDK +#import +#else // GTM_IPHONE_SDK +#import +#import +#endif // GTM_IPHONE_SDK + +@class GTMABPerson; +@class GTMABGroup; +@class GTMABRecord; + +GTM_EXTERN NSString *const kGTMABUnknownPropertyName; + +#if GTM_IPHONE_SDK + +@class UIImage; +typedef ABRecordID GTMABRecordID; +typedef ABPropertyID GTMABPropertyID; +typedef UIImage GTMABImage; +typedef ABPersonCompositeNameFormat GTMABPersonCompositeNameFormat; +typedef ABMultiValueIdentifier GTMABMultiValueIdentifier; + enum _GTMABPropertyType { + kGTMABInvalidPropertyType = kABInvalidPropertyType, + kGTMABStringPropertyType = kABStringPropertyType, + kGTMABIntegerPropertyType = kABIntegerPropertyType, + kGTMABRealPropertyType = kABRealPropertyType, + kGTMABDateTimePropertyType = kABDateTimePropertyType, + kGTMABDictionaryPropertyType = kABDictionaryPropertyType, + kGTMABMultiStringPropertyType = kABMultiStringPropertyType, + kGTMABMultiIntegerPropertyType = kABMultiIntegerPropertyType, + kGTMABMultiRealPropertyType = kABMultiRealPropertyType, + kGTMABMultiDateTimePropertyType = kABMultiDateTimePropertyType, + kGTMABMultiDictionaryPropertyType = kABMultiDictionaryPropertyType, +}; +typedef CFIndex GTMABPropertyType; +#define kGTMABPersonFirstNameProperty kABPersonFirstNameProperty +#define kGTMABPersonLastNameProperty kABPersonLastNameProperty +#define kGTMABPersonBirthdayProperty kABPersonBirthdayProperty +#define kGTMABPersonPhoneProperty kABPersonPhoneProperty +#define kGTMABPersonPhoneMainLabel kABPersonPhoneMainLabel +#define kGTMABGroupNameProperty kABGroupNameProperty + +#define kGTMABMultiValueInvalidIdentifier kABMultiValueInvalidIdentifier +#define kGTMABRecordInvalidID kABRecordInvalidID + +#else // GTM_IPHONE_SDK + +typedef NSString* GTMABRecordID; +typedef NSString* GTMABPropertyID; +typedef NSString* GTMABMultiValueIdentifier; +typedef NSImage GTMABImage; +typedef uint32_t GTMABPersonCompositeNameFormat; +enum { + kABPersonCompositeNameFormatFirstNameFirst = 0, + kABPersonCompositeNameFormatLastNameFirst = 1 +}; +enum _GTMABPropertyType { + kGTMABInvalidPropertyType = kABErrorInProperty, + kGTMABStringPropertyType = kABStringProperty, + kGTMABIntegerPropertyType = kABIntegerProperty, + kGTMABRealPropertyType = kABRealProperty, + kGTMABDateTimePropertyType = kABDateProperty, + kGTMABDictionaryPropertyType = kABDictionaryProperty, + kGTMABMultiStringPropertyType = kABMultiStringProperty, + kGTMABMultiIntegerPropertyType = kABMultiIntegerProperty, + kGTMABMultiRealPropertyType = kABMultiRealProperty, + kGTMABMultiDateTimePropertyType = kABMultiDateProperty, + kGTMABMultiDictionaryPropertyType = kABMultiDictionaryProperty, +}; +typedef CFIndex GTMABPropertyType; +#define kGTMABPersonFirstNameProperty kABFirstNameProperty +#define kGTMABPersonLastNameProperty kABLastNameProperty +#define kGTMABPersonBirthdayProperty kABBirthdayProperty +#define kGTMABPersonPhoneProperty kABPhoneProperty +#define kGTMABPersonPhoneMainLabel kABPhoneMainLabel +#define kGTMABGroupNameProperty kABGroupNameProperty + +#define kGTMABMultiValueInvalidIdentifier @"ABMultiValueInvalidIdentifier" +#define kGTMABRecordInvalidID @"ABRecordInvalidID" +extern NSString* const kABPersonRecordType; +extern NSString* const kABGroupRecordType; + +#endif // GTM_IPHONE_SDK + +// Wrapper for an AddressBook on iPhone +@interface GTMABAddressBook : NSObject { + @private + GTM_WEAK_ON_MACOS ABAddressBookRef addressBook_; +} + +// Returns a new instance of an address book. ++ (GTMABAddressBook *)addressBook; + +// Return the address book reference +- (ABAddressBookRef)addressBookRef; + +// Saves changes made since the last save +// Return YES if successful (or there was no change) +- (BOOL)save; + +// Returns YES if there are unsaved changes +// The unsaved changes flag is automatically set when changes are made +// As of iPhone 2.1, this does not work, and will always return NO. +// Radar 6200638: ABAddressBookHasUnsavedChanges doesn't work +- (BOOL)hasUnsavedChanges; + +// Returns a GTMABPerson matching an ID +// Returns nil if the record could not be found +- (GTMABPerson *)personForId:(GTMABRecordID)uniqueId; + +// Returns a GTMABGroup matching an ID +// Returns nil if the record could not be found +- (GTMABGroup *)groupForId:(GTMABRecordID)uniqueId; + +// Adds a record (ABPerson or ABGroup) to the AddressBook database +// Be sure to read notes for -people and -group. +- (BOOL)addRecord:(GTMABRecord *)record; + +// Removes a record (ABPerson or ABGroup) from the AddressBook database +- (BOOL)removeRecord:(GTMABRecord *)record; + +// Returns an array (GTMABPerson) of all the people in the AddressBook database +// As of iPhone 2.1, this array will not contain new entries until you save +// the address book. +// Radar 6200703: ABAddressBookAddRecord doesn't add an item to the people array +// until it's saved +- (NSArray *)people; + +// Returns an array of all the groups (GTMABGroup) in the AddressBook database +// As of iPhone 2.1, this array will not contain new entries until you save +// the address book. +// Radar 6200703: ABAddressBookAddRecord doesn't add an item to the people array +// until it's saved +- (NSArray *)groups; + +// 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. +// Ignores case. +- (NSArray *)peopleWithCompositeNameWithPrefix:(NSString *)prefix; + +// 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. +// Ignores case. +- (NSArray *)groupsWithCompositeNameWithPrefix:(NSString *)prefix; + +// Returns a localized name for a given label ++ (NSString *)localizedLabel:(NSString *)label; + +@end + +// Wrapper for a ABRecord on iPhone. +// A abstract class. Instantiate one of the concrete subclasses, GTMABPerson or +// GTMABGroup. +@interface GTMABRecord : NSObject { + @private + ABRecordRef record_; +} + +// Create a record with a recordRef. +// Since GTMABRecord is an abstract base class, attempting to create one +// of these directly will throw an exception. Use with one of the concrete +// subclasses. ++ (id)recordWithRecord:(ABRecordRef)record; + +// Designated initializer +// Since GTMABRecord is an abstract base class, attempting to create one +// of these directly will throw an exception. Use with one of the concrete +// subclasses. +- (id)initWithRecord:(ABRecordRef)record; + +// Return our recordRef +- (ABRecordRef)recordRef; + +// Return the recordID for the record +- (GTMABRecordID)recordID; + +// Returns the value of a given property. +// The type of the value depends on the property type. +- (id)valueForProperty:(GTMABPropertyID)property; + +// Set the value of a given property. +// The type of the value must match the property type. +// Returns YES if value set properly +- (BOOL)setValue:(id)value forProperty:(GTMABPropertyID)property; + +// Removes the value for the property +// Returns yes if value removed +- (BOOL)removeValueForProperty:(GTMABPropertyID)property; + +// returns a human friendly name for the record +- (NSString *)compositeName; + +// returns the type of a property ++ (GTMABPropertyType)typeOfProperty:(GTMABPropertyID)property; + +// returns a human friendly localized name for a property ++ (NSString *)localizedPropertyName:(GTMABPropertyID)property; +@end + +// Wrapper for an ABPerson on iPhone +@interface GTMABPerson : GTMABRecord + +// Creates a person with a first name and a last name. ++ (GTMABPerson *)personWithFirstName:(NSString *)first + lastName:(NSString *)last; + +// Sets image data for a person. Data must be to a block of data that +// will create a valid GTMABImage. +- (BOOL)setImageData:(NSData *)data; + +// Returns the image data. +- (NSData *)imageData; + +// Returns the image for a person +- (GTMABImage *)image; + +// Sets a the image for a person +- (BOOL)setImage:(GTMABImage *)image; + +// Returns the format in with names are composited ++ (GTMABPersonCompositeNameFormat)compositeNameFormat; +@end + +// Wrapper for a ABGroup on iPhone +@interface GTMABGroup : GTMABRecord +// Create a new group named |name| ++ (GTMABGroup *)groupNamed:(NSString *)name; + +// Return an array of members (GTMABPerson) +- (NSArray *)members; + +// Add a member to a group +- (BOOL)addMember:(GTMABPerson *)person; + +// Remove a member from a group +- (BOOL)removeMember:(GTMABPerson *)person; +@end + +// GTMABMultiValue does not support NSFastEnumeration because in +// the Apple frameworks it returns identifiers which are already NSStrings. +// In our case identifiers aren't NS types, and it doesn't make sense +// to convert them to NSNumbers just to convert them back so you can +// actually get at the values and labels. +// Instead we supply valueEnumerator and labelEnumerator which you can +// fast enumerate on to get values and labels directly. +@interface GTMABMultiValue : NSObject { + @protected + ABMultiValueRef multiValue_; +} + +// Create a multi value +- (id)initWithMultiValue:(ABMultiValueRef)multiValue; + +// return it's ref +- (ABMultiValueRef)multiValueRef; + +// Returns the number of value/label pairs +- (NSUInteger)count; + +// Returns a value at a given index +// Returns nil if index is out of bounds +- (id)valueAtIndex:(NSUInteger)idx; + +// Returns a label at a given index +// Returns nil if index is out of bounds +- (NSString *)labelAtIndex:(NSUInteger)idx; + +// Returns an identifier at a given index +// Returns kABMultiValueInvalidIdentifier if index is out of bounds +- (GTMABMultiValueIdentifier)identifierAtIndex:(NSUInteger)idx; + +// Returns the index of a given identifier +// Returns NSNotFound if not found +- (NSUInteger)indexForIdentifier:(GTMABMultiValueIdentifier)identifier; + +// Type of the contents of this multivalue +- (GTMABPropertyType)propertyType; + +// Returns the value for a given identifier +// Returns nil if the identifier is not found +- (id)valueForIdentifier:(GTMABMultiValueIdentifier)identifier; + +// Returns the value for a given identifier +// Returns nil if the identifier is not found +- (NSString *)labelForIdentifier:(GTMABMultiValueIdentifier)identifier; + +// Returns an enumerator for enumerating through values +- (NSEnumerator *)valueEnumerator; + +// Returns an enumerator for enumerating through labels +- (NSEnumerator *)labelEnumerator; + +@end + +@interface GTMABMutableMultiValue : GTMABMultiValue { + @private + // Use unsigned long here instead of NSUInteger because that's what + // NSFastEnumeration Protocol wants currently (iPhone 2.1) + unsigned long mutations_; +} + +// Create a new mutable multivalue with a given type ++ (id)valueWithPropertyType:(GTMABPropertyType)type; + +// Create a new mutable multivalue with a given type +- (id)initWithPropertyType:(GTMABPropertyType)type; + +// Create a new mutable multivalue based on |multiValue| +- (id)initWithMutableMultiValue:(ABMutableMultiValueRef)multiValue; + +// Adds a value with its label +// Returns the identifier if successful, kABMultiValueInvalidIdentifier +// otherwise. +- (GTMABMultiValueIdentifier)addValue:(id)value withLabel:(CFStringRef)label; + +// Insert a value/label pair at a given index +// Returns the identifier if successful. kABMultiValueInvalidIdentifier +// otherwise +// If index is out of bounds, returns kABMultiValueInvalidIdentifier. +- (GTMABMultiValueIdentifier)insertValue:(id)value + withLabel:(CFStringRef)label + atIndex:(NSUInteger)index; + +// Removes a value/label pair at a given index +// Returns NO if index out of bounds +- (BOOL)removeValueAndLabelAtIndex:(NSUInteger)index; + +// Replaces a value at a given index +// Returns NO if index out of bounds +- (BOOL)replaceValueAtIndex:(NSUInteger)index withValue:(id)value; + +// Replaces a label at a given index +// Returns NO if index out of bounds +- (BOOL)replaceLabelAtIndex:(NSUInteger)index withLabel:(CFStringRef)label; + +@end 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 + diff --git a/AddressBook/GTMABAddressBook.strings b/AddressBook/GTMABAddressBook.strings new file mode 100644 index 0000000..b5e010e Binary files /dev/null and b/AddressBook/GTMABAddressBook.strings differ diff --git a/AddressBook/GTMABAddressBookTest.m b/AddressBook/GTMABAddressBookTest.m new file mode 100644 index 0000000..73b0eca --- /dev/null +++ b/AddressBook/GTMABAddressBookTest.m @@ -0,0 +1,689 @@ +// +// GTMAddressBookTest.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 "GTMSenTestCase.h" +#import "GTMABAddressBook.h" + +static NSString *const kGTMABTestFirstName = @"GTMABAddressBookTestFirstName"; +static NSString *const kGTMABTestLastName = @"GTMABAddressBookTestLastName"; +static NSString *const kGTMABTestGroupName = @"GTMABAddressBookTestGroupName"; + +@interface GTMABAddressBookTest : GTMTestCase { + @private + GTMABAddressBook *book_; +} +@end + + +@implementation GTMABAddressBookTest +- (void)setUp { + // Create a book forcing it out of it's autorelease pool. + // I force it out of the release pool, so that we will see any errors + // for it immediately at teardown, and it will be clear which release + // caused us problems. + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + book_ = [[GTMABAddressBook addressBook] retain]; + [pool release]; + STAssertNotNil(book_, nil); + NSArray *people + = [book_ peopleWithCompositeNameWithPrefix:kGTMABTestFirstName]; + GTMABPerson *person; + GTM_FOREACH_OBJECT(person, people) { + [book_ removeRecord:person]; + } + NSArray *groups + = [book_ groupsWithCompositeNameWithPrefix:kGTMABTestGroupName]; + GTMABGroup *group; + GTM_FOREACH_OBJECT(group, groups) { + [book_ removeRecord:group]; + } + [book_ save]; +} + +- (void)tearDown { + [book_ release]; +} + +- (void)testGenericAddressBook { + STAssertEqualObjects([GTMABAddressBook localizedLabel:(NSString *)kABHomeLabel], + @"home", + nil); + STAssertThrows([GTMABRecord recordWithRecord:nil], nil); +} + +- (void)testAddingAndRemovingPerson { + // Create a person + GTMABPerson *person = [GTMABPerson personWithFirstName:kGTMABTestFirstName + lastName:kGTMABTestLastName]; + STAssertNotNil(person, nil); + + // Add person + NSArray *people = [book_ people]; + STAssertFalse([people containsObject:person], nil); + STAssertTrue([book_ addRecord:person], nil); +#if GTM_IPHONE_SDK + // Normally this next line would be STAssertTrue, however due to + // Radar 6200638: ABAddressBookHasUnsavedChanges doesn't work + // We will check to make sure it stays broken ;-) + STAssertFalse([book_ hasUnsavedChanges], nil); +#else // GTM_IPHONE_SDK + STAssertTrue([book_ hasUnsavedChanges], nil); +#endif // GTM_IPHONE_SDK + + people = [book_ people]; + STAssertNotNil(people, nil); +#if GTM_IPHONE_SDK + // Normally this next line would be STAssertTrue, however due to + // Radar 6200703: ABAddressBookAddRecord doesn't add an item to the people + // array until it's saved + // We will check to make sure it stays broken ;-) + STAssertFalse([people containsObject:person], nil); +#else // GTM_IPHONE_SDK + STAssertTrue([people containsObject:person], nil); +#endif // GTM_IPHONE_SDK + + // Save book_ + STAssertTrue([book_ save], nil); + people = [book_ people]; + STAssertNotNil(people, nil); + STAssertTrue([people containsObject:person], nil); + people = [book_ peopleWithCompositeNameWithPrefix:kGTMABTestFirstName]; + STAssertEqualObjects([people objectAtIndex:0], person, nil); + + GTMABRecordID recordID = [person recordID]; + STAssertNotEquals(recordID, kGTMABRecordInvalidID, nil); + + GTMABRecord *record = [book_ personForId:recordID]; + STAssertEqualObjects(record, person, nil); + + // Remove person + STAssertTrue([book_ removeRecord:person], nil); + people = [book_ peopleWithCompositeNameWithPrefix:kGTMABTestFirstName]; + STAssertEquals([people count], (NSUInteger)0, nil); + +#if GTM_IPHONE_SDK + // Normally this next line would be STAssertTrue, however due to + // Radar 6200638: ABAddressBookHasUnsavedChanges doesn't work + // We will check to make sure it stays broken ;-) + STAssertFalse([book_ hasUnsavedChanges], nil); +#else // GTM_IPHONE_SDK + STAssertTrue([book_ hasUnsavedChanges], nil); +#endif // GTM_IPHONE_SDK + people = [book_ people]; + STAssertFalse([people containsObject:person], nil); + + // Save Book + STAssertTrue([book_ save], nil); + people = [book_ people]; + STAssertFalse([book_ hasUnsavedChanges], nil); + STAssertFalse([people containsObject:person], nil); + record = [book_ personForId:recordID]; + STAssertNil(record, nil); + + // Bogus data + STAssertFalse([book_ addRecord:nil], nil); + STAssertFalse([book_ removeRecord:nil], nil); + + STAssertNotNULL([book_ addressBookRef], nil); + +} + +- (void)testAddingAndRemovingGroup { + // Create a group + GTMABGroup *group = [GTMABGroup groupNamed:kGTMABTestGroupName]; + STAssertNotNil(group, nil); + + // Add group + NSArray *groups = [book_ groups]; + STAssertFalse([groups containsObject:group], nil); + STAssertTrue([book_ addRecord:group], nil); +#if GTM_IPHONE_SDK + // Normally this next line would be STAssertTrue, however due to + // Radar 6200638: ABAddressBookHasUnsavedChanges doesn't work + // We will check to make sure it stays broken ;-) + STAssertFalse([book_ hasUnsavedChanges], nil); +#else // GTM_IPHONE_SDK + STAssertTrue([book_ hasUnsavedChanges], nil); +#endif // GTM_IPHONE_SDK + + groups = [book_ groups]; + STAssertNotNil(groups, nil); +#if GTM_IPHONE_SDK + // Normally this next line would be STAssertTrue, however due to + // Radar 6200703: ABAddressBookAddRecord doesn't add an item to the groups + // array until it's saved + // We will check to make sure it stays broken ;-) + STAssertFalse([groups containsObject:group], nil); +#else // GTM_IPHONE_SDK + STAssertTrue([groups containsObject:group], nil); +#endif // GTM_IPHONE_SDK + + // Save book_ + STAssertTrue([book_ save], nil); + groups = [book_ groups]; + STAssertNotNil(groups, nil); + STAssertTrue([groups containsObject:group], nil); + groups = [book_ groupsWithCompositeNameWithPrefix:kGTMABTestGroupName]; + STAssertEqualObjects([groups objectAtIndex:0], group, nil); + + GTMABRecordID recordID = [group recordID]; + STAssertNotEquals(recordID, kGTMABRecordInvalidID, nil); + + GTMABRecord *record = [book_ groupForId:recordID]; + STAssertEqualObjects(record, group, nil); + + // Remove group + STAssertTrue([book_ removeRecord:group], nil); + +#if GTM_IPHONE_SDK + // Normally this next line would be STAssertTrue, however due to + // Radar 6200638: ABAddressBookHasUnsavedChanges doesn't work + // We will check to make sure it stays broken ;-) + STAssertFalse([book_ hasUnsavedChanges], nil); +#else // GTM_IPHONE_SDK + STAssertTrue([book_ hasUnsavedChanges], nil); +#endif // GTM_IPHONE_SDK + groups = [book_ groups]; + STAssertFalse([groups containsObject:group], nil); + + // Save Book + STAssertTrue([book_ save], nil); + groups = [book_ groups]; + STAssertFalse([book_ hasUnsavedChanges], nil); + STAssertFalse([groups containsObject:group], nil); + groups = [book_ groupsWithCompositeNameWithPrefix:kGTMABTestGroupName]; + STAssertEquals([groups count], (NSUInteger)0, nil); + record = [book_ groupForId:recordID]; + STAssertNil(record, nil); +} + +- (void)testPerson { + GTMABPerson *person = [[[GTMABPerson alloc] initWithRecord:nil] autorelease]; + STAssertNil(person, nil); + person = [GTMABPerson personWithFirstName:kGTMABTestFirstName + lastName:nil]; + STAssertNotNil(person, nil); + STAssertEqualObjects([person compositeName], kGTMABTestFirstName, nil); + NSString *firstName = [person valueForProperty:kGTMABPersonFirstNameProperty]; + STAssertEqualObjects(firstName, kGTMABTestFirstName, nil); + NSString *lastName = [person valueForProperty:kGTMABPersonLastNameProperty]; + STAssertNil(lastName, nil); + STAssertTrue([person removeValueForProperty:kGTMABPersonFirstNameProperty], nil); + STAssertFalse([person removeValueForProperty:kGTMABPersonFirstNameProperty], nil); + STAssertFalse([person removeValueForProperty:kGTMABPersonLastNameProperty], nil); + STAssertFalse([person setValue:nil forProperty:kGTMABPersonFirstNameProperty], nil); + STAssertFalse([person setValue:[NSNumber numberWithInt:1] + forProperty:kGTMABPersonFirstNameProperty], nil); + STAssertFalse([person setValue:@"Bart" + forProperty:kGTMABPersonBirthdayProperty], nil); + + GTMABPropertyType property + = [GTMABPerson typeOfProperty:kGTMABPersonLastNameProperty]; + STAssertEquals(property, (GTMABPropertyType)kGTMABStringPropertyType, nil); + + NSString *string + = [GTMABPerson localizedPropertyName:kGTMABPersonLastNameProperty]; + STAssertEqualObjects(string, @"Last", nil); + + string = [GTMABPerson localizedPropertyName:kGTMABRecordInvalidID]; +#ifdef GTM_IPHONE_SDK + STAssertEqualObjects(string, kGTMABUnknownPropertyName, nil); +#else // GTM_IPHONE_SDK + STAssertEqualObjects(string, kGTMABRecordInvalidID, nil); +#endif // GTM_IPHONE_SDK + string = [person description]; + STAssertNotNil(string, nil); + + GTMABPersonCompositeNameFormat format = [GTMABPerson compositeNameFormat]; + STAssertTrue(format == kABPersonCompositeNameFormatFirstNameFirst || + format == kABPersonCompositeNameFormatLastNameFirst, nil); + + NSData *data = [person imageData]; + STAssertNil(data, nil); + STAssertTrue([person setImageData:nil], nil); + data = [person imageData]; + STAssertNil(data, nil); + NSBundle *bundle = [NSBundle bundleForClass:[self class]]; + NSString *phonePath = [bundle pathForResource:@"phone" ofType:@"png"]; + STAssertNotNil(phonePath, nil); + GTMABImage *image + = [[[GTMABImage alloc] initWithContentsOfFile:phonePath] autorelease]; + STAssertNotNil(image, nil); +#ifdef GTM_IPHONE_SDK + data = UIImagePNGRepresentation(image); +#else // GTM_IPHONE_SDK + data = [image TIFFRepresentation]; +#endif // GTM_IPHONE_SDK + STAssertTrue([person setImageData:data], nil); + NSData *data2 = [person imageData]; + STAssertEqualObjects(data, data2, nil); + STAssertTrue([person setImageData:nil], nil); + data = [person imageData]; + STAssertNil(data, nil); + + STAssertTrue([person setImage:image], nil); + GTMABImage *image2 = [person image]; + STAssertNotNil(image2, nil); +#ifdef GTM_IPHONE_SDK + STAssertEqualObjects(UIImagePNGRepresentation(image), + UIImagePNGRepresentation(image2), nil); +#else // GTM_IPHONE_SDK + STAssertEqualObjects([image TIFFRepresentation], + [image2 TIFFRepresentation], nil); +#endif // GTM_IPHONE_SDK + + person = [GTMABPerson personWithFirstName:kGTMABTestFirstName + lastName:kGTMABTestLastName]; + + data = [NSData dataWithBytes:"a" length:1]; + STAssertFalse([person setImageData:data], nil); + + GTMABMutableMultiValue *value + = [GTMABMutableMultiValue valueWithPropertyType:kGTMABStringPropertyType]; + STAssertNotNil(value, nil); + STAssertNotEquals([value addValue:@"222-222-2222" + withLabel:(CFStringRef)kABHomeLabel], + kGTMABMultiValueInvalidIdentifier, nil); + STAssertNotEquals([value addValue:@"333-333-3333" + withLabel:(CFStringRef)kABWorkLabel], + kGTMABMultiValueInvalidIdentifier, nil); + STAssertTrue([person setValue:value + forProperty:kGTMABPersonPhoneProperty], nil); + id value2 = [person valueForProperty:kGTMABPersonPhoneProperty]; + STAssertNotNil(value2, nil); + STAssertEqualObjects(value, value2, nil); + STAssertEquals([value hash], [value2 hash], nil); + STAssertNotEquals([person hash], (NSUInteger)0, nil); +} + +- (void)testGroup { + GTMABGroup *group = [[[GTMABGroup alloc] initWithRecord:nil] autorelease]; + STAssertNil(group, nil); + group = [GTMABGroup groupNamed:kGTMABTestGroupName]; + STAssertNotNil(group, nil); + STAssertEqualObjects([group compositeName], kGTMABTestGroupName, nil); + NSString *name = [group valueForProperty:kABGroupNameProperty]; + STAssertEqualObjects(name, kGTMABTestGroupName, nil); + NSString *lastName = [group valueForProperty:kGTMABPersonLastNameProperty]; + STAssertNil(lastName, nil); + STAssertTrue([group removeValueForProperty:kABGroupNameProperty], nil); + STAssertFalse([group removeValueForProperty:kABGroupNameProperty], nil); + STAssertFalse([group removeValueForProperty:kGTMABPersonLastNameProperty], nil); + STAssertFalse([group setValue:nil forProperty:kABGroupNameProperty], nil); + STAssertFalse([group setValue:[NSNumber numberWithInt:1] + forProperty:kABGroupNameProperty], nil); + STAssertFalse([group setValue:@"Bart" + forProperty:kGTMABPersonBirthdayProperty], nil); + + ABPropertyType property = [GTMABGroup typeOfProperty:kABGroupNameProperty]; + STAssertEquals(property, (ABPropertyType)kGTMABStringPropertyType, nil); + + property = [GTMABGroup typeOfProperty:kGTMABPersonLastNameProperty]; + STAssertEquals(property, (ABPropertyType)kGTMABInvalidPropertyType, nil); + + NSString *string = [GTMABGroup localizedPropertyName:kABGroupNameProperty]; + STAssertEqualObjects(string, @"Name", nil); + + string = [GTMABGroup localizedPropertyName:kGTMABPersonLastNameProperty]; + STAssertEqualObjects(string, kGTMABUnknownPropertyName, nil); + + string = [GTMABGroup localizedPropertyName:kGTMABRecordInvalidID]; + STAssertEqualObjects(string, kGTMABUnknownPropertyName, nil); + + string = [group description]; + STAssertNotNil(string, nil); + + // Adding and removing members + group = [GTMABGroup groupNamed:kGTMABTestGroupName]; + NSArray *members = [group members]; + STAssertEquals([members count], (NSUInteger)0, @"Members: %@", members); + + STAssertFalse([group addMember:nil], nil); + + members = [group members]; + STAssertEquals([members count], (NSUInteger)0, @"Members: %@", members); + + GTMABPerson *person = [GTMABPerson personWithFirstName:kGTMABTestFirstName + lastName:kGTMABTestLastName]; + STAssertNotNil(person, nil); + STAssertTrue([book_ addRecord:person], nil); + STAssertTrue([book_ save], nil); + STAssertTrue([book_ addRecord:group], nil); + STAssertTrue([book_ save], nil); + STAssertTrue([group addMember:person], nil); + STAssertTrue([book_ save], nil); + members = [group members]; + STAssertEquals([members count], (NSUInteger)1, @"Members: %@", members); + STAssertTrue([group removeMember:person], nil); + STAssertFalse([group removeMember:person], nil); + STAssertFalse([group removeMember:nil], nil); + STAssertTrue([book_ removeRecord:group], nil); + STAssertTrue([book_ removeRecord:person], nil); + STAssertTrue([book_ save], nil); +} + + +- (void)testMultiValues { + STAssertThrows([[GTMABMultiValue alloc] init], nil); + STAssertThrows([[GTMABMutableMultiValue alloc] init], nil); + GTMABMultiValue *value = [[GTMABMultiValue alloc] initWithMultiValue:nil]; + STAssertNil(value, nil); + GTMABMutableMultiValue *mutValue + = [GTMABMutableMultiValue valueWithPropertyType:kGTMABInvalidPropertyType]; + STAssertNil(mutValue, nil); + mutValue + = [[[GTMABMutableMultiValue alloc] + initWithMutableMultiValue:nil] autorelease]; + STAssertNil(mutValue, nil); + mutValue + = [[[GTMABMutableMultiValue alloc] + initWithMultiValue:nil] autorelease]; + STAssertNil(mutValue, nil); +#if GTM_IPHONE_SDK + // Only the IPhone version actually allows you to check types of a multivalue + // before you stick anything in it + const GTMABPropertyType types[] = { + kGTMABStringPropertyType, + kGTMABIntegerPropertyType, + kGTMABRealPropertyType, + kGTMABDateTimePropertyType, + kGTMABDictionaryPropertyType, + kGTMABMultiStringPropertyType, + kGTMABMultiIntegerPropertyType, + kGTMABMultiRealPropertyType, + kGTMABMultiDateTimePropertyType, + kGTMABMultiDictionaryPropertyType + }; + for (size_t i = 0; i < sizeof(types) / sizeof(GTMABPropertyType); ++i) { + mutValue = [GTMABMutableMultiValue valueWithPropertyType:types[i]]; + STAssertNotNil(mutValue, nil); + // Oddly the Apple APIs allow you to create a mutable multi value with + // either a property type of kABFooPropertyType or kABMultiFooPropertyType + // and apparently you get back basically the same thing. However if you + // ask a type that you created with kABMultiFooPropertyType for it's type + // it returns just kABFooPropertyType. + STAssertEquals([mutValue propertyType], + (GTMABPropertyType)(types[i] & ~kABMultiValueMask), nil); + } +#endif // GTM_IPHONE_SDK + mutValue + = [GTMABMutableMultiValue valueWithPropertyType:kGTMABStringPropertyType]; + STAssertNotNil(mutValue, nil); + value = [[mutValue copy] autorelease]; + STAssertEqualObjects([value class], [GTMABMultiValue class], nil); + mutValue = [[value mutableCopy] autorelease]; + STAssertEqualObjects([mutValue class], [GTMABMutableMultiValue class], nil); + STAssertEquals([mutValue count], (NSUInteger)0, nil); + STAssertNil([mutValue valueAtIndex:0], nil); + STAssertNil([mutValue labelAtIndex:0], nil); +#if GTM_IPHONE_SDK + STAssertEquals([mutValue identifierAtIndex:0], + kGTMABMultiValueInvalidIdentifier, nil); + STAssertEquals([mutValue propertyType], + (GTMABPropertyType)kGTMABStringPropertyType, nil); +#else // GTM_IPHONE_SDK + STAssertEqualObjects([mutValue identifierAtIndex:0], + kGTMABMultiValueInvalidIdentifier, nil); +#endif // GTM_IPHONE_SDK + GTMABMultiValueIdentifier ident + = [mutValue addValue:nil withLabel:(CFStringRef)kABHomeLabel]; +#if GTM_IPHONE_SDK + STAssertEquals(ident, kGTMABMultiValueInvalidIdentifier, nil); +#else // GTM_IPHONE_SDK + STAssertEqualObjects(ident, kGTMABMultiValueInvalidIdentifier, nil); +#endif // GTM_IPHONE_SDK + + ident = [mutValue addValue:@"val1" + withLabel:nil]; +#if GTM_IPHONE_SDK + STAssertEquals(ident, kGTMABMultiValueInvalidIdentifier, nil); +#else // GTM_IPHONE_SDK + STAssertEqualObjects(ident, kGTMABMultiValueInvalidIdentifier, nil); +#endif // GTM_IPHONE_SDK + ident = [mutValue insertValue:@"val1" + withLabel:nil + atIndex:0]; +#if GTM_IPHONE_SDK + STAssertEquals(ident, kGTMABMultiValueInvalidIdentifier, nil); +#else // GTM_IPHONE_SDK + STAssertEqualObjects(ident, kGTMABMultiValueInvalidIdentifier, nil); +#endif // GTM_IPHONE_SDK + ident = [mutValue insertValue:nil + withLabel:(CFStringRef)kABHomeLabel + atIndex:0]; +#if GTM_IPHONE_SDK + STAssertEquals(ident, kGTMABMultiValueInvalidIdentifier, nil); +#else // GTM_IPHONE_SDK + STAssertEqualObjects(ident, kGTMABMultiValueInvalidIdentifier, nil); +#endif // GTM_IPHONE_SDK + ident = [mutValue addValue:@"val1" + withLabel:(CFStringRef)kABHomeLabel]; +#if GTM_IPHONE_SDK + STAssertNotEquals(ident, kGTMABMultiValueInvalidIdentifier, nil); +#else // GTM_IPHONE_SDK + STAssertNotEqualObjects(ident, kGTMABMultiValueInvalidIdentifier, nil); +#endif // GTM_IPHONE_SDK + GTMABMultiValueIdentifier identCheck = [mutValue identifierAtIndex:0]; +#if GTM_IPHONE_SDK + STAssertEquals(ident, identCheck, nil); +#else // GTM_IPHONE_SDK + STAssertEqualObjects(ident, identCheck, nil); +#endif // GTM_IPHONE_SDK + NSUInteger idx = [mutValue indexForIdentifier:ident]; + STAssertEquals(idx, (NSUInteger)0, nil); + STAssertTrue([mutValue replaceLabelAtIndex:0 + withLabel:(CFStringRef)kABWorkLabel], nil); + STAssertFalse([mutValue replaceLabelAtIndex:10 + withLabel:(CFStringRef)kABWorkLabel], nil); + STAssertTrue([mutValue replaceValueAtIndex:0 + withValue:@"newVal1"], nil); + STAssertFalse([mutValue replaceValueAtIndex:10 + withValue:@"newVal1"], nil); + + STAssertEqualObjects([mutValue valueForIdentifier:ident], @"newVal1", nil); + STAssertEqualObjects([mutValue labelForIdentifier:ident], + (NSString *)kABWorkLabel, nil); + + GTMABMultiValueIdentifier ident2 + = [mutValue insertValue:@"val2" + withLabel:(CFStringRef)kABOtherLabel + atIndex:0]; + STAssertNotEquals(ident2, kGTMABMultiValueInvalidIdentifier, nil); + STAssertNotEquals(ident2, ident, nil); + GTMABMultiValueIdentifier ident3 + = [mutValue insertValue:@"val3" + withLabel:(CFStringRef)kGTMABPersonPhoneMainLabel + atIndex:10]; +#if GTM_IPHONE_SDK + STAssertEquals(ident3, kGTMABMultiValueInvalidIdentifier, nil); +#else // GTM_IPHONE_SDK + STAssertEqualObjects(ident3, kGTMABMultiValueInvalidIdentifier, nil); +#endif // GTM_IPHONE_SDK + NSUInteger idx3 = [mutValue indexForIdentifier:ident3]; + STAssertEquals(idx3, (NSUInteger)NSNotFound, nil); + STAssertTrue([mutValue removeValueAndLabelAtIndex:1], nil); + STAssertFalse([mutValue removeValueAndLabelAtIndex:1], nil); + + NSUInteger idx4 + = [mutValue indexForIdentifier:kGTMABMultiValueInvalidIdentifier]; + STAssertEquals(idx4, (NSUInteger)NSNotFound, nil); + + STAssertNotNULL([mutValue multiValueRef], nil); + + // Enumerator test + mutValue + = [GTMABMutableMultiValue valueWithPropertyType:kGTMABIntegerPropertyType]; + STAssertNotNil(mutValue, nil); + for (int i = 0; i < 100; i++) { + NSString *label = [NSString stringWithFormat:@"label %d", i]; + NSNumber *val = [NSNumber numberWithInt:i]; + STAssertNotEquals([mutValue addValue:val + withLabel:(CFStringRef)label], + kGTMABMultiValueInvalidIdentifier, nil); + } + int count = 0; + NSString *label; + GTM_FOREACH_ENUMEREE(label, [mutValue labelEnumerator]) { + NSString *testLabel = [NSString stringWithFormat:@"label %d", count++]; + STAssertEqualObjects(label, testLabel, nil); + } + count = 0; + value = [[mutValue copy] autorelease]; + NSNumber *val; + GTM_FOREACH_ENUMEREE(val, [value valueEnumerator]) { + STAssertEqualObjects(val, [NSNumber numberWithInt:count++], nil); + } + + // Test messing with the values while we're enumerating them + NSEnumerator *labelEnum = [mutValue labelEnumerator]; + NSEnumerator *valueEnum = [mutValue valueEnumerator]; + STAssertNotNil(labelEnum, nil); + STAssertNotNil(valueEnum, nil); + STAssertNotNil([labelEnum nextObject], nil); + STAssertNotNil([valueEnum nextObject], nil); + STAssertTrue([mutValue removeValueAndLabelAtIndex:0], nil); + STAssertThrows([labelEnum nextObject], nil); + STAssertThrows([valueEnum nextObject], nil); + + // Test messing with the values while we're fast enumerating them + // Should throw an exception on the second access. + BOOL exceptionThrown = NO; + // Start at one because we removed index 0 above. + count = 1; + @try { + GTM_FOREACH_ENUMEREE(label, [mutValue labelEnumerator]) { + NSString *testLabel = [NSString stringWithFormat:@"label %d", count++]; + STAssertEqualObjects(label, testLabel, nil); + STAssertTrue([mutValue removeValueAndLabelAtIndex:50], nil); + } + } @catch(NSException *e) { + STAssertEqualObjects([e name], NSGenericException, @"Got %@ instead", e); + STAssertEquals(count, 2, + @"Should have caught it on the second access"); + exceptionThrown = YES; + } // COV_NF_LINE - because we always catch, this brace doesn't get exec'd + STAssertTrue(exceptionThrown, @"We should have thrown an exception" + @" because the values under the enumerator were modified"); + +} + +#if GTM_IPHONE_SDK +- (void)testRadar6208390 { + GTMABPropertyType types[] = { + kGTMABStringPropertyType, + kGTMABIntegerPropertyType, + kGTMABRealPropertyType, + kGTMABDateTimePropertyType, + kGTMABDictionaryPropertyType + }; + for (size_t j = 0; j < sizeof(types) / sizeof(ABPropertyType); ++j) { + ABPropertyType type = types[j]; +#ifdef GTM_IPHONE_SDK + ABMultiValueRef ref = ABMultiValueCreateMutable(type); +#else // GTM_IPHONE_SDK + ABMutableMultiValueRef ref = ABMultiValueCreateMutable(); +#endif // GTM_IPHONE_SDK + STAssertNotNULL(ref, nil); + NSString *label = [[NSString alloc] initWithString:@"label"]; + STAssertNotNil(label, nil); + id val = nil; + if (type == kGTMABDictionaryPropertyType) { + val = [[NSDictionary alloc] initWithObjectsAndKeys:@"1", @"1", nil]; + } else if (type == kGTMABStringPropertyType) { + val = [[NSString alloc] initWithFormat:@"value %d"]; + } else if (type == kGTMABIntegerPropertyType + || type == kGTMABRealPropertyType ) { + val = [[NSNumber alloc] initWithInt:143]; + } else if (type == kGTMABDateTimePropertyType) { + val = [[NSDate alloc] init]; + } + STAssertNotNil(val, + @"Testing type %d, %@", type, val); + NSUInteger firstRetainCount = [val retainCount]; + STAssertNotEquals(firstRetainCount, + (NSUInteger)0, + @"Testing type %d, %@", type, val); + + GTMABMultiValueIdentifier identifier; + STAssertTrue(ABMultiValueAddValueAndLabel(ref, + val, + (CFStringRef)label, + &identifier), + @"Testing type %d, %@", type, val); + NSUInteger secondRetainCount = [val retainCount]; + STAssertEquals(firstRetainCount + 1, + secondRetainCount, + @"Testing type %d, %@", type, val); + [label release]; + [val release]; + NSUInteger thirdRetainCount = [val retainCount]; + STAssertEquals(firstRetainCount, + thirdRetainCount, + @"Testing type %d, %@", type, val); + + id oldVal = val; + val = (id)ABMultiValueCopyValueAtIndex(ref, 0); + NSUInteger fourthRetainCount = [val retainCount]; + + // kABDictionaryPropertyTypes appear to do an actual copy, so the retain + // count checking trick won't work. We only check the retain count if + // we didn't get a new version. + if (val == oldVal) { + if (type == kGTMABIntegerPropertyType + || type == kGTMABRealPropertyType) { + // We are verifying that yes indeed 6208390 is still broken + STAssertEquals(fourthRetainCount, + thirdRetainCount, + @"Testing type %d, %@. If you see this error it may " + @"be time to update the code to change retain behaviors" + @"with this os version", type, val); + } else { + STAssertEquals(fourthRetainCount, + thirdRetainCount + 1, + @"Testing type %d, %@", type, val); + [val release]; + } + } else { + [val release]; + } + CFRelease(ref); + } +} + +// Globals used by testRadar6240394. +static GTMABPropertyID gGTMTestID; +static const GTMABPropertyID *gGTMTestIDPtr; + +void __attribute__((constructor))SetUpIDForTestRadar6240394(void) { + // These must be set up BEFORE ABAddressBookCreate is called. + gGTMTestID = kGTMABPersonLastNameProperty; + gGTMTestIDPtr = &kGTMABPersonLastNameProperty; +} + +- (void)testRadar6240394 { + // As of iPhone SDK 2.1, the property IDs aren't initialized until + // ABAddressBookCreate is actually called. They will return zero until + // then. Logged as radar 6240394. + STAssertEquals(gGTMTestID, 0, @"If this isn't zero, Apple has fixed 6240394"); + (void)ABAddressBookCreate(); + STAssertEquals(*gGTMTestIDPtr, kGTMABPersonLastNameProperty, + @"If this doesn't work, something else has broken"); +} + +#endif // GTM_IPHONE_SDK +@end diff --git a/AddressBook/TestData/phone.png b/AddressBook/TestData/phone.png new file mode 100644 index 0000000..7e4051f Binary files /dev/null and b/AddressBook/TestData/phone.png differ -- cgit v1.2.3