#import "UnityAppController.h" #import "UnityAppController+ViewHandling.h" #import "UnityAppController+Rendering.h" #import "iPhone_Sensors.h" #import #import #import #import #import #include // MSAA_DEFAULT_SAMPLE_COUNT was removed // ENABLE_INTERNAL_PROFILER and related defines were moved to iPhone_Profiler.h // kFPS define for removed: you can use Application.targetFrameRate (30 fps by default) // DisplayLink is the only run loop mode now - all others were removed #include "CrashReporter.h" #include "UI/OrientationSupport.h" #include "UI/UnityView.h" #include "UI/Keyboard.h" #include "UI/SplashScreen.h" #include "Unity/InternalProfiler.h" #include "Unity/DisplayManager.h" #include "Unity/ObjCRuntime.h" #include "PluginBase/AppDelegateListener.h" #include #include #include #include #include // we assume that app delegate is never changed and we can cache it, instead of re-query UIApplication every time UnityAppController* _UnityAppController = nil; UnityAppController* GetAppController() { return _UnityAppController; } // we keep old bools around to support "old" code that might have used them bool _ios81orNewer = false, _ios82orNewer = false, _ios83orNewer = false, _ios90orNewer = false, _ios91orNewer = false; bool _ios100orNewer = false, _ios101orNewer = false, _ios102orNewer = false, _ios103orNewer = false; bool _ios110orNewer = false, _ios111orNewer = false, _ios112orNewer = false; bool _ios130orNewer = false, _ios140orNewer = false; // was unity rendering already inited: we should not touch rendering while this is false bool _renderingInited = false; // was unity inited: we should not touch unity api while this is false bool _unityAppReady = false; // see if there's a need to do internal player pause/resume handling // // Typically the trampoline code should manage this internally, but // there are use cases, videoplayer, plugin code, etc where the player // is paused before the internal handling comes relevant. Avoid // overriding externally managed player pause/resume handling by // caching the state bool _wasPausedExternal = false; // should we skip present on next draw: used in corner cases (like rotation) to fill both draw-buffers with some content bool _skipPresent = false; // was app "resigned active": some operations do not make sense while app is in background bool _didResignActive = false; // was startUnity scheduled: used to make startup robust in case of locking device static bool _startUnityScheduled = false; #if UNITY_SUPPORT_ROTATION // Required to enable specific orientation for some presentation controllers: see supportedInterfaceOrientationsForWindow below for details NSInteger _forceInterfaceOrientationMask = 0; #endif @implementation UnityAppController @synthesize unityView = _unityView; @synthesize unityDisplayLink = _displayLink; @synthesize rootView = _rootView; @synthesize rootViewController = _rootController; @synthesize mainDisplay = _mainDisplay; @synthesize renderDelegate = _renderDelegate; @synthesize quitHandler = _quitHandler; #if UNITY_SUPPORT_ROTATION @synthesize interfaceOrientation = _curOrientation; #endif - (id)init { if ((self = _UnityAppController = [super init])) { // due to clang issues with generating warning for overriding deprecated methods // we will simply assert if deprecated methods are present // NB: methods table is initied at load (before this call), so it is ok to check for override NSAssert(![self respondsToSelector: @selector(createUnityViewImpl)], @"createUnityViewImpl is deprecated and will not be called. Override createUnityView" ); NSAssert(![self respondsToSelector: @selector(createViewHierarchyImpl)], @"createViewHierarchyImpl is deprecated and will not be called. Override willStartWithViewController" ); NSAssert(![self respondsToSelector: @selector(createViewHierarchy)], @"createViewHierarchy is deprecated and will not be implemented. Use createUI" ); } return self; } - (void)setWindow:(id)object {} - (UIWindow*)window { return _window; } - (void)shouldAttachRenderDelegate {} - (void)preStartUnity {} - (void)startUnity:(UIApplication*)application { NSAssert(_unityAppReady == NO, @"[UnityAppController startUnity:] called after Unity has been initialized"); UnityInitApplicationGraphics(); // we make sure that first level gets correct display list and orientation [[DisplayManager Instance] updateDisplayListCacheInUnity]; UnityLoadApplication(); Profiler_InitProfiler(); [self showGameUI]; [self createDisplayLink]; UnitySetPlayerFocus(1); AVAudioSession* audioSession = [AVAudioSession sharedInstance]; // If Unity audio is disabled, we set the category to ambient to make sure we don't mute other app's audio. We set the audio session // to active so we can get outputVolume callbacks. If Unity audio is enabled, FMOD should have already handled all of this AVAudioSession init. if (!UnityIsAudioManagerAvailableAndEnabled()) { [audioSession setCategory: AVAudioSessionCategoryAmbient error: nil]; [audioSession setActive: YES error: nil]; } [audioSession addObserver: self forKeyPath: @"outputVolume" options: 0 context: nil]; UnityUpdateMuteState([audioSession outputVolume] < 0.01f ? 1 : 0); #if UNITY_REPLAY_KIT_AVAILABLE void InitUnityReplayKit(); // Classes/Unity/UnityReplayKit.mm InitUnityReplayKit(); #endif } extern "C" void UnityDestroyDisplayLink() { [GetAppController() destroyDisplayLink]; } extern "C" void UnityRequestQuit() { _didResignActive = true; if (GetAppController().quitHandler) GetAppController().quitHandler(); else exit(0); } extern void SensorsCleanup(); extern "C" void UnityCleanupTrampoline() { // Unity view and viewController will not necessary be destroyed right after this function execution. // We need to ensure that these objects will not receive any callbacks from system during that time. [_UnityAppController window].rootViewController = nil; [[_UnityAppController unityView] removeFromSuperview]; // Prevent multiple cleanups if (_UnityAppController == nil) return; [KeyboardDelegate Destroy]; SensorsCleanup(); Profiler_UninitProfiler(); [DisplayManager Destroy]; UnityDestroyDisplayLink(); _UnityAppController = nil; } #if UNITY_SUPPORT_ROTATION - (NSUInteger)application:(UIApplication*)application supportedInterfaceOrientationsForWindow:(UIWindow*)window { // No rootViewController is set because we are switching from one view controller to another, all orientations should be enabled if ([window rootViewController] == nil) return UIInterfaceOrientationMaskAll; // During splash screen show phase no forced orientations should be allowed. // This will prevent unwanted rotation while splash screen is on and application is not yet ready to present (Ex. Fogbugz cases: 1190428, 1269547). if (!_unityAppReady) return [_rootController supportedInterfaceOrientations]; // Some presentation controllers (e.g. UIImagePickerController) require portrait orientation and will throw exception if it is not supported. // At the same time enabling all orientations by returning UIInterfaceOrientationMaskAll might cause unwanted orientation change // (e.g. when using UIActivityViewController to "share to" another application, iOS will use supportedInterfaceOrientations to possibly reorient). // So to avoid exception we are returning combination of constraints for root view controller and orientation requested by iOS. // _forceInterfaceOrientationMask is updated in willChangeStatusBarOrientation, which is called if some presentation controller insists on orientation change. return [[window rootViewController] supportedInterfaceOrientations] | _forceInterfaceOrientationMask; } - (void)application:(UIApplication*)application willChangeStatusBarOrientation:(UIInterfaceOrientation)newStatusBarOrientation duration:(NSTimeInterval)duration { // Setting orientation mask which is requested by iOS: see supportedInterfaceOrientationsForWindow above for details _forceInterfaceOrientationMask = 1 << newStatusBarOrientation; } #endif #if !PLATFORM_TVOS #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-implementations" #pragma clang diagnostic ignored "-Wdeprecated-declarations" - (void)application:(UIApplication*)application didReceiveLocalNotification:(UILocalNotification*)notification { AppController_SendNotificationWithArg(kUnityDidReceiveLocalNotification, notification); UnitySendLocalNotification(notification); } #pragma clang diagnostic pop #endif #if UNITY_USES_REMOTE_NOTIFICATIONS - (void)application:(UIApplication*)application didReceiveRemoteNotification:(NSDictionary*)userInfo { AppController_SendNotificationWithArg(kUnityDidReceiveRemoteNotification, userInfo); UnitySendRemoteNotification(userInfo); } - (void)application:(UIApplication*)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken { AppController_SendNotificationWithArg(kUnityDidRegisterForRemoteNotificationsWithDeviceToken, deviceToken); UnitySendDeviceToken(deviceToken); } #if !PLATFORM_TVOS - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))handler { AppController_SendNotificationWithArg(kUnityDidReceiveRemoteNotification, userInfo); UnitySendRemoteNotification(userInfo); if (handler) { handler(UIBackgroundFetchResultNoData); } } #endif - (void)application:(UIApplication*)application didFailToRegisterForRemoteNotificationsWithError:(NSError*)error { AppController_SendNotificationWithArg(kUnityDidFailToRegisterForRemoteNotificationsWithError, error); UnitySendRemoteNotificationError(error); // alas people do not check remote notification error through api (which is clunky, i agree) so log here to have at least some visibility ::printf("\nFailed to register for remote notifications:\n%s\n\n", [[error localizedDescription] UTF8String]); } #endif // UIApplicationOpenURLOptionsKey was added only in ios10 sdk, while we still support ios9 sdk - (BOOL)application:(UIApplication*)app openURL:(NSURL*)url options:(NSDictionary*)options { id sourceApplication = options[UIApplicationOpenURLOptionsSourceApplicationKey], annotation = options[UIApplicationOpenURLOptionsAnnotationKey]; NSMutableDictionary* notifData = [NSMutableDictionary dictionaryWithCapacity: 3]; if (url) { notifData[@"url"] = url; UnitySetAbsoluteURL(url.absoluteString.UTF8String); } if (sourceApplication) notifData[@"sourceApplication"] = sourceApplication; if (annotation) notifData[@"annotation"] = annotation; AppController_SendNotificationWithArg(kUnityOnOpenURL, notifData); return YES; } - (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity #if defined(__IPHONE_12_0) || defined(__TVOS_12_0) restorationHandler:(void (^)(NSArray > * _Nullable restorableObjects))restorationHandler #else restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler #endif { NSURL* url = userActivity.webpageURL; if (url) UnitySetAbsoluteURL(url.absoluteString.UTF8String); return YES; } - (BOOL)application:(UIApplication*)application willFinishLaunchingWithOptions:(NSDictionary*)launchOptions { AppController_SendNotificationWithArg(kUnityWillFinishLaunchingWithOptions, launchOptions); return YES; } #if (PLATFORM_IOS && defined(__IPHONE_13_0)) || (PLATFORM_TVOS && defined(__TVOS_13_0)) - (UIWindowScene*)pickStartupWindowScene:(NSSet*)scenes API_AVAILABLE(ios(13.0), tvos(13.0)) { // if we have scene with UISceneActivationStateForegroundActive - pick it // otherwise UISceneActivationStateForegroundInactive will work // it will be the scene going into active state // if there were no active/inactive scenes (only background) we should allow background scene // this might happen in some cases with native plugins doing "things" UIWindowScene *foregroundScene = nil, *backgroundScene = nil; for (UIScene* scene in scenes) { if (![scene isKindOfClass: [UIWindowScene class]]) continue; UIWindowScene* windowScene = (UIWindowScene*)scene; if (scene.activationState == UISceneActivationStateForegroundActive) return windowScene; if (scene.activationState == UISceneActivationStateForegroundInactive) foregroundScene = windowScene; else if (scene.activationState == UISceneActivationStateBackground) backgroundScene = windowScene; } return foregroundScene ? foregroundScene : backgroundScene; } #endif - (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions { ::printf("-> applicationDidFinishLaunching()\n"); // send notfications #if !PLATFORM_TVOS #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" if (UILocalNotification* notification = [launchOptions objectForKey: UIApplicationLaunchOptionsLocalNotificationKey]) UnitySendLocalNotification(notification); if ([UIDevice currentDevice].generatesDeviceOrientationNotifications == NO) [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; #pragma clang diagnostic pop #endif UnityInitApplicationNoGraphics(UnityDataBundleDir()); [self selectRenderingAPI]; [UnityRenderingView InitializeForAPI: self.renderingAPI]; #if (PLATFORM_IOS && defined(__IPHONE_13_0)) || (PLATFORM_TVOS && defined(__TVOS_13_0)) if (@available(iOS 13, tvOS 13, *)) _window = [[UIWindow alloc] initWithWindowScene: [self pickStartupWindowScene: application.connectedScenes]]; else #endif _window = [[UIWindow alloc] initWithFrame: [UIScreen mainScreen].bounds]; _unityView = [self createUnityView]; [DisplayManager Initialize]; _mainDisplay = [DisplayManager Instance].mainDisplay; [_mainDisplay createWithWindow: _window andView: _unityView]; [self createUI]; [self preStartUnity]; // if you wont use keyboard you may comment it out at save some memory [KeyboardDelegate Initialize]; return YES; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([keyPath isEqual: @"outputVolume"]) { UnityUpdateMuteState([[AVAudioSession sharedInstance] outputVolume] < 0.01f ? 1 : 0); } } - (void)applicationDidEnterBackground:(UIApplication*)application { ::printf("-> applicationDidEnterBackground()\n"); } - (void)applicationWillEnterForeground:(UIApplication*)application { ::printf("-> applicationWillEnterForeground()\n"); // applicationWillEnterForeground: might sometimes arrive *before* actually initing unity (e.g. locking on startup) if (_unityAppReady) { // if we were showing video before going to background - the view size may be changed while we are in background [GetAppController().unityView recreateRenderingSurfaceIfNeeded]; } } - (void)applicationDidBecomeActive:(UIApplication*)application { ::printf("-> applicationDidBecomeActive()\n"); [self removeSnapshotViewController]; if (_unityAppReady) { if (UnityIsPaused() && _wasPausedExternal == false) { UnityWillResume(); UnityPause(0); } if (_wasPausedExternal) { if (UnityIsFullScreenPlaying()) TryResumeFullScreenVideo(); } // need to do this with delay because FMOD restarts audio in AVAudioSessionInterruptionNotification handler [self performSelector: @selector(updateUnityAudioOutput) withObject: nil afterDelay: 0.1]; UnitySetPlayerFocus(1); } else if (!_startUnityScheduled) { _startUnityScheduled = true; [self performSelector: @selector(startUnity:) withObject: application afterDelay: 0]; } _didResignActive = false; } - (void)updateUnityAudioOutput { UnityUpdateMuteState([[AVAudioSession sharedInstance] outputVolume] < 0.01f ? 1 : 0); } - (void)addSnapshotViewController { if (!_didResignActive || self->_snapshotViewController) { return; } UIView* snapshotView = [self createSnapshotView]; if (snapshotView != nil) { UIViewController* snapshotViewController = [AllocUnityViewController() init]; snapshotViewController.modalPresentationStyle = UIModalPresentationFullScreen; snapshotViewController.view = snapshotView; [self->_rootController presentViewController: snapshotViewController animated: false completion: nil]; self->_snapshotViewController = snapshotViewController; } } - (void)removeSnapshotViewController { // do this on the main queue async so that if we try to create one // and remove in the same frame, this always happens after in the same queue dispatch_async(dispatch_get_main_queue(), ^{ if (self->_snapshotViewController) { // we've got a view on top of the snapshot view (3rd party plugin/social media login etc). if (self->_snapshotViewController.presentedViewController) { [self performSelector: @selector(removeSnapshotViewController) withObject: nil afterDelay: 0.05]; return; } [self->_snapshotViewController dismissViewControllerAnimated: NO completion: nil]; self->_snapshotViewController = nil; // Make sure that the keyboard input field regains focus after the application becomes active. [[KeyboardDelegate Instance] becomeFirstResponder]; } }); } - (void)applicationWillResignActive:(UIApplication*)application { ::printf("-> applicationWillResignActive()\n"); if (_unityAppReady) { UnitySetPlayerFocus(0); // signal unity that the frame rendering have ended // as we will not get the callback from the display link current frame UnityDisplayLinkCallback(0); _wasPausedExternal = UnityIsPaused(); if (_wasPausedExternal == false) { // Pause Unity only if we don't need special background processing // otherwise batched player loop can be called to run user scripts. if (!UnityGetUseCustomAppBackgroundBehavior()) { #if UNITY_SNAPSHOT_VIEW_ON_APPLICATION_PAUSE // Force player to do one more frame, so scripts get a chance to render custom screen for minimized app in task manager. // NB: UnityWillPause will schedule OnApplicationPause message, which will be sent normally inside repaint (unity player loop) // NB: We will actually pause after the loop (when calling UnityPause). UnityWillPause(); [self repaint]; UnityWaitForFrame(); [self addSnapshotViewController]; #endif UnityPause(1); } } } _didResignActive = true; } - (void)applicationDidReceiveMemoryWarning:(UIApplication*)application { ::printf("WARNING -> applicationDidReceiveMemoryWarning()\n"); UnityLowMemory(); } - (void)applicationWillTerminate:(UIApplication*)application { ::printf("-> applicationWillTerminate()\n"); // Only clean up if Unity has finished initializing, else the clean up process will crash, // this happens if the app is force closed immediately after opening it. if (_unityAppReady) { UnityCleanup(); UnityCleanupTrampoline(); } } - (void)application:(UIApplication*)application handleEventsForBackgroundURLSession:(nonnull NSString *)identifier completionHandler:(nonnull void (^)())completionHandler { NSDictionary* arg = @{identifier: completionHandler}; AppController_SendNotificationWithArg(kUnityHandleEventsForBackgroundURLSession, arg); } @end void AppController_SendNotification(NSString* name) { [[NSNotificationCenter defaultCenter] postNotificationName: name object: GetAppController()]; } void AppController_SendNotificationWithArg(NSString* name, id arg) { [[NSNotificationCenter defaultCenter] postNotificationName: name object: GetAppController() userInfo: arg]; } void AppController_SendUnityViewControllerNotification(NSString* name) { [[NSNotificationCenter defaultCenter] postNotificationName: name object: UnityGetGLViewController()]; } extern "C" UIWindow* UnityGetMainWindow() { return GetAppController().mainDisplay.window; } extern "C" UIViewController* UnityGetGLViewController() { return GetAppController().rootViewController; } extern "C" UIView* UnityGetGLView() { return GetAppController().unityView; } extern "C" ScreenOrientation UnityCurrentOrientation() { return GetAppController().unityView.contentOrientation; } bool LogToNSLogHandler(LogType logType, const char* log, va_list list) { NSLogv([NSString stringWithUTF8String: log], list); return true; } static void AddNewAPIImplIfNeeded(); // From https://stackoverflow.com/questions/4744826/detecting-if-ios-app-is-run-in-debugger static bool isDebuggerAttachedToConsole(void) // Returns true if the current process is being debugged (either // running under the debugger or has a debugger attached post facto). { int junk; int mib[4]; struct kinfo_proc info; size_t size; // Initialize the flags so that, if sysctl fails for some bizarre // reason, we get a predictable result. info.kp_proc.p_flag = 0; // Initialize mib, which tells sysctl the info we want, in this case // we're looking for information about a specific process ID. mib[0] = CTL_KERN; mib[1] = KERN_PROC; mib[2] = KERN_PROC_PID; mib[3] = getpid(); // Call sysctl. size = sizeof(info); junk = sysctl(mib, sizeof(mib) / sizeof(*mib), &info, &size, NULL, 0); assert(junk == 0); // We're being debugged if the P_TRACED flag is set. return ((info.kp_proc.p_flag & P_TRACED) != 0); } void UnityInitTrampoline() { InitCrashHandling(); NSString* version = [[UIDevice currentDevice] systemVersion]; #define CHECK_VER(s) [version compare: s options: NSNumericSearch] != NSOrderedAscending _ios81orNewer = CHECK_VER(@"8.1"); _ios82orNewer = CHECK_VER(@"8.2"); _ios83orNewer = CHECK_VER(@"8.3"); _ios90orNewer = CHECK_VER(@"9.0"); _ios91orNewer = CHECK_VER(@"9.1"); _ios100orNewer = CHECK_VER(@"10.0"); _ios101orNewer = CHECK_VER(@"10.1"); _ios102orNewer = CHECK_VER(@"10.2"); _ios103orNewer = CHECK_VER(@"10.3"); _ios110orNewer = CHECK_VER(@"11.0"); _ios111orNewer = CHECK_VER(@"11.1"); _ios112orNewer = CHECK_VER(@"11.2"); _ios130orNewer = CHECK_VER(@"13.0"); _ios140orNewer = CHECK_VER(@"14.0"); #undef CHECK_VER AddNewAPIImplIfNeeded(); #if !TARGET_IPHONE_SIMULATOR // Use NSLog logging if a debugger is not attached, otherwise we write to stdout. if (!isDebuggerAttachedToConsole()) UnitySetLogEntryHandler(LogToNSLogHandler); #endif } extern "C" bool UnityiOS81orNewer() { return _ios81orNewer; } extern "C" bool UnityiOS82orNewer() { return _ios82orNewer; } extern "C" bool UnityiOS90orNewer() { return _ios90orNewer; } extern "C" bool UnityiOS91orNewer() { return _ios91orNewer; } extern "C" bool UnityiOS100orNewer() { return _ios100orNewer; } extern "C" bool UnityiOS101orNewer() { return _ios101orNewer; } extern "C" bool UnityiOS102orNewer() { return _ios102orNewer; } extern "C" bool UnityiOS103orNewer() { return _ios103orNewer; } extern "C" bool UnityiOS110orNewer() { return _ios110orNewer; } extern "C" bool UnityiOS111orNewer() { return _ios111orNewer; } extern "C" bool UnityiOS112orNewer() { return _ios112orNewer; } extern "C" bool UnityiOS130orNewer() { return _ios130orNewer; } extern "C" bool UnityiOS140orNewer() { return _ios140orNewer; } // sometimes apple adds new api with obvious fallback on older ios. // in that case we simply add these functions ourselves to simplify code static void AddNewAPIImplIfNeeded() { if (![[UIScreen class] instancesRespondToSelector: @selector(maximumFramesPerSecond)]) { IMP UIScreen_MaximumFramesPerSecond_IMP = imp_implementationWithBlock(^NSInteger(id _self) { return 60; }); class_replaceMethod([UIScreen class], @selector(maximumFramesPerSecond), UIScreen_MaximumFramesPerSecond_IMP, UIScreen_maximumFramesPerSecond_Enc); } if (![[UIView class] instancesRespondToSelector: @selector(safeAreaInsets)]) { IMP UIView_SafeAreaInsets_IMP = imp_implementationWithBlock(^UIEdgeInsets(id _self) { return UIEdgeInsetsZero; }); class_replaceMethod([UIView class], @selector(safeAreaInsets), UIView_SafeAreaInsets_IMP, UIView_safeAreaInsets_Enc); } } // xcode11 uses new compiler-rt lib // if we build unity player lib with xcode11 and then user links final project with older xcode // the link fails with Undefined Symbol ___isPlatformVersionAtLeast // hence we add this as a temporary hack until we start requiring xcode11 #if __clang_major__ < 11 extern "C" int32_t __isOSVersionAtLeast(int32_t Major, int32_t Minor, int32_t Subminor); extern "C" int32_t __isPlatformVersionAtLeast(uint32_t Platform, uint32_t Major, uint32_t Minor, uint32_t Subminor) { return __isOSVersionAtLeast(Major, Minor, Subminor); } #endif // starting with xcode 11.4 apple changed FD_SET and related macro to use weakly imported __darwin_check_fd_set_overflow // alas if we build xcode project with OLDER xcode this function is missing // and we build unity lib with xcode11+, thus producing linker error // we mimic the logic of apple sdk itself (this part is open sourced): // if __darwin_check_fd_set_overflow is not present the caller returns 1, so do we #ifndef __IPHONE_13_4 extern "C" int __darwin_check_fd_set_overflow(int, const void *, int) { return 1; } #endif