1// Copyright 2015-present 650 Industries. All rights reserved. 2 3#import "EXEnvironment.h" 4#import "EXKernelDevKeyCommands.h" 5#import "EXKernel.h" 6#import "EXKernelAppRegistry.h" 7#import "EXReactAppManager.h" 8 9#import <React/RCTDefines.h> 10#import <React/RCTUtils.h> 11 12#import <UIKit/UIKit.h> 13 14@interface EXKeyCommand : NSObject <NSCopying> 15 16@property (nonatomic, strong) UIKeyCommand *keyCommand; 17@property (nonatomic, copy) void (^block)(UIKeyCommand *); 18 19@end 20 21@implementation EXKeyCommand 22 23- (instancetype)initWithKeyCommand:(UIKeyCommand *)keyCommand 24 block:(void (^)(UIKeyCommand *))block 25{ 26 if ((self = [super init])) { 27 _keyCommand = keyCommand; 28 _block = block; 29 } 30 return self; 31} 32 33RCT_NOT_IMPLEMENTED(- (instancetype)init) 34 35- (id)copyWithZone:(__unused NSZone *)zone 36{ 37 return self; 38} 39 40- (NSUInteger)hash 41{ 42 return _keyCommand.input.hash ^ _keyCommand.modifierFlags; 43} 44 45- (BOOL)isEqual:(EXKeyCommand *)object 46{ 47 if (![object isKindOfClass:[EXKeyCommand class]]) { 48 return NO; 49 } 50 return [self matchesInput:object.keyCommand.input 51 flags:object.keyCommand.modifierFlags]; 52} 53 54- (BOOL)matchesInput:(NSString *)input flags:(UIKeyModifierFlags)flags 55{ 56 return [_keyCommand.input isEqual:input] && _keyCommand.modifierFlags == flags; 57} 58 59- (NSString *)description 60{ 61 return [NSString stringWithFormat:@"<%@:%p input=\"%@\" flags=%zd hasBlock=%@>", 62 [self class], self, _keyCommand.input, _keyCommand.modifierFlags, 63 _block ? @"YES" : @"NO"]; 64} 65 66@end 67 68@interface EXKernelDevKeyCommands () 69 70@property (nonatomic, strong) NSMutableSet<EXKeyCommand *> *commands; 71+ (void)handleKeyboardEvent:(UIEvent *)event; 72 73@end 74 75#if TARGET_IPHONE_SIMULATOR 76@interface UIEvent (UIPhysicalKeyboardEvent) 77 78@property (nonatomic) NSString *_modifiedInput; 79@property (nonatomic) NSString *_unmodifiedInput; 80@property (nonatomic) UIKeyModifierFlags _modifierFlags; 81@property (nonatomic) BOOL _isKeyDown; 82@property (nonatomic) long _keyCode; 83 84@end 85 86@implementation UIApplication (EXKeyCommands) 87 88- (void)EX_handleKeyUIEventSwizzle:(UIEvent *)event 89{ 90 BOOL interactionEnabled = !UIApplication.sharedApplication.isIgnoringInteractionEvents; 91 BOOL hasFirstResponder = NO; 92 93 if (interactionEnabled) { 94 UIResponder *firstResponder = nil; 95 for (UIWindow *window in [self windows]) { 96 firstResponder = [window valueForKey:@"firstResponder"]; 97 if (firstResponder) { 98 hasFirstResponder = YES; 99 break; 100 } 101 } 102 103 104 // Call the original swizzled method 105 [self EX_handleKeyUIEventSwizzle:event]; 106 107 if (firstResponder) { 108 BOOL isTextField = [firstResponder isKindOfClass: [UITextField class]] || [firstResponder isKindOfClass: [UITextView class]]; 109 110 // this is a runtime header that is not publicly exported from the Webkit.framework 111 // obfuscating selector WKContentView 112 NSArray<NSString *> *webViewClass = @[ @"WKCo", @"ntentVi", @"ew"]; 113 Class WKContentView = NSClassFromString([webViewClass componentsJoinedByString:@""]); 114 115 BOOL isWebView = [firstResponder isKindOfClass:[WKContentView class]]; 116 117 if (!isTextField && !isWebView) { 118 [EXKernelDevKeyCommands handleKeyboardEvent:event]; 119 } 120 } 121 } 122}; 123 124@end 125#endif 126 127@implementation UIResponder (EXKeyCommands) 128 129- (NSArray<UIKeyCommand *> *)EX_keyCommands 130{ 131 NSSet<EXKeyCommand *> *commands = [EXKernelDevKeyCommands sharedInstance].commands; 132 return [[commands valueForKeyPath:@"keyCommand"] allObjects]; 133} 134 135- (void)EX_handleKeyCommand:(UIKeyCommand *)key 136{ 137 // NOTE: throttle the key handler because on iOS 9 the handleKeyCommand: 138 // method gets called repeatedly if the command key is held down. 139 static NSTimeInterval lastCommand = 0; 140 if (CACurrentMediaTime() - lastCommand > 0.5) { 141 for (EXKeyCommand *command in [EXKernelDevKeyCommands sharedInstance].commands) { 142 if ([command.keyCommand.input isEqualToString:key.input] && 143 command.keyCommand.modifierFlags == key.modifierFlags) { 144 if (command.block) { 145 command.block(key); 146 lastCommand = CACurrentMediaTime(); 147 } 148 } 149 } 150 } 151} 152 153@end 154 155@implementation EXKernelDevKeyCommands 156 157+ (instancetype)sharedInstance 158{ 159 static EXKernelDevKeyCommands *instance; 160 static dispatch_once_t once; 161 dispatch_once(&once, ^{ 162 if (!instance) { 163 instance = [[EXKernelDevKeyCommands alloc] init]; 164 } 165 }); 166 return instance; 167} 168 169+ (void)initialize 170{ 171 // capture keycommands across all bridges. 172 // this is the same approach taken by RCTKeyCommands, 173 // but that class is disabled in the expo react native fork 174 // since there may be many instances of it. 175 RCTSwapInstanceMethods([UIResponder class], 176 @selector(keyCommands), 177 @selector(EX_keyCommands)); 178 179#if TARGET_IPHONE_SIMULATOR 180 SEL originalKeyboardSelector = NSSelectorFromString(@"handleKeyUIEvent:"); 181 RCTSwapInstanceMethods([UIApplication class], 182 originalKeyboardSelector, 183 @selector(EX_handleKeyUIEventSwizzle:)); 184#endif 185} 186 187#if TARGET_IPHONE_SIMULATOR 188+(void)handleKeyboardEvent:(UIEvent *)event 189{ 190 static NSTimeInterval lastCommand = 0; 191 192 if (event._isKeyDown) { 193 if (CACurrentMediaTime() - lastCommand > 0.5) { 194 NSString *input = event._modifiedInput; 195 if ([input isEqualToString: @"r"]) { 196 [[EXKernel sharedInstance] reloadVisibleApp]; 197 } 198 199 lastCommand = CACurrentMediaTime(); 200 } 201 } 202} 203#endif 204 205- (instancetype)init 206{ 207 if ((self = [super init])) { 208 _commands = [NSMutableSet set]; 209 } 210 return self; 211} 212 213#pragma mark - expo dev commands 214 215- (void)registerDevCommands 216{ 217 __weak typeof(self) weakSelf = self; 218 [self registerKeyCommandWithInput:@"d" 219 modifierFlags:UIKeyModifierCommand 220 action:^(__unused UIKeyCommand *_) { 221 [weakSelf _handleMenuCommand]; 222 }]; 223 [self registerKeyCommandWithInput:@"r" 224 modifierFlags:UIKeyModifierCommand 225 action:^(__unused UIKeyCommand *_) { 226 [weakSelf _handleRefreshCommand]; 227 }]; 228 [self registerKeyCommandWithInput:@"n" 229 modifierFlags:UIKeyModifierCommand 230 action:^(__unused UIKeyCommand *_) { 231 [weakSelf _handleDisableDebuggingCommand]; 232 }]; 233 [self registerKeyCommandWithInput:@"i" 234 modifierFlags:UIKeyModifierCommand 235 action:^(__unused UIKeyCommand *_) { 236 [weakSelf _handleToggleInspectorCommand]; 237 }]; 238 [self registerKeyCommandWithInput:@"k" 239 modifierFlags:UIKeyModifierCommand | UIKeyModifierControl 240 action:^(__unused UIKeyCommand *_) { 241 [weakSelf _handleKernelMenuCommand]; 242 }]; 243 244} 245 246- (void)_handleMenuCommand 247{ 248 if ([EXEnvironment sharedEnvironment].isDetached) { 249 [[EXKernel sharedInstance].visibleApp.appManager showDevMenu]; 250 } else { 251 [[EXKernel sharedInstance] switchTasks]; 252 } 253} 254 255- (void)_handleRefreshCommand 256{ 257 // This reloads only JS 258 // [[EXKernel sharedInstance].visibleApp.appManager reloadBridge]; 259 260 // This reloads manifest and JS 261 [[EXKernel sharedInstance] reloadVisibleApp]; 262} 263 264- (void)_handleDisableDebuggingCommand 265{ 266 [[EXKernel sharedInstance].visibleApp.appManager disableRemoteDebugging]; 267} 268 269- (void)_handleToggleRemoteDebuggingCommand 270{ 271 [[EXKernel sharedInstance].visibleApp.appManager toggleRemoteDebugging]; 272 // This reloads manifest and JS 273 [[EXKernel sharedInstance] reloadVisibleApp]; 274} 275 276- (void)_handleTogglePerformanceMonitorCommand 277{ 278 [[EXKernel sharedInstance].visibleApp.appManager togglePerformanceMonitor]; 279} 280 281- (void)_handleToggleInspectorCommand 282{ 283 [[EXKernel sharedInstance].visibleApp.appManager toggleElementInspector]; 284} 285 286- (void)_handleKernelMenuCommand 287{ 288 if ([EXKernel sharedInstance].visibleApp == [EXKernel sharedInstance].appRegistry.homeAppRecord) { 289 [[EXKernel sharedInstance].appRegistry.homeAppRecord.appManager showDevMenu]; 290 } 291} 292 293#pragma mark - managing list of commands 294 295- (void)registerKeyCommandWithInput:(NSString *)input 296 modifierFlags:(UIKeyModifierFlags)flags 297 action:(void (^)(UIKeyCommand *))block 298{ 299 RCTAssertMainQueue(); 300 301 UIKeyCommand *command = [UIKeyCommand keyCommandWithInput:input 302 modifierFlags:flags 303 action:@selector(EX_handleKeyCommand:)]; 304 305 EXKeyCommand *keyCommand = [[EXKeyCommand alloc] initWithKeyCommand:command block:block]; 306 [_commands removeObject:keyCommand]; 307 [_commands addObject:keyCommand]; 308} 309 310- (void)unregisterKeyCommandWithInput:(NSString *)input 311 modifierFlags:(UIKeyModifierFlags)flags 312{ 313 RCTAssertMainQueue(); 314 315 for (EXKeyCommand *command in _commands.allObjects) { 316 if ([command matchesInput:input flags:flags]) { 317 [_commands removeObject:command]; 318 break; 319 } 320 } 321} 322 323- (BOOL)isKeyCommandRegisteredForInput:(NSString *)input 324 modifierFlags:(UIKeyModifierFlags)flags 325{ 326 RCTAssertMainQueue(); 327 328 for (EXKeyCommand *command in _commands) { 329 if ([command matchesInput:input flags:flags]) { 330 return YES; 331 } 332 } 333 return NO; 334} 335 336@end 337 338