From a20e4a2602a046f35eaffcf70fd4b107e9b55bde Mon Sep 17 00:00:00 2001 From: Jonathan Carter Date: Tue, 24 Nov 2015 09:50:34 -0800 Subject: [PATCH] Adding comments to CodePush class --- CodePush.h | 15 ++++- CodePush.m | 185 +++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 157 insertions(+), 43 deletions(-) diff --git a/CodePush.h b/CodePush.h index d24387b..f2e967b 100644 --- a/CodePush.h +++ b/CodePush.h @@ -1,7 +1,18 @@ #import "RCTBridgeModule.h" -@interface CodePush : NSObject +@interface CodePush : NSObject +/* + * This method is used to retreive the URL for the most recent + * version of the JavaScript bundle. This could be either the + * bundle that was packaged with the app binary, or the bundle + * that was downloaded as part of a CodePush update. The value returned + * should be used to "bootstrap" the React Native bridge. + * + * This method assumes that your JS bundle is named "main.jsbundle" + * and therefore, if it isn't, you should use either the bundleURLForResource: + * or bundleURLForResource:withExtension: methods to override thsat behavior. +*/ + (NSURL *)bundleURL; + (NSURL *)bundleURLForResource:(NSString *)resourceName; @@ -25,7 +36,7 @@ @end -@interface CodePushDownloadHandler : NSObject +@interface CodePushDownloadHandler : NSObject @property (strong) NSOutputStream *outputFileStream; @property long expectedContentLength; diff --git a/CodePush.m b/CodePush.m index d0eaa5f..3d611a4 100644 --- a/CodePush.m +++ b/CodePush.m @@ -1,8 +1,9 @@ #import "RCTBridgeModule.h" -#import "RCTEventDispatcher.h" #import "RCTConvert.h" +#import "RCTEventDispatcher.h" #import "RCTRootView.h" #import "RCTUtils.h" + #import "CodePush.h" @implementation CodePush { @@ -15,16 +16,17 @@ static BOOL didUpdate = NO; static NSTimer *_timer; static BOOL usingTestFolder = NO; -static NSString * const FailedUpdatesKey = @"CODE_PUSH_FAILED_UPDATES"; -static NSString * const PendingUpdateKey = @"CODE_PUSH_PENDING_UPDATE"; +static NSString *const FailedUpdatesKey = @"CODE_PUSH_FAILED_UPDATES"; +static NSString *const PendingUpdateKey = @"CODE_PUSH_PENDING_UPDATE"; // These keys are already "namespaced" by the PendingUpdateKey, so // their values don't need to be obfuscated to prevent collision with app data -static NSString * const PendingUpdateHashKey = @"hash"; -static NSString * const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; +static NSString *const PendingUpdateHashKey = @"hash"; +static NSString *const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; @synthesize bridge = _bridge; +// Public Obj-C API (see header for method comments) + (NSURL *)bundleURL { return [self bundleURLForResource:@"main"]; @@ -43,8 +45,7 @@ static NSString * const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; NSString *packageFile = [CodePushPackage getCurrentPackageBundlePath:&error]; NSURL *binaryJsBundleUrl = [[NSBundle mainBundle] URLForResource:resourceName withExtension:resourceExtension]; - if (error || !packageFile) - { + if (error || !packageFile) { return binaryJsBundleUrl; } @@ -61,14 +62,21 @@ static NSString * const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; } } -// Public Obj-C API + (NSString *)getDocumentsDirectory { NSString *documentsDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]; return documentsDirectory; } -// Internal API methods +// Private API methods + +/* + * This method cancels the currently running rollback + * timer, which has the effect of stopping an automatic + * rollback from occuring. + * + * Note: This method is safe to call from any thread. + */ - (void)cancelRollbackTimer { dispatch_async(dispatch_get_main_queue(), ^{ @@ -76,6 +84,14 @@ static NSString * const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; }); } +/* + * This method checks to see whether a "pending udpate" has been applied + * (e.g. install was called with a non-immediate mode), but the app hasn't + * yet been restarted (either naturally or synthentically). If there is one, + * it will restart the app (if specified), and start the rollback timer. + * + * Note: This method is safe to call from any thread. + */ - (void)checkForPendingUpdate:(BOOL)needsRestart { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @@ -93,15 +109,24 @@ static NSString * const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; if ([pendingHash isEqualToString:currentHash]) { int rollbackTimeout = [pendingUpdate[PendingUpdateRollbackTimeoutKey] intValue]; [self initializeUpdateWithRollbackTimeout:rollbackTimeout needsRestart:needsRestart]; - - // Clear the pending update and sync - [preferences removeObjectForKey:PendingUpdateKey]; - [preferences synchronize]; + } else { + // NOTE: We shouldn't ever reach here } + + // Clear the pending update and sync + [preferences removeObjectForKey:PendingUpdateKey]; + [preferences synchronize]; } }); } +/* + * This method is meant as a handler for the global app + * resume notification, and therefore, should not be called + * directly. It simply checks to see whether there is a pending + * update that is meant to be installed on resume, and if so + * it applies it and restarts the app. + */ - (void)checkForPendingUpdateDuringResume { // In order to ensure that CodePush doesn't impact the app's @@ -112,6 +137,12 @@ static NSString * const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; } } +/* + * This method is used by the React Native bridge to allow + * our plugin to expose constants to the JS-side. In our case + * we're simply exporting enum values so that the JS and Native + * sides of the plugin can be in sync. + */ - (NSDictionary *)constantsToExport { // Export the values of the CodePushInstallMode enum @@ -151,6 +182,14 @@ static NSString * const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; return self; } +/* + * This method performs the actual initialization work for a update + * to ensure that the neccessary state is setup, including: + * -------------------------------------------------------- + * 1. Updating the current bundle URL to point at the latest update on disk + * 2. Optionally restarting the app to load the new bundle + * 3. Optionally starting the rollback protection timer + */ - (void)initializeUpdateWithRollbackTimeout:(int)rollbackTimeout needsRestart:(BOOL)needsRestart { @@ -167,6 +206,10 @@ static NSString * const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; } } +/* + * This method checks to see whether a specific package hash + * has previously failed installation. + */ - (BOOL)isFailedHash:(NSString*)packageHash { NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults]; @@ -174,6 +217,11 @@ static NSString * const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; return (failedUpdates != nil && [failedUpdates containsObject:packageHash]); } +/* + * This method updates the React Native bridge's bundle URL + * to point at the latest CodePush update, and then restarts + * the bridge. This isn't meant to be called directly. + */ - (void)loadBundle { // If the current bundle URL is using http(s), then assume the dev @@ -187,6 +235,13 @@ static NSString * const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; [_bridge reload]; } +/* + * This method is used when an update has failed installation + * and the app needs to be rolled back to the previous bundle. + * This method is automatically called when the rollback timer + * expires without the app indicating whether the update succeeded, + * and therefore, it shouldn't be called directly. + */ - (void)rollbackPackage { NSError *error; @@ -201,6 +256,11 @@ static NSString * const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; [self loadBundle]; } +/* + * When an update failed to apply, this method can be called + * to store its hash so that it can be ignored on future + * attempts to check the server for an update. + */ - (void)saveFailedUpdate:(NSString *)packageHash { NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults]; @@ -218,6 +278,11 @@ static NSString * const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; [preferences synchronize]; } +/* + * When an update is installed whose mode isn't IMMEDIATE, this method + * can be called to store the pending update's metadata (e.g. rollbackTimeout) + * so that it can be used when the actual update application occurs at a later point. + */ - (void)savePendingUpdate:(NSString *)packageHash rollbackTimeout:(int)rollbackTimeout { @@ -232,6 +297,10 @@ static NSString * const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; [preferences synchronize]; } +/* + * This method handles starting the actual rollback timer + * after an update has been installed. + */ - (void)startRollbackTimer:(int)rollbackTimeout { double timeoutInSeconds = rollbackTimeout / 1000; @@ -243,42 +312,57 @@ static NSString * const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; } // JavaScript-exported module methods + +/* + * This is native-side of the RemotePackage.download method + */ RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { [CodePushPackage downloadPackage:updatePackage - progressCallback:^(long expectedContentLength, long receivedContentLength) { - [self.bridge.eventDispatcher - sendAppEventWithName:@"CodePushDownloadProgress" - body:@{ - @"totalBytes":[NSNumber numberWithLong:expectedContentLength], - @"receivedBytes":[NSNumber numberWithLong:receivedContentLength] - }]; - } - doneCallback:^{ - NSError *err; - NSDictionary *newPackage = [CodePushPackage - getPackage:updatePackage[@"packageHash"] - error:&err]; - - if (err) { - return reject(err); - } - - resolve(newPackage); - } - failCallback:^(NSError *err) { - reject(err); - }]; + // The download is progressing forward + progressCallback:^(long expectedContentLength, long receivedContentLength) { + // Notify the script-side about the progress + [self.bridge.eventDispatcher + sendAppEventWithName:@"CodePushDownloadProgress" + body:@{ + @"totalBytes":[NSNumber numberWithLong:expectedContentLength], + @"receivedBytes":[NSNumber numberWithLong:receivedContentLength] + }]; + } + // The download completed + doneCallback:^{ + NSError *err; + NSDictionary *newPackage = [CodePushPackage getPackage:updatePackage[@"packageHash"] error:&err]; + + if (err) { + return reject(err); + } + + resolve(newPackage); + } + // The download failed + failCallback:^(NSError *err) { + reject(err); + }]; } +/* + * This is the native side of the CodePush.getConfiguration method. It isn't + * currently exposed via the "react-native-code-push" module, and is used + * internally only by the CodePush.checkForUpdate method in order to get the + * app version, as well as the deployment key that was configured in the Info.plist file. + */ RCT_EXPORT_METHOD(getConfiguration:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { resolve([[CodePushConfig current] configuration]); } +/* + * This method is the native side of the CodePush.getCurrentPackage method. + */ RCT_EXPORT_METHOD(getCurrentPackage:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { @@ -293,6 +377,9 @@ RCT_EXPORT_METHOD(getCurrentPackage:(RCTPromiseResolveBlock)resolve }); } +/* + * This method is the native side of the LocalPackage.install method. + */ RCT_EXPORT_METHOD(installUpdate:(NSDictionary*)updatePackage rollbackTimeout:(int)rollbackTimeout installMode:(CodePushInstallMode)installMode @@ -318,6 +405,10 @@ RCT_EXPORT_METHOD(installUpdate:(NSDictionary*)updatePackage }); } +/* + * This method isn't publically exposed via the "react-native-code-push" + * module, and is only used internally to populate the RemotePackage.failedApply property. + */ RCT_EXPORT_METHOD(isFailedUpdate:(NSString *)packageHash resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) @@ -326,19 +417,26 @@ RCT_EXPORT_METHOD(isFailedUpdate:(NSString *)packageHash resolve(@(isFailedHash)); } +/* + * This method isn't publically exposed via the "react-native-code-push" + * module, and is only used internally to populate the LocalPackage.isFirstRun property. + */ RCT_EXPORT_METHOD(isFirstRun:(NSString *)packageHash resolve:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { 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)); } +/* + * This method is the native side of the CodePush.notifyApplicationReady() method. + */ RCT_EXPORT_METHOD(notifyApplicationReady:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { @@ -346,13 +444,18 @@ RCT_EXPORT_METHOD(notifyApplicationReady:(RCTPromiseResolveBlock)resolve resolve([NSNull null]); } -// This function is exposed solely for immediately installed -// update support, and shouldn't be consumed directly by user code. +/* + * This method isn't publically exposed via the "react-native-code-push" + * module, and is only used internally to support immediately installed updates. + */ RCT_EXPORT_METHOD(restartImmedidateUpdate:(int)rollbackTimeout) { [self initializeUpdateWithRollbackTimeout:rollbackTimeout needsRestart:YES]; } +/* + * This method is the native side of the CodePush.restartPendingUpdate() method. + */ RCT_EXPORT_METHOD(restartPendingUpdate) { [self checkForPendingUpdate:YES];