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
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  }
132  return self;
133}
134
135#pragma mark - expo dev commands
136
137- (void)registerDevCommands
138{
139  __weak typeof(self) weakSelf = self;
140  [self registerKeyCommandWithInput:@"d"
141                      modifierFlags:UIKeyModifierCommand
142                             action:^(__unused UIKeyCommand *_) {
143                               [weakSelf _handleMenuCommand];
144                             }];
145  [self registerKeyCommandWithInput:@"r"
146                      modifierFlags:UIKeyModifierCommand
147                             action:^(__unused UIKeyCommand *_) {
148                               [weakSelf _handleRefreshCommand];
149                             }];
150  [self registerKeyCommandWithInput:@"n"
151                      modifierFlags:UIKeyModifierCommand
152                             action:^(__unused UIKeyCommand *_) {
153                               [weakSelf _handleDisableDebuggingCommand];
154                             }];
155  [self registerKeyCommandWithInput:@"i"
156                      modifierFlags:UIKeyModifierCommand
157                             action:^(__unused UIKeyCommand *_) {
158                               [weakSelf _handleToggleInspectorCommand];
159                             }];
160  [self registerKeyCommandWithInput:@"k"
161                      modifierFlags:UIKeyModifierCommand | UIKeyModifierControl
162                             action:^(__unused UIKeyCommand *_) {
163                               [weakSelf _handleKernelMenuCommand];
164                             }];
165
166}
167
168- (void)_handleMenuCommand
169{
170  if ([EXEnvironment sharedEnvironment].isDetached) {
171    [[EXKernel sharedInstance].visibleApp.appManager showDevMenu];
172  } else {
173    [[EXKernel sharedInstance] switchTasks];
174  }
175}
176
177- (void)_handleRefreshCommand
178{
179  // This reloads only JS
180  //  [[EXKernel sharedInstance].visibleApp.appManager reloadBridge];
181
182  // This reloads manifest and JS
183  [[EXKernel sharedInstance] reloadVisibleApp];
184}
185
186- (void)_handleDisableDebuggingCommand
187{
188  [[EXKernel sharedInstance].visibleApp.appManager disableRemoteDebugging];
189}
190
191- (void)_handleToggleRemoteDebuggingCommand
192{
193  [[EXKernel sharedInstance].visibleApp.appManager toggleRemoteDebugging];
194  // This reloads manifest and JS
195  [[EXKernel sharedInstance] reloadVisibleApp];
196}
197
198- (void)_handleTogglePerformanceMonitorCommand
199{
200  [[EXKernel sharedInstance].visibleApp.appManager togglePerformanceMonitor];
201}
202
203- (void)_handleToggleInspectorCommand
204{
205  [[EXKernel sharedInstance].visibleApp.appManager toggleElementInspector];
206}
207
208- (void)_handleKernelMenuCommand
209{
210  if ([EXKernel sharedInstance].visibleApp == [EXKernel sharedInstance].appRegistry.homeAppRecord) {
211    [[EXKernel sharedInstance].appRegistry.homeAppRecord.appManager showDevMenu];
212  }
213}
214
215#pragma mark - managing list of commands
216
217- (void)registerKeyCommandWithInput:(NSString *)input
218                      modifierFlags:(UIKeyModifierFlags)flags
219                             action:(void (^)(UIKeyCommand *))block
220{
221  RCTAssertMainQueue();
222
223  UIKeyCommand *command = [UIKeyCommand keyCommandWithInput:input
224                                              modifierFlags:flags
225                                                     action:@selector(EX_handleKeyCommand:)];
226
227  EXKeyCommand *keyCommand = [[EXKeyCommand alloc] initWithKeyCommand:command block:block];
228  [_commands removeObject:keyCommand];
229  [_commands addObject:keyCommand];
230}
231
232- (void)unregisterKeyCommandWithInput:(NSString *)input
233                        modifierFlags:(UIKeyModifierFlags)flags
234{
235  RCTAssertMainQueue();
236
237  for (EXKeyCommand *command in _commands.allObjects) {
238    if ([command matchesInput:input flags:flags]) {
239      [_commands removeObject:command];
240      break;
241    }
242  }
243}
244
245- (BOOL)isKeyCommandRegisteredForInput:(NSString *)input
246                         modifierFlags:(UIKeyModifierFlags)flags
247{
248  RCTAssertMainQueue();
249
250  for (EXKeyCommand *command in _commands) {
251    if ([command matchesInput:input flags:flags]) {
252      return YES;
253    }
254  }
255  return NO;
256}
257
258@end
259
260