1// Copyright 2015-present 650 Industries. All rights reserved.
2
3#import "EXFrameReactAppManager.h"
4#import "EXKernelDevKeyCommands.h"
5#import "EXKernel.h"
6#import "EXKernelBridgeRegistry.h"
7#import "EXKernelReactAppManager.h"
8#import "EXShellManager.h"
9
10#import <React/RCTDefines.h>
11#import <React/RCTUtils.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    _isLegacyMenuBehaviorEnabled = NO;
133    [self _addDevCommands];
134  }
135  return self;
136}
137
138#pragma mark - expo dev commands
139
140- (void)_addDevCommands
141{
142  __weak typeof(self) weakSelf = self;
143  [self registerKeyCommandWithInput:@"d"
144                      modifierFlags:UIKeyModifierCommand
145                             action:^(__unused UIKeyCommand *_) {
146                               [weakSelf _handleMenuCommand];
147                             }];
148  [self registerKeyCommandWithInput:@"r"
149                      modifierFlags:UIKeyModifierCommand
150                             action:^(__unused UIKeyCommand *_) {
151                               [weakSelf _handleRefreshCommand];
152                             }];
153  [self registerKeyCommandWithInput:@"n"
154                      modifierFlags:UIKeyModifierCommand
155                             action:^(__unused UIKeyCommand *_) {
156                               [weakSelf _handleDisableDebuggingCommand];
157                             }];
158  [self registerKeyCommandWithInput:@"i"
159                      modifierFlags:UIKeyModifierCommand
160                             action:^(__unused UIKeyCommand *_) {
161                               [weakSelf _handleToggleInspectorCommand];
162                             }];
163  [self registerKeyCommandWithInput:@"k"
164                      modifierFlags:UIKeyModifierCommand | UIKeyModifierControl
165                             action:^(__unused UIKeyCommand *_) {
166                               [weakSelf _handleKernelMenuCommand];
167                             }];
168}
169
170- (void)_handleMenuCommand
171{
172  if ([EXShellManager sharedInstance].isDetached || _isLegacyMenuBehaviorEnabled) {
173    [[EXKernel sharedInstance].bridgeRegistry.lastKnownForegroundAppManager showDevMenu];
174  } else {
175    [[EXKernel sharedInstance] dispatchKernelJSEvent:@"switchTasks" body:@{} onSuccess:nil onFailure:nil];
176  }
177}
178
179- (void)_handleRefreshCommand
180{
181  [[EXKernel sharedInstance].bridgeRegistry.lastKnownForegroundAppManager reloadBridge];
182}
183
184- (void)_handleDisableDebuggingCommand
185{
186  [[EXKernel sharedInstance].bridgeRegistry.lastKnownForegroundAppManager disableRemoteDebugging];
187}
188
189- (void)_handleToggleInspectorCommand
190{
191  [[EXKernel sharedInstance].bridgeRegistry.lastKnownForegroundAppManager toggleElementInspector];
192}
193
194- (void)_handleKernelMenuCommand
195{
196  EXReactAppManager *foregroundAppManager = [EXKernel sharedInstance].bridgeRegistry.lastKnownForegroundAppManager;
197  if (foregroundAppManager == [EXKernel sharedInstance].bridgeRegistry.kernelAppManager) {
198    [foregroundAppManager showDevMenu];
199  }
200}
201
202#pragma mark - managing list of commands
203
204- (void)registerKeyCommandWithInput:(NSString *)input
205                      modifierFlags:(UIKeyModifierFlags)flags
206                             action:(void (^)(UIKeyCommand *))block
207{
208  RCTAssertMainQueue();
209
210  UIKeyCommand *command = [UIKeyCommand keyCommandWithInput:input
211                                              modifierFlags:flags
212                                                     action:@selector(EX_handleKeyCommand:)];
213
214  EXKeyCommand *keyCommand = [[EXKeyCommand alloc] initWithKeyCommand:command block:block];
215  [_commands removeObject:keyCommand];
216  [_commands addObject:keyCommand];
217}
218
219- (void)unregisterKeyCommandWithInput:(NSString *)input
220                        modifierFlags:(UIKeyModifierFlags)flags
221{
222  RCTAssertMainQueue();
223
224  for (EXKeyCommand *command in _commands.allObjects) {
225    if ([command matchesInput:input flags:flags]) {
226      [_commands removeObject:command];
227      break;
228    }
229  }
230}
231
232- (BOOL)isKeyCommandRegisteredForInput:(NSString *)input
233                         modifierFlags:(UIKeyModifierFlags)flags
234{
235  RCTAssertMainQueue();
236
237  for (EXKeyCommand *command in _commands) {
238    if ([command matchesInput:input flags:flags]) {
239      return YES;
240    }
241  }
242  return NO;
243}
244
245@end
246
247