// // NGARemoteAccessibleObject.m // P4Menu // // Created by Michael Bishop on 10/7/11. // Copyright 2011 __MyCompanyName__. All rights reserved. // #import "NGAUtilities.h" #import "NGARemoteAccessibleObject.h" #ifndef NSAccessibilityWebAreaRole NSString * NSAccessibilityWebAreaRole = @"AXWebArea"; #endif RUNTIME_CONSTANT_FUNCTION(SOLogger *, memLog, // [[SOLogger alloc] initWithFacility:@"NGARAO.memory" options:SOLoggerDefaultASLOptions] nil ); RUNTIME_CONSTANT_FUNCTION(SOLogger *, values, // [[SOLogger alloc] initWithFacility:@"NGARAO.values" options:SOLoggerDefaultASLOptions] nil ); RUNTIME_CONSTANT_FUNCTION(SOLogger *, pollLog, // [[SOLogger alloc] initWithFacility:@"NGARAO.polling" options:SOLoggerDefaultASLOptions] nil ); RUNTIME_CONSTANT_FUNCTION(SOLogger *, observLog, // [[SOLogger alloc] initWithFacility:@"NGARAO.observing" options:SOLoggerDefaultASLOptions] nil ); /* Notes on how this all works. GOAL: The goal is to implement KVC/KVO on a remote accessibility object. PROBLEMS ======== 1. While it's easy to map valueForUndefinedKey: into retrieving an accessibility attribute, it's more difficult to be able to notify observing objects through KVO that a value has changed. The accesibility API has a list of notifications that can be sent when a remote value has changed, but even then, the previous value is not sent. To make matters worse, whether or not the notifications actually work is dependent on the level of accesibility support from the server app (we are the client app at this point). It's not uncommon for a remote app to allow registration of a notification, but not send a notification when the value changes. For this reason, we need to use polling as a backup system to notifications to detect changes in values. To keep track of previous values, we store them in a cache and return them when calling willChangeValueForKey:, then replace them with the new value and call didChangeValueForKey:. SOLUTION To implement KVO, we need to keep track of a key's "old" value both so we can return it as the old value (which happens immediately after calling "willChangeValueForKey:"). We also need the old value to compare with when polling. So, whenever a key is observed (and we can tell through addObserver:forKeyPath:options:context:) we immediately cache the current value. That value is always what is returned from valueForUndefinedKey:. Then we set up two mechanisms for determining when the value changes. To start, we see if there is a notification relevant to that key and if there is we register for it. Then, as a backup, we also make a note of that key that it should be checked at polling time. To make sure we have a "polling time", we register with the polling manager so it will tell us when it's time to poll. If at any point we receive the notification, two things happen. 1. We stop polling for that key. We know the notification works so there is no need to waste time constanly checking that key 2. We call valueForKey:didChangeToNewValue:. This calls willChangeValueForKey: which then calls valueForKey: which calls valueForUndefinedKey: which returns our cached value. Then we update the cache and call didChangeValueForKey: which again reads from the cache (which has the new value. If, it is time to poll we read the *actual* value from the accessibility api and if it differs from the value in the cache, we call valueForKey:didChangeToNewValue: with the newly read value. This also goes through the will/didChangeValueForKey: rigamarole described previously. A note about the dictionaries for storing keys that are polled and keys observed through notification: They are used to store reference counts for the keys. We keep track of how often the request has come through to observe those keys. We can only register for a accessibility notification once or we get an error. In addition, we only want to be registered with the polling manager if we have received requests to observe a key. When the reference count of all the polled keys goes to zero, we unregister with the polling manager. 2. When creating an AXUIElementRef through AXUIElementCopyAttributeValue, AXUIElementCreateApplication, or AXUIElementCreateSystemWide, you can call the method twice and receive two different AXUIElementRef objects even though they are technically equal. Think of it like creating two separate NSStrings that both contain the characters spelling: "hello". They have different pointer values, but are technically equivalent. Indeed, going back to the different AXUIElementRef objects, you can pass them to CFEqual() and it will return TRUE. This is nice, but creates a problem for us because we'd like to have one NGARemoteAccessibleObject per equivalent AXUIElementRef object, to make KVO easier. SOLUTION For this reason, we "intern" all the NGARemoteAccessibleObject instances. Read http://en.wikipedia.org/wiki/String_interning and substitute "NGARemoteAccessibleObject" for "String". To do this, we have a NON-RETAINING NSMapTable (called tableOfAllNGARemoteAccessibleObjects) which contains all the NGARemoteAccessibleObject instances that are created. When we receive an initWithAXUIElementRef: call, we init 'self' only enough to assign the AXUIElementRef, then we look up in tableOfAllNGARemoteAccessibleObjects to see if an NGARemoteAccessibleObject instance exists that is equivalent to the AXUIElementRef. If so, we return that with an incremented retain count and autorelease the self object. Yay, only one unique NGARemoteAccessibleObject is returned for an equivalent AXUIElementRef! Now we have another problem... Sometimes we receive a notification from the accessibility API that says a given AXUIElementRef is now invalid and should no longer be passed to any accessibiity apis. When this happens, we know we'd like to safely deallocate the equivalent NGARemoteAccessibleObject so to accomplish this, we must make sure its retain count eventually goes to 0. To do this, we have a method "removeSelfFromSystem". It first removes itself from any other NGARemoteAccessibleObject instance's cache by going through all the NGARemoteAccessibleObject instances in tableOfAllNGARemoteAccessibleObjects and searching their caches. Then, we remove the object from the polling managery. Any remaining references to the object are external and when released, will destroy the NGARemoteAccessibleObject. In the dealloc method, we remove it from the tableOfAllNGARemoteAccessibleObjects (which didn't retain them remember?) */ @interface NGARemoteAccessibilityNotificationManager : NSObject { @private AXObserverRef ref; NSMapTable * _keysObservedByNotifications; // observer->key->usageCount pid_t processIdentifier; } +(NGARemoteAccessibilityNotificationManager*)observerForProcessIdentifier:(pid_t)pid; -(id)initWithProcessIdentifier:(pid_t)pid; -(NSUInteger)notificationCountForObject:(NGARemoteAccessibleObject*)observer notificationName:(NSString*)notificationName; -(NSSet*)keysObservedByObject:(NGARemoteAccessibleObject*)observer; -(void)registerObject:(NGARemoteAccessibleObject*)observer forNotification:(NSString *)notificationName; -(void)unregisterObject:(NGARemoteAccessibleObject*)observer forNotification:(NSString *)notificationName; -(void)unregisterObjectForAllNotifications:(NGARemoteAccessibleObject*)observer; -(void)_clearObject:(NGARemoteAccessibleObject*)observer notificationName:(NSString*)notificationName; @end @interface NGARemoteAccessibleObject () -(id)accessibilityAttributeValueForKey:(NSString*)key; -(id)transformedAttributeValueFromRawAttributeValue:(CFTypeRef)attributeValue; -(NSUInteger)observationCountForKey:(NSString*)key; -(void)beginObservingKeyPath:(NSString*)keyPath; -(void)endObservingKeyPath:(NSString*)keyPath; -(void)beginObservingNotificationWithName:(NSString*)notificationName; -(void)endObservingNotificationWithName:(NSString*)notificationName; -(void)addNotificationObservationForKey:(NSString*)key; -(void)removeNotificationObservationForKey:(NSString*)key; -(NSUInteger)addPollingObservationForKey:(NSString*)key; -(NSUInteger)removePollingObservationForKey:(NSString*)key; -(void)notifyObserversOfNewValuesForKeysInDictionary:(NSDictionary*)newValues; -(void)notifyObserversThatValueForKey:(NSString*)key didChangeToNewValue:(id)newValue; -(void)markSelfAsDead; -(void)removeAllCachedObjectsMatchingObject:(NGARemoteAccessibleObject*)element; -(NSString*)notificationNameForKey:(NSString*)key; -(NSString*)keyForNotificationName:(NSString*)notificationName; -(void)didReceiveAXNotification:(NSString*)notification; @property (nonatomic,readonly) AXUIElementRef elementRef; @property (nonatomic, readonly) NGARemoteAccessibilityNotificationManager * notificationManager; @property (nonatomic,readonly) NGARemoteAccessibleObject * applicationElement; @property (nonatomic,readonly) NSString * roleFullName; @property (nonatomic,readonly) NSArray * attributeNames; @property (nonatomic,readonly) NSArray * attributeValues; @end static NSTimeInterval kPollingInterval = 0.5; static NSString * kAccessibilityPrefix = @"AX"; @interface NGARemoteAccessibleObjectPollingManager : NSObject { @private NSMutableSet * _pollingElements; NSTimer * _pollingTimer; } +(NGARemoteAccessibleObjectPollingManager*)sharedManager; -(void)addPollingForObject:(NGARemoteAccessibleObject*)element; -(void)removePollingForObject:(NGARemoteAccessibleObject*)element; @end RUNTIME_CONSTANT_FUNCTION(NSMapTable *, tableOfAllNGARemoteAccessibleObjects, RETAIN([NSMapTable mapTableWithStrongToWeakObjects]) ); static NSString * firstKeyInKeyPath(NSString*keyPath, NSString ** remainingPath) { NSRange range = [keyPath rangeOfString:@"."]; NSString * firstKey = keyPath; NSString * remaining = nil; if ( range.location == NSNotFound ) goto end; firstKey = [keyPath substringToIndex:range.location]; if ( range.location+1 < [keyPath length] ) remaining = [keyPath substringFromIndex:range.location +1]; end: if ( remainingPath ) *remainingPath = remaining; return firstKey; } static BOOL refIsElementType( CFTypeRef ref ) { if ( !ref ) return NO; return CFGetTypeID(ref) == AXUIElementGetTypeID(); } static NSString * AXErrorDescription(const AXError error) { switch (error) { case kAXErrorSuccess: return @"No Error. Sucess!"; case kAXErrorFailure: return @"Failure"; case kAXErrorIllegalArgument: return @"Illegal Argument"; case kAXErrorInvalidUIElement: return @"Invalid UIElement"; case kAXErrorInvalidUIElementObserver: return @"Invalid UIElement Observer"; case kAXErrorCannotComplete: return @"Cannot Complete"; case kAXErrorAttributeUnsupported: return @"Attribute Unsupported"; case kAXErrorActionUnsupported: return @"Action Unsupported"; case kAXErrorNotificationUnsupported: return @"Notification Unsupported"; case kAXErrorNotImplemented: return @"Not Implemented"; case kAXErrorNotificationAlreadyRegistered: return @"Notification Already Registered"; case kAXErrorNotificationNotRegistered: return @"Notification Not Registered"; case kAXErrorAPIDisabled: return @"API Disabled"; case kAXErrorNoValue: return @"No Value"; case kAXErrorParameterizedAttributeUnsupported: return @"Parameterized Attribute Unsupported"; case kAXErrorNotEnoughPrecision: return @"Not Enough Precision"; } return [NSString stringWithFormat:@"", error]; } static BOOL elementIsValid(AXUIElementRef ref) { pid_t pid; AXError error = AXUIElementGetPid(ref, &pid); return error != kAXErrorInvalidUIElement; } static pid_t pidForAXUIElement(AXUIElementRef elementRef) { pid_t pid; AXError error = AXUIElementGetPid(elementRef, &pid); if ( error != noErr ) return -1; return pid; } #pragma mark - @implementation NGARemoteAccessibleObject @synthesize elementRef = _element; #pragma mark INIT/DEALLOC -(id)initWithAXUIElementRef:(AXUIElementRef)ref { if ( !(self = [super init]) ) return nil; if ( ref == NULL || !AXAPIEnabled() ) { if ( !AXAPIEnabled() ) LOG_CRITICAL(@"Accessiblity not enabled!"); AUTORELEASE(self); return nil; } // We can't return any remote accessible objects for our own process if ( pidForAXUIElement(ref) == [[NSProcessInfo processInfo] processIdentifier] ) { AUTORELEASE(self); return nil; } // If there is already a NGARemoteAccessibleObject with this AXUIElementRef value, we want to return // that, not a new one so we see if there is one stored in "tableOfAllNGARemoteAccessibleObjects". // At this point, there is enough information to determine A) equality and B) hash values so // we are good to go. // if a NGARemoteAccessibleObject already exists with an equivalent AXUIElementRef as us, then we just return // a retained version of that (this is the flyweight pattern). This returns the same object // as the one in the table, but with the retain count incremented. NGARemoteAccessibleObject * member = [tableOfAllNGARemoteAccessibleObjects() objectForKey:(id)ref]; if ( member ) { LOG_DEBUG_LOG(memLog(), @"Already have a instance for %@ returning that instance...", member); AUTORELEASE( self ); return RETAIN(member); } if ( !elementIsValid(ref) ) { LOG_DEBUG_LOG(memLog(), @"Element is invalid and we don't already have an equivalent instance. returning nil", member); AUTORELEASE( self ); return nil; } // if we got that far, the NGARemoteAccessibleObject is not in the table so we add ourselves // and continue the initialization. CFStringRef description = CFCopyDescription(ref); [(id)description autorelease]; LOG_DEBUG_LOG(memLog(), @"Adding %p to table with key: %p (%@)", self, ref, description ); [tableOfAllNGARemoteAccessibleObjects() setObject:self forKey:(id)ref]; LOG_DEBUG_LOG(memLog(), @"%lu accessible objects alive", tableOfAllNGARemoteAccessibleObjects().count); _element = CFRetain(ref); _savedProcessIdentifier = pidForAXUIElement(ref); _cachedAttributeValues = [NSMutableDictionary new]; _keysObservedByPolling = [NSMutableDictionary new]; [self beginObservingNotificationWithName:NSAccessibilityUIElementDestroyedNotification]; LOG_DEBUG_LOG(memLog(), @"%@ (%p): INIT.", self, self ); return self; } -(id)init { return [self initWithAXUIElementRef:NULL]; } - (id)copyWithZone:(NSZone *)zone { return [self retain]; } -(void)dealloc { id object = [tableOfAllNGARemoteAccessibleObjects() objectForKey:(id)_element]; LOG_DEBUG_LOG(memLog(), @"Removing %p from table", object ); [tableOfAllNGARemoteAccessibleObjects() removeObjectForKey:(id)_element]; NSAssert( [tableOfAllNGARemoteAccessibleObjects() objectForKey:(id)_element] == nil, @"Element Not Removed!!!!!" ); LOG_DEBUG_LOG(memLog(), @"%lu accessible objects alive", tableOfAllNGARemoteAccessibleObjects().count); [self.notificationManager unregisterObjectForAllNotifications:self]; [[NGARemoteAccessibleObjectPollingManager sharedManager] removePollingForObject:self]; RELEASE(_keysObservedByPolling); RELEASE(_notificationObserver); RELEASE(_cachedAttributeValues); LOG_DEBUG_LOG(memLog(), @"DEALLOC: self: %p", self); CLEAN_CFRELEASE(_element); [super dealloc]; } #pragma mark Class Methods +(NGARemoteAccessibleObject*)elementForAXUIElementRef:(AXUIElementRef)ref { id existingInstance = [tableOfAllNGARemoteAccessibleObjects() objectForKey:(id)ref]; if ( existingInstance ) return AUTORELEASE(RETAIN(existingInstance)); if (!elementIsValid(ref)) return nil; return AUTORELEASE([[NGARemoteAccessibleObject alloc] initWithAXUIElementRef:ref]); } +(NGARemoteAccessibleObject*)applicationElementForProcessIdentifier:(pid_t)pid { AXUIElementRef applicationElementRef = AXUIElementCreateApplication(pid); NGARemoteAccessibleObject * object = [NGARemoteAccessibleObject elementForAXUIElementRef:applicationElementRef]; CLEAN_CFRELEASE(applicationElementRef); return object; } +(NGARemoteAccessibleObject*)sharedSystemElement { AXUIElementRef systemWideElement = AXUIElementCreateSystemWide(); NGARemoteAccessibleObject * object = [NGARemoteAccessibleObject elementForAXUIElementRef:systemWideElement]; CLEAN_CFRELEASE(systemWideElement); return object; } #pragma mark NSObject methods -(NSString*)description { BOOL selfIsFocusedElement = [[self valueForKey:NSAccessibilityFocusedAttribute] boolValue]; NSString * focusedLabel = selfIsFocusedElement ? @"*" : @""; return [NSString stringWithFormat:@"0x%x{%@,%@<%@>}", self, [NSRunningApplication runningApplicationWithProcessIdentifier:self.processIdentifier].localizedName, focusedLabel, self.roleFullName]; } -(NSUInteger)hash { return CFHash(self.elementRef); } -(BOOL)isEqual:(id)object { if ( ![object isKindOfClass:[NGARemoteAccessibleObject class]] ) return NO; return CFEqual(self.elementRef, [(NGARemoteAccessibleObject*)object elementRef]); } -(void*)observationInfo { return _observationInfo; } - (void)setObservationInfo:(void *)observationInfo { _observationInfo = observationInfo; } #pragma mark - AXNotification interface -(void)beginObservingNotificationWithName:(NSString*)notificationName { [self.notificationManager registerObject:self forNotification:notificationName]; ; } -(void)endObservingNotificationWithName:(NSString*)notificationName { [self.notificationManager unregisterObject:self forNotification:notificationName]; } #pragma mark - KVC -(id)valueForUndefinedKey:(NSString *)key { if ( ![key hasPrefix:kAccessibilityPrefix] ) return [super valueForUndefinedKey:key]; id attributeValue = nil; BOOL readFromCache = [self observationCountForKey:key] > 0; if ( readFromCache ) attributeValue = [_cachedAttributeValues objectConvertingNullToNilForKey:key]; else attributeValue = [self accessibilityAttributeValueForKey:key]; if ( !_isCurrentlyPolling && ![key isEqualToString:NSAccessibilityRoleAttribute] && ![key isEqualToString:NSAccessibilitySubroleAttribute] && ![key isEqualToString:NSAccessibilityFocusedAttribute] ) { [values() info:@"%@: Asked for key '%@'. Returning %@", self, key, attributeValue]; } return attributeValue; } #pragma mark - -(id)accessibilityAttributeValueForKey:(NSString*)key { if (_isDead) return nil; CFTypeRef attributeValueRef = NULL; CFTypeRef keyRef = (CFStringRef)key; AXError error = AXUIElementCopyAttributeValue( self.elementRef, keyRef, &attributeValueRef ); if ( error == kAXErrorAttributeUnsupported ) return nil; if ( error == kAXErrorNoValue ) return nil; if ( error == kAXErrorInvalidUIElement ) return nil; if ( error != noErr ) { LOG_ERROR( @"accessibilityAttributeValueForKey() returned error when fetching attribute '%@' on element %p. %@(%d)", key, self, AXErrorDescription(error), (int)error ); return nil; } id attributeValue = [self transformedAttributeValueFromRawAttributeValue:attributeValueRef]; if (attributeValueRef) CFRelease(attributeValueRef); return attributeValue; } -(id)transformedAttributeValueFromRawAttributeValue:(CFTypeRef)attributeValue { if (!attributeValue) return nil; if ( refIsElementType(attributeValue) ) return [NGARemoteAccessibleObject elementForAXUIElementRef:attributeValue]; if ( CFGetTypeID(attributeValue) == AXValueGetTypeID() ) { AXValueType valueType = AXValueGetType(attributeValue); switch ( valueType ) { case kAXValueCGPointType: { CGPoint value; AXValueGetValue(attributeValue, valueType, &value); return [NSValue valueWithPoint:NSPointFromCGPoint(value)]; } case kAXValueCGSizeType: { CGSize value; AXValueGetValue(attributeValue, valueType, &value); return [NSValue valueWithSize:NSSizeFromCGSize(value)]; } case kAXValueCGRectType: { CGRect value; AXValueGetValue(attributeValue, valueType, &value); return [NSValue valueWithRect:NSRectFromCGRect(value)]; } case kAXValueCFRangeType: { CFRange value; AXValueGetValue(attributeValue, valueType, &value); return [NSValue valueWithRange:NSMakeRange(value.location, value.length)]; } default: return nil; } } if ( CFGetTypeID(attributeValue) == CFArrayGetTypeID() ) { NSArray * array = (NSArray*)attributeValue; // transform the array NSMutableArray * transformedArray = [NSMutableArray arrayWithCapacity:[array count]]; for (id item in array) { CFTypeRef itemRef = (CFTypeRef)item; [transformedArray addObject:[self transformedAttributeValueFromRawAttributeValue:itemRef]]; } return [NSArray arrayWithArray:transformedArray]; } return AUTORELEASE(RETAIN((id)attributeValue)); } #pragma mark - KVO -(void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context { [observLog() info:@"%@: Will add an observer for keyPath:'%@'", self, keyPath]; [self beginObservingKeyPath:keyPath]; [super addObserver:observer forKeyPath:keyPath options:options context:context]; [observLog() info:@"%@: Did add an observer for keyPath:'%@'", self, keyPath]; } -(void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath { [observLog() info:@"%@: Will remove observer for keyPath:'%@'", self, keyPath]; [super removeObserver:observer forKeyPath:keyPath]; if (!_isRemovingWithContext) [self endObservingKeyPath:keyPath]; [observLog() info:@"%@: Did remove observer for keyPath:'%@'", self, keyPath]; } -(void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context { [observLog() info:@"%@: Will remove observer (with context 0x%p) for keyPath:'%@'", self, context, keyPath]; SEL removeObserverforKeyPathcontext = @selector(removeObserver:forKeyPath:context:); if ( [super respondsToSelector:removeObserverforKeyPathcontext] ) { _isRemovingWithContext = YES; [super removeObserver:observer forKeyPath:keyPath context:context]; _isRemovingWithContext = NO; } // If there is no context, then removeObserver:forKeyPath: was called, which we have overridden [self endObservingKeyPath:keyPath]; [observLog() info:@"%@: Did remove observer (with context 0x%p) for keyPath:'%@'", self, context, keyPath]; } #pragma mark - -(NSUInteger)observationCountForKey:(NSString*)key { return [[_keysObservedByPolling valueForKey:key] unsignedIntegerValue] + [self.notificationManager notificationCountForObject:self notificationName:[self notificationNameForKey:key]]; } -(void)beginObservingKeyPath:(NSString*)keyPath { NSString * remaining = nil; NSString * key = firstKeyInKeyPath(keyPath, &remaining); // break down non-keys into keys to observe if ( ![key hasPrefix:kAccessibilityPrefix] ) { NSSet * keyPathsAffectingValuesForKeyPath = [[self class] keyPathsForValuesAffectingValueForKey:key]; for (NSString * affectedKeyPath in keyPathsAffectingValuesForKeyPath) [self beginObservingKeyPath:[affectedKeyPath NGA_stringByAddingKeyPath:remaining]]; return; } [self addPollingObservationForKey:key]; if ( [self observationCountForKey:key] == 1 ) { // Set the key in the cache (we'll need to to send willChangeValueForKey: messages) id rawValue = [self accessibilityAttributeValueForKey:key]; [_cachedAttributeValues setObjectConvertingNilToNull:rawValue forKey:key]; } [self addNotificationObservationForKey:key]; if ( [self observationCountForKey:key] == 1 ) { // Set the key in the cache (we'll need to to send willChangeValueForKey: messages) id rawValue = [self accessibilityAttributeValueForKey:key]; [_cachedAttributeValues setObjectConvertingNilToNull:rawValue forKey:key]; } } -(void)endObservingKeyPath:(NSString*)keyPath { NSString * remaining = nil; NSString * key = firstKeyInKeyPath(keyPath, &remaining); // break down non-keys into keys to end observation if ( ![key hasPrefix:kAccessibilityPrefix] ) { NSSet * keyPathsAffectingValuesForKeyPath = [[self class] keyPathsForValuesAffectingValueForKey:key]; for (NSString * affectedKeyPath in keyPathsAffectingValuesForKeyPath) [self endObservingKeyPath:[affectedKeyPath NGA_stringByAddingKeyPath:remaining]]; return; } [self removeNotificationObservationForKey:key]; if ( [self observationCountForKey:key] == 0 ) [_cachedAttributeValues removeObjectForKey:key]; [self removePollingObservationForKey:key]; if ( [self observationCountForKey:key] == 0 ) [_cachedAttributeValues removeObjectForKey:key]; } #pragma mark - -(void)notifyObserversOfNewValuesForKeysInDictionary:(NSDictionary*)newValues { NSArray * keys = [newValues allKeys]; [self notifyChangesToKeyValues:[NSSet setWithArray:keys] occuringInBlock:^(id unretainedSelf) { [observLog() info:@"%@: ### WILL CHANGE ### FROM %@", unretainedSelf, [unretainedSelf dictionaryWithValuesForKeys:keys]]; [((NGARemoteAccessibleObject*)unretainedSelf)->_cachedAttributeValues setValuesForKeysWithDictionary:newValues]; [observLog() info:@"%@: ### DID CHANGE ### TO %@", unretainedSelf, newValues]; }]; } -(void)notifyObserversThatValueForKey:(NSString*)key didChangeToNewValue:(id)newValue { if ( !newValue ) [observLog() info:@"new value is nil!"]; [self notifyObserversOfNewValuesForKeysInDictionary:[NSDictionary dictionaryWithObject:newValue ? newValue : [NSNull null] forKey:key]]; } #pragma mark - accessibility notifications //NSAccessibilityFocusedWindowChangedNotification RUNTIME_CONSTANT_FUNCTION(NSDictionary*, keyToNotificationDictionary, RETAIN([NSDictionary dictionaryWithObjectsAndKeys: NSAccessibilityValueChangedNotification, NSAccessibilityValueAttribute, NSAccessibilityMainWindowChangedNotification, NSAccessibilityMainWindowAttribute, NSAccessibilityTitleChangedNotification, NSAccessibilityTitleAttribute, NSAccessibilityFocusedUIElementChangedNotification, NSAccessibilityFocusedUIElementAttribute, NSAccessibilityResizedNotification, NSAccessibilitySizeAttribute, NSAccessibilityMovedNotification, NSAccessibilityPositionAttribute, nil]) ) RUNTIME_CONSTANT_FUNCTION(NSDictionary*, notificationToKeyDictionary, RETAIN([NSDictionary dictionaryWithObjectsAndKeys: NSAccessibilityValueAttribute, NSAccessibilityValueChangedNotification, NSAccessibilityMainWindowAttribute, NSAccessibilityMainWindowChangedNotification, NSAccessibilityTitleAttribute, NSAccessibilityTitleChangedNotification, NSAccessibilityFocusedUIElementAttribute, NSAccessibilityFocusedUIElementChangedNotification, NSAccessibilitySizeAttribute, NSAccessibilityResizedNotification, NSAccessibilityPositionAttribute, NSAccessibilityMovedNotification, nil]) ) -(NSString*)notificationNameForKey:(NSString*)key { return [keyToNotificationDictionary() valueForKey:key]; } -(NSString*)keyForNotificationName:(NSString*)notificationName { return [notificationToKeyDictionary() valueForKey:notificationName]; } -(void)addNotificationObservationForKey:(NSString*)key { NSString * notificationName = [self notificationNameForKey:key]; [self beginObservingNotificationWithName:notificationName]; } -(void)removeNotificationObservationForKey:(NSString*)key { NSString * notificationName = [self notificationNameForKey:key]; [self endObservingNotificationWithName:notificationName]; } +(void)didReceiveAXNotification:(NSString*)notification withParameter:(AXUIElementRef)elementRef { id changedObject = [tableOfAllNGARemoteAccessibleObjects() objectForKey:(id)elementRef]; [changedObject didReceiveAXNotification:notification]; } -(void)didReceiveAXNotification:(NSString*)notification; { if ([notification isEqualToString:NSAccessibilityUIElementDestroyedNotification]) { [observLog() info:@"%@: Received Notification:'%@' for element:%@", self, notification, self]; [self markSelfAsDead]; return; } // We are being notified because a key changed. NSString * key = [self keyForNotificationName:notification]; // And now notify our observers that the value changed! [self notifyObserversThatValueForKey:key didChangeToNewValue:[self accessibilityAttributeValueForKey:key]]; return; } #pragma mark - polling -(NSUInteger)addPollingObservationForKey:(NSString*)key { if ( [_keysObservedByPolling count] == 0 ) [[NGARemoteAccessibleObjectPollingManager sharedManager] addPollingForObject:self]; NSUInteger usageCount = [[_keysObservedByPolling objectForKey:key] unsignedIntegerValue]; if ( usageCount == 0 ) [pollLog() info:@"%@: Added polling observation for key:'%@'", self, key]; usageCount++; [_keysObservedByPolling setObject:[NSNumber numberWithUnsignedInteger:usageCount] forKey:key]; return usageCount; } -(NSUInteger)removePollingObservationForKey:(NSString*)key { NSUInteger usageCount = [[_keysObservedByPolling objectForKey:key] unsignedIntegerValue]; if ( usageCount == 0 ) return usageCount; usageCount--; if ( usageCount > 0 ) { [_keysObservedByPolling setObject:[NSNumber numberWithUnsignedInteger:usageCount] forKey:key]; } else { [pollLog() info:@"%@: Removed polling observation for key:'%@'", self, key]; [_keysObservedByPolling removeObjectForKey:key]; } if ( [_keysObservedByPolling count] == 0 ) [[NGARemoteAccessibleObjectPollingManager sharedManager] removePollingForObject:self]; return usageCount; } -(void)checkForChangesToPolledKeys { _isCurrentlyPolling = YES; NSMutableDictionary * newValues = [NSMutableDictionary dictionary]; for (NSString * key in [_keysObservedByPolling allKeys]) { id cachedValue = [_cachedAttributeValues objectConvertingNullToNilForKey:key]; id currentValue = [self accessibilityAttributeValueForKey:key]; if ( !NSOBJECT_ISEQUAL(cachedValue, currentValue) ) [newValues setObjectConvertingNilToNull:currentValue forKey:key]; } if ( [newValues count] > 0 ) [self notifyObserversOfNewValuesForKeysInDictionary:newValues]; _isCurrentlyPolling = NO; } #pragma mark - Handling object destroyed notification -(void)markSelfAsDead { // Because this method removes all references to self from all other NGARemoteAccessibleObjects, // they could very well be the only thing retaining self. Since the table of all objects does // not retain the objects, this method could cause the retain count for self to hit 0. // For this reason, we want to delay our own release until this method completes. [[self retain] autorelease]; NSMutableSet * setOfAllObservedKeys = [NSMutableSet set]; [setOfAllObservedKeys addObjectsFromArray:[_keysObservedByPolling allKeys]]; [setOfAllObservedKeys unionSet:[self.notificationManager keysObservedByObject:self]]; [self notifyChangesToKeyValues:setOfAllObservedKeys occuringInBlock:^(id unretainedSelf) { [((NGARemoteAccessibleObject*)unretainedSelf)->_cachedAttributeValues removeAllObjects]; ((NGARemoteAccessibleObject*)unretainedSelf)->_isDead = YES; // makes sure all further valueForKey: calls return nil }]; [self.notificationManager unregisterObjectForAllNotifications:self]; // we will no longer change our values so we can wipe our record of it. // Since we have been marked as destroyed, we no longer are registered with the observer. [_keysObservedByPolling removeAllObjects]; LOG_DEBUG_LOG(memLog(), @"%@: Removing all references to self", self ); // remove all known references to this object // First, the polling manager [[NGARemoteAccessibleObjectPollingManager sharedManager] removePollingForObject:self]; // Then from any other accessible object that might have references to us in its cache NSEnumerator * objectEnumerator = [tableOfAllNGARemoteAccessibleObjects() objectEnumerator]; id object = nil; while ((object = [objectEnumerator nextObject])) { [object removeAllCachedObjectsMatchingObject:self]; } } -(void)removeAllCachedObjectsMatchingObject:(NGARemoteAccessibleObject*)element { NSArray * changingKeys = [_cachedAttributeValues allKeysForObject:element]; if ( [changingKeys count] > 0 ) LOG_DEBUG_LOG(memLog(), @"%@: Removing cached value:%@ which is contained in the keys:%@", self, element, changingKeys); [self notifyChangesToKeyValues:[NSSet setWithArray:changingKeys] occuringInBlock:^(id unretainedSelf) { [((NGARemoteAccessibleObject*)unretainedSelf)->_cachedAttributeValues removeObjectsForKeys:changingKeys]; }]; } #pragma mark - Public Getters -(pid_t)processIdentifier { return _savedProcessIdentifier; } #pragma mark Private Getters -(NGARemoteAccessibleObject*)applicationElement { if ( NSSTRING_ISEQUALTOSTRING(self.roleName, NSAccessibilityApplicationRole) ) return RETAIN(AUTORELEASE(self)); return [NGARemoteAccessibleObject applicationElementForProcessIdentifier:self.processIdentifier]; } +(NSSet*)keyPathsForValuesAffectingApplicationElement { return [NSSet setWithObjects:@"processIdentifier", nil]; } -(NGARemoteAccessibilityNotificationManager*)notificationManager { if ( !_notificationObserver ) _notificationObserver = [[NGARemoteAccessibilityNotificationManager observerForProcessIdentifier:self.processIdentifier] retain]; return _notificationObserver; } -(NSArray*)attributeNames { CFArrayRef propertyNames = NULL; AXUIElementCopyAttributeNames(self.elementRef, &propertyNames); return [(NSArray*)propertyNames autorelease]; } -(NSArray*)attributeValues { NSMutableArray * properties = [NSMutableArray arrayWithCapacity:self.attributeNames.count]; for (NSString * propertyName in self.attributeNames) [properties addObject:[self valueForKey:propertyName]]; return [NSArray arrayWithArray:properties ]; } -(NSString*)roleFullName { if ( !self.subRoleName ) return self.roleName; return [NSString stringWithFormat:@"%@:%@", self.roleName, self.subRoleName]; } +(NSSet*)keyPathsForValuesAffectingRoleFullName { return [NSSet setWithObjects:@"subRoleName", @"roleName", nil]; } @end #pragma mark - NGARemoteAccessibleObjectPollingManager @implementation NGARemoteAccessibleObjectPollingManager SINGLETON_IMPLEMENTATION(NGARemoteAccessibleObjectPollingManager, sharedManager) -(id)init { if ( !(self = [super init]) ) return nil; _pollingElements = [NSMutableSet new]; return self; } DEALLOC( RELEASE(_pollingTimer); RELEASE(_pollingElements); ); #pragma mark - -(void)addPollingForObject:(NGARemoteAccessibleObject*)element { if ( [_pollingElements count] == 0 ) _pollingTimer = RETAIN([NSTimer scheduledTimerWithTimeInterval:kPollingInterval target:self selector:@selector(pollingTimerFired:) userInfo:nil repeats:YES]); [_pollingElements addObject:[NSValue valueWithNonretainedObject:element]]; LOG_DEBUG_LOG(pollLog(), @"%@", element); } -(void)removePollingForObject:(NGARemoteAccessibleObject*)element { LOG_DEBUG_LOG(pollLog(), @"%@", element); [_pollingElements removeObject:[NSValue valueWithNonretainedObject:element]]; if ( [_pollingElements count] == 0 ) RELEASE(_pollingTimer); } #pragma mark - -(void)pollingTimerFired:(NSTimer*)timer { for (NSValue *nonretainedObject in [NSSet setWithSet:_pollingElements]) { // _polling elements could have removed the current object // as a result of the polling timer firing if ( ![_pollingElements containsObject:nonretainedObject] ) continue; id element = [nonretainedObject nonretainedObjectValue]; [element checkForChangesToPolledKeys]; } } @end #pragma mark - NGARemoteAccessibilityNotificationManager #define PID_KEY(x) ([NSNumber numberWithProcessIdentifier:(x)]) RUNTIME_CONSTANT_FUNCTION(NSMapTable *, tableOfAllNGAAccessibilityObserverObjects, RETAIN([NSMapTable mapTableWithKeyOptions:NSMapTableStrongMemory valueOptions:NSMapTableZeroingWeakMemory]) ); static void NGAAXObserverRefCallback( AXObserverRef observerRef, AXUIElementRef element, CFStringRef notification, void *refcon); @implementation NGARemoteAccessibilityNotificationManager +(NGARemoteAccessibilityNotificationManager*)observerForProcessIdentifier:(pid_t)pid { id existingInstance = [tableOfAllNGAAccessibilityObserverObjects() objectForKey:PID_KEY(pid)]; if ( existingInstance ) return AUTORELEASE(RETAIN(existingInstance)); return [[[NGARemoteAccessibilityNotificationManager alloc] initWithProcessIdentifier:pid] autorelease]; } -(id)initWithProcessIdentifier:(pid_t)pid { if ( !(self = [super init]) ) return nil; id key = PID_KEY(pid); NGARemoteAccessibilityNotificationManager * member = [tableOfAllNGAAccessibilityObserverObjects() objectForKey:key]; if ( member ) { LOG_DEBUG_LOG(observLog(), @"Already have a instance for %@ returning that instance...", member ); AUTORELEASE( self ); return RETAIN(member); } AXError error = AXObserverCreate(pid, NGAAXObserverRefCallback, &ref); if ( error != noErr ) { [self autorelease]; return nil; } CFRunLoopAddSource( CFRunLoopGetMain(), AXObserverGetRunLoopSource( ref ), kCFRunLoopCommonModes); [tableOfAllNGAAccessibilityObserverObjects() setObject:self forKey:key]; processIdentifier = pid; _keysObservedByNotifications = [[NSMapTable mapTableWithWeakToStrongObjects] retain]; LOG_DEBUG_LOG(observLog(), @"%@: INIT.", self ); return self; } -(void)dealloc { [tableOfAllNGAAccessibilityObserverObjects() removeObjectForKey:PID_KEY(processIdentifier)]; NSMutableArray * allKeys = [NSMutableArray arrayWithCapacity:_keysObservedByNotifications.count]; NSEnumerator * enumerator = [_keysObservedByNotifications keyEnumerator]; id object = nil; while ( (object = [enumerator nextObject]) ) [allKeys addObject:object]; for (id observer in allKeys) [self unregisterObjectForAllNotifications:observer]; RELEASE(_keysObservedByNotifications); CLEAN_CFRELEASE(ref); [super dealloc]; } -(NSRunningApplication*)runningApplication { return [NSRunningApplication runningApplicationWithProcessIdentifier:processIdentifier]; } -(NSString*)description { return [NSString stringWithFormat:@"NGARemoteAccessibilityObserver 0x%p (app:%@ pid:%d)", self, self.runningApplication.bundleIdentifier, self.runningApplication.processIdentifier]; } -(NSSet*)keysObservedByObject:(NGARemoteAccessibleObject*)object { NSMutableDictionary * observationCountsForKeys = [_keysObservedByNotifications objectForKey:object]; return [NSSet setWithArray:[observationCountsForKeys allKeys]]; } -(NSUInteger)notificationCountForObject:(NGARemoteAccessibleObject*)object notificationName:(NSString*)notificationName; { NSMutableDictionary * observationCountsForKeys = [_keysObservedByNotifications objectForKey:object]; return [[observationCountsForKeys objectForKey:notificationName] unsignedIntegerValue]; } -(void)registerObject:(NGARemoteAccessibleObject*)object forNotification:(NSString *)notificationName { if ( !notificationName ) return; LOG_DEBUG_LOG(observLog(), @"%@ addObserveringAccessibleObject: '%@' notificationName: '%@'", self, object, notificationName); NSMutableDictionary * observationCountsForKeys = [_keysObservedByNotifications objectForKey:object]; if ( !observationCountsForKeys ) { observationCountsForKeys = [NSMutableDictionary dictionary]; [_keysObservedByNotifications setObject:observationCountsForKeys forKey:object]; } NSUInteger usageCount = [[observationCountsForKeys objectForKey:notificationName] unsignedIntegerValue]; if ( usageCount == 0 ) { AXError error = AXObserverAddNotification(ref, object.elementRef, (CFStringRef)notificationName, (__bridge void*)self); if ( error != noErr ) LOG_DEBUG(@"%@: Could not add notification '%@' because of error \"%@(%d)\"", self, notificationName, AXErrorDescription(error), error); } usageCount++; [observationCountsForKeys setObject:[NSNumber numberWithUnsignedInteger:usageCount] forKey:notificationName]; } -(void)unregisterObject:(NGARemoteAccessibleObject*)object forNotification:(NSString *)notificationName; { if ( !notificationName ) return; NSMutableDictionary * observationCountsForKeys = [_keysObservedByNotifications objectForKey:object]; if ( !observationCountsForKeys ) { LOG_DEBUG(@"No notifications registered for %@", object); return; } NSUInteger usageCount = [[observationCountsForKeys objectForKey:notificationName] unsignedIntegerValue]; usageCount--; if ( usageCount > 0 ) { [observationCountsForKeys setObject:[NSNumber numberWithUnsignedInteger:usageCount] forKey:notificationName]; return; } NSAssert(usageCount == 0, @"usageCount != 0"); [self _clearObject:object notificationName:notificationName]; } -(void)unregisterObjectForAllNotifications:(NGARemoteAccessibleObject*)object { NSMutableDictionary * observationCountsForKeys = [_keysObservedByNotifications objectForKey:object]; NSArray * allKeys = [observationCountsForKeys allKeys]; for (NSString * notification in allKeys) [self _clearObject:object notificationName:notification]; NSAssert(([_keysObservedByNotifications objectForKey:object] == nil), @"not all notifications removed for %@", object); } -(void)_clearObject:(NGARemoteAccessibleObject*)object notificationName:(NSString*)notificationName { AXError error = AXObserverRemoveNotification(ref, object.elementRef, (CFStringRef)notificationName); if ( error != noErr ) LOG_DEBUG(@"Notification: '%@' cannot be removed on %@! \"%@(%d)\"", notificationName, self, AXErrorDescription(error), error); NSMutableDictionary * observationCountsForKeys = [_keysObservedByNotifications objectForKey:object]; [observationCountsForKeys removeObjectForKey:notificationName]; if ( observationCountsForKeys.count == 0 ) [_keysObservedByNotifications removeObjectForKey:object]; } @end void NGAAXObserverRefCallback( AXObserverRef observerRef, AXUIElementRef element, CFStringRef notification, void *refcon) { [NGARemoteAccessibleObject didReceiveAXNotification:(NSString*)notification withParameter:element]; } @implementation NGARemoteAccessibleObject (CoreAccessors) -(NGARemoteAccessibleObject*)parent { return [self valueForKey:NSAccessibilityParentAttribute]; } +(NSSet*)keyPathsForValuesAffectingParent { return [NSSet setWithObject:NSAccessibilityParentAttribute]; } -(NSUInteger)countOfChildren { id cachedChildren = [_cachedAttributeValues objectConvertingNullToNilForKey:NSAccessibilityChildrenAttribute]; if ( cachedChildren ) return [cachedChildren count]; CFIndex count = 0; AXUIElementGetAttributeValueCount(self.elementRef, kAXChildrenAttribute, &count); return (NSUInteger)count; } -(NSArray*)children { return [self valueForKey:NSAccessibilityChildrenAttribute]; } +(NSSet*)keyPathsForValuesAffectingChildren { return [NSSet setWithObject:NSAccessibilityChildrenAttribute]; } -(NSString*)roleName { return [self valueForKey:NSAccessibilityRoleAttribute]; } +(NSSet*)keyPathsForValuesAffectingRoleName { return [NSSet setWithObject:NSAccessibilityRoleAttribute]; } -(NSString*)roleDescription { return [self valueForKey:NSAccessibilityRoleDescriptionAttribute]; } +(NSSet*)keyPathsForValuesAffectingRoleDescription { return [NSSet setWithObject:NSAccessibilityRoleDescriptionAttribute]; } -(NSString*)subRoleName { return [self valueForKey:NSAccessibilitySubroleAttribute]; } +(NSSet*)keyPathsForValuesAffectingSubRoleName { return [NSSet setWithObject:NSAccessibilitySubroleAttribute]; } -(NSString*)roleFullDescription { return NSAccessibilityRoleDescription(self.roleName, self.subRoleName); } +(NSSet*)keyPathsForValuesAffectingRoleFullDescription { return [NSSet setWithObjects:@"subRoleName", @"roleName", nil]; } -(id)value { return [self valueForKey:NSAccessibilityValueAttribute]; } +(NSSet*)keyPathsForValuesAffectingValue { return [NSSet setWithObject:NSAccessibilityValueAttribute]; } @end