10fbf6a33SBen Roth// Copyright 2015-present 650 Industries. All rights reserved.
20fbf6a33SBen Roth
300fcd3c6SBen Roth#import "EXEnvironment.h"
40fbf6a33SBen Roth#import "EXKernelDevKeyCommands.h"
50fbf6a33SBen Roth#import "EXKernel.h"
689c6c39cSEric Samelson#import "EXKernelAppRegistry.h"
77f6367d0SBen Roth#import "EXReactAppManager.h"
80fbf6a33SBen Roth
9bd9e856bSJames Ide#import <React/RCTDefines.h>
10bd9e856bSJames Ide#import <React/RCTUtils.h>
110fbf6a33SBen Roth
120fbf6a33SBen Roth#import <UIKit/UIKit.h>
130fbf6a33SBen Roth
140fbf6a33SBen Roth@interface EXKeyCommand : NSObject <NSCopying>
150fbf6a33SBen Roth
160fbf6a33SBen Roth@property (nonatomic, strong) UIKeyCommand *keyCommand;
170fbf6a33SBen Roth@property (nonatomic, copy) void (^block)(UIKeyCommand *);
180fbf6a33SBen Roth
190fbf6a33SBen Roth@end
200fbf6a33SBen Roth
210fbf6a33SBen Roth@implementation EXKeyCommand
220fbf6a33SBen Roth
230fbf6a33SBen Roth- (instancetype)initWithKeyCommand:(UIKeyCommand *)keyCommand
240fbf6a33SBen Roth                             block:(void (^)(UIKeyCommand *))block
250fbf6a33SBen Roth{
260fbf6a33SBen Roth  if ((self = [super init])) {
270fbf6a33SBen Roth    _keyCommand = keyCommand;
280fbf6a33SBen Roth    _block = block;
290fbf6a33SBen Roth  }
300fbf6a33SBen Roth  return self;
310fbf6a33SBen Roth}
320fbf6a33SBen Roth
330fbf6a33SBen RothRCT_NOT_IMPLEMENTED(- (instancetype)init)
340fbf6a33SBen Roth
350fbf6a33SBen Roth- (id)copyWithZone:(__unused NSZone *)zone
360fbf6a33SBen Roth{
370fbf6a33SBen Roth  return self;
380fbf6a33SBen Roth}
390fbf6a33SBen Roth
400fbf6a33SBen Roth- (NSUInteger)hash
410fbf6a33SBen Roth{
420fbf6a33SBen Roth  return _keyCommand.input.hash ^ _keyCommand.modifierFlags;
430fbf6a33SBen Roth}
440fbf6a33SBen Roth
450fbf6a33SBen Roth- (BOOL)isEqual:(EXKeyCommand *)object
460fbf6a33SBen Roth{
470fbf6a33SBen Roth  if (![object isKindOfClass:[EXKeyCommand class]]) {
480fbf6a33SBen Roth    return NO;
490fbf6a33SBen Roth  }
500fbf6a33SBen Roth  return [self matchesInput:object.keyCommand.input
510fbf6a33SBen Roth                      flags:object.keyCommand.modifierFlags];
520fbf6a33SBen Roth}
530fbf6a33SBen Roth
540fbf6a33SBen Roth- (BOOL)matchesInput:(NSString *)input flags:(UIKeyModifierFlags)flags
550fbf6a33SBen Roth{
560fbf6a33SBen Roth  return [_keyCommand.input isEqual:input] && _keyCommand.modifierFlags == flags;
570fbf6a33SBen Roth}
580fbf6a33SBen Roth
590fbf6a33SBen Roth- (NSString *)description
600fbf6a33SBen Roth{
610fbf6a33SBen Roth  return [NSString stringWithFormat:@"<%@:%p input=\"%@\" flags=%zd hasBlock=%@>",
620fbf6a33SBen Roth          [self class], self, _keyCommand.input, _keyCommand.modifierFlags,
630fbf6a33SBen Roth          _block ? @"YES" : @"NO"];
640fbf6a33SBen Roth}
650fbf6a33SBen Roth
660fbf6a33SBen Roth@end
670fbf6a33SBen Roth
680fbf6a33SBen Roth@interface EXKernelDevKeyCommands ()
690fbf6a33SBen Roth
700fbf6a33SBen Roth@property (nonatomic, strong) NSMutableSet<EXKeyCommand *> *commands;
7105a98924Sandy+ (void)handleKeyboardEvent:(UIEvent *)event;
720fbf6a33SBen Roth
730fbf6a33SBen Roth@end
740fbf6a33SBen Roth
7505a98924Sandy#if TARGET_IPHONE_SIMULATOR
7605a98924Sandy@interface UIEvent (UIPhysicalKeyboardEvent)
7705a98924Sandy
7805a98924Sandy@property (nonatomic) NSString *_modifiedInput;
7905a98924Sandy@property (nonatomic) NSString *_unmodifiedInput;
8005a98924Sandy@property (nonatomic) UIKeyModifierFlags _modifierFlags;
8105a98924Sandy@property (nonatomic) BOOL _isKeyDown;
8205a98924Sandy@property (nonatomic) long _keyCode;
8305a98924Sandy
8405a98924Sandy@end
8505a98924Sandy
8605a98924Sandy@implementation UIApplication (EXKeyCommands)
8705a98924Sandy
8805a98924Sandy- (void)EX_handleKeyUIEventSwizzle:(UIEvent *)event
8905a98924Sandy{
9005a98924Sandy  BOOL interactionEnabled = !UIApplication.sharedApplication.isIgnoringInteractionEvents;
9105a98924Sandy  BOOL hasFirstResponder = NO;
9205a98924Sandy
9305a98924Sandy  if (interactionEnabled) {
9405a98924Sandy    UIResponder *firstResponder = nil;
9505a98924Sandy    for (UIWindow *window in [self windows]) {
9605a98924Sandy      firstResponder = [window valueForKey:@"firstResponder"];
9705a98924Sandy      if (firstResponder) {
9805a98924Sandy        hasFirstResponder = YES;
9905a98924Sandy        break;
10005a98924Sandy      }
10105a98924Sandy    }
10205a98924Sandy
10305a98924Sandy
10405a98924Sandy    // Call the original swizzled method
10505a98924Sandy    [self EX_handleKeyUIEventSwizzle:event];
10605a98924Sandy
10705a98924Sandy    if (firstResponder) {
10805a98924Sandy      BOOL isTextField = [firstResponder isKindOfClass: [UITextField class]] || [firstResponder isKindOfClass: [UITextView class]];
10905a98924Sandy
11072551c71Sandy      // this is a runtime header that is not publicly exported from the Webkit.framework
11172551c71Sandy      // obfuscating selector WKContentView
11272551c71Sandy      NSArray<NSString *> *webViewClass = @[ @"WKCo", @"ntentVi", @"ew"];
11372551c71Sandy      Class WKContentView = NSClassFromString([webViewClass componentsJoinedByString:@""]);
11472551c71Sandy
11572551c71Sandy      BOOL isWebView = [firstResponder isKindOfClass:[WKContentView class]];
11672551c71Sandy
11772551c71Sandy      if (!isTextField && !isWebView) {
11805a98924Sandy        [EXKernelDevKeyCommands handleKeyboardEvent:event];
11905a98924Sandy      }
12005a98924Sandy    }
12105a98924Sandy  }
12205a98924Sandy};
12305a98924Sandy
12405a98924Sandy@end
12505a98924Sandy#endif
12605a98924Sandy
1270fbf6a33SBen Roth@implementation UIResponder (EXKeyCommands)
1280fbf6a33SBen Roth
1290fbf6a33SBen Roth- (NSArray<UIKeyCommand *> *)EX_keyCommands
1300fbf6a33SBen Roth{
131*6c55e685SŁukasz Kosmaty  if ([self isKindOfClass:[UITextView class]] || [self isKindOfClass:[UITextField class]]) {
132*6c55e685SŁukasz Kosmaty    return @[];
133*6c55e685SŁukasz Kosmaty  }
1340fbf6a33SBen Roth  NSSet<EXKeyCommand *> *commands = [EXKernelDevKeyCommands sharedInstance].commands;
1350fbf6a33SBen Roth  return [[commands valueForKeyPath:@"keyCommand"] allObjects];
1360fbf6a33SBen Roth}
1370fbf6a33SBen Roth
1380fbf6a33SBen Roth- (void)EX_handleKeyCommand:(UIKeyCommand *)key
1390fbf6a33SBen Roth{
1400fbf6a33SBen Roth  // NOTE: throttle the key handler because on iOS 9 the handleKeyCommand:
1410fbf6a33SBen Roth  // method gets called repeatedly if the command key is held down.
1420fbf6a33SBen Roth  static NSTimeInterval lastCommand = 0;
1430fbf6a33SBen Roth  if (CACurrentMediaTime() - lastCommand > 0.5) {
1440fbf6a33SBen Roth    for (EXKeyCommand *command in [EXKernelDevKeyCommands sharedInstance].commands) {
1450fbf6a33SBen Roth      if ([command.keyCommand.input isEqualToString:key.input] &&
1460fbf6a33SBen Roth          command.keyCommand.modifierFlags == key.modifierFlags) {
1470fbf6a33SBen Roth        if (command.block) {
1480fbf6a33SBen Roth          command.block(key);
1490fbf6a33SBen Roth          lastCommand = CACurrentMediaTime();
1500fbf6a33SBen Roth        }
1510fbf6a33SBen Roth      }
1520fbf6a33SBen Roth    }
1530fbf6a33SBen Roth  }
1540fbf6a33SBen Roth}
1550fbf6a33SBen Roth
1560fbf6a33SBen Roth@end
1570fbf6a33SBen Roth
1580fbf6a33SBen Roth@implementation EXKernelDevKeyCommands
1590fbf6a33SBen Roth
1600fbf6a33SBen Roth+ (instancetype)sharedInstance
1610fbf6a33SBen Roth{
1620fbf6a33SBen Roth  static EXKernelDevKeyCommands *instance;
1630fbf6a33SBen Roth  static dispatch_once_t once;
1640fbf6a33SBen Roth  dispatch_once(&once, ^{
1650fbf6a33SBen Roth    if (!instance) {
1660fbf6a33SBen Roth      instance = [[EXKernelDevKeyCommands alloc] init];
1670fbf6a33SBen Roth    }
1680fbf6a33SBen Roth  });
1690fbf6a33SBen Roth  return instance;
1700fbf6a33SBen Roth}
1710fbf6a33SBen Roth
1720fbf6a33SBen Roth+ (void)initialize
1730fbf6a33SBen Roth{
1740fbf6a33SBen Roth  // capture keycommands across all bridges.
1750fbf6a33SBen Roth  // this is the same approach taken by RCTKeyCommands,
1760fbf6a33SBen Roth  // but that class is disabled in the expo react native fork
1770fbf6a33SBen Roth  // since there may be many instances of it.
1780fbf6a33SBen Roth  RCTSwapInstanceMethods([UIResponder class],
1790fbf6a33SBen Roth                         @selector(keyCommands),
1800fbf6a33SBen Roth                         @selector(EX_keyCommands));
18105a98924Sandy
18205a98924Sandy#if TARGET_IPHONE_SIMULATOR
18305a98924Sandy  SEL originalKeyboardSelector = NSSelectorFromString(@"handleKeyUIEvent:");
18405a98924Sandy  RCTSwapInstanceMethods([UIApplication class],
18505a98924Sandy                         originalKeyboardSelector,
18605a98924Sandy                         @selector(EX_handleKeyUIEventSwizzle:));
18705a98924Sandy#endif
1880fbf6a33SBen Roth}
1890fbf6a33SBen Roth
19005a98924Sandy#if TARGET_IPHONE_SIMULATOR
19105a98924Sandy+(void)handleKeyboardEvent:(UIEvent *)event
19205a98924Sandy{
19305a98924Sandy  static NSTimeInterval lastCommand = 0;
19405a98924Sandy
19505a98924Sandy  if (event._isKeyDown) {
19605a98924Sandy    if (CACurrentMediaTime() - lastCommand > 0.5) {
19705a98924Sandy      NSString *input = event._modifiedInput;
19805a98924Sandy      if ([input isEqualToString: @"r"]) {
19905a98924Sandy        [[EXKernel sharedInstance] reloadVisibleApp];
20005a98924Sandy      }
20105a98924Sandy
20205a98924Sandy      lastCommand = CACurrentMediaTime();
20305a98924Sandy    }
20405a98924Sandy  }
20505a98924Sandy}
20605a98924Sandy#endif
20705a98924Sandy
2080fbf6a33SBen Roth- (instancetype)init
2090fbf6a33SBen Roth{
2100fbf6a33SBen Roth  if ((self = [super init])) {
2110fbf6a33SBen Roth    _commands = [NSMutableSet set];
2120fbf6a33SBen Roth  }
2130fbf6a33SBen Roth  return self;
2140fbf6a33SBen Roth}
2150fbf6a33SBen Roth
2160fbf6a33SBen Roth#pragma mark - expo dev commands
2170fbf6a33SBen Roth
21824a0cefbSTomasz Sapeta- (void)registerDevCommands
2190fbf6a33SBen Roth{
2200fbf6a33SBen Roth  __weak typeof(self) weakSelf = self;
2210fbf6a33SBen Roth  [self registerKeyCommandWithInput:@"d"
2220fbf6a33SBen Roth                      modifierFlags:UIKeyModifierCommand
2230fbf6a33SBen Roth                             action:^(__unused UIKeyCommand *_) {
2240fbf6a33SBen Roth                               [weakSelf _handleMenuCommand];
2250fbf6a33SBen Roth                             }];
2260fbf6a33SBen Roth  [self registerKeyCommandWithInput:@"r"
2270fbf6a33SBen Roth                      modifierFlags:UIKeyModifierCommand
2280fbf6a33SBen Roth                             action:^(__unused UIKeyCommand *_) {
2290fbf6a33SBen Roth                               [weakSelf _handleRefreshCommand];
2300fbf6a33SBen Roth                             }];
2310fbf6a33SBen Roth  [self registerKeyCommandWithInput:@"n"
2320fbf6a33SBen Roth                      modifierFlags:UIKeyModifierCommand
2330fbf6a33SBen Roth                             action:^(__unused UIKeyCommand *_) {
2340fbf6a33SBen Roth                               [weakSelf _handleDisableDebuggingCommand];
2350fbf6a33SBen Roth                             }];
2360fbf6a33SBen Roth  [self registerKeyCommandWithInput:@"i"
2370fbf6a33SBen Roth                      modifierFlags:UIKeyModifierCommand
2380fbf6a33SBen Roth                             action:^(__unused UIKeyCommand *_) {
2390fbf6a33SBen Roth                               [weakSelf _handleToggleInspectorCommand];
2400fbf6a33SBen Roth                             }];
2410fbf6a33SBen Roth  [self registerKeyCommandWithInput:@"k"
2420fbf6a33SBen Roth                      modifierFlags:UIKeyModifierCommand | UIKeyModifierControl
2430fbf6a33SBen Roth                             action:^(__unused UIKeyCommand *_) {
2440fbf6a33SBen Roth                               [weakSelf _handleKernelMenuCommand];
2450fbf6a33SBen Roth                             }];
24669f88450SEvan Bacon
2470fbf6a33SBen Roth}
2480fbf6a33SBen Roth
2490fbf6a33SBen Roth- (void)_handleMenuCommand
2500fbf6a33SBen Roth{
25124a0cefbSTomasz Sapeta  if ([EXEnvironment sharedEnvironment].isDetached) {
2527f6367d0SBen Roth    [[EXKernel sharedInstance].visibleApp.appManager showDevMenu];
2530fbf6a33SBen Roth  } else {
2547f6367d0SBen Roth    [[EXKernel sharedInstance] switchTasks];
2550fbf6a33SBen Roth  }
2560fbf6a33SBen Roth}
2570fbf6a33SBen Roth
2580fbf6a33SBen Roth- (void)_handleRefreshCommand
2590fbf6a33SBen Roth{
260b333b418SBrent Vatne  // This reloads only JS
261b333b418SBrent Vatne  //  [[EXKernel sharedInstance].visibleApp.appManager reloadBridge];
262b333b418SBrent Vatne
263b333b418SBrent Vatne  // This reloads manifest and JS
264b333b418SBrent Vatne  [[EXKernel sharedInstance] reloadVisibleApp];
2650fbf6a33SBen Roth}
2660fbf6a33SBen Roth
2670fbf6a33SBen Roth- (void)_handleDisableDebuggingCommand
2680fbf6a33SBen Roth{
2697f6367d0SBen Roth  [[EXKernel sharedInstance].visibleApp.appManager disableRemoteDebugging];
2700fbf6a33SBen Roth}
2710fbf6a33SBen Roth
27269f88450SEvan Bacon- (void)_handleToggleRemoteDebuggingCommand
27369f88450SEvan Bacon{
27469f88450SEvan Bacon  [[EXKernel sharedInstance].visibleApp.appManager toggleRemoteDebugging];
27569f88450SEvan Bacon  // This reloads manifest and JS
27669f88450SEvan Bacon  [[EXKernel sharedInstance] reloadVisibleApp];
27769f88450SEvan Bacon}
27869f88450SEvan Bacon
27969f88450SEvan Bacon- (void)_handleTogglePerformanceMonitorCommand
28069f88450SEvan Bacon{
28169f88450SEvan Bacon  [[EXKernel sharedInstance].visibleApp.appManager togglePerformanceMonitor];
28269f88450SEvan Bacon}
28369f88450SEvan Bacon
2840fbf6a33SBen Roth- (void)_handleToggleInspectorCommand
2850fbf6a33SBen Roth{
2867f6367d0SBen Roth  [[EXKernel sharedInstance].visibleApp.appManager toggleElementInspector];
2870fbf6a33SBen Roth}
2880fbf6a33SBen Roth
2890fbf6a33SBen Roth- (void)_handleKernelMenuCommand
2900fbf6a33SBen Roth{
2917f6367d0SBen Roth  if ([EXKernel sharedInstance].visibleApp == [EXKernel sharedInstance].appRegistry.homeAppRecord) {
2927f6367d0SBen Roth    [[EXKernel sharedInstance].appRegistry.homeAppRecord.appManager showDevMenu];
2930fbf6a33SBen Roth  }
2940fbf6a33SBen Roth}
2950fbf6a33SBen Roth
2960fbf6a33SBen Roth#pragma mark - managing list of commands
2970fbf6a33SBen Roth
2980fbf6a33SBen Roth- (void)registerKeyCommandWithInput:(NSString *)input
2990fbf6a33SBen Roth                      modifierFlags:(UIKeyModifierFlags)flags
3000fbf6a33SBen Roth                             action:(void (^)(UIKeyCommand *))block
3010fbf6a33SBen Roth{
3020fbf6a33SBen Roth  RCTAssertMainQueue();
3030fbf6a33SBen Roth
3040fbf6a33SBen Roth  UIKeyCommand *command = [UIKeyCommand keyCommandWithInput:input
3050fbf6a33SBen Roth                                              modifierFlags:flags
3060fbf6a33SBen Roth                                                     action:@selector(EX_handleKeyCommand:)];
3070fbf6a33SBen Roth
3080fbf6a33SBen Roth  EXKeyCommand *keyCommand = [[EXKeyCommand alloc] initWithKeyCommand:command block:block];
3090fbf6a33SBen Roth  [_commands removeObject:keyCommand];
3100fbf6a33SBen Roth  [_commands addObject:keyCommand];
3110fbf6a33SBen Roth}
3120fbf6a33SBen Roth
3130fbf6a33SBen Roth- (void)unregisterKeyCommandWithInput:(NSString *)input
3140fbf6a33SBen Roth                        modifierFlags:(UIKeyModifierFlags)flags
3150fbf6a33SBen Roth{
3160fbf6a33SBen Roth  RCTAssertMainQueue();
3170fbf6a33SBen Roth
3180fbf6a33SBen Roth  for (EXKeyCommand *command in _commands.allObjects) {
3190fbf6a33SBen Roth    if ([command matchesInput:input flags:flags]) {
3200fbf6a33SBen Roth      [_commands removeObject:command];
3210fbf6a33SBen Roth      break;
3220fbf6a33SBen Roth    }
3230fbf6a33SBen Roth  }
3240fbf6a33SBen Roth}
3250fbf6a33SBen Roth
3260fbf6a33SBen Roth- (BOOL)isKeyCommandRegisteredForInput:(NSString *)input
3270fbf6a33SBen Roth                         modifierFlags:(UIKeyModifierFlags)flags
3280fbf6a33SBen Roth{
3290fbf6a33SBen Roth  RCTAssertMainQueue();
3300fbf6a33SBen Roth
3310fbf6a33SBen Roth  for (EXKeyCommand *command in _commands) {
3320fbf6a33SBen Roth    if ([command matchesInput:input flags:flags]) {
3330fbf6a33SBen Roth      return YES;
3340fbf6a33SBen Roth    }
3350fbf6a33SBen Roth  }
3360fbf6a33SBen Roth  return NO;
3370fbf6a33SBen Roth}
3380fbf6a33SBen Roth
3390fbf6a33SBen Roth@end
3400fbf6a33SBen Roth
341