From c56a6f293534df5b88b864a0b3bbd80bc8d42a2e Mon Sep 17 00:00:00 2001 From: Jonathan Carter Date: Sun, 15 Nov 2015 12:46:47 -0800 Subject: [PATCH] Restart on resume --- CodePush.h | 8 +- CodePush.ios.js | 5 ++ CodePush.m | 113 +++++++++++++++++++++-------- CodePush.xcodeproj/project.pbxproj | 6 +- RCTConvert+CodePushRestartMode.m | 14 ++++ package-mixins.js | 5 +- 6 files changed, 115 insertions(+), 36 deletions(-) create mode 100644 RCTConvert+CodePushRestartMode.m diff --git a/CodePush.h b/CodePush.h index 1e95da9..390373a 100644 --- a/CodePush.h +++ b/CodePush.h @@ -70,4 +70,10 @@ failCallback:(void (^)(NSError *err))failCallback; + (void)rollbackPackage; -@end \ No newline at end of file +@end + +typedef NS_ENUM(NSInteger, CodePushRestartMode) { + CodePushRestartModeNone, + CodePushRestartModeImmediate, + CodePushRestartModeOnNextResume +}; \ No newline at end of file diff --git a/CodePush.ios.js b/CodePush.ios.js index f20ad56..23d0c4e 100644 --- a/CodePush.ios.js +++ b/CodePush.ios.js @@ -206,6 +206,11 @@ var CodePush = { notifyApplicationReady: NativeCodePush.notifyApplicationReady, setUpTestDependencies: setUpTestDependencies, sync: sync, + RestartMode: { + NONE: NativeCodePush.codePushRestartModeNone, // Don't artificially restart the app. Allow the update to be "picked up" on the next app restart + IMMEDIATE: NativeCodePush.codePushRestartModeImmediate, // Restart the app immediately + ON_NEXT_RESUME: NativeCodePush.codePushRestartModeOnNextResume // Restart the app the next time it is resumed from the background + }, SyncStatus: { UP_TO_DATE: 0, // The running app is up-to-date UPDATE_IGNORED: 1, // The app had an optional update and the end-user chose to ignore it diff --git a/CodePush.m b/CodePush.m index e0d4ec6..3f3f6ba 100644 --- a/CodePush.m +++ b/CodePush.m @@ -1,5 +1,6 @@ #import "RCTBridgeModule.h" #import "RCTEventDispatcher.h" +#import "RCTConvert.h" #import "RCTRootView.h" #import "RCTUtils.h" #import "CodePush.h" @@ -39,7 +40,7 @@ NSString * const PendingUpdateKey = @"CODE_PUSH_PENDING_UPDATE"; NSDictionary *appFileAttribs = [[NSFileManager defaultManager] attributesOfItemAtPath:packageFile error:nil]; NSDate *binaryDate = [binaryFileAttributes objectForKey:NSFileModificationDate]; NSDate *packageDate = [appFileAttribs objectForKey:NSFileModificationDate]; - + if ([binaryDate compare:packageDate] == NSOrderedAscending) { // Return package file because it is newer than the app store binary's JS bundle return [[NSURL alloc] initFileURLWithPath:packageFile]; @@ -56,26 +57,30 @@ NSString * const PendingUpdateKey = @"CODE_PUSH_PENDING_UPDATE"; }); } -- (CodePush *)init -{ - self = [super init]; +- (void)checkForPendingUpdate:(BOOL)isAppStart { + NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults]; + NSDictionary *pendingUpdate = [preferences objectForKey:PendingUpdateKey]; - if (self) { - NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults]; - NSDictionary *pendingUpdate = [preferences objectForKey:PendingUpdateKey]; + if (pendingUpdate) + { + NSError *error; + NSString *pendingHash = pendingUpdate[@"hash"]; + NSString *currentHash = [CodePushPackage getCurrentPackageHash:&error]; - if (pendingUpdate) - { - NSError *error; - NSString *pendingHash = pendingUpdate[@"hash"]; - NSString *currentHash = [CodePushPackage getCurrentPackageHash:&error]; - - // If the current hash is equivalent to the pending hash, then the app - // restart "picked up" the new update, but we need to kick off the - // rollback timer and ensure that the necessary state is setup. - if ([pendingHash isEqualToString:currentHash]) { + // If the current hash is equivalent to the pending hash, then the app + // restart "picked up" the new update, but we need to kick off the + // rollback timer and ensure that the necessary state is setup. + if ([pendingHash isEqualToString:currentHash]) { + // We only want to initialize the rollback timer in two scenarios: + // 1) The app has been restarted, and therefore, the pending update is already applied + // 2) The app has been resumed, and the pending update indicates it supports being restarted on resume + if (isAppStart || (!isAppStart && [pendingUpdate[@"allowRestartOnResume"] boolValue])) + { int rollbackTimeout = [pendingUpdate[@"rollbackTimeout"] intValue]; - [self initializeUpdateWithRollbackTimeout:rollbackTimeout needsRestart:NO]; + + // If the app wasn't restarted "naturally", then we need to restart it manually + BOOL needsRestart = !isAppStart; + [self initializeUpdateWithRollbackTimeout:rollbackTimeout needsRestart:needsRestart]; // Clear the pending update and sync [preferences removeObjectForKey:PendingUpdateKey]; @@ -83,11 +88,47 @@ NSString * const PendingUpdateKey = @"CODE_PUSH_PENDING_UPDATE"; } } } +} + +- (void)checkForPendingUpdateDuringResume { + [self checkForPendingUpdate:NO]; +} + +- (NSDictionary *)constantsToExport +{ + // Export the values of the CodePushRestartMode enum + // so that the script-side can easily stay in sync + return @{ @"codePushRestartModeNone": @(CodePushRestartModeNone), + @"codePushRestartModeImmediate": @(CodePushRestartModeImmediate), + @"codePushRestartModeOnNextResume": @(CodePushRestartModeOnNextResume) + }; +}; + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (CodePush *)init +{ + self = [super init]; + + if (self) { + [self checkForPendingUpdate:YES]; + + // Register for app resume notifications so that we + // can check for pending updates which support "restart on resume" + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(checkForPendingUpdateDuringResume) + name:UIApplicationWillEnterForegroundNotification + object:[UIApplication sharedApplication]]; + } return self; } -- (void)initializeUpdateWithRollbackTimeout:(int)rollbackTimeout needsRestart:(BOOL)needsRestart { +- (void)initializeUpdateWithRollbackTimeout:(int)rollbackTimeout + needsRestart:(BOOL)needsRestart +{ didUpdate = YES; if (needsRestart) { @@ -101,7 +142,8 @@ NSString * const PendingUpdateKey = @"CODE_PUSH_PENDING_UPDATE"; } } -- (BOOL)isFailedHash:(NSString*)packageHash { +- (BOOL)isFailedHash:(NSString*)packageHash +{ NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults]; NSMutableArray *failedUpdates = [preferences objectForKey:FailedUpdatesKey]; return (failedUpdates != nil && [failedUpdates containsObject:packageHash]); @@ -146,13 +188,17 @@ NSString * const PendingUpdateKey = @"CODE_PUSH_PENDING_UPDATE"; } - (void)savePendingUpdate:(NSString *)packageHash - rollbackTimeout:(int)rollbackTimeout { + rollbackTimeout:(int)rollbackTimeout + allowRestartOnResume:(BOOL)allowRestartOnResume +{ // Since we're not restarting, we need to store the fact that the update // was applied, but hasn't yet become "active". NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults]; NSDictionary *pendingUpdate = [[NSDictionary alloc] initWithObjectsAndKeys: packageHash,@"hash", - rollbackTimeout,@"rollbackTimeout", nil]; + [NSNumber numberWithInt:rollbackTimeout],@"rollbackTimeout", + [NSNumber numberWithBool:allowRestartOnResume],@"allowRestartOnResume", + nil]; [preferences setObject:pendingUpdate forKey:PendingUpdateKey]; [preferences synchronize]; @@ -170,10 +216,10 @@ NSString * const PendingUpdateKey = @"CODE_PUSH_PENDING_UPDATE"; // JavaScript-exported module methods RCT_EXPORT_METHOD(applyUpdate:(NSDictionary*)updatePackage - rollbackTimeout:(int)rollbackTimeout - restartImmediately:(BOOL)restartImmediately - resolver:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) + rollbackTimeout:(int)rollbackTimeout + restartMode:(CodePushRestartMode)restartMode + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSError *error; @@ -183,10 +229,13 @@ RCT_EXPORT_METHOD(applyUpdate:(NSDictionary*)updatePackage if (error) { reject(error); } else { - if (restartImmediately) { + if (restartMode == CodePushRestartModeImmediate) { [self initializeUpdateWithRollbackTimeout:rollbackTimeout needsRestart:YES]; } else { - [self savePendingUpdate:updatePackage[@"packageHash"] rollbackTimeout:rollbackTimeout]; + BOOL allowsRestartOnResume = (restartMode == CodePushRestartModeOnNextResume); + [self savePendingUpdate:updatePackage[@"packageHash"] + rollbackTimeout:rollbackTimeout + allowRestartOnResume:allowsRestartOnResume]; } } }); @@ -256,10 +305,10 @@ RCT_EXPORT_METHOD(isFirstRun:(NSString *)packageHash { NSError *error; BOOL isFirstRun = didUpdate - && nil != packageHash - && [packageHash length] > 0 - && [packageHash isEqualToString:[CodePushPackage getCurrentPackageHash:&error]]; - + && nil != packageHash + && [packageHash length] > 0 + && [packageHash isEqualToString:[CodePushPackage getCurrentPackageHash:&error]]; + resolve(@(isFirstRun)); } diff --git a/CodePush.xcodeproj/project.pbxproj b/CodePush.xcodeproj/project.pbxproj index 5e3c4f7..9c87fec 100644 --- a/CodePush.xcodeproj/project.pbxproj +++ b/CodePush.xcodeproj/project.pbxproj @@ -8,7 +8,8 @@ /* Begin PBXBuildFile section */ 13BE3DEE1AC21097009241FE /* CodePush.m in Sources */ = {isa = PBXBuildFile; fileRef = 13BE3DED1AC21097009241FE /* CodePush.m */; }; - 54FFEDE01BF550630061DD23 /* CodePushDownloadHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 54FFEDDF1BF550630061DD23 /* CodePushDownloadHandler.m */; settings = {ASSET_TAGS = (); }; }; + 1B23B9141BF9267B000BB2F0 /* RCTConvert+CodePushRestartMode.m in Sources */ = {isa = PBXBuildFile; fileRef = 1B23B9131BF9267B000BB2F0 /* RCTConvert+CodePushRestartMode.m */; }; + 54FFEDE01BF550630061DD23 /* CodePushDownloadHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 54FFEDDF1BF550630061DD23 /* CodePushDownloadHandler.m */; }; 810D4E6D1B96935000B397E9 /* CodePushPackage.m in Sources */ = {isa = PBXBuildFile; fileRef = 810D4E6C1B96935000B397E9 /* CodePushPackage.m */; }; 81D51F3A1B6181C2000DA084 /* CodePushConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 81D51F391B6181C2000DA084 /* CodePushConfig.m */; }; /* End PBXBuildFile section */ @@ -29,6 +30,7 @@ 134814201AA4EA6300B7C361 /* libCodePush.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libCodePush.a; sourceTree = BUILT_PRODUCTS_DIR; }; 13BE3DEC1AC21097009241FE /* CodePush.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CodePush.h; sourceTree = ""; }; 13BE3DED1AC21097009241FE /* CodePush.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CodePush.m; sourceTree = ""; }; + 1B23B9131BF9267B000BB2F0 /* RCTConvert+CodePushRestartMode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "RCTConvert+CodePushRestartMode.m"; sourceTree = ""; }; 54FFEDDF1BF550630061DD23 /* CodePushDownloadHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CodePushDownloadHandler.m; sourceTree = ""; }; 810D4E6C1B96935000B397E9 /* CodePushPackage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CodePushPackage.m; sourceTree = ""; }; 81D51F391B6181C2000DA084 /* CodePushConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CodePushConfig.m; sourceTree = ""; }; @@ -56,6 +58,7 @@ 58B511D21A9E6C8500147676 = { isa = PBXGroup; children = ( + 1B23B9131BF9267B000BB2F0 /* RCTConvert+CodePushRestartMode.m */, 54FFEDDF1BF550630061DD23 /* CodePushDownloadHandler.m */, 810D4E6C1B96935000B397E9 /* CodePushPackage.m */, 81D51F391B6181C2000DA084 /* CodePushConfig.m */, @@ -121,6 +124,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1B23B9141BF9267B000BB2F0 /* RCTConvert+CodePushRestartMode.m in Sources */, 81D51F3A1B6181C2000DA084 /* CodePushConfig.m in Sources */, 54FFEDE01BF550630061DD23 /* CodePushDownloadHandler.m in Sources */, 13BE3DEE1AC21097009241FE /* CodePush.m in Sources */, diff --git a/RCTConvert+CodePushRestartMode.m b/RCTConvert+CodePushRestartMode.m new file mode 100644 index 0000000..f2d090f --- /dev/null +++ b/RCTConvert+CodePushRestartMode.m @@ -0,0 +1,14 @@ +#import "CodePush.h" +#import "RCTConvert.h" + +// Extending the RCTConvert class allows the React Native +// bridge to handle args of type "CodePushRestartMode" +@implementation RCTConvert (CodePushRestartMode) + +RCT_ENUM_CONVERTER(CodePushRestartMode, (@{ @"codePushRestartModeNone": @(CodePushRestartModeNone), + @"codePushRestartModeImmediate": @(CodePushRestartModeImmediate), + @"codePushRestartModeOnNextResume": @(CodePushRestartModeOnNextResume) }), + CodePushRestartModeImmediate, // Default enum value + integerValue) + +@end \ No newline at end of file diff --git a/package-mixins.js b/package-mixins.js index 8c576b6..b7c090f 100644 --- a/package-mixins.js +++ b/package-mixins.js @@ -1,5 +1,6 @@ var extend = require("extend"); var { NativeAppEventEmitter } = require("react-native"); +var { RestartMode } = require("react-native-code-push"); module.exports = (NativeCodePush) => { var remote = { @@ -36,8 +37,8 @@ module.exports = (NativeCodePush) => { }; var local = { - apply: function apply(rollbackTimeout = 0, restartImmediately = true) { - return NativeCodePush.applyUpdate(this, rollbackTimeout, restartImmediately); + apply: function apply(rollbackTimeout = 0, restartMode = RestartMode.IMMEDIATE) { + return NativeCodePush.applyUpdate(this, rollbackTimeout, restartMode); } };