// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/ui/broadcaster/chrome_broadcaster.h"
#import <objc/runtime.h>
#import <memory>
#import "base/apple/foundation_util.h"
#import "base/check.h"
#import "base/ios/crb_protocol_observers.h"
#import "base/notreached.h"
namespace {
// Constructs an NSInvocation that will be used for repeated execution of
// `selector`. `selector` must return void and take exactly one argument; it is
// an error otherwise.
NSInvocation* InvocationForBroadcasterSelector(SEL selector) {
struct objc_method_description methodDesc = protocol_getMethodDescription(
@protocol(ChromeBroadcastObserver), selector,
NO /* not a required method */, YES /* an instance method */);
DCHECK(methodDesc.types);
NSMethodSignature* method =
[NSMethodSignature signatureWithObjCTypes:methodDesc.types];
DCHECK(method);
// There should always be exactly three arguments: the two implicit arguments
// that every Objective-C method has (self and _cmd), and the single value
// argument for the broadcast value.
DCHECK(method.numberOfArguments == 3);
// Methods should always return void.
DCHECK(strcmp(method.methodReturnType, @encode(void)) == 0);
NSInvocation* invocation =
[NSInvocation invocationWithMethodSignature:method];
invocation.selector = selector;
return invocation;
}
}
// Protocol observer subclass that explicitly implements <BroadcastObserver>.
// Mostly this is used for the non-retaining observer set; this requires
// observers to be removed before they dealloc. It would be better to track
// observer lifetime via associated objects and remove them automatically.
@interface BroadcastObservers : CRBProtocolObservers<ChromeBroadcastObserver>
+ (instancetype)observers;
@end
@implementation BroadcastObservers
+ (instancetype)observers {
return [self observersWithProtocol:@protocol(ChromeBroadcastObserver)];
}
@end
// An object that collects the information about a single observed property.
@interface BroadcastItem : NSObject
// The observed object.
@property(nonatomic, readonly) NSObject* object;
// The observed key path.
@property(nonatomic, readonly, copy) NSString* key;
// The name associated with this observation.
@property(nonatomic, readonly, copy) NSString* name;
// The current value of `key` on `object`.
@property(nonatomic, readonly) NSValue* currentValue;
// Designated initializer.
- (instancetype)initWithObject:(NSObject*)object
key:(NSString*)key
name:(NSString*)name NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
// Add `observer` as a KVO of the object and key represented by the receiver.
- (void)addObserver:(NSObject*)observer;
// Remove `observer` from the KVO represented by the receiver.
- (void)removeObserver:(NSObject*)observer;
@end
@implementation BroadcastItem
@synthesize object = _object;
@synthesize key = _key;
@synthesize name = _name;
- (instancetype)initWithObject:(NSObject*)object
key:(NSString*)key
name:(NSString*)name {
if ((self = [super init])) {
_object = object;
_key = [key copy];
_name = [name copy];
}
return self;
}
- (NSValue*)currentValue {
return [self.object valueForKey:self.key];
}
- (void)addObserver:(NSObject*)observer {
// Important: because the NSKeyValueObservingOptionInitial is passed in,
// addObserver:forKeyPath:options:context: will trigger a notification before
// it returns, so all of the infrastructure for handling the notification must
// be in place before the -addObserver... call.
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld |
NSKeyValueObservingOptionInitial;
// So that the selector name to be used for this object/key pair can be
// retrieved, it's added as an opaque 'context' object. These names are
// constant strings used as keys in the immutable _observerInvocations
// dictionary, which will thus live as long as this object does.
[self.object addObserver:observer
forKeyPath:self.key
options:options
context:(__bridge void*)self.name];
}
- (void)removeObserver:(NSObject*)observer {
[self.object removeObserver:observer
forKeyPath:self.key
context:(__bridge void*)self.name];
}
@end
@interface ChromeBroadcaster ()
// Map of selectors (as strings) to observers.
@property(nonatomic, readonly)
NSMutableDictionary<NSString*, BroadcastObservers*>* observers;
// Map of selectors (as strings) to broadcast items.
@property(nonatomic, readonly)
NSMutableDictionary<NSString*, BroadcastItem*>* items;
// Map of selectors (as strings) to invocations to be called on observers.
// Invocations should be fetched from this dictionary via the
// -invocationForName:value: method.
@property(nonatomic, readonly)
NSDictionary<NSString*, NSInvocation*>* observerInvocations;
@end
@implementation ChromeBroadcaster
@synthesize observers = _observers;
@synthesize items = _items;
@synthesize observerInvocations = _observerInvocations;
- (instancetype)init {
if ((self = [super init])) {
_observers =
[[NSMutableDictionary<NSString*, BroadcastObservers*> alloc] init];
_items = [[NSMutableDictionary<NSString*, BroadcastItem*> alloc] init];
// Pre-build the map of selector names to invocations. The source of
// selectors is the optional methods defined (directly) in the
// BroadcastObserver protocol.
NSMutableDictionary<NSString*, NSInvocation*>* observerInvocations =
[[NSMutableDictionary<NSString*, NSInvocation*> alloc] init];
unsigned int methodCount;
objc_method_description* instanceMethods =
protocol_copyMethodDescriptionList(
@protocol(ChromeBroadcastObserver), NO /* not required methods */,
YES /* instance methods */, &methodCount);
for (unsigned int i = 0; i < methodCount; i++) {
struct objc_method_description method = instanceMethods[i];
NSString* name = NSStringFromSelector(method.name);
observerInvocations[name] = InvocationForBroadcasterSelector(method.name);
}
free(instanceMethods);
_observerInvocations = [observerInvocations copy];
}
return self;
}
- (void)dealloc {
for (NSString* name in self.items.allKeys) {
[self stopBroadcastingForSelector:NSSelectorFromString(name)];
}
}
- (void)broadcastValue:(NSString*)valueKey
ofObject:(NSObject*)object
selector:(SEL)selector {
NSString* name = NSStringFromSelector(selector);
// Sanity check: `selector` must be one of the selectors that are mapped.
DCHECK(self.observerInvocations[name]);
// Sanity check: `selector` must not already be broadcast.
DCHECK(!self.items[name]);
// TODO(crbug.com/40519578) -- Another sanity check is needed here -- verify
// that the value to be observed is of the type that `selector` expects.
self.items[name] =
[[BroadcastItem alloc] initWithObject:object key:valueKey name:name];
[self.items[name] addObserver:self];
}
// This is usually only needed when the broadcasting object goes away, since
// it's an exception for an object with key-value observers to dealloc. This
// should just be handled by associating monitor objects with the broadcasting
// object instead.
- (void)stopBroadcastingForSelector:(SEL)selector {
NSString* name = NSStringFromSelector(selector);
[self.items[name] removeObserver:self];
[self.items removeObjectForKey:name];
}
- (void)addObserver:(id<ChromeBroadcastObserver>)observer
forSelector:(SEL)selector {
NSString* name = NSStringFromSelector(selector);
// Sanity check: `selector` must be one of the keys that are mapped.
DCHECK(self.observerInvocations[name]);
// Sanity check: `observer` must implement the selector for `selector`.
DCHECK([observer respondsToSelector:selector]);
if (!self.observers[name])
self.observers[name] = [BroadcastObservers observers];
// If the key is already being broadcast, update the observer immediately.
if (self.items[name]) {
NSInvocation* call =
[self invocationForName:name value:self.items[name].currentValue];
[call invokeWithTarget:observer];
}
[self.observers[name] addObserver:observer];
}
- (void)removeObserver:(id<ChromeBroadcastObserver>)observer
forSelector:(SEL)selector {
NSString* name = NSStringFromSelector(selector);
// Sanity check: `selector` must be one of the selectors that are mapped.
DCHECK(self.observerInvocations[name]);
[self.observers[name] removeObserver:observer];
if (self.observers[name].empty)
[self.observers removeObjectForKey:name];
}
#pragma mark - KVO
- (void)observeValueForKeyPath:(NSString*)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey, id>*)change
context:(void*)context {
// Bridge cast the context back to a selector name.
NSString* name = (__bridge NSString*)context;
// Sanity check: `name` must be one of the selectors that are mapped.
DCHECK(self.observerInvocations[name]);
// Sanity check: `object` should be the object currently being observed for
// `name`.
DCHECK(self.items[name].object == object);
BroadcastObservers* observers = self.observers[name];
if (!observers)
return;
// Sanity check: this isn't a change to a collection -- where the observed
// property is a collection object and this change is (for example) the
// addition of a new object to the collection. That kind of KVO isn't
// supported by ChromeBroadcaster.
DCHECK([change[NSKeyValueChangeKindKey]
isEqualToValue:@(NSKeyValueChangeSetting)]);
// If strings or other non-value types are being broadcast, then this will
// need to change. Either value will be nil if they aren't actually NSValues.
NSValue* newValue =
base::apple::ObjCCast<NSValue>(change[NSKeyValueChangeNewKey]);
NSValue* oldValue =
base::apple::ObjCCast<NSValue>(change[NSKeyValueChangeOldKey]);
// If the value is unchanged -- if the old and new values are equal -- then
// return without notifying observers.
// -isEqualToValue doesn't deal with nil arguments well, so nil check oldValue
// here.
if (oldValue && [newValue isEqualToValue:oldValue])
return;
NSInvocation* call = [self invocationForName:name value:newValue];
[call invokeWithTarget:observers];
}
#pragma mark - internal
// Returns the invocation for the selector named `name`, populated with
// `value` as the argument.
// This method mutates the invocations stored in `self.observerInvocations`, so
// any code that gets an invocation from that dictionary to be invoked should
// do so through this method.
- (NSInvocation*)invocationForName:(NSString*)name value:(NSValue*)value {
NSInvocation* invocation = self.observerInvocations[name];
// Attempt to cast `value` into an NSNumber; ObjCCast will instead return
// nil if this isn't possible.
NSNumber* valueAsNumber = base::apple::ObjCCast<NSNumber>(value);
std::string type([invocation.methodSignature getArgumentTypeAtIndex:2]);
if (type == @encode(BOOL)) {
DCHECK(valueAsNumber);
BOOL boolValue = valueAsNumber.boolValue;
[invocation setArgument:&boolValue atIndex:2];
} else if (type == @encode(CGFloat)) {
DCHECK(valueAsNumber);
// CGFloat is a float on 32-bit devices, but a double on 64-bit devices.
#if CGFLOAT_IS_DOUBLE
CGFloat cgfloatValue = valueAsNumber.doubleValue;
#else
CGFloat cgfloatValue = valueAsNumber.floatValue;
#endif
[invocation setArgument:&cgfloatValue atIndex:2];
} else if (type == @encode(CGRect)) {
CGRect rectValue = value.CGRectValue;
[invocation setArgument:&rectValue atIndex:2];
} else if (type == @encode(CGSize)) {
CGSize sizeValue = value.CGSizeValue;
[invocation setArgument:&sizeValue atIndex:2];
} else if (type == @encode(UIEdgeInsets)) {
UIEdgeInsets insetValue = value.UIEdgeInsetsValue;
[invocation setArgument:&insetValue atIndex:2];
} else if (type == @encode(int)) {
DCHECK(valueAsNumber);
int intValue = valueAsNumber.intValue;
[invocation setArgument:&intValue atIndex:2];
} else {
// Add more clauses as needed.
NOTREACHED_IN_MIGRATION() << "Unknown argument type: " << type;
return nil;
}
return invocation;
}
@end