diff --git a/GoogleDataTransport.podspec b/GoogleDataTransport.podspec index 2cf45ac0fc7..d05ca21050a 100644 --- a/GoogleDataTransport.podspec +++ b/GoogleDataTransport.podspec @@ -41,6 +41,11 @@ Shared library for iOS SDK data transport needs. test_spec.source_files = ['GoogleDataTransport/Tests/Unit/**/*.{h,m}'] + common_test_sources end + s.test_spec 'Tests-Lifecycle' do |test_spec| + test_spec.requires_app_host = false + test_spec.source_files = ['GoogleDataTransport/Tests/Lifecycle/**/*.{h,m}'] + common_test_sources + end + # Integration test specs s.test_spec 'Tests-Integration' do |test_spec| test_spec.requires_app_host = false diff --git a/GoogleDataTransport/GoogleDataTransport/Classes/GDTLifecycle.m b/GoogleDataTransport/GoogleDataTransport/Classes/GDTLifecycle.m index 9ff5b240c92..dc7a2571007 100644 --- a/GoogleDataTransport/GoogleDataTransport/Classes/GDTLifecycle.m +++ b/GoogleDataTransport/GoogleDataTransport/Classes/GDTLifecycle.m @@ -69,27 +69,27 @@ - (void)dealloc { } - (void)applicationDidEnterBackground:(NSNotification *)notification { - // UIApplication *application = [UIApplication sharedApplication]; - // [[GDTTransformer sharedInstance] appWillBackground:application]; - // [[GDTStorage sharedInstance] appWillBackground:application]; - // [[GDTUploadCoordinator sharedInstance] appWillBackground:application]; - // [[GDTRegistrar sharedInstance] appWillBackground:application]; + UIApplication *application = [UIApplication sharedApplication]; + [[GDTTransformer sharedInstance] appWillBackground:application]; + [[GDTStorage sharedInstance] appWillBackground:application]; + [[GDTUploadCoordinator sharedInstance] appWillBackground:application]; + [[GDTRegistrar sharedInstance] appWillBackground:application]; } - (void)applicationWillEnterForeground:(NSNotification *)notification { - // UIApplication *application = [UIApplication sharedApplication]; - // [[GDTTransformer sharedInstance] appWillForeground:application]; - // [[GDTStorage sharedInstance] appWillForeground:application]; - // [[GDTUploadCoordinator sharedInstance] appWillForeground:application]; - // [[GDTRegistrar sharedInstance] appWillForeground:application]; + UIApplication *application = [UIApplication sharedApplication]; + [[GDTTransformer sharedInstance] appWillForeground:application]; + [[GDTStorage sharedInstance] appWillForeground:application]; + [[GDTUploadCoordinator sharedInstance] appWillForeground:application]; + [[GDTRegistrar sharedInstance] appWillForeground:application]; } - (void)applicationWillTerminate:(NSNotification *)notification { - // UIApplication *application = [UIApplication sharedApplication]; - // [[GDTTransformer sharedInstance] appWillTerminate:application]; - // [[GDTStorage sharedInstance] appWillTerminate:application]; - // [[GDTUploadCoordinator sharedInstance] appWillTerminate:application]; - // [[GDTRegistrar sharedInstance] appWillTerminate:application]; + UIApplication *application = [UIApplication sharedApplication]; + [[GDTTransformer sharedInstance] appWillTerminate:application]; + [[GDTStorage sharedInstance] appWillTerminate:application]; + [[GDTUploadCoordinator sharedInstance] appWillTerminate:application]; + [[GDTRegistrar sharedInstance] appWillTerminate:application]; } @end diff --git a/GoogleDataTransport/GoogleDataTransport/Classes/GDTRegistrar.m b/GoogleDataTransport/GoogleDataTransport/Classes/GDTRegistrar.m index 91bd0730cbb..872653c86d5 100644 --- a/GoogleDataTransport/GoogleDataTransport/Classes/GDTRegistrar.m +++ b/GoogleDataTransport/GoogleDataTransport/Classes/GDTRegistrar.m @@ -89,4 +89,39 @@ - (void)registerPrioritizer:(id)prioritizer target:(GDTTarget)ta return targetToPrioritizer; } +#pragma mark - GDTLifecycleProtocol + +- (void)appWillBackground:(nonnull UIApplication *)app { + dispatch_async(_registrarQueue, ^{ + for (id uploader in [self->_targetToUploader allValues]) { + [uploader appWillBackground:app]; + } + for (id prioritizer in [self->_targetToPrioritizer allValues]) { + [prioritizer appWillBackground:app]; + } + }); +} + +- (void)appWillForeground:(nonnull UIApplication *)app { + dispatch_async(_registrarQueue, ^{ + for (id uploader in [self->_targetToUploader allValues]) { + [uploader appWillForeground:app]; + } + for (id prioritizer in [self->_targetToPrioritizer allValues]) { + [prioritizer appWillForeground:app]; + } + }); +} + +- (void)appWillTerminate:(nonnull UIApplication *)app { + dispatch_sync(_registrarQueue, ^{ + for (id uploader in [self->_targetToUploader allValues]) { + [uploader appWillTerminate:app]; + } + for (id prioritizer in [self->_targetToPrioritizer allValues]) { + [prioritizer appWillTerminate:app]; + } + }); +} + @end diff --git a/GoogleDataTransport/GoogleDataTransport/Classes/GDTStorage.h b/GoogleDataTransport/GoogleDataTransport/Classes/GDTStorage.h index ad47c837845..6e3b98b64ae 100644 --- a/GoogleDataTransport/GoogleDataTransport/Classes/GDTStorage.h +++ b/GoogleDataTransport/GoogleDataTransport/Classes/GDTStorage.h @@ -16,13 +16,15 @@ #import +#import + @class GDTEvent; @class GDTStoredEvent; NS_ASSUME_NONNULL_BEGIN /** Manages the storage of events. This class is thread-safe. */ -@interface GDTStorage : NSObject +@interface GDTStorage : NSObject /** Creates and/or returns the storage singleton. * diff --git a/GoogleDataTransport/GoogleDataTransport/Classes/GDTStorage.m b/GoogleDataTransport/GoogleDataTransport/Classes/GDTStorage.m index bbe6336ce7b..a8b430d424b 100644 --- a/GoogleDataTransport/GoogleDataTransport/Classes/GDTStorage.m +++ b/GoogleDataTransport/GoogleDataTransport/Classes/GDTStorage.m @@ -23,6 +23,7 @@ #import "GDTAssert.h" #import "GDTConsoleLogger.h" #import "GDTEvent_Private.h" +#import "GDTLifecycle.h" #import "GDTRegistrar_Private.h" #import "GDTUploadCoordinator.h" @@ -75,6 +76,13 @@ - (instancetype)init { - (void)storeEvent:(GDTEvent *)event { [self createEventDirectoryIfNotExists]; + __block UIBackgroundTaskIdentifier bgID = UIBackgroundTaskInvalid; + if (_runningInBackground) { + bgID = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ + [[UIApplication sharedApplication] endBackgroundTask:bgID]; + }]; + } + dispatch_async(_storageQueue, ^{ // Check that a backend implementation is available for this target. NSInteger target = event.target; @@ -99,6 +107,12 @@ - (void)storeEvent:(GDTEvent *)event { if (event.qosTier == GDTEventQoSFast) { [self.uploader forceUploadForTarget:target]; } + + // If running in the background, save state to disk and end the associated background task. + if (bgID != UIBackgroundTaskInvalid) { + [NSKeyedArchiver archiveRootObject:self toFile:[GDTStorage archivePath]]; + [[UIApplication sharedApplication] endBackgroundTask:bgID]; + } }); } @@ -180,6 +194,29 @@ - (void)addEventToTrackingCollections:(GDTStoredEvent *)event { _targetToEventSet[event.target] = events; } +#pragma mark - GDTLifecycleProtocol + +- (void)appWillForeground:(UIApplication *)app { + [NSKeyedUnarchiver unarchiveObjectWithFile:[GDTStorage archivePath]]; + self->_runningInBackground = NO; +} + +- (void)appWillBackground:(UIApplication *)app { + self->_runningInBackground = YES; + [NSKeyedArchiver archiveRootObject:self toFile:[GDTStorage archivePath]]; + // Create an immediate background task to run until the end of the current queue of work. + __block UIBackgroundTaskIdentifier bgID = [app beginBackgroundTaskWithExpirationHandler:^{ + [app endBackgroundTask:bgID]; + }]; + dispatch_async(_storageQueue, ^{ + [app endBackgroundTask:bgID]; + }); +} + +- (void)appWillTerminate:(UIApplication *)application { + [NSKeyedArchiver archiveRootObject:self toFile:[GDTStorage archivePath]]; +} + #pragma mark - NSSecureCoding /** The NSKeyedCoder key for the storedEvents property. */ diff --git a/GoogleDataTransport/GoogleDataTransport/Classes/GDTTransformer.h b/GoogleDataTransport/GoogleDataTransport/Classes/GDTTransformer.h index 1dbbe25f3a4..9b1ba3e70e0 100644 --- a/GoogleDataTransport/GoogleDataTransport/Classes/GDTTransformer.h +++ b/GoogleDataTransport/GoogleDataTransport/Classes/GDTTransformer.h @@ -16,6 +16,8 @@ #import +#import + @class GDTEvent; @protocol GDTEventTransformer; @@ -28,7 +30,7 @@ NS_ASSUME_NONNULL_BEGIN * maintain state (or at least, there's nothing to stop them from doing that) and the same instances * may be used across multiple instances. */ -@interface GDTTransformer : NSObject +@interface GDTTransformer : NSObject /** Instantiates or returns the event transformer singleton. * @@ -37,6 +39,9 @@ NS_ASSUME_NONNULL_BEGIN + (instancetype)sharedInstance; /** Writes the result of applying the given transformers' -transform method on the given event. + * + * @note If the app is suspended, a background task will be created to complete work in-progress, + * but this method will not send any further events until the app is resumed. * * @param event The event to apply transformers on. * @param transformers The list of transformers to apply. diff --git a/GoogleDataTransport/GoogleDataTransport/Classes/GDTTransformer.m b/GoogleDataTransport/GoogleDataTransport/Classes/GDTTransformer.m index 0afa6db3536..04b07d2cc9d 100644 --- a/GoogleDataTransport/GoogleDataTransport/Classes/GDTTransformer.m +++ b/GoogleDataTransport/GoogleDataTransport/Classes/GDTTransformer.m @@ -21,6 +21,7 @@ #import "GDTAssert.h" #import "GDTConsoleLogger.h" +#import "GDTLifecycle.h" #import "GDTStorage.h" @implementation GDTTransformer @@ -46,6 +47,13 @@ - (instancetype)init { - (void)transformEvent:(GDTEvent *)event withTransformers:(NSArray> *)transformers { GDTAssert(event, @"You can't write a nil event"); + + __block UIBackgroundTaskIdentifier bgID = UIBackgroundTaskInvalid; + if (_runningInBackground) { + bgID = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ + [[UIApplication sharedApplication] endBackgroundTask:bgID]; + }]; + } dispatch_async(_eventWritingQueue, ^{ GDTEvent *transformedEvent = event; for (id transformer in transformers) { @@ -61,7 +69,34 @@ - (void)transformEvent:(GDTEvent *)event } } [self.storageInstance storeEvent:transformedEvent]; + if (self->_runningInBackground) { + [[UIApplication sharedApplication] endBackgroundTask:bgID]; + } + }); +} + +#pragma mark - GDTLifecycleProtocol + +- (void)appWillForeground:(UIApplication *)app { + dispatch_async(_eventWritingQueue, ^{ + self->_runningInBackground = NO; }); } +- (void)appWillBackground:(UIApplication *)app { + // Create an immediate background task to run until the end of the current queue of work. + __block UIBackgroundTaskIdentifier bgID = [app beginBackgroundTaskWithExpirationHandler:^{ + [app endBackgroundTask:bgID]; + }]; + dispatch_async(_eventWritingQueue, ^{ + [app endBackgroundTask:bgID]; + }); +} + +- (void)appWillTerminate:(UIApplication *)application { + // Flush the queue immediately. + dispatch_sync(_eventWritingQueue, ^{ + }); +} + @end diff --git a/GoogleDataTransport/GoogleDataTransport/Classes/GDTUploadCoordinator.h b/GoogleDataTransport/GoogleDataTransport/Classes/GDTUploadCoordinator.h index 38ccd9fe27d..3aebef4e7bc 100644 --- a/GoogleDataTransport/GoogleDataTransport/Classes/GDTUploadCoordinator.h +++ b/GoogleDataTransport/GoogleDataTransport/Classes/GDTUploadCoordinator.h @@ -16,6 +16,8 @@ #import +#import + #import "GDTRegistrar.h" NS_ASSUME_NONNULL_BEGIN @@ -23,7 +25,7 @@ NS_ASSUME_NONNULL_BEGIN /** This class connects storage and uploader implementations, providing events to an uploader * and informing the storage what events were successfully uploaded or not. */ -@interface GDTUploadCoordinator : NSObject +@interface GDTUploadCoordinator : NSObject /** Creates and/or returrns the singleton. * diff --git a/GoogleDataTransport/GoogleDataTransport/Classes/GDTUploadCoordinator.m b/GoogleDataTransport/GoogleDataTransport/Classes/GDTUploadCoordinator.m index 70ad244af91..5820444c63e 100644 --- a/GoogleDataTransport/GoogleDataTransport/Classes/GDTUploadCoordinator.m +++ b/GoogleDataTransport/GoogleDataTransport/Classes/GDTUploadCoordinator.m @@ -82,8 +82,12 @@ - (void)forceUploadForTarget:(GDTTarget)target { [self->_forcedUploadQueue removeLastObject]; }; - // Enqueue the force upload block if there's an in-flight upload for that target already. - if (self->_targetToInFlightEventSet[targetNumber]) { + 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. + } else if (self->_targetToInFlightEventSet[targetNumber]) { [self->_forcedUploadQueue insertObject:forceUploadBlock atIndex:0]; } else { forceUploadBlock(); @@ -102,7 +106,8 @@ - (GDTStorage *)storage { return _storage; } -// This should always be called in a thread-safe manner. +// 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; @@ -124,7 +129,9 @@ - (GDTUploaderCompletionBlock)onCompleteBlock { GDTAssert(events, @"There should be an in-flight event set to remove."); [strongSelf.storage removeEvents:events]; [strongSelf->_targetToInFlightEventSet removeObjectForKey:targetNumber]; - if (strongSelf->_forcedUploadQueue.count) { + if (strongSelf->_runningInBackground) { + [NSKeyedArchiver archiveRootObject:self toFile:[GDTUploadCoordinator archivePath]]; + } else if (strongSelf->_forcedUploadQueue.count) { GDTUploadCoordinatorForceUploadBlock queuedBlock = [strongSelf->_forcedUploadQueue lastObject]; if (queuedBlock) { @@ -155,7 +162,9 @@ - (void)startTimer { dispatch_source_set_timer(strongSelf->_timer, DISPATCH_TIME_NOW, strongSelf->_timerInterval, strongSelf->_timerLeeway); dispatch_source_set_event_handler(strongSelf->_timer, ^{ - [self checkPrioritizersAndUploadEvents]; + if (!strongSelf->_runningInBackground) { + [strongSelf checkPrioritizersAndUploadEvents]; + } }); dispatch_resume(strongSelf->_timer); }); @@ -174,6 +183,9 @@ - (void)stopTimer { - (void)checkPrioritizersAndUploadEvents { __weak GDTUploadCoordinator *weakSelf = self; dispatch_async(_coordinationQueue, ^{ + if (self->_runningInBackground) { + return; + } static int count = 0; count++; GDTUploadCoordinator *strongSelf = weakSelf; @@ -224,4 +236,77 @@ - (GDTUploadConditions)uploadConditions { return targetsReadyForUpload; } +#pragma mark - NSSecureCoding support + +/** The keyed archiver key for the _targetToNextUploadTimes property. */ +static NSString *const kTargetToNextUploadTimesKey = + @"GDTUploadCoordinatorTargetToNextUploadTimesKey"; + +/** The keyed archiver key for the _targetToInFlightEventSet property. */ +static NSString *const kTargetToInFlightEventSetKey = + @"GDTUploadCoordinatorTargetToInFlightEventSetKey"; + +/** The keyed archiver key for the _forcedUploadQueue property. */ +static NSString *const kForcedUploadQueueKey = @"GDTUploadCoordinatorForcedUploadQueueKey"; + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + GDTUploadCoordinator *sharedInstance = [self.class sharedInstance]; + dispatch_sync(sharedInstance->_coordinationQueue, ^{ + sharedInstance->_targetToNextUploadTimes = + [aDecoder decodeObjectOfClass:[NSMutableDictionary class] + forKey:kTargetToNextUploadTimesKey]; + sharedInstance->_targetToInFlightEventSet = + [aDecoder decodeObjectOfClass:[NSMutableDictionary class] + forKey:kTargetToInFlightEventSetKey]; + sharedInstance->_forcedUploadQueue = [aDecoder decodeObjectOfClass:[NSMutableArray class] + forKey:kForcedUploadQueueKey]; + }); + return sharedInstance; +} + +// Needs to always be called on the queue to be thread-safe. +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:self->_targetToNextUploadTimes forKey:kTargetToNextUploadTimesKey]; + [aCoder encodeObject:self->_targetToInFlightEventSet forKey:kTargetToInFlightEventSetKey]; + [aCoder encodeObject:self->_forcedUploadQueue forKey:kForcedUploadQueueKey]; +} + +#pragma mark - GDTLifecycleProtocol + +- (void)appWillForeground:(UIApplication *)app { + // Not entirely thread-safe, but it should be fine. + self->_runningInBackground = NO; + [self startTimer]; + [NSKeyedUnarchiver unarchiveObjectWithFile:[GDTUploadCoordinator archivePath]]; +} + +- (void)appWillBackground:(UIApplication *)app { + // Not entirely thread-safe, but it should be fine. + self->_runningInBackground = YES; + + // Should be thread-safe. If it ends up not being, put this in a dispatch_sync. + [self stopTimer]; + + // Create an immediate background task to run until the end of the current queue of work. + __block UIBackgroundTaskIdentifier bgID = [app beginBackgroundTaskWithExpirationHandler:^{ + [NSKeyedArchiver archiveRootObject:self toFile:[GDTUploadCoordinator archivePath]]; + [app endBackgroundTask:bgID]; + }]; + dispatch_async(_coordinationQueue, ^{ + [NSKeyedArchiver archiveRootObject:self toFile:[GDTUploadCoordinator archivePath]]; + [app endBackgroundTask:bgID]; + }); +} + +- (void)appWillTerminate:(UIApplication *)application { + dispatch_sync(_coordinationQueue, ^{ + [self stopTimer]; + [NSKeyedArchiver archiveRootObject:self toFile:[GDTUploadCoordinator archivePath]]; + }); +} + @end diff --git a/GoogleDataTransport/GoogleDataTransport/Classes/Public/GDTPrioritizer.h b/GoogleDataTransport/GoogleDataTransport/Classes/Public/GDTPrioritizer.h index 7b76e95fdf2..b9243f0a92d 100644 --- a/GoogleDataTransport/GoogleDataTransport/Classes/Public/GDTPrioritizer.h +++ b/GoogleDataTransport/GoogleDataTransport/Classes/Public/GDTPrioritizer.h @@ -16,6 +16,7 @@ #import +#import #import @class GDTStoredEvent; @@ -41,7 +42,7 @@ typedef NS_OPTIONS(NSInteger, GDTUploadConditions) { * stateful objects that prioritize events upon insertion into storage and remain prepared to return * a set of filenames to the storage system. */ -@protocol GDTPrioritizer +@protocol GDTPrioritizer @required diff --git a/GoogleDataTransport/GoogleDataTransport/Classes/Public/GDTRegistrar.h b/GoogleDataTransport/GoogleDataTransport/Classes/Public/GDTRegistrar.h index f1ee1411f43..8d532b3ee48 100644 --- a/GoogleDataTransport/GoogleDataTransport/Classes/Public/GDTRegistrar.h +++ b/GoogleDataTransport/GoogleDataTransport/Classes/Public/GDTRegistrar.h @@ -23,7 +23,7 @@ NS_ASSUME_NONNULL_BEGIN /** Manages the registration of targets with the transport SDK. */ -@interface GDTRegistrar : NSObject +@interface GDTRegistrar : NSObject /** Creates and/or returns the singleton instance. * diff --git a/GoogleDataTransport/GoogleDataTransport/Classes/Public/GDTUploader.h b/GoogleDataTransport/GoogleDataTransport/Classes/Public/GDTUploader.h index e9e1a36d266..3b532af0466 100644 --- a/GoogleDataTransport/GoogleDataTransport/Classes/Public/GDTUploader.h +++ b/GoogleDataTransport/GoogleDataTransport/Classes/Public/GDTUploader.h @@ -17,6 +17,7 @@ #import #import +#import #import NS_ASSUME_NONNULL_BEGIN @@ -33,7 +34,7 @@ typedef void (^GDTUploaderCompletionBlock)(GDTTarget target, NSError *_Nullable uploadError); /** This protocol defines the common interface for uploader implementations. */ -@protocol GDTUploader +@protocol GDTUploader @required diff --git a/GoogleDataTransport/GoogleDataTransport/Classes/Public/GoogleDataTransport.h b/GoogleDataTransport/GoogleDataTransport/Classes/Public/GoogleDataTransport.h index 823a0b5e4e3..e5cca3cfbae 100644 --- a/GoogleDataTransport/GoogleDataTransport/Classes/Public/GoogleDataTransport.h +++ b/GoogleDataTransport/GoogleDataTransport/Classes/Public/GoogleDataTransport.h @@ -18,6 +18,7 @@ #import "GDTEvent.h" #import "GDTEventDataObject.h" #import "GDTEventTransformer.h" +#import "GDTLifecycle.h" #import "GDTPrioritizer.h" #import "GDTRegistrar.h" #import "GDTStoredEvent.h" diff --git a/GoogleDataTransport/Tests/Common/Categories/GDTStorage+Testing.m b/GoogleDataTransport/Tests/Common/Categories/GDTStorage+Testing.m index 6c57fcc162c..5dd14843fc0 100644 --- a/GoogleDataTransport/Tests/Common/Categories/GDTStorage+Testing.m +++ b/GoogleDataTransport/Tests/Common/Categories/GDTStorage+Testing.m @@ -16,12 +16,16 @@ #import "GDTStorage+Testing.h" +#import "GDTStorage_Private.h" + @implementation GDTStorage (Testing) - (void)reset { dispatch_sync(self.storageQueue, ^{ [self.targetToEventSet removeAllObjects]; [self.storedEvents removeAllObjects]; + NSError *error; + [[NSFileManager defaultManager] removeItemAtPath:[GDTStorage archivePath] error:&error]; }); } diff --git a/GoogleDataTransport/Tests/Integration/Helpers/GDTIntegrationTestPrioritizer.m b/GoogleDataTransport/Tests/Integration/Helpers/GDTIntegrationTestPrioritizer.m index 24e83b12c04..07d4b4c260b 100644 --- a/GoogleDataTransport/Tests/Integration/Helpers/GDTIntegrationTestPrioritizer.m +++ b/GoogleDataTransport/Tests/Integration/Helpers/GDTIntegrationTestPrioritizer.m @@ -77,4 +77,13 @@ - (GDTUploadPackage *)uploadPackageWithConditions:(GDTUploadConditions)condition return uploadPackage; } +- (void)appWillBackground:(UIApplication *)app { +} + +- (void)appWillForeground:(UIApplication *)app { +} + +- (void)appWillTerminate:(UIApplication *)application { +} + @end diff --git a/GoogleDataTransport/Tests/Integration/Helpers/GDTIntegrationTestUploader.m b/GoogleDataTransport/Tests/Integration/Helpers/GDTIntegrationTestUploader.m index 96da7e0235d..c3702bca2ec 100644 --- a/GoogleDataTransport/Tests/Integration/Helpers/GDTIntegrationTestUploader.m +++ b/GoogleDataTransport/Tests/Integration/Helpers/GDTIntegrationTestUploader.m @@ -72,4 +72,13 @@ - (void)uploadPackage:(GDTUploadPackage *)package [uploadTask resume]; } +- (void)appWillBackground:(UIApplication *)app { +} + +- (void)appWillForeground:(UIApplication *)app { +} + +- (void)appWillTerminate:(UIApplication *)application { +} + @end diff --git a/GoogleDataTransport/Tests/Lifecycle/GDTLifecycleTest.m b/GoogleDataTransport/Tests/Lifecycle/GDTLifecycleTest.m new file mode 100644 index 00000000000..de49731a366 --- /dev/null +++ b/GoogleDataTransport/Tests/Lifecycle/GDTLifecycleTest.m @@ -0,0 +1,182 @@ +/* + * 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 + +#import + +#import "GDTStorage_Private.h" +#import "GDTTransformer_Private.h" +#import "GDTUploadCoordinator_Private.h" + +#import "GDTLifecycleTestPrioritizer.h" +#import "GDTLifecycleTestUploader.h" +#import "GDTStorage+Testing.h" +#import "GDTUploadCoordinator+Testing.h" + +/** Waits for the result of waitBlock to be YES, or times out and fails. + * + * @param waitBlock The block to periodically execute. + * @param timeInterval The timeout. + */ +#define GDTWaitForBlock(waitBlock, timeInterval) \ + { \ + NSPredicate *pred = \ + [NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, \ + NSDictionary *_Nullable bindings) { \ + return waitBlock(); \ + }]; \ + XCTestExpectation *expectation = [self expectationForPredicate:pred \ + evaluatedWithObject:[[NSObject alloc] init] \ + handler:^BOOL { \ + return YES; \ + }]; \ + [self waitForExpectations:@[ expectation ] timeout:timeInterval]; \ + } + +/** A test-only event data object used in this integration test. */ +@interface GDTLifecycleTestEvent : NSObject + +@end + +@implementation GDTLifecycleTestEvent + +- (NSData *)transportBytes { + // In real usage, protobuf's -data method or a custom implementation using nanopb are used. + return [[NSString stringWithFormat:@"%@", [NSDate date]] dataUsingEncoding:NSUTF8StringEncoding]; +} + +@end + +@interface GDTLifecycleTest : XCTestCase + +/** The test prioritizer. */ +@property(nonatomic) GDTLifecycleTestPrioritizer *prioritizer; + +/** The test uploader. */ +@property(nonatomic) GDTLifecycleTestUploader *uploader; + +@end + +@implementation GDTLifecycleTest + +- (void)setUp { + [super setUp]; + // Don't check the error, because it'll be populated in cases where the file doesn't exist. + NSError *error; + [[NSFileManager defaultManager] removeItemAtPath:[GDTStorage archivePath] error:&error]; + [[NSFileManager defaultManager] removeItemAtPath:[GDTUploadCoordinator archivePath] error:&error]; + self.uploader = [[GDTLifecycleTestUploader alloc] init]; + [[GDTRegistrar sharedInstance] registerUploader:self.uploader target:kGDTTargetTest]; + + self.prioritizer = [[GDTLifecycleTestPrioritizer alloc] init]; + [[GDTRegistrar sharedInstance] registerPrioritizer:self.prioritizer target:kGDTTargetTest]; + [[GDTStorage sharedInstance] reset]; + [[GDTUploadCoordinator sharedInstance] reset]; +} + +/** Tests that the library serializes itself to disk when the app backgrounds. */ +- (void)testBackgrounding { + GDTTransport *transport = [[GDTTransport alloc] initWithMappingID:@"test" + transformers:nil + target:kGDTTargetTest]; + GDTEvent *event = [transport eventForTransport]; + event.dataObject = [[GDTLifecycleTestEvent alloc] init]; + XCTAssertEqual([GDTStorage sharedInstance].storedEvents.count, 0); + [transport sendDataEvent:event]; + GDTWaitForBlock( + ^BOOL { + return [GDTStorage sharedInstance].storedEvents.count > 0; + }, + 5.0); + + NSNotificationCenter *notifCenter = [NSNotificationCenter defaultCenter]; + [notifCenter postNotificationName:UIApplicationDidEnterBackgroundNotification object:nil]; + XCTAssertTrue([GDTStorage sharedInstance].runningInBackground); + XCTAssertTrue([GDTUploadCoordinator sharedInstance].runningInBackground); + GDTWaitForBlock( + ^BOOL { + NSFileManager *fm = [NSFileManager defaultManager]; + return [fm fileExistsAtPath:[GDTStorage archivePath] isDirectory:NULL] && + [fm fileExistsAtPath:[GDTUploadCoordinator archivePath] isDirectory:NULL]; + }, + 5.0); +} + +/** Tests that the library deserializes itself from disk when the app foregrounds. */ +- (void)testForegrounding { + GDTTransport *transport = [[GDTTransport alloc] initWithMappingID:@"test" + transformers:nil + target:kGDTTargetTest]; + GDTEvent *event = [transport eventForTransport]; + event.dataObject = [[GDTLifecycleTestEvent alloc] init]; + XCTAssertEqual([GDTStorage sharedInstance].storedEvents.count, 0); + [transport sendDataEvent:event]; + GDTWaitForBlock( + ^BOOL { + return [GDTStorage sharedInstance].storedEvents.count > 0; + }, + 5.0); + + NSNotificationCenter *notifCenter = [NSNotificationCenter defaultCenter]; + [notifCenter postNotificationName:UIApplicationDidEnterBackgroundNotification object:nil]; + + GDTWaitForBlock( + ^BOOL { + NSFileManager *fm = [NSFileManager defaultManager]; + return [fm fileExistsAtPath:[GDTStorage archivePath] isDirectory:NULL] && + [fm fileExistsAtPath:[GDTUploadCoordinator archivePath] isDirectory:NULL]; + }, + 5.0); + + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; + [notifCenter postNotificationName:UIApplicationWillEnterForegroundNotification object:nil]; + XCTAssertFalse([GDTStorage sharedInstance].runningInBackground); + XCTAssertFalse([GDTUploadCoordinator sharedInstance].runningInBackground); + GDTWaitForBlock( + ^BOOL { + return [GDTStorage sharedInstance].storedEvents.count > 0; + }, + 5.0); +} + +/** Tests that the library gracefully stops doing stuff when terminating. */ +- (void)testTermination { + GDTTransport *transport = [[GDTTransport alloc] initWithMappingID:@"test" + transformers:nil + target:kGDTTargetTest]; + GDTEvent *event = [transport eventForTransport]; + event.dataObject = [[GDTLifecycleTestEvent alloc] init]; + XCTAssertEqual([GDTStorage sharedInstance].storedEvents.count, 0); + [transport sendDataEvent:event]; + GDTWaitForBlock( + ^BOOL { + return [GDTStorage sharedInstance].storedEvents.count > 0; + }, + 5.0); + + NSNotificationCenter *notifCenter = [NSNotificationCenter defaultCenter]; + [notifCenter postNotificationName:UIApplicationWillTerminateNotification object:nil]; + GDTWaitForBlock( + ^BOOL { + NSFileManager *fm = [NSFileManager defaultManager]; + return [fm fileExistsAtPath:[GDTStorage archivePath] isDirectory:NULL] && + [fm fileExistsAtPath:[GDTUploadCoordinator archivePath] isDirectory:NULL]; + }, + 5.0); +} + +@end diff --git a/GoogleDataTransport/Tests/Lifecycle/Helpers/GDTLifecycleTestPrioritizer.h b/GoogleDataTransport/Tests/Lifecycle/Helpers/GDTLifecycleTestPrioritizer.h new file mode 100644 index 00000000000..48c6716a211 --- /dev/null +++ b/GoogleDataTransport/Tests/Lifecycle/Helpers/GDTLifecycleTestPrioritizer.h @@ -0,0 +1,24 @@ +/* + * 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 + +/** An integration test prioritization class. */ +@interface GDTLifecycleTestPrioritizer : NSObject + +@end diff --git a/GoogleDataTransport/Tests/Lifecycle/Helpers/GDTLifecycleTestPrioritizer.m b/GoogleDataTransport/Tests/Lifecycle/Helpers/GDTLifecycleTestPrioritizer.m new file mode 100644 index 00000000000..5657da8d98d --- /dev/null +++ b/GoogleDataTransport/Tests/Lifecycle/Helpers/GDTLifecycleTestPrioritizer.m @@ -0,0 +1,72 @@ +/* + * 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 "GDTLifecycleTestPrioritizer.h" + +@interface GDTLifecycleTestPrioritizer () + +/** Events that are only supposed to be uploaded whilst on wifi. */ +@property(nonatomic) NSMutableSet *events; + +/** The queue on which this prioritizer operates. */ +@property(nonatomic) dispatch_queue_t queue; + +@end + +@implementation GDTLifecycleTestPrioritizer + +- (instancetype)init { + self = [super init]; + if (self) { + _queue = dispatch_queue_create("com.google.GDTLifecycleTestPrioritizer", DISPATCH_QUEUE_SERIAL); + _events = [[NSMutableSet alloc] init]; + [[GDTRegistrar sharedInstance] registerPrioritizer:self target:kGDTTargetTest]; + } + return self; +} + +- (void)prioritizeEvent:(GDTStoredEvent *)event { + dispatch_async(_queue, ^{ + [self.events addObject:event]; + }); +} + +- (void)unprioritizeEvents:(NSSet *)events { + dispatch_async(_queue, ^{ + for (GDTStoredEvent *event in events) { + [self.events removeObject:event]; + } + }); +} + +- (GDTUploadPackage *)uploadPackageWithConditions:(GDTUploadConditions)conditions { + __block GDTUploadPackage *uploadPackage = [[GDTUploadPackage alloc] init]; + dispatch_sync(_queue, ^{ + uploadPackage.events = self.events; + }); + return uploadPackage; +} + +- (void)appWillBackground:(UIApplication *)app { +} + +- (void)appWillForeground:(UIApplication *)app { +} + +- (void)appWillTerminate:(UIApplication *)application { +} + +@end diff --git a/GoogleDataTransport/Tests/Lifecycle/Helpers/GDTLifecycleTestUploader.h b/GoogleDataTransport/Tests/Lifecycle/Helpers/GDTLifecycleTestUploader.h new file mode 100644 index 00000000000..3369e7a9b1d --- /dev/null +++ b/GoogleDataTransport/Tests/Lifecycle/Helpers/GDTLifecycleTestUploader.h @@ -0,0 +1,26 @@ +/* + * 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 + +#import "GDTLifecycleTestUploader.h" + +/** An integration test uploader. */ +@interface GDTLifecycleTestUploader : NSObject + +@end diff --git a/GoogleDataTransport/Tests/Lifecycle/Helpers/GDTLifecycleTestUploader.m b/GoogleDataTransport/Tests/Lifecycle/Helpers/GDTLifecycleTestUploader.m new file mode 100644 index 00000000000..22bd05b976a --- /dev/null +++ b/GoogleDataTransport/Tests/Lifecycle/Helpers/GDTLifecycleTestUploader.m @@ -0,0 +1,34 @@ +/* + * 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 "GDTLifecycleTestUploader.h" + +@implementation GDTLifecycleTestUploader + +- (void)uploadPackage:(GDTUploadPackage *)package + onComplete:(GDTUploaderCompletionBlock)onComplete { +} + +- (void)appWillBackground:(UIApplication *)app { +} + +- (void)appWillForeground:(UIApplication *)app { +} + +- (void)appWillTerminate:(UIApplication *)application { +} + +@end diff --git a/GoogleDataTransport/Tests/Unit/Helpers/GDTTestPrioritizer.m b/GoogleDataTransport/Tests/Unit/Helpers/GDTTestPrioritizer.m index 28ea28f48a8..71690fff6fd 100644 --- a/GoogleDataTransport/Tests/Unit/Helpers/GDTTestPrioritizer.m +++ b/GoogleDataTransport/Tests/Unit/Helpers/GDTTestPrioritizer.m @@ -44,4 +44,13 @@ - (void)prioritizeEvent:(GDTStoredEvent *)event { - (void)unprioritizeEvents:(NSSet *)events { } +- (void)appWillBackground:(UIApplication *)app { +} + +- (void)appWillForeground:(UIApplication *)app { +} + +- (void)appWillTerminate:(UIApplication *)application { +} + @end diff --git a/GoogleDataTransport/Tests/Unit/Helpers/GDTTestUploader.m b/GoogleDataTransport/Tests/Unit/Helpers/GDTTestUploader.m index dcc90ed1d5c..115b5e9f4ca 100644 --- a/GoogleDataTransport/Tests/Unit/Helpers/GDTTestUploader.m +++ b/GoogleDataTransport/Tests/Unit/Helpers/GDTTestUploader.m @@ -27,4 +27,13 @@ - (void)uploadPackage:(GDTUploadPackage *)package } } +- (void)appWillBackground:(nonnull UIApplication *)app { +} + +- (void)appWillForeground:(nonnull UIApplication *)app { +} + +- (void)appWillTerminate:(UIApplication *)application { +} + @end