1/* RetroArch - A frontend for libretro.
2 *  Copyright (C) 2011-2016 - Daniel De Matteis
3 *
4 * RetroArch is free software: you can redistribute it and/or modify it under the terms
5 * of the GNU General Public License as published by the Free Software Found-
6 * ation, either version 3 of the License, or (at your option) any later version.
7 *
8 * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
9 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
10 * PURPOSE. See the GNU General Public License for more details.
11 *
12 * You should have received a copy of the GNU General Public License along with RetroArch.
13 * If not, see <http://www.gnu.org/licenses/>.
14 */
15
16#include <stdint.h>
17#include <stddef.h>
18#include <stdlib.h>
19#include <string.h>
20
21#include <boolean.h>
22
23#include <file/file_path.h>
24#include <queues/task_queue.h>
25#include <string/stdstring.h>
26#include <retro_timers.h>
27
28#include "cocoa/cocoa_common.h"
29#include "cocoa/apple_platform.h"
30#include "../ui_companion_driver.h"
31#include "../../configuration.h"
32#include "../../frontend/frontend.h"
33#include "../../input/drivers/cocoa_input.h"
34#include "../../input/drivers_keyboard/keyboard_event_apple.h"
35#include "../../retroarch.h"
36
37#ifdef HAVE_MENU
38#include "../../menu/menu_setting.h"
39#endif
40
41#import <AVFoundation/AVFoundation.h>
42
43#if defined(HAVE_COCOA_METAL) || defined(HAVE_COCOATOUCH)
44id<ApplePlatform> apple_platform;
45#else
46static id apple_platform;
47#endif
48static CFRunLoopObserverRef iterate_observer;
49
50/* Forward declaration */
51static void apple_rarch_exited(void);
52
53static void rarch_enable_ui(void)
54{
55   bool boolean = true;
56
57   ui_companion_set_foreground(true);
58
59   rarch_ctl(RARCH_CTL_SET_PAUSED, &boolean);
60   rarch_ctl(RARCH_CTL_SET_IDLE,   &boolean);
61   retroarch_menu_running();
62}
63
64static void rarch_disable_ui(void)
65{
66   bool boolean = false;
67
68   ui_companion_set_foreground(false);
69
70   rarch_ctl(RARCH_CTL_SET_PAUSED, &boolean);
71   rarch_ctl(RARCH_CTL_SET_IDLE,   &boolean);
72   retroarch_menu_running_finished(false);
73}
74
75static void ui_companion_cocoatouch_event_command(
76      void *data, enum event_command cmd) { }
77
78static void rarch_draw_observer(CFRunLoopObserverRef observer,
79    CFRunLoopActivity activity, void *info)
80{
81   int          ret   = runloop_iterate();
82
83   task_queue_check();
84
85   if (ret == -1)
86   {
87      ui_companion_cocoatouch_event_command(
88            NULL, CMD_EVENT_MENU_SAVE_CURRENT_CONFIG);
89      main_exit(NULL);
90      return;
91   }
92
93   if (rarch_ctl(RARCH_CTL_IS_IDLE, NULL))
94      return;
95   CFRunLoopWakeUp(CFRunLoopGetMain());
96}
97
98apple_frontend_settings_t apple_frontend_settings;
99
100void get_ios_version(int *major, int *minor)
101{
102    NSArray *decomposed_os_version = [[UIDevice currentDevice].systemVersion componentsSeparatedByString:@"."];
103
104    if (major && decomposed_os_version.count > 0)
105        *major = (int)[decomposed_os_version[0] integerValue];
106    if (minor && decomposed_os_version.count > 1)
107        *minor = (int)[decomposed_os_version[1] integerValue];
108}
109
110/* Input helpers: This is kept here because it needs ObjC */
111static void handle_touch_event(NSArray* touches)
112{
113   unsigned i;
114   cocoa_input_data_t *apple = (cocoa_input_data_t*)input_driver_get_data();
115   float scale               = cocoa_screen_get_native_scale();
116
117   if (!apple)
118      return;
119
120   apple->touch_count = 0;
121
122   for (i = 0; i < touches.count && (apple->touch_count < MAX_TOUCHES); i++)
123   {
124      UITouch      *touch = [touches objectAtIndex:i];
125      CGPoint       coord = [touch locationInView:[touch view]];
126      if (touch.phase != UITouchPhaseEnded && touch.phase != UITouchPhaseCancelled)
127      {
128         apple->touches[apple->touch_count   ].screen_x = coord.x * scale;
129         apple->touches[apple->touch_count ++].screen_y = coord.y * scale;
130      }
131   }
132}
133
134#ifndef HAVE_APPLE_STORE
135/* iOS7 Keyboard support */
136@interface UIEvent(iOS7Keyboard)
137@property(readonly, nonatomic) long long _keyCode;
138@property(readonly, nonatomic) _Bool _isKeyDown;
139@property(retain, nonatomic) NSString *_privateInput;
140@property(nonatomic) long long _modifierFlags;
141- (struct __IOHIDEvent { }*)_hidEvent;
142@end
143
144@interface UIApplication(iOS7Keyboard)
145- (void)handleKeyUIEvent:(UIEvent*)event;
146- (id)_keyCommandForEvent:(UIEvent*)event;
147@end
148#endif
149
150@interface RApplication : UIApplication
151@end
152
153@implementation RApplication
154
155#ifndef HAVE_APPLE_STORE
156/* Keyboard handler for iOS 7. */
157
158/* This is copied here as it isn't
159 * defined in any standard iOS header */
160enum
161{
162   NSAlphaShiftKeyMask = 1 << 16,
163   NSShiftKeyMask      = 1 << 17,
164   NSControlKeyMask    = 1 << 18,
165   NSAlternateKeyMask  = 1 << 19,
166   NSCommandKeyMask    = 1 << 20,
167   NSNumericPadKeyMask = 1 << 21,
168   NSHelpKeyMask       = 1 << 22,
169   NSFunctionKeyMask   = 1 << 23,
170   NSDeviceIndependentModifierFlagsMask = 0xffff0000U
171};
172
173/* This is specifically for iOS 9, according to the private headers */
174-(void)handleKeyUIEvent:(UIEvent *)event
175{
176    /* This gets called twice with the same timestamp
177     * for each keypress, that's fine for polling
178     * but is bad for business with events. */
179    static double last_time_stamp;
180
181    if (last_time_stamp == event.timestamp)
182       return [super handleKeyUIEvent:event];
183
184    last_time_stamp = event.timestamp;
185
186    /* If the _hidEvent is null, [event _keyCode] will crash.
187     * (This happens with the on screen keyboard). */
188    if (event._hidEvent)
189    {
190        NSString       *ch = (NSString*)event._privateInput;
191        uint32_t character = 0;
192        uint32_t mod       = 0;
193        NSUInteger mods    = event._modifierFlags;
194
195        if (mods & NSAlphaShiftKeyMask)
196           mod |= RETROKMOD_CAPSLOCK;
197        if (mods & NSShiftKeyMask)
198           mod |= RETROKMOD_SHIFT;
199        if (mods & NSControlKeyMask)
200           mod |= RETROKMOD_CTRL;
201        if (mods & NSAlternateKeyMask)
202           mod |= RETROKMOD_ALT;
203        if (mods & NSCommandKeyMask)
204           mod |= RETROKMOD_META;
205        if (mods & NSNumericPadKeyMask)
206           mod |= RETROKMOD_NUMLOCK;
207
208        if (ch && ch.length != 0)
209        {
210            unsigned i;
211            character = [ch characterAtIndex:0];
212
213            apple_input_keyboard_event(event._isKeyDown,
214                  (uint32_t)event._keyCode, 0, mod,
215                  RETRO_DEVICE_KEYBOARD);
216
217            for (i = 1; i < ch.length; i++)
218                apple_input_keyboard_event(event._isKeyDown,
219                      0, [ch characterAtIndex:i], mod,
220                      RETRO_DEVICE_KEYBOARD);
221        }
222
223        apple_input_keyboard_event(event._isKeyDown,
224              (uint32_t)event._keyCode, character, mod,
225              RETRO_DEVICE_KEYBOARD);
226    }
227
228    [super handleKeyUIEvent:event];
229}
230
231/* This is for iOS versions < 9.0 */
232- (id)_keyCommandForEvent:(UIEvent*)event
233{
234   /* This gets called twice with the same timestamp
235    * for each keypress, that's fine for polling
236    * but is bad for business with events. */
237   static double last_time_stamp;
238
239   if (last_time_stamp == event.timestamp)
240      return [super _keyCommandForEvent:event];
241   last_time_stamp = event.timestamp;
242
243   /* If the _hidEvent is null, [event _keyCode] will crash.
244    * (This happens with the on screen keyboard). */
245   if (event._hidEvent)
246   {
247      NSString       *ch = (NSString*)event._privateInput;
248      uint32_t character = 0;
249      uint32_t mod       = 0;
250      NSUInteger mods    = event._modifierFlags;
251
252      if (mods & NSAlphaShiftKeyMask)
253         mod |= RETROKMOD_CAPSLOCK;
254      if (mods & NSShiftKeyMask)
255         mod |= RETROKMOD_SHIFT;
256      if (mods & NSControlKeyMask)
257         mod |= RETROKMOD_CTRL;
258      if (mods & NSAlternateKeyMask)
259         mod |= RETROKMOD_ALT;
260      if (mods & NSCommandKeyMask)
261         mod |= RETROKMOD_META;
262      if (mods & NSNumericPadKeyMask)
263         mod |= RETROKMOD_NUMLOCK;
264
265      if (ch && ch.length != 0)
266      {
267         unsigned i;
268         character = [ch characterAtIndex:0];
269
270         apple_input_keyboard_event(event._isKeyDown,
271               (uint32_t)event._keyCode, 0, mod,
272               RETRO_DEVICE_KEYBOARD);
273
274         for (i = 1; i < ch.length; i++)
275            apple_input_keyboard_event(event._isKeyDown,
276                  0, [ch characterAtIndex:i], mod,
277                  RETRO_DEVICE_KEYBOARD);
278      }
279
280      apple_input_keyboard_event(event._isKeyDown,
281            (uint32_t)event._keyCode, character, mod,
282            RETRO_DEVICE_KEYBOARD);
283   }
284
285   return [super _keyCommandForEvent:event];
286}
287#endif
288
289#define GSEVENT_TYPE_KEYDOWN 10
290#define GSEVENT_TYPE_KEYUP 11
291
292- (void)sendEvent:(UIEvent *)event
293{
294   [super sendEvent:event];
295
296   if (event.allTouches.count)
297      handle_touch_event(event.allTouches.allObjects);
298
299#if __IPHONE_OS_VERSION_MAX_ALLOWED < 70000
300   {
301      int major, minor;
302      get_ios_version(&major, &minor);
303
304      if ((major < 7) && [event respondsToSelector:@selector(_gsEvent)])
305      {
306         /* Keyboard event hack for iOS versions prior to iOS 7.
307          *
308          * Derived from:
309                  * http://nacho4d-nacho4d.blogspot.com/2012/01/
310                  * catching-keyboard-events-in-ios.html
311                  */
312         const uint8_t *eventMem = objc_unretainedPointer([event performSelector:@selector(_gsEvent)]);
313         int           eventType = eventMem ? *(int*)&eventMem[8] : 0;
314
315         switch (eventType)
316         {
317            case GSEVENT_TYPE_KEYDOWN:
318            case GSEVENT_TYPE_KEYUP:
319               apple_input_keyboard_event(eventType == GSEVENT_TYPE_KEYDOWN,
320                     *(uint16_t*)&eventMem[0x3C], 0, 0, RETRO_DEVICE_KEYBOARD);
321               break;
322         }
323      }
324   }
325#endif
326}
327
328@end
329
330@implementation RetroArch_iOS
331
332#pragma mark - ApplePlatform
333-(id)renderView { return _renderView; }
334-(bool)hasFocus { return YES; }
335
336- (void)setViewType:(apple_view_type_t)vt
337{
338   if (vt == _vt)
339      return;
340
341   _vt = vt;
342   if (_renderView != nil)
343   {
344      [_renderView removeFromSuperview];
345      _renderView = nil;
346   }
347
348   switch (vt)
349   {
350#ifdef HAVE_COCOA_METAL
351      case APPLE_VIEW_TYPE_VULKAN:
352       case APPLE_VIEW_TYPE_METAL:
353         {
354            MetalView *v = [MetalView new];
355            v.paused = YES;
356            v.enableSetNeedsDisplay = NO;
357#if TARGET_OS_IOS
358            v.multipleTouchEnabled = YES;
359#endif
360            _renderView = v;
361         }
362         break;
363#endif
364       case APPLE_VIEW_TYPE_OPENGL_ES:
365         _renderView = (BRIDGE GLKView*)glkitview_init();
366         break;
367
368       case APPLE_VIEW_TYPE_NONE:
369                         default:
370         return;
371   }
372
373   _renderView.translatesAutoresizingMaskIntoConstraints = NO;
374   UIView *rootView = [CocoaView get].view;
375   [rootView addSubview:_renderView];
376   [[_renderView.topAnchor constraintEqualToAnchor:rootView.topAnchor] setActive:YES];
377   [[_renderView.bottomAnchor constraintEqualToAnchor:rootView.bottomAnchor] setActive:YES];
378   [[_renderView.leadingAnchor constraintEqualToAnchor:rootView.leadingAnchor] setActive:YES];
379   [[_renderView.trailingAnchor constraintEqualToAnchor:rootView.trailingAnchor] setActive:YES];
380}
381
382- (apple_view_type_t)viewType { return _vt; }
383
384- (void)setVideoMode:(gfx_ctx_mode_t)mode
385{
386#ifdef HAVE_COCOA_METAL
387   MetalView *metalView = (MetalView*) _renderView;
388   CGFloat scale = [[UIScreen mainScreen] scale];
389   [metalView setDrawableSize:CGSizeMake(
390         _renderView.bounds.size.width * scale,
391         _renderView.bounds.size.height * scale
392         )];
393#endif
394}
395
396- (void)setCursorVisible:(bool)v { /* no-op for iOS */ }
397- (bool)setDisableDisplaySleep:(bool)disable { /* no-op for iOS */ return NO; }
398+ (RetroArch_iOS*)get { return (RetroArch_iOS*)[[UIApplication sharedApplication] delegate]; }
399
400-(NSString*)documentsDirectory
401{
402   if (_documentsDirectory == nil)
403   {
404#if TARGET_OS_IOS
405      NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
406#elif TARGET_OS_TV
407      NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
408#endif
409
410      _documentsDirectory = paths.firstObject;
411   }
412   return _documentsDirectory;
413}
414
415- (void)applicationDidFinishLaunching:(UIApplication *)application
416{
417   NSError *error;
418   char arguments[]   = "retroarch";
419   char       *argv[] = {arguments,   NULL};
420   int argc           = 1;
421   apple_platform     = self;
422
423   [self setDelegate:self];
424
425   /* Setup window */
426   self.window        = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
427   [self.window makeKeyAndVisible];
428
429   [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient error:&error];
430
431   [self refreshSystemConfig];
432   [self showGameView];
433
434   if (rarch_main(argc, argv, NULL))
435      apple_rarch_exited();
436
437   iterate_observer = CFRunLoopObserverCreate(0, kCFRunLoopBeforeWaiting,
438         true, 0, rarch_draw_observer, 0);
439   CFRunLoopAddObserver(CFRunLoopGetMain(), iterate_observer, kCFRunLoopCommonModes);
440
441#ifdef HAVE_MFI
442   extern void *apple_gamecontroller_joypad_init(void *data);
443   apple_gamecontroller_joypad_init(NULL);
444#endif
445}
446
447- (void)applicationDidEnterBackground:(UIApplication *)application { }
448
449- (void)applicationWillTerminate:(UIApplication *)application
450{
451   CFRunLoopObserverInvalidate(iterate_observer);
452   CFRelease(iterate_observer);
453   iterate_observer = NULL;
454}
455
456- (void)applicationDidBecomeActive:(UIApplication *)application
457{
458   settings_t *settings            = config_get_ptr();
459   bool ui_companion_start_on_boot = settings->bools.ui_companion_start_on_boot;
460
461   if (!ui_companion_start_on_boot)
462      [self showGameView];
463}
464
465-(BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
466   NSFileManager *manager = [NSFileManager defaultManager];
467   NSString     *filename = (NSString*)url.path.lastPathComponent;
468   NSError         *error = nil;
469   NSString  *destination = [self.documentsDirectory stringByAppendingPathComponent:filename];
470
471   // copy file to documents directory if its not already inside of documents directory
472   if ([url startAccessingSecurityScopedResource]) {
473      if (![[url path] containsString: self.documentsDirectory])
474         if (![manager fileExistsAtPath:destination])
475            if (![manager copyItemAtPath:[url path] toPath:destination error:&error])
476               printf("%s\n", [[error description] UTF8String]);
477      [url stopAccessingSecurityScopedResource];
478   }
479   return true;
480}
481
482- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated
483{
484#if TARGET_OS_IOS
485   [self setToolbarHidden:![[viewController toolbarItems] count] animated:YES];
486#endif
487   [self refreshSystemConfig];
488}
489
490- (void)showGameView
491{
492   [self popToRootViewControllerAnimated:NO];
493
494#if TARGET_OS_IOS
495   [self setToolbarHidden:true animated:NO];
496   [[UIApplication sharedApplication] setStatusBarHidden:true withAnimation:UIStatusBarAnimationNone];
497#endif
498
499   [[UIApplication sharedApplication] setIdleTimerDisabled:true];
500   [self.window setRootViewController:[CocoaView get]];
501
502   dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
503         command_event(CMD_EVENT_AUDIO_START, NULL);
504         });
505   rarch_disable_ui();
506}
507
508- (IBAction)showPauseMenu:(id)sender
509{
510   rarch_enable_ui();
511
512#if TARGET_OS_IOS
513   [[UIApplication sharedApplication] setStatusBarHidden:false withAnimation:UIStatusBarAnimationNone];
514#endif
515
516   [[UIApplication sharedApplication] setIdleTimerDisabled:false];
517   [self.window setRootViewController:self];
518}
519
520- (void)refreshSystemConfig
521{
522#if TARGET_OS_IOS
523   /* Get enabled orientations */
524   apple_frontend_settings.orientation_flags = UIInterfaceOrientationMaskAll;
525
526   if (string_is_equal(apple_frontend_settings.orientations, "landscape"))
527      apple_frontend_settings.orientation_flags =
528           UIInterfaceOrientationMaskLandscape;
529   else if (string_is_equal(apple_frontend_settings.orientations, "portrait"))
530      apple_frontend_settings.orientation_flags =
531           UIInterfaceOrientationMaskPortrait
532         | UIInterfaceOrientationMaskPortraitUpsideDown;
533#endif
534}
535
536- (void)supportOtherAudioSessions { }
537@end
538
539int main(int argc, char *argv[])
540{
541   @autoreleasepool {
542      return UIApplicationMain(argc, argv, NSStringFromClass([RApplication class]), NSStringFromClass([RetroArch_iOS class]));
543   }
544}
545
546static void apple_rarch_exited(void)
547{
548   RetroArch_iOS *ap = (RetroArch_iOS *)apple_platform;
549
550   if (!ap)
551      return;
552   [ap showPauseMenu:ap];
553}
554