diff --git a/app/LoadedApp/LoadedApp.tsx b/app/LoadedApp/LoadedApp.tsx index 9707effca..8a9617cb8 100644 --- a/app/LoadedApp/LoadedApp.tsx +++ b/app/LoadedApp/LoadedApp.tsx @@ -298,6 +298,8 @@ export class LoadedAppClass extends Component { if (this.state.appState.match(/inactive|background/) && nextAppState === 'active') { //console.log('App has come to the foreground!'); + // deactivate the interruption sync flag + await RPC.rpc_setInterruptSyncAfterBatch('false'); // reading background task info if (Platform.OS === 'ios') { // this file only exists in IOS BS. diff --git a/components/SyncReport/SyncReport.tsx b/components/SyncReport/SyncReport.tsx index 8cde557fa..99b3e3ec8 100644 --- a/components/SyncReport/SyncReport.tsx +++ b/components/SyncReport/SyncReport.tsx @@ -49,7 +49,7 @@ const SyncReport: React.FunctionComponent = ({ closeModal }) => useEffect(() => { (async () => await RPC.rpc_setInterruptSyncAfterBatch('false'))(); - setTimeout(() => setShowBackgroundLegend(false), 5000); // 5 seconds only + setTimeout(() => setShowBackgroundLegend(false), 10000); // 10 seconds only }, []); // ref: https://github.com/zingolabs/zingo-mobile/issues/327 @@ -219,7 +219,7 @@ const SyncReport: React.FunctionComponent = ({ closeModal }) => /> )} - {background.batches > 0 && background.date > 0 && showBackgroundLegend && ( + {background.date > 0 && showBackgroundLegend && ( = ({ closeModal }) => diff --git a/ios/ZingoMobile.xcodeproj/project.pbxproj b/ios/ZingoMobile.xcodeproj/project.pbxproj index 3c53c5f3a..dd3eb9c99 100644 --- a/ios/ZingoMobile.xcodeproj/project.pbxproj +++ b/ios/ZingoMobile.xcodeproj/project.pbxproj @@ -12,6 +12,9 @@ 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 1F1CE02628C13DBD00A954D7 /* librustios.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DB4DE513264B0EBA0084F17D /* librustios.a */; }; + 1F7D321D2B71D44800D2879C /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F7D321C2B71D44800D2879C /* SystemConfiguration.framework */; }; + 1F7D32272B73DF3800D2879C /* Reachability.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F7D32262B73DF3800D2879C /* Reachability.m */; }; + 1F7D32282B73DF3800D2879C /* Reachability.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F7D32262B73DF3800D2879C /* Reachability.m */; }; 1FE8E9AC296B85FC004A256B /* BackgroundTasks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1FE8E9AB296B85FC004A256B /* BackgroundTasks.framework */; }; 5F9FEE8650525C4AF43859AB /* libPods-ZingoMobile.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1794449599A63970811D5CF0 /* libPods-ZingoMobile.a */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; @@ -45,6 +48,9 @@ 1F26E03C28C0E45B009737D7 /* librustios.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = librustios.a; path = "../rust/target/aarch64-apple-ios-sim/release/librustios.a"; sourceTree = ""; }; 1F70326429905E6A001D70A2 /* ZingoMobile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = ZingoMobile.entitlements; path = ZingoMobile/ZingoMobile.entitlements; sourceTree = ""; }; 1F7638CA28BD7C3E0017F9B6 /* librustios.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = librustios.a; path = ../rust/target/universal/release/librustios.a; sourceTree = ""; }; + 1F7D321C2B71D44800D2879C /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; + 1F7D32252B73DF1600D2879C /* Reachability.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Reachability.h; path = ZingoMobile/Reachability.h; sourceTree = ""; }; + 1F7D32262B73DF3800D2879C /* Reachability.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Reachability.m; path = ZingoMobile/Reachability.m; sourceTree = ""; }; 1FE8E9AB296B85FC004A256B /* BackgroundTasks.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = BackgroundTasks.framework; path = System/Library/Frameworks/BackgroundTasks.framework; sourceTree = SDKROOT; }; 57C30AB6DAB41A2ED2699835 /* libPods-ZingoMobile-ZingoMobileTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ZingoMobile-ZingoMobileTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 65B24C2E2D48C83D6B5DDF6E /* Pods-ZingoMobile-ZingoMobileTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ZingoMobile-ZingoMobileTests.debug.xcconfig"; path = "Target Support Files/Pods-ZingoMobile-ZingoMobileTests/Pods-ZingoMobile-ZingoMobileTests.debug.xcconfig"; sourceTree = ""; }; @@ -72,6 +78,7 @@ files = ( 1FE8E9AC296B85FC004A256B /* BackgroundTasks.framework in Frameworks */, 5F9FEE8650525C4AF43859AB /* libPods-ZingoMobile.a in Frameworks */, + 1F7D321D2B71D44800D2879C /* SystemConfiguration.framework in Frameworks */, 1F1CE02628C13DBD00A954D7 /* librustios.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -99,6 +106,8 @@ 13B07FAE1A68108700A75B9A /* ZingoMobile */ = { isa = PBXGroup; children = ( + 1F7D32262B73DF3800D2879C /* Reachability.m */, + 1F7D32252B73DF1600D2879C /* Reachability.h */, 1F70326429905E6A001D70A2 /* ZingoMobile.entitlements */, DB4DE516264B103E0084F17D /* RPCModule.h */, DB4DE517264B103E0084F17D /* RPCModule.m */, @@ -115,6 +124,7 @@ 2D16E6871FA4F8E400B85C8A /* Frameworks */ = { isa = PBXGroup; children = ( + 1F7D321C2B71D44800D2879C /* SystemConfiguration.framework */, 1FE8E9AB296B85FC004A256B /* BackgroundTasks.framework */, DB4DE513264B0EBA0084F17D /* librustios.a */, 1F7638CA28BD7C3E0017F9B6 /* librustios.a */, @@ -423,6 +433,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1F7D32282B73DF3800D2879C /* Reachability.m in Sources */, 00E356F31AD99517003FC87E /* ZingoMobileTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -433,6 +444,7 @@ files = ( 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */, DB4DE518264B103E0084F17D /* RPCModule.m in Sources */, + 1F7D32272B73DF3800D2879C /* Reachability.m in Sources */, 13B07FC11A68108700A75B9A /* main.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ios/ZingoMobile/AppDelegate.m b/ios/ZingoMobile/AppDelegate.m index f53854398..3b0591408 100644 --- a/ios/ZingoMobile/AppDelegate.m +++ b/ios/ZingoMobile/AppDelegate.m @@ -8,6 +8,7 @@ #import #import "RPCModule.h" #import "rust.h" +#import "reachability.h" #ifdef FB_SONARKIT_ENABLED #import @@ -29,17 +30,11 @@ static void InitializeFlipper(UIApplication *application) { #endif @implementation AppDelegate -static BOOL _syncFinished = true; - -+ (BOOL)syncFinished { - return _syncFinished; -} - -+ (void)setSyncFinished:(BOOL)newSyncFinished { - _syncFinished = newSyncFinished; -} static NSString* syncTask = @"Zingo_Processing_Task_ID"; +static NSString* syncSchedulerTask = @"Zingo_Processing_Scheduler_Task_ID"; +static BOOL isConnectedToWifi = false; +static BGProcessingTask *bgTask = nil; - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { @@ -49,7 +44,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge - moduleName:@"Zingo!" + moduleName:@"Zingo!" initialProperties:nil]; rootView.backgroundColor = [[UIColor alloc] initWithRed:0.0f green:0.0f blue:0.0f alpha:1]; @@ -59,6 +54,12 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( rootViewController.view = rootView; self.window.rootViewController = rootViewController; [self.window makeKeyAndVisible]; + + if (@available(iOS 13.0, *)) { + NSLog(@"BGTask handleBackgroundTask"); + [self handleBackgroundTask]; + } + return YES; } @@ -86,123 +87,127 @@ - (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull N restorationHandler:restorationHandler]; } -- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions +- (void)applicationWillEnterForeground:(UIApplication *)application { if (@available(iOS 13.0, *)) { - NSLog(@"configureProcessingTask"); - [self configureProcessingTask]; + // cancel existing sync process (if any). + NSLog(@"BGTask foreground"); + [self stopSyncingProcess:nil]; + + // cancel bg task + if ([bgTask isKindOfClass:[BGTask class]]) { + NSLog(@"BGTask foreground - sync task CANCEL"); + [bgTask setTaskCompletedWithSuccess:NO]; + } + bgTask = nil; } - return YES; -} - -- (void)applicationWillEnterForeground:(UIApplication *)application -{ - // cancel existing task (if any) - NSLog(@"scheduleProcessingTask CANCEL - foreground"); - [self setSyncFinished:true]; - [BGTaskScheduler.sharedScheduler cancelTaskRequestWithIdentifier:syncTask]; } - (void)applicationDidEnterBackground:(UIApplication *)application { if (@available(iOS 13.0, *)) { - NSLog(@"scheduleProcessingTask"); - [self scheduleProcessingTask]; - } -} - --(void)configureProcessingTask { - if (@available(iOS 13.0, *)) { - [[BGTaskScheduler sharedScheduler] registerForTaskWithIdentifier:syncTask - usingQueue:nil - launchHandler:^(BGTask *task) { - NSLog(@"configureProcessingTask run"); - [NSThread detachNewThreadSelector:@selector(syncingProcessBackgroundTask:) toTarget:self withObject:nil]; - [self syncingStatusProcessBackgroundTask:nil]; + // cancel existing sync process (if any). + NSLog(@"BGTask background"); + [self stopSyncingProcess:nil]; + + // cancel bg task + if ([bgTask isKindOfClass:[BGTask class]]) { + NSLog(@"BGTask background - sync task CANCEL"); + [bgTask setTaskCompletedWithSuccess:NO]; + } + bgTask = nil; - }]; - } else { - // No fallback - } + NSLog(@"BGTask background - scheduleBackgroundTask"); + [self scheduleBackgroundTask]; + NSLog(@"BGTask background - scheduleSchedulerBackgroundTask"); + [self scheduleSchedulerBackgroundTask]; + } } --(void)syncingProcessBackgroundTask:(NSString *)noValue { - //do things with task +-(void)stopSyncingProcess:(NSString *)noValue { + // if the in_progress from syncstatus is true + // there is a sync progress running somehow + // we need to stop it and the only tool I have is + // using the `interrupting` sync flag... + // and waiting for the end. @autoreleasepool { - BOOL exists = [self wallet__exists]; - - if (exists) { + NSLog(@"BGTask stopSyncingProcess"); + char *resp = execute("syncstatus", ""); + NSString* respStr = [NSString stringWithUTF8String:resp]; + rust_free(resp); + NSLog(@"BGTask stopSyncingProcess - status response %@", respStr); - NSLog(@"handleProcessingTask sync begin"); - [self setSyncFinished:false]; + NSData *data = [respStr dataUsingEncoding:NSUTF8StringEncoding]; + id jsonResp = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + NSString *inProgressStr = [jsonResp valueForKey:@"in_progress"]; + BOOL inProgress = [inProgressStr boolValue]; - char *resp2 = execute("sync", ""); + while(inProgress) { + char *resp2 = execute("interrupt_sync_after_batch", "true"); NSString* respStr2 = [NSString stringWithUTF8String:resp2]; - rust_free(resp2); - - NSLog(@"handleProcessingTask sync end %@", respStr2); - - [self setSyncFinished:true]; - - // the execute `sync` already save the wallet when finished + NSLog(@"BGTask stopSyncingProcess - interrupt syncing %@", respStr2); - } else { + [NSThread sleepForTimeInterval: 0.5]; - [self setSyncFinished:true]; - NSLog(@"handleProcessingTask No exists wallet"); + char *resp = execute("syncstatus", ""); + NSString* respStr = [NSString stringWithUTF8String:resp]; + rust_free(resp); + NSLog(@"BGTask stopSyncingProcess - status response %@", respStr); + NSData *data = [respStr dataUsingEncoding:NSUTF8StringEncoding]; + id jsonResp = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + NSString *inProgressStr = [jsonResp valueForKey:@"in_progress"]; + inProgress = [inProgressStr boolValue]; } + NSLog(@"BGTask stopSyncingProcess - syncing process STOPPED"); + } } --(void)syncingStatusProcessBackgroundTask:(NSString *)noValue { +-(void)syncingProcessBackgroundTask:(NSString *)noValue { + //do things with task @autoreleasepool { + + NSLog(@"BGTask syncingProcessBackgroundTask"); + BOOL exists = [self wallet__exists]; - NSLog(@"handleProcessingTask sync status begin %i", self.syncFinished); - NSInteger prevBatch = -1; + if (exists) { + // stop syncing first, just in case. + [self stopSyncingProcess:nil]; - while(!self.syncFinished) { - [NSThread sleepForTimeInterval: 2.0]; - char *resp = execute("syncstatus", ""); + // we need to sync without interruption, I run this just in case + char *resp = execute("interrupt_sync_after_batch", "false"); NSString* respStr = [NSString stringWithUTF8String:resp]; + NSLog(@"BGTask syncingProcessBackgroundTask - no interrupt syncing %@", respStr); rust_free(resp); - NSLog(@"handleProcessingTask sync status response %i %@", self.syncFinished, respStr); - NSData *data = [respStr dataUsingEncoding:NSUTF8StringEncoding]; - id jsonResp = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; - NSString *batchStr = [jsonResp valueForKey:@"batch_num"]; - NSInteger batch = [batchStr integerValue]; - BOOL progress = [jsonResp valueForKey:@"in_progress"]; + // the task is running here blocking this execution until this process finished: + // 1. finished the syncing. + // 2. interrupted by a flag then it finished the current batch. - NSLog(@"handleProcessingTask batch number %i %@", self.syncFinished, batchStr); + NSLog(@"BGTask syncingProcessBackgroundTask - sync BEGIN"); - if (prevBatch != -1 && batch > 0 && prevBatch != batch) { - // save the wallet - RPCModule *rpcmodule = [RPCModule new]; - [rpcmodule saveWalletInternal]; + char *resp2 = execute("sync", ""); + NSString* respStr2 = [NSString stringWithUTF8String:resp2]; + rust_free(resp2); - // save info in background json - NSTimeInterval timeStamp = [[NSDate date] timeIntervalSince1970]; - // NSTimeInterval is defined as double - NSNumber *timeStampObj = [NSNumber numberWithDouble: timeStamp]; - NSString *timeStampStr = [timeStampObj stringValue]; - NSString *jsonBackgroud = [NSString stringWithFormat: @"%@%@%@%@%@", @"{\"batches\": \"", batchStr, @"\", \"date\": \"", timeStampStr, @"\"}"]; - [rpcmodule saveBackgroundFile:jsonBackgroud]; + NSLog(@"BGTask syncingProcessBackgroundTask - sync END %@", respStr2); - NSLog(@"handleProcessingTask save wallet & background batch %i %@ %i %@", self.syncFinished, batchStr, progress, timeStampStr); - } - prevBatch = batch; - } + } else { + + NSLog(@"BGTask syncingProcessBackgroundTask - No exists wallet file END"); - // we don't want to save if the sync is finished: - // 1. OS kill the task -> to save is dangerous. - // 2. When the App go to foreground -> same. - // 3. If sync is finished -> the wallet is already saved. + } } + + NSLog(@"BGTask syncingProcessBackgroundTask - syncing task STOPPED"); + [bgTask setTaskCompletedWithSuccess:YES]; + bgTask = nil; + } -(BOOL)wallet__exists { @@ -214,7 +219,6 @@ -(BOOL)wallet__exists { NSString *fileName = [NSString stringWithFormat:@"%@/wallet.dat.txt", documentsDirectory]; BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:fileName]; - // RCTLogInfo(@"Wallet exists: %d", (int)fileExists); if (fileExists) { return true; @@ -223,25 +227,180 @@ -(BOOL)wallet__exists { } } --(void)scheduleProcessingTask { - if (@available(iOS 13.0, *)) { - NSLog(@"schedulingProcessingTask"); - NSError *error = NULL; - // cancel existing task (if any) - [BGTaskScheduler.sharedScheduler cancelTaskRequestWithIdentifier:syncTask]; - // new task - BGProcessingTaskRequest *request = [[BGProcessingTaskRequest alloc] initWithIdentifier:syncTask]; - request.requiresNetworkConnectivity = YES; - request.earliestBeginDate = nil; - BOOL success = [[BGTaskScheduler sharedScheduler] submitTaskRequest:request error:&error]; - if (!success) { - // Errorcodes https://stackoverflow.com/a/58224050/872051 - NSLog(@"Failed to submit request: %@", error); - } else { - NSLog(@"Success submit request %@", request); +// NEW BACKGROUND SCHEDULING TASKS + +- (void)handleBackgroundTask { + // We require the background task to run when connected to the power and wifi + Reachability *reachability = [Reachability reachabilityForInternetConnection]; + NetworkStatus networkStatus = [reachability currentReachabilityStatus]; + + if (networkStatus == ReachableViaWiFi) { + isConnectedToWifi = true; + } else { + isConnectedToWifi = false; + } + NSLog(@"BGTask isConnectedToWifi %@", isConnectedToWifi ? @"true" : @"false"); + + [self registerTasks]; +} + +- (void)registerTasks { + BOOL bcgSyncTaskResult; + bcgSyncTaskResult = [[BGTaskScheduler sharedScheduler] registerForTaskWithIdentifier:syncTask usingQueue:dispatch_get_main_queue() + launchHandler:^(BGTask *task) { + if (![task isKindOfClass:[BGTask class]]) { + return; + } + NSLog(@"BGTask BGTaskScheduler.shared.register SYNC called"); + bgTask = (BGProcessingTask *)task; + [self startBackgroundTask:nil]; + }]; + + NSLog(@"BGTask SYNC registered %d", bcgSyncTaskResult); + + BOOL bcgSchedulerTaskResult; + bcgSchedulerTaskResult = [[BGTaskScheduler sharedScheduler] registerForTaskWithIdentifier:syncSchedulerTask usingQueue:dispatch_get_main_queue() + launchHandler:^(BGTask *task) { + if (![task isKindOfClass:[BGTask class]]) { + return; } + NSLog(@"BGTask BGTaskScheduler.shared.register SCHEDULER called"); + [self scheduleSchedulerBackgroundTask]; + [self scheduleBackgroundTask]; + [task setTaskCompletedWithSuccess:YES]; + }]; + + NSLog(@"BGTask SCHEDULER registered %d", bcgSchedulerTaskResult); +} + +- (void)startBackgroundTask:(NSString *)noValue { + NSLog(@"BGTask startBackgroundTask called"); + + // Schedule tasks for the next time + [self scheduleBackgroundTask]; + [self scheduleSchedulerBackgroundTask]; + + if (!isConnectedToWifi) { + NSLog(@"BGTask startBackgroundTask: not connected to the wifi"); + [bgTask setTaskCompletedWithSuccess:NO]; + bgTask = nil; + return; + } + + // I can check the time remaining here & make a choice + // when this `time remaing` is cracy big, something wrong is happening. + // in my testing this time is always something like 300 seconds. (the famous 5 min). + NSLog(@"BEFORE RUN TASKS - Time Remaining: %f", [[UIApplication sharedApplication] backgroundTimeRemaining]); + if ([[UIApplication sharedApplication] backgroundTimeRemaining] > 1000000000) { + NSLog(@"BGTask startBackgroundTask: time remainig TOO cracy high %f", [[UIApplication sharedApplication] backgroundTimeRemaining]); + [bgTask setTaskCompletedWithSuccess:NO]; + bgTask = nil; + return; + } + + // Start the syncing + NSLog(@"BGTask startBackgroundTask run sync task"); + // in order to run only one task + dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ + [self syncingProcessBackgroundTask:nil]; + }); + + bgTask.expirationHandler = ^{ + NSLog(@"BGTask startBackgroundTask - expirationHandler called"); + // interrupting the sync process, I can't wait to see if the process is over + // because I have no time enough to run all of this task here. + char *resp2 = execute("interrupt_sync_after_batch", "true"); + NSString* respStr2 = [NSString stringWithUTF8String:resp2]; + NSLog(@"BGTask startBackgroundTask - expirationHandler interrupt syncing %@", respStr2); + + // save the wallet + RPCModule *rpcmodule = [RPCModule new]; + [rpcmodule saveWalletInternal]; + NSLog(@"BGTask startBackgroundTask - expirationHandler Save Wallet"); + + // save info in background json + NSTimeInterval timeStamp = [[NSDate date] timeIntervalSince1970]; + // NSTimeInterval is defined as double + NSNumber *timeStampObj = [NSNumber numberWithDouble: timeStamp]; + NSString *timeStampStr = [timeStampObj stringValue]; + NSString *jsonBackgroud = [NSString stringWithFormat: @"%@%@%@%@%@", @"{\"batches\": \"", @"0", @"\", \"date\": \"", timeStampStr, @"\"}"]; + [rpcmodule saveBackgroundFile:jsonBackgroud]; + NSLog(@"BGTask startBackgroundTask - expirationHandler Save background JSON"); + + [bgTask setTaskCompletedWithSuccess:NO]; + bgTask = nil; + NSLog(@"BGTask startBackgroundTask - expirationHandler THE END"); + }; +} + +- (void)scheduleBackgroundTask { + // This method can be called as many times as needed, the previously submitted + // request will be overridden by the new one. + NSLog(@"BGTask scheduleBackgroundTask called"); + + BGProcessingTaskRequest *request = [[BGProcessingTaskRequest alloc] initWithIdentifier:syncTask]; + + // PRODUCTION + NSDate *today = [[NSCalendar currentCalendar] startOfDayForDate:[NSDate date]]; + NSDate *tomorrow = [[NSCalendar currentCalendar] dateByAddingUnit:NSCalendarUnitDay value:1 toDate:today options:0]; + + NSDateComponents *earlyMorningComponent = [[NSDateComponents alloc] init]; + earlyMorningComponent.hour = 3; + earlyMorningComponent.minute = arc4random_uniform(61); + NSDate *earlyMorning = [[NSCalendar currentCalendar] dateByAddingComponents:earlyMorningComponent toDate:tomorrow options:0]; + + // DEVELOPMENT + //NSDate *now = [NSDate date]; + + //NSDate *twoMinutesLater = [now dateByAddingTimeInterval:120]; // 2 minutes = 120 seconds + + request.earliestBeginDate = earlyMorning; + //request.earliestBeginDate = twoMinutesLater; + request.requiresExternalPower = YES; + request.requiresNetworkConnectivity = YES; + + NSError *error = nil; + [[BGTaskScheduler sharedScheduler] submitTaskRequest:request error:&error]; + if (error) { + NSLog(@"BGTask scheduleBackgroundTask failed to submit, error: %@", error); } else { - // No fallback + NSLog(@"BGTask scheduleBackgroundTask succeeded to submit"); } } + +- (void)scheduleSchedulerBackgroundTask { + // This method can be called as many times as needed, the previously submitted + // request will be overridden by the new one. + NSLog(@"BGTask scheduleSchedulerBackgroundTask called"); + + BGProcessingTaskRequest *request = [[BGProcessingTaskRequest alloc] initWithIdentifier:syncSchedulerTask]; + + // PRODUCTION + NSDate *today = [[NSCalendar currentCalendar] startOfDayForDate:[NSDate date]]; + NSDate *tomorrow = [[NSCalendar currentCalendar] dateByAddingUnit:NSCalendarUnitDay value:1 toDate:today options:0]; + + NSDateComponents *afternoonComponent = [[NSDateComponents alloc] init]; + afternoonComponent.hour = 14; + afternoonComponent.minute = arc4random_uniform(61); + NSDate *afternoon = [[NSCalendar currentCalendar] dateByAddingComponents:afternoonComponent toDate:tomorrow options:0]; + + // DEVELOPMENT + //NSDate *now = [NSDate date]; + + //NSDate *fiveMinutesLater = [now dateByAddingTimeInterval:300]; // 5 minutes = 300 seconds + + request.earliestBeginDate = afternoon; + //request.earliestBeginDate = fiveMinutesLater; + request.requiresExternalPower = NO; + request.requiresNetworkConnectivity = NO; + + NSError *error = nil; + [[BGTaskScheduler sharedScheduler] submitTaskRequest:request error:&error]; + if (error) { + NSLog(@"BGTask scheduleSchedulerBackgroundTask failed to submit, error: %@", error); + } else { + NSLog(@"BGTask scheduleSchedulerBackgroundTask succeeded to submit"); + } +} + @end diff --git a/ios/ZingoMobile/Info.plist b/ios/ZingoMobile/Info.plist index 014ac1986..575b627f5 100644 --- a/ios/ZingoMobile/Info.plist +++ b/ios/ZingoMobile/Info.plist @@ -7,6 +7,7 @@ com.transistorsoft.fetch Zingo_Refresh_Task_ID Zingo_Processing_Task_ID + Zingo_Processing_Scheduler_Task_ID CFBundleDevelopmentRegion en diff --git a/ios/ZingoMobile/Reachability.h b/ios/ZingoMobile/Reachability.h new file mode 100644 index 000000000..73883c35b --- /dev/null +++ b/ios/ZingoMobile/Reachability.h @@ -0,0 +1,103 @@ +/* + Copyright (c) 2011, Tony Million. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + */ + +#import +#import + +//! Project version number for MacOSReachability. +FOUNDATION_EXPORT double ReachabilityVersionNumber; + +//! Project version string for MacOSReachability. +FOUNDATION_EXPORT const unsigned char ReachabilityVersionString[]; + +/** + * Create NS_ENUM macro if it does not exist on the targeted version of iOS or OS X. + * + * @see http://nshipster.com/ns_enum-ns_options/ + **/ +#ifndef NS_ENUM +#define NS_ENUM(_type, _name) enum _name : _type _name; enum _name : _type +#endif + +extern NSString *const kReachabilityChangedNotification; + +typedef NS_ENUM(NSInteger, NetworkStatus) { + // Apple NetworkStatus Compatible Names. + NotReachable = 0, + ReachableViaWiFi = 2, + ReachableViaWWAN = 1 +}; + +@class Reachability; + +typedef void (^NetworkReachable)(Reachability * reachability); +typedef void (^NetworkUnreachable)(Reachability * reachability); +typedef void (^NetworkReachability)(Reachability * reachability, SCNetworkConnectionFlags flags); + + +@interface Reachability : NSObject + +@property (nonatomic, copy) NetworkReachable reachableBlock; +@property (nonatomic, copy) NetworkUnreachable unreachableBlock; +@property (nonatomic, copy) NetworkReachability reachabilityBlock; + +@property (nonatomic, assign) BOOL reachableOnWWAN; + + ++(instancetype)reachabilityWithHostname:(NSString*)hostname; +// This is identical to the function above, but is here to maintain +//compatibility with Apples original code. (see .m) ++(instancetype)reachabilityWithHostName:(NSString*)hostname; ++(instancetype)reachabilityForInternetConnection; ++(instancetype)reachabilityWithAddress:(void *)hostAddress; ++(instancetype)reachabilityForLocalWiFi; ++(instancetype)reachabilityWithURL:(NSURL*)url; + +-(instancetype)initWithReachabilityRef:(SCNetworkReachabilityRef)ref; + +-(BOOL)startNotifier; +-(void)stopNotifier; + +-(BOOL)isReachable; +-(BOOL)isReachableViaWWAN; +-(BOOL)isReachableViaWiFi; + +// WWAN may be available, but not active until a connection has been established. +// WiFi may require a connection for VPN on Demand. +-(BOOL)isConnectionRequired; // Identical DDG variant. +-(BOOL)connectionRequired; // Apple's routine. +// Dynamic, on demand connection? +-(BOOL)isConnectionOnDemand; +// Is user intervention required? +-(BOOL)isInterventionRequired; + +-(NetworkStatus)currentReachabilityStatus; +-(SCNetworkReachabilityFlags)reachabilityFlags; +-(NSString*)currentReachabilityString; +-(NSString*)currentReachabilityFlags; + +@end diff --git a/ios/ZingoMobile/Reachability.m b/ios/ZingoMobile/Reachability.m new file mode 100644 index 000000000..5324390d9 --- /dev/null +++ b/ios/ZingoMobile/Reachability.m @@ -0,0 +1,508 @@ +/* + Copyright (c) 2011, Tony Million. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + */ + +#import "Reachability.h" + +#import +#import +#import +#import +#import +#import + + +NSString *const kReachabilityChangedNotification = @"kReachabilityChangedNotification"; + + +@interface Reachability () + +@property (nonatomic, assign) SCNetworkReachabilityRef reachabilityRef; +@property (nonatomic, strong) dispatch_queue_t reachabilitySerialQueue; +@property (nonatomic, strong) id reachabilityObject; + +-(void)reachabilityChanged:(SCNetworkReachabilityFlags)flags; +-(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags; + +@end + + +static NSString *reachabilityFlags(SCNetworkReachabilityFlags flags) +{ + return [NSString stringWithFormat:@"%c%c %c%c%c%c%c%c%c", +#if TARGET_OS_IPHONE + (flags & kSCNetworkReachabilityFlagsIsWWAN) ? 'W' : '-', +#else + 'X', +#endif + (flags & kSCNetworkReachabilityFlagsReachable) ? 'R' : '-', + (flags & kSCNetworkReachabilityFlagsConnectionRequired) ? 'c' : '-', + (flags & kSCNetworkReachabilityFlagsTransientConnection) ? 't' : '-', + (flags & kSCNetworkReachabilityFlagsInterventionRequired) ? 'i' : '-', + (flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) ? 'C' : '-', + (flags & kSCNetworkReachabilityFlagsConnectionOnDemand) ? 'D' : '-', + (flags & kSCNetworkReachabilityFlagsIsLocalAddress) ? 'l' : '-', + (flags & kSCNetworkReachabilityFlagsIsDirect) ? 'd' : '-']; +} + +// Start listening for reachability notifications on the current run loop +static void TMReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void* info) +{ +#pragma unused (target) + + Reachability *reachability = ((__bridge Reachability*)info); + + // We probably don't need an autoreleasepool here, as GCD docs state each queue has its own autorelease pool, + // but what the heck eh? + @autoreleasepool + { + [reachability reachabilityChanged:flags]; + } +} + + +@implementation Reachability + +#pragma mark - Class Constructor Methods + ++(instancetype)reachabilityWithHostName:(NSString*)hostname +{ + return [Reachability reachabilityWithHostname:hostname]; +} + ++(instancetype)reachabilityWithHostname:(NSString*)hostname +{ + SCNetworkReachabilityRef ref = SCNetworkReachabilityCreateWithName(NULL, [hostname UTF8String]); + if (ref) + { + id reachability = [[self alloc] initWithReachabilityRef:ref]; + + return reachability; + } + + return nil; +} + ++(instancetype)reachabilityWithAddress:(void *)hostAddress +{ + SCNetworkReachabilityRef ref = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (const struct sockaddr*)hostAddress); + if (ref) + { + id reachability = [[self alloc] initWithReachabilityRef:ref]; + + return reachability; + } + + return nil; +} + ++(instancetype)reachabilityForInternetConnection +{ + struct sockaddr_in zeroAddress; + bzero(&zeroAddress, sizeof(zeroAddress)); + zeroAddress.sin_len = sizeof(zeroAddress); + zeroAddress.sin_family = AF_INET; + + return [self reachabilityWithAddress:&zeroAddress]; +} + ++(instancetype)reachabilityForLocalWiFi +{ + struct sockaddr_in localWifiAddress; + bzero(&localWifiAddress, sizeof(localWifiAddress)); + localWifiAddress.sin_len = sizeof(localWifiAddress); + localWifiAddress.sin_family = AF_INET; + // IN_LINKLOCALNETNUM is defined in as 169.254.0.0 + localWifiAddress.sin_addr.s_addr = htonl(IN_LINKLOCALNETNUM); + + return [self reachabilityWithAddress:&localWifiAddress]; +} + ++(instancetype)reachabilityWithURL:(NSURL*)url +{ + id reachability; + + NSString *host = url.host; + BOOL isIpAddress = [self isIpAddress:host]; + + if (isIpAddress) + { + NSNumber *port = url.port ?: [url.scheme isEqualToString:@"https"] ? @(443) : @(80); + + struct sockaddr_in address; + address.sin_len = sizeof(address); + address.sin_family = AF_INET; + address.sin_port = htons([port intValue]); + address.sin_addr.s_addr = inet_addr([host UTF8String]); + + reachability = [self reachabilityWithAddress:&address]; + } + else + { + reachability = [self reachabilityWithHostname:host]; + } + + return reachability; +} + ++(BOOL)isIpAddress:(NSString*)host +{ + struct in_addr pin; + return 1 == inet_aton([host UTF8String], &pin); +} + + +// Initialization methods + +-(instancetype)initWithReachabilityRef:(SCNetworkReachabilityRef)ref +{ + self = [super init]; + if (self != nil) + { + self.reachableOnWWAN = YES; + self.reachabilityRef = ref; + + // We need to create a serial queue. + // We allocate this once for the lifetime of the notifier. + + self.reachabilitySerialQueue = dispatch_queue_create("com.tonymillion.reachability", NULL); + } + + return self; +} + +-(void)dealloc +{ + [self stopNotifier]; + + if(self.reachabilityRef) + { + CFRelease(self.reachabilityRef); + self.reachabilityRef = nil; + } + + self.reachableBlock = nil; + self.unreachableBlock = nil; + self.reachabilityBlock = nil; + self.reachabilitySerialQueue = nil; +} + +#pragma mark - Notifier Methods + +// Notifier +// NOTE: This uses GCD to trigger the blocks - they *WILL NOT* be called on THE MAIN THREAD +// - In other words DO NOT DO ANY UI UPDATES IN THE BLOCKS. +// INSTEAD USE dispatch_async(dispatch_get_main_queue(), ^{UISTUFF}) (or dispatch_sync if you want) + +-(BOOL)startNotifier +{ + // allow start notifier to be called multiple times + if(self.reachabilityObject && (self.reachabilityObject == self)) + { + return YES; + } + + + SCNetworkReachabilityContext context = { 0, NULL, NULL, NULL, NULL }; + context.info = (__bridge void *)self; + + if(SCNetworkReachabilitySetCallback(self.reachabilityRef, TMReachabilityCallback, &context)) + { + // Set it as our reachability queue, which will retain the queue + if(SCNetworkReachabilitySetDispatchQueue(self.reachabilityRef, self.reachabilitySerialQueue)) + { + // this should do a retain on ourself, so as long as we're in notifier mode we shouldn't disappear out from under ourselves + // woah + self.reachabilityObject = self; + return YES; + } + else + { +#ifdef DEBUG + NSLog(@"SCNetworkReachabilitySetDispatchQueue() failed: %s", SCErrorString(SCError())); +#endif + + // UH OH - FAILURE - stop any callbacks! + SCNetworkReachabilitySetCallback(self.reachabilityRef, NULL, NULL); + } + } + else + { +#ifdef DEBUG + NSLog(@"SCNetworkReachabilitySetCallback() failed: %s", SCErrorString(SCError())); +#endif + } + + // if we get here we fail at the internet + self.reachabilityObject = nil; + return NO; +} + +-(void)stopNotifier +{ + // First stop, any callbacks! + SCNetworkReachabilitySetCallback(self.reachabilityRef, NULL, NULL); + + // Unregister target from the GCD serial dispatch queue. + SCNetworkReachabilitySetDispatchQueue(self.reachabilityRef, NULL); + + self.reachabilityObject = nil; +} + +#pragma mark - reachability tests + +// This is for the case where you flick the airplane mode; +// you end up getting something like this: +//Reachability: WR ct----- +//Reachability: -- ------- +//Reachability: WR ct----- +//Reachability: -- ------- +// We treat this as 4 UNREACHABLE triggers - really apple should do better than this + +#define testcase (kSCNetworkReachabilityFlagsConnectionRequired | kSCNetworkReachabilityFlagsTransientConnection) + +-(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags +{ + BOOL connectionUP = YES; + + if(!(flags & kSCNetworkReachabilityFlagsReachable)) + connectionUP = NO; + + if( (flags & testcase) == testcase ) + connectionUP = NO; + +#if TARGET_OS_IPHONE + if(flags & kSCNetworkReachabilityFlagsIsWWAN) + { + // We're on 3G. + if(!self.reachableOnWWAN) + { + // We don't want to connect when on 3G. + connectionUP = NO; + } + } +#endif + + return connectionUP; +} + +-(BOOL)isReachable +{ + SCNetworkReachabilityFlags flags; + + if(!SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) + return NO; + + return [self isReachableWithFlags:flags]; +} + +-(BOOL)isReachableViaWWAN +{ +#if TARGET_OS_IPHONE + + SCNetworkReachabilityFlags flags = 0; + + if(SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) + { + // Check we're REACHABLE + if(flags & kSCNetworkReachabilityFlagsReachable) + { + // Now, check we're on WWAN + if(flags & kSCNetworkReachabilityFlagsIsWWAN) + { + return YES; + } + } + } +#endif + + return NO; +} + +-(BOOL)isReachableViaWiFi +{ + SCNetworkReachabilityFlags flags = 0; + + if(SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) + { + // Check we're reachable + if((flags & kSCNetworkReachabilityFlagsReachable)) + { +#if TARGET_OS_IPHONE + // Check we're NOT on WWAN + if((flags & kSCNetworkReachabilityFlagsIsWWAN)) + { + return NO; + } +#endif + return YES; + } + } + + return NO; +} + + +// WWAN may be available, but not active until a connection has been established. +// WiFi may require a connection for VPN on Demand. +-(BOOL)isConnectionRequired +{ + return [self connectionRequired]; +} + +-(BOOL)connectionRequired +{ + SCNetworkReachabilityFlags flags; + + if(SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) + { + return (flags & kSCNetworkReachabilityFlagsConnectionRequired); + } + + return NO; +} + +// Dynamic, on demand connection? +-(BOOL)isConnectionOnDemand +{ + SCNetworkReachabilityFlags flags; + + if (SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) + { + return ((flags & kSCNetworkReachabilityFlagsConnectionRequired) && + (flags & (kSCNetworkReachabilityFlagsConnectionOnTraffic | kSCNetworkReachabilityFlagsConnectionOnDemand))); + } + + return NO; +} + +// Is user intervention required? +-(BOOL)isInterventionRequired +{ + SCNetworkReachabilityFlags flags; + + if (SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) + { + return ((flags & kSCNetworkReachabilityFlagsConnectionRequired) && + (flags & kSCNetworkReachabilityFlagsInterventionRequired)); + } + + return NO; +} + + +#pragma mark - reachability status stuff + +-(NetworkStatus)currentReachabilityStatus +{ + if([self isReachable]) + { + if([self isReachableViaWiFi]) + return ReachableViaWiFi; + +#if TARGET_OS_IPHONE + return ReachableViaWWAN; +#endif + } + + return NotReachable; +} + +-(SCNetworkReachabilityFlags)reachabilityFlags +{ + SCNetworkReachabilityFlags flags = 0; + + if(SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) + { + return flags; + } + + return 0; +} + +-(NSString*)currentReachabilityString +{ + NetworkStatus temp = [self currentReachabilityStatus]; + + if(temp == ReachableViaWWAN) + { + // Updated for the fact that we have CDMA phones now! + return NSLocalizedString(@"Cellular", @""); + } + if (temp == ReachableViaWiFi) + { + return NSLocalizedString(@"WiFi", @""); + } + + return NSLocalizedString(@"No Connection", @""); +} + +-(NSString*)currentReachabilityFlags +{ + return reachabilityFlags([self reachabilityFlags]); +} + +#pragma mark - Callback function calls this method + +-(void)reachabilityChanged:(SCNetworkReachabilityFlags)flags +{ + if([self isReachableWithFlags:flags]) + { + if(self.reachableBlock) + { + self.reachableBlock(self); + } + } + else + { + if(self.unreachableBlock) + { + self.unreachableBlock(self); + } + } + + if(self.reachabilityBlock) + { + self.reachabilityBlock(self, flags); + } + + // this makes sure the change notification happens on the MAIN THREAD + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:kReachabilityChangedNotification + object:self]; + }); +} + +#pragma mark - Debug Description + +- (NSString *) description +{ + NSString *description = [NSString stringWithFormat:@"<%@: %p (%@)>", + NSStringFromClass([self class]), self, [self currentReachabilityFlags]]; + return description; +} + +@end