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