diff --git a/GoogleDataTransport/GoogleDataTransport/Classes/GDTReachability.h b/GoogleDataTransport/GoogleDataTransport/Classes/GDTReachability.h new file mode 100644 index 00000000000..27f0feb808a --- /dev/null +++ b/GoogleDataTransport/GoogleDataTransport/Classes/GDTReachability.h @@ -0,0 +1,31 @@ +/* + * Copyright 2019 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** This class helps determine upload conditions by determining connectivity. */ +@interface GDTReachability : NSObject + +/** The current set flags indicating network conditions */ ++ (SCNetworkReachabilityFlags)currentFlags; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleDataTransport/GoogleDataTransport/Classes/GDTReachability.m b/GoogleDataTransport/GoogleDataTransport/Classes/GDTReachability.m new file mode 100644 index 00000000000..baedc4b6b72 --- /dev/null +++ b/GoogleDataTransport/GoogleDataTransport/Classes/GDTReachability.m @@ -0,0 +1,104 @@ +/* + * Copyright 2019 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GDTReachability.h" +#import "GDTReachability_Private.h" + +#import + +/** Sets the _callbackFlag ivar whenever the network changes. + * + * @param reachability The reachability object calling back. + * @param flags The new flag values. + * @param info Any data that might be passed in by the callback. + */ +static void GDTReachabilityCallback(SCNetworkReachabilityRef reachability, + SCNetworkReachabilityFlags flags, + void *info); + +@implementation GDTReachability { + /** The reachability object. */ + SCNetworkReachabilityRef _reachabilityRef; + + /** The queue on which callbacks and all work will occur. */ + dispatch_queue_t _reachabilityQueue; + + /** Flags specified by reachability callbacks. */ + SCNetworkConnectionFlags _callbackFlags; +} + ++ (void)load { + [self sharedInstance]; +} + ++ (instancetype)sharedInstance { + static GDTReachability *sharedInstance; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[GDTReachability alloc] init]; + }); + return sharedInstance; +} + ++ (SCNetworkReachabilityFlags)currentFlags { + __block SCNetworkReachabilityFlags currentFlags; + dispatch_sync([GDTReachability sharedInstance] -> _reachabilityQueue, ^{ + GDTReachability *reachability = [GDTReachability sharedInstance]; + currentFlags = reachability->_flags ? reachability->_flags : reachability->_callbackFlags; + }); + return currentFlags; +} + +- (instancetype)init { + self = [super init]; + if (self) { + struct sockaddr_in zeroAddress; + bzero(&zeroAddress, sizeof(zeroAddress)); + zeroAddress.sin_len = sizeof(zeroAddress); + zeroAddress.sin_family = AF_INET; + + _reachabilityQueue = dispatch_queue_create("com.google.GDTReachability", DISPATCH_QUEUE_SERIAL); + _reachabilityRef = SCNetworkReachabilityCreateWithAddress( + kCFAllocatorDefault, (const struct sockaddr *)&zeroAddress); + Boolean success = SCNetworkReachabilitySetDispatchQueue(_reachabilityRef, _reachabilityQueue); + NSAssert(success, @"The reachability queue wasn't set."); + success = SCNetworkReachabilitySetCallback(_reachabilityRef, GDTReachabilityCallback, NULL); + NSAssert(success, @"The reachability callback wasn't set."); + + // Get the initial set of flags. + dispatch_async(_reachabilityQueue, ^{ + Boolean valid = SCNetworkReachabilityGetFlags(self->_reachabilityRef, &self->_flags); + if (!valid) { + self->_flags = 0; + } + }); + } + return self; +} + +- (void)setCallbackFlags:(SCNetworkReachabilityFlags)flags { + if (_callbackFlags != flags) { + self->_callbackFlags = flags; + } +} + +@end + +static void GDTReachabilityCallback(SCNetworkReachabilityRef reachability, + SCNetworkReachabilityFlags flags, + void *info) { + [[GDTReachability sharedInstance] setCallbackFlags:flags]; +} diff --git a/GoogleDataTransport/GoogleDataTransport/Classes/GDTUploadCoordinator.m b/GoogleDataTransport/GoogleDataTransport/Classes/GDTUploadCoordinator.m index 5820444c63e..5ba819f82b1 100644 --- a/GoogleDataTransport/GoogleDataTransport/Classes/GDTUploadCoordinator.m +++ b/GoogleDataTransport/GoogleDataTransport/Classes/GDTUploadCoordinator.m @@ -20,6 +20,7 @@ #import "GDTAssert.h" #import "GDTClock.h" #import "GDTConsoleLogger.h" +#import "GDTReachability.h" #import "GDTRegistrar_Private.h" #import "GDTStorage.h" #import "GDTUploadPackage_Private.h" @@ -85,8 +86,11 @@ - (void)forceUploadForTarget:(GDTTarget)target { if (self->_runningInBackground) { [self->_forcedUploadQueue insertObject:forceUploadBlock atIndex:0]; [NSKeyedArchiver archiveRootObject:self toFile:[GDTUploadCoordinator archivePath]]; - - // Enqueue the force upload block if there's an in-flight upload for that target already. + // Enqueue the force upload block if conditions are bad or if there's an in-flight upload for + // that target already. + } else if (([self uploadConditions] & GDTUploadConditionNoNetwork) == + GDTUploadConditionNoNetwork) { + [self->_forcedUploadQueue insertObject:forceUploadBlock atIndex:0]; } else if (self->_targetToInFlightEventSet[targetNumber]) { [self->_forcedUploadQueue insertObject:forceUploadBlock atIndex:0]; } else { @@ -109,39 +113,34 @@ - (GDTStorage *)storage { // This should always be called in a thread-safe manner. When running the background, in theory, // the uploader's background task should be calling this. - (GDTUploaderCompletionBlock)onCompleteBlock { - __weak GDTUploadCoordinator *weakSelf = self; static GDTUploaderCompletionBlock onCompleteBlock; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ onCompleteBlock = ^(GDTTarget target, GDTClock *nextUploadAttemptUTC, NSError *error) { - GDTUploadCoordinator *strongSelf = weakSelf; - if (strongSelf) { - dispatch_async(strongSelf.coordinationQueue, ^{ - NSNumber *targetNumber = @(target); - if (error) { - GDTLogWarning(GDTMCWUploadFailed, @"Error during upload: %@", error); - [strongSelf->_targetToInFlightEventSet removeObjectForKey:targetNumber]; - return; - } - strongSelf->_targetToNextUploadTimes[targetNumber] = nextUploadAttemptUTC; - NSSet *events = - [strongSelf->_targetToInFlightEventSet objectForKey:targetNumber]; - GDTAssert(events, @"There should be an in-flight event set to remove."); - [strongSelf.storage removeEvents:events]; - [strongSelf->_targetToInFlightEventSet removeObjectForKey:targetNumber]; - if (strongSelf->_runningInBackground) { - [NSKeyedArchiver archiveRootObject:self toFile:[GDTUploadCoordinator archivePath]]; - } else if (strongSelf->_forcedUploadQueue.count) { - GDTUploadCoordinatorForceUploadBlock queuedBlock = - [strongSelf->_forcedUploadQueue lastObject]; - if (queuedBlock) { - dispatch_async(strongSelf->_coordinationQueue, ^{ - queuedBlock(); - }); - } + dispatch_async(self->_coordinationQueue, ^{ + NSNumber *targetNumber = @(target); + if (error) { + GDTLogWarning(GDTMCWUploadFailed, @"Error during upload: %@", error); + [self->_targetToInFlightEventSet removeObjectForKey:targetNumber]; + return; + } + self->_targetToNextUploadTimes[targetNumber] = nextUploadAttemptUTC; + NSSet *events = + [self->_targetToInFlightEventSet objectForKey:targetNumber]; + GDTAssert(events, @"There should be an in-flight event set to remove."); + [self->_storage removeEvents:events]; + [self->_targetToInFlightEventSet removeObjectForKey:targetNumber]; + if (self->_runningInBackground) { + [NSKeyedArchiver archiveRootObject:self toFile:[GDTUploadCoordinator archivePath]]; + } else if (self->_forcedUploadQueue.count) { + GDTUploadCoordinatorForceUploadBlock queuedBlock = [self->_forcedUploadQueue lastObject]; + if (queuedBlock) { + dispatch_async(self->_coordinationQueue, ^{ + queuedBlock(); + }); } - }); - } + } + }); }; }); return onCompleteBlock; @@ -153,20 +152,17 @@ - (GDTUploaderCompletionBlock)onCompleteBlock { * check the next-upload clocks of all targets to determine if an upload attempt can be made. */ - (void)startTimer { - __weak GDTUploadCoordinator *weakSelf = self; dispatch_sync(_coordinationQueue, ^{ - GDTUploadCoordinator *strongSelf = weakSelf; - GDTAssert(strongSelf, @"self must be real to start a timer."); - strongSelf->_timer = - dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, strongSelf->_coordinationQueue); - dispatch_source_set_timer(strongSelf->_timer, DISPATCH_TIME_NOW, strongSelf->_timerInterval, - strongSelf->_timerLeeway); - dispatch_source_set_event_handler(strongSelf->_timer, ^{ - if (!strongSelf->_runningInBackground) { - [strongSelf checkPrioritizersAndUploadEvents]; + self->_timer = + dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self->_coordinationQueue); + dispatch_source_set_timer(self->_timer, DISPATCH_TIME_NOW, self->_timerInterval, + self->_timerLeeway); + dispatch_source_set_event_handler(self->_timer, ^{ + if (!self->_runningInBackground) { + [self checkPrioritizersAndUploadEvents]; } }); - dispatch_resume(strongSelf->_timer); + dispatch_resume(self->_timer); }); } @@ -181,36 +177,72 @@ - (void)stopTimer { * events for that target or not. If so, queries the prioritizers */ - (void)checkPrioritizersAndUploadEvents { - __weak GDTUploadCoordinator *weakSelf = self; dispatch_async(_coordinationQueue, ^{ if (self->_runningInBackground) { return; } + + GDTUploadConditions conds = [self uploadConditions]; + if ((conds & GDTUploadConditionNoNetwork) == GDTUploadConditionNoNetwork) { + return; + } + static int count = 0; count++; - GDTUploadCoordinator *strongSelf = weakSelf; - if (strongSelf) { - NSArray *targetsReadyForUpload = [self targetsReadyForUpload]; - for (NSNumber *target in targetsReadyForUpload) { - id prioritizer = strongSelf->_registrar.targetToPrioritizer[target]; - id uploader = strongSelf->_registrar.targetToUploader[target]; - GDTAssert(prioritizer && uploader, @"Target '%@' is missing an implementation", target); - GDTUploadConditions conds = [self uploadConditions]; - GDTUploadPackage *package = [[prioritizer uploadPackageWithConditions:conds] copy]; - package.storage = strongSelf.storage; - if (package.events && package.events.count > 0) { - strongSelf->_targetToInFlightEventSet[target] = package.events; - [uploader uploadPackage:package onComplete:self.onCompleteBlock]; - } + NSArray *targetsReadyForUpload = [self targetsReadyForUpload]; + for (NSNumber *target in targetsReadyForUpload) { + id prioritizer = self->_registrar.targetToPrioritizer[target]; + id uploader = self->_registrar.targetToUploader[target]; + GDTAssert(prioritizer && uploader, @"Target '%@' is missing an implementation", target); + GDTUploadPackage *package = [[prioritizer uploadPackageWithConditions:conds] copy]; + package.storage = self.storage; + if (package.events && package.events.count > 0) { + self->_targetToInFlightEventSet[target] = package.events; + [uploader uploadPackage:package onComplete:self.onCompleteBlock]; } } }); } -/** */ +/** Returns the current upload conditions after making determinations about the network connection. + * + * @return The current upload conditions. + */ - (GDTUploadConditions)uploadConditions { - // TODO: Compute the real upload conditions. - return GDTUploadConditionMobileData; + SCNetworkReachabilityFlags currentFlags = [GDTReachability currentFlags]; + + BOOL reachable = + (currentFlags & kSCNetworkReachabilityFlagsReachable) == kSCNetworkReachabilityFlagsReachable; + BOOL connectionRequired = (currentFlags & kSCNetworkReachabilityFlagsConnectionRequired) == + kSCNetworkReachabilityFlagsConnectionRequired; + BOOL interventionRequired = (currentFlags & kSCNetworkReachabilityFlagsInterventionRequired) == + kSCNetworkReachabilityFlagsInterventionRequired; + BOOL connectionOnDemand = (currentFlags & kSCNetworkReachabilityFlagsConnectionOnDemand) == + kSCNetworkReachabilityFlagsConnectionOnDemand; + BOOL connectionOnTraffic = (currentFlags & kSCNetworkReachabilityFlagsConnectionOnTraffic) == + kSCNetworkReachabilityFlagsConnectionOnTraffic; + BOOL isWWAN = + (currentFlags & kSCNetworkReachabilityFlagsIsWWAN) == kSCNetworkReachabilityFlagsIsWWAN; + + if (!reachable) { + return GDTUploadConditionNoNetwork; + } + + GDTUploadConditions conditions = 0; + conditions |= !connectionRequired ? GDTUploadConditionWifiData : conditions; + conditions |= isWWAN ? GDTUploadConditionMobileData : conditions; + if ((connectionOnTraffic || connectionOnDemand) && !interventionRequired) { + conditions = GDTUploadConditionWifiData; + } + + BOOL wifi = (conditions & GDTUploadConditionWifiData) == GDTUploadConditionWifiData; + BOOL cell = (conditions & GDTUploadConditionMobileData) == GDTUploadConditionMobileData; + + if (!(wifi || cell)) { + conditions = GDTUploadConditionUnclearConnection; + } + + return conditions; } /** Checks the next upload time for each target and returns an array of targets that are diff --git a/GoogleDataTransport/GoogleDataTransport/Classes/Private/GDTReachability_Private.h b/GoogleDataTransport/GoogleDataTransport/Classes/Private/GDTReachability_Private.h new file mode 100644 index 00000000000..29c49706ff9 --- /dev/null +++ b/GoogleDataTransport/GoogleDataTransport/Classes/Private/GDTReachability_Private.h @@ -0,0 +1,30 @@ +/* + * Copyright 2019 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GDTReachability.h" + +@interface GDTReachability () + +/** Allows manually setting the flags for testing purposes. */ +@property(nonatomic, readwrite) SCNetworkReachabilityFlags flags; + +/** Creates/returns the singleton instance of this class. + * + * @return The singleton instance of this class. + */ ++ (instancetype)sharedInstance; + +@end diff --git a/GoogleDataTransport/GoogleDataTransport/Classes/Public/GDTPrioritizer.h b/GoogleDataTransport/GoogleDataTransport/Classes/Public/GDTPrioritizer.h index b9243f0a92d..23518926777 100644 --- a/GoogleDataTransport/GoogleDataTransport/Classes/Public/GDTPrioritizer.h +++ b/GoogleDataTransport/GoogleDataTransport/Classes/Public/GDTPrioritizer.h @@ -28,14 +28,20 @@ NS_ASSUME_NONNULL_BEGIN */ typedef NS_OPTIONS(NSInteger, GDTUploadConditions) { + /** An upload shouldn't be attempted, because there's no network. */ + GDTUploadConditionNoNetwork = 1 << 0, + /** An upload would likely use mobile data. */ - GDTUploadConditionMobileData, + GDTUploadConditionMobileData = 1 << 1, /** An upload would likely use wifi data. */ - GDTUploadConditionWifiData, + GDTUploadConditionWifiData = 1 << 2, + + /** An upload uses some sort of network connection, but it's unclear which. */ + GDTUploadConditionUnclearConnection = 1 << 3, /** A high priority event has occurred. */ - GDTUploadConditionHighPriority, + GDTUploadConditionHighPriority = 1 << 4, }; /** This protocol defines the common interface of event prioritization. Prioritizers are diff --git a/GoogleDataTransport/Tests/Integration/GDTIntegrationTest.m b/GoogleDataTransport/Tests/Integration/GDTIntegrationTest.m index 04051f5188d..4086632b4b5 100644 --- a/GoogleDataTransport/Tests/Integration/GDTIntegrationTest.m +++ b/GoogleDataTransport/Tests/Integration/GDTIntegrationTest.m @@ -22,6 +22,7 @@ #import "GDTIntegrationTestUploader.h" #import "GDTTestServer.h" +#import "GDTReachability_Private.h" #import "GDTStorage_Private.h" #import "GDTUploadCoordinator+Testing.h" @@ -84,6 +85,9 @@ - (void)testEndToEndEvent { XCTestExpectation *expectation = [self expectationWithDescription:@"server got the request"]; expectation.assertForOverFulfill = NO; + // Manually set the reachability flag. + [GDTReachability sharedInstance].flags = kSCNetworkReachabilityFlagsReachable; + // Create the server. GDTTestServer *testServer = [[GDTTestServer alloc] init]; [testServer setResponseCompletedBlock:^(GCDWebServerRequest *_Nonnull request, diff --git a/GoogleDataTransport/Tests/Integration/Helpers/GDTIntegrationTestPrioritizer.m b/GoogleDataTransport/Tests/Integration/Helpers/GDTIntegrationTestPrioritizer.m index 07d4b4c260b..0e746b475ec 100644 --- a/GoogleDataTransport/Tests/Integration/Helpers/GDTIntegrationTestPrioritizer.m +++ b/GoogleDataTransport/Tests/Integration/Helpers/GDTIntegrationTestPrioritizer.m @@ -69,7 +69,7 @@ - (GDTUploadPackage *)uploadPackageWithConditions:(GDTUploadConditions)condition [[GDTIntegrationTestUploadPackage alloc] init]; dispatch_sync(_queue, ^{ if ((conditions & GDTUploadConditionWifiData) == GDTUploadConditionWifiData) { - uploadPackage.events = self.wifiOnlyEvents; + uploadPackage.events = [self.wifiOnlyEvents setByAddingObjectsFromSet:self.nonWifiEvents]; } else { uploadPackage.events = self.nonWifiEvents; } diff --git a/GoogleDataTransport/Tests/Unit/GDTTestCase.m b/GoogleDataTransport/Tests/Unit/GDTTestCase.m index 5c788a80614..98b0bb1a1c3 100644 --- a/GoogleDataTransport/Tests/Unit/GDTTestCase.m +++ b/GoogleDataTransport/Tests/Unit/GDTTestCase.m @@ -16,11 +16,13 @@ #import "GDTTestCase.h" +#import "GDTReachability_Private.h" #import "GDTUploadCoordinator+Testing.h" @implementation GDTTestCase - (void)setUp { + [GDTReachability sharedInstance].flags = kSCNetworkReachabilityFlagsReachable; [[GDTUploadCoordinator sharedInstance] stopTimer]; } diff --git a/GoogleDataTransportCCTSupport/GoogleDataTransportCCTSupport/Classes/GDTCCTPrioritizer.m b/GoogleDataTransportCCTSupport/GoogleDataTransportCCTSupport/Classes/GDTCCTPrioritizer.m index 7125173ba4d..b262aafca8f 100644 --- a/GoogleDataTransportCCTSupport/GoogleDataTransportCCTSupport/Classes/GDTCCTPrioritizer.m +++ b/GoogleDataTransportCCTSupport/GoogleDataTransportCCTSupport/Classes/GDTCCTPrioritizer.m @@ -168,4 +168,15 @@ typedef NS_ENUM(NSInteger, GDTCCTQoSTier) { }]; } +#pragma mark - GDTLifecycleProtocol + +- (void)appWillBackground:(UIApplication *)app { +} + +- (void)appWillForeground:(UIApplication *)app { +} + +- (void)appWillTerminate:(UIApplication *)application { +} + @end diff --git a/GoogleDataTransportCCTSupport/GoogleDataTransportCCTSupport/Classes/GDTCCTUploader.m b/GoogleDataTransportCCTSupport/GoogleDataTransportCCTSupport/Classes/GDTCCTUploader.m index 175befe572b..4ab61c732d1 100644 --- a/GoogleDataTransportCCTSupport/GoogleDataTransportCCTSupport/Classes/GDTCCTUploader.m +++ b/GoogleDataTransportCCTSupport/GoogleDataTransportCCTSupport/Classes/GDTCCTUploader.m @@ -138,4 +138,15 @@ - (nonnull NSData *)constructRequestProtoFromPackage:(GDTUploadPackage *)package return data ? data : [[NSData alloc] init]; } +#pragma mark - GDTLifecycleProtocol + +- (void)appWillBackground:(UIApplication *)app { +} + +- (void)appWillForeground:(UIApplication *)app { +} + +- (void)appWillTerminate:(UIApplication *)application { +} + @end