439 lines
19 KiB
Objective-C
439 lines
19 KiB
Objective-C
// Copyright 2019 Google LLC
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
#import <TargetConditionals.h>
|
|
|
|
#import "GoogleUtilities/AppDelegateSwizzler/Private/GULAppDelegateSwizzler.h"
|
|
#import "GoogleUtilities/Common/GULLoggerCodes.h"
|
|
#import "GoogleUtilities/Environment/Private/GULAppEnvironmentUtil.h"
|
|
#import "GoogleUtilities/Logger/Private/GULLogger.h"
|
|
#import "GoogleUtilities/Network/Private/GULMutableDictionary.h"
|
|
#import "GoogleUtilities/SceneDelegateSwizzler/Internal/GULSceneDelegateSwizzler_Private.h"
|
|
#import "GoogleUtilities/SceneDelegateSwizzler/Private/GULSceneDelegateSwizzler.h"
|
|
|
|
#import <objc/runtime.h>
|
|
|
|
#if UISCENE_SUPPORTED
|
|
API_AVAILABLE(ios(13.0), tvos(13.0))
|
|
typedef void (*GULOpenURLContextsIMP)(id, SEL, UIScene *, NSSet<UIOpenURLContext *> *);
|
|
|
|
API_AVAILABLE(ios(13.0), tvos(13.0))
|
|
typedef void (^GULSceneDelegateInterceptorCallback)(id<UISceneDelegate>);
|
|
|
|
// The strings below are the keys for associated objects.
|
|
static char const *const kGULRealIMPBySelectorKey = "GUL_realIMPBySelector";
|
|
static char const *const kGULRealClassKey = "GUL_realClass";
|
|
#endif // UISCENE_SUPPORTED
|
|
|
|
static GULLoggerService kGULLoggerSwizzler = @"[GoogleUtilities/SceneDelegateSwizzler]";
|
|
|
|
// Since Firebase SDKs also use this for app delegate proxying, in order to not be a breaking change
|
|
// we disable App Delegate proxying when either of these two flags are set to NO.
|
|
|
|
/** Plist key that allows Firebase developers to disable App and Scene Delegate Proxying. */
|
|
static NSString *const kGULFirebaseSceneDelegateProxyEnabledPlistKey =
|
|
@"FirebaseAppDelegateProxyEnabled";
|
|
|
|
/** Plist key that allows developers not using Firebase to disable App and Scene Delegate Proxying.
|
|
*/
|
|
static NSString *const kGULGoogleUtilitiesSceneDelegateProxyEnabledPlistKey =
|
|
@"GoogleUtilitiesAppDelegateProxyEnabled";
|
|
|
|
/** The prefix of the Scene Delegate. */
|
|
static NSString *const kGULSceneDelegatePrefix = @"GUL_";
|
|
|
|
/**
|
|
* This class is necessary to store the delegates in an NSArray without retaining them.
|
|
* [NSValue valueWithNonRetainedObject] also provides this functionality, but does not provide a
|
|
* zeroing pointer. This will cause EXC_BAD_ACCESS when trying to access the object after it is
|
|
* dealloced. Instead, this container stores a weak, zeroing reference to the object, which
|
|
* automatically is set to nil by the runtime when the object is dealloced.
|
|
*/
|
|
@interface GULSceneZeroingWeakContainer : NSObject
|
|
|
|
/** Stores a weak object. */
|
|
@property(nonatomic, weak) id object;
|
|
|
|
@end
|
|
|
|
@implementation GULSceneZeroingWeakContainer
|
|
@end
|
|
|
|
@implementation GULSceneDelegateSwizzler
|
|
|
|
#pragma mark - Public methods
|
|
|
|
+ (BOOL)isSceneDelegateProxyEnabled {
|
|
return [GULAppDelegateSwizzler isAppDelegateProxyEnabled];
|
|
}
|
|
|
|
+ (void)proxyOriginalSceneDelegate {
|
|
#if UISCENE_SUPPORTED
|
|
if ([GULAppEnvironmentUtil isAppExtension]) {
|
|
return;
|
|
}
|
|
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
if (@available(iOS 13.0, tvOS 13.0, *)) {
|
|
if (![GULSceneDelegateSwizzler isSceneDelegateProxyEnabled]) {
|
|
return;
|
|
}
|
|
[[NSNotificationCenter defaultCenter]
|
|
addObserver:self
|
|
selector:@selector(handleSceneWillConnectToNotification:)
|
|
name:UISceneWillConnectNotification
|
|
object:nil];
|
|
}
|
|
});
|
|
#endif // UISCENE_SUPPORTED
|
|
}
|
|
|
|
#if UISCENE_SUPPORTED
|
|
+ (GULSceneDelegateInterceptorID)registerSceneDelegateInterceptor:(id<UISceneDelegate>)interceptor {
|
|
NSAssert(interceptor, @"SceneDelegateProxy cannot add nil interceptor");
|
|
NSAssert([interceptor conformsToProtocol:@protocol(UISceneDelegate)],
|
|
@"SceneDelegateProxy interceptor does not conform to UIApplicationDelegate");
|
|
|
|
if (!interceptor) {
|
|
GULLogError(kGULLoggerSwizzler, NO,
|
|
[NSString stringWithFormat:@"I-SWZ%06ld",
|
|
(long)kGULSwizzlerMessageCodeSceneDelegateSwizzling000],
|
|
@"SceneDelegateProxy cannot add nil interceptor.");
|
|
return nil;
|
|
}
|
|
if (![interceptor conformsToProtocol:@protocol(UISceneDelegate)]) {
|
|
GULLogError(kGULLoggerSwizzler, NO,
|
|
[NSString stringWithFormat:@"I-SWZ%06ld",
|
|
(long)kGULSwizzlerMessageCodeSceneDelegateSwizzling001],
|
|
@"SceneDelegateProxy interceptor does not conform to UIApplicationDelegate");
|
|
return nil;
|
|
}
|
|
|
|
// The ID should be the same given the same interceptor object.
|
|
NSString *interceptorID =
|
|
[NSString stringWithFormat:@"%@%p", kGULSceneDelegatePrefix, interceptor];
|
|
if (!interceptorID.length) {
|
|
GULLogError(kGULLoggerSwizzler, NO,
|
|
[NSString stringWithFormat:@"I-SWZ%06ld",
|
|
(long)kGULSwizzlerMessageCodeSceneDelegateSwizzling002],
|
|
@"SceneDelegateProxy cannot create Interceptor ID.");
|
|
return nil;
|
|
}
|
|
GULSceneZeroingWeakContainer *weakObject = [[GULSceneZeroingWeakContainer alloc] init];
|
|
weakObject.object = interceptor;
|
|
[GULSceneDelegateSwizzler interceptors][interceptorID] = weakObject;
|
|
return interceptorID;
|
|
}
|
|
|
|
+ (void)unregisterSceneDelegateInterceptorWithID:(GULSceneDelegateInterceptorID)interceptorID {
|
|
NSAssert(interceptorID, @"SceneDelegateProxy cannot unregister nil interceptor ID.");
|
|
NSAssert(((NSString *)interceptorID).length != 0,
|
|
@"SceneDelegateProxy cannot unregister empty interceptor ID.");
|
|
|
|
if (!interceptorID) {
|
|
GULLogError(kGULLoggerSwizzler, NO,
|
|
[NSString stringWithFormat:@"I-SWZ%06ld",
|
|
(long)kGULSwizzlerMessageCodeSceneDelegateSwizzling003],
|
|
@"SceneDelegateProxy cannot unregister empty interceptor ID.");
|
|
return;
|
|
}
|
|
|
|
GULSceneZeroingWeakContainer *weakContainer =
|
|
[GULSceneDelegateSwizzler interceptors][interceptorID];
|
|
if (!weakContainer.object) {
|
|
GULLogError(kGULLoggerSwizzler, NO,
|
|
[NSString stringWithFormat:@"I-SWZ%06ld",
|
|
(long)kGULSwizzlerMessageCodeSceneDelegateSwizzling004],
|
|
@"SceneDelegateProxy cannot unregister interceptor that was not registered. "
|
|
"Interceptor ID %@",
|
|
interceptorID);
|
|
return;
|
|
}
|
|
|
|
[[GULSceneDelegateSwizzler interceptors] removeObjectForKey:interceptorID];
|
|
}
|
|
|
|
#pragma mark - Helper methods
|
|
|
|
+ (GULMutableDictionary *)interceptors {
|
|
static dispatch_once_t onceToken;
|
|
static GULMutableDictionary *sInterceptors;
|
|
dispatch_once(&onceToken, ^{
|
|
sInterceptors = [[GULMutableDictionary alloc] init];
|
|
});
|
|
return sInterceptors;
|
|
}
|
|
|
|
+ (void)clearInterceptors {
|
|
[[self interceptors] removeAllObjects];
|
|
}
|
|
|
|
+ (nullable NSValue *)originalImplementationForSelector:(SEL)selector object:(id)object {
|
|
NSDictionary *realImplementationBySelector =
|
|
objc_getAssociatedObject(object, &kGULRealIMPBySelectorKey);
|
|
return realImplementationBySelector[NSStringFromSelector(selector)];
|
|
}
|
|
|
|
+ (void)proxyDestinationSelector:(SEL)destinationSelector
|
|
implementationsFromSourceSelector:(SEL)sourceSelector
|
|
fromClass:(Class)sourceClass
|
|
toClass:(Class)destinationClass
|
|
realClass:(Class)realClass
|
|
storeDestinationImplementationTo:
|
|
(NSMutableDictionary<NSString *, NSValue *> *)destinationImplementationsBySelector {
|
|
[self addInstanceMethodWithDestinationSelector:destinationSelector
|
|
withImplementationFromSourceSelector:sourceSelector
|
|
fromClass:sourceClass
|
|
toClass:destinationClass];
|
|
IMP sourceImplementation =
|
|
[GULSceneDelegateSwizzler implementationOfMethodSelector:destinationSelector
|
|
fromClass:realClass];
|
|
NSValue *sourceImplementationPointer = [NSValue valueWithPointer:sourceImplementation];
|
|
|
|
NSString *destinationSelectorString = NSStringFromSelector(destinationSelector);
|
|
destinationImplementationsBySelector[destinationSelectorString] = sourceImplementationPointer;
|
|
}
|
|
|
|
/** Copies a method identified by the methodSelector from one class to the other. After this method
|
|
* is called, performing [toClassInstance methodSelector] will be similar to calling
|
|
* [fromClassInstance methodSelector]. This method does nothing if toClass already has a method
|
|
* identified by methodSelector.
|
|
*
|
|
* @param methodSelector The SEL that identifies both the method on the fromClass as well as the
|
|
* one on the toClass.
|
|
* @param fromClass The class from which a method is sourced.
|
|
* @param toClass The class to which the method is added. If the class already has a method with
|
|
* the same selector, this has no effect.
|
|
*/
|
|
+ (void)addInstanceMethodWithSelector:(SEL)methodSelector
|
|
fromClass:(Class)fromClass
|
|
toClass:(Class)toClass {
|
|
[self addInstanceMethodWithDestinationSelector:methodSelector
|
|
withImplementationFromSourceSelector:methodSelector
|
|
fromClass:fromClass
|
|
toClass:toClass];
|
|
}
|
|
|
|
/** Copies a method identified by the sourceSelector from the fromClass as a method for the
|
|
* destinationSelector on the toClass. After this method is called, performing
|
|
* [toClassInstance destinationSelector] will be similar to calling
|
|
* [fromClassInstance sourceSelector]. This method does nothing if toClass already has a method
|
|
* identified by destinationSelector.
|
|
*
|
|
* @param destinationSelector The SEL that identifies the method on the toClass.
|
|
* @param sourceSelector The SEL that identifies the method on the fromClass.
|
|
* @param fromClass The class from which a method is sourced.
|
|
* @param toClass The class to which the method is added. If the class already has a method with
|
|
* the same selector, this has no effect.
|
|
*/
|
|
+ (void)addInstanceMethodWithDestinationSelector:(SEL)destinationSelector
|
|
withImplementationFromSourceSelector:(SEL)sourceSelector
|
|
fromClass:(Class)fromClass
|
|
toClass:(Class)toClass {
|
|
Method method = class_getInstanceMethod(fromClass, sourceSelector);
|
|
IMP methodIMP = method_getImplementation(method);
|
|
const char *types = method_getTypeEncoding(method);
|
|
if (!class_addMethod(toClass, destinationSelector, methodIMP, types)) {
|
|
GULLogWarning(
|
|
kGULLoggerSwizzler, NO,
|
|
[NSString
|
|
stringWithFormat:@"I-SWZ%06ld", (long)kGULSwizzlerMessageCodeSceneDelegateSwizzling009],
|
|
@"Cannot copy method to destination selector %@ as it already exists",
|
|
NSStringFromSelector(destinationSelector));
|
|
}
|
|
}
|
|
|
|
/** Gets the IMP of the instance method on the class identified by the selector.
|
|
*
|
|
* @param selector The selector of which the IMP is to be fetched.
|
|
* @param aClass The class from which the IMP is to be fetched.
|
|
* @return The IMP of the instance method identified by selector and aClass.
|
|
*/
|
|
+ (IMP)implementationOfMethodSelector:(SEL)selector fromClass:(Class)aClass {
|
|
Method aMethod = class_getInstanceMethod(aClass, selector);
|
|
return method_getImplementation(aMethod);
|
|
}
|
|
|
|
/** Enumerates through all the interceptors and if they respond to a given selector, executes a
|
|
* GULSceneDelegateInterceptorCallback with the interceptor.
|
|
*
|
|
* @param methodSelector The SEL to check if an interceptor responds to.
|
|
* @param callback the GULSceneDelegateInterceptorCallback.
|
|
*/
|
|
+ (void)notifyInterceptorsWithMethodSelector:(SEL)methodSelector
|
|
callback:(GULSceneDelegateInterceptorCallback)callback
|
|
API_AVAILABLE(ios(13.0)) {
|
|
if (!callback) {
|
|
return;
|
|
}
|
|
|
|
NSDictionary *interceptors = [GULSceneDelegateSwizzler interceptors].dictionary;
|
|
[interceptors enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
|
|
GULSceneZeroingWeakContainer *interceptorContainer = obj;
|
|
id interceptor = interceptorContainer.object;
|
|
if (!interceptor) {
|
|
GULLogWarning(
|
|
kGULLoggerSwizzler, NO,
|
|
[NSString stringWithFormat:@"I-SWZ%06ld",
|
|
(long)kGULSwizzlerMessageCodeSceneDelegateSwizzling010],
|
|
@"SceneDelegateProxy cannot find interceptor with ID %@. Removing the interceptor.", key);
|
|
[[GULSceneDelegateSwizzler interceptors] removeObjectForKey:key];
|
|
return;
|
|
}
|
|
if ([interceptor respondsToSelector:methodSelector]) {
|
|
callback(interceptor);
|
|
}
|
|
}];
|
|
}
|
|
|
|
+ (void)handleSceneWillConnectToNotification:(NSNotification *)notification {
|
|
if (@available(iOS 13.0, tvOS 13.0, *)) {
|
|
if ([notification.object isKindOfClass:[UIScene class]]) {
|
|
UIScene *scene = (UIScene *)notification.object;
|
|
[GULSceneDelegateSwizzler proxySceneDelegateIfNeeded:scene];
|
|
}
|
|
}
|
|
}
|
|
|
|
#pragma mark - [Donor Methods] UISceneDelegate URL handler
|
|
|
|
- (void)scene:(UIScene *)scene
|
|
openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts API_AVAILABLE(ios(13.0), tvos(13.0)) {
|
|
if (@available(iOS 13.0, tvOS 13.0, *)) {
|
|
SEL methodSelector = @selector(scene:openURLContexts:);
|
|
// Call the real implementation if the real Scene Delegate has any.
|
|
NSValue *openURLContextsIMPPointer =
|
|
[GULSceneDelegateSwizzler originalImplementationForSelector:methodSelector object:self];
|
|
GULOpenURLContextsIMP openURLContextsIMP = [openURLContextsIMPPointer pointerValue];
|
|
|
|
[GULSceneDelegateSwizzler
|
|
notifyInterceptorsWithMethodSelector:methodSelector
|
|
callback:^(id<UISceneDelegate> interceptor) {
|
|
if ([interceptor
|
|
conformsToProtocol:@protocol(UISceneDelegate)]) {
|
|
id<UISceneDelegate> sceneInterceptor =
|
|
(id<UISceneDelegate>)interceptor;
|
|
[sceneInterceptor scene:scene openURLContexts:URLContexts];
|
|
}
|
|
}];
|
|
|
|
if (openURLContextsIMP) {
|
|
openURLContextsIMP(self, methodSelector, scene, URLContexts);
|
|
}
|
|
}
|
|
}
|
|
|
|
+ (void)proxySceneDelegateIfNeeded:(UIScene *)scene {
|
|
Class realClass = [scene.delegate class];
|
|
NSString *className = NSStringFromClass(realClass);
|
|
|
|
// Skip proxying if failed to get the delegate class name for some reason (e.g. `delegate == nil`)
|
|
// or the class has a prefix of kGULAppDelegatePrefix, which means it has been proxied before.
|
|
if (className == nil || [className hasPrefix:kGULSceneDelegatePrefix]) {
|
|
return;
|
|
}
|
|
|
|
NSString *classNameWithPrefix = [kGULSceneDelegatePrefix stringByAppendingString:className];
|
|
NSString *newClassName =
|
|
[NSString stringWithFormat:@"%@-%@", classNameWithPrefix, [NSUUID UUID].UUIDString];
|
|
|
|
if (NSClassFromString(newClassName)) {
|
|
GULLogError(
|
|
kGULLoggerSwizzler, NO,
|
|
[NSString
|
|
stringWithFormat:@"I-SWZ%06ld",
|
|
(long)
|
|
kGULSwizzlerMessageCodeSceneDelegateSwizzlingInvalidSceneDelegate],
|
|
@"Cannot create a proxy for Scene Delegate. Subclass already exists. Original Class"
|
|
@": %@, subclass: %@",
|
|
className, newClassName);
|
|
return;
|
|
}
|
|
|
|
// Register the new class as subclass of the real one. Do not allocate more than the real class
|
|
// size.
|
|
Class sceneDelegateSubClass = objc_allocateClassPair(realClass, newClassName.UTF8String, 0);
|
|
if (sceneDelegateSubClass == Nil) {
|
|
GULLogError(
|
|
kGULLoggerSwizzler, NO,
|
|
[NSString
|
|
stringWithFormat:@"I-SWZ%06ld",
|
|
(long)
|
|
kGULSwizzlerMessageCodeSceneDelegateSwizzlingInvalidSceneDelegate],
|
|
@"Cannot create a proxy for Scene Delegate. Subclass already exists. Original Class"
|
|
@": %@, subclass: Nil",
|
|
className);
|
|
return;
|
|
}
|
|
|
|
NSMutableDictionary<NSString *, NSValue *> *realImplementationsBySelector =
|
|
[[NSMutableDictionary alloc] init];
|
|
|
|
// For scene:openURLContexts:
|
|
SEL openURLContextsSEL = @selector(scene:openURLContexts:);
|
|
[self proxyDestinationSelector:openURLContextsSEL
|
|
implementationsFromSourceSelector:openURLContextsSEL
|
|
fromClass:[GULSceneDelegateSwizzler class]
|
|
toClass:sceneDelegateSubClass
|
|
realClass:realClass
|
|
storeDestinationImplementationTo:realImplementationsBySelector];
|
|
|
|
// Store original implementations to a fake property of the original delegate.
|
|
objc_setAssociatedObject(scene.delegate, &kGULRealIMPBySelectorKey,
|
|
[realImplementationsBySelector copy], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
objc_setAssociatedObject(scene.delegate, &kGULRealClassKey, realClass,
|
|
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
|
|
// The subclass size has to be exactly the same size with the original class size. The subclass
|
|
// cannot have more ivars/properties than its superclass since it will cause an offset in memory
|
|
// that can lead to overwriting the isa of an object in the next frame.
|
|
if (class_getInstanceSize(realClass) != class_getInstanceSize(sceneDelegateSubClass)) {
|
|
GULLogError(
|
|
kGULLoggerSwizzler, NO,
|
|
[NSString
|
|
stringWithFormat:@"I-SWZ%06ld",
|
|
(long)
|
|
kGULSwizzlerMessageCodeSceneDelegateSwizzlingInvalidSceneDelegate],
|
|
@"Cannot create subclass of Scene Delegate, because the created subclass is not the "
|
|
@"same size. %@",
|
|
className);
|
|
NSAssert(NO, @"Classes must be the same size to swizzle isa");
|
|
return;
|
|
}
|
|
|
|
// Make the newly created class to be the subclass of the real Scene Delegate class.
|
|
objc_registerClassPair(sceneDelegateSubClass);
|
|
if (object_setClass(scene.delegate, sceneDelegateSubClass)) {
|
|
GULLogDebug(
|
|
kGULLoggerSwizzler, NO,
|
|
[NSString
|
|
stringWithFormat:@"I-SWZ%06ld",
|
|
(long)
|
|
kGULSwizzlerMessageCodeSceneDelegateSwizzlingInvalidSceneDelegate],
|
|
@"Successfully created Scene Delegate Proxy automatically. To disable the "
|
|
@"proxy, set the flag %@ to NO (Boolean) in the Info.plist",
|
|
[GULSceneDelegateSwizzler correctSceneDelegateProxyKey]);
|
|
}
|
|
}
|
|
|
|
+ (NSString *)correctSceneDelegateProxyKey {
|
|
return NSClassFromString(@"FIRCore") ? kGULFirebaseSceneDelegateProxyEnabledPlistKey
|
|
: kGULGoogleUtilitiesSceneDelegateProxyEnabledPlistKey;
|
|
}
|
|
|
|
#endif // UISCENE_SUPPORTED
|
|
|
|
@end
|