aboutsummaryrefslogtreecommitdiffhomepage
path: root/GoogleUtilities
diff options
context:
space:
mode:
authorGravatar Paul Beusterien <paulbeusterien@google.com>2018-07-11 08:28:35 -0700
committerGravatar GitHub <noreply@github.com>2018-07-11 08:28:35 -0700
commitc6b4b03fffc3cea7c9525e5c79dce28f52900521 (patch)
tree0e8a237940dcd4b4100c00b9fac428e657619ab8 /GoogleUtilities
parent25f8691970a9f765a87ab3125776598c92e02744 (diff)
Move GoogleUtilities out of Firebase directory (#1516)
Diffstat (limited to 'GoogleUtilities')
-rw-r--r--GoogleUtilities/Environment/third_party/GULAppEnvironmentUtil.h43
-rw-r--r--GoogleUtilities/Environment/third_party/GULAppEnvironmentUtil.m236
-rw-r--r--GoogleUtilities/Logger/GULLogger.m214
-rw-r--r--GoogleUtilities/Logger/Private/GULLogger.h151
-rw-r--r--GoogleUtilities/Logger/Public/GULLoggerLevel.h35
-rw-r--r--GoogleUtilities/NSData+zlib/GULNSData+zlib.h49
-rw-r--r--GoogleUtilities/NSData+zlib/GULNSData+zlib.m207
-rw-r--r--GoogleUtilities/Network/GULMutableDictionary.m97
-rw-r--r--GoogleUtilities/Network/GULNetwork.m388
-rw-r--r--GoogleUtilities/Network/GULNetworkConstants.m40
-rw-r--r--GoogleUtilities/Network/GULNetworkURLSession.m669
-rw-r--r--GoogleUtilities/Network/Private/GULMutableDictionary.h46
-rw-r--r--GoogleUtilities/Network/Private/GULNetwork.h87
-rw-r--r--GoogleUtilities/Network/Private/GULNetworkConstants.h79
-rw-r--r--GoogleUtilities/Network/Private/GULNetworkLoggerProtocol.h51
-rw-r--r--GoogleUtilities/Network/Private/GULNetworkMessageCode.h44
-rw-r--r--GoogleUtilities/Network/Private/GULNetworkURLSession.h60
-rw-r--r--GoogleUtilities/Reachability/GULReachabilityChecker+Internal.h47
-rw-r--r--GoogleUtilities/Reachability/GULReachabilityChecker.m240
-rw-r--r--GoogleUtilities/Reachability/Private/GULReachabilityChecker.h77
-rw-r--r--GoogleUtilities/Reachability/Private/GULReachabilityMessageCode.h27
21 files changed, 2887 insertions, 0 deletions
diff --git a/GoogleUtilities/Environment/third_party/GULAppEnvironmentUtil.h b/GoogleUtilities/Environment/third_party/GULAppEnvironmentUtil.h
new file mode 100644
index 0000000..5b56271
--- /dev/null
+++ b/GoogleUtilities/Environment/third_party/GULAppEnvironmentUtil.h
@@ -0,0 +1,43 @@
+/*
+ * 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>
+
+@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.
++ (NSString *)deviceModel;
+
+/// 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;
+
+@end
diff --git a/GoogleUtilities/Environment/third_party/GULAppEnvironmentUtil.m b/GoogleUtilities/Environment/third_party/GULAppEnvironmentUtil.m
new file mode 100644
index 0000000..e96ba70
--- /dev/null
+++ b/GoogleUtilities/Environment/third_party/GULAppEnvironmentUtil.m
@@ -0,0 +1,236 @@
+// 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 "GULAppEnvironmentUtil.h"
+
+#import <Foundation/Foundation.h>
+#import <dlfcn.h>
+#import <mach-o/dyld.h>
+#import <sys/utsname.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
+
+@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";
+
+/// The following copyright from Landon J. Fuller applies to the isAppEncrypted function.
+///
+/// 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>
+static BOOL IsAppEncrypted() {
+ 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;
+}
+
+static BOOL HasSCInfoFolder() {
+#if TARGET_OS_IOS || TARGET_OS_TV
+ 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() {
+#if TARGET_OS_IOS || TARGET_OS_TV
+ 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_IOS || TARGET_OS_TV
+ NSString *platform = [GULAppEnvironmentUtil deviceModel];
+ return [platform isEqual:@"x86_64"] || [platform isEqual:@"i386"];
+#elif TARGET_OS_OSX
+ return NO;
+#endif
+}
+
++ (NSString *)deviceModel {
+ static dispatch_once_t once;
+ static NSString *deviceModel;
+
+ dispatch_once(&once, ^{
+ struct utsname systemInfo;
+ if (uname(&systemInfo) == 0) {
+ deviceModel = [NSString stringWithUTF8String:systemInfo.machine];
+ }
+ });
+ return deviceModel;
+}
+
++ (NSString *)systemVersion {
+ // 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;
+}
+
++ (BOOL)isAppExtension {
+#if TARGET_OS_IOS || TARGET_OS_TV
+ // 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
+}
+
+@end
diff --git a/GoogleUtilities/Logger/GULLogger.m b/GoogleUtilities/Logger/GULLogger.m
new file mode 100644
index 0000000..5c808ea
--- /dev/null
+++ b/GoogleUtilities/Logger/GULLogger.m
@@ -0,0 +1,214 @@
+// 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 "Private/GULLogger.h"
+
+#include <asl.h>
+
+#import <GoogleUtilities/GULAppEnvironmentUtil.h>
+#import "Public/GULLoggerLevel.h"
+
+/// Key for the debug mode bit in NSUserDefaults.
+NSString *const kGULPersistedDebugModeKey = @"/google/utilities/debug_mode";
+
+/// 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 const char *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(BOOL overrideSTDERR, BOOL forceDebugMode) {
+ 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
+
+ // Override the aslOptions to ASL_OPT_STDERR if the override argument is passed in.
+ if (overrideSTDERR) {
+ aslOptions = ASL_OPT_STDERR;
+ }
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations" // asl is deprecated
+ // Initialize the ASL client handle.
+ sGULLoggerClient = asl_open(NULL, kGULLoggerASLClientFacilityName, aslOptions);
+ sGULLoggerDebugMode = forceDebugMode;
+ sGULLoggerMaximumLevel = GULLoggerLevelNotice;
+
+ if (forceDebugMode) {
+ asl_set_filter(sGULLoggerClient, ASL_FILTER_MASK_UPTO(ASL_LEVEL_DEBUG));
+ } else {
+ // Set the filter used by system/device log. Initialize in default mode.
+ asl_set_filter(sGULLoggerClient, ASL_FILTER_MASK_UPTO(ASL_LEVEL_NOTICE));
+ }
+
+ // We should disable debug mode if we are running from App Store.
+ if (sGULLoggerDebugMode && [GULAppEnvironmentUtil isFromAppStore]) {
+ sGULLoggerDebugMode = NO;
+ }
+
+ 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 GULSetLoggerLevel(GULLoggerLevel loggerLevel) {
+ if (loggerLevel < GULLoggerLevelMin || loggerLevel > GULLoggerLevelMax) {
+ GULLogError(kGULLoggerLogger, NO, @"I-COR000023", @"Invalid logger level, %ld",
+ (long)loggerLevel);
+ return;
+ }
+ GULLoggerInitializeASL(NO, NO);
+ // 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(NO, NO);
+ if (sGULLoggerDebugMode) {
+ return YES;
+ }
+ return (BOOL)(loggerLevel <= sGULLoggerMaximumLevel);
+}
+
+#ifdef DEBUG
+void GULResetLogger() {
+ sGULLoggerOnceToken = 0;
+}
+
+aslclient getGULLoggerClient() {
+ return sGULLoggerClient;
+}
+
+dispatch_queue_t getGULClientQueue() {
+ return sGULClientQueue;
+}
+
+BOOL getGULLoggerDebugMode() {
+ return sGULLoggerDebugMode;
+}
+#endif
+
+void GULLoggerRegisterVersion(const char *version) {
+ sVersion = version;
+}
+
+void GULLogBasic(GULLoggerLevel level,
+ GULLoggerService service,
+ BOOL forceLog,
+ NSString *messageCode,
+ NSString *message,
+ va_list args_ptr) {
+ GULLoggerInitializeASL(NO, NO);
+ if (!(level <= sGULLoggerMaximumLevel || sGULLoggerDebugMode || forceLog)) {
+ return;
+ }
+
+#ifdef DEBUG
+ NSCAssert(messageCode.length == 11, @"Incorrect message code length.");
+ NSRange messageCodeRange = NSMakeRange(0, messageCode.length);
+ NSUInteger numberOfMatches =
+ [sMessageCodeRegex numberOfMatchesInString:messageCode options:0 range:messageCodeRange];
+ NSCAssert(numberOfMatches == 1, @"Incorrect message code format.");
+#endif
+ NSString *logMsg = [[NSString alloc] initWithFormat:message arguments:args_ptr];
+ logMsg = [NSString stringWithFormat:@"%s - %@[%@] %@", sVersion, service, messageCode, logMsg];
+ dispatch_async(sGULClientQueue, ^{
+ asl_log(sGULLoggerClient, NULL, level, "%s", logMsg.UTF8String);
+ });
+}
+#pragma clang diagnostic pop
+
+/**
+ * Generates the logging functions using macros.
+ *
+ * Calling GULLogError(kGULLoggerCore, @"I-COR000001", @"Configure %@ failed.", @"blah") shows:
+ * yyyy-mm-dd hh:mm:ss.SSS sender[PID] <Error> [{service}][I-COR000001] Configure blah failed.
+ * Calling GULLogDebug(kGULLoggerCore, @"I-COR000001", @"Configure succeed.") shows:
+ * yyyy-mm-dd hh:mm:ss.SSS sender[PID] <Debug> [{service}][I-COR000001] 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
diff --git a/GoogleUtilities/Logger/Private/GULLogger.h b/GoogleUtilities/Logger/Private/GULLogger.h
new file mode 100644
index 0000000..c8b12e8
--- /dev/null
+++ b/GoogleUtilities/Logger/Private/GULLogger.h
@@ -0,0 +1,151 @@
+/*
+ * 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 <GoogleUtilities/GULLoggerLevel.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * The services used in the logger.
+ */
+typedef NSString *const GULLoggerService;
+
+#ifdef __cplusplus
+extern "C" {
+#endif // __cplusplus
+
+/**
+ * Initialize GULLogger.
+ * (required) overrideSTDERR Override the aslOptions to ASL_OPT_STDERR.
+ * (required) forceDebugMode Force the asl level to ASL_LEVEL_DEBUG.
+ */
+extern void GULLoggerInitializeASL(BOOL overrideSTDERR, BOOL forceDebugMode);
+
+/**
+ * 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(const char *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
diff --git a/GoogleUtilities/Logger/Public/GULLoggerLevel.h b/GoogleUtilities/Logger/Public/GULLoggerLevel.h
new file mode 100644
index 0000000..81ff212
--- /dev/null
+++ b/GoogleUtilities/Logger/Public/GULLoggerLevel.h
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+
+/**
+ * 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);
diff --git a/GoogleUtilities/NSData+zlib/GULNSData+zlib.h b/GoogleUtilities/NSData+zlib/GULNSData+zlib.h
new file mode 100644
index 0000000..36f94a7
--- /dev/null
+++ b/GoogleUtilities/NSData+zlib/GULNSData+zlib.h
@@ -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
diff --git a/GoogleUtilities/NSData+zlib/GULNSData+zlib.m b/GoogleUtilities/NSData+zlib/GULNSData+zlib.m
new file mode 100644
index 0000000..cd3394a
--- /dev/null
+++ b/GoogleUtilities/NSData+zlib/GULNSData+zlib.m
@@ -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 "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
diff --git a/GoogleUtilities/Network/GULMutableDictionary.m b/GoogleUtilities/Network/GULMutableDictionary.m
new file mode 100644
index 0000000..d281eb4
--- /dev/null
+++ b/GoogleUtilities/Network/GULMutableDictionary.m
@@ -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 "Private/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[key];
+ });
+ return object;
+}
+
+- (void)setObject:(id)object forKey:(id<NSCopying>)key {
+ dispatch_async(_queue, ^{
+ self->_objects[key] = object;
+ });
+}
+
+- (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 {
+ // The method this calls is already synchronized.
+ return [self objectForKey:key];
+}
+
+- (void)setObject:(id)obj forKeyedSubscript:(id<NSCopying>)key {
+ // The method this calls is already synchronized.
+ [self setObject:obj forKey:key];
+}
+
+- (NSDictionary *)dictionary {
+ __block NSDictionary *dictionary;
+ dispatch_sync(_queue, ^{
+ dictionary = [self->_objects copy];
+ });
+ return dictionary;
+}
+
+@end
diff --git a/GoogleUtilities/Network/GULNetwork.m b/GoogleUtilities/Network/GULNetwork.m
new file mode 100644
index 0000000..233500b
--- /dev/null
+++ b/GoogleUtilities/Network/GULNetwork.m
@@ -0,0 +1,388 @@
+// 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 "Private/GULNetwork.h"
+#import "Private/GULNetworkMessageCode.h"
+
+#import <GoogleUtilities/GULLogger.h>
+#import <GoogleUtilities/GULNSData+zlib.h>
+#import <GoogleUtilities/GULReachabilityChecker.h>
+#import "Private/GULMutableDictionary.h"
+#import "Private/GULNetworkConstants.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 {
+ 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;
+ }
+
+ 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
diff --git a/GoogleUtilities/Network/GULNetworkConstants.m b/GoogleUtilities/Network/GULNetworkConstants.m
new file mode 100644
index 0000000..90bd03d
--- /dev/null
+++ b/GoogleUtilities/Network/GULNetworkConstants.m
@@ -0,0 +1,40 @@
+// 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 "Private/GULNetworkConstants.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]";
diff --git a/GoogleUtilities/Network/GULNetworkURLSession.m b/GoogleUtilities/Network/GULNetworkURLSession.m
new file mode 100644
index 0000000..cb8a204
--- /dev/null
+++ b/GoogleUtilities/Network/GULNetworkURLSession.m
@@ -0,0 +1,669 @@
+// 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 "Private/GULNetworkURLSession.h"
+
+#import <GoogleUtilities/GULLogger.h>
+#import "Private/GULMutableDictionary.h"
+#import "Private/GULNetworkConstants.h"
+#import "Private/GULNetworkMessageCode.h"
+
+@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;
+
+ /// The session configuration.
+ NSURLSessionConfiguration *_sessionConfig;
+
+ /// 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.
+ NSArray *paths =
+ NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
+ NSString *applicationSupportDirectory = paths.firstObject;
+ NSArray *tempPathComponents = @[
+ applicationSupportDirectory, 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.
+- (NSString *)sessionIDFromAsyncPOSTRequest:(NSURLRequest *)request
+ completionHandler:(GULNetworkURLSessionCompletionHandler)handler {
+ // 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 excluding 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];
+ _sessionConfig.URLCache = nil;
+ session = [NSURLSession sessionWithConfiguration:_sessionConfig
+ delegate:self
+ delegateQueue:[NSOperationQueue mainQueue]];
+ postRequestTask = [session uploadTaskWithRequest:request fromData:request.HTTPBody];
+ }
+
+ 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;
+ }
+
+ // Save the session into memory.
+ NSMapTable *sessionIdentifierToFetcherMap = [[self class] sessionIDToFetcherMap];
+ [sessionIdentifierToFetcherMap setObject:self forKey:_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.
+- (NSString *)sessionIDFromAsyncGETRequest:(NSURLRequest *)request
+ completionHandler:(GULNetworkURLSessionCompletionHandler)handler {
+ 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;
+ }
+
+ // Save the session into memory.
+ NSMapTable *sessionIdentifierToFetcherMap = [[self class] sessionIDToFetcherMap];
+ [sessionIdentifierToFetcherMap setObject:self forKey:_sessionID];
+
+ _request = [request copy];
+
+ _completionHandler = [handler copy];
+ [downloadTask resume];
+
+ return _sessionID;
+}
+
+#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 {
+ 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 {
+ [_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 {
+ // 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];
+}
+
+- (void)URLSession:(NSURLSession *)session
+ task:(NSURLSessionTask *)task
+ didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
+ completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition,
+ NSURLCredential *credential))completionHandler {
+ // 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]) {
+ trustError = SecTrustEvaluate(serverTrust, &trustEval);
+ }
+
+ 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 {
+#if (TARGET_OS_OSX && defined(MAC_OS_X_VERSION_10_10) && \
+ MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_10) || \
+ TARGET_OS_TV || \
+ (TARGET_OS_IOS && defined(__IPHONE_8_0) && __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0)
+
+ // iOS 8/10.10 builds require the new backgroundSessionConfiguration method name.
+ return [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:sessionID];
+
+#elif (TARGET_OS_OSX && defined(MAC_OS_X_VERSION_10_10) && \
+ MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_10) || \
+ (TARGET_OS_IOS && defined(__IPHONE_8_0) && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0)
+
+ // Do a runtime check to avoid a deprecation warning about using
+ // +backgroundSessionConfiguration: on iOS 8.
+ if ([NSURLSessionConfiguration
+ respondsToSelector:@selector(backgroundSessionConfigurationWithIdentifier:)]) {
+ // Running on iOS 8+/OS X 10.10+.
+ return [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:sessionID];
+ } else {
+ // Running on iOS 7/OS X 10.9.
+ return [NSURLSessionConfiguration backgroundSessionConfiguration:sessionID];
+ }
+
+#else
+ // Building with an SDK earlier than iOS 8/OS X 10.10.
+ return [NSURLSessionConfiguration backgroundSessionConfiguration:sessionID];
+#endif
+}
+
+- (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 {
+ NSMapTable *sessionIdentifierToFetcherMap = [self sessionIDToFetcherMap];
+ GULNetworkURLSession *session = [sessionIdentifierToFetcherMap objectForKey:sessionIdentifier];
+ if (!session && [sessionIdentifier hasPrefix:kGULNetworkBackgroundSessionConfigIDPrefix]) {
+ session = [[GULNetworkURLSession alloc] initWithNetworkLoggerDelegate:nil];
+ [session setSessionID:sessionIdentifier];
+ [sessionIdentifierToFetcherMap setObject:session forKey:sessionIdentifier];
+ }
+ return session;
+}
+
+/// Returns a map of the fetcher by session ID. Creates a map if it is not created.
++ (NSMapTable *)sessionIDToFetcherMap {
+ static NSMapTable *sessionIDToFetcherMap;
+
+ static dispatch_once_t sessionMapOnceToken;
+ dispatch_once(&sessionMapOnceToken, ^{
+ sessionIDToFetcherMap = [NSMapTable strongToWeakObjectsMapTable];
+ });
+ return sessionIDToFetcherMap;
+}
+
+/// 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 {
+ 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)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);
+ });
+ }
+}
+
+- (void)populateSessionConfig:(NSURLSessionConfiguration *)sessionConfig
+ withRequest:(NSURLRequest *)request {
+ sessionConfig.HTTPAdditionalHeaders = request.allHTTPHeaderFields;
+ sessionConfig.timeoutIntervalForRequest = request.timeoutInterval;
+ sessionConfig.timeoutIntervalForResource = request.timeoutInterval;
+ sessionConfig.requestCachePolicy = request.cachePolicy;
+}
+
+@end
diff --git a/GoogleUtilities/Network/Private/GULMutableDictionary.h b/GoogleUtilities/Network/Private/GULMutableDictionary.h
new file mode 100644
index 0000000..a8cc45b
--- /dev/null
+++ b/GoogleUtilities/Network/Private/GULMutableDictionary.h
@@ -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
diff --git a/GoogleUtilities/Network/Private/GULNetwork.h b/GoogleUtilities/Network/Private/GULNetwork.h
new file mode 100644
index 0000000..0e75ae5
--- /dev/null
+++ b/GoogleUtilities/Network/Private/GULNetwork.h
@@ -0,0 +1,87 @@
+/*
+ * 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;
+
+/// 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
diff --git a/GoogleUtilities/Network/Private/GULNetworkConstants.h b/GoogleUtilities/Network/Private/GULNetworkConstants.h
new file mode 100644
index 0000000..44d440b
--- /dev/null
+++ b/GoogleUtilities/Network/Private/GULNetworkConstants.h
@@ -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>
+#import <GoogleUtilities/GULLogger.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;
+
+#pragma mark - Error Domain
+
+extern NSString *const kGULNetworkErrorDomain;
+
+/// The logger service for GULNetwork.
+extern GULLoggerService kGULLoggerNetwork;
diff --git a/GoogleUtilities/Network/Private/GULNetworkLoggerProtocol.h b/GoogleUtilities/Network/Private/GULNetworkLoggerProtocol.h
new file mode 100644
index 0000000..f1be590
--- /dev/null
+++ b/GoogleUtilities/Network/Private/GULNetworkLoggerProtocol.h
@@ -0,0 +1,51 @@
+/*
+ * 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/GULLoggerLevel.h>
+
+#import "GULNetworkMessageCode.h"
+
+/// The log levels used by GULNetworkLogger.
+typedef NS_ENUM(NSInteger, GULNetworkLogLevel) {
+ kGULNetworkLogLevelError = GULLoggerLevelError,
+ kGULNetworkLogLevelWarning = GULLoggerLevelWarning,
+ kGULNetworkLogLevelInfo = GULLoggerLevelInfo,
+ kGULNetworkLogLevelDebug = GULLoggerLevelDebug,
+};
+
+@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
diff --git a/GoogleUtilities/Network/Private/GULNetworkMessageCode.h b/GoogleUtilities/Network/Private/GULNetworkMessageCode.h
new file mode 100644
index 0000000..ce78e60
--- /dev/null
+++ b/GoogleUtilities/Network/Private/GULNetworkMessageCode.h
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+
+// 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
+};
diff --git a/GoogleUtilities/Network/Private/GULNetworkURLSession.h b/GoogleUtilities/Network/Private/GULNetworkURLSession.h
new file mode 100644
index 0000000..81190c6
--- /dev/null
+++ b/GoogleUtilities/Network/Private/GULNetworkURLSession.h
@@ -0,0 +1,60 @@
+/*
+ * 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"
+
+typedef void (^GULNetworkCompletionHandler)(NSHTTPURLResponse *response,
+ NSData *data,
+ NSError *error);
+typedef void (^GULNetworkURLSessionCompletionHandler)(NSHTTPURLResponse *response,
+ NSData *data,
+ NSString *sessionID,
+ NSError *error);
+typedef void (^GULNetworkSystemCompletionHandler)(void);
+
+/// The protocol that uses NSURLSession for iOS >= 7.0 to handle requests and responses.
+@interface GULNetworkURLSession
+ : NSObject <NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionDownloadDelegate>
+
+/// 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) 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:(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.
+- (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.
+- (NSString *)sessionIDFromAsyncGETRequest:(NSURLRequest *)request
+ completionHandler:(GULNetworkURLSessionCompletionHandler)handler;
+
+@end
diff --git a/GoogleUtilities/Reachability/GULReachabilityChecker+Internal.h b/GoogleUtilities/Reachability/GULReachabilityChecker+Internal.h
new file mode 100644
index 0000000..8883c4d
--- /dev/null
+++ b/GoogleUtilities/Reachability/GULReachabilityChecker+Internal.h
@@ -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 <GoogleUtilities/GULReachabilityChecker.h>
+
+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;
+};
+
+@interface GULReachabilityChecker (Internal)
+
+- (const struct GULReachabilityApi *)reachabilityApi;
+- (void)setReachabilityApi:(const struct GULReachabilityApi *)reachabilityApi;
+
+@end
diff --git a/GoogleUtilities/Reachability/GULReachabilityChecker.m b/GoogleUtilities/Reachability/GULReachabilityChecker.m
new file mode 100644
index 0000000..1ddacdf
--- /dev/null
+++ b/GoogleUtilities/Reachability/GULReachabilityChecker.m
@@ -0,0 +1,240 @@
+// 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 "GULReachabilityChecker+Internal.h"
+#import "Private/GULReachabilityChecker.h"
+#import "Private/GULReachabilityMessageCode.h"
+
+#import <GoogleUtilities/GULLogger.h>
+#import <GoogleUtilities/GULReachabilityChecker.h>
+
+static GULLoggerService kGULLoggerReachability = @"[GULReachability]";
+
+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";
+
+@interface GULReachabilityChecker ()
+
+@property(nonatomic, assign) const struct GULReachabilityApi *reachabilityApi;
+@property(nonatomic, assign) GULReachabilityStatus reachabilityStatus;
+@property(nonatomic, copy) NSString *host;
+@property(nonatomic, assign) SCNetworkReachabilityRef reachability;
+
+@end
+
+@implementation GULReachabilityChecker
+
+@synthesize reachabilityApi = reachabilityApi_;
+@synthesize reachability = reachability_;
+
+- (const struct GULReachabilityApi *)reachabilityApi {
+ return reachabilityApi_;
+}
+
+- (void)setReachabilityApi:(const struct GULReachabilityApi *)reachabilityApi {
+ 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;
+}
+
+@synthesize reachabilityStatus = reachabilityStatus_;
+@synthesize host = host_;
+@synthesize reachabilityDelegate = reachabilityDelegate_;
+
+- (BOOL)isActive {
+ return reachability_ != nil;
+}
+
+- (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) {
+ [self setReachabilityDelegate:reachabilityDelegate];
+ reachabilityApi_ = &kGULDefaultReachabilityApi;
+ reachabilityStatus_ = kGULReachabilityUnknown;
+ host_ = [host copy];
+ reachability_ = nil;
+ }
+ return self;
+}
+
+- (void)dealloc {
+ reachabilityDelegate_ = nil;
+ [self stop];
+}
+
+- (BOOL)start {
+ 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;
+}
+
+- (void)stop {
+ if (reachability_) {
+ reachabilityStatus_ = kGULReachabilityUnknown;
+ reachabilityApi_->unscheduleFromRunLoopFn(reachability_, CFRunLoopGetMain(),
+ kCFRunLoopCommonModes);
+ reachabilityApi_->releaseFn(reachability_);
+ reachability_ = nil;
+ }
+}
+
+- (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
+ 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
+ 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_];
+ }
+}
+
+@end
+
+static void ReachabilityCallback(SCNetworkReachabilityRef reachability,
+ SCNetworkReachabilityFlags flags,
+ void *info) {
+ GULReachabilityChecker *checker = (__bridge GULReachabilityChecker *)info;
+ [checker reachabilityFlagsChanged:flags];
+}
+
+// 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];
+ }
+}
diff --git a/GoogleUtilities/Reachability/Private/GULReachabilityChecker.h b/GoogleUtilities/Reachability/Private/GULReachabilityChecker.h
new file mode 100644
index 0000000..b317a0b
--- /dev/null
+++ b/GoogleUtilities/Reachability/Private/GULReachabilityChecker.h
@@ -0,0 +1,77 @@
+/*
+ * 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 <SystemConfiguration/SystemConfiguration.h>
+
+/// 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
diff --git a/GoogleUtilities/Reachability/Private/GULReachabilityMessageCode.h b/GoogleUtilities/Reachability/Private/GULReachabilityMessageCode.h
new file mode 100644
index 0000000..283cdd5
--- /dev/null
+++ b/GoogleUtilities/Reachability/Private/GULReachabilityMessageCode.h
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+
+// 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
+};