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#import <React/RCTPackagerConnection.h> 12 13#import <UIKit/UIKit.h> 14 15@interface EXKeyCommand : NSObject <NSCopying> 16 17@property (nonatomic, strong) UIKeyCommand *keyCommand; 18@property (nonatomic, copy) void (^block)(UIKeyCommand *); 19 20@end 21 22@implementation EXKeyCommand 23 24- (instancetype)initWithKeyCommand:(UIKeyCommand *)keyCommand 25 block:(void (^)(UIKeyCommand *))block 26{ 27 if ((self = [super init])) { 28 _keyCommand = keyCommand; 29 _block = block; 30 } 31 return self; 32} 33 34RCT_NOT_IMPLEMENTED(- (instancetype)init) 35 36- (id)copyWithZone:(__unused NSZone *)zone 37{ 38 return self; 39} 40 41- (NSUInteger)hash 42{ 43 return _keyCommand.input.hash ^ _keyCommand.modifierFlags; 44} 45 46- (BOOL)isEqual:(EXKeyCommand *)object 47{ 48 if (![object isKindOfClass:[EXKeyCommand class]]) { 49 return NO; 50 } 51 return [self matchesInput:object.keyCommand.input 52 flags:object.keyCommand.modifierFlags]; 53} 54 55- (BOOL)matchesInput:(NSString *)input flags:(UIKeyModifierFlags)flags 56{ 57 return [_keyCommand.input isEqual:input] && _keyCommand.modifierFlags == flags; 58} 59 60- (NSString *)description 61{ 62 return [NSString stringWithFormat:@"<%@:%p input=\"%@\" flags=%zd hasBlock=%@>", 63 [self class], self, _keyCommand.input, _keyCommand.modifierFlags, 64 _block ? @"YES" : @"NO"]; 65} 66 67@end 68 69@interface EXKernelDevKeyCommands () 70 71@property (nonatomic, strong) NSMutableSet<EXKeyCommand *> *commands; 72 73@end 74 75@implementation UIResponder (EXKeyCommands) 76 77- (NSArray<UIKeyCommand *> *)EX_keyCommands 78{ 79 NSSet<EXKeyCommand *> *commands = [EXKernelDevKeyCommands sharedInstance].commands; 80 return [[commands valueForKeyPath:@"keyCommand"] allObjects]; 81} 82 83- (void)EX_handleKeyCommand:(UIKeyCommand *)key 84{ 85 // NOTE: throttle the key handler because on iOS 9 the handleKeyCommand: 86 // method gets called repeatedly if the command key is held down. 87 static NSTimeInterval lastCommand = 0; 88 if (CACurrentMediaTime() - lastCommand > 0.5) { 89 for (EXKeyCommand *command in [EXKernelDevKeyCommands sharedInstance].commands) { 90 if ([command.keyCommand.input isEqualToString:key.input] && 91 command.keyCommand.modifierFlags == key.modifierFlags) { 92 if (command.block) { 93 command.block(key); 94 lastCommand = CACurrentMediaTime(); 95 } 96 } 97 } 98 } 99} 100 101@end 102 103@implementation EXKernelDevKeyCommands 104 105+ (instancetype)sharedInstance 106{ 107 static EXKernelDevKeyCommands *instance; 108 static dispatch_once_t once; 109 dispatch_once(&once, ^{ 110 if (!instance) { 111 instance = [[EXKernelDevKeyCommands alloc] init]; 112 } 113 }); 114 return instance; 115} 116 117+ (void)initialize 118{ 119 // capture keycommands across all bridges. 120 // this is the same approach taken by RCTKeyCommands, 121 // but that class is disabled in the expo react native fork 122 // since there may be many instances of it. 123 RCTSwapInstanceMethods([UIResponder class], 124 @selector(keyCommands), 125 @selector(EX_keyCommands)); 126} 127 128- (instancetype)init 129{ 130 if ((self = [super init])) { 131 _commands = [NSMutableSet set]; 132 } 133 return self; 134} 135 136#pragma mark - expo dev commands 137 138- (void)registerDevCommands 139{ 140 __weak typeof(self) weakSelf = self; 141 [self registerKeyCommandWithInput:@"d" 142 modifierFlags:UIKeyModifierCommand 143 action:^(__unused UIKeyCommand *_) { 144 [weakSelf _handleMenuCommand]; 145 }]; 146 [self registerKeyCommandWithInput:@"r" 147 modifierFlags:UIKeyModifierCommand 148 action:^(__unused UIKeyCommand *_) { 149 [weakSelf _handleRefreshCommand]; 150 }]; 151 [self registerKeyCommandWithInput:@"n" 152 modifierFlags:UIKeyModifierCommand 153 action:^(__unused UIKeyCommand *_) { 154 [weakSelf _handleDisableDebuggingCommand]; 155 }]; 156 [self registerKeyCommandWithInput:@"i" 157 modifierFlags:UIKeyModifierCommand 158 action:^(__unused UIKeyCommand *_) { 159 [weakSelf _handleToggleInspectorCommand]; 160 }]; 161 [self registerKeyCommandWithInput:@"k" 162 modifierFlags:UIKeyModifierCommand | UIKeyModifierControl 163 action:^(__unused UIKeyCommand *_) { 164 [weakSelf _handleKernelMenuCommand]; 165 }]; 166 167 // Attach listeners to the bundler's dev server web socket connection. 168 // This enables tools to automatically reload the client remotely (i.e. in expo-cli). 169 170 // Enable a lot of tools under the same command namespace 171 [[RCTPackagerConnection sharedPackagerConnection] 172 addNotificationHandler:^(id params) { 173 if (params != [NSNull null] && (NSDictionary *)params) { 174 NSDictionary *_params = (NSDictionary *)params; 175 if (_params[@"name"] != nil && (NSString *)_params[@"name"]) { 176 NSString *name = _params[@"name"]; 177 if ([name isEqualToString:@"reload"]) { 178 [weakSelf _handleRefreshCommand]; 179 } else if ([name isEqualToString:@"toggleDevMenu"]) { 180 [weakSelf _handleMenuCommand]; 181 } else if ([name isEqualToString:@"toggleRemoteDebugging"]) { 182 [weakSelf _handleToggleRemoteDebuggingCommand]; 183 } else if ([name isEqualToString:@"toggleElementInspector"]) { 184 [weakSelf _handleToggleInspectorCommand]; 185 } else if ([name isEqualToString:@"togglePerformanceMonitor"]) { 186 [weakSelf _handleTogglePerformanceMonitorCommand]; 187 } 188 } 189 } 190 } 191 queue:dispatch_get_main_queue() 192 forMethod:@"sendDevCommand"]; 193 194 // These (reload and devMenu) are here to match RN dev tooling. 195 196 // Reload the app on "reload" 197 [[RCTPackagerConnection sharedPackagerConnection] 198 addNotificationHandler:^(id params) { 199 [weakSelf _handleRefreshCommand]; 200 } 201 queue:dispatch_get_main_queue() 202 forMethod:@"reload"]; 203 204 // Open the dev menu on "devMenu" 205 [[RCTPackagerConnection sharedPackagerConnection] 206 addNotificationHandler:^(id params) { 207 [weakSelf _handleMenuCommand]; 208 } 209 queue:dispatch_get_main_queue() 210 forMethod:@"devMenu"]; 211} 212 213- (void)_handleMenuCommand 214{ 215 if ([EXEnvironment sharedEnvironment].isDetached) { 216 [[EXKernel sharedInstance].visibleApp.appManager showDevMenu]; 217 } else { 218 [[EXKernel sharedInstance] switchTasks]; 219 } 220} 221 222- (void)_handleRefreshCommand 223{ 224 // This reloads only JS 225 // [[EXKernel sharedInstance].visibleApp.appManager reloadBridge]; 226 227 // This reloads manifest and JS 228 [[EXKernel sharedInstance] reloadVisibleApp]; 229} 230 231- (void)_handleDisableDebuggingCommand 232{ 233 [[EXKernel sharedInstance].visibleApp.appManager disableRemoteDebugging]; 234} 235 236- (void)_handleToggleRemoteDebuggingCommand 237{ 238 [[EXKernel sharedInstance].visibleApp.appManager toggleRemoteDebugging]; 239 // This reloads manifest and JS 240 [[EXKernel sharedInstance] reloadVisibleApp]; 241} 242 243- (void)_handleTogglePerformanceMonitorCommand 244{ 245 [[EXKernel sharedInstance].visibleApp.appManager togglePerformanceMonitor]; 246} 247 248- (void)_handleToggleInspectorCommand 249{ 250 [[EXKernel sharedInstance].visibleApp.appManager toggleElementInspector]; 251} 252 253- (void)_handleKernelMenuCommand 254{ 255 if ([EXKernel sharedInstance].visibleApp == [EXKernel sharedInstance].appRegistry.homeAppRecord) { 256 [[EXKernel sharedInstance].appRegistry.homeAppRecord.appManager showDevMenu]; 257 } 258} 259 260#pragma mark - managing list of commands 261 262- (void)registerKeyCommandWithInput:(NSString *)input 263 modifierFlags:(UIKeyModifierFlags)flags 264 action:(void (^)(UIKeyCommand *))block 265{ 266 RCTAssertMainQueue(); 267 268 UIKeyCommand *command = [UIKeyCommand keyCommandWithInput:input 269 modifierFlags:flags 270 action:@selector(EX_handleKeyCommand:)]; 271 272 EXKeyCommand *keyCommand = [[EXKeyCommand alloc] initWithKeyCommand:command block:block]; 273 [_commands removeObject:keyCommand]; 274 [_commands addObject:keyCommand]; 275} 276 277- (void)unregisterKeyCommandWithInput:(NSString *)input 278 modifierFlags:(UIKeyModifierFlags)flags 279{ 280 RCTAssertMainQueue(); 281 282 for (EXKeyCommand *command in _commands.allObjects) { 283 if ([command matchesInput:input flags:flags]) { 284 [_commands removeObject:command]; 285 break; 286 } 287 } 288} 289 290- (BOOL)isKeyCommandRegisteredForInput:(NSString *)input 291 modifierFlags:(UIKeyModifierFlags)flags 292{ 293 RCTAssertMainQueue(); 294 295 for (EXKeyCommand *command in _commands) { 296 if ([command matchesInput:input flags:flags]) { 297 return YES; 298 } 299 } 300 return NO; 301} 302 303@end 304 305