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
14NSNotificationName kEXKernelDidChangeMenuBehaviorNotification = @"EXKernelDidChangeMenuBehaviorNotification";
15
16@interface EXKeyCommand : NSObject <NSCopying>
17
18@property (nonatomic, strong) UIKeyCommand *keyCommand;
19@property (nonatomic, copy) void (^block)(UIKeyCommand *);
20
21@end
22
23@implementation EXKeyCommand
24
25- (instancetype)initWithKeyCommand:(UIKeyCommand *)keyCommand
26                             block:(void (^)(UIKeyCommand *))block
27{
28  if ((self = [super init])) {
29    _keyCommand = keyCommand;
30    _block = block;
31  }
32  return self;
33}
34
35RCT_NOT_IMPLEMENTED(- (instancetype)init)
36
37- (id)copyWithZone:(__unused NSZone *)zone
38{
39  return self;
40}
41
42- (NSUInteger)hash
43{
44  return _keyCommand.input.hash ^ _keyCommand.modifierFlags;
45}
46
47- (BOOL)isEqual:(EXKeyCommand *)object
48{
49  if (![object isKindOfClass:[EXKeyCommand class]]) {
50    return NO;
51  }
52  return [self matchesInput:object.keyCommand.input
53                      flags:object.keyCommand.modifierFlags];
54}
55
56- (BOOL)matchesInput:(NSString *)input flags:(UIKeyModifierFlags)flags
57{
58  return [_keyCommand.input isEqual:input] && _keyCommand.modifierFlags == flags;
59}
60
61- (NSString *)description
62{
63  return [NSString stringWithFormat:@"<%@:%p input=\"%@\" flags=%zd hasBlock=%@>",
64          [self class], self, _keyCommand.input, _keyCommand.modifierFlags,
65          _block ? @"YES" : @"NO"];
66}
67
68@end
69
70@interface EXKernelDevKeyCommands ()
71
72@property (nonatomic, strong) NSMutableSet<EXKeyCommand *> *commands;
73
74@end
75
76@implementation UIResponder (EXKeyCommands)
77
78- (NSArray<UIKeyCommand *> *)EX_keyCommands
79{
80  NSSet<EXKeyCommand *> *commands = [EXKernelDevKeyCommands sharedInstance].commands;
81  return [[commands valueForKeyPath:@"keyCommand"] allObjects];
82}
83
84- (void)EX_handleKeyCommand:(UIKeyCommand *)key
85{
86  // NOTE: throttle the key handler because on iOS 9 the handleKeyCommand:
87  // method gets called repeatedly if the command key is held down.
88  static NSTimeInterval lastCommand = 0;
89  if (CACurrentMediaTime() - lastCommand > 0.5) {
90    for (EXKeyCommand *command in [EXKernelDevKeyCommands sharedInstance].commands) {
91      if ([command.keyCommand.input isEqualToString:key.input] &&
92          command.keyCommand.modifierFlags == key.modifierFlags) {
93        if (command.block) {
94          command.block(key);
95          lastCommand = CACurrentMediaTime();
96        }
97      }
98    }
99  }
100}
101
102@end
103
104@implementation EXKernelDevKeyCommands
105
106+ (instancetype)sharedInstance
107{
108  static EXKernelDevKeyCommands *instance;
109  static dispatch_once_t once;
110  dispatch_once(&once, ^{
111    if (!instance) {
112      instance = [[EXKernelDevKeyCommands alloc] init];
113    }
114  });
115  return instance;
116}
117
118+ (void)initialize
119{
120  // capture keycommands across all bridges.
121  // this is the same approach taken by RCTKeyCommands,
122  // but that class is disabled in the expo react native fork
123  // since there may be many instances of it.
124  RCTSwapInstanceMethods([UIResponder class],
125                         @selector(keyCommands),
126                         @selector(EX_keyCommands));
127}
128
129- (instancetype)init
130{
131  if ((self = [super init])) {
132    _commands = [NSMutableSet set];
133    _isLegacyMenuBehaviorEnabled = NO;
134    [self _addDevCommands];
135  }
136  return self;
137}
138
139- (void)setIsLegacyMenuBehaviorEnabled:(BOOL)isLegacyMenuBehaviorEnabled
140{
141  _isLegacyMenuBehaviorEnabled = isLegacyMenuBehaviorEnabled;
142  [[NSNotificationCenter defaultCenter] postNotificationName:kEXKernelDidChangeMenuBehaviorNotification object:nil];
143}
144
145- (BOOL)isLegacyMenuButtonAvailable
146{
147    BOOL isSimulator = NO;
148#if TARGET_OS_SIMULATOR
149    isSimulator = YES;
150#endif
151  return (
152    isSimulator
153    && _isLegacyMenuBehaviorEnabled
154    && [EXKernel sharedInstance].appRegistry.appEnumerator.allObjects.count > 0
155  );
156}
157
158#pragma mark - expo dev commands
159
160- (void)_addDevCommands
161{
162  __weak typeof(self) weakSelf = self;
163  [self registerKeyCommandWithInput:@"d"
164                      modifierFlags:UIKeyModifierCommand
165                             action:^(__unused UIKeyCommand *_) {
166                               [weakSelf _handleMenuCommand];
167                             }];
168  [self registerKeyCommandWithInput:@"r"
169                      modifierFlags:UIKeyModifierCommand
170                             action:^(__unused UIKeyCommand *_) {
171                               [weakSelf _handleRefreshCommand];
172                             }];
173  [self registerKeyCommandWithInput:@"n"
174                      modifierFlags:UIKeyModifierCommand
175                             action:^(__unused UIKeyCommand *_) {
176                               [weakSelf _handleDisableDebuggingCommand];
177                             }];
178  [self registerKeyCommandWithInput:@"i"
179                      modifierFlags:UIKeyModifierCommand
180                             action:^(__unused UIKeyCommand *_) {
181                               [weakSelf _handleToggleInspectorCommand];
182                             }];
183  [self registerKeyCommandWithInput:@"k"
184                      modifierFlags:UIKeyModifierCommand | UIKeyModifierControl
185                             action:^(__unused UIKeyCommand *_) {
186                               [weakSelf _handleKernelMenuCommand];
187                             }];
188}
189
190- (void)_handleMenuCommand
191{
192  if ([EXEnvironment sharedEnvironment].isDetached || _isLegacyMenuBehaviorEnabled) {
193    [[EXKernel sharedInstance].visibleApp.appManager showDevMenu];
194  } else {
195    [[EXKernel sharedInstance] switchTasks];
196  }
197}
198
199- (void)_handleRefreshCommand
200{
201  // This reloads only JS
202  //  [[EXKernel sharedInstance].visibleApp.appManager reloadBridge];
203
204  // This reloads manifest and JS
205  [[EXKernel sharedInstance] reloadVisibleApp];
206}
207
208- (void)_handleDisableDebuggingCommand
209{
210  [[EXKernel sharedInstance].visibleApp.appManager disableRemoteDebugging];
211}
212
213- (void)_handleToggleInspectorCommand
214{
215  [[EXKernel sharedInstance].visibleApp.appManager toggleElementInspector];
216}
217
218- (void)_handleKernelMenuCommand
219{
220  if ([EXKernel sharedInstance].visibleApp == [EXKernel sharedInstance].appRegistry.homeAppRecord) {
221    [[EXKernel sharedInstance].appRegistry.homeAppRecord.appManager showDevMenu];
222  }
223}
224
225#pragma mark - managing list of commands
226
227- (void)registerKeyCommandWithInput:(NSString *)input
228                      modifierFlags:(UIKeyModifierFlags)flags
229                             action:(void (^)(UIKeyCommand *))block
230{
231  RCTAssertMainQueue();
232
233  UIKeyCommand *command = [UIKeyCommand keyCommandWithInput:input
234                                              modifierFlags:flags
235                                                     action:@selector(EX_handleKeyCommand:)];
236
237  EXKeyCommand *keyCommand = [[EXKeyCommand alloc] initWithKeyCommand:command block:block];
238  [_commands removeObject:keyCommand];
239  [_commands addObject:keyCommand];
240}
241
242- (void)unregisterKeyCommandWithInput:(NSString *)input
243                        modifierFlags:(UIKeyModifierFlags)flags
244{
245  RCTAssertMainQueue();
246
247  for (EXKeyCommand *command in _commands.allObjects) {
248    if ([command matchesInput:input flags:flags]) {
249      [_commands removeObject:command];
250      break;
251    }
252  }
253}
254
255- (BOOL)isKeyCommandRegisteredForInput:(NSString *)input
256                         modifierFlags:(UIKeyModifierFlags)flags
257{
258  RCTAssertMainQueue();
259
260  for (EXKeyCommand *command in _commands) {
261    if ([command matchesInput:input flags:flags]) {
262      return YES;
263    }
264  }
265  return NO;
266}
267
268@end
269
270