Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion Firebase/Auth/Source/Auth/FIRAuth.m
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,11 @@ + (FIRActionCodeOperation)actionCodeOperationForRequestType:(NSString *)requestT
#pragma mark - FIRAuth

#if TARGET_OS_IOS
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
@interface FIRAuth () <UIApplicationDelegate, UISceneDelegate, FIRLibrary, FIRComponentLifecycleMaintainer>
#else
@interface FIRAuth () <UIApplicationDelegate, FIRLibrary, FIRComponentLifecycleMaintainer>
#endif
#else
@interface FIRAuth () <FIRLibrary, FIRComponentLifecycleMaintainer>
#endif
Expand Down Expand Up @@ -1387,7 +1391,17 @@ - (BOOL)canHandleURL:(NSURL *)URL {
});
return result;
}
#endif

#pragma mark - UISceneDelegate
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
- (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts API_AVAILABLE(ios(13.0)) {
for (UIOpenURLContext *urlContext in URLContexts) {
NSURL *url = [urlContext URL];
[self canHandleURL:url];
}
}
#endif // __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
#endif // TARGET_OS_IOS

#pragma mark - Internal Methods

Expand Down
143 changes: 143 additions & 0 deletions GoogleUtilities/AppDelegateSwizzler/GULAppDelegateSwizzler.m
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ typedef BOOL (*GULRealOpenURLSourceApplicationAnnotationIMP)(
typedef BOOL (*GULRealOpenURLOptionsIMP)(
id, SEL, GULApplication *, NSURL *, NSDictionary<NSString *, id> *);

#if UISCENE_SUPPORTED
API_AVAILABLE(ios(13.0), tvos(13.0))
typedef void (*GULOpenURLContextsIMP)(id, SEL, UIScene *, NSSet<UIOpenURLContext *> *);
#endif // UISCENE_SUPPORTED

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wstrict-prototypes"
typedef void (*GULRealHandleEventsForBackgroundURLSessionIMP)(
Expand Down Expand Up @@ -289,6 +294,20 @@ + (void)proxyOriginalDelegate {
id<GULApplicationDelegate> originalDelegate =
[GULAppDelegateSwizzler sharedApplication].delegate;
[GULAppDelegateSwizzler proxyAppDelegate:originalDelegate];

#if UISCENE_SUPPORTED
if (@available(iOS 13.0, tvOS 13.0, *)) {
if (![GULAppDelegateSwizzler isAppDelegateProxyEnabled]) {
return;
} else {
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(handleSceneWillConnectToNotification:)
name:UISceneWillConnectNotification
object:nil];
}
}
#endif // UISCENE_SUPPORTED
});
}

Expand Down Expand Up @@ -682,6 +701,17 @@ + (void)notifyInterceptorsWithMethodSelector:(SEL)methodSelector
}];
}

#if UISCENE_SUPPORTED
+ (void)handleSceneWillConnectToNotification:(NSNotification *)notification {
if (@available(iOS 13.0, tvOS 13.0, *)) {
if ([notification.object isKindOfClass:[UIScene class]]) {
UIScene *scene = (UIScene *)notification.object;
[GULAppDelegateSwizzler proxySceneDelegate:scene];
}
}
}
#endif // UISCENE_SUPPORTED

// The methods below are donor methods which are added to the dynamic subclass of the App Delegate.
// They are called within the scope of the real App Delegate so |self| does not refer to the
// GULAppDelegateSwizzler instance but the real App Delegate instance.
Expand Down Expand Up @@ -761,6 +791,36 @@ - (BOOL)application:(GULApplication *)application

#endif // TARGET_OS_IOS

#pragma mark - [Donor Methods] UISceneDelegate URL handler

#if UISCENE_SUPPORTED
- (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 App Delegate has any.
NSValue *openURLContextsIMPPointer =
[GULAppDelegateSwizzler originalImplementationForSelector:methodSelector object:self];
GULOpenURLContextsIMP openURLContextsIMP = [openURLContextsIMPPointer pointerValue];

[GULAppDelegateSwizzler
notifyInterceptorsWithMethodSelector:methodSelector
callback:^(id<GULApplicationDelegate> interceptor) {
if ([interceptor
conformsToProtocol:@protocol(UISceneDelegate)]) {
id<UISceneDelegate> sceneInterceptor =
(id<UISceneDelegate>)interceptor;
[sceneInterceptor scene:scene openURLContexts:URLContexts];
}
}];

if (openURLContextsIMP) {
openURLContextsIMP(self, methodSelector, scene, URLContexts);
}
}
}
#endif // UISCENE_SUPPORTED

#pragma mark - [Donor Methods] Network overridden handler methods

#if TARGET_OS_IOS || TARGET_OS_TV
Expand Down Expand Up @@ -1002,6 +1062,89 @@ + (void)proxyAppDelegate:(id<GULApplicationDelegate>)appDelegate {
}
}

#if UISCENE_SUPPORTED
+ (void)proxySceneDelegate:(UIScene *)scene {
Class realClass = [scene.delegate class];
NSString *className = NSStringFromClass(realClass);

// Skip proxying if the class has a prefix of kGULAppDelegatePrefix, which means it has been
// proxied before.
if ([className hasPrefix:kGULAppDelegatePrefix]) {
return;
}

NSString *classNameWithPrefix = [kGULAppDelegatePrefix stringByAppendingString:className];
NSString *newClassName =
[NSString stringWithFormat:@"%@-%@", classNameWithPrefix, [NSUUID UUID].UUIDString];

if (NSClassFromString(newClassName)) {
GULLogError(kGULLoggerSwizzler, NO,
[NSString stringWithFormat:@"I-SWZ%06ld",
(long)kGULSwizzlerMessageCodeAppDelegateSwizzling005],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Not sure if we should reuse the same message code. I think we should create separate message codes for this method. Also the message should be updated to replace "App Delegate" by "Scene Delegate"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, dedicated message codes should be better. Can I add it directly? or is there any review process for GULLogger?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to add them here. A meaningful name is the recent recommendation (like kGULSwizzlerMessageCodeAppDelegateSwizzlingInvalidAppDelegate in the enum).

@"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)kGULSwizzlerMessageCodeAppDelegateSwizzling006],
@"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:[GULAppDelegateSwizzler class]
toClass:sceneDelegateSubClass
realClass:realClass
storeDestinationImplementationTo:realImplementationsBySelector];

// Store original implementations to a fake property of the original delegate.
objc_setAssociatedObject(scene.delegate, &kGULRealIMPBySelectorKey,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: It think readability of the class may be slightly improved if we create a "set" counterpart for originalImplementationForSelector: object: method and use it everywhere instead of referring to kGULRealIMPBySelectorKey directly. It may be a subject of a followup PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like using kGULRealIMPBySelectorKey may remind people that it's a runtime associated object and people will be more careful. While you suggestion totally makes sense. We can discuss more in a followup pr.

[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)kGULSwizzlerMessageCodeAppDelegateSwizzling007],
@"Cannot create subclass of App 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)kGULSwizzlerMessageCodeAppDelegateSwizzling008],
@"Successfully created App Delegate Proxy automatically. To disable the "
@"proxy, set the flag %@ to NO (Boolean) in the Info.plist",
[GULAppDelegateSwizzler correctAppDelegateProxyKey]);
}
}
#endif // UISCENE_SUPPORTED

#pragma mark - Methods to print correct debug logs

+ (NSString *)correctAppDelegateProxyKey {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
#import <GoogleUtilities/GULAppDelegateSwizzler.h>
#import <GoogleUtilities/GULMutableDictionary.h>

#if ((TARGET_OS_IOS || TARGET_OS_TV) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= 130000))
#define UISCENE_SUPPORTED 1
#endif

@class GULApplication;

NS_ASSUME_NONNULL_BEGIN
Expand Down Expand Up @@ -50,6 +54,17 @@ NS_ASSUME_NONNULL_BEGIN
*/
+ (id<GULApplicationDelegate>)originalDelegate;

#if UISCENE_SUPPORTED

/** ISA Swizzles the given appDelegate as the original app delegate would be.
*
* @param scene The scene whose delegate needs to be isa swizzled. This should conform to the
* scene delegate protocol.
*/
+ (void)proxySceneDelegate:(UIScene *)scene API_AVAILABLE(ios(13.0), tvos(13.0));

#endif // UISCENE_SUPPORTED

@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,20 @@ - (BOOL)application:(GULApplication *)application

@end

#pragma mark - Scene Delegate

#if ((TARGET_OS_IOS || TARGET_OS_TV) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= 130000))
@protocol TestSceneProtocol <GULApplicationDelegate, UISceneDelegate>
@end

API_AVAILABLE(ios(13.0), tvos(13.0))
@interface GULTestSceneDelegate : NSObject <UISceneDelegate>
@end

@implementation GULTestSceneDelegate
@end
#endif

@interface GULAppDelegateSwizzlerTest : XCTestCase
@property(nonatomic, strong) id mockSharedApplication;
@end
Expand Down Expand Up @@ -1301,4 +1315,85 @@ - (void)testAppDelegateIsProxiedIncludingAPNSMethodsWhenEnabled {
XCTAssertNotEqualObjects([originalAppDelegate class], originalAppDelegateClass);
}

#pragma mark - Test UISceneDelegate proxy

#if ((TARGET_OS_IOS || TARGET_OS_TV) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= 130000))

- (void)testProxySceneDelegate {
if (@available(iOS 13, tvOS 13, *)) {
GULTestSceneDelegate *realSceneDelegate = [[GULTestSceneDelegate alloc] init];
id mockSharedScene = OCMClassMock([UIScene class]);
OCMStub([mockSharedScene delegate]).andReturn(realSceneDelegate);
size_t sizeBefore = class_getInstanceSize([GULTestSceneDelegate class]);

Class realSceneDelegateClassBefore = [realSceneDelegate class];

[GULAppDelegateSwizzler proxySceneDelegate:mockSharedScene];

XCTAssertTrue([realSceneDelegate isKindOfClass:[GULTestSceneDelegate class]]);

NSString *newClassName = NSStringFromClass([realSceneDelegate class]);
XCTAssertTrue([newClassName hasPrefix:@"GUL_"]);
// It is no longer GULTestSceneDelegate class instance.
XCTAssertFalse([realSceneDelegate isMemberOfClass:[GULTestSceneDelegate class]]);

size_t sizeAfter = class_getInstanceSize([realSceneDelegate class]);

// Class size must stay the same.
XCTAssertEqual(sizeBefore, sizeAfter);

// After being proxied, it should be able to respond to the required method selector.
XCTAssertTrue([realSceneDelegate respondsToSelector:@selector(scene:openURLContexts:)]);

// Make sure that the class has changed.
XCTAssertNotEqualObjects([realSceneDelegate class], realSceneDelegateClassBefore);
}
}

- (void)testProxyProxiedSceneDelegate {
if (@available(iOS 13, tvOS 13, *)) {
GULTestSceneDelegate *realSceneDelegate = [[GULTestSceneDelegate alloc] init];
id mockSharedScene = OCMClassMock([UIScene class]);
OCMStub([mockSharedScene delegate]).andReturn(realSceneDelegate);

// Proxy the scene delegate for the 1st time.
[GULAppDelegateSwizzler proxySceneDelegate:mockSharedScene];

Class realSceneDelegateClassBefore = [realSceneDelegate class];

// Proxy the scene delegate for the 2nd time.
[GULAppDelegateSwizzler proxySceneDelegate:mockSharedScene];

// Make sure that the class isn't changed.
XCTAssertEqualObjects([realSceneDelegate class], realSceneDelegateClassBefore);
}
}

- (void)testSceneOpenURLContextsIsInvokedOnInterceptors {
if (@available(iOS 13, tvOS 13, *)) {
NSSet *urlContexts = [NSSet set];

GULTestSceneDelegate *realSceneDelegate = [[GULTestSceneDelegate alloc] init];
id mockSharedScene = OCMClassMock([UIScene class]);
OCMStub([mockSharedScene delegate]).andReturn(realSceneDelegate);

id interceptor = OCMProtocolMock(@protocol(TestSceneProtocol));
OCMExpect([interceptor scene:mockSharedScene openURLContexts:urlContexts]);

id interceptor2 = OCMProtocolMock(@protocol(TestSceneProtocol));
OCMExpect([interceptor2 scene:mockSharedScene openURLContexts:urlContexts]);

[GULAppDelegateSwizzler proxySceneDelegate:mockSharedScene];

[GULAppDelegateSwizzler registerAppDelegateInterceptor:interceptor];
[GULAppDelegateSwizzler registerAppDelegateInterceptor:interceptor2];

[realSceneDelegate scene:mockSharedScene openURLContexts:urlContexts];
OCMVerifyAll(interceptor);
OCMVerifyAll(interceptor2);
}
}

#endif

@end