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