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  if ([self isKindOfClass:[UITextView class]] || [self isKindOfClass:[UITextField class]]) {
132    return @[];
133  }
134  NSSet<EXKeyCommand *> *commands = [EXKernelDevKeyCommands sharedInstance].commands;
135  return [[commands valueForKeyPath:@"keyCommand"] allObjects];
136}
137
138- (void)EX_handleKeyCommand:(UIKeyCommand *)key
139{
140  // NOTE: throttle the key handler because on iOS 9 the handleKeyCommand:
141  // method gets called repeatedly if the command key is held down.
142  static NSTimeInterval lastCommand = 0;
143  if (CACurrentMediaTime() - lastCommand > 0.5) {
144    for (EXKeyCommand *command in [EXKernelDevKeyCommands sharedInstance].commands) {
145      if ([command.keyCommand.input isEqualToString:key.input] &&
146          command.keyCommand.modifierFlags == key.modifierFlags) {
147        if (command.block) {
148          command.block(key);
149          lastCommand = CACurrentMediaTime();
150        }
151      }
152    }
153  }
154}
155
156@end
157
158@implementation EXKernelDevKeyCommands
159
160+ (instancetype)sharedInstance
161{
162  static EXKernelDevKeyCommands *instance;
163  static dispatch_once_t once;
164  dispatch_once(&once, ^{
165    if (!instance) {
166      instance = [[EXKernelDevKeyCommands alloc] init];
167    }
168  });
169  return instance;
170}
171
172+ (void)initialize
173{
174  // capture keycommands across all bridges.
175  // this is the same approach taken by RCTKeyCommands,
176  // but that class is disabled in the expo react native fork
177  // since there may be many instances of it.
178  RCTSwapInstanceMethods([UIResponder class],
179                         @selector(keyCommands),
180                         @selector(EX_keyCommands));
181
182#if TARGET_IPHONE_SIMULATOR
183  SEL originalKeyboardSelector = NSSelectorFromString(@"handleKeyUIEvent:");
184  RCTSwapInstanceMethods([UIApplication class],
185                         originalKeyboardSelector,
186                         @selector(EX_handleKeyUIEventSwizzle:));
187#endif
188}
189
190#if TARGET_IPHONE_SIMULATOR
191+(void)handleKeyboardEvent:(UIEvent *)event
192{
193  static NSTimeInterval lastCommand = 0;
194
195  if (event._isKeyDown) {
196    if (CACurrentMediaTime() - lastCommand > 0.5) {
197      NSString *input = event._modifiedInput;
198      if ([input isEqualToString: @"r"]) {
199        [[EXKernel sharedInstance] reloadVisibleApp];
200      }
201
202      lastCommand = CACurrentMediaTime();
203    }
204  }
205}
206#endif
207
208- (instancetype)init
209{
210  if ((self = [super init])) {
211    _commands = [NSMutableSet set];
212  }
213  return self;
214}
215
216#pragma mark - expo dev commands
217
218- (void)registerDevCommands
219{
220  __weak typeof(self) weakSelf = self;
221  [self registerKeyCommandWithInput:@"d"
222                      modifierFlags:UIKeyModifierCommand
223                             action:^(__unused UIKeyCommand *_) {
224                               [weakSelf _handleMenuCommand];
225                             }];
226  [self registerKeyCommandWithInput:@"r"
227                      modifierFlags:UIKeyModifierCommand
228                             action:^(__unused UIKeyCommand *_) {
229                               [weakSelf _handleRefreshCommand];
230                             }];
231  [self registerKeyCommandWithInput:@"n"
232                      modifierFlags:UIKeyModifierCommand
233                             action:^(__unused UIKeyCommand *_) {
234                               [weakSelf _handleDisableDebuggingCommand];
235                             }];
236  [self registerKeyCommandWithInput:@"i"
237                      modifierFlags:UIKeyModifierCommand
238                             action:^(__unused UIKeyCommand *_) {
239                               [weakSelf _handleToggleInspectorCommand];
240                             }];
241  [self registerKeyCommandWithInput:@"k"
242                      modifierFlags:UIKeyModifierCommand | UIKeyModifierControl
243                             action:^(__unused UIKeyCommand *_) {
244                               [weakSelf _handleKernelMenuCommand];
245                             }];
246
247}
248
249- (void)_handleMenuCommand
250{
251  if ([EXEnvironment sharedEnvironment].isDetached) {
252    [[EXKernel sharedInstance].visibleApp.appManager showDevMenu];
253  } else {
254    [[EXKernel sharedInstance] switchTasks];
255  }
256}
257
258- (void)_handleRefreshCommand
259{
260  // This reloads only JS
261  //  [[EXKernel sharedInstance].visibleApp.appManager reloadBridge];
262
263  // This reloads manifest and JS
264  [[EXKernel sharedInstance] reloadVisibleApp];
265}
266
267- (void)_handleDisableDebuggingCommand
268{
269  [[EXKernel sharedInstance].visibleApp.appManager disableRemoteDebugging];
270}
271
272- (void)_handleToggleRemoteDebuggingCommand
273{
274  [[EXKernel sharedInstance].visibleApp.appManager toggleRemoteDebugging];
275  // This reloads manifest and JS
276  [[EXKernel sharedInstance] reloadVisibleApp];
277}
278
279- (void)_handleTogglePerformanceMonitorCommand
280{
281  [[EXKernel sharedInstance].visibleApp.appManager togglePerformanceMonitor];
282}
283
284- (void)_handleToggleInspectorCommand
285{
286  [[EXKernel sharedInstance].visibleApp.appManager toggleElementInspector];
287}
288
289- (void)_handleKernelMenuCommand
290{
291  if ([EXKernel sharedInstance].visibleApp == [EXKernel sharedInstance].appRegistry.homeAppRecord) {
292    [[EXKernel sharedInstance].appRegistry.homeAppRecord.appManager showDevMenu];
293  }
294}
295
296#pragma mark - managing list of commands
297
298- (void)registerKeyCommandWithInput:(NSString *)input
299                      modifierFlags:(UIKeyModifierFlags)flags
300                             action:(void (^)(UIKeyCommand *))block
301{
302  RCTAssertMainQueue();
303
304  UIKeyCommand *command = [UIKeyCommand keyCommandWithInput:input
305                                              modifierFlags:flags
306                                                     action:@selector(EX_handleKeyCommand:)];
307
308  EXKeyCommand *keyCommand = [[EXKeyCommand alloc] initWithKeyCommand:command block:block];
309  [_commands removeObject:keyCommand];
310  [_commands addObject:keyCommand];
311}
312
313- (void)unregisterKeyCommandWithInput:(NSString *)input
314                        modifierFlags:(UIKeyModifierFlags)flags
315{
316  RCTAssertMainQueue();
317
318  for (EXKeyCommand *command in _commands.allObjects) {
319    if ([command matchesInput:input flags:flags]) {
320      [_commands removeObject:command];
321      break;
322    }
323  }
324}
325
326- (BOOL)isKeyCommandRegisteredForInput:(NSString *)input
327                         modifierFlags:(UIKeyModifierFlags)flags
328{
329  RCTAssertMainQueue();
330
331  for (EXKeyCommand *command in _commands) {
332    if ([command matchesInput:input flags:flags]) {
333      return YES;
334    }
335  }
336  return NO;
337}
338
339@end
340
341