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
9#import "RCTDefines.h"
10#import "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
72@end
73
74@implementation UIResponder (EXKeyCommands)
75
76- (NSArray<UIKeyCommand *> *)EX_keyCommands
77{
78  NSSet<EXKeyCommand *> *commands = [EXKernelDevKeyCommands sharedInstance].commands;
79  return [[commands valueForKeyPath:@"keyCommand"] allObjects];
80}
81
82- (void)EX_handleKeyCommand:(UIKeyCommand *)key
83{
84  // NOTE: throttle the key handler because on iOS 9 the handleKeyCommand:
85  // method gets called repeatedly if the command key is held down.
86  static NSTimeInterval lastCommand = 0;
87  if (CACurrentMediaTime() - lastCommand > 0.5) {
88    for (EXKeyCommand *command in [EXKernelDevKeyCommands sharedInstance].commands) {
89      if ([command.keyCommand.input isEqualToString:key.input] &&
90          command.keyCommand.modifierFlags == key.modifierFlags) {
91        if (command.block) {
92          command.block(key);
93          lastCommand = CACurrentMediaTime();
94        }
95      }
96    }
97  }
98}
99
100@end
101
102@implementation EXKernelDevKeyCommands
103
104+ (instancetype)sharedInstance
105{
106  static EXKernelDevKeyCommands *instance;
107  static dispatch_once_t once;
108  dispatch_once(&once, ^{
109    if (!instance) {
110      instance = [[EXKernelDevKeyCommands alloc] init];
111    }
112  });
113  return instance;
114}
115
116+ (void)initialize
117{
118  // capture keycommands across all bridges.
119  // this is the same approach taken by RCTKeyCommands,
120  // but that class is disabled in the expo react native fork
121  // since there may be many instances of it.
122  RCTSwapInstanceMethods([UIResponder class],
123                         @selector(keyCommands),
124                         @selector(EX_keyCommands));
125}
126
127- (instancetype)init
128{
129  if ((self = [super init])) {
130    _commands = [NSMutableSet set];
131    _isLegacyMenuBehaviorEnabled = NO;
132    [self _addDevCommands];
133  }
134  return self;
135}
136
137#pragma mark - expo dev commands
138
139- (void)_addDevCommands
140{
141  __weak typeof(self) weakSelf = self;
142  [self registerKeyCommandWithInput:@"d"
143                      modifierFlags:UIKeyModifierCommand
144                             action:^(__unused UIKeyCommand *_) {
145                               [weakSelf _handleMenuCommand];
146                             }];
147  [self registerKeyCommandWithInput:@"r"
148                      modifierFlags:UIKeyModifierCommand
149                             action:^(__unused UIKeyCommand *_) {
150                               [weakSelf _handleRefreshCommand];
151                             }];
152  [self registerKeyCommandWithInput:@"n"
153                      modifierFlags:UIKeyModifierCommand
154                             action:^(__unused UIKeyCommand *_) {
155                               [weakSelf _handleDisableDebuggingCommand];
156                             }];
157  [self registerKeyCommandWithInput:@"i"
158                      modifierFlags:UIKeyModifierCommand
159                             action:^(__unused UIKeyCommand *_) {
160                               [weakSelf _handleToggleInspectorCommand];
161                             }];
162  [self registerKeyCommandWithInput:@"k"
163                      modifierFlags:UIKeyModifierCommand | UIKeyModifierControl
164                             action:^(__unused UIKeyCommand *_) {
165                               [weakSelf _handleKernelMenuCommand];
166                             }];
167}
168
169- (void)_handleMenuCommand
170{
171  BOOL detached = NO;
172#ifdef EX_DETACHED
173  detached = YES;
174#endif
175  if (detached || _isLegacyMenuBehaviorEnabled) {
176    [[EXKernel sharedInstance].bridgeRegistry.lastKnownForegroundAppManager showDevMenu];
177  } else {
178    [[EXKernel sharedInstance] dispatchKernelJSEvent:@"switchTasks" body:@{} onSuccess:nil onFailure:nil];
179  }
180}
181
182- (void)_handleRefreshCommand
183{
184  [[EXKernel sharedInstance].bridgeRegistry.lastKnownForegroundAppManager reloadBridge];
185}
186
187- (void)_handleDisableDebuggingCommand
188{
189  [[EXKernel sharedInstance].bridgeRegistry.lastKnownForegroundAppManager disableRemoteDebugging];
190}
191
192- (void)_handleToggleInspectorCommand
193{
194  [[EXKernel sharedInstance].bridgeRegistry.lastKnownForegroundAppManager toggleElementInspector];
195}
196
197- (void)_handleKernelMenuCommand
198{
199  EXReactAppManager *foregroundAppManager = [EXKernel sharedInstance].bridgeRegistry.lastKnownForegroundAppManager;
200  if (foregroundAppManager == [EXKernel sharedInstance].bridgeRegistry.kernelAppManager) {
201    [foregroundAppManager showDevMenu];
202  }
203}
204
205#pragma mark - managing list of commands
206
207- (void)registerKeyCommandWithInput:(NSString *)input
208                      modifierFlags:(UIKeyModifierFlags)flags
209                             action:(void (^)(UIKeyCommand *))block
210{
211  RCTAssertMainQueue();
212
213  UIKeyCommand *command = [UIKeyCommand keyCommandWithInput:input
214                                              modifierFlags:flags
215                                                     action:@selector(EX_handleKeyCommand:)];
216
217  EXKeyCommand *keyCommand = [[EXKeyCommand alloc] initWithKeyCommand:command block:block];
218  [_commands removeObject:keyCommand];
219  [_commands addObject:keyCommand];
220}
221
222- (void)unregisterKeyCommandWithInput:(NSString *)input
223                        modifierFlags:(UIKeyModifierFlags)flags
224{
225  RCTAssertMainQueue();
226
227  for (EXKeyCommand *command in _commands.allObjects) {
228    if ([command matchesInput:input flags:flags]) {
229      [_commands removeObject:command];
230      break;
231    }
232  }
233}
234
235- (BOOL)isKeyCommandRegisteredForInput:(NSString *)input
236                         modifierFlags:(UIKeyModifierFlags)flags
237{
238  RCTAssertMainQueue();
239
240  for (EXKeyCommand *command in _commands) {
241    if ([command matchesInput:input flags:flags]) {
242      return YES;
243    }
244  }
245  return NO;
246}
247
248@end
249
250