2 // AIAddressBookUserIconSource.m
5 // Created by Evan Schoenberg on 1/4/08.
8 #import "AIAddressBookUserIconSource.h"
9 #import "ESAddressBookIntegrationPlugin.h"
10 #import <Adium/AIPreferenceControllerProtocol.h>
11 #import <AddressBook/AddressBook.h>
12 #import <Adium/AIUserIcons.h>
13 #import <Adium/AIMetaContact.h>
15 #import <AIUtilities/AIImageAdditions.h>
17 #define KEY_AB_IMAGE_SYNC @"AB Image Sync"
18 #define KEY_AB_PREFER_ADDRESS_BOOK_IMAGES @"AB Prefer AB Images"
20 @implementation AIAddressBookUserIconSource
24 if ((self = [super init])) {
25 //Tracking dictionary for asynchronous image loads
26 trackingDict = [[NSMutableDictionary alloc] init];
27 trackingDictPersonToTagNumber = [[NSMutableDictionary alloc] init];
28 trackingDictTagNumberToPerson = [[NSMutableDictionary alloc] init];
29 priority = AIUserIconLowPriority;
31 [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_ADDRESSBOOK];
39 [trackingDictPersonToTagNumber release]; trackingDictPersonToTagNumber = nil;
40 [trackingDictTagNumberToPerson release]; trackingDictTagNumberToPerson = nil;
46 * @brief AIUserIcons wants this source to update its user icon for an object
48 * Call +[AIUserIcons userIconSource:didDetermineUserIcon:asynchronously:forObject:] with the new icon, if appropriate
50 * @result An AIUserIconSourceQueryResult indicating the result
52 - (AIUserIconSourceQueryResult)updateUserIconForObject:(AIListObject *)inObject
54 if ([self queueDelayedFetchOfImageForPerson:[[ESAddressBookIntegrationPlugin class] personForListObject:inObject]
56 return AIUserIconSourceLookingUpIconAsynchronously;
58 return AIUserIconSourceDidNotFindIcon;
63 * @brief The priority at which this source should be used. See the #defines in AIUserIcons.h for posible values.
65 - (AIUserIconPriority)priority
72 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
73 object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
75 AIUserIconPriority oldPriority = priority;
77 preferAddressBookImages = [[prefDict objectForKey:KEY_AB_PREFER_ADDRESS_BOOK_IMAGES] boolValue];
78 useABImages = [[prefDict objectForKey:KEY_AB_USE_IMAGES] boolValue];
80 priority = (preferAddressBookImages ? AIUserIconHighPriority : AIUserIconLowPriority);
81 if (priority != oldPriority) {
82 [AIUserIcons userIconSource:self priorityDidChange:priority fromPriority:oldPriority];
86 #pragma mark Address Book
88 * @brief Called when the address book completes an asynchronous image lookup
90 * @param inData NSData representing an NSImage
91 * @param tag A tag indicating the lookup with which this call is associated. We use a tracking dictionary, trackingDict, to associate this int back to a usable object.
93 - (void)consumeImageData:(NSData *)inData forTag:(int)tag
98 AIListObject *listObject;
99 // AIListContact *parentContact;
103 tagNumber = [NSNumber numberWithInt:tag];
105 //Apply the image to the appropriate listObject
106 image = (inData ? [[[NSImage alloc] initWithData:inData] autorelease] : nil);
108 //Address book can feed us giant images, which we really don't want to keep around
109 NSSize size = [image size];
110 if (size.width > 96 || size.height > 96)
111 image = [image imageByScalingToSize:NSMakeSize(96, 96)];
113 //Get the object from our tracking dictionary
114 setOrObject = [trackingDict objectForKey:tagNumber];
116 if ([setOrObject isKindOfClass:[AIListObject class]]) {
117 listObject = (AIListObject *)setOrObject;
119 [AIUserIcons userIconSource:self
120 didDetermineUserIcon:image
122 forObject:listObject];
124 } else /*if ([setOrObject isKindOfClass:[NSSet class]])*/{
125 NSEnumerator *enumerator;
127 //Apply the image to each listObject at the appropriate priority
128 enumerator = [(NSSet *)setOrObject objectEnumerator];
129 while ((listObject = [enumerator nextObject])) {
130 [AIUserIcons userIconSource:self
131 didDetermineUserIcon:image
133 forObject:listObject];
137 //No further need for the dictionary entries
138 [trackingDict removeObjectForKey:tagNumber];
140 if ((uniqueID = [trackingDictTagNumberToPerson objectForKey:tagNumber])) {
141 [trackingDictPersonToTagNumber removeObjectForKey:uniqueID];
142 [trackingDictTagNumberToPerson removeObjectForKey:tagNumber];
148 * @brief Queue an asynchronous image fetch for person associated with inObject
150 * Image lookups are done asynchronously. This allows other processing to be done between image calls, improving the perceived
151 * speed. [Evan: I have seen one instance of this being problematic. My localhost loop was broken due to odd network problems,
152 * and the asynchronous lookup therefore hung the problem. Submitted as radar 3977541.]
154 * We load from the same ABPerson for multiple AIListObjects, one for each service/UID combination times
155 * the number of accounts on that service. We therefore aggregate the lookups to lower the address book search
156 * and image/data creation overhead.
158 * @param person The ABPerson to fetch the image from
159 * @param inObject The AIListObject with which to ultimately associate the image
161 - (BOOL)queueDelayedFetchOfImageForPerson:(ABPerson *)person object:(AIListObject *)inObject
163 if (!person) return NO;
165 /* Some mild complexity here. If inObject is a metacontact, we should only proceed if
166 * none of its contained contacts have a higher-priority user icon than we will be.
167 * This prevents a metacontact-associated address book image from overriding a serverside
168 * contained-contact image if that isn't the sort of thing that the user might be into.
170 if ([inObject isKindOfClass:[AIMetaContact class]]) {
171 NSEnumerator *enumerator = [[(AIMetaContact *)inObject listContacts] objectEnumerator];
172 AIListContact *listContact;
173 while ((listContact = [enumerator nextObject])) {
174 if (![AIUserIcons userIconSource:self changeWouldBeRelevantForObject:inObject])
183 uniqueId = [person uniqueId];
185 //Check if we already have a tag for the loading of another object with the same
187 if ((tagNumber = [trackingDictPersonToTagNumber objectForKey:uniqueId])) {
189 NSMutableSet *objectSet;
191 previousValue = [trackingDict objectForKey:tagNumber];
193 if ([previousValue isKindOfClass:[AIListObject class]]) {
194 //If the old value is just a listObject, create an array with the old object
196 objectSet = [NSMutableSet setWithObjects:previousValue,inObject,nil];
198 //Store the array in the tracking dict
199 [trackingDict setObject:objectSet forKey:tagNumber];
201 } else /*if ([previousValue isKindOfClass:[NSMutableArray class]])*/{
202 //Add the new object to the previously-created array
203 [(NSMutableSet *)previousValue addObject:inObject];
207 //Begin the image load
208 tag = [person beginLoadingImageDataForClient:self];
209 tagNumber = [NSNumber numberWithInt:tag];
211 //We need to be able to take a tagNumber and retrieve the object
212 [trackingDict setObject:inObject forKey:tagNumber];
214 //We also want to take a person's uniqueID and potentially find an existing tag number
215 [trackingDictPersonToTagNumber setObject:tagNumber forKey:uniqueId];
216 [trackingDictTagNumberToPerson setObject:uniqueId forKey:tagNumber];