diff --git a/CodePush.h b/CodePush.h index 0ebb64a..223ecbf 100644 --- a/CodePush.h +++ b/CodePush.h @@ -1,12 +1,5 @@ #import "RCTBridgeModule.h" -@interface CodePush : NSObject - -+ (NSURL *)getBundleUrl; -+ (NSString *)getDocumentsDirectory; - -@end - @interface CodePushConfig : NSObject + (void)setDeploymentKey:(NSString *)deploymentKey; @@ -29,6 +22,31 @@ @end +@interface CodePushDownloadHandler : NSObject + +@property (nonatomic, strong) NSOutputStream *outputFileStream; +@property long expectedContentLength; +@property long receivedContentLength; +@property (copy)void (^progressCallback)(long, long); +@property (copy)void (^doneCallback)(); +@property (copy)void (^failCallback)(NSError *err); + +- (id)init:(NSString *)downloadFilePath +progressCallback:(void (^)(long, long))progressCallback +doneCallback:(void (^)())doneCallback +failCallback:(void (^)(NSError *err))failCallback; + +- (void)download:(NSString*)url; + +@end + +@interface CodePush : NSObject + ++ (NSURL *)getBundleUrl; ++ (NSString *)getDocumentsDirectory; + +@end + @interface CodePushPackage : NSObject + (void)applyPackage:(NSDictionary *)updatePackage @@ -43,10 +61,11 @@ + (NSString *)getPackageFolderPath:(NSString *)packageHash; - + (void)downloadPackage:(NSDictionary *)updatePackage - error:(NSError **)error; + progressCallback:(void (^)(long, long))progressCallback + doneCallback:(void (^)())doneCallback + failCallback:(void (^)(NSError *err))failCallback; + (void)rollbackPackage; -@end \ No newline at end of file +@end diff --git a/CodePush.m b/CodePush.m index 8d838d6..42e1732 100644 --- a/CodePush.m +++ b/CodePush.m @@ -1,4 +1,5 @@ #import "RCTBridgeModule.h" +#import "RCTEventDispatcher.h" #import "RCTRootView.h" #import "RCTUtils.h" #import "CodePush.h" @@ -195,27 +196,33 @@ RCT_EXPORT_METHOD(applyUpdate:(NSDictionary*)updatePackage } RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage - resolver:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - NSError *err; - [CodePushPackage downloadPackage:updatePackage - error:&err]; - - if (err) { - return reject(err); - } - - NSDictionary *newPackage = [CodePushPackage getPackage:updatePackage[@"packageHash"] - error:&err]; - - if (err) { - return reject(err); - } - - resolve(newPackage); - }); + [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); + }]; } RCT_EXPORT_METHOD(getConfiguration:(RCTPromiseResolveBlock)resolve diff --git a/CodePush.xcodeproj/project.pbxproj b/CodePush.xcodeproj/project.pbxproj index f757d19..5e3c4f7 100644 --- a/CodePush.xcodeproj/project.pbxproj +++ b/CodePush.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* 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 = (); }; }; 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 */ @@ -28,6 +29,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 = ""; }; + 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 = ""; }; /* End PBXFileReference section */ @@ -54,6 +56,7 @@ 58B511D21A9E6C8500147676 = { isa = PBXGroup; children = ( + 54FFEDDF1BF550630061DD23 /* CodePushDownloadHandler.m */, 810D4E6C1B96935000B397E9 /* CodePushPackage.m */, 81D51F391B6181C2000DA084 /* CodePushConfig.m */, 13BE3DEC1AC21097009241FE /* CodePush.h */, @@ -119,6 +122,7 @@ buildActionMask = 2147483647; files = ( 81D51F3A1B6181C2000DA084 /* CodePushConfig.m in Sources */, + 54FFEDE01BF550630061DD23 /* CodePushDownloadHandler.m in Sources */, 13BE3DEE1AC21097009241FE /* CodePush.m in Sources */, 810D4E6D1B96935000B397E9 /* CodePushPackage.m in Sources */, ); diff --git a/CodePushDownloadHandler.m b/CodePushDownloadHandler.m new file mode 100644 index 0000000..5d1992a --- /dev/null +++ b/CodePushDownloadHandler.m @@ -0,0 +1,76 @@ +#import "CodePush.h" + +@implementation CodePushDownloadHandler + +- (id)init:(NSString *)downloadFilePath +progressCallback:(void (^)(long, long))progressCallback +doneCallback:(void (^)())doneCallback +failCallback:(void (^)(NSError *err))failCallback { + self.outputFileStream = [NSOutputStream outputStreamToFileAtPath:downloadFilePath + append:NO]; + self.receivedContentLength = 0; + self.progressCallback = progressCallback; + self.doneCallback = doneCallback; + self.failCallback = failCallback; + return self; +} + +-(void)download:(NSString*)url { + NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url] + cachePolicy:NSURLRequestUseProtocolCachePolicy + timeoutInterval:60.0]; + + NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request + delegate:self + startImmediately:NO]; + [connection scheduleInRunLoop:[NSRunLoop mainRunLoop] + forMode:NSDefaultRunLoopMode]; + [connection start]; +} + +#pragma mark NSURLConnection Delegate Methods + +- (NSCachedURLResponse *)connection:(NSURLConnection *)connection + willCacheResponse:(NSCachedURLResponse*)cachedResponse { + // Return nil to indicate not necessary to store a cached response for this connection + return nil; +} + +-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { + self.expectedContentLength = response.expectedContentLength; + [self.outputFileStream open]; +} + +-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { + self.receivedContentLength = self.receivedContentLength + [data length]; + + NSUInteger bytesLeft = [data length]; + + do { + NSUInteger bytesWritten = [self.outputFileStream write:[data bytes] + maxLength:bytesLeft]; + if (bytesWritten == -1) { + break; + } + + bytesLeft -= bytesWritten; + } while (bytesLeft>0); + + self.progressCallback(self.expectedContentLength, self.receivedContentLength); + + if (bytesLeft) { + self.failCallback([self.outputFileStream streamError]); + } +} + +- (void)connection:(NSURLConnection*)connection didFailWithError:(NSError*)error +{ + self.failCallback(error); +} + +-(void)connectionDidFinishLoading:(NSURLConnection *)connection { + [self.outputFileStream close]; + self.doneCallback(); +} + +@end \ No newline at end of file diff --git a/CodePushPackage.m b/CodePushPackage.m index 5522a27..0badea5 100644 --- a/CodePushPackage.m +++ b/CodePushPackage.m @@ -4,6 +4,17 @@ NSString * const StatusFile = @"codepush.json"; ++ (CodePushPackage*)sharedInstance { + static dispatch_once_t predicate = 0; + __strong static id sharedInstance = nil; + //static id sharedObject = nil; //if you're not using ARC + dispatch_once(&predicate, ^{ + sharedInstance = [[self alloc] init]; + //sharedObject = [[[self alloc] init] retain]; // if you're not using ARC + }); + return sharedInstance; +} + + (NSString *)getCodePushPath { return [[CodePush getDocumentsDirectory] stringByAppendingPathComponent:@"CodePush"]; @@ -149,50 +160,51 @@ NSString * const StatusFile = @"codepush.json"; } + (void)downloadPackage:(NSDictionary *)updatePackage - error:(NSError **)error + progressCallback:(void (^)(long, long))progressCallback + doneCallback:(void (^)())doneCallback + failCallback:(void (^)(NSError *err))failCallback { - NSString *packageFolderPath = [self getPackageFolderPath:updatePackage[@"packageHash"]]; + NSString *packageFolderPath = [CodePushPackage getPackageFolderPath:updatePackage[@"packageHash"]]; + NSError *error; if (![[NSFileManager defaultManager] fileExistsAtPath:packageFolderPath]) { [[NSFileManager defaultManager] createDirectoryAtPath:packageFolderPath withIntermediateDirectories:YES attributes:nil - error:error]; + error:&error]; } - if (*error) { - return; + if (error) { + return failCallback(error); } - NSURL *url = [[NSURL alloc] initWithString:updatePackage[@"downloadUrl"]]; - NSString *updateContents = [[NSString alloc] initWithContentsOfURL:url - encoding:NSUTF8StringEncoding - error:error]; - if (*error) { - return; - } + NSString *downloadFilePath = [packageFolderPath stringByAppendingPathComponent:@"app.jsbundle"]; - [updateContents writeToFile:[packageFolderPath stringByAppendingPathComponent:@"app.jsbundle"] - atomically:YES - encoding:NSUTF8StringEncoding - error:error]; - if (*error) { - return; - } + CodePushDownloadHandler *downloadHandler = [[CodePushDownloadHandler alloc] + init:downloadFilePath + progressCallback:progressCallback + doneCallback:^{ + NSError *error; + NSData *updateSerializedData = [NSJSONSerialization + dataWithJSONObject:updatePackage + options:0 + error:&error]; + NSString *packageJsonString = [[NSString alloc] + initWithData:updateSerializedData encoding:NSUTF8StringEncoding]; + + [packageJsonString writeToFile:[packageFolderPath stringByAppendingPathComponent:@"app.json"] + atomically:YES + encoding:NSUTF8StringEncoding + error:&error]; + if (error) { + failCallback(error); + } else { + doneCallback(); + } + } + failCallback:failCallback]; - NSData *updateSerializedData = [NSJSONSerialization dataWithJSONObject:updatePackage - options:0 - error:error]; - - if (*error) { - return; - } - - NSString *packageJsonString = [[NSString alloc] initWithData:updateSerializedData encoding:NSUTF8StringEncoding]; - [packageJsonString writeToFile:[packageFolderPath stringByAppendingPathComponent:@"app.json"] - atomically:YES - encoding:NSUTF8StringEncoding - error:error]; + [downloadHandler download:updatePackage[@"downloadUrl"]]; } + (void)applyPackage:(NSDictionary *)updatePackage @@ -207,7 +219,7 @@ NSString * const StatusFile = @"codepush.json"; [info setValue:info[@"currentPackage"] forKey:@"previousPackage"]; [info setValue:packageHash forKey:@"currentPackage"]; - + [self updateCurrentPackageInfo:info error:error]; } diff --git a/Examples/CodePushDemoApp/iOS/Info.plist b/Examples/CodePushDemoApp/iOS/Info.plist index e1033b9..7497133 100644 --- a/Examples/CodePushDemoApp/iOS/Info.plist +++ b/Examples/CodePushDemoApp/iOS/Info.plist @@ -44,6 +44,6 @@ NSLocationWhenInUseUsageDescription CodePushDeploymentKey - deployment-key-here + your-deployment-key diff --git a/Examples/CodePushDemoApp/index.ios.js b/Examples/CodePushDemoApp/index.ios.js index 739b7c7..cd18da9 100644 --- a/Examples/CodePushDemoApp/index.ios.js +++ b/Examples/CodePushDemoApp/index.ios.js @@ -32,14 +32,23 @@ var CodePushDemoApp = React.createClass({ return { update: false }; }, handlePress: function() { - this.state.update.download().done((localPackage) => { + this.state.update.download((progress) => { + this.setState({ + progress:progress + }); + }).done((localPackage) => { localPackage.apply().done(); }); }, render: function() { var updateView; - if (this.state.update) { + + if (this.state.progress) { + updateView = ( + {this.state.progress.receivedBytes} of {this.state.progress.totalBytes} bytes received + ); + } else if (this.state.update) { updateView = ( Update Available: {'\n'} {this.state.update.scriptVersion} - {this.state.update.description} @@ -49,6 +58,7 @@ var CodePushDemoApp = React.createClass({ ); }; + return ( diff --git a/package-mixins.js b/package-mixins.js index 8590a60..1bdb07b 100644 --- a/package-mixins.js +++ b/package-mixins.js @@ -1,20 +1,33 @@ var extend = require("extend"); +var { NativeAppEventEmitter } = require('react-native'); module.exports = (NativeCodePush) => { var remote = { abortDownload: function abortDownload() { return NativeCodePush.abortDownload(this); }, - download: function download() { + download: function download(progressHandler) { if (!this.downloadUrl) { return Promise.reject(new Error("Cannot download an update without a download url")); } - + + // Use event subscription to obtain download progress. + var downloadProgressSubscription = NativeAppEventEmitter.addListener( + 'CodePushDownloadProgress', + progressHandler + ); + // Use the downloaded package info. Native code will save the package info // so that the client knows what the current package version is. return NativeCodePush.downloadUpdate(this) .then((downloadedPackage) => { + downloadProgressSubscription.remove(); return extend({}, downloadedPackage, local); + }) + .catch((error) => { + downloadProgressSubscription.remove(); + // Rethrow the error for subsequent handlers down the promise chain. + throw error; }); } };