This commit is contained in:
Yao
2024-12-20 17:49:45 +08:00
parent 86b0363ce1
commit 654d456c7d
7011 changed files with 1705926 additions and 7 deletions

View File

@@ -0,0 +1,1072 @@
// Copyright 2018 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/Internal/GULAppDelegateSwizzler_Private.h"
#import "GoogleUtilities/AppDelegateSwizzler/Public/GoogleUtilities/GULAppDelegateSwizzler.h"
#import "GoogleUtilities/Common/GULLoggerCodes.h"
#import "GoogleUtilities/Environment/Public/GoogleUtilities/GULAppEnvironmentUtil.h"
#import "GoogleUtilities/Logger/Public/GoogleUtilities/GULLogger.h"
#import "GoogleUtilities/Network/Public/GoogleUtilities/GULMutableDictionary.h"
#import <dispatch/group.h>
#import <objc/runtime.h>
// Implementations need to be typed before calling the implementation directly to cast the
// arguments and the return types correctly. Otherwise, it will crash the app.
typedef BOOL (*GULRealOpenURLSourceApplicationAnnotationIMP)(
id, SEL, GULApplication *, NSURL *, NSString *, id);
typedef BOOL (*GULRealOpenURLOptionsIMP)(
id, SEL, GULApplication *, NSURL *, NSDictionary<NSString *, id> *);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wstrict-prototypes"
typedef void (*GULRealHandleEventsForBackgroundURLSessionIMP)(
id, SEL, GULApplication *, NSString *, void (^)());
#pragma clang diagnostic pop
typedef BOOL (*GULRealContinueUserActivityIMP)(
id, SEL, GULApplication *, NSUserActivity *, void (^)(NSArray *restorableObjects));
typedef void (*GULRealDidRegisterForRemoteNotificationsIMP)(id, SEL, GULApplication *, NSData *);
typedef void (*GULRealDidFailToRegisterForRemoteNotificationsIMP)(id,
SEL,
GULApplication *,
NSError *);
typedef void (*GULRealDidReceiveRemoteNotificationIMP)(id, SEL, GULApplication *, NSDictionary *);
#if !TARGET_OS_WATCH && !TARGET_OS_OSX
typedef void (*GULRealDidReceiveRemoteNotificationWithCompletionIMP)(
id, SEL, GULApplication *, NSDictionary *, void (^)(UIBackgroundFetchResult));
#endif // !TARGET_OS_WATCH && !TARGET_OS_OSX
typedef void (^GULAppDelegateInterceptorCallback)(id<GULApplicationDelegate>);
// The strings below are the keys for associated objects.
static char const *const kGULRealIMPBySelectorKey = "GUL_realIMPBySelector";
static char const *const kGULRealClassKey = "GUL_realClass";
static NSString *const kGULAppDelegateKeyPath = @"delegate";
static GULLoggerService kGULLoggerSwizzler = @"[GoogleUtilities/AppDelegateSwizzler]";
// 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 kGULFirebaseAppDelegateProxyEnabledPlistKey =
@"FirebaseAppDelegateProxyEnabled";
/** Plist key that allows developers not using Firebase to disable App and Scene Delegate Proxying.
*/
static NSString *const kGULGoogleUtilitiesAppDelegateProxyEnabledPlistKey =
@"GoogleUtilitiesAppDelegateProxyEnabled";
/** The prefix of the App Delegate. */
static NSString *const kGULAppDelegatePrefix = @"GUL_";
/** The original instance of App Delegate. */
static id<GULApplicationDelegate> gOriginalAppDelegate;
/** The original App Delegate class */
static Class gOriginalAppDelegateClass;
/** The subclass of the original App Delegate. */
static Class gAppDelegateSubclass;
/** Remote notification methods selectors
*
* We have to opt out of referencing APNS related App Delegate methods directly to prevent
* an Apple review warning email about missing Push Notification Entitlement
* (like here: https://github.com/firebase/firebase-ios-sdk/issues/2807). From our experience, the
* warning is triggered when any of the symbols is present in the application sent to review, even
* if the code is never executed. Because GULAppDelegateSwizzler may be used by applications that
* are not using APNS we have to refer to the methods indirectly using selector constructed from
* string.
*
* NOTE: None of the methods is proxied unless it is explicitly requested by calling the method
* +[GULAppDelegateSwizzler proxyOriginalDelegateIncludingAPNSMethods]
*/
static NSString *const kGULDidRegisterForRemoteNotificationsSEL =
@"application:didRegisterForRemoteNotificationsWithDeviceToken:";
static NSString *const kGULDidFailToRegisterForRemoteNotificationsSEL =
@"application:didFailToRegisterForRemoteNotificationsWithError:";
static NSString *const kGULDidReceiveRemoteNotificationSEL =
@"application:didReceiveRemoteNotification:";
static NSString *const kGULDidReceiveRemoteNotificationWithCompletionSEL =
@"application:didReceiveRemoteNotification:fetchCompletionHandler:";
/**
* 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 GULZeroingWeakContainer : NSObject
/** Stores a weak object. */
@property(nonatomic, weak) id object;
@end
@implementation GULZeroingWeakContainer
@end
@interface GULAppDelegateObserver : NSObject
@end
@implementation GULAppDelegateObserver {
BOOL _isObserving;
}
+ (GULAppDelegateObserver *)sharedInstance {
static GULAppDelegateObserver *instance;
static dispatch_once_t once;
dispatch_once(&once, ^{
instance = [[GULAppDelegateObserver alloc] init];
});
return instance;
}
- (void)observeUIApplication {
if (_isObserving) {
return;
}
[[GULAppDelegateSwizzler sharedApplication]
addObserver:self
forKeyPath:kGULAppDelegateKeyPath
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
_isObserving = YES;
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if ([keyPath isEqual:kGULAppDelegateKeyPath]) {
id newValue = change[NSKeyValueChangeNewKey];
id oldValue = change[NSKeyValueChangeOldKey];
if ([newValue isEqual:oldValue]) {
return;
}
// Free the stored app delegate instance because it has been changed to a different instance to
// avoid keeping it alive forever.
if ([oldValue isEqual:gOriginalAppDelegate]) {
gOriginalAppDelegate = nil;
// Remove the observer. Parse it to NSObject to avoid warning.
[[GULAppDelegateSwizzler sharedApplication] removeObserver:self
forKeyPath:kGULAppDelegateKeyPath];
_isObserving = NO;
}
}
}
@end
@implementation GULAppDelegateSwizzler
static dispatch_once_t sProxyAppDelegateOnceToken;
static dispatch_once_t sProxyAppDelegateRemoteNotificationOnceToken;
#pragma mark - Public methods
+ (BOOL)isAppDelegateProxyEnabled {
NSDictionary *infoDictionary = [NSBundle mainBundle].infoDictionary;
id isFirebaseProxyEnabledPlistValue = infoDictionary[kGULFirebaseAppDelegateProxyEnabledPlistKey];
id isGoogleProxyEnabledPlistValue =
infoDictionary[kGULGoogleUtilitiesAppDelegateProxyEnabledPlistKey];
// Enabled by default.
BOOL isFirebaseAppDelegateProxyEnabled = YES;
BOOL isGoogleUtilitiesAppDelegateProxyEnabled = YES;
if ([isFirebaseProxyEnabledPlistValue isKindOfClass:[NSNumber class]]) {
isFirebaseAppDelegateProxyEnabled = [isFirebaseProxyEnabledPlistValue boolValue];
}
if ([isGoogleProxyEnabledPlistValue isKindOfClass:[NSNumber class]]) {
isGoogleUtilitiesAppDelegateProxyEnabled = [isGoogleProxyEnabledPlistValue boolValue];
}
// Only deactivate the proxy if it is explicitly disabled by app developers using either one of
// the plist flags.
return isFirebaseAppDelegateProxyEnabled && isGoogleUtilitiesAppDelegateProxyEnabled;
}
+ (GULAppDelegateInterceptorID)registerAppDelegateInterceptor:
(id<GULApplicationDelegate>)interceptor {
NSAssert(interceptor, @"AppDelegateProxy cannot add nil interceptor");
NSAssert([interceptor conformsToProtocol:@protocol(GULApplicationDelegate)],
@"AppDelegateProxy interceptor does not conform to UIApplicationDelegate");
if (!interceptor) {
GULLogError(kGULLoggerSwizzler, NO,
[NSString stringWithFormat:@"I-SWZ%06ld",
(long)kGULSwizzlerMessageCodeAppDelegateSwizzling000],
@"AppDelegateProxy cannot add nil interceptor.");
return nil;
}
if (![interceptor conformsToProtocol:@protocol(GULApplicationDelegate)]) {
GULLogError(kGULLoggerSwizzler, NO,
[NSString stringWithFormat:@"I-SWZ%06ld",
(long)kGULSwizzlerMessageCodeAppDelegateSwizzling001],
@"AppDelegateProxy interceptor does not conform to UIApplicationDelegate");
return nil;
}
// The ID should be the same given the same interceptor object.
NSString *interceptorID = [NSString stringWithFormat:@"%@%p", kGULAppDelegatePrefix, interceptor];
if (!interceptorID.length) {
GULLogError(kGULLoggerSwizzler, NO,
[NSString stringWithFormat:@"I-SWZ%06ld",
(long)kGULSwizzlerMessageCodeAppDelegateSwizzling002],
@"AppDelegateProxy cannot create Interceptor ID.");
return nil;
}
GULZeroingWeakContainer *weakObject = [[GULZeroingWeakContainer alloc] init];
weakObject.object = interceptor;
[GULAppDelegateSwizzler interceptors][interceptorID] = weakObject;
return interceptorID;
}
+ (void)unregisterAppDelegateInterceptorWithID:(GULAppDelegateInterceptorID)interceptorID {
NSAssert(interceptorID, @"AppDelegateProxy cannot unregister nil interceptor ID.");
NSAssert(((NSString *)interceptorID).length != 0,
@"AppDelegateProxy cannot unregister empty interceptor ID.");
if (!interceptorID) {
GULLogError(kGULLoggerSwizzler, NO,
[NSString stringWithFormat:@"I-SWZ%06ld",
(long)kGULSwizzlerMessageCodeAppDelegateSwizzling003],
@"AppDelegateProxy cannot unregister empty interceptor ID.");
return;
}
GULZeroingWeakContainer *weakContainer = [GULAppDelegateSwizzler interceptors][interceptorID];
if (!weakContainer.object) {
GULLogError(kGULLoggerSwizzler, NO,
[NSString stringWithFormat:@"I-SWZ%06ld",
(long)kGULSwizzlerMessageCodeAppDelegateSwizzling004],
@"AppDelegateProxy cannot unregister interceptor that was not registered. "
"Interceptor ID %@",
interceptorID);
return;
}
[[GULAppDelegateSwizzler interceptors] removeObjectForKey:interceptorID];
}
+ (void)proxyOriginalDelegate {
if ([GULAppEnvironmentUtil isAppExtension]) {
return;
}
dispatch_once(&sProxyAppDelegateOnceToken, ^{
id<GULApplicationDelegate> originalDelegate =
[GULAppDelegateSwizzler sharedApplication].delegate;
[GULAppDelegateSwizzler proxyAppDelegate:originalDelegate];
});
}
+ (void)proxyOriginalDelegateIncludingAPNSMethods {
if ([GULAppEnvironmentUtil isAppExtension]) {
return;
}
[self proxyOriginalDelegate];
dispatch_once(&sProxyAppDelegateRemoteNotificationOnceToken, ^{
id<GULApplicationDelegate> appDelegate = [GULAppDelegateSwizzler sharedApplication].delegate;
NSMutableDictionary *realImplementationsBySelector =
[objc_getAssociatedObject(appDelegate, &kGULRealIMPBySelectorKey) mutableCopy];
[self proxyRemoteNotificationsMethodsWithAppDelegateSubClass:gAppDelegateSubclass
realClass:gOriginalAppDelegateClass
appDelegate:appDelegate
realImplementationsBySelector:realImplementationsBySelector];
objc_setAssociatedObject(appDelegate, &kGULRealIMPBySelectorKey,
[realImplementationsBySelector copy], OBJC_ASSOCIATION_RETAIN);
[self reassignAppDelegate];
});
}
#pragma mark - Create proxy
+ (GULApplication *)sharedApplication {
if ([GULAppEnvironmentUtil isAppExtension]) {
return nil;
}
id sharedApplication = nil;
Class uiApplicationClass = NSClassFromString(kGULApplicationClassName);
if (uiApplicationClass &&
[uiApplicationClass respondsToSelector:(NSSelectorFromString(@"sharedApplication"))]) {
sharedApplication = [uiApplicationClass sharedApplication];
}
return sharedApplication;
}
#pragma mark - Override default methods
/** Creates a new subclass of the class of the given object and sets the isa value of the given
* object to the new subclass. Additionally this copies methods to that new subclass that allow us
* to intercept UIApplicationDelegate methods. This is better known as isa swizzling.
*
* @param appDelegate The object to which you want to isa swizzle. This has to conform to the
* UIApplicationDelegate subclass.
* @return Returns the new subclass.
*/
+ (nullable Class)createSubclassWithObject:(id<GULApplicationDelegate>)appDelegate {
Class realClass = [appDelegate class];
// Create GUL_<RealAppDelegate>_<UUID>
NSString *classNameWithPrefix =
[kGULAppDelegatePrefix stringByAppendingString:NSStringFromClass(realClass)];
NSString *newClassName =
[NSString stringWithFormat:@"%@-%@", classNameWithPrefix, [NSUUID UUID].UUIDString];
if (NSClassFromString(newClassName)) {
GULLogError(kGULLoggerSwizzler, NO,
[NSString stringWithFormat:@"I-SWZ%06ld",
(long)kGULSwizzlerMessageCodeAppDelegateSwizzling005],
@"Cannot create a proxy for App Delegate. Subclass already exists. Original Class: "
@"%@, subclass: %@",
NSStringFromClass(realClass), newClassName);
return nil;
}
// Register the new class as subclass of the real one. Do not allocate more than the real class
// size.
Class appDelegateSubClass = objc_allocateClassPair(realClass, newClassName.UTF8String, 0);
if (appDelegateSubClass == Nil) {
GULLogError(kGULLoggerSwizzler, NO,
[NSString stringWithFormat:@"I-SWZ%06ld",
(long)kGULSwizzlerMessageCodeAppDelegateSwizzling006],
@"Cannot create a proxy for App Delegate. Subclass already exists. Original Class: "
@"%@, subclass: Nil",
NSStringFromClass(realClass));
return nil;
}
NSMutableDictionary<NSString *, NSValue *> *realImplementationsBySelector =
[[NSMutableDictionary alloc] init];
// For application:continueUserActivity:restorationHandler:
SEL continueUserActivitySEL = @selector(application:continueUserActivity:restorationHandler:);
[self proxyDestinationSelector:continueUserActivitySEL
implementationsFromSourceSelector:continueUserActivitySEL
fromClass:[GULAppDelegateSwizzler class]
toClass:appDelegateSubClass
realClass:realClass
storeDestinationImplementationTo:realImplementationsBySelector];
#if TARGET_OS_IOS || TARGET_OS_TV
// Add the following methods from GULAppDelegate class, and store the real implementation so it
// can forward to the real one.
// For application:openURL:options:
SEL applicationOpenURLOptionsSEL = @selector(application:openURL:options:);
if ([appDelegate respondsToSelector:applicationOpenURLOptionsSEL]) {
// Only add the application:openURL:options: method if the original AppDelegate implements it.
// This fixes a bug if an app only implements application:openURL:sourceApplication:annotation:
// (if we add the `options` method, iOS sees that one exists and does not call the
// `sourceApplication` method, which in this case is the only one the app implements).
[self proxyDestinationSelector:applicationOpenURLOptionsSEL
implementationsFromSourceSelector:applicationOpenURLOptionsSEL
fromClass:[GULAppDelegateSwizzler class]
toClass:appDelegateSubClass
realClass:realClass
storeDestinationImplementationTo:realImplementationsBySelector];
}
// For application:handleEventsForBackgroundURLSession:completionHandler:
SEL handleEventsForBackgroundURLSessionSEL = @selector(application:
handleEventsForBackgroundURLSession:completionHandler:);
[self proxyDestinationSelector:handleEventsForBackgroundURLSessionSEL
implementationsFromSourceSelector:handleEventsForBackgroundURLSessionSEL
fromClass:[GULAppDelegateSwizzler class]
toClass:appDelegateSubClass
realClass:realClass
storeDestinationImplementationTo:realImplementationsBySelector];
#endif // TARGET_OS_IOS || TARGET_OS_TV
#if TARGET_OS_IOS
// For application:openURL:sourceApplication:annotation:
SEL openURLSourceApplicationAnnotationSEL = @selector(application:
openURL:sourceApplication:annotation:);
[self proxyDestinationSelector:openURLSourceApplicationAnnotationSEL
implementationsFromSourceSelector:openURLSourceApplicationAnnotationSEL
fromClass:[GULAppDelegateSwizzler class]
toClass:appDelegateSubClass
realClass:realClass
storeDestinationImplementationTo:realImplementationsBySelector];
#endif // TARGET_OS_IOS
// Override the description too so the custom class name will not show up.
[GULAppDelegateSwizzler addInstanceMethodWithDestinationSelector:@selector(description)
withImplementationFromSourceSelector:@selector(fakeDescription)
fromClass:[self class]
toClass:appDelegateSubClass];
// Store original implementations to a fake property of the original delegate.
objc_setAssociatedObject(appDelegate, &kGULRealIMPBySelectorKey,
[realImplementationsBySelector copy], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(appDelegate, &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(appDelegateSubClass)) {
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. %@",
NSStringFromClass(realClass));
NSAssert(NO, @"Classes must be the same size to swizzle isa");
return nil;
}
// Make the newly created class to be the subclass of the real App Delegate class.
objc_registerClassPair(appDelegateSubClass);
if (object_setClass(appDelegate, appDelegateSubClass)) {
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]);
}
return appDelegateSubClass;
}
+ (void)proxyRemoteNotificationsMethodsWithAppDelegateSubClass:(Class)appDelegateSubClass
realClass:(Class)realClass
appDelegate:(id)appDelegate
realImplementationsBySelector:
(NSMutableDictionary *)realImplementationsBySelector {
if (realClass == nil || appDelegateSubClass == nil || appDelegate == nil ||
realImplementationsBySelector == nil) {
// The App Delegate has not been swizzled.
return;
}
// For application:didRegisterForRemoteNotificationsWithDeviceToken:
SEL didRegisterForRemoteNotificationsSEL =
NSSelectorFromString(kGULDidRegisterForRemoteNotificationsSEL);
SEL didRegisterForRemoteNotificationsDonorSEL = @selector(application:
donor_didRegisterForRemoteNotificationsWithDeviceToken:);
[self proxyDestinationSelector:didRegisterForRemoteNotificationsSEL
implementationsFromSourceSelector:didRegisterForRemoteNotificationsDonorSEL
fromClass:[GULAppDelegateSwizzler class]
toClass:appDelegateSubClass
realClass:realClass
storeDestinationImplementationTo:realImplementationsBySelector];
// For application:didFailToRegisterForRemoteNotificationsWithError:
SEL didFailToRegisterForRemoteNotificationsSEL =
NSSelectorFromString(kGULDidFailToRegisterForRemoteNotificationsSEL);
SEL didFailToRegisterForRemoteNotificationsDonorSEL = @selector(application:
donor_didFailToRegisterForRemoteNotificationsWithError:);
[self proxyDestinationSelector:didFailToRegisterForRemoteNotificationsSEL
implementationsFromSourceSelector:didFailToRegisterForRemoteNotificationsDonorSEL
fromClass:[GULAppDelegateSwizzler class]
toClass:appDelegateSubClass
realClass:realClass
storeDestinationImplementationTo:realImplementationsBySelector];
// For application:didReceiveRemoteNotification:
SEL didReceiveRemoteNotificationSEL = NSSelectorFromString(kGULDidReceiveRemoteNotificationSEL);
SEL didReceiveRemoteNotificationDonorSEL = @selector(application:
donor_didReceiveRemoteNotification:);
[self proxyDestinationSelector:didReceiveRemoteNotificationSEL
implementationsFromSourceSelector:didReceiveRemoteNotificationDonorSEL
fromClass:[GULAppDelegateSwizzler class]
toClass:appDelegateSubClass
realClass:realClass
storeDestinationImplementationTo:realImplementationsBySelector];
// For application:didReceiveRemoteNotification:fetchCompletionHandler:
#if !TARGET_OS_WATCH && !TARGET_OS_OSX
SEL didReceiveRemoteNotificationWithCompletionSEL =
NSSelectorFromString(kGULDidReceiveRemoteNotificationWithCompletionSEL);
SEL didReceiveRemoteNotificationWithCompletionDonorSEL =
@selector(application:donor_didReceiveRemoteNotification:fetchCompletionHandler:);
if ([appDelegate respondsToSelector:didReceiveRemoteNotificationWithCompletionSEL]) {
// Only add the application:didReceiveRemoteNotification:fetchCompletionHandler: method if
// the original AppDelegate implements it.
// This fixes a bug if an app only implements application:didReceiveRemoteNotification:
// (if we add the method with completion, iOS sees that one exists and does not call
// the method without the completion, which in this case is the only one the app implements).
[self proxyDestinationSelector:didReceiveRemoteNotificationWithCompletionSEL
implementationsFromSourceSelector:didReceiveRemoteNotificationWithCompletionDonorSEL
fromClass:[GULAppDelegateSwizzler class]
toClass:appDelegateSubClass
realClass:realClass
storeDestinationImplementationTo:realImplementationsBySelector];
}
#endif // !TARGET_OS_WATCH && !TARGET_OS_OSX
}
/// We have to do this to invalidate the cache that caches the original respondsToSelector of
/// openURL handlers. Without this, it won't call the default implementations because the system
/// checks and caches them.
/// Register KVO only once. Otherwise, the observing method will be called as many times as
/// being registered.
+ (void)reassignAppDelegate {
#if !TARGET_OS_WATCH
id<GULApplicationDelegate> delegate = [self sharedApplication].delegate;
[self sharedApplication].delegate = nil;
[self sharedApplication].delegate = delegate;
gOriginalAppDelegate = delegate;
[[GULAppDelegateObserver sharedInstance] observeUIApplication];
#endif
}
#pragma mark - Helper methods
+ (GULMutableDictionary *)interceptors {
static dispatch_once_t onceToken;
static GULMutableDictionary *sInterceptors;
dispatch_once(&onceToken, ^{
sInterceptors = [[GULMutableDictionary alloc] init];
});
return sInterceptors;
}
+ (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 =
[GULAppDelegateSwizzler 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)kGULSwizzlerMessageCodeAppDelegateSwizzling009],
@"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
* GULAppDelegateInterceptorCallback with the interceptor.
*
* @param methodSelector The SEL to check if an interceptor responds to.
* @param callback the GULAppDelegateInterceptorCallback.
*/
+ (void)notifyInterceptorsWithMethodSelector:(SEL)methodSelector
callback:(GULAppDelegateInterceptorCallback)callback {
if (!callback) {
return;
}
NSDictionary *interceptors = [GULAppDelegateSwizzler interceptors].dictionary;
[interceptors enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
GULZeroingWeakContainer *interceptorContainer = obj;
id interceptor = interceptorContainer.object;
if (!interceptor) {
GULLogWarning(
kGULLoggerSwizzler, NO,
[NSString
stringWithFormat:@"I-SWZ%06ld", (long)kGULSwizzlerMessageCodeAppDelegateSwizzling010],
@"AppDelegateProxy cannot find interceptor with ID %@. Removing the interceptor.", key);
[[GULAppDelegateSwizzler interceptors] removeObjectForKey:key];
return;
}
if ([interceptor respondsToSelector:methodSelector]) {
callback(interceptor);
}
}];
}
// 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.
#pragma mark - [Donor Methods] Overridden instance description method
- (NSString *)fakeDescription {
Class realClass = objc_getAssociatedObject(self, &kGULRealClassKey);
return [NSString stringWithFormat:@"<%@: %p>", realClass, self];
}
#pragma mark - [Donor Methods] URL overridden handler methods
#if TARGET_OS_IOS || TARGET_OS_TV
- (BOOL)application:(GULApplication *)application
openURL:(NSURL *)url
options:(NSDictionary<NSString *, id> *)options {
SEL methodSelector = @selector(application:openURL:options:);
// Call the real implementation if the real App Delegate has any.
NSValue *openURLIMPPointer =
[GULAppDelegateSwizzler originalImplementationForSelector:methodSelector object:self];
GULRealOpenURLOptionsIMP openURLOptionsIMP = [openURLIMPPointer pointerValue];
__block BOOL returnedValue = NO;
// This is needed to for the library to be warning free on iOS versions < 9.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
[GULAppDelegateSwizzler
notifyInterceptorsWithMethodSelector:methodSelector
callback:^(id<GULApplicationDelegate> interceptor) {
returnedValue |= [interceptor application:application
openURL:url
options:options];
}];
#pragma clang diagnostic pop
if (openURLOptionsIMP) {
returnedValue |= openURLOptionsIMP(self, methodSelector, application, url, options);
}
return returnedValue;
}
#endif // TARGET_OS_IOS || TARGET_OS_TV
// TODO(Xcode 15): When Xcode 15 is the minimum supported Xcode version,
// it will be unnecessary to check if `TARGET_OS_VISION` is defined.
#if TARGET_OS_IOS && (!defined(TARGET_OS_VISION) || !TARGET_OS_VISION)
- (BOOL)application:(GULApplication *)application
openURL:(NSURL *)url
sourceApplication:(NSString *)sourceApplication
annotation:(id)annotation {
SEL methodSelector = @selector(application:openURL:sourceApplication:annotation:);
// Call the real implementation if the real App Delegate has any.
NSValue *openURLSourceAppAnnotationIMPPointer =
[GULAppDelegateSwizzler originalImplementationForSelector:methodSelector object:self];
GULRealOpenURLSourceApplicationAnnotationIMP openURLSourceApplicationAnnotationIMP =
[openURLSourceAppAnnotationIMPPointer pointerValue];
__block BOOL returnedValue = NO;
[GULAppDelegateSwizzler
notifyInterceptorsWithMethodSelector:methodSelector
callback:^(id<GULApplicationDelegate> interceptor) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
returnedValue |= [interceptor application:application
openURL:url
sourceApplication:sourceApplication
annotation:annotation];
#pragma clang diagnostic pop
}];
if (openURLSourceApplicationAnnotationIMP) {
returnedValue |= openURLSourceApplicationAnnotationIMP(self, methodSelector, application, url,
sourceApplication, annotation);
}
return returnedValue;
}
#endif // TARGET_OS_IOS && (!defined(TARGET_OS_VISION) || !TARGET_OS_VISION)
#pragma mark - [Donor Methods] Network overridden handler methods
#if TARGET_OS_IOS || TARGET_OS_TV
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wstrict-prototypes"
- (void)application:(GULApplication *)application
handleEventsForBackgroundURLSession:(NSString *)identifier
completionHandler:(void (^)())completionHandler API_AVAILABLE(ios(7.0)) {
#pragma clang diagnostic pop
SEL methodSelector = @selector(application:
handleEventsForBackgroundURLSession:completionHandler:);
NSValue *handleBackgroundSessionPointer =
[GULAppDelegateSwizzler originalImplementationForSelector:methodSelector object:self];
GULRealHandleEventsForBackgroundURLSessionIMP handleBackgroundSessionIMP =
[handleBackgroundSessionPointer pointerValue];
// Notify interceptors.
[GULAppDelegateSwizzler
notifyInterceptorsWithMethodSelector:methodSelector
callback:^(id<GULApplicationDelegate> interceptor) {
[interceptor application:application
handleEventsForBackgroundURLSession:identifier
completionHandler:completionHandler];
}];
// Call the real implementation if the real App Delegate has any.
if (handleBackgroundSessionIMP) {
handleBackgroundSessionIMP(self, methodSelector, application, identifier, completionHandler);
}
}
#endif // TARGET_OS_IOS || TARGET_OS_TV
#pragma mark - [Donor Methods] User Activities overridden handler methods
- (BOOL)application:(GULApplication *)application
continueUserActivity:(NSUserActivity *)userActivity
restorationHandler:(void (^)(NSArray *restorableObjects))restorationHandler {
SEL methodSelector = @selector(application:continueUserActivity:restorationHandler:);
NSValue *continueUserActivityIMPPointer =
[GULAppDelegateSwizzler originalImplementationForSelector:methodSelector object:self];
GULRealContinueUserActivityIMP continueUserActivityIMP =
continueUserActivityIMPPointer.pointerValue;
__block BOOL returnedValue = NO;
#if !TARGET_OS_WATCH
[GULAppDelegateSwizzler
notifyInterceptorsWithMethodSelector:methodSelector
callback:^(id<GULApplicationDelegate> interceptor) {
returnedValue |= [interceptor application:application
continueUserActivity:userActivity
restorationHandler:restorationHandler];
}];
#endif
// Call the real implementation if the real App Delegate has any.
if (continueUserActivityIMP) {
returnedValue |= continueUserActivityIMP(self, methodSelector, application, userActivity,
restorationHandler);
}
return returnedValue;
}
#pragma mark - [Donor Methods] Remote Notifications
- (void)application:(GULApplication *)application
donor_didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
SEL methodSelector = NSSelectorFromString(kGULDidRegisterForRemoteNotificationsSEL);
NSValue *didRegisterForRemoteNotificationsIMPPointer =
[GULAppDelegateSwizzler originalImplementationForSelector:methodSelector object:self];
GULRealDidRegisterForRemoteNotificationsIMP didRegisterForRemoteNotificationsIMP =
[didRegisterForRemoteNotificationsIMPPointer pointerValue];
// Notify interceptors.
[GULAppDelegateSwizzler
notifyInterceptorsWithMethodSelector:methodSelector
callback:^(id<GULApplicationDelegate> interceptor) {
NSInvocation *invocation = [GULAppDelegateSwizzler
appDelegateInvocationForSelector:methodSelector];
[invocation setTarget:interceptor];
[invocation setSelector:methodSelector];
[invocation setArgument:(void *)(&application) atIndex:2];
[invocation setArgument:(void *)(&deviceToken) atIndex:3];
[invocation invoke];
}];
// Call the real implementation if the real App Delegate has any.
if (didRegisterForRemoteNotificationsIMP) {
didRegisterForRemoteNotificationsIMP(self, methodSelector, application, deviceToken);
}
}
- (void)application:(GULApplication *)application
donor_didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
SEL methodSelector = NSSelectorFromString(kGULDidFailToRegisterForRemoteNotificationsSEL);
NSValue *didFailToRegisterForRemoteNotificationsIMPPointer =
[GULAppDelegateSwizzler originalImplementationForSelector:methodSelector object:self];
GULRealDidFailToRegisterForRemoteNotificationsIMP didFailToRegisterForRemoteNotificationsIMP =
[didFailToRegisterForRemoteNotificationsIMPPointer pointerValue];
// Notify interceptors.
[GULAppDelegateSwizzler
notifyInterceptorsWithMethodSelector:methodSelector
callback:^(id<GULApplicationDelegate> interceptor) {
NSInvocation *invocation = [GULAppDelegateSwizzler
appDelegateInvocationForSelector:methodSelector];
[invocation setTarget:interceptor];
[invocation setSelector:methodSelector];
[invocation setArgument:(void *)(&application) atIndex:2];
[invocation setArgument:(void *)(&error) atIndex:3];
[invocation invoke];
}];
// Call the real implementation if the real App Delegate has any.
if (didFailToRegisterForRemoteNotificationsIMP) {
didFailToRegisterForRemoteNotificationsIMP(self, methodSelector, application, error);
}
}
#if !TARGET_OS_WATCH && !TARGET_OS_OSX
- (void)application:(GULApplication *)application
donor_didReceiveRemoteNotification:(NSDictionary *)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
SEL methodSelector = NSSelectorFromString(kGULDidReceiveRemoteNotificationWithCompletionSEL);
NSValue *didReceiveRemoteNotificationWithCompletionIMPPointer =
[GULAppDelegateSwizzler originalImplementationForSelector:methodSelector object:self];
GULRealDidReceiveRemoteNotificationWithCompletionIMP
didReceiveRemoteNotificationWithCompletionIMP =
[didReceiveRemoteNotificationWithCompletionIMPPointer pointerValue];
dispatch_group_t __block callbackGroup = dispatch_group_create();
NSMutableArray<NSNumber *> *__block fetchResults = [NSMutableArray array];
void (^localCompletionHandler)(UIBackgroundFetchResult) =
^void(UIBackgroundFetchResult fetchResult) {
[fetchResults addObject:[NSNumber numberWithInt:(int)fetchResult]];
dispatch_group_leave(callbackGroup);
};
// Notify interceptors.
[GULAppDelegateSwizzler
notifyInterceptorsWithMethodSelector:methodSelector
callback:^(id<GULApplicationDelegate> interceptor) {
dispatch_group_enter(callbackGroup);
NSInvocation *invocation = [GULAppDelegateSwizzler
appDelegateInvocationForSelector:methodSelector];
[invocation setTarget:interceptor];
[invocation setSelector:methodSelector];
[invocation setArgument:(void *)(&application) atIndex:2];
[invocation setArgument:(void *)(&userInfo) atIndex:3];
[invocation setArgument:(void *)(&localCompletionHandler)
atIndex:4];
[invocation invoke];
}];
// Call the real implementation if the real App Delegate has any.
if (didReceiveRemoteNotificationWithCompletionIMP) {
dispatch_group_enter(callbackGroup);
didReceiveRemoteNotificationWithCompletionIMP(self, methodSelector, application, userInfo,
localCompletionHandler);
}
dispatch_group_notify(callbackGroup, dispatch_get_main_queue(), ^() {
BOOL allFetchesFailed = YES;
BOOL anyFetchHasNewData = NO;
for (NSNumber *oneResult in fetchResults) {
UIBackgroundFetchResult result = oneResult.intValue;
switch (result) {
case UIBackgroundFetchResultNoData:
allFetchesFailed = NO;
break;
case UIBackgroundFetchResultNewData:
allFetchesFailed = NO;
anyFetchHasNewData = YES;
break;
case UIBackgroundFetchResultFailed:
break;
}
}
UIBackgroundFetchResult finalFetchResult = UIBackgroundFetchResultNoData;
if (allFetchesFailed) {
finalFetchResult = UIBackgroundFetchResultFailed;
} else if (anyFetchHasNewData) {
finalFetchResult = UIBackgroundFetchResultNewData;
} else {
finalFetchResult = UIBackgroundFetchResultNoData;
}
completionHandler(finalFetchResult);
});
}
#endif // !TARGET_OS_WATCH && !TARGET_OS_OSX
- (void)application:(GULApplication *)application
donor_didReceiveRemoteNotification:(NSDictionary *)userInfo {
SEL methodSelector = NSSelectorFromString(kGULDidReceiveRemoteNotificationSEL);
NSValue *didReceiveRemoteNotificationIMPPointer =
[GULAppDelegateSwizzler originalImplementationForSelector:methodSelector object:self];
GULRealDidReceiveRemoteNotificationIMP didReceiveRemoteNotificationIMP =
[didReceiveRemoteNotificationIMPPointer pointerValue];
// Notify interceptors.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[GULAppDelegateSwizzler
notifyInterceptorsWithMethodSelector:methodSelector
callback:^(id<GULApplicationDelegate> interceptor) {
NSInvocation *invocation = [GULAppDelegateSwizzler
appDelegateInvocationForSelector:methodSelector];
[invocation setTarget:interceptor];
[invocation setSelector:methodSelector];
[invocation setArgument:(void *)(&application) atIndex:2];
[invocation setArgument:(void *)(&userInfo) atIndex:3];
[invocation invoke];
}];
#pragma clang diagnostic pop
// Call the real implementation if the real App Delegate has any.
if (didReceiveRemoteNotificationIMP) {
didReceiveRemoteNotificationIMP(self, methodSelector, application, userInfo);
}
}
+ (nullable NSInvocation *)appDelegateInvocationForSelector:(SEL)selector {
struct objc_method_description methodDescription =
protocol_getMethodDescription(@protocol(GULApplicationDelegate), selector, NO, YES);
if (methodDescription.types == NULL) {
return nil;
}
NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:methodDescription.types];
return [NSInvocation invocationWithMethodSignature:signature];
}
+ (void)proxyAppDelegate:(id<GULApplicationDelegate>)appDelegate {
if (![appDelegate conformsToProtocol:@protocol(GULApplicationDelegate)]) {
GULLogNotice(
kGULLoggerSwizzler, NO,
[NSString
stringWithFormat:@"I-SWZ%06ld",
(long)kGULSwizzlerMessageCodeAppDelegateSwizzlingInvalidAppDelegate],
@"App Delegate does not conform to UIApplicationDelegate protocol. %@",
[GULAppDelegateSwizzler correctAlternativeWhenAppDelegateProxyNotCreated]);
return;
}
id<GULApplicationDelegate> originalDelegate = appDelegate;
// Do not create a subclass if it is not enabled.
if (![GULAppDelegateSwizzler isAppDelegateProxyEnabled]) {
GULLogNotice(kGULLoggerSwizzler, NO,
[NSString stringWithFormat:@"I-SWZ%06ld",
(long)kGULSwizzlerMessageCodeAppDelegateSwizzling011],
@"App Delegate Proxy is disabled. %@",
[GULAppDelegateSwizzler correctAlternativeWhenAppDelegateProxyNotCreated]);
return;
}
// Do not accept nil delegate.
if (!originalDelegate) {
GULLogError(kGULLoggerSwizzler, NO,
[NSString stringWithFormat:@"I-SWZ%06ld",
(long)kGULSwizzlerMessageCodeAppDelegateSwizzling012],
@"Cannot create App Delegate Proxy because App Delegate instance is nil. %@",
[GULAppDelegateSwizzler correctAlternativeWhenAppDelegateProxyNotCreated]);
return;
}
@try {
gOriginalAppDelegateClass = [originalDelegate class];
gAppDelegateSubclass = [self createSubclassWithObject:originalDelegate];
[self reassignAppDelegate];
} @catch (NSException *exception) {
GULLogError(kGULLoggerSwizzler, NO,
[NSString stringWithFormat:@"I-SWZ%06ld",
(long)kGULSwizzlerMessageCodeAppDelegateSwizzling013],
@"Cannot create App Delegate Proxy. %@",
[GULAppDelegateSwizzler correctAlternativeWhenAppDelegateProxyNotCreated]);
return;
}
}
#pragma mark - Methods to print correct debug logs
+ (NSString *)correctAppDelegateProxyKey {
return NSClassFromString(@"FIRCore") ? kGULFirebaseAppDelegateProxyEnabledPlistKey
: kGULGoogleUtilitiesAppDelegateProxyEnabledPlistKey;
}
+ (NSString *)correctAlternativeWhenAppDelegateProxyNotCreated {
return NSClassFromString(@"FIRCore")
? @"To log deep link campaigns manually, call the methods in "
@"FIRAnalytics+AppDelegate.h."
: @"";
}
#pragma mark - Private Methods for Testing
+ (void)clearInterceptors {
[[self interceptors] removeAllObjects];
}
+ (void)resetProxyOriginalDelegateOnceToken {
sProxyAppDelegateOnceToken = 0;
sProxyAppDelegateRemoteNotificationOnceToken = 0;
}
+ (id<GULApplicationDelegate>)originalDelegate {
return gOriginalAppDelegate;
}
@end

View File

@@ -0,0 +1,439 @@
// 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/Public/GoogleUtilities/GULSceneDelegateSwizzler.h"
#import "GoogleUtilities/AppDelegateSwizzler/Internal/GULSceneDelegateSwizzler_Private.h"
#import "GoogleUtilities/AppDelegateSwizzler/Public/GoogleUtilities/GULAppDelegateSwizzler.h"
#import "GoogleUtilities/Common/GULLoggerCodes.h"
#import "GoogleUtilities/Environment/Public/GoogleUtilities/GULAppEnvironmentUtil.h"
#import "GoogleUtilities/Logger/Public/GoogleUtilities/GULLogger.h"
#import "GoogleUtilities/Network/Public/GoogleUtilities/GULMutableDictionary.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

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2018 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 <Foundation/Foundation.h>
#import "GoogleUtilities/AppDelegateSwizzler/Public/GoogleUtilities/GULAppDelegateSwizzler.h"
#import "GoogleUtilities/Network/Public/GoogleUtilities/GULMutableDictionary.h"
@class GULApplication;
NS_ASSUME_NONNULL_BEGIN
@interface GULAppDelegateSwizzler ()
/** ISA Swizzles the given appDelegate as the original app delegate would be.
*
* @param appDelegate The object that needs to be isa swizzled. This should conform to the
* application delegate protocol.
*/
+ (void)proxyAppDelegate:(id<GULApplicationDelegate>)appDelegate;
/** Returns a dictionary containing interceptor IDs mapped to a GULZeroingWeakContainer.
*
* @return A dictionary of the form {NSString : GULZeroingWeakContainer}, where the NSString is
* the interceptorID.
*/
+ (GULMutableDictionary *)interceptors;
/** Deletes all the registered interceptors. */
+ (void)clearInterceptors;
/** Resets the token that prevents the app delegate proxy from being isa swizzled multiple times. */
+ (void)resetProxyOriginalDelegateOnceToken;
/** Returns the original app delegate that was proxied.
*
* @return The original app delegate instance that was proxied.
*/
+ (id<GULApplicationDelegate>)originalDelegate;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,48 @@
/*
* 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 <Foundation/Foundation.h>
#import "GoogleUtilities/AppDelegateSwizzler/Public/GoogleUtilities/GULSceneDelegateSwizzler.h"
#import "GoogleUtilities/Network/Public/GoogleUtilities/GULMutableDictionary.h"
NS_ASSUME_NONNULL_BEGIN
@interface GULSceneDelegateSwizzler ()
#if UISCENE_SUPPORTED
/** Returns a dictionary containing interceptor IDs mapped to a GULZeroingWeakContainer.
*
* @return A dictionary of the form {NSString : GULZeroingWeakContainer}, where the NSString is
* the interceptorID.
*/
+ (GULMutableDictionary *)interceptors;
/** Deletes all the registered interceptors. */
+ (void)clearInterceptors;
/** 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)proxySceneDelegateIfNeeded:(UIScene *)scene API_AVAILABLE(ios(13.0), tvos(13.0));
#endif // UISCENE_SUPPORTED
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,107 @@
/*
* Copyright 2018 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 <Foundation/Foundation.h>
#import "GULApplication.h"
NS_ASSUME_NONNULL_BEGIN
typedef NSString *const GULAppDelegateInterceptorID;
/** This class contains methods that isa swizzle the app delegate. */
@interface GULAppDelegateSwizzler : NSProxy
/** Registers an app delegate interceptor whose methods will be invoked as they're invoked on the
* original app delegate.
*
* @param interceptor An instance of a class that conforms to the application delegate protocol.
* The interceptor is NOT retained.
* @return A unique GULAppDelegateInterceptorID if interceptor was successfully registered; nil
* if it fails.
*/
+ (nullable GULAppDelegateInterceptorID)registerAppDelegateInterceptor:
(id<GULApplicationDelegate>)interceptor;
/** Unregisters an interceptor with the given ID if it exists.
*
* @param interceptorID The object that was generated when the interceptor was registered.
*/
+ (void)unregisterAppDelegateInterceptorWithID:(GULAppDelegateInterceptorID)interceptorID;
/** This method ensures that the original app delegate has been proxied. Call this before
* registering your interceptor. This method is safe to call multiple times (but it only proxies
* the app delegate once).
*
* This method doesn't proxy APNS related methods:
* @code
* - application:didRegisterForRemoteNotificationsWithDeviceToken:
* - application:didFailToRegisterForRemoteNotificationsWithError:
* - application:didReceiveRemoteNotification:fetchCompletionHandler:
* - application:didReceiveRemoteNotification:
* @endcode
*
* To proxy these methods use +[GULAppDelegateSwizzler
* proxyOriginalDelegateIncludingAPNSMethods]. The methods have to be proxied separately to
* avoid potential warnings from Apple review about missing Push Notification Entitlement (e.g.
* https://github.com/firebase/firebase-ios-sdk/issues/2807)
*
* The method has no effect for extensions.
*
* @see proxyOriginalDelegateIncludingAPNSMethods
*/
+ (void)proxyOriginalDelegate;
/** This method ensures that the original app delegate has been proxied including APNS related
* methods. Call this before registering your interceptor. This method is safe to call multiple
* times (but it only proxies the app delegate once) or
* after +[GULAppDelegateSwizzler proxyOriginalDelegate]
*
* This method calls +[GULAppDelegateSwizzler proxyOriginalDelegate] under the hood.
* After calling this method the following App Delegate methods will be proxied in addition to
* the methods proxied by proxyOriginalDelegate:
* @code
* - application:didRegisterForRemoteNotificationsWithDeviceToken:
* - application:didFailToRegisterForRemoteNotificationsWithError:
* - application:didReceiveRemoteNotification:fetchCompletionHandler:
* - application:didReceiveRemoteNotification:
* @endcode
*
* The method has no effect for extensions.
*
* @see proxyOriginalDelegate
*/
+ (void)proxyOriginalDelegateIncludingAPNSMethods;
/** Indicates whether app delegate proxy is explicitly disabled or enabled. Enabled by default.
*
* @return YES if AppDelegateProxy is Enabled, NO otherwise.
*/
+ (BOOL)isAppDelegateProxyEnabled;
/** Returns the current sharedApplication.
*
* @return the current application instance if in an app, or nil if in extension or if it doesn't
* exist.
*/
+ (nullable GULApplication *)sharedApplication;
/** Do not initialize this class. */
- (instancetype)init NS_UNAVAILABLE;
NS_ASSUME_NONNULL_END
@end

View File

@@ -0,0 +1,50 @@
/*
* 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 <Foundation/Foundation.h>
#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION)
#import <UIKit/UIKit.h>
#define GULApplication UIApplication
#define GULApplicationDelegate UIApplicationDelegate
#define GULUserActivityRestoring UIUserActivityRestoring
static NSString *const kGULApplicationClassName = @"UIApplication";
#elif TARGET_OS_OSX
#import <AppKit/AppKit.h>
#define GULApplication NSApplication
#define GULApplicationDelegate NSApplicationDelegate
#define GULUserActivityRestoring NSUserActivityRestoring
static NSString *const kGULApplicationClassName = @"NSApplication";
#elif TARGET_OS_WATCH
#import <WatchKit/WatchKit.h>
// We match the according watchOS API but swizzling should not work in watch
#define GULApplication WKExtension
#define GULApplicationDelegate WKExtensionDelegate
#define GULUserActivityRestoring NSUserActivityRestoring
static NSString *const kGULApplicationClassName = @"WKExtension";
#endif

View File

@@ -0,0 +1,76 @@
/*
* 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 <Foundation/Foundation.h>
#import <TargetConditionals.h>
#if !TARGET_OS_OSX
#import <UIKit/UIKit.h>
#endif // !TARGET_OS_OSX
#if ((TARGET_OS_IOS || TARGET_OS_TV) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= 130000))
#define UISCENE_SUPPORTED 1
#endif
NS_ASSUME_NONNULL_BEGIN
typedef NSString *const GULSceneDelegateInterceptorID;
/** This class contains methods that isa swizzle the scene delegate. */
@interface GULSceneDelegateSwizzler : NSProxy
#if UISCENE_SUPPORTED
/** Registers a scene delegate interceptor whose methods will be invoked as they're invoked on the
* original scene delegate.
*
* @param interceptor An instance of a class that conforms to the application delegate protocol.
* The interceptor is NOT retained.
* @return A unique GULSceneDelegateInterceptorID if interceptor was successfully registered; nil
* if it fails.
*/
+ (nullable GULSceneDelegateInterceptorID)registerSceneDelegateInterceptor:
(id<UISceneDelegate>)interceptor API_AVAILABLE(ios(13.0), tvos(13.0));
/** Unregisters an interceptor with the given ID if it exists.
*
* @param interceptorID The object that was generated when the interceptor was registered.
*/
+ (void)unregisterSceneDelegateInterceptorWithID:(GULSceneDelegateInterceptorID)interceptorID
API_AVAILABLE(ios(13.0), tvos(13.0));
/** Do not initialize this class. */
- (instancetype)init NS_UNAVAILABLE;
#endif // UISCENE_SUPPORTED
/** This method ensures that the original scene delegate has been proxied. Call this before
* registering your interceptor. This method is safe to call multiple times (but it only proxies
* the scene delegate once).
*
* The method has no effect for extensions.
*/
+ (void)proxyOriginalSceneDelegate;
/** Indicates whether scene delegate proxy is explicitly disabled or enabled. Enabled by default.
*
* @return YES if SceneDelegateProxy is Enabled, NO otherwise.
*/
+ (BOOL)isSceneDelegateProxyEnabled;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2018 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 <Foundation/Foundation.h>
typedef NS_ENUM(NSInteger, GULSwizzlerMessageCode) {
// App Delegate Swizzling.
kGULSwizzlerMessageCodeAppDelegateSwizzling000 = 1000, // I-SWZ001000
kGULSwizzlerMessageCodeAppDelegateSwizzling001 = 1001, // I-SWZ001001
kGULSwizzlerMessageCodeAppDelegateSwizzling002 = 1002, // I-SWZ001002
kGULSwizzlerMessageCodeAppDelegateSwizzling003 = 1003, // I-SWZ001003
kGULSwizzlerMessageCodeAppDelegateSwizzling004 = 1004, // I-SWZ001004
kGULSwizzlerMessageCodeAppDelegateSwizzling005 = 1005, // I-SWZ001005
kGULSwizzlerMessageCodeAppDelegateSwizzling006 = 1006, // I-SWZ001006
kGULSwizzlerMessageCodeAppDelegateSwizzling007 = 1007, // I-SWZ001007
kGULSwizzlerMessageCodeAppDelegateSwizzling008 = 1008, // I-SWZ001008
kGULSwizzlerMessageCodeAppDelegateSwizzling009 = 1009, // I-SWZ001009
kGULSwizzlerMessageCodeAppDelegateSwizzling010 = 1010, // I-SWZ001010
kGULSwizzlerMessageCodeAppDelegateSwizzling011 = 1011, // I-SWZ001011
kGULSwizzlerMessageCodeAppDelegateSwizzling012 = 1012, // I-SWZ001012
kGULSwizzlerMessageCodeAppDelegateSwizzling013 = 1013, // I-SWZ001013
kGULSwizzlerMessageCodeAppDelegateSwizzlingInvalidAppDelegate = 1014, // I-SWZ001014
// Scene Delegate Swizzling.
kGULSwizzlerMessageCodeSceneDelegateSwizzling000 = 1100, // I-SWZ001100
kGULSwizzlerMessageCodeSceneDelegateSwizzling001 = 1101, // I-SWZ001101
kGULSwizzlerMessageCodeSceneDelegateSwizzling002 = 1102, // I-SWZ001102
kGULSwizzlerMessageCodeSceneDelegateSwizzling003 = 1103, // I-SWZ001103
kGULSwizzlerMessageCodeSceneDelegateSwizzling004 = 1104, // I-SWZ001104
kGULSwizzlerMessageCodeSceneDelegateSwizzling005 = 1105, // I-SWZ001105
kGULSwizzlerMessageCodeSceneDelegateSwizzling006 = 1106, // I-SWZ001106
kGULSwizzlerMessageCodeSceneDelegateSwizzling007 = 1107, // I-SWZ001107
kGULSwizzlerMessageCodeSceneDelegateSwizzling008 = 1108, // I-SWZ001108
kGULSwizzlerMessageCodeSceneDelegateSwizzling009 = 1109, // I-SWZ001109
kGULSwizzlerMessageCodeSceneDelegateSwizzling010 = 1110, // I-SWZ001110
kGULSwizzlerMessageCodeSceneDelegateSwizzling011 = 1111, // I-SWZ001111
kGULSwizzlerMessageCodeSceneDelegateSwizzling012 = 1112, // I-SWZ001112
kGULSwizzlerMessageCodeSceneDelegateSwizzling013 = 1113, // I-SWZ001113
kGULSwizzlerMessageCodeSceneDelegateSwizzlingInvalidSceneDelegate = 1114, // I-SWZ001114
// Method Swizzling.
kGULSwizzlerMessageCodeMethodSwizzling000 = 2000, // I-SWZ002000
};

View File

@@ -0,0 +1,305 @@
// Copyright 2017 Google
//
// 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 "GoogleUtilities/Environment/Public/GoogleUtilities/GULAppEnvironmentUtil.h"
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <sys/sysctl.h>
#import <sys/utsname.h>
#import "third_party/IsAppEncrypted/Public/IsAppEncrypted.h"
#if TARGET_OS_IOS
#import <UIKit/UIKit.h>
#endif
@implementation GULAppEnvironmentUtil
/// A key for the Info.plist to enable or disable checking if the App Store is running in a sandbox.
/// This will affect your data integrity when using Firebase Analytics, as it will disable some
/// necessary checks.
static NSString *const kFIRAppStoreReceiptURLCheckEnabledKey =
@"FirebaseAppStoreReceiptURLCheckEnabled";
/// The file name of the sandbox receipt. This is available on iOS >= 8.0
static NSString *const kFIRAIdentitySandboxReceiptFileName = @"sandboxReceipt";
static BOOL HasSCInfoFolder(void) {
#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_WATCH
NSString *bundlePath = [NSBundle mainBundle].bundlePath;
NSString *scInfoPath = [bundlePath stringByAppendingPathComponent:@"SC_Info"];
return [[NSFileManager defaultManager] fileExistsAtPath:scInfoPath];
#elif TARGET_OS_OSX
return NO;
#endif
}
static BOOL HasEmbeddedMobileProvision(void) {
#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_WATCH
return [[NSBundle mainBundle] pathForResource:@"embedded" ofType:@"mobileprovision"].length > 0;
#elif TARGET_OS_OSX
return NO;
#endif
}
+ (BOOL)isFromAppStore {
static dispatch_once_t isEncryptedOnce;
static BOOL isEncrypted = NO;
dispatch_once(&isEncryptedOnce, ^{
isEncrypted = IsAppEncrypted();
});
if ([GULAppEnvironmentUtil isSimulator]) {
return NO;
}
// If an app contain the sandboxReceipt file, it means its coming from TestFlight
// This must be checked before the SCInfo Folder check below since TestFlight apps may
// also have an SCInfo folder.
if ([GULAppEnvironmentUtil isAppStoreReceiptSandbox]) {
return NO;
}
if (HasSCInfoFolder()) {
// When iTunes downloads a .ipa, it also gets a customized .sinf file which is added to the
// main SC_Info directory.
return YES;
}
// For iOS >= 8.0, iTunesMetadata.plist is moved outside of the sandbox. Any attempt to read
// the iTunesMetadata.plist outside of the sandbox will be rejected by Apple.
// If the app does not contain the embedded.mobileprovision which is stripped out by Apple when
// the app is submitted to store, then it is highly likely that it is from Apple Store.
return isEncrypted && !HasEmbeddedMobileProvision();
}
+ (BOOL)isAppStoreReceiptSandbox {
// Since checking the App Store's receipt URL can be memory intensive, check the option in the
// Info.plist if developers opted out of this check.
id enableSandboxCheck =
[[NSBundle mainBundle] objectForInfoDictionaryKey:kFIRAppStoreReceiptURLCheckEnabledKey];
if (enableSandboxCheck && [enableSandboxCheck isKindOfClass:[NSNumber class]] &&
![enableSandboxCheck boolValue]) {
return NO;
}
NSURL *appStoreReceiptURL = [NSBundle mainBundle].appStoreReceiptURL;
NSString *appStoreReceiptFileName = appStoreReceiptURL.lastPathComponent;
return [appStoreReceiptFileName isEqualToString:kFIRAIdentitySandboxReceiptFileName];
}
+ (BOOL)isSimulator {
#if TARGET_OS_SIMULATOR
return YES;
#elif TARGET_OS_MACCATALYST
return NO;
#elif TARGET_OS_IOS || TARGET_OS_TV
NSString *platform = [GULAppEnvironmentUtil deviceModel];
return [platform isEqual:@"x86_64"] || [platform isEqual:@"i386"];
#elif TARGET_OS_OSX
return NO;
#endif
return NO;
}
+ (NSString *)getSysctlEntry:(const char *)sysctlKey {
static NSString *entryValue;
size_t size;
sysctlbyname(sysctlKey, NULL, &size, NULL, 0);
if (size > 0) {
char *entryValueCStr = malloc(size);
sysctlbyname(sysctlKey, entryValueCStr, &size, NULL, 0);
entryValue = [NSString stringWithCString:entryValueCStr encoding:NSUTF8StringEncoding];
free(entryValueCStr);
return entryValue;
} else {
return nil;
}
}
+ (NSString *)deviceModel {
static dispatch_once_t once;
static NSString *deviceModel;
#if TARGET_OS_OSX || TARGET_OS_MACCATALYST
dispatch_once(&once, ^{
// The `uname` function only returns x86_64 for Macs. Use `sysctlbyname` instead, but fall back
// to the `uname` function if it fails.
deviceModel = [GULAppEnvironmentUtil getSysctlEntry:"hw.model"];
if (deviceModel.length == 0) {
struct utsname systemInfo;
if (uname(&systemInfo) == 0) {
deviceModel = [NSString stringWithUTF8String:systemInfo.machine];
}
}
});
#else
dispatch_once(&once, ^{
struct utsname systemInfo;
if (uname(&systemInfo) == 0) {
deviceModel = [NSString stringWithUTF8String:systemInfo.machine];
}
});
#endif // TARGET_OS_OSX || TARGET_OS_MACCATALYST
return deviceModel;
}
+ (NSString *)deviceSimulatorModel {
static dispatch_once_t once;
static NSString *model = nil;
dispatch_once(&once, ^{
#if TARGET_OS_SIMULATOR
#if TARGET_OS_WATCH
model = @"watchOS Simulator";
#elif TARGET_OS_TV
model = @"tvOS Simulator";
#elif defined(TARGET_OS_VISION) && TARGET_OS_VISION
model = @"visionOS Simulator";
#elif TARGET_OS_IOS
switch ([[UIDevice currentDevice] userInterfaceIdiom]) {
case UIUserInterfaceIdiomPhone:
model = @"iOS Simulator (iPhone)";
break;
case UIUserInterfaceIdiomPad:
model = @"iOS Simulator (iPad)";
break;
default:
model = @"iOS Simulator (Unknown)";
break;
}
#endif
#elif TARGET_OS_EMBEDDED
model = [GULAppEnvironmentUtil getSysctlEntry:"hw.machine"];
#else
model = [GULAppEnvironmentUtil getSysctlEntry:"hw.model"];
#endif
});
return model;
}
+ (NSString *)systemVersion {
#if TARGET_OS_IOS
return [UIDevice currentDevice].systemVersion;
#elif TARGET_OS_OSX || TARGET_OS_TV || TARGET_OS_WATCH || \
(defined(TARGET_OS_VISION) && TARGET_OS_VISION)
// Assemble the systemVersion, excluding the patch version if it's 0.
NSOperatingSystemVersion osVersion = [NSProcessInfo processInfo].operatingSystemVersion;
NSMutableString *versionString = [[NSMutableString alloc]
initWithFormat:@"%ld.%ld", (long)osVersion.majorVersion, (long)osVersion.minorVersion];
if (osVersion.patchVersion != 0) {
[versionString appendFormat:@".%ld", (long)osVersion.patchVersion];
}
return versionString;
#endif
}
+ (BOOL)isAppExtension {
#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_WATCH
// Documented by <a href="https://goo.gl/RRB2Up">Apple</a>
BOOL appExtension = [[[NSBundle mainBundle] bundlePath] hasSuffix:@".appex"];
return appExtension;
#elif TARGET_OS_OSX
return NO;
#endif
}
+ (BOOL)isIOS7OrHigher {
return YES;
}
+ (BOOL)hasSwiftRuntime {
// The class
// [Swift._SwiftObject](https://github.com/apple/swift/blob/5eac3e2818eb340b11232aff83edfbd1c307fa03/stdlib/public/runtime/SwiftObject.h#L35)
// is a part of Swift runtime, so it should be present if Swift runtime is available.
BOOL hasSwiftRuntime =
objc_lookUpClass("Swift._SwiftObject") != nil ||
// Swift object class name before
// https://github.com/apple/swift/commit/9637b4a6e11ddca72f5f6dbe528efc7c92f14d01
objc_getClass("_TtCs12_SwiftObject") != nil;
return hasSwiftRuntime;
}
+ (NSString *)applePlatform {
NSString *applePlatform = @"unknown";
// When a Catalyst app is run on macOS then both `TARGET_OS_MACCATALYST` and `TARGET_OS_IOS` are
// `true`, which means the condition list is order-sensitive.
#if TARGET_OS_MACCATALYST
applePlatform = @"maccatalyst";
#elif TARGET_OS_IOS && (!defined(TARGET_OS_VISION) || !TARGET_OS_VISION)
#if defined(__IPHONE_14_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
if (@available(iOS 14.0, *)) {
// Early iOS 14 betas do not include isiOSAppOnMac (#6969)
applePlatform = ([[NSProcessInfo processInfo] respondsToSelector:@selector(isiOSAppOnMac)] &&
[NSProcessInfo processInfo].isiOSAppOnMac)
? @"ios_on_mac"
: @"ios";
} else {
applePlatform = @"ios";
}
#else // defined(__IPHONE_14_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
applePlatform = @"ios";
#endif // defined(__IPHONE_14_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
#elif TARGET_OS_TV
applePlatform = @"tvos";
#elif TARGET_OS_OSX
applePlatform = @"macos";
#elif TARGET_OS_WATCH
applePlatform = @"watchos";
#elif defined(TARGET_OS_VISION) && TARGET_OS_VISION
applePlatform = @"visionos";
#endif // TARGET_OS_MACCATALYST
return applePlatform;
}
+ (NSString *)appleDevicePlatform {
NSString *firebasePlatform = [GULAppEnvironmentUtil applePlatform];
#if TARGET_OS_IOS
// This check is necessary because iOS-only apps running on iPad
// will report UIUserInterfaceIdiomPhone via UI_USER_INTERFACE_IDIOM().
if ([firebasePlatform isEqualToString:@"ios"] &&
([[UIDevice currentDevice].model.lowercaseString containsString:@"ipad"] ||
[[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad)) {
return @"ipados";
}
#endif
return firebasePlatform;
}
+ (NSString *)deploymentType {
#if SWIFT_PACKAGE
NSString *deploymentType = @"swiftpm";
#elif FIREBASE_BUILD_CARTHAGE
NSString *deploymentType = @"carthage";
#elif FIREBASE_BUILD_ZIP_FILE
NSString *deploymentType = @"zip";
#elif COCOAPODS
NSString *deploymentType = @"cocoapods";
#else
NSString *deploymentType = @"unknown";
#endif
return deploymentType;
}
@end

View File

@@ -0,0 +1,153 @@
/*
* Copyright 2019 Google
*
* 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 "GoogleUtilities/Environment/Public/GoogleUtilities/GULHeartbeatDateStorage.h"
#import "GoogleUtilities/Environment/Public/GoogleUtilities/GULSecureCoding.h"
NSString *const kGULHeartbeatStorageDirectory = @"Google/FIRApp";
@interface GULHeartbeatDateStorage ()
/** The name of the file that stores heartbeat information. */
@property(nonatomic, readonly) NSString *fileName;
@end
@implementation GULHeartbeatDateStorage
@synthesize fileURL = _fileURL;
- (instancetype)initWithFileName:(NSString *)fileName {
if (fileName == nil) return nil;
self = [super init];
if (self) {
_fileName = fileName;
}
return self;
}
/** Lazy getter for fileURL.
* @return fileURL where heartbeat information is stored.
*/
- (NSURL *)fileURL {
if (!_fileURL) {
NSURL *directoryURL = [self directoryPathURL];
[self checkAndCreateDirectory:directoryURL];
_fileURL = [directoryURL URLByAppendingPathComponent:_fileName];
}
return _fileURL;
}
/** Returns the URL path of the directory for heartbeat storage data.
* @return the URL path of the directory for heartbeat storage data.
*/
- (NSURL *)directoryPathURL {
NSArray<NSString *> *paths;
#if TARGET_OS_TV
paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
#else
paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
#endif // TARGET_OS_TV
NSString *rootPath = [paths lastObject];
NSURL *rootURL = [NSURL fileURLWithPath:rootPath];
NSURL *directoryURL = [rootURL URLByAppendingPathComponent:kGULHeartbeatStorageDirectory];
return directoryURL;
}
/** Check for the existence of the directory specified by the URL, and create it if it does not
* exist.
* @param directoryPathURL The path to the directory that needs to exist.
*/
- (void)checkAndCreateDirectory:(NSURL *)directoryPathURL {
NSError *error;
if (![directoryPathURL checkResourceIsReachableAndReturnError:&error]) {
NSError *error;
[[NSFileManager defaultManager] createDirectoryAtURL:directoryPathURL
withIntermediateDirectories:YES
attributes:nil
error:&error];
}
}
- (nullable NSDate *)heartbeatDateForTag:(NSString *)tag {
@synchronized(self.class) {
NSDictionary *heartbeatDictionary = [self heartbeatDictionaryWithFileURL:self.fileURL];
NSDate *heartbeatDate = heartbeatDictionary[tag];
// Validate the value type. If the storage file was corrupted or updated with a different format
// by a newer SDK version the value type may be different.
if (![heartbeatDate isKindOfClass:[NSDate class]]) {
heartbeatDate = nil;
}
return heartbeatDate;
}
}
- (BOOL)setHearbeatDate:(NSDate *)date forTag:(NSString *)tag {
// Synchronize on the class to ensure that the different instances of the class will not access
// the same file concurrently.
// TODO: Consider a different synchronization strategy here and in `-heartbeatDateForTag:` method.
// Currently no heartbeats can be read/written concurrently even if they are in different files.
@synchronized(self.class) {
NSMutableDictionary *heartbeatDictionary =
[[self heartbeatDictionaryWithFileURL:self.fileURL] mutableCopy];
heartbeatDictionary[tag] = date;
NSError *error;
BOOL isSuccess = [self writeDictionary:[heartbeatDictionary copy]
forWritingURL:self.fileURL
error:&error];
return isSuccess;
}
}
- (NSDictionary *)heartbeatDictionaryWithFileURL:(NSURL *)readingFileURL {
NSDictionary *heartbeatDictionary;
NSError *error;
NSData *objectData = [NSData dataWithContentsOfURL:readingFileURL options:0 error:&error];
if (objectData.length > 0 && error == nil) {
NSSet<Class> *objectClasses =
[NSSet setWithArray:@[ NSDictionary.class, NSDate.class, NSString.class ]];
heartbeatDictionary = [GULSecureCoding unarchivedObjectOfClasses:objectClasses
fromData:objectData
error:&error];
}
if (heartbeatDictionary.count == 0 || error != nil) {
heartbeatDictionary = [NSDictionary dictionary];
}
return heartbeatDictionary;
}
- (BOOL)writeDictionary:(NSDictionary *)dictionary
forWritingURL:(NSURL *)writingFileURL
error:(NSError **)outError {
// Archive a mutable copy `dictionary` for writing to disk. This is done for
// backwards compatibility. See Google Utilities issue #36 for more context.
// TODO: Remove usage of mutable copy in a future version of Google Utilities.
NSData *data = [GULSecureCoding archivedDataWithRootObject:[dictionary mutableCopy]
error:outError];
if (data.length == 0) {
return NO;
}
return [data writeToURL:writingFileURL atomically:YES];
}
@end

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2021 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 "GoogleUtilities/Environment/Public/GoogleUtilities/GULHeartbeatDateStorageUserDefaults.h"
@interface GULHeartbeatDateStorageUserDefaults ()
/** The storage to store the date of the last sent heartbeat. */
@property(nonatomic, readonly) NSUserDefaults *userDefaults;
/** The key for user defaults to store heartbeat information. */
@property(nonatomic, readonly) NSString *key;
@end
@implementation GULHeartbeatDateStorageUserDefaults
- (instancetype)initWithDefaults:(NSUserDefaults *)defaults key:(NSString *)key {
self = [super init];
if (self) {
_userDefaults = defaults;
_key = key;
}
return self;
}
- (NSMutableDictionary *)heartbeatDictionaryFromDefaults {
NSDictionary *heartbeatDict = [self.userDefaults objectForKey:self.key];
if (heartbeatDict != nil) {
return [heartbeatDict mutableCopy];
} else {
return [NSMutableDictionary dictionary];
}
}
- (nullable NSDate *)heartbeatDateForTag:(NSString *)tag {
NSDate *date = nil;
@synchronized(self.userDefaults) {
NSMutableDictionary *dict = [self heartbeatDictionaryFromDefaults];
date = dict[tag];
}
return date;
}
- (BOOL)setHearbeatDate:(NSDate *)date forTag:(NSString *)tag {
@synchronized(self.userDefaults) {
NSMutableDictionary *dict = [self heartbeatDictionaryFromDefaults];
dict[tag] = date;
[self.userDefaults setObject:dict forKey:self.key];
}
return true;
}
@end

View File

@@ -0,0 +1,103 @@
// Copyright 2019 Google
//
// 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 "GoogleUtilities/Environment/Public/GoogleUtilities/GULSecureCoding.h"
NSString *const kGULSecureCodingError = @"GULSecureCodingError";
@implementation GULSecureCoding
+ (nullable id)unarchivedObjectOfClasses:(NSSet<Class> *)classes
fromData:(NSData *)data
error:(NSError **)outError {
id object;
#if __has_builtin(__builtin_available)
if (@available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *)) {
object = [NSKeyedUnarchiver unarchivedObjectOfClasses:classes fromData:data error:outError];
} else
#endif // __has_builtin(__builtin_available)
{
@try {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
#pragma clang diagnostic pop
unarchiver.requiresSecureCoding = YES;
object = [unarchiver decodeObjectOfClasses:classes forKey:NSKeyedArchiveRootObjectKey];
} @catch (NSException *exception) {
if (outError) {
*outError = [self archivingErrorWithException:exception];
}
}
if (object == nil && outError && *outError == nil) {
NSString *failureReason = @"NSKeyedUnarchiver failed to unarchive data.";
*outError = [NSError errorWithDomain:kGULSecureCodingError
code:-1
userInfo:@{NSLocalizedFailureReasonErrorKey : failureReason}];
}
}
return object;
}
+ (nullable id)unarchivedObjectOfClass:(Class)class
fromData:(NSData *)data
error:(NSError **)outError {
return [self unarchivedObjectOfClasses:[NSSet setWithObject:class] fromData:data error:outError];
}
+ (nullable NSData *)archivedDataWithRootObject:(id<NSCoding>)object error:(NSError **)outError {
NSData *archiveData;
#if __has_builtin(__builtin_available)
if (@available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *)) {
archiveData = [NSKeyedArchiver archivedDataWithRootObject:object
requiringSecureCoding:YES
error:outError];
} else
#endif // __has_builtin(__builtin_available)
{
@try {
NSMutableData *data = [NSMutableData data];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
#pragma clang diagnostic pop
archiver.requiresSecureCoding = YES;
[archiver encodeObject:object forKey:NSKeyedArchiveRootObjectKey];
[archiver finishEncoding];
archiveData = [data copy];
} @catch (NSException *exception) {
if (outError) {
*outError = [self archivingErrorWithException:exception];
}
}
}
return archiveData;
}
+ (NSError *)archivingErrorWithException:(NSException *)exception {
NSString *failureReason = [NSString
stringWithFormat:@"NSKeyedArchiver exception with name: %@, reason: %@, userInfo: %@",
exception.name, exception.reason, exception.userInfo];
NSDictionary *errorUserInfo = @{NSLocalizedFailureReasonErrorKey : failureReason};
return [NSError errorWithDomain:kGULSecureCodingError code:-1 userInfo:errorUserInfo];
}
@end

View File

@@ -0,0 +1,138 @@
// Copyright 2022 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 "GoogleUtilities/Environment/Public/GoogleUtilities/GULNetworkInfo.h"
#import <Foundation/Foundation.h>
#import <TargetConditionals.h>
#if __has_include("CoreTelephony/CTTelephonyNetworkInfo.h") && !TARGET_OS_MACCATALYST && \
!TARGET_OS_OSX && !TARGET_OS_TV && !TARGET_OS_WATCH
#define TARGET_HAS_MOBILE_CONNECTIVITY
#import <CoreTelephony/CTCarrier.h>
#import <CoreTelephony/CTTelephonyNetworkInfo.h>
#import <SystemConfiguration/SystemConfiguration.h>
#endif
@implementation GULNetworkInfo
#ifdef TARGET_HAS_MOBILE_CONNECTIVITY
+ (CTTelephonyNetworkInfo *)getNetworkInfo {
static CTTelephonyNetworkInfo *networkInfo;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
networkInfo = [[CTTelephonyNetworkInfo alloc] init];
});
return networkInfo;
}
#endif
+ (NSString *_Nullable)getNetworkMobileCountryCode {
#ifdef TARGET_HAS_MOBILE_CONNECTIVITY
CTTelephonyNetworkInfo *networkInfo = [GULNetworkInfo getNetworkInfo];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
CTCarrier *provider = networkInfo.subscriberCellularProvider;
#pragma clang diagnostic push
return provider.mobileCountryCode;
#endif
return nil;
}
+ (NSString *_Nullable)getNetworkMobileNetworkCode {
#ifdef TARGET_HAS_MOBILE_CONNECTIVITY
CTTelephonyNetworkInfo *networkInfo = [GULNetworkInfo getNetworkInfo];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
CTCarrier *provider = networkInfo.subscriberCellularProvider;
#pragma clang diagnostic push
return provider.mobileNetworkCode;
#endif
return nil;
}
/**
* Returns the formatted MccMnc if the inputs are valid, otherwise nil
* @param mcc The Mobile Country Code returned from `getNetworkMobileCountryCode`
* @param mnc The Mobile Network Code returned from `getNetworkMobileNetworkCode`
* @returns A string with the concatenated mccMnc if both inputs are valid, otherwise nil
*/
+ (NSString *_Nullable)formatMcc:(NSString *)mcc andMNC:(NSString *)mnc {
// These are both nil if the target does not support mobile connectivity
if (mcc == nil && mnc == nil) {
return nil;
}
if (mcc.length != 3 || mnc.length < 2 || mnc.length > 3) {
return nil;
}
// If the resulting appended mcc + mnc contains characters that are not
// decimal digits, return nil
static NSCharacterSet *notDigits;
static dispatch_once_t token;
dispatch_once(&token, ^{
notDigits = [[NSCharacterSet decimalDigitCharacterSet] invertedSet];
});
NSString *mccMnc = [mcc stringByAppendingString:mnc];
if ([mccMnc rangeOfCharacterFromSet:notDigits].location != NSNotFound) {
return nil;
}
return mccMnc;
}
+ (GULNetworkType)getNetworkType {
GULNetworkType networkType = GULNetworkTypeNone;
#ifdef TARGET_HAS_MOBILE_CONNECTIVITY
static SCNetworkReachabilityRef reachabilityRef = 0;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
reachabilityRef = SCNetworkReachabilityCreateWithName(kCFAllocatorSystemDefault, "google.com");
});
if (!reachabilityRef) {
return GULNetworkTypeNone;
}
SCNetworkReachabilityFlags reachabilityFlags = 0;
SCNetworkReachabilityGetFlags(reachabilityRef, &reachabilityFlags);
// Parse the network flags to set the network type.
if (reachabilityFlags & kSCNetworkReachabilityFlagsReachable) {
if (reachabilityFlags & kSCNetworkReachabilityFlagsIsWWAN) {
networkType = GULNetworkTypeMobile;
} else {
networkType = GULNetworkTypeWIFI;
}
}
#endif
return networkType;
}
+ (NSString *)getNetworkRadioType {
#ifdef TARGET_HAS_MOBILE_CONNECTIVITY
CTTelephonyNetworkInfo *networkInfo = [GULNetworkInfo getNetworkInfo];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
return networkInfo.currentRadioAccessTechnology;
#pragma clang diagnostic pop
#else
return @"";
#endif
}
@end

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2017 Google
*
* 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 <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface GULAppEnvironmentUtil : NSObject
/// Indicates whether the app is from Apple Store or not. Returns NO if the app is on simulator,
/// development environment or sideloaded.
+ (BOOL)isFromAppStore;
/// Indicates whether the app is a Testflight app. Returns YES if the app has sandbox receipt.
/// Returns NO otherwise.
+ (BOOL)isAppStoreReceiptSandbox;
/// Indicates whether the app is on simulator or not at runtime depending on the device
/// architecture.
+ (BOOL)isSimulator;
/// The current device model. Returns an empty string if device model cannot be retrieved.
+ (nullable NSString *)deviceModel;
/// The current device model, with simulator-specific values. Returns an empty string if device
/// model cannot be retrieved.
+ (nullable NSString *)deviceSimulatorModel;
/// The current operating system version. Returns an empty string if the system version cannot be
/// retrieved.
+ (NSString *)systemVersion;
/// Indicates whether it is running inside an extension or an app.
+ (BOOL)isAppExtension;
/// @return Returns @YES when is run on iOS version greater or equal to 7.0
+ (BOOL)isIOS7OrHigher DEPRECATED_MSG_ATTRIBUTE(
"Always `YES` because only iOS 8 and higher supported. The method will be removed.");
/// @return YES if Swift runtime detected in the app.
+ (BOOL)hasSwiftRuntime __deprecated;
/// @return An Apple platform. Possible values "ios", "tvos", "macos", "watchos", "maccatalyst", and
/// "visionos".
+ (NSString *)applePlatform;
/// @return An Apple Device platform. Same possible values as `applePlatform`, with the addition of
/// "ipados".
+ (NSString *)appleDevicePlatform;
/// @return The way the library was added to the app, e.g. "swiftpm", "cocoapods", etc.
+ (NSString *)deploymentType;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2021 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 <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/**
* Describes an object that can store and fetch heartbeat dates for given tags.
*/
@protocol GULHeartbeatDateStorable <NSObject>
/**
* Reads the date from the specified file for the given tag.
* @return Returns date if exists, otherwise `nil`.
*/
- (nullable NSDate *)heartbeatDateForTag:(NSString *)tag;
/**
* Saves the date for the specified tag in the specified file.
* @return YES on success, NO otherwise.
*/
- (BOOL)setHearbeatDate:(NSDate *)date forTag:(NSString *)tag;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2019 Google
*
* 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 <Foundation/Foundation.h>
#import "GULHeartbeatDateStorable.h"
NS_ASSUME_NONNULL_BEGIN
/// The name of the directory where the heartbeat data is stored.
extern NSString *const kGULHeartbeatStorageDirectory;
/// Stores either a date or a dictionary to a specified file.
@interface GULHeartbeatDateStorage : NSObject <GULHeartbeatDateStorable>
- (instancetype)init NS_UNAVAILABLE;
@property(nonatomic, readonly) NSURL *fileURL;
/**
* Default initializer.
* @param fileName The name of the file to store the date information.
* exist, it will be created if needed.
*/
- (instancetype)initWithFileName:(NSString *)fileName;
/**
* Reads the date from the specified file for the given tag.
* @return Returns date if exists, otherwise `nil`.
*/
- (nullable NSDate *)heartbeatDateForTag:(NSString *)tag;
/**
* Saves the date for the specified tag in the specified file.
* @return YES on success, NO otherwise.
*/
- (BOOL)setHearbeatDate:(NSDate *)date forTag:(NSString *)tag;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2021 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 <Foundation/Foundation.h>
#import "GULHeartbeatDateStorable.h"
NS_ASSUME_NONNULL_BEGIN
/// Stores either a date or a dictionary to a specified file.
@interface GULHeartbeatDateStorageUserDefaults : NSObject <GULHeartbeatDateStorable>
/**
* Default initializer. tvOS can only write to the cache directory and
* there are no guarantees that the directory will persist. User defaults will
* be retained, so that should be used instead.
* @param defaults User defaults instance to store the heartbeat information.
* @param key The key to be used with the user defaults instance.
*/
- (instancetype)initWithDefaults:(NSUserDefaults *)defaults key:(NSString *)key;
- (instancetype)init NS_UNAVAILABLE;
/**
* Reads the date from the specified file for the given tag.
* @return Returns date if exists, otherwise `nil`.
*/
- (nullable NSDate *)heartbeatDateForTag:(NSString *)tag;
/**
* Saves the date for the specified tag in the specified file.
* @return YES on success, NO otherwise.
*/
- (BOOL)setHearbeatDate:(NSDate *)date forTag:(NSString *)tag;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2019 Google
*
* 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 <Foundation/Foundation.h>
@class FBLPromise<ValueType>;
NS_ASSUME_NONNULL_BEGIN
/// The class provides a convenient, multiplatform abstraction of the Keychain.
///
/// When using this API on macOS, the corresponding target must be signed with a provisioning
/// profile that has the Keychain Sharing capability enabled.
@interface GULKeychainStorage : NSObject
- (instancetype)init NS_UNAVAILABLE;
/** Initializes the keychain storage with Keychain Service name.
* @param service A Keychain Service name that will be used to store and retrieve objects. See also
* `kSecAttrService`.
*/
- (instancetype)initWithService:(NSString *)service;
/**
* Get an object by key.
* @param key The key.
* @param objectClass The expected object class required by `NSSecureCoding`.
* @param accessGroup The Keychain Access Group.
*
* @return Returns a promise. It is resolved with an object stored by key if exists. It is resolved
* with `nil` when the object not found. It fails on a Keychain error.
*/
- (FBLPromise<id<NSSecureCoding>> *)getObjectForKey:(NSString *)key
objectClass:(Class)objectClass
accessGroup:(nullable NSString *)accessGroup;
/**
* Saves the given object by the given key.
* @param object The object to store.
* @param key The key to store the object. If there is an existing object by the key, it will be
* overridden.
* @param accessGroup The Keychain Access Group.
*
* @return Returns which is resolved with `[NSNull null]` on success.
*/
- (FBLPromise<NSNull *> *)setObject:(id<NSSecureCoding>)object
forKey:(NSString *)key
accessGroup:(nullable NSString *)accessGroup;
/**
* Removes the object by the given key.
* @param key The key to store the object. If there is an existing object by the key, it will be
* overridden.
* @param accessGroup The Keychain Access Group.
*
* @return Returns which is resolved with `[NSNull null]` on success.
*/
- (FBLPromise<NSNull *> *)removeObjectForKey:(NSString *)key
accessGroup:(nullable NSString *)accessGroup;
#if TARGET_OS_OSX
/// If not `nil`, then only this keychain will be used to save and read data (see
/// `kSecMatchSearchList` and `kSecUseKeychain`. It is mostly intended to be used by unit tests.
@property(nonatomic, nullable) SecKeychainRef keychainRef;
#endif // TARGET_OS_OSX
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2019 Google
*
* 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 <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
FOUNDATION_EXPORT NSString *const kGULKeychainUtilsErrorDomain;
/// A collection of helper functions that abstract away common Keychain operations.
///
/// When using this API on macOS, the corresponding target must be signed with a provisioning
/// profile that has the Keychain Sharing capability enabled.
@interface GULKeychainUtils : NSObject
/** Fetches a keychain item data matching to the provided query.
* @param query A dictionary with Keychain query parameters. See docs for `SecItemCopyMatching` for
* details.
* @param outError A pointer to `NSError` instance or `NULL`. The instance at `outError` will be
* assigned with an error if there is.
* @returns Data for the first Keychain Item matching the provided query or `nil` if there is not
* such an item (`outError` will be `nil` in this case) or an error occurred.
*/
+ (nullable NSData *)getItemWithQuery:(NSDictionary *)query
error:(NSError *_Nullable *_Nullable)outError;
/** Stores data to a Keychain Item matching to the provided query. An existing Keychain Item
* matching the query parameters will be updated or a new will be created.
* @param item A Keychain Item data to store.
* @param query A dictionary with Keychain query parameters. See docs for `SecItemAdd` and
* `SecItemUpdate` for details.
* @param outError A pointer to `NSError` instance or `NULL`. The instance at `outError` will be
* assigned with an error if there is.
* @returns `YES` when data was successfully stored, `NO` otherwise.
*/
+ (BOOL)setItem:(NSData *)item
withQuery:(NSDictionary *)query
error:(NSError *_Nullable *_Nullable)outError;
/** Removes a Keychain Item matching to the provided query.
* @param query A dictionary with Keychain query parameters. See docs for `SecItemDelete` for
* details.
* @param outError A pointer to `NSError` instance or `NULL`. The instance at `outError` will be
* assigned with an error if there is.
* @returns `YES` if the item was removed successfully or doesn't exist, `NO` otherwise.
*/
+ (BOOL)removeItemWithQuery:(NSDictionary *)query error:(NSError *_Nullable *_Nullable)outError;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,57 @@
// Copyright 2022 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 <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/// The type of network that the device is running with. Values should correspond to the NetworkType
/// values in android/play/playlog/proto/clientanalytics.proto
typedef NS_ENUM(NSInteger, GULNetworkType) {
GULNetworkTypeNone = -1,
GULNetworkTypeMobile = 0,
GULNetworkTypeWIFI = 1,
};
/// Collection of utilities to read network status information
@interface GULNetworkInfo : NSObject
/// Returns the cellular mobile country code (mcc) if CoreTelephony is supported, otherwise nil
+ (NSString *_Nullable)getNetworkMobileCountryCode;
/// Returns the cellular mobile network code (mnc) if CoreTelephony is supported, otherwise nil
+ (NSString *_Nullable)getNetworkMobileNetworkCode;
/**
* Returns the formatted MccMnc if the inputs are valid, otherwise nil
* @param mcc The Mobile Country Code returned from `getNetworkMobileCountryCode`
* @param mnc The Mobile Network Code returned from `getNetworkMobileNetworkCode`
* @returns A string with the concatenated mccMnc if both inputs are valid, otherwise nil
*/
+ (NSString *_Nullable)formatMcc:(NSString *_Nullable)mcc andMNC:(NSString *_Nullable)mnc;
/// Returns an enum indicating the network type. The enum values should be easily transferrable to
/// the NetworkType value in android/play/playlog/proto/clientanalytics.proto. Right now this always
/// returns None on platforms other than iOS. This should be updated in the future to return Wi-Fi
/// values for the other platforms when applicable.
+ (GULNetworkType)getNetworkType;
/// Returns a string indicating the radio access technology used by the app. The return value will
/// be one of CTRadioAccess constants defined in
/// https://developer.apple.com/documentation/coretelephony/cttelephonynetworkinfo/radio_access_technology_constants
+ (NSString *)getNetworkRadioType;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,36 @@
// Copyright 2019 Google
//
// 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 <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/** The class wraps `NSKeyedArchiver` and `NSKeyedUnarchiver` API to provide a unified secure coding
* methods for iOS versions before and after 11.
*/
@interface GULSecureCoding : NSObject
+ (nullable id)unarchivedObjectOfClasses:(NSSet<Class> *)classes
fromData:(NSData *)data
error:(NSError **)outError;
+ (nullable id)unarchivedObjectOfClass:(Class)class
fromData:(NSData *)data
error:(NSError **)outError;
+ (nullable NSData *)archivedDataWithRootObject:(id<NSCoding>)object error:(NSError **)outError;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2020 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 <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/** The class represents HTTP response received from `NSURLSession`. */
@interface GULURLSessionDataResponse : NSObject
@property(nonatomic, readonly) NSHTTPURLResponse *HTTPResponse;
@property(nonatomic, nullable, readonly) NSData *HTTPBody;
- (instancetype)initWithResponse:(NSHTTPURLResponse *)response HTTPBody:(nullable NSData *)body;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2020 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 <Foundation/Foundation.h>
@class FBLPromise<Value>;
@class GULURLSessionDataResponse;
NS_ASSUME_NONNULL_BEGIN
/** Promise based API for `NSURLSession`. */
@interface NSURLSession (GULPromises)
/** Creates a promise wrapping `-[NSURLSession dataTaskWithRequest:completionHandler:]` method.
* @param URLRequest The request to create a data task with.
* @return A promise that is fulfilled when an HTTP response is received (with any response code),
* or is rejected with the error passed to the task completion.
*/
- (FBLPromise<GULURLSessionDataResponse *> *)gul_dataTaskPromiseWithRequest:
(NSURLRequest *)URLRequest;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,198 @@
/*
* Copyright 2019 Google
*
* 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 "GoogleUtilities/Environment/Public/GoogleUtilities/GULKeychainStorage.h"
#import <Security/Security.h>
#if __has_include(<FBLPromises/FBLPromises.h>)
#import <FBLPromises/FBLPromises.h>
#else
#import "FBLPromises.h"
#endif
#import "GoogleUtilities/Environment/Public/GoogleUtilities/GULKeychainUtils.h"
#import "GoogleUtilities/Environment/Public/GoogleUtilities/GULSecureCoding.h"
@interface GULKeychainStorage ()
@property(nonatomic, readonly) dispatch_queue_t keychainQueue;
@property(nonatomic, readonly) dispatch_queue_t inMemoryCacheQueue;
@property(nonatomic, readonly) NSString *service;
@property(nonatomic, readonly) NSCache<NSString *, id<NSSecureCoding>> *inMemoryCache;
@end
@implementation GULKeychainStorage
- (instancetype)initWithService:(NSString *)service {
NSCache *cache = [[NSCache alloc] init];
// Cache up to 5 installations.
cache.countLimit = 5;
return [self initWithService:service cache:cache];
}
- (instancetype)initWithService:(NSString *)service cache:(NSCache *)cache {
self = [super init];
if (self) {
_keychainQueue =
dispatch_queue_create("com.gul.KeychainStorage.Keychain", DISPATCH_QUEUE_SERIAL);
_inMemoryCacheQueue =
dispatch_queue_create("com.gul.KeychainStorage.InMemoryCache", DISPATCH_QUEUE_SERIAL);
_service = [service copy];
_inMemoryCache = cache;
}
return self;
}
#pragma mark - Public
- (FBLPromise<id<NSSecureCoding>> *)getObjectForKey:(NSString *)key
objectClass:(Class)objectClass
accessGroup:(nullable NSString *)accessGroup {
return [FBLPromise onQueue:self.inMemoryCacheQueue
do:^id _Nullable {
// Return cached object or fail otherwise.
id object = [self.inMemoryCache objectForKey:key];
return object
?: [[NSError alloc]
initWithDomain:FBLPromiseErrorDomain
code:FBLPromiseErrorCodeValidationFailure
userInfo:nil];
}]
.recover(^id _Nullable(NSError *error) {
// Look for the object in the keychain.
return [self getObjectFromKeychainForKey:key
objectClass:objectClass
accessGroup:accessGroup];
});
}
- (FBLPromise<NSNull *> *)setObject:(id<NSSecureCoding>)object
forKey:(NSString *)key
accessGroup:(nullable NSString *)accessGroup {
return [FBLPromise onQueue:self.inMemoryCacheQueue
do:^id _Nullable {
// Save to the in-memory cache first.
[self.inMemoryCache setObject:object forKey:[key copy]];
return [NSNull null];
}]
.thenOn(self.keychainQueue, ^id(id result) {
// Then store the object to the keychain.
NSDictionary *query = [self keychainQueryWithKey:key accessGroup:accessGroup];
NSError *error;
NSData *encodedObject = [GULSecureCoding archivedDataWithRootObject:object error:&error];
if (!encodedObject) {
return error;
}
if (![GULKeychainUtils setItem:encodedObject withQuery:query error:&error]) {
return error;
}
return [NSNull null];
});
}
- (FBLPromise<NSNull *> *)removeObjectForKey:(NSString *)key
accessGroup:(nullable NSString *)accessGroup {
return [FBLPromise onQueue:self.inMemoryCacheQueue
do:^id _Nullable {
[self.inMemoryCache removeObjectForKey:key];
return nil;
}]
.thenOn(self.keychainQueue, ^id(id result) {
NSDictionary *query = [self keychainQueryWithKey:key accessGroup:accessGroup];
NSError *error;
if (![GULKeychainUtils removeItemWithQuery:query error:&error]) {
return error;
}
return [NSNull null];
});
}
#pragma mark - Private
- (FBLPromise<id<NSSecureCoding>> *)getObjectFromKeychainForKey:(NSString *)key
objectClass:(Class)objectClass
accessGroup:(nullable NSString *)accessGroup {
// Look for the object in the keychain.
return [FBLPromise
onQueue:self.keychainQueue
do:^id {
NSDictionary *query = [self keychainQueryWithKey:key accessGroup:accessGroup];
NSError *error;
NSData *encodedObject = [GULKeychainUtils getItemWithQuery:query error:&error];
if (error) {
return error;
}
if (!encodedObject) {
return nil;
}
id object = [GULSecureCoding unarchivedObjectOfClass:objectClass
fromData:encodedObject
error:&error];
if (error) {
return error;
}
return object;
}]
.thenOn(self.inMemoryCacheQueue,
^id<NSSecureCoding> _Nullable(id<NSSecureCoding> _Nullable object) {
// Save object to the in-memory cache if exists and return the object.
if (object) {
[self.inMemoryCache setObject:object forKey:[key copy]];
}
return object;
});
}
- (void)resetInMemoryCache {
[self.inMemoryCache removeAllObjects];
}
#pragma mark - Keychain
- (NSMutableDictionary<NSString *, id> *)keychainQueryWithKey:(NSString *)key
accessGroup:(nullable NSString *)accessGroup {
NSMutableDictionary<NSString *, id> *query = [NSMutableDictionary dictionary];
query[(__bridge NSString *)kSecClass] = (__bridge NSString *)kSecClassGenericPassword;
query[(__bridge NSString *)kSecAttrService] = self.service;
query[(__bridge NSString *)kSecAttrAccount] = key;
if (accessGroup) {
query[(__bridge NSString *)kSecAttrAccessGroup] = accessGroup;
}
if (@available(iOS 13.0, macOS 10.15, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, *)) {
// Ensures that the keychain query behaves the same across all platforms.
// See go/firebase-macos-keychain-popups for details.
query[(__bridge id)kSecUseDataProtectionKeychain] = (__bridge id)kCFBooleanTrue;
}
#if TARGET_OS_OSX
if (self.keychainRef) {
query[(__bridge NSString *)kSecUseKeychain] = (__bridge id)(self.keychainRef);
query[(__bridge NSString *)kSecMatchSearchList] = @[ (__bridge id)(self.keychainRef) ];
}
#endif // TARGET_OS_OSX
return query;
}
@end

View File

@@ -0,0 +1,133 @@
/*
* Copyright 2019 Google
*
* 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 "GoogleUtilities/Environment/Public/GoogleUtilities/GULKeychainUtils.h"
NSString *const kGULKeychainUtilsErrorDomain = @"com.gul.keychain.ErrorDomain";
@implementation GULKeychainUtils
+ (nullable NSData *)getItemWithQuery:(NSDictionary *)query
error:(NSError *_Nullable *_Nullable)outError {
NSMutableDictionary *mutableGetItemQuery =
[[[self class] multiplatformQueryWithQuery:query] mutableCopy];
mutableGetItemQuery[(__bridge id)kSecReturnData] = @YES;
mutableGetItemQuery[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
CFDataRef result = NULL;
OSStatus status =
SecItemCopyMatching((__bridge CFDictionaryRef)mutableGetItemQuery, (CFTypeRef *)&result);
if (status == errSecSuccess && result != NULL) {
if (outError) {
*outError = nil;
}
return (__bridge_transfer NSData *)result;
}
if (status == errSecItemNotFound) {
if (outError) {
*outError = nil;
}
} else {
if (outError) {
*outError = [self keychainErrorWithFunction:@"SecItemCopyMatching" status:status];
}
}
return nil;
}
+ (BOOL)setItem:(NSData *)item
withQuery:(NSDictionary *)query
error:(NSError *_Nullable *_Nullable)outError {
NSDictionary *multiplatformQuery = [[self class] multiplatformQueryWithQuery:query];
NSData *existingItem = [self getItemWithQuery:multiplatformQuery error:outError];
if (outError && *outError) {
return NO;
}
OSStatus status;
if (!existingItem) {
NSMutableDictionary *mutableAddItemQuery = [multiplatformQuery mutableCopy];
mutableAddItemQuery[(__bridge id)kSecAttrAccessible] =
(__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
mutableAddItemQuery[(__bridge id)kSecValueData] = item;
status = SecItemAdd((__bridge CFDictionaryRef)mutableAddItemQuery, NULL);
} else {
NSDictionary *attributes = @{(__bridge id)kSecValueData : item};
status = SecItemUpdate((__bridge CFDictionaryRef)multiplatformQuery,
(__bridge CFDictionaryRef)attributes);
}
if (status == noErr) {
if (outError) {
*outError = nil;
}
return YES;
}
NSString *function = existingItem ? @"SecItemUpdate" : @"SecItemAdd";
if (outError) {
*outError = [self keychainErrorWithFunction:function status:status];
}
return NO;
}
+ (BOOL)removeItemWithQuery:(NSDictionary *)query error:(NSError *_Nullable *_Nullable)outError {
NSDictionary *deleteItemQuery = [[self class] multiplatformQueryWithQuery:query];
OSStatus status = SecItemDelete((__bridge CFDictionaryRef)deleteItemQuery);
if (status == noErr || status == errSecItemNotFound) {
if (outError) {
*outError = nil;
}
return YES;
}
if (outError) {
*outError = [self keychainErrorWithFunction:@"SecItemDelete" status:status];
}
return NO;
}
#pragma mark - Private
/// Returns a `NSDictionary` query that behaves the same across all platforms.
/// - Note: In practice, this API only makes a difference to keychain queries on macOS.
/// See go/firebase-macos-keychain-popups for details.
/// - Parameter query: A query to create the protected keychain query with.
+ (NSDictionary *)multiplatformQueryWithQuery:(NSDictionary *)query {
NSMutableDictionary *multiplatformQuery = [query mutableCopy];
if (@available(iOS 13.0, macOS 10.15, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, *)) {
multiplatformQuery[(__bridge id)kSecUseDataProtectionKeychain] = (__bridge id)kCFBooleanTrue;
}
return [multiplatformQuery copy];
}
#pragma mark - Errors
+ (NSError *)keychainErrorWithFunction:(NSString *)keychainFunction status:(OSStatus)status {
NSString *failureReason = [NSString stringWithFormat:@"%@ (%li)", keychainFunction, (long)status];
NSDictionary *userInfo = @{NSLocalizedFailureReasonErrorKey : failureReason};
return [NSError errorWithDomain:kGULKeychainUtilsErrorDomain code:0 userInfo:userInfo];
}
@end

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2020 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 "GoogleUtilities/Environment/Public/GoogleUtilities/GULURLSessionDataResponse.h"
@implementation GULURLSessionDataResponse
- (instancetype)initWithResponse:(NSHTTPURLResponse *)response HTTPBody:(NSData *)body {
self = [super init];
if (self) {
_HTTPResponse = response;
_HTTPBody = body;
}
return self;
}
@end

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2020 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 "GoogleUtilities/Environment/Public/GoogleUtilities/NSURLSession+GULPromises.h"
#if __has_include(<FBLPromises/FBLPromises.h>)
#import <FBLPromises/FBLPromises.h>
#else
#import "FBLPromises.h"
#endif
#import "GoogleUtilities/Environment/Public/GoogleUtilities/GULURLSessionDataResponse.h"
@implementation NSURLSession (GULPromises)
- (FBLPromise<GULURLSessionDataResponse *> *)gul_dataTaskPromiseWithRequest:
(NSURLRequest *)URLRequest {
return [FBLPromise async:^(FBLPromiseFulfillBlock fulfill, FBLPromiseRejectBlock reject) {
[[self dataTaskWithRequest:URLRequest
completionHandler:^(NSData *_Nullable data, NSURLResponse *_Nullable response,
NSError *_Nullable error) {
if (error) {
reject(error);
} else {
fulfill([[GULURLSessionDataResponse alloc]
initWithResponse:(NSHTTPURLResponse *)response
HTTPBody:data]);
}
}] resume];
}];
}
@end

View File

@@ -0,0 +1,219 @@
// Copyright 2018 Google
//
// 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 "GoogleUtilities/Logger/Public/GoogleUtilities/GULLogger.h"
#include <asl.h>
#import "GoogleUtilities/Environment/Public/GoogleUtilities/GULAppEnvironmentUtil.h"
#import "GoogleUtilities/Logger/Public/GoogleUtilities/GULLoggerLevel.h"
/// ASL client facility name used by GULLogger.
const char *kGULLoggerASLClientFacilityName = "com.google.utilities.logger";
static dispatch_once_t sGULLoggerOnceToken;
static aslclient sGULLoggerClient;
static dispatch_queue_t sGULClientQueue;
static BOOL sGULLoggerDebugMode;
static GULLoggerLevel sGULLoggerMaximumLevel;
// Allow clients to register a version to include in the log.
static NSString *sVersion = @"";
static GULLoggerService kGULLoggerLogger = @"[GULLogger]";
#ifdef DEBUG
/// The regex pattern for the message code.
static NSString *const kMessageCodePattern = @"^I-[A-Z]{3}[0-9]{6}$";
static NSRegularExpression *sMessageCodeRegex;
#endif
void GULLoggerInitializeASL(void) {
dispatch_once(&sGULLoggerOnceToken, ^{
NSInteger majorOSVersion = [[GULAppEnvironmentUtil systemVersion] integerValue];
uint32_t aslOptions = ASL_OPT_STDERR;
#if TARGET_OS_SIMULATOR
// The iOS 11 simulator doesn't need the ASL_OPT_STDERR flag.
if (majorOSVersion >= 11) {
aslOptions = 0;
}
#else
// Devices running iOS 10 or higher don't need the ASL_OPT_STDERR flag.
if (majorOSVersion >= 10) {
aslOptions = 0;
}
#endif // TARGET_OS_SIMULATOR
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations" // asl is deprecated
// Initialize the ASL client handle.
sGULLoggerClient = asl_open(NULL, kGULLoggerASLClientFacilityName, aslOptions);
sGULLoggerMaximumLevel = GULLoggerLevelNotice;
// Set the filter used by system/device log. Initialize in default mode.
asl_set_filter(sGULLoggerClient, ASL_FILTER_MASK_UPTO(ASL_LEVEL_NOTICE));
sGULClientQueue = dispatch_queue_create("GULLoggingClientQueue", DISPATCH_QUEUE_SERIAL);
dispatch_set_target_queue(sGULClientQueue,
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0));
#ifdef DEBUG
sMessageCodeRegex = [NSRegularExpression regularExpressionWithPattern:kMessageCodePattern
options:0
error:NULL];
#endif
});
}
void GULLoggerEnableSTDERR(void) {
asl_add_log_file(sGULLoggerClient, STDERR_FILENO);
}
void GULLoggerForceDebug(void) {
// We should enable debug mode if we're not running from App Store.
if (![GULAppEnvironmentUtil isFromAppStore]) {
sGULLoggerDebugMode = YES;
GULSetLoggerLevel(GULLoggerLevelDebug);
}
}
GULLoggerLevel GULGetLoggerLevel(void) {
return sGULLoggerMaximumLevel;
}
__attribute__((no_sanitize("thread"))) void GULSetLoggerLevel(GULLoggerLevel loggerLevel) {
if (loggerLevel < GULLoggerLevelMin || loggerLevel > GULLoggerLevelMax) {
GULLogError(kGULLoggerLogger, NO, @"I-COR000023", @"Invalid logger level, %ld",
(long)loggerLevel);
return;
}
GULLoggerInitializeASL();
// We should not raise the logger level if we are running from App Store.
if (loggerLevel >= GULLoggerLevelNotice && [GULAppEnvironmentUtil isFromAppStore]) {
return;
}
sGULLoggerMaximumLevel = loggerLevel;
dispatch_async(sGULClientQueue, ^{
asl_set_filter(sGULLoggerClient, ASL_FILTER_MASK_UPTO(loggerLevel));
});
}
/**
* Check if the level is high enough to be loggable.
*/
__attribute__((no_sanitize("thread"))) BOOL GULIsLoggableLevel(GULLoggerLevel loggerLevel) {
GULLoggerInitializeASL();
if (sGULLoggerDebugMode) {
return YES;
}
return (BOOL)(loggerLevel <= sGULLoggerMaximumLevel);
}
#ifdef DEBUG
void GULResetLogger(void) {
sGULLoggerOnceToken = 0;
sGULLoggerDebugMode = NO;
sGULLoggerMaximumLevel = GULLoggerLevelNotice;
}
aslclient getGULLoggerClient(void) {
return sGULLoggerClient;
}
dispatch_queue_t getGULClientQueue(void) {
return sGULClientQueue;
}
BOOL getGULLoggerDebugMode(void) {
return sGULLoggerDebugMode;
}
#endif
void GULLoggerRegisterVersion(NSString *version) {
sVersion = version;
}
void GULLogBasic(GULLoggerLevel level,
GULLoggerService service,
BOOL forceLog,
NSString *messageCode,
NSString *message,
va_list args_ptr) {
GULLoggerInitializeASL();
if (!(level <= sGULLoggerMaximumLevel || sGULLoggerDebugMode || forceLog)) {
return;
}
#ifdef DEBUG
NSCAssert(messageCode.length == 11, @"Incorrect message code length.");
NSRange messageCodeRange = NSMakeRange(0, messageCode.length);
NSUInteger __unused numberOfMatches =
[sMessageCodeRegex numberOfMatchesInString:messageCode options:0 range:messageCodeRange];
NSCAssert(numberOfMatches == 1, @"Incorrect message code format.");
#endif
NSString *logMsg;
if (args_ptr == NULL) {
logMsg = message;
} else {
logMsg = [[NSString alloc] initWithFormat:message arguments:args_ptr];
}
logMsg = [NSString stringWithFormat:@"%@ - %@[%@] %@", sVersion, service, messageCode, logMsg];
dispatch_async(sGULClientQueue, ^{
asl_log(sGULLoggerClient, NULL, (int)level, "%s", logMsg.UTF8String);
});
}
#pragma clang diagnostic pop
/**
* Generates the logging functions using macros.
*
* Calling GULLogError({service}, @"I-XYZ000001", @"Configure %@ failed.", @"blah") shows:
* yyyy-mm-dd hh:mm:ss.SSS sender[PID] <Error> [{service}][I-XYZ000001] Configure blah failed.
* Calling GULLogDebug({service}, @"I-XYZ000001", @"Configure succeed.") shows:
* yyyy-mm-dd hh:mm:ss.SSS sender[PID] <Debug> [{service}][I-XYZ000001] Configure succeed.
*/
#define GUL_LOGGING_FUNCTION(level) \
void GULLog##level(GULLoggerService service, BOOL force, NSString *messageCode, \
NSString *message, ...) { \
va_list args_ptr; \
va_start(args_ptr, message); \
GULLogBasic(GULLoggerLevel##level, service, force, messageCode, message, args_ptr); \
va_end(args_ptr); \
}
GUL_LOGGING_FUNCTION(Error)
GUL_LOGGING_FUNCTION(Warning)
GUL_LOGGING_FUNCTION(Notice)
GUL_LOGGING_FUNCTION(Info)
GUL_LOGGING_FUNCTION(Debug)
#undef GUL_MAKE_LOGGER
#pragma mark - GULLoggerWrapper
@implementation GULLoggerWrapper
+ (void)logWithLevel:(GULLoggerLevel)level
withService:(GULLoggerService)service
withCode:(NSString *)messageCode
withMessage:(NSString *)message
withArgs:(va_list)args {
GULLogBasic(level, service, NO, messageCode, message, args);
}
@end

View File

@@ -0,0 +1,164 @@
/*
* Copyright 2018 Google
*
* 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 <Foundation/Foundation.h>
#import "GULLoggerLevel.h"
NS_ASSUME_NONNULL_BEGIN
/**
* The services used in the logger.
*/
typedef NSString *const GULLoggerService;
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
/**
* Initialize GULLogger.
*/
extern void GULLoggerInitializeASL(void);
/**
* Override log level to Debug.
*/
void GULLoggerForceDebug(void);
/**
* Turn on logging to STDERR.
*/
extern void GULLoggerEnableSTDERR(void);
/**
* Gets the current GULLoggerLevel.
*/
extern GULLoggerLevel GULGetLoggerLevel(void);
/**
* Changes the default logging level of GULLoggerLevelNotice to a user-specified level.
* The default level cannot be set above GULLoggerLevelNotice if the app is running from App Store.
* (required) log level (one of the GULLoggerLevel enum values).
*/
extern void GULSetLoggerLevel(GULLoggerLevel loggerLevel);
/**
* Checks if the specified logger level is loggable given the current settings.
* (required) log level (one of the GULLoggerLevel enum values).
*/
extern BOOL GULIsLoggableLevel(GULLoggerLevel loggerLevel);
/**
* Register version to include in logs.
* (required) version
*/
extern void GULLoggerRegisterVersion(NSString *version);
/**
* Logs a message to the Xcode console and the device log. If running from AppStore, will
* not log any messages with a level higher than GULLoggerLevelNotice to avoid log spamming.
* (required) log level (one of the GULLoggerLevel enum values).
* (required) service name of type GULLoggerService.
* (required) message code starting with "I-" which means iOS, followed by a capitalized
* three-character service identifier and a six digit integer message ID that is unique
* within the service.
* An example of the message code is @"I-COR000001".
* (required) message string which can be a format string.
* (optional) variable arguments list obtained from calling va_start, used when message is a format
* string.
*/
extern void GULLogBasic(GULLoggerLevel level,
GULLoggerService service,
BOOL forceLog,
NSString *messageCode,
NSString *message,
// On 64-bit simulators, va_list is not a pointer, so cannot be marked nullable
// See: http://stackoverflow.com/q/29095469
#if __LP64__ && TARGET_OS_SIMULATOR || TARGET_OS_OSX
va_list args_ptr
#else
va_list _Nullable args_ptr
#endif
);
/**
* The following functions accept the following parameters in order:
* (required) service name of type GULLoggerService.
* (required) message code starting from "I-" which means iOS, followed by a capitalized
* three-character service identifier and a six digit integer message ID that is unique
* within the service.
* An example of the message code is @"I-COR000001".
* See go/firebase-log-proposal for details.
* (required) message string which can be a format string.
* (optional) the list of arguments to substitute into the format string.
* Example usage:
* GULLogError(kGULLoggerCore, @"I-COR000001", @"Configuration of %@ failed.", app.name);
*/
extern void GULLogError(GULLoggerService service,
BOOL force,
NSString *messageCode,
NSString *message,
...) NS_FORMAT_FUNCTION(4, 5);
extern void GULLogWarning(GULLoggerService service,
BOOL force,
NSString *messageCode,
NSString *message,
...) NS_FORMAT_FUNCTION(4, 5);
extern void GULLogNotice(GULLoggerService service,
BOOL force,
NSString *messageCode,
NSString *message,
...) NS_FORMAT_FUNCTION(4, 5);
extern void GULLogInfo(GULLoggerService service,
BOOL force,
NSString *messageCode,
NSString *message,
...) NS_FORMAT_FUNCTION(4, 5);
extern void GULLogDebug(GULLoggerService service,
BOOL force,
NSString *messageCode,
NSString *message,
...) NS_FORMAT_FUNCTION(4, 5);
#ifdef __cplusplus
} // extern "C"
#endif // __cplusplus
@interface GULLoggerWrapper : NSObject
/**
* Objective-C wrapper for GULLogBasic to allow weak linking to GULLogger
* (required) log level (one of the GULLoggerLevel enum values).
* (required) service name of type GULLoggerService.
* (required) message code starting with "I-" which means iOS, followed by a capitalized
* three-character service identifier and a six digit integer message ID that is unique
* within the service.
* An example of the message code is @"I-COR000001".
* (required) message string which can be a format string.
* (optional) variable arguments list obtained from calling va_start, used when message is a format
* string.
*/
+ (void)logWithLevel:(GULLoggerLevel)level
withService:(GULLoggerService)service
withCode:(NSString *)messageCode
withMessage:(NSString *)message
withArgs:(va_list)args;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2018 Google
*
* 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 <Foundation/Foundation.h>
/**
* The log levels used by internal logging.
*/
typedef NS_ENUM(NSInteger, GULLoggerLevel) {
/** Error level, matches ASL_LEVEL_ERR. */
GULLoggerLevelError = 3,
/** Warning level, matches ASL_LEVEL_WARNING. */
GULLoggerLevelWarning = 4,
/** Notice level, matches ASL_LEVEL_NOTICE. */
GULLoggerLevelNotice = 5,
/** Info level, matches ASL_LEVEL_INFO. */
GULLoggerLevelInfo = 6,
/** Debug level, matches ASL_LEVEL_DEBUG. */
GULLoggerLevelDebug = 7,
/** Minimum log level. */
GULLoggerLevelMin = GULLoggerLevelError,
/** Maximum log level. */
GULLoggerLevelMax = GULLoggerLevelDebug
} NS_SWIFT_NAME(GoogleLoggerLevel);

View File

@@ -0,0 +1,207 @@
// Copyright 2018 Google
//
// 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 "GoogleUtilities/NSData+zlib/Public/GoogleUtilities/GULNSData+zlib.h"
#import <zlib.h>
#define kChunkSize 1024
#define Z_DEFAULT_COMPRESSION (-1)
NSString *const GULNSDataZlibErrorDomain = @"com.google.GULNSDataZlibErrorDomain";
NSString *const GULNSDataZlibErrorKey = @"GULNSDataZlibErrorKey";
NSString *const GULNSDataZlibRemainingBytesKey = @"GULNSDataZlibRemainingBytesKey";
@implementation NSData (GULGzip)
+ (NSData *)gul_dataByInflatingGzippedData:(NSData *)data error:(NSError **)error {
const void *bytes = [data bytes];
NSUInteger length = [data length];
if (!bytes || !length) {
return nil;
}
#if defined(__LP64__) && __LP64__
// Don't support > 32bit length for 64 bit, see note in header.
if (length > UINT_MAX) {
return nil;
}
#endif
z_stream strm;
bzero(&strm, sizeof(z_stream));
// Setup the input.
strm.avail_in = (unsigned int)length;
strm.next_in = (unsigned char *)bytes;
int windowBits = 15; // 15 to enable any window size
windowBits += 32; // and +32 to enable zlib or gzip header detection.
int retCode;
if ((retCode = inflateInit2(&strm, windowBits)) != Z_OK) {
if (error) {
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:retCode]
forKey:GULNSDataZlibErrorKey];
*error = [NSError errorWithDomain:GULNSDataZlibErrorDomain
code:GULNSDataZlibErrorInternal
userInfo:userInfo];
}
return nil;
}
// Hint the size at 4x the input size.
NSMutableData *result = [NSMutableData dataWithCapacity:(length * 4)];
unsigned char output[kChunkSize];
// Loop to collect the data.
do {
// Update what we're passing in.
strm.avail_out = kChunkSize;
strm.next_out = output;
retCode = inflate(&strm, Z_NO_FLUSH);
if ((retCode != Z_OK) && (retCode != Z_STREAM_END)) {
if (error) {
NSMutableDictionary *userInfo =
[NSMutableDictionary dictionaryWithObject:[NSNumber numberWithInt:retCode]
forKey:GULNSDataZlibErrorKey];
if (strm.msg) {
NSString *message = [NSString stringWithUTF8String:strm.msg];
if (message) {
[userInfo setObject:message forKey:NSLocalizedDescriptionKey];
}
}
*error = [NSError errorWithDomain:GULNSDataZlibErrorDomain
code:GULNSDataZlibErrorInternal
userInfo:userInfo];
}
inflateEnd(&strm);
return nil;
}
// Collect what we got.
unsigned gotBack = kChunkSize - strm.avail_out;
if (gotBack > 0) {
[result appendBytes:output length:gotBack];
}
} while (retCode == Z_OK);
// Make sure there wasn't more data tacked onto the end of a valid compressed stream.
if (strm.avail_in != 0) {
if (error) {
NSDictionary *userInfo =
[NSDictionary dictionaryWithObject:[NSNumber numberWithUnsignedInt:strm.avail_in]
forKey:GULNSDataZlibRemainingBytesKey];
*error = [NSError errorWithDomain:GULNSDataZlibErrorDomain
code:GULNSDataZlibErrorDataRemaining
userInfo:userInfo];
}
result = nil;
}
// The only way out of the loop was by hitting the end of the stream.
NSAssert(retCode == Z_STREAM_END,
@"Thought we finished inflate w/o getting a result of stream end, code %d", retCode);
// Clean up.
inflateEnd(&strm);
return result;
}
+ (NSData *)gul_dataByGzippingData:(NSData *)data error:(NSError **)error {
const void *bytes = [data bytes];
NSUInteger length = [data length];
int level = Z_DEFAULT_COMPRESSION;
if (!bytes || !length) {
return nil;
}
#if defined(__LP64__) && __LP64__
// Don't support > 32bit length for 64 bit, see note in header.
if (length > UINT_MAX) {
if (error) {
*error = [NSError errorWithDomain:GULNSDataZlibErrorDomain
code:GULNSDataZlibErrorGreaterThan32BitsToCompress
userInfo:nil];
}
return nil;
}
#endif
z_stream strm;
bzero(&strm, sizeof(z_stream));
int memLevel = 8; // Default.
int windowBits = 15 + 16; // Enable gzip header instead of zlib header.
int retCode;
if ((retCode = deflateInit2(&strm, level, Z_DEFLATED, windowBits, memLevel,
Z_DEFAULT_STRATEGY)) != Z_OK) {
if (error) {
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:retCode]
forKey:GULNSDataZlibErrorKey];
*error = [NSError errorWithDomain:GULNSDataZlibErrorDomain
code:GULNSDataZlibErrorInternal
userInfo:userInfo];
}
return nil;
}
// Hint the size at 1/4 the input size.
NSMutableData *result = [NSMutableData dataWithCapacity:(length / 4)];
unsigned char output[kChunkSize];
// Setup the input.
strm.avail_in = (unsigned int)length;
strm.next_in = (unsigned char *)bytes;
// Collect the data.
do {
// update what we're passing in
strm.avail_out = kChunkSize;
strm.next_out = output;
retCode = deflate(&strm, Z_FINISH);
if ((retCode != Z_OK) && (retCode != Z_STREAM_END)) {
if (error) {
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:retCode]
forKey:GULNSDataZlibErrorKey];
*error = [NSError errorWithDomain:GULNSDataZlibErrorDomain
code:GULNSDataZlibErrorInternal
userInfo:userInfo];
}
deflateEnd(&strm);
return nil;
}
// Collect what we got.
unsigned gotBack = kChunkSize - strm.avail_out;
if (gotBack > 0) {
[result appendBytes:output length:gotBack];
}
} while (retCode == Z_OK);
// If the loop exits, it used all input and the stream ended.
NSAssert(strm.avail_in == 0,
@"Should have finished deflating without using all input, %u bytes left", strm.avail_in);
NSAssert(retCode == Z_STREAM_END,
@"thought we finished deflate w/o getting a result of stream end, code %d", retCode);
// Clean up.
deflateEnd(&strm);
return result;
}
@end

View File

@@ -0,0 +1,49 @@
// Copyright 2018 Google
//
// 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 <Foundation/Foundation.h>
/// This is a copy of Google Toolbox for Mac library to avoid creating an extra framework.
// NOTE: For 64bit, none of these apis handle input sizes >32bits, they will return nil when given
// such data. To handle data of that size you really should be streaming it rather then doing it all
// in memory.
@interface NSData (GULGzip)
/// Returns an data as the result of decompressing the payload of |data|.The data to decompress must
/// be a gzipped payloads.
+ (NSData *)gul_dataByInflatingGzippedData:(NSData *)data error:(NSError **)error;
/// Returns an compressed data with the result of gzipping the payload of |data|. Uses the default
/// compression level.
+ (NSData *)gul_dataByGzippingData:(NSData *)data error:(NSError **)error;
FOUNDATION_EXPORT NSString *const GULNSDataZlibErrorDomain;
FOUNDATION_EXPORT NSString *const GULNSDataZlibErrorKey; // NSNumber
FOUNDATION_EXPORT NSString *const GULNSDataZlibRemainingBytesKey; // NSNumber
typedef NS_ENUM(NSInteger, GULNSDataZlibError) {
GULNSDataZlibErrorGreaterThan32BitsToCompress = 1024,
// An internal zlib error.
// GULNSDataZlibErrorKey will contain the error value.
// NSLocalizedDescriptionKey may contain an error string from zlib.
// Look in zlib.h for list of errors.
GULNSDataZlibErrorInternal,
// There was left over data in the buffer that was not used.
// GULNSDataZlibRemainingBytesKey will contain number of remaining bytes.
GULNSDataZlibErrorDataRemaining
};
@end

View File

@@ -0,0 +1,101 @@
// Copyright 2017 Google
//
// 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 "GoogleUtilities/Network/Public/GoogleUtilities/GULMutableDictionary.h"
@implementation GULMutableDictionary {
/// The mutable dictionary.
NSMutableDictionary *_objects;
/// Serial synchronization queue. All reads should use dispatch_sync, while writes use
/// dispatch_async.
dispatch_queue_t _queue;
}
- (instancetype)init {
self = [super init];
if (self) {
_objects = [[NSMutableDictionary alloc] init];
_queue = dispatch_queue_create("GULMutableDictionary", DISPATCH_QUEUE_SERIAL);
}
return self;
}
- (NSString *)description {
__block NSString *description;
dispatch_sync(_queue, ^{
description = self->_objects.description;
});
return description;
}
- (id)objectForKey:(id)key {
__block id object;
dispatch_sync(_queue, ^{
object = [self->_objects objectForKey:key];
});
return object;
}
- (void)setObject:(id)object forKey:(id<NSCopying>)key {
dispatch_async(_queue, ^{
[self->_objects setObject:object forKey:key];
});
}
- (void)removeObjectForKey:(id)key {
dispatch_async(_queue, ^{
[self->_objects removeObjectForKey:key];
});
}
- (void)removeAllObjects {
dispatch_async(_queue, ^{
[self->_objects removeAllObjects];
});
}
- (NSUInteger)count {
__block NSUInteger count;
dispatch_sync(_queue, ^{
count = self->_objects.count;
});
return count;
}
- (id)objectForKeyedSubscript:(id<NSCopying>)key {
__block id object;
dispatch_sync(_queue, ^{
object = self->_objects[key];
});
return object;
}
- (void)setObject:(id)obj forKeyedSubscript:(id<NSCopying>)key {
dispatch_async(_queue, ^{
self->_objects[key] = obj;
});
}
- (NSDictionary *)dictionary {
__block NSDictionary *dictionary;
dispatch_sync(_queue, ^{
dictionary = [self->_objects copy];
});
return dictionary;
}
@end

View File

@@ -0,0 +1,405 @@
// Copyright 2017 Google
//
// 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 "GoogleUtilities/Network/Public/GoogleUtilities/GULNetwork.h"
#import "GoogleUtilities/Network/Public/GoogleUtilities/GULNetworkMessageCode.h"
#import "GoogleUtilities/Logger/Public/GoogleUtilities/GULLogger.h"
#import "GoogleUtilities/NSData+zlib/Public/GoogleUtilities/GULNSData+zlib.h"
#import "GoogleUtilities/Network/GULNetworkInternal.h"
#import "GoogleUtilities/Network/Public/GoogleUtilities/GULMutableDictionary.h"
#import "GoogleUtilities/Network/Public/GoogleUtilities/GULNetworkConstants.h"
#import "GoogleUtilities/Reachability/Public/GoogleUtilities/GULReachabilityChecker.h"
/// Constant string for request header Content-Encoding.
static NSString *const kGULNetworkContentCompressionKey = @"Content-Encoding";
/// Constant string for request header Content-Encoding value.
static NSString *const kGULNetworkContentCompressionValue = @"gzip";
/// Constant string for request header Content-Length.
static NSString *const kGULNetworkContentLengthKey = @"Content-Length";
/// Constant string for request header Content-Type.
static NSString *const kGULNetworkContentTypeKey = @"Content-Type";
/// Constant string for request header Content-Type value.
static NSString *const kGULNetworkContentTypeValue = @"application/x-www-form-urlencoded";
/// Constant string for GET request method.
static NSString *const kGULNetworkGETRequestMethod = @"GET";
/// Constant string for POST request method.
static NSString *const kGULNetworkPOSTRequestMethod = @"POST";
/// Default constant string as a prefix for network logger.
static NSString *const kGULNetworkLogTag = @"Google/Utilities/Network";
@interface GULNetwork () <GULReachabilityDelegate, GULNetworkLoggerDelegate>
@end
@implementation GULNetwork {
/// Network reachability.
GULReachabilityChecker *_reachability;
/// The dictionary of requests by session IDs { NSString : id }.
GULMutableDictionary *_requests;
}
- (instancetype)init {
return [self initWithReachabilityHost:kGULNetworkReachabilityHost];
}
- (instancetype)initWithReachabilityHost:(NSString *)reachabilityHost {
self = [super init];
if (self) {
// Setup reachability.
_reachability = [[GULReachabilityChecker alloc] initWithReachabilityDelegate:self
withHost:reachabilityHost];
if (![_reachability start]) {
return nil;
}
_requests = [[GULMutableDictionary alloc] init];
_timeoutInterval = kGULNetworkTimeOutInterval;
}
return self;
}
- (void)dealloc {
_reachability.reachabilityDelegate = nil;
[_reachability stop];
}
#pragma mark - External Methods
+ (void)handleEventsForBackgroundURLSessionID:(NSString *)sessionID
completionHandler:(GULNetworkSystemCompletionHandler)completionHandler {
[GULNetworkURLSession handleEventsForBackgroundURLSessionID:sessionID
completionHandler:completionHandler];
}
- (NSString *)postURL:(NSURL *)url
payload:(NSData *)payload
queue:(dispatch_queue_t)queue
usingBackgroundSession:(BOOL)usingBackgroundSession
completionHandler:(GULNetworkCompletionHandler)handler {
return [self postURL:url
headers:nil
payload:payload
queue:queue
usingBackgroundSession:usingBackgroundSession
completionHandler:handler];
}
- (NSString *)postURL:(NSURL *)url
headers:(NSDictionary *)headers
payload:(NSData *)payload
queue:(dispatch_queue_t)queue
usingBackgroundSession:(BOOL)usingBackgroundSession
completionHandler:(GULNetworkCompletionHandler)handler {
if (!url.absoluteString.length) {
[self handleErrorWithCode:GULErrorCodeNetworkInvalidURL queue:queue withHandler:handler];
return nil;
}
NSTimeInterval timeOutInterval = _timeoutInterval ?: kGULNetworkTimeOutInterval;
NSMutableURLRequest *request =
[[NSMutableURLRequest alloc] initWithURL:url
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:timeOutInterval];
if (!request) {
[self handleErrorWithCode:GULErrorCodeNetworkSessionTaskCreation
queue:queue
withHandler:handler];
return nil;
}
request.allHTTPHeaderFields = headers;
NSError *compressError = nil;
NSData *compressedData = [NSData gul_dataByGzippingData:payload error:&compressError];
if (!compressedData || compressError) {
if (compressError || payload.length > 0) {
// If the payload is not empty but it fails to compress the payload, something has been wrong.
[self handleErrorWithCode:GULErrorCodeNetworkPayloadCompression
queue:queue
withHandler:handler];
return nil;
}
compressedData = [[NSData alloc] init];
}
NSString *postLength = @(compressedData.length).stringValue;
// Set up the request with the compressed data.
[request setValue:postLength forHTTPHeaderField:kGULNetworkContentLengthKey];
request.HTTPBody = compressedData;
request.HTTPMethod = kGULNetworkPOSTRequestMethod;
[request setValue:kGULNetworkContentTypeValue forHTTPHeaderField:kGULNetworkContentTypeKey];
[request setValue:kGULNetworkContentCompressionValue
forHTTPHeaderField:kGULNetworkContentCompressionKey];
GULNetworkURLSession *fetcher = [[GULNetworkURLSession alloc] initWithNetworkLoggerDelegate:self];
fetcher.backgroundNetworkEnabled = usingBackgroundSession;
__weak GULNetwork *weakSelf = self;
NSString *requestID = [fetcher
sessionIDFromAsyncPOSTRequest:request
completionHandler:^(NSHTTPURLResponse *response, NSData *data,
NSString *sessionID, NSError *error) {
GULNetwork *strongSelf = weakSelf;
if (!strongSelf) {
return;
}
dispatch_queue_t queueToDispatch = queue ? queue : dispatch_get_main_queue();
dispatch_async(queueToDispatch, ^{
if (sessionID.length) {
[strongSelf->_requests removeObjectForKey:sessionID];
}
if (handler) {
handler(response, data, error);
}
});
}];
if (!requestID) {
[self handleErrorWithCode:GULErrorCodeNetworkSessionTaskCreation
queue:queue
withHandler:handler];
return nil;
}
[self GULNetwork_logWithLevel:kGULNetworkLogLevelDebug
messageCode:kGULNetworkMessageCodeNetwork000
message:@"Uploading data. Host"
context:url];
_requests[requestID] = fetcher;
return requestID;
}
- (NSString *)getURL:(NSURL *)url
headers:(NSDictionary *)headers
queue:(dispatch_queue_t)queue
usingBackgroundSession:(BOOL)usingBackgroundSession
completionHandler:(GULNetworkCompletionHandler)handler {
if (!url.absoluteString.length) {
[self handleErrorWithCode:GULErrorCodeNetworkInvalidURL queue:queue withHandler:handler];
return nil;
}
NSTimeInterval timeOutInterval = _timeoutInterval ?: kGULNetworkTimeOutInterval;
NSMutableURLRequest *request =
[[NSMutableURLRequest alloc] initWithURL:url
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:timeOutInterval];
if (!request) {
[self handleErrorWithCode:GULErrorCodeNetworkSessionTaskCreation
queue:queue
withHandler:handler];
return nil;
}
request.HTTPMethod = kGULNetworkGETRequestMethod;
request.allHTTPHeaderFields = headers;
GULNetworkURLSession *fetcher = [[GULNetworkURLSession alloc] initWithNetworkLoggerDelegate:self];
fetcher.backgroundNetworkEnabled = usingBackgroundSession;
__weak GULNetwork *weakSelf = self;
NSString *requestID = [fetcher
sessionIDFromAsyncGETRequest:request
completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSString *sessionID,
NSError *error) {
GULNetwork *strongSelf = weakSelf;
if (!strongSelf) {
return;
}
dispatch_queue_t queueToDispatch = queue ? queue : dispatch_get_main_queue();
dispatch_async(queueToDispatch, ^{
if (sessionID.length) {
[strongSelf->_requests removeObjectForKey:sessionID];
}
if (handler) {
handler(response, data, error);
}
});
}];
if (!requestID) {
[self handleErrorWithCode:GULErrorCodeNetworkSessionTaskCreation
queue:queue
withHandler:handler];
return nil;
}
[self GULNetwork_logWithLevel:kGULNetworkLogLevelDebug
messageCode:kGULNetworkMessageCodeNetwork001
message:@"Downloading data. Host"
context:url];
_requests[requestID] = fetcher;
return requestID;
}
- (BOOL)hasUploadInProgress {
return _requests.count > 0;
}
#pragma mark - Network Reachability
/// Tells reachability delegate to call reachabilityDidChangeToStatus: to notify the network
/// reachability has changed.
- (void)reachability:(GULReachabilityChecker *)reachability
statusChanged:(GULReachabilityStatus)status {
_networkConnected = (status == kGULReachabilityViaCellular || status == kGULReachabilityViaWifi);
[_reachabilityDelegate reachabilityDidChange];
}
#pragma mark - Network logger delegate
- (void)setLoggerDelegate:(id<GULNetworkLoggerDelegate>)loggerDelegate {
// Explicitly check whether the delegate responds to the methods because conformsToProtocol does
// not work correctly even though the delegate does respond to the methods.
if (!loggerDelegate ||
![loggerDelegate respondsToSelector:@selector(GULNetwork_logWithLevel:
messageCode:message:contexts:)] ||
![loggerDelegate respondsToSelector:@selector(GULNetwork_logWithLevel:
messageCode:message:context:)] ||
![loggerDelegate respondsToSelector:@selector(GULNetwork_logWithLevel:
messageCode:message:)]) {
GULLogError(kGULLoggerNetwork, NO,
[NSString stringWithFormat:@"I-NET%06ld", (long)kGULNetworkMessageCodeNetwork002],
@"Cannot set the network logger delegate: delegate does not conform to the network "
"logger protocol.");
return;
}
_loggerDelegate = loggerDelegate;
}
#pragma mark - Private methods
/// Handles network error and calls completion handler with the error.
- (void)handleErrorWithCode:(NSInteger)code
queue:(dispatch_queue_t)queue
withHandler:(GULNetworkCompletionHandler)handler {
NSDictionary *userInfo = @{kGULNetworkErrorContext : @"Failed to create network request"};
NSError *error = [[NSError alloc] initWithDomain:kGULNetworkErrorDomain
code:code
userInfo:userInfo];
[self GULNetwork_logWithLevel:kGULNetworkLogLevelWarning
messageCode:kGULNetworkMessageCodeNetwork002
message:@"Failed to create network request. Code, error"
contexts:@[ @(code), error ]];
if (handler) {
dispatch_queue_t queueToDispatch = queue ? queue : dispatch_get_main_queue();
dispatch_async(queueToDispatch, ^{
handler(nil, nil, error);
});
}
}
#pragma mark - Network logger
- (void)GULNetwork_logWithLevel:(GULNetworkLogLevel)logLevel
messageCode:(GULNetworkMessageCode)messageCode
message:(NSString *)message
contexts:(NSArray *)contexts {
// Let the delegate log the message if there is a valid logger delegate. Otherwise, just log
// errors/warnings/info messages to the console log.
if (_loggerDelegate) {
[_loggerDelegate GULNetwork_logWithLevel:logLevel
messageCode:messageCode
message:message
contexts:contexts];
return;
}
if (_isDebugModeEnabled || logLevel == kGULNetworkLogLevelError ||
logLevel == kGULNetworkLogLevelWarning || logLevel == kGULNetworkLogLevelInfo) {
NSString *formattedMessage = GULStringWithLogMessage(message, logLevel, contexts);
NSLog(@"%@", formattedMessage);
GULLogBasic((GULLoggerLevel)logLevel, kGULLoggerNetwork, NO,
[NSString stringWithFormat:@"I-NET%06ld", (long)messageCode], formattedMessage,
NULL);
}
}
- (void)GULNetwork_logWithLevel:(GULNetworkLogLevel)logLevel
messageCode:(GULNetworkMessageCode)messageCode
message:(NSString *)message
context:(id)context {
if (_loggerDelegate) {
[_loggerDelegate GULNetwork_logWithLevel:logLevel
messageCode:messageCode
message:message
context:context];
return;
}
NSArray *contexts = context ? @[ context ] : @[];
[self GULNetwork_logWithLevel:logLevel messageCode:messageCode message:message contexts:contexts];
}
- (void)GULNetwork_logWithLevel:(GULNetworkLogLevel)logLevel
messageCode:(GULNetworkMessageCode)messageCode
message:(NSString *)message {
if (_loggerDelegate) {
[_loggerDelegate GULNetwork_logWithLevel:logLevel messageCode:messageCode message:message];
return;
}
[self GULNetwork_logWithLevel:logLevel messageCode:messageCode message:message contexts:@[]];
}
/// Returns a string for the given log level (e.g. kGULNetworkLogLevelError -> @"ERROR").
static NSString *GULLogLevelDescriptionFromLogLevel(GULNetworkLogLevel logLevel) {
static NSDictionary *levelNames = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
levelNames = @{
@(kGULNetworkLogLevelError) : @"ERROR",
@(kGULNetworkLogLevelWarning) : @"WARNING",
@(kGULNetworkLogLevelInfo) : @"INFO",
@(kGULNetworkLogLevelDebug) : @"DEBUG"
};
});
return levelNames[@(logLevel)];
}
/// Returns a formatted string to be used for console logging.
static NSString *GULStringWithLogMessage(NSString *message,
GULNetworkLogLevel logLevel,
NSArray *contexts) {
if (!message) {
message = @"(Message was nil)";
} else if (!message.length) {
message = @"(Message was empty)";
}
NSMutableString *result = [[NSMutableString alloc]
initWithFormat:@"<%@/%@> %@", kGULNetworkLogTag, GULLogLevelDescriptionFromLogLevel(logLevel),
message];
if (!contexts.count) {
return result;
}
NSMutableArray *formattedContexts = [[NSMutableArray alloc] init];
for (id item in contexts) {
[formattedContexts addObject:(item != [NSNull null] ? item : @"(nil)")];
}
[result appendString:@": "];
[result appendString:[formattedContexts componentsJoinedByString:@", "]];
return result;
}
@end

View File

@@ -0,0 +1,41 @@
// Copyright 2017 Google
//
// 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 "GoogleUtilities/Network/Public/GoogleUtilities/GULNetworkConstants.h"
#import "GoogleUtilities/Logger/Public/GoogleUtilities/GULLogger.h"
#import <Foundation/Foundation.h>
NSString *const kGULNetworkBackgroundSessionConfigIDPrefix = @"com.gul.network.background-upload";
NSString *const kGULNetworkApplicationSupportSubdirectory = @"GUL/Network";
NSString *const kGULNetworkTempDirectoryName = @"GULNetworkTemporaryDirectory";
const NSTimeInterval kGULNetworkTempFolderExpireTime = 60 * 60; // 1 hour
const NSTimeInterval kGULNetworkTimeOutInterval = 60; // 1 minute.
NSString *const kGULNetworkReachabilityHost = @"app-measurement.com";
NSString *const kGULNetworkErrorContext = @"Context";
const int kGULNetworkHTTPStatusOK = 200;
const int kGULNetworkHTTPStatusNoContent = 204;
const int kGULNetworkHTTPStatusCodeMultipleChoices = 300;
const int kGULNetworkHTTPStatusCodeMovedPermanently = 301;
const int kGULNetworkHTTPStatusCodeFound = 302;
const int kGULNetworkHTTPStatusCodeNotModified = 304;
const int kGULNetworkHTTPStatusCodeMovedTemporarily = 307;
const int kGULNetworkHTTPStatusCodeNotFound = 404;
const int kGULNetworkHTTPStatusCodeCannotAcceptTraffic = 429;
const int kGULNetworkHTTPStatusCodeUnavailable = 503;
NSString *const kGULNetworkErrorDomain = @"com.gul.network.ErrorDomain";
GULLoggerService kGULLoggerNetwork = @"[GULNetwork]";

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2017 Google
*
* 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 <Foundation/Foundation.h>
#import "GoogleUtilities/Logger/Public/GoogleUtilities/GULLogger.h"
extern NSString *const kGULNetworkErrorDomain;
/// The logger service for GULNetwork.
extern GULLoggerService kGULLoggerNetwork;

View File

@@ -0,0 +1,744 @@
// Copyright 2017 Google
//
// 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 <Foundation/Foundation.h>
#import "GoogleUtilities/Network/Public/GoogleUtilities/GULNetworkURLSession.h"
#import "GoogleUtilities/Logger/Public/GoogleUtilities/GULLogger.h"
#import "GoogleUtilities/Network/GULNetworkInternal.h"
#import "GoogleUtilities/Network/Public/GoogleUtilities/GULMutableDictionary.h"
#import "GoogleUtilities/Network/Public/GoogleUtilities/GULNetworkConstants.h"
#import "GoogleUtilities/Network/Public/GoogleUtilities/GULNetworkMessageCode.h"
@interface GULNetworkURLSession () <NSURLSessionDelegate,
NSURLSessionDataDelegate,
NSURLSessionDownloadDelegate,
NSURLSessionTaskDelegate>
@end
@implementation GULNetworkURLSession {
/// The handler to be called when the request completes or error has occurs.
GULNetworkURLSessionCompletionHandler _completionHandler;
/// Session ID generated randomly with a fixed prefix.
NSString *_sessionID;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
/// The session configuration. NSURLSessionConfiguration' is only available on iOS 7.0 or newer.
NSURLSessionConfiguration *_sessionConfig;
/// The current NSURLSession.
NSURLSession *__weak _Nullable _URLSession;
#pragma clang diagnostic pop
/// The path to the directory where all temporary files are stored before uploading.
NSURL *_networkDirectoryURL;
/// The downloaded data from fetching.
NSData *_downloadedData;
/// The path to the temporary file which stores the uploading data.
NSURL *_uploadingFileURL;
/// The current request.
NSURLRequest *_request;
}
#pragma mark - Init
- (instancetype)initWithNetworkLoggerDelegate:(id<GULNetworkLoggerDelegate>)networkLoggerDelegate {
self = [super init];
if (self) {
// Create URL to the directory where all temporary files to upload have to be stored.
#if TARGET_OS_TV
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
#else
NSArray *paths =
NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
#endif
NSString *storageDirectory = paths.firstObject;
NSArray *tempPathComponents = @[
storageDirectory, kGULNetworkApplicationSupportSubdirectory, kGULNetworkTempDirectoryName
];
_networkDirectoryURL = [NSURL fileURLWithPathComponents:tempPathComponents];
_sessionID = [NSString stringWithFormat:@"%@-%@", kGULNetworkBackgroundSessionConfigIDPrefix,
[[NSUUID UUID] UUIDString]];
_loggerDelegate = networkLoggerDelegate;
}
return self;
}
#pragma mark - External Methods
#pragma mark - To be called from AppDelegate
+ (void)handleEventsForBackgroundURLSessionID:(NSString *)sessionID
completionHandler:
(GULNetworkSystemCompletionHandler)systemCompletionHandler {
// The session may not be Analytics background. Ignore those that do not have the prefix.
if (![sessionID hasPrefix:kGULNetworkBackgroundSessionConfigIDPrefix]) {
return;
}
GULNetworkURLSession *fetcher = [self fetcherWithSessionIdentifier:sessionID];
if (fetcher != nil) {
[fetcher addSystemCompletionHandler:systemCompletionHandler forSession:sessionID];
} else {
GULLogError(kGULLoggerNetwork, NO,
[NSString stringWithFormat:@"I-NET%06ld", (long)kGULNetworkMessageCodeNetwork003],
@"Failed to retrieve background session with ID %@ after app is relaunched.",
sessionID);
}
}
#pragma mark - External Methods
/// Sends an async POST request using NSURLSession for iOS >= 7.0, and returns an ID of the
/// connection.
- (nullable NSString *)sessionIDFromAsyncPOSTRequest:(NSURLRequest *)request
completionHandler:(GULNetworkURLSessionCompletionHandler)handler
API_AVAILABLE(ios(7.0)) {
// NSURLSessionUploadTask does not work with NSData in the background.
// To avoid this issue, write the data to a temporary file to upload it.
// Make a temporary file with the data subset.
_uploadingFileURL = [self temporaryFilePathWithSessionID:_sessionID];
NSError *writeError;
NSURLSessionUploadTask *postRequestTask;
NSURLSession *session;
BOOL didWriteFile = NO;
// Clean up the entire temp folder to avoid temp files that remain in case the previous session
// crashed and did not clean up.
[self maybeRemoveTempFilesAtURL:_networkDirectoryURL
expiringTime:kGULNetworkTempFolderExpireTime];
// If there is no background network enabled, no need to write to file. This will allow default
// network session which runs on the foreground.
if (_backgroundNetworkEnabled && [self ensureTemporaryDirectoryExists]) {
didWriteFile = [request.HTTPBody writeToFile:_uploadingFileURL.path
options:NSDataWritingAtomic
error:&writeError];
if (writeError) {
[_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelError
messageCode:kGULNetworkMessageCodeURLSession000
message:@"Failed to write request data to file"
context:writeError];
}
}
if (didWriteFile) {
// Exclude this file from backing up to iTunes. There are conflicting reports that excluding
// directory from backing up does not exclude files of that directory from backing up.
[self excludeFromBackupForURL:_uploadingFileURL];
_sessionConfig = [self backgroundSessionConfigWithSessionID:_sessionID];
[self populateSessionConfig:_sessionConfig withRequest:request];
session = [NSURLSession sessionWithConfiguration:_sessionConfig
delegate:self
delegateQueue:[NSOperationQueue mainQueue]];
postRequestTask = [session uploadTaskWithRequest:request fromFile:_uploadingFileURL];
} else {
// If we cannot write to file, just send it in the foreground.
_sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
[self populateSessionConfig:_sessionConfig withRequest:request];
session = [NSURLSession sessionWithConfiguration:_sessionConfig
delegate:self
delegateQueue:[NSOperationQueue mainQueue]];
// To avoid a runtime warning in Xcode 15 Beta 4, the given `URLRequest`
// should have a nil `HTTPBody`. To workaround this, the given `URLRequest`
// is copied and the `HTTPBody` data is removed.
NSData *givenRequestHTTPBody = [request.HTTPBody copy];
NSMutableURLRequest *requestWithoutHTTPBody = [request mutableCopy];
requestWithoutHTTPBody.HTTPBody = nil;
postRequestTask = [session uploadTaskWithRequest:requestWithoutHTTPBody
fromData:givenRequestHTTPBody];
}
if (!session || !postRequestTask) {
NSError *error = [[NSError alloc]
initWithDomain:kGULNetworkErrorDomain
code:GULErrorCodeNetworkRequestCreation
userInfo:@{kGULNetworkErrorContext : @"Cannot create network session"}];
[self callCompletionHandler:handler withResponse:nil data:nil error:error];
return nil;
}
_URLSession = session;
// Save the session into memory.
[[self class] setSessionInFetcherMap:self forSessionID:_sessionID];
_request = [request copy];
// Store completion handler because background session does not accept handler block but custom
// delegate.
_completionHandler = [handler copy];
[postRequestTask resume];
return _sessionID;
}
/// Sends an async GET request using NSURLSession for iOS >= 7.0, and returns an ID of the session.
- (nullable NSString *)sessionIDFromAsyncGETRequest:(NSURLRequest *)request
completionHandler:(GULNetworkURLSessionCompletionHandler)handler
API_AVAILABLE(ios(7.0)) {
if (_backgroundNetworkEnabled) {
_sessionConfig = [self backgroundSessionConfigWithSessionID:_sessionID];
} else {
_sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
}
[self populateSessionConfig:_sessionConfig withRequest:request];
// Do not cache the GET request.
_sessionConfig.URLCache = nil;
NSURLSession *session = [NSURLSession sessionWithConfiguration:_sessionConfig
delegate:self
delegateQueue:[NSOperationQueue mainQueue]];
NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:request];
if (!session || !downloadTask) {
NSError *error = [[NSError alloc]
initWithDomain:kGULNetworkErrorDomain
code:GULErrorCodeNetworkRequestCreation
userInfo:@{kGULNetworkErrorContext : @"Cannot create network session"}];
[self callCompletionHandler:handler withResponse:nil data:nil error:error];
return nil;
}
_URLSession = session;
// Save the session into memory.
[[self class] setSessionInFetcherMap:self forSessionID:_sessionID];
_request = [request copy];
_completionHandler = [handler copy];
[downloadTask resume];
return _sessionID;
}
#pragma mark - NSURLSessionDataDelegate
/// Called by the NSURLSession when the data task has received some of the expected data.
/// Once the session is completed, URLSession:task:didCompleteWithError will be called and the
/// completion handler will be called with the downloaded data.
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data {
@synchronized(self) {
NSMutableData *mutableData = [[NSMutableData alloc] init];
if (_downloadedData) {
mutableData = _downloadedData.mutableCopy;
}
[mutableData appendData:data];
_downloadedData = mutableData;
}
}
#pragma mark - NSURLSessionTaskDelegate
/// Called by the NSURLSession once the download task is completed. The file is saved in the
/// provided URL so we need to read the data and store into _downloadedData. Once the session is
/// completed, URLSession:task:didCompleteWithError will be called and the completion handler will
/// be called with the downloaded data.
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)task
didFinishDownloadingToURL:(NSURL *)url API_AVAILABLE(ios(7.0)) {
if (!url.path) {
[_loggerDelegate
GULNetwork_logWithLevel:kGULNetworkLogLevelError
messageCode:kGULNetworkMessageCodeURLSession001
message:@"Unable to read downloaded data from empty temp path"];
_downloadedData = nil;
return;
}
NSError *error;
_downloadedData = [NSData dataWithContentsOfFile:url.path options:0 error:&error];
if (error) {
[_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelError
messageCode:kGULNetworkMessageCodeURLSession002
message:@"Cannot read the content of downloaded data"
context:error];
_downloadedData = nil;
}
}
#if TARGET_OS_IOS || TARGET_OS_TV
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
API_AVAILABLE(ios(7.0)) {
[_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelDebug
messageCode:kGULNetworkMessageCodeURLSession003
message:@"Background session finished"
context:session.configuration.identifier];
[self callSystemCompletionHandler:session.configuration.identifier];
}
#endif
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error API_AVAILABLE(ios(7.0)) {
// Avoid any chance of recursive behavior leading to it being used repeatedly.
GULNetworkURLSessionCompletionHandler handler = _completionHandler;
_completionHandler = nil;
if (task.response) {
// The following assertion should always be true for HTTP requests, see https://goo.gl/gVLxT7.
NSAssert([task.response isKindOfClass:[NSHTTPURLResponse class]], @"URL response must be HTTP");
// The server responded so ignore the error created by the system.
error = nil;
} else if (!error) {
error = [[NSError alloc]
initWithDomain:kGULNetworkErrorDomain
code:GULErrorCodeNetworkInvalidResponse
userInfo:@{kGULNetworkErrorContext : @"Network Error: Empty network response"}];
}
[self callCompletionHandler:handler
withResponse:(NSHTTPURLResponse *)task.response
data:_downloadedData
error:error];
// Remove the temp file to avoid trashing devices with lots of temp files.
[self removeTempItemAtURL:_uploadingFileURL];
// Try to clean up stale files again.
[self maybeRemoveTempFilesAtURL:_networkDirectoryURL
expiringTime:kGULNetworkTempFolderExpireTime];
// This is called without checking the sessionID here since non-background sessions
// won't have an ID.
[session finishTasksAndInvalidate];
// Explicitly remove the session so it won't be reused. The weak map table should
// remove the session on deallocation, but dealloc may not happen immediately after
// calling `finishTasksAndInvalidate`.
NSString *sessionID = session.configuration.identifier;
[[self class] setSessionInFetcherMap:nil forSessionID:sessionID];
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition,
NSURLCredential *credential))completionHandler
API_AVAILABLE(ios(7.0)) {
// The handling is modeled after GTMSessionFetcher.
if ([challenge.protectionSpace.authenticationMethod
isEqualToString:NSURLAuthenticationMethodServerTrust]) {
SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
if (serverTrust == NULL) {
[_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelDebug
messageCode:kGULNetworkMessageCodeURLSession004
message:@"Received empty server trust for host. Host"
context:_request.URL];
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
return;
}
NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust];
if (!credential) {
[_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelWarning
messageCode:kGULNetworkMessageCodeURLSession005
message:@"Unable to verify server identity. Host"
context:_request.URL];
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
return;
}
[_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelDebug
messageCode:kGULNetworkMessageCodeURLSession006
message:@"Received SSL challenge for host. Host"
context:_request.URL];
void (^callback)(BOOL) = ^(BOOL allow) {
if (allow) {
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
} else {
[self->_loggerDelegate
GULNetwork_logWithLevel:kGULNetworkLogLevelDebug
messageCode:kGULNetworkMessageCodeURLSession007
message:@"Cancelling authentication challenge for host. Host"
context:self->_request.URL];
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
}
};
// Retain the trust object to avoid a SecTrustEvaluate() crash on iOS 7.
CFRetain(serverTrust);
// Evaluate the certificate chain.
//
// The delegate queue may be the main thread. Trust evaluation could cause some
// blocking network activity, so we must evaluate async, as documented at
// https://developer.apple.com/library/ios/technotes/tn2232/
dispatch_queue_t evaluateBackgroundQueue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(evaluateBackgroundQueue, ^{
SecTrustResultType trustEval = kSecTrustResultInvalid;
BOOL shouldAllow;
OSStatus trustError;
@synchronized([GULNetworkURLSession class]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
trustError = SecTrustEvaluate(serverTrust, &trustEval);
#pragma clang diagnostic pop
}
if (trustError != errSecSuccess) {
[self->_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelError
messageCode:kGULNetworkMessageCodeURLSession008
message:@"Cannot evaluate server trust. Error, host"
contexts:@[ @(trustError), self->_request.URL ]];
shouldAllow = NO;
} else {
// Having a trust level "unspecified" by the user is the usual result, described at
// https://developer.apple.com/library/mac/qa/qa1360
shouldAllow =
(trustEval == kSecTrustResultUnspecified || trustEval == kSecTrustResultProceed);
}
// Call the call back with the permission.
callback(shouldAllow);
CFRelease(serverTrust);
});
return;
}
// Default handling for other Auth Challenges.
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
}
#pragma mark - Internal Methods
/// Stores system completion handler with session ID as key.
- (void)addSystemCompletionHandler:(GULNetworkSystemCompletionHandler)handler
forSession:(NSString *)identifier {
if (!handler) {
[_loggerDelegate
GULNetwork_logWithLevel:kGULNetworkLogLevelError
messageCode:kGULNetworkMessageCodeURLSession009
message:@"Cannot store nil system completion handler in network"];
return;
}
if (!identifier.length) {
[_loggerDelegate
GULNetwork_logWithLevel:kGULNetworkLogLevelError
messageCode:kGULNetworkMessageCodeURLSession010
message:@"Cannot store system completion handler with empty network "
"session identifier"];
return;
}
GULMutableDictionary *systemCompletionHandlers =
[[self class] sessionIDToSystemCompletionHandlerDictionary];
if (systemCompletionHandlers[identifier]) {
[_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelWarning
messageCode:kGULNetworkMessageCodeURLSession011
message:@"Got multiple system handlers for a single session ID"
context:identifier];
}
systemCompletionHandlers[identifier] = handler;
}
/// Calls the system provided completion handler with the session ID stored in the dictionary.
/// The handler will be removed from the dictionary after being called.
- (void)callSystemCompletionHandler:(NSString *)identifier {
GULMutableDictionary *systemCompletionHandlers =
[[self class] sessionIDToSystemCompletionHandlerDictionary];
GULNetworkSystemCompletionHandler handler = [systemCompletionHandlers objectForKey:identifier];
if (handler) {
[systemCompletionHandlers removeObjectForKey:identifier];
dispatch_async(dispatch_get_main_queue(), ^{
handler();
});
}
}
/// Sets or updates the session ID of this session.
- (void)setSessionID:(NSString *)sessionID {
_sessionID = [sessionID copy];
}
/// Creates a background session configuration with the session ID using the supported method.
- (NSURLSessionConfiguration *)backgroundSessionConfigWithSessionID:(NSString *)sessionID {
return [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:sessionID];
}
- (void)maybeRemoveTempFilesAtURL:(NSURL *)folderURL expiringTime:(NSTimeInterval)staleTime {
if (!folderURL.absoluteString.length) {
return;
}
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *error = nil;
NSArray *properties = @[ NSURLCreationDateKey ];
NSArray *directoryContent =
[fileManager contentsOfDirectoryAtURL:folderURL
includingPropertiesForKeys:properties
options:NSDirectoryEnumerationSkipsSubdirectoryDescendants
error:&error];
if (error && error.code != NSFileReadNoSuchFileError) {
[_loggerDelegate
GULNetwork_logWithLevel:kGULNetworkLogLevelDebug
messageCode:kGULNetworkMessageCodeURLSession012
message:@"Cannot get files from the temporary network folder. Error"
context:error];
return;
}
if (!directoryContent.count) {
return;
}
NSTimeInterval now = [NSDate date].timeIntervalSince1970;
for (NSURL *tempFile in directoryContent) {
NSDate *creationDate;
BOOL getCreationDate = [tempFile getResourceValue:&creationDate
forKey:NSURLCreationDateKey
error:NULL];
if (!getCreationDate) {
continue;
}
NSTimeInterval creationTimeInterval = creationDate.timeIntervalSince1970;
if (fabs(now - creationTimeInterval) > staleTime) {
[self removeTempItemAtURL:tempFile];
}
}
}
/// Removes the temporary file written to disk for sending the request. It has to be cleaned up
/// after the session is done.
- (void)removeTempItemAtURL:(NSURL *)fileURL {
if (!fileURL.absoluteString.length) {
return;
}
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *error = nil;
if (![fileManager removeItemAtURL:fileURL error:&error] && error.code != NSFileNoSuchFileError) {
[_loggerDelegate
GULNetwork_logWithLevel:kGULNetworkLogLevelError
messageCode:kGULNetworkMessageCodeURLSession013
message:@"Failed to remove temporary uploading data file. Error"
context:error.localizedDescription];
}
}
/// Gets the fetcher with the session ID.
+ (instancetype)fetcherWithSessionIdentifier:(NSString *)sessionIdentifier {
GULNetworkURLSession *session = [self sessionFromFetcherMapForSessionID:sessionIdentifier];
if (!session && [sessionIdentifier hasPrefix:kGULNetworkBackgroundSessionConfigIDPrefix]) {
session = [[GULNetworkURLSession alloc] initWithNetworkLoggerDelegate:nil];
[session setSessionID:sessionIdentifier];
[self setSessionInFetcherMap:session forSessionID:sessionIdentifier];
}
return session;
}
/// Returns a map of the fetcher by session ID. Creates a map if it is not created.
/// When reading and writing from/to the session map, don't use this method directly.
/// To avoid thread safety issues, use one of the helper methods at the bottom of the
/// file: setSessionInFetcherMap:forSessionID:, sessionFromFetcherMapForSessionID:
+ (NSMapTable<NSString *, GULNetworkURLSession *> *)sessionIDToFetcherMap {
static NSMapTable *sessionIDToFetcherMap;
static dispatch_once_t sessionMapOnceToken;
dispatch_once(&sessionMapOnceToken, ^{
sessionIDToFetcherMap = [NSMapTable strongToWeakObjectsMapTable];
});
return sessionIDToFetcherMap;
}
+ (NSLock *)sessionIDToFetcherMapReadWriteLock {
static NSLock *lock;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
lock = [[NSLock alloc] init];
});
return lock;
}
/// Returns a map of system provided completion handler by session ID. Creates a map if it is not
/// created.
+ (GULMutableDictionary *)sessionIDToSystemCompletionHandlerDictionary {
static GULMutableDictionary *systemCompletionHandlers;
static dispatch_once_t systemCompletionHandlerOnceToken;
dispatch_once(&systemCompletionHandlerOnceToken, ^{
systemCompletionHandlers = [[GULMutableDictionary alloc] init];
});
return systemCompletionHandlers;
}
- (NSURL *)temporaryFilePathWithSessionID:(NSString *)sessionID {
NSString *tempName = [NSString stringWithFormat:@"GULUpload_temp_%@", sessionID];
return [_networkDirectoryURL URLByAppendingPathComponent:tempName];
}
/// Makes sure that the directory to store temp files exists. If not, tries to create it and returns
/// YES. If there is anything wrong, returns NO.
- (BOOL)ensureTemporaryDirectoryExists {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *error = nil;
// Create a temporary directory if it does not exist or was deleted.
if ([_networkDirectoryURL checkResourceIsReachableAndReturnError:&error]) {
return YES;
}
if (error && error.code != NSFileReadNoSuchFileError) {
[_loggerDelegate
GULNetwork_logWithLevel:kGULNetworkLogLevelWarning
messageCode:kGULNetworkMessageCodeURLSession014
message:@"Error while trying to access Network temp folder. Error"
context:error];
}
NSError *writeError = nil;
[fileManager createDirectoryAtURL:_networkDirectoryURL
withIntermediateDirectories:YES
attributes:nil
error:&writeError];
if (writeError) {
[_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelError
messageCode:kGULNetworkMessageCodeURLSession015
message:@"Cannot create temporary directory. Error"
context:writeError];
return NO;
}
// Set the iCloud exclusion attribute on the Documents URL.
[self excludeFromBackupForURL:_networkDirectoryURL];
return YES;
}
- (void)excludeFromBackupForURL:(NSURL *)url {
if (!url.path) {
return;
}
// Set the iCloud exclusion attribute on the Documents URL.
NSError *preventBackupError = nil;
[url setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:&preventBackupError];
if (preventBackupError) {
[_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelError
messageCode:kGULNetworkMessageCodeURLSession016
message:@"Cannot exclude temporary folder from iTunes backup"];
}
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
newRequest:(NSURLRequest *)request
completionHandler:(void (^)(NSURLRequest *))completionHandler API_AVAILABLE(ios(7.0)) {
NSArray *nonAllowedRedirectionCodes = @[
@(kGULNetworkHTTPStatusCodeFound), @(kGULNetworkHTTPStatusCodeMovedPermanently),
@(kGULNetworkHTTPStatusCodeMovedTemporarily), @(kGULNetworkHTTPStatusCodeMultipleChoices)
];
// Allow those not in the non allowed list to be followed.
if (![nonAllowedRedirectionCodes containsObject:@(response.statusCode)]) {
completionHandler(request);
return;
}
// Do not allow redirection if the response code is in the non-allowed list.
NSURLRequest *newRequest = request;
if (response) {
newRequest = nil;
}
completionHandler(newRequest);
}
#pragma mark - Helper Methods
+ (void)setSessionInFetcherMap:(GULNetworkURLSession *)session forSessionID:(NSString *)sessionID {
[[self sessionIDToFetcherMapReadWriteLock] lock];
GULNetworkURLSession *existingSession =
[[[self class] sessionIDToFetcherMap] objectForKey:sessionID];
if (existingSession) {
if (session) {
NSString *message = [NSString stringWithFormat:@"Discarding session: %@", existingSession];
[existingSession->_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelInfo
messageCode:kGULNetworkMessageCodeURLSession019
message:message];
}
[existingSession->_URLSession finishTasksAndInvalidate];
}
if (session) {
[[[self class] sessionIDToFetcherMap] setObject:session forKey:sessionID];
} else {
[[[self class] sessionIDToFetcherMap] removeObjectForKey:sessionID];
}
[[self sessionIDToFetcherMapReadWriteLock] unlock];
}
+ (nullable GULNetworkURLSession *)sessionFromFetcherMapForSessionID:(NSString *)sessionID {
[[self sessionIDToFetcherMapReadWriteLock] lock];
GULNetworkURLSession *session = [[[self class] sessionIDToFetcherMap] objectForKey:sessionID];
[[self sessionIDToFetcherMapReadWriteLock] unlock];
return session;
}
- (void)callCompletionHandler:(GULNetworkURLSessionCompletionHandler)handler
withResponse:(NSHTTPURLResponse *)response
data:(NSData *)data
error:(NSError *)error {
if (error) {
[_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelError
messageCode:kGULNetworkMessageCodeURLSession017
message:@"Encounter network error. Code, error"
contexts:@[ @(error.code), error ]];
}
if (handler) {
dispatch_async(dispatch_get_main_queue(), ^{
handler(response, data, self->_sessionID, error);
});
}
}
// Always use the request parameters even if the default session configuration is more restrictive.
- (void)populateSessionConfig:(NSURLSessionConfiguration *)sessionConfig
withRequest:(NSURLRequest *)request API_AVAILABLE(ios(7.0)) {
sessionConfig.HTTPAdditionalHeaders = request.allHTTPHeaderFields;
sessionConfig.timeoutIntervalForRequest = request.timeoutInterval;
sessionConfig.timeoutIntervalForResource = request.timeoutInterval;
sessionConfig.requestCachePolicy = request.cachePolicy;
}
@end

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2017 Google
*
* 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 <Foundation/Foundation.h>
/// A mutable dictionary that provides atomic accessor and mutators.
@interface GULMutableDictionary : NSObject
/// Returns an object given a key in the dictionary or nil if not found.
- (id)objectForKey:(id)key;
/// Updates the object given its key or adds it to the dictionary if it is not in the dictionary.
- (void)setObject:(id)object forKey:(id<NSCopying>)key;
/// Removes the object given its session ID from the dictionary.
- (void)removeObjectForKey:(id)key;
/// Removes all objects.
- (void)removeAllObjects;
/// Returns the number of current objects in the dictionary.
- (NSUInteger)count;
/// Returns an object given a key in the dictionary or nil if not found.
- (id)objectForKeyedSubscript:(id<NSCopying>)key;
/// Updates the object given its key or adds it to the dictionary if it is not in the dictionary.
- (void)setObject:(id)obj forKeyedSubscript:(id<NSCopying>)key;
/// Returns the immutable dictionary.
- (NSDictionary *)dictionary;
@end

View File

@@ -0,0 +1,97 @@
/*
* Copyright 2017 Google
*
* 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 <Foundation/Foundation.h>
#import "GULNetworkConstants.h"
#import "GULNetworkLoggerProtocol.h"
#import "GULNetworkURLSession.h"
/// Delegate protocol for GULNetwork events.
@protocol GULNetworkReachabilityDelegate
/// Tells the delegate to handle events when the network reachability changes to connected or not
/// connected.
- (void)reachabilityDidChange;
@end
/// The Network component that provides network status and handles network requests and responses.
/// This is not thread safe.
///
/// NOTE:
/// User must add FIRAnalytics handleEventsForBackgroundURLSessionID:completionHandler to the
/// AppDelegate application:handleEventsForBackgroundURLSession:completionHandler:
@interface GULNetwork : NSObject
/// Indicates if network connectivity is available.
@property(nonatomic, readonly, getter=isNetworkConnected) BOOL networkConnected;
/// Indicates if there are any uploads in progress.
@property(nonatomic, readonly, getter=hasUploadInProgress) BOOL uploadInProgress;
/// An optional delegate that can be used in the event when network reachability changes.
@property(nonatomic, weak) id<GULNetworkReachabilityDelegate> reachabilityDelegate;
/// An optional delegate that can be used to log messages, warnings or errors that occur in the
/// network operations.
@property(nonatomic, weak) id<GULNetworkLoggerDelegate> loggerDelegate;
/// Indicates whether the logger should display debug messages.
@property(nonatomic, assign) BOOL isDebugModeEnabled;
/// The time interval in seconds for the network request to timeout.
@property(nonatomic, assign) NSTimeInterval timeoutInterval;
/// Initializes with the default reachability host.
- (instancetype)init;
/// Initializes with a custom reachability host.
- (instancetype)initWithReachabilityHost:(NSString *)reachabilityHost;
/// Handles events when background session with the given ID has finished.
+ (void)handleEventsForBackgroundURLSessionID:(NSString *)sessionID
completionHandler:(GULNetworkSystemCompletionHandler)completionHandler;
/// Compresses and sends a POST request with the provided data to the URL. The session will be
/// background session if usingBackgroundSession is YES. Otherwise, the POST session is default
/// session. Returns a session ID or nil if an error occurs.
- (NSString *)postURL:(NSURL *)url
payload:(NSData *)payload
queue:(dispatch_queue_t)queue
usingBackgroundSession:(BOOL)usingBackgroundSession
completionHandler:(GULNetworkCompletionHandler)handler;
/// Compresses and sends a POST request with the provided headers and data to the URL. The session
/// will be background session if usingBackgroundSession is YES. Otherwise, the POST session is
/// default session. Returns a session ID or nil if an error occurs.
- (NSString *)postURL:(NSURL *)url
headers:(NSDictionary *)headers
payload:(NSData *)payload
queue:(dispatch_queue_t)queue
usingBackgroundSession:(BOOL)usingBackgroundSession
completionHandler:(GULNetworkCompletionHandler)handler;
/// Sends a GET request with the provided data to the URL. The session will be background session
/// if usingBackgroundSession is YES. Otherwise, the GET session is default session. Returns a
/// session ID or nil if an error occurs.
- (NSString *)getURL:(NSURL *)url
headers:(NSDictionary *)headers
queue:(dispatch_queue_t)queue
usingBackgroundSession:(BOOL)usingBackgroundSession
completionHandler:(GULNetworkCompletionHandler)handler;
@end

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2017 Google
*
* 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 <Foundation/Foundation.h>
/// Error codes in Firebase Network error domain.
/// Note: these error codes should never change. It would make it harder to decode the errors if
/// we inadvertently altered any of these codes in a future SDK version.
typedef NS_ENUM(NSInteger, GULNetworkErrorCode) {
/// Unknown error.
GULNetworkErrorCodeUnknown = 0,
/// Error occurs when the request URL is invalid.
GULErrorCodeNetworkInvalidURL = 1,
/// Error occurs when request cannot be constructed.
GULErrorCodeNetworkRequestCreation = 2,
/// Error occurs when payload cannot be compressed.
GULErrorCodeNetworkPayloadCompression = 3,
/// Error occurs when session task cannot be created.
GULErrorCodeNetworkSessionTaskCreation = 4,
/// Error occurs when there is no response.
GULErrorCodeNetworkInvalidResponse = 5
};
#pragma mark - Network constants
/// The prefix of the ID of the background session.
extern NSString *const kGULNetworkBackgroundSessionConfigIDPrefix;
/// The sub directory to store the files of data that is being uploaded in the background.
extern NSString *const kGULNetworkApplicationSupportSubdirectory;
/// Name of the temporary directory that stores files for background uploading.
extern NSString *const kGULNetworkTempDirectoryName;
/// The period when the temporary uploading file can stay.
extern const NSTimeInterval kGULNetworkTempFolderExpireTime;
/// The default network request timeout interval.
extern const NSTimeInterval kGULNetworkTimeOutInterval;
/// The host to check the reachability of the network.
extern NSString *const kGULNetworkReachabilityHost;
/// The key to get the error context of the UserInfo.
extern NSString *const kGULNetworkErrorContext;
#pragma mark - Network Status Code
extern const int kGULNetworkHTTPStatusOK;
extern const int kGULNetworkHTTPStatusNoContent;
extern const int kGULNetworkHTTPStatusCodeMultipleChoices;
extern const int kGULNetworkHTTPStatusCodeMovedPermanently;
extern const int kGULNetworkHTTPStatusCodeFound;
extern const int kGULNetworkHTTPStatusCodeNotModified;
extern const int kGULNetworkHTTPStatusCodeMovedTemporarily;
extern const int kGULNetworkHTTPStatusCodeNotFound;
extern const int kGULNetworkHTTPStatusCodeCannotAcceptTraffic;
extern const int kGULNetworkHTTPStatusCodeUnavailable;

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2017 Google
*
* 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 <Foundation/Foundation.h>
#import "GULNetworkMessageCode.h"
/// The log levels used by GULNetworkLogger.
typedef NS_ENUM(NSInteger, GULNetworkLogLevel) {
kGULNetworkLogLevelError = 3,
kGULNetworkLogLevelWarning = 4,
kGULNetworkLogLevelInfo = 6,
kGULNetworkLogLevelDebug = 7,
};
@protocol GULNetworkLoggerDelegate <NSObject>
@required
/// Tells the delegate to log a message with an array of contexts and the log level.
- (void)GULNetwork_logWithLevel:(GULNetworkLogLevel)logLevel
messageCode:(GULNetworkMessageCode)messageCode
message:(NSString *)message
contexts:(NSArray *)contexts;
/// Tells the delegate to log a message with a context and the log level.
- (void)GULNetwork_logWithLevel:(GULNetworkLogLevel)logLevel
messageCode:(GULNetworkMessageCode)messageCode
message:(NSString *)message
context:(id)context;
/// Tells the delegate to log a message with the log level.
- (void)GULNetwork_logWithLevel:(GULNetworkLogLevel)logLevel
messageCode:(GULNetworkMessageCode)messageCode
message:(NSString *)message;
@end

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2017 Google
*
* 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 <Foundation/Foundation.h>
// Make sure these codes do not overlap with any contained in the FIRAMessageCode enum.
typedef NS_ENUM(NSInteger, GULNetworkMessageCode) {
// GULNetwork.m
kGULNetworkMessageCodeNetwork000 = 900000, // I-NET900000
kGULNetworkMessageCodeNetwork001 = 900001, // I-NET900001
kGULNetworkMessageCodeNetwork002 = 900002, // I-NET900002
kGULNetworkMessageCodeNetwork003 = 900003, // I-NET900003
// GULNetworkURLSession.m
kGULNetworkMessageCodeURLSession000 = 901000, // I-NET901000
kGULNetworkMessageCodeURLSession001 = 901001, // I-NET901001
kGULNetworkMessageCodeURLSession002 = 901002, // I-NET901002
kGULNetworkMessageCodeURLSession003 = 901003, // I-NET901003
kGULNetworkMessageCodeURLSession004 = 901004, // I-NET901004
kGULNetworkMessageCodeURLSession005 = 901005, // I-NET901005
kGULNetworkMessageCodeURLSession006 = 901006, // I-NET901006
kGULNetworkMessageCodeURLSession007 = 901007, // I-NET901007
kGULNetworkMessageCodeURLSession008 = 901008, // I-NET901008
kGULNetworkMessageCodeURLSession009 = 901009, // I-NET901009
kGULNetworkMessageCodeURLSession010 = 901010, // I-NET901010
kGULNetworkMessageCodeURLSession011 = 901011, // I-NET901011
kGULNetworkMessageCodeURLSession012 = 901012, // I-NET901012
kGULNetworkMessageCodeURLSession013 = 901013, // I-NET901013
kGULNetworkMessageCodeURLSession014 = 901014, // I-NET901014
kGULNetworkMessageCodeURLSession015 = 901015, // I-NET901015
kGULNetworkMessageCodeURLSession016 = 901016, // I-NET901016
kGULNetworkMessageCodeURLSession017 = 901017, // I-NET901017
kGULNetworkMessageCodeURLSession018 = 901018, // I-NET901018
kGULNetworkMessageCodeURLSession019 = 901019, // I-NET901019
};

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2017 Google
*
* 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 <Foundation/Foundation.h>
#import "GULNetworkLoggerProtocol.h"
NS_ASSUME_NONNULL_BEGIN
typedef void (^GULNetworkCompletionHandler)(NSHTTPURLResponse *_Nullable response,
NSData *_Nullable data,
NSError *_Nullable error);
typedef void (^GULNetworkURLSessionCompletionHandler)(NSHTTPURLResponse *_Nullable response,
NSData *_Nullable data,
NSString *sessionID,
NSError *_Nullable error);
typedef void (^GULNetworkSystemCompletionHandler)(void);
/// The protocol that uses NSURLSession for iOS >= 7.0 to handle requests and responses.
@interface GULNetworkURLSession : NSObject
/// Indicates whether the background network is enabled. Default value is NO.
@property(nonatomic, getter=isBackgroundNetworkEnabled) BOOL backgroundNetworkEnabled;
/// The logger delegate to log message, errors or warnings that occur during the network operations.
@property(nonatomic, weak, nullable) id<GULNetworkLoggerDelegate> loggerDelegate;
/// Calls the system provided completion handler after the background session is finished.
+ (void)handleEventsForBackgroundURLSessionID:(NSString *)sessionID
completionHandler:(GULNetworkSystemCompletionHandler)completionHandler;
/// Initializes with logger delegate.
- (instancetype)initWithNetworkLoggerDelegate:
(nullable id<GULNetworkLoggerDelegate>)networkLoggerDelegate NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
/// Sends an asynchronous POST request and calls the provided completion handler when the request
/// completes or when errors occur, and returns an ID of the session/connection.
- (nullable NSString *)sessionIDFromAsyncPOSTRequest:(NSURLRequest *)request
completionHandler:(GULNetworkURLSessionCompletionHandler)handler;
/// Sends an asynchronous GET request and calls the provided completion handler when the request
/// completes or when errors occur, and returns an ID of the session.
- (nullable NSString *)sessionIDFromAsyncGETRequest:(NSURLRequest *)request
completionHandler:(GULNetworkURLSessionCompletionHandler)handler;
NS_ASSUME_NONNULL_END
@end

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array>
</array>
<key>NSPrivacyCollectedDataTypes</key>
<array>
</array>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>1C8F.1</string>
<string>C56D.1</string>
</array>
</dict>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2017 Google
*
* 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 "GoogleUtilities/Reachability/Public/GoogleUtilities/GULReachabilityChecker.h"
#if !TARGET_OS_WATCH
typedef SCNetworkReachabilityRef (*GULReachabilityCreateWithNameFn)(CFAllocatorRef allocator,
const char *host);
typedef Boolean (*GULReachabilitySetCallbackFn)(SCNetworkReachabilityRef target,
SCNetworkReachabilityCallBack callback,
SCNetworkReachabilityContext *context);
typedef Boolean (*GULReachabilityScheduleWithRunLoopFn)(SCNetworkReachabilityRef target,
CFRunLoopRef runLoop,
CFStringRef runLoopMode);
typedef Boolean (*GULReachabilityUnscheduleFromRunLoopFn)(SCNetworkReachabilityRef target,
CFRunLoopRef runLoop,
CFStringRef runLoopMode);
typedef void (*GULReachabilityReleaseFn)(CFTypeRef cf);
struct GULReachabilityApi {
GULReachabilityCreateWithNameFn createWithNameFn;
GULReachabilitySetCallbackFn setCallbackFn;
GULReachabilityScheduleWithRunLoopFn scheduleWithRunLoopFn;
GULReachabilityUnscheduleFromRunLoopFn unscheduleFromRunLoopFn;
GULReachabilityReleaseFn releaseFn;
};
#endif
@interface GULReachabilityChecker (Internal)
- (const struct GULReachabilityApi *)reachabilityApi;
- (void)setReachabilityApi:(const struct GULReachabilityApi *)reachabilityApi;
@end

View File

@@ -0,0 +1,263 @@
// Copyright 2017 Google
//
// 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 <Foundation/Foundation.h>
#import "GoogleUtilities/Reachability/Public/GoogleUtilities/GULReachabilityChecker.h"
#import "GoogleUtilities/Reachability/GULReachabilityChecker+Internal.h"
#import "GoogleUtilities/Reachability/GULReachabilityMessageCode.h"
#import "GoogleUtilities/Logger/Public/GoogleUtilities/GULLogger.h"
static GULLoggerService kGULLoggerReachability = @"[GULReachability]";
#if !TARGET_OS_WATCH
static void ReachabilityCallback(SCNetworkReachabilityRef reachability,
SCNetworkReachabilityFlags flags,
void *info);
static const struct GULReachabilityApi kGULDefaultReachabilityApi = {
SCNetworkReachabilityCreateWithName,
SCNetworkReachabilitySetCallback,
SCNetworkReachabilityScheduleWithRunLoop,
SCNetworkReachabilityUnscheduleFromRunLoop,
CFRelease,
};
static NSString *const kGULReachabilityUnknownStatus = @"Unknown";
static NSString *const kGULReachabilityConnectedStatus = @"Connected";
static NSString *const kGULReachabilityDisconnectedStatus = @"Disconnected";
#endif
@interface GULReachabilityChecker ()
@property(nonatomic, assign) const struct GULReachabilityApi *reachabilityApi;
@property(nonatomic, assign) GULReachabilityStatus reachabilityStatus;
@property(nonatomic, copy) NSString *host;
#if !TARGET_OS_WATCH
@property(nonatomic, assign) SCNetworkReachabilityRef reachability;
#endif
@end
@implementation GULReachabilityChecker
@synthesize reachabilityApi = reachabilityApi_;
#if !TARGET_OS_WATCH
@synthesize reachability = reachability_;
#endif
- (const struct GULReachabilityApi *)reachabilityApi {
return reachabilityApi_;
}
- (void)setReachabilityApi:(const struct GULReachabilityApi *)reachabilityApi {
#if !TARGET_OS_WATCH
if (reachability_) {
GULLogError(kGULLoggerReachability, NO,
[NSString stringWithFormat:@"I-REA%06ld", (long)kGULReachabilityMessageCode000],
@"Cannot change reachability API while reachability is running. "
@"Call stop first.");
return;
}
reachabilityApi_ = reachabilityApi;
#endif
}
@synthesize reachabilityStatus = reachabilityStatus_;
@synthesize host = host_;
@synthesize reachabilityDelegate = reachabilityDelegate_;
- (BOOL)isActive {
#if !TARGET_OS_WATCH
return reachability_ != nil;
#else
return NO;
#endif
}
- (void)setReachabilityDelegate:(id<GULReachabilityDelegate>)reachabilityDelegate {
if (reachabilityDelegate &&
(![(NSObject *)reachabilityDelegate conformsToProtocol:@protocol(GULReachabilityDelegate)])) {
GULLogError(kGULLoggerReachability, NO,
[NSString stringWithFormat:@"I-NET%06ld", (long)kGULReachabilityMessageCode005],
@"Reachability delegate doesn't conform to Reachability protocol.");
return;
}
reachabilityDelegate_ = reachabilityDelegate;
}
- (instancetype)initWithReachabilityDelegate:(id<GULReachabilityDelegate>)reachabilityDelegate
withHost:(NSString *)host {
self = [super init];
if (!host || !host.length) {
GULLogError(kGULLoggerReachability, NO,
[NSString stringWithFormat:@"I-REA%06ld", (long)kGULReachabilityMessageCode001],
@"Invalid host specified");
return nil;
}
if (self) {
#if !TARGET_OS_WATCH
[self setReachabilityDelegate:reachabilityDelegate];
reachabilityApi_ = &kGULDefaultReachabilityApi;
reachabilityStatus_ = kGULReachabilityUnknown;
host_ = [host copy];
reachability_ = nil;
#endif
}
return self;
}
- (void)dealloc {
reachabilityDelegate_ = nil;
[self stop];
}
- (BOOL)start {
#if TARGET_OS_WATCH
return NO;
#else
if (!reachability_) {
reachability_ = reachabilityApi_->createWithNameFn(kCFAllocatorDefault, [host_ UTF8String]);
if (!reachability_) {
return NO;
}
SCNetworkReachabilityContext context = {
0, /* version */
(__bridge void *)(self), /* info (passed as last parameter to reachability callback) */
NULL, /* retain */
NULL, /* release */
NULL /* copyDescription */
};
if (!reachabilityApi_->setCallbackFn(reachability_, ReachabilityCallback, &context) ||
!reachabilityApi_->scheduleWithRunLoopFn(reachability_, CFRunLoopGetMain(),
kCFRunLoopCommonModes)) {
reachabilityApi_->releaseFn(reachability_);
reachability_ = nil;
GULLogError(kGULLoggerReachability, NO,
[NSString stringWithFormat:@"I-REA%06ld", (long)kGULReachabilityMessageCode002],
@"Failed to start reachability handle");
return NO;
}
}
GULLogDebug(kGULLoggerReachability, NO,
[NSString stringWithFormat:@"I-REA%06ld", (long)kGULReachabilityMessageCode003],
@"Monitoring the network status");
return YES;
#endif
}
- (void)stop {
#if !TARGET_OS_WATCH
if (reachability_) {
reachabilityStatus_ = kGULReachabilityUnknown;
reachabilityApi_->unscheduleFromRunLoopFn(reachability_, CFRunLoopGetMain(),
kCFRunLoopCommonModes);
reachabilityApi_->releaseFn(reachability_);
reachability_ = nil;
}
#endif
}
#if !TARGET_OS_WATCH
- (GULReachabilityStatus)statusForFlags:(SCNetworkReachabilityFlags)flags {
GULReachabilityStatus status = kGULReachabilityNotReachable;
// If the Reachable flag is not set, we definitely don't have connectivity.
if (flags & kSCNetworkReachabilityFlagsReachable) {
// Reachable flag is set. Check further flags.
if (!(flags & kSCNetworkReachabilityFlagsConnectionRequired)) {
// Connection required flag is not set, so we have connectivity.
#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION)
status = (flags & kSCNetworkReachabilityFlagsIsWWAN) ? kGULReachabilityViaCellular
: kGULReachabilityViaWifi;
#elif TARGET_OS_OSX
status = kGULReachabilityViaWifi;
#endif
} else if ((flags & (kSCNetworkReachabilityFlagsConnectionOnDemand |
kSCNetworkReachabilityFlagsConnectionOnTraffic)) &&
!(flags & kSCNetworkReachabilityFlagsInterventionRequired)) {
// If the connection on demand or connection on traffic flag is set, and user intervention
// is not required, we have connectivity.
#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION)
status = (flags & kSCNetworkReachabilityFlagsIsWWAN) ? kGULReachabilityViaCellular
: kGULReachabilityViaWifi;
#elif TARGET_OS_OSX
status = kGULReachabilityViaWifi;
#endif
}
}
return status;
}
- (void)reachabilityFlagsChanged:(SCNetworkReachabilityFlags)flags {
GULReachabilityStatus status = [self statusForFlags:flags];
if (reachabilityStatus_ != status) {
NSString *reachabilityStatusString;
if (status == kGULReachabilityUnknown) {
reachabilityStatusString = kGULReachabilityUnknownStatus;
} else {
reachabilityStatusString = (status == kGULReachabilityNotReachable)
? kGULReachabilityDisconnectedStatus
: kGULReachabilityConnectedStatus;
}
GULLogDebug(kGULLoggerReachability, NO,
[NSString stringWithFormat:@"I-REA%06ld", (long)kGULReachabilityMessageCode004],
@"Network status has changed. Code:%@, status:%@", @(status),
reachabilityStatusString);
reachabilityStatus_ = status;
[reachabilityDelegate_ reachability:self statusChanged:reachabilityStatus_];
}
}
#endif
@end
#if !TARGET_OS_WATCH
static void ReachabilityCallback(SCNetworkReachabilityRef reachability,
SCNetworkReachabilityFlags flags,
void *info) {
GULReachabilityChecker *checker = (__bridge GULReachabilityChecker *)info;
[checker reachabilityFlagsChanged:flags];
}
#endif
// This function used to be at the top of the file, but it was moved here
// as a workaround for a suspected compiler bug. When compiled in Release mode
// and run on an iOS device with WiFi disabled, the reachability code crashed
// when calling SCNetworkReachabilityScheduleWithRunLoop, or shortly thereafter.
// After unsuccessfully trying to diagnose the cause of the crash, it was
// discovered that moving this function to the end of the file magically fixed
// the crash. If you are going to edit this file, exercise caution and make sure
// to test thoroughly with an iOS device under various network conditions.
const NSString *GULReachabilityStatusString(GULReachabilityStatus status) {
switch (status) {
case kGULReachabilityUnknown:
return @"Reachability Unknown";
case kGULReachabilityNotReachable:
return @"Not reachable";
case kGULReachabilityViaWifi:
return @"Reachable via Wifi";
case kGULReachabilityViaCellular:
return @"Reachable via Cellular Data";
default:
return [NSString stringWithFormat:@"Invalid reachability status %d", (int)status];
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2017 Google
*
* 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 <Foundation/Foundation.h>
// Make sure these codes do not overlap with any contained in the FIRAMessageCode enum.
typedef NS_ENUM(NSInteger, GULReachabilityMessageCode) {
// GULReachabilityChecker.m
kGULReachabilityMessageCode000 = 902000, // I-NET902000
kGULReachabilityMessageCode001 = 902001, // I-NET902001
kGULReachabilityMessageCode002 = 902002, // I-NET902002
kGULReachabilityMessageCode003 = 902003, // I-NET902003
kGULReachabilityMessageCode004 = 902004, // I-NET902004
kGULReachabilityMessageCode005 = 902005, // I-NET902005
kGULReachabilityMessageCode006 = 902006, // I-NET902006
};

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2017 Google
*
* 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 <Foundation/Foundation.h>
#if !TARGET_OS_WATCH
#import <SystemConfiguration/SystemConfiguration.h>
#endif
/// Reachability Status
typedef enum {
kGULReachabilityUnknown, ///< Have not yet checked or been notified whether host is reachable.
kGULReachabilityNotReachable, ///< Host is not reachable.
kGULReachabilityViaWifi, ///< Host is reachable via Wifi.
kGULReachabilityViaCellular, ///< Host is reachable via cellular.
} GULReachabilityStatus;
const NSString *GULReachabilityStatusString(GULReachabilityStatus status);
@class GULReachabilityChecker;
/// Google Analytics iOS Reachability Checker.
@protocol GULReachabilityDelegate
@required
/// Called when network status has changed.
- (void)reachability:(GULReachabilityChecker *)reachability
statusChanged:(GULReachabilityStatus)status;
@end
/// Google Analytics iOS Network Status Checker.
@interface GULReachabilityChecker : NSObject
/// The last known reachability status, or GULReachabilityStatusUnknown if the
/// checker is not active.
@property(nonatomic, readonly) GULReachabilityStatus reachabilityStatus;
/// The host to which reachability status is to be checked.
@property(nonatomic, copy, readonly) NSString *host;
/// The delegate to be notified of reachability status changes.
@property(nonatomic, weak) id<GULReachabilityDelegate> reachabilityDelegate;
/// `YES` if the reachability checker is active, `NO` otherwise.
@property(nonatomic, readonly) BOOL isActive;
/// Initialize the reachability checker. Note that you must call start to begin checking for and
/// receiving notifications about network status changes.
///
/// @param reachabilityDelegate The delegate to be notified when reachability status to host
/// changes.
///
/// @param host The name of the host.
///
- (instancetype)initWithReachabilityDelegate:(id<GULReachabilityDelegate>)reachabilityDelegate
withHost:(NSString *)host;
- (instancetype)init NS_UNAVAILABLE;
/// Start checking for reachability to the specified host. This has no effect if the status
/// checker is already checking for connectivity.
///
/// @return `YES` if initiating status checking was successful or the status checking has already
/// been initiated, `NO` otherwise.
- (BOOL)start;
/// Stop checking for reachability to the specified host. This has no effect if the status
/// checker is not checking for connectivity.
- (void)stop;
@end

View File

@@ -0,0 +1,213 @@
// Copyright 2018 Google
//
// 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 "GoogleUtilities/UserDefaults/Public/GoogleUtilities/GULUserDefaults.h"
#import "GoogleUtilities/Logger/Public/GoogleUtilities/GULLogger.h"
NS_ASSUME_NONNULL_BEGIN
static NSTimeInterval const kGULSynchronizeInterval = 1.0;
static NSString *const kGULLogFormat = @"I-GUL%06ld";
static GULLoggerService kGULLogUserDefaultsService = @"[GoogleUtilities/UserDefaults]";
typedef NS_ENUM(NSInteger, GULUDMessageCode) {
GULUDMessageCodeInvalidKeyGet = 1,
GULUDMessageCodeInvalidKeySet = 2,
GULUDMessageCodeInvalidObjectSet = 3,
GULUDMessageCodeSynchronizeFailed = 4,
};
@interface GULUserDefaults ()
/// Equivalent to the suite name for NSUserDefaults.
@property(readonly) CFStringRef appNameRef;
@property(atomic) BOOL isPreferenceFileExcluded;
@end
@implementation GULUserDefaults {
// The application name is the same with the suite name of the NSUserDefaults, and it is used for
// preferences.
CFStringRef _appNameRef;
}
+ (GULUserDefaults *)standardUserDefaults {
static GULUserDefaults *standardUserDefaults;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
standardUserDefaults = [[GULUserDefaults alloc] init];
});
return standardUserDefaults;
}
- (instancetype)init {
return [self initWithSuiteName:nil];
}
- (instancetype)initWithSuiteName:(nullable NSString *)suiteName {
self = [super init];
NSString *name = [suiteName copy];
if (self) {
// `kCFPreferencesCurrentApplication` maps to the same defaults database as
// `[NSUserDefaults standardUserDefaults]`.
_appNameRef =
name.length ? (__bridge_retained CFStringRef)name : kCFPreferencesCurrentApplication;
}
return self;
}
- (void)dealloc {
// If we're using a custom `_appNameRef` it needs to be released. If it's a constant, it shouldn't
// need to be released since we don't own it.
if (CFStringCompare(_appNameRef, kCFPreferencesCurrentApplication, 0) != kCFCompareEqualTo) {
CFRelease(_appNameRef);
}
[NSObject cancelPreviousPerformRequestsWithTarget:self
selector:@selector(synchronize)
object:nil];
}
- (nullable id)objectForKey:(NSString *)defaultName {
NSString *key = [defaultName copy];
if (![key isKindOfClass:[NSString class]] || !key.length) {
GULLogWarning(@"<GoogleUtilities>", NO,
[NSString stringWithFormat:kGULLogFormat, (long)GULUDMessageCodeInvalidKeyGet],
@"Cannot get object for invalid user default key.");
return nil;
}
return (__bridge_transfer id)CFPreferencesCopyAppValue((__bridge CFStringRef)key, _appNameRef);
}
- (void)setObject:(nullable id)value forKey:(NSString *)defaultName {
NSString *key = [defaultName copy];
if (![key isKindOfClass:[NSString class]] || !key.length) {
GULLogWarning(kGULLogUserDefaultsService, NO,
[NSString stringWithFormat:kGULLogFormat, (long)GULUDMessageCodeInvalidKeySet],
@"Cannot set object for invalid user default key.");
return;
}
if (!value) {
CFPreferencesSetAppValue((__bridge CFStringRef)key, NULL, _appNameRef);
[self scheduleSynchronize];
return;
}
BOOL isAcceptableValue =
[value isKindOfClass:[NSString class]] || [value isKindOfClass:[NSNumber class]] ||
[value isKindOfClass:[NSArray class]] || [value isKindOfClass:[NSDictionary class]] ||
[value isKindOfClass:[NSDate class]] || [value isKindOfClass:[NSData class]];
if (!isAcceptableValue) {
GULLogWarning(kGULLogUserDefaultsService, NO,
[NSString stringWithFormat:kGULLogFormat, (long)GULUDMessageCodeInvalidObjectSet],
@"Cannot set invalid object to user defaults. Must be a string, number, array, "
@"dictionary, date, or data. Value: %@",
value);
return;
}
CFPreferencesSetAppValue((__bridge CFStringRef)key, (__bridge CFStringRef)value, _appNameRef);
[self scheduleSynchronize];
}
- (void)removeObjectForKey:(NSString *)key {
[self setObject:nil forKey:key];
}
#pragma mark - Getters
- (NSInteger)integerForKey:(NSString *)defaultName {
NSNumber *object = [self objectForKey:defaultName];
return object.integerValue;
}
- (float)floatForKey:(NSString *)defaultName {
NSNumber *object = [self objectForKey:defaultName];
return object.floatValue;
}
- (double)doubleForKey:(NSString *)defaultName {
NSNumber *object = [self objectForKey:defaultName];
return object.doubleValue;
}
- (BOOL)boolForKey:(NSString *)defaultName {
NSNumber *object = [self objectForKey:defaultName];
return object.boolValue;
}
- (nullable NSString *)stringForKey:(NSString *)defaultName {
return [self objectForKey:defaultName];
}
- (nullable NSArray *)arrayForKey:(NSString *)defaultName {
return [self objectForKey:defaultName];
}
- (nullable NSDictionary<NSString *, id> *)dictionaryForKey:(NSString *)defaultName {
return [self objectForKey:defaultName];
}
#pragma mark - Setters
- (void)setInteger:(NSInteger)integer forKey:(NSString *)defaultName {
[self setObject:@(integer) forKey:defaultName];
}
- (void)setFloat:(float)value forKey:(NSString *)defaultName {
[self setObject:@(value) forKey:defaultName];
}
- (void)setDouble:(double)doubleNumber forKey:(NSString *)defaultName {
[self setObject:@(doubleNumber) forKey:defaultName];
}
- (void)setBool:(BOOL)boolValue forKey:(NSString *)defaultName {
[self setObject:@(boolValue) forKey:defaultName];
}
#pragma mark - Save data
- (void)synchronize {
if (!CFPreferencesAppSynchronize(_appNameRef)) {
GULLogError(kGULLogUserDefaultsService, NO,
[NSString stringWithFormat:kGULLogFormat, (long)GULUDMessageCodeSynchronizeFailed],
@"Cannot synchronize user defaults to disk");
}
}
#pragma mark - Private methods
- (void)scheduleSynchronize {
// Synchronize data using a timer so that multiple set... calls can be coalesced under one
// synchronize.
[NSObject cancelPreviousPerformRequestsWithTarget:self
selector:@selector(synchronize)
object:nil];
// This method may be called on multiple queues (due to set... methods can be called on any queue)
// synchronize can be scheduled on different queues, so make sure that it does not crash. If this
// instance goes away, self will be released also, no one will retain it and the schedule won't be
// called.
[self performSelector:@selector(synchronize) withObject:nil afterDelay:kGULSynchronizeInterval];
}
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,110 @@
// Copyright 2018 Google
//
// 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 <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/// A thread-safe user defaults that uses C functions from CFPreferences.h instead of
/// `NSUserDefaults`. This is to avoid sending an `NSNotification` when it's changed from a
/// background thread to avoid crashing. // TODO: Insert radar number here.
@interface GULUserDefaults : NSObject
/// A shared user defaults similar to +[NSUserDefaults standardUserDefaults] and accesses the same
/// data of the standardUserDefaults.
+ (GULUserDefaults *)standardUserDefaults;
/// Initializes preferences with a suite name that is the same with the NSUserDefaults' suite name.
/// Both of CFPreferences and NSUserDefaults share the same plist file so their data will exactly
/// the same.
///
/// @param suiteName The name of the suite of the user defaults.
- (instancetype)initWithSuiteName:(nullable NSString *)suiteName;
#pragma mark - Getters
/// Searches the receiver's search list for a default with the key 'defaultName' and return it. If
/// another process has changed defaults in the search list, NSUserDefaults will automatically
/// update to the latest values. If the key in question has been marked as ubiquitous via a Defaults
/// Configuration File, the latest value may not be immediately available, and the registered value
/// will be returned instead.
- (nullable id)objectForKey:(NSString *)defaultName;
/// Equivalent to -objectForKey:, except that it will return nil if the value is not an NSArray.
- (nullable NSArray *)arrayForKey:(NSString *)defaultName;
/// Equivalent to -objectForKey:, except that it will return nil if the value
/// is not an NSDictionary.
- (nullable NSDictionary<NSString *, id> *)dictionaryForKey:(NSString *)defaultName;
/// Equivalent to -objectForKey:, except that it will convert NSNumber values to their NSString
/// representation. If a non-string non-number value is found, nil will be returned.
- (nullable NSString *)stringForKey:(NSString *)defaultName;
/// Equivalent to -objectForKey:, except that it converts the returned value to an NSInteger. If the
/// value is an NSNumber, the result of -integerValue will be returned. If the value is an NSString,
/// it will be converted to NSInteger if possible. If the value is a boolean, it will be converted
/// to either 1 for YES or 0 for NO. If the value is absent or can't be converted to an integer, 0
/// will be returned.
- (NSInteger)integerForKey:(NSString *)defaultName;
/// Similar to -integerForKey:, except that it returns a float, and boolean values will not be
/// converted.
- (float)floatForKey:(NSString *)defaultName;
/// Similar to -integerForKey:, except that it returns a double, and boolean values will not be
/// converted.
- (double)doubleForKey:(NSString *)defaultName;
/// Equivalent to -objectForKey:, except that it converts the returned value to a BOOL. If the value
/// is an NSNumber, NO will be returned if the value is 0, YES otherwise. If the value is an
/// NSString, values of "YES" or "1" will return YES, and values of "NO", "0", or any other string
/// will return NO. If the value is absent or can't be converted to a BOOL, NO will be returned.
- (BOOL)boolForKey:(NSString *)defaultName;
#pragma mark - Setters
/// Immediately stores a value (or removes the value if `nil` is passed as the value) for the
/// provided key in the search list entry for the receiver's suite name in the current user and any
/// host, then asynchronously stores the value persistently, where it is made available to other
/// processes.
- (void)setObject:(nullable id)value forKey:(NSString *)defaultName;
/// Equivalent to -setObject:forKey: except that the value is converted from a float to an NSNumber.
- (void)setFloat:(float)value forKey:(NSString *)defaultName;
/// Equivalent to -setObject:forKey: except that the value is converted from a double to an
/// NSNumber.
- (void)setDouble:(double)value forKey:(NSString *)defaultName;
/// Equivalent to -setObject:forKey: except that the value is converted from an NSInteger to an
/// NSNumber.
- (void)setInteger:(NSInteger)value forKey:(NSString *)defaultName;
/// Equivalent to -setObject:forKey: except that the value is converted from a BOOL to an NSNumber.
- (void)setBool:(BOOL)value forKey:(NSString *)defaultName;
#pragma mark - Removing Defaults
/// Equivalent to -[... setObject:nil forKey:defaultName]
- (void)removeObjectForKey:(NSString *)defaultName;
#pragma mark - Save data
/// Blocks the calling thread until all in-progress set operations have completed.
- (void)synchronize;
@end
NS_ASSUME_NONNULL_END

224
Pods/GoogleUtilities/LICENSE generated Normal file
View File

@@ -0,0 +1,224 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.
================================================================================
Copyright (c) 2017 Landon J. Fuller <landon@landonf.org>
All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

189
Pods/GoogleUtilities/README.md generated Normal file
View File

@@ -0,0 +1,189 @@
[![Version](https://img.shields.io/cocoapods/v/GoogleUtilities.svg?style=flat)](https://cocoapods.org/pods/GoogleUtilities)
[![License](https://img.shields.io/cocoapods/l/GoogleUtilities.svg?style=flat)](https://cocoapods.org/pods/GoogleUtilities)
[![Platform](https://img.shields.io/cocoapods/p/GoogleUtilities.svg?style=flat)](https://cocoapods.org/pods/GoogleUtilities)
[![Actions Status][gh-google-utilities-badge]][gh-actions]
# GoogleUtilities
GoogleUtilities provides a set of utilities for Firebase and other Google SDKs for Apple platform
development.
The utilities are not directly supported for non-Google library usage.
## Integration Testing
These instructions apply to minor and patch version updates. Major versions need
a customized adaptation.
After the CI is green:
* Determine the next version for release by checking the
[tagged releases](https://github.com/google/GoogleUtilities/tags).
Ensure that the next release version keeps the Swift PM and CocoaPods versions in sync.
* Verify that the releasing version is the latest entry in the [CHANGELOG.md](CHANGELOG.md),
updating it if necessary.
* Update the version in the podspec to match the latest entry in the [CHANGELOG.md](CHANGELOG.md)
* Checkout the `main` branch and ensure it is up to date
```console
git checkout main
git pull
```
* Add the CocoaPods tag (`{version}` will be the latest version in the [podspec](GoogleUtilities.podspec#L3))
```console
git tag CocoaPods-{version}
git push origin CocoaPods-{version}
```
* Push the podspec to the designated repo
* If this version of GoogleUtilities is intended to launch **before or with** the next Firebase release:
<details>
<summary>Push to <b>SpecsStaging</b></summary>
```console
pod repo push --skip-tests staging GoogleUtilities.podspec
```
If the command fails with `Unable to find the 'staging' repo.`, add the staging repo with:
```console
pod repo add staging git@github.com:firebase/SpecsStaging.git
```
</details>
* Otherwise:
<details>
<summary>Push to <b>SpecsDev</b></summary>
```console
pod repo push --skip-tests dev GoogleUtilities.podspec
```
If the command fails with `Unable to find the 'dev' repo.`, add the dev repo with:
```console
pod repo add dev git@github.com:firebase/SpecsDev.git
```
</details>
* Run Firebase CI by waiting until next nightly or adding a PR that touches `Gemfile`.
* On google3, run copybara using the command below. Then, start a global TAP on the generated CL. Deflake as needed.
```console
third_party/firebase/ios/Releases/run_copy_bara.py --directory GoogleUtilities --branch main
```
## Publishing
The release process is as follows:
1. [Tag and release for Swift PM](#swift-package-manager)
2. [Publish to CocoaPods](#cocoapods)
3. [Create GitHub Release](#create-github-release)
4. [Perform post release cleanup](#post-release-cleanup)
### Swift Package Manager
By creating and [pushing a tag](https://github.com/google/GoogleUtilities/tags)
for Swift PM, the newly tagged version will be immediately released for public use.
Given this, please verify the intended time of release for Swift PM.
* Add a version tag for Swift PM
```console
git tag {version}
git push origin {version}
```
*Note: Ensure that any inflight PRs that depend on the new `GoogleUtilities` version are updated to point to the
newly tagged version rather than a checksum.*
### CocoaPods
* Publish the newly versioned pod to CocoaPods
It's recommended to point to the `GoogleUtilities.podspec` in `staging` to make sure the correct spec is being published.
```console
pod trunk push ~/.cocoapods/repos/staging/GoogleUtilities/{version}/GoogleUtilities.podspec
```
*Note: In some cases, it may be acceptable to `pod trunk push` with the `--skip-tests` flag. Please double check with
the maintainers before doing so.*
The pod push was successful if the above command logs: `🚀 GoogleUtilities ({version}) successfully published`.
In addition, a new commit that publishes the new version (co-authored by [CocoaPodsAtGoogle](https://github.com/CocoaPodsAtGoogle))
should appear in the [CocoaPods specs repo](https://github.com/CocoaPods/Specs). Last, the latest version should be displayed
on [GoogleUtilities's CocoaPods page](https://cocoapods.org/pods/GoogleUtilities).
### [Create GitHub Release](https://github.com/google/GoogleUtilities/releases/new/)
Update the [release template](https://github.com/google/GoogleUtilities/releases/new/)'s **Tag version** and **Release title**
fields with the latest version. In addition, reference the [Release Notes](./CHANGELOG.md) in the release's description.
See [this release](https://github.com/google/GoogleUtilities/releases/edit/7.7.0) for an example.
*Don't forget to perform the [post release cleanup](#post-release-cleanup)!*
### Post Release Cleanup
<details>
<summary>Clean up <b>SpecsStaging</b></summary>
```console
pwd=$(pwd)
mkdir -p /tmp/release-cleanup && cd $_
git clone git@github.com:firebase/SpecsStaging.git
cd SpecsStaging/
git rm -rf GoogleUtilities/
git commit -m "Post publish cleanup"
git push origin master
rm -rf /tmp/release-cleanup
cd $pwd
```
</details>
## Development
To develop in this repository, ensure that you have at least the following software:
* Xcode 12.0 (or later)
* CocoaPods 1.10.0 (or later)
* [CocoaPods generate](https://github.com/square/cocoapods-generate)
For the pod that you want to develop:
`pod gen GoogleUtilities.podspec --local-sources=./ --auto-open --platforms=ios`
Note: If the CocoaPods cache is out of date, you may need to run
`pod repo update` before the `pod gen` command.
Note: Set the `--platforms` option to `macos` or `tvos` to develop/test for
those platforms. Since 10.2, Xcode does not properly handle multi-platform
CocoaPods workspaces.
### Development for Catalyst
* `pod gen GoogleUtilities.podspec --local-sources=./ --auto-open --platforms=ios`
* Check the Mac box in the App-iOS Build Settings
* Sign the App in the Settings Signing & Capabilities tab
* Click Pods in the Project Manager
* Add Signing to the iOS host app and unit test targets
* Select the Unit-unit scheme
* Run it to build and test
Alternatively disable signing in each target:
* Go to Build Settings tab
* Click `+`
* Select `Add User-Defined Setting`
* Add `CODE_SIGNING_REQUIRED` setting with a value of `NO`
### Code Formatting
To ensure that the code is formatted consistently, run the script
[./scripts/check.sh](https://github.com/firebase/firebase-ios-sdk/blob/main/scripts/check.sh)
before creating a PR.
GitHub Actions will verify that any code changes are done in a style compliant
way. Install `clang-format` and `mint`:
```console
brew install clang-format@17
brew install mint
```
### Running Unit Tests
Select a scheme and press Command-u to build a component and run its unit tests.
## Contributing
See [Contributing](CONTRIBUTING.md).
## License
The contents of this repository is licensed under the
[Apache License, version 2.0](http://www.apache.org/licenses/LICENSE-2.0).
[gh-actions]: https://github.com/firebase/firebase-ios-sdk/actions
[gh-google-utilities-badge]: https://github.com/firebase/firebase-ios-sdk/workflows/google-utilities/badge.svg

View File

@@ -0,0 +1,104 @@
// Copyright (c) 2017 Landon J. Fuller <landon@landonf.org>
// All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//
// Comment from
// <a href="http://iphonedevwiki.net/index.php/Crack_prevention">iPhone Dev
// Wiki Crack Prevention</a>:
// App Store binaries are signed by both their developer and Apple. This
// encrypts the binary so that decryption keys are needed in order to make the
// binary readable. When iOS executes the binary, the decryption keys are used
// to decrypt the binary into a readable state where it is then loaded into
// memory and executed. iOS can tell the encryption status of a binary via the
// cryptid structure member of LC_ENCRYPTION_INFO MachO load command. If
// cryptid is a non-zero value then the binary is encrypted.
//
// 'Cracking' works by letting the kernel decrypt the binary then siphoning the
// decrypted data into a new binary file, resigning, and repackaging. This will
// only work on jailbroken devices as codesignature validation has been
// removed. Resigning takes place because while the codesignature doesn't have
// to be valid thanks to the jailbreak, it does have to be in place unless you
// have AppSync or similar to disable codesignature checks.
//
// More information at
// <a href="http://landonf.org/2009/02/index.html">Landon Fuller's blog</a>
#import "third_party/IsAppEncrypted/Public/IsAppEncrypted.h"
#import <mach-o/dyld.h>
#import <objc/objc.h>
/// The encryption info struct and constants are missing from the iPhoneSimulator SDK, but not from
/// the iPhoneOS or Mac OS X SDKs. Since one doesn't ever ship a Simulator binary, we'll just
/// provide the definitions here.
#if TARGET_OS_SIMULATOR && !defined(LC_ENCRYPTION_INFO)
#define LC_ENCRYPTION_INFO 0x21
struct encryption_info_command {
uint32_t cmd;
uint32_t cmdsize;
uint32_t cryptoff;
uint32_t cryptsize;
uint32_t cryptid;
};
#endif
BOOL IsAppEncrypted(void) {
const struct mach_header *executableHeader = NULL;
for (uint32_t i = 0; i < _dyld_image_count(); i++) {
const struct mach_header *header = _dyld_get_image_header(i);
if (header && header->filetype == MH_EXECUTE) {
executableHeader = header;
break;
}
}
if (!executableHeader) {
return NO;
}
BOOL is64bit = (executableHeader->magic == MH_MAGIC_64);
uintptr_t cursor = (uintptr_t)executableHeader +
(is64bit ? sizeof(struct mach_header_64) : sizeof(struct mach_header));
const struct segment_command *segmentCommand = NULL;
uint32_t i = 0;
while (i++ < executableHeader->ncmds) {
segmentCommand = (struct segment_command *)cursor;
if (!segmentCommand) {
continue;
}
if ((!is64bit && segmentCommand->cmd == LC_ENCRYPTION_INFO) ||
(is64bit && segmentCommand->cmd == LC_ENCRYPTION_INFO_64)) {
if (is64bit) {
struct encryption_info_command_64 *cryptCmd =
(struct encryption_info_command_64 *)segmentCommand;
return cryptCmd && cryptCmd->cryptid != 0;
} else {
struct encryption_info_command *cryptCmd = (struct encryption_info_command *)segmentCommand;
return cryptCmd && cryptCmd->cryptid != 0;
}
}
cursor += segmentCommand->cmdsize;
}
return NO;
}

View File

@@ -0,0 +1,24 @@
// Copyright (c) 2017 Landon J. Fuller <landon@landonf.org>
// All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
#import <objc/objc.h>
BOOL IsAppEncrypted(void);