diff --git a/CodePush.h b/CodePush.h index 0ebb64a..1e95da9 100644 --- a/CodePush.h +++ b/CodePush.h @@ -29,6 +29,24 @@ @end +@interface CodePushDownloadHandler : NSObject + +@property (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 CodePushPackage : NSObject + (void)applyPackage:(NSDictionary *)updatePackage @@ -36,6 +54,7 @@ + (NSDictionary *)getCurrentPackage:(NSError **)error; + (NSString *)getCurrentPackageFolderPath:(NSError **)error; ++ (NSString *)getCurrentPackageBundlePath:(NSError **)error; + (NSString *)getCurrentPackageHash:(NSError **)error; + (NSDictionary *)getPackage:(NSString *)packageHash @@ -45,7 +64,9 @@ + (void)downloadPackage:(NSDictionary *)updatePackage - error:(NSError **)error; + progressCallback:(void (^)(long, long))progressCallback + doneCallback:(void (^)())doneCallback + failCallback:(void (^)(NSError *err))failCallback; + (void)rollbackPackage; diff --git a/CodePush.m b/CodePush.m index 8d838d6..e0d4ec6 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" @@ -13,7 +14,6 @@ BOOL didUpdate = NO; NSString * const FailedUpdatesKey = @"CODE_PUSH_FAILED_UPDATES"; NSString * const PendingUpdateKey = @"CODE_PUSH_PENDING_UPDATE"; -NSString * const UpdateBundleFileName = @"app.jsbundle"; @synthesize bridge = _bridge; @@ -27,16 +27,14 @@ NSString * const UpdateBundleFileName = @"app.jsbundle"; + (NSURL *)getBundleUrl { NSError *error; - NSString *packageFolder = [CodePushPackage getCurrentPackageFolderPath:&error]; + NSString *packageFile = [CodePushPackage getCurrentPackageBundlePath:&error]; NSURL *binaryJsBundleUrl = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; - if (error || !packageFolder) + if (error || !packageFile) { return binaryJsBundleUrl; } - NSString *packageFile = [packageFolder stringByAppendingPathComponent:UpdateBundleFileName]; - NSDictionary *binaryFileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[binaryJsBundleUrl path] error:nil]; NSDictionary *appFileAttribs = [[NSFileManager defaultManager] attributesOfItemAtPath:packageFile error:nil]; NSDate *binaryDate = [binaryFileAttributes objectForKey:NSFileModificationDate]; @@ -198,24 +196,30 @@ RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage 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..31725d5 --- /dev/null +++ b/CodePushDownloadHandler.m @@ -0,0 +1,85 @@ +#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]; + + NSInteger bytesLeft = [data length]; + + do { + NSInteger bytesWritten = [self.outputFileStream write:[data bytes] + maxLength:bytesLeft]; + if (bytesWritten == -1) { + break; + } + + bytesLeft -= bytesWritten; + } while (bytesLeft > 0); + + self.progressCallback(self.expectedContentLength, self.receivedContentLength); + + // bytesLeft should not be negative. + assert(bytesLeft >= 0); + + if (bytesLeft) { + [self.outputFileStream close]; + [connection cancel]; + self.failCallback([self.outputFileStream streamError]); + } +} + +- (void)connection:(NSURLConnection*)connection didFailWithError:(NSError*)error +{ + [self.outputFileStream close]; + self.failCallback(error); +} + +-(void)connectionDidFinishLoading:(NSURLConnection *)connection { + // We should have received all of the bytes if this is called. + assert(self.receivedContentLength == self.expectedContentLength); + + [self.outputFileStream close]; + self.doneCallback(); +} + +@end \ No newline at end of file diff --git a/CodePushPackage.m b/CodePushPackage.m index 5522a27..1fa696b 100644 --- a/CodePushPackage.m +++ b/CodePushPackage.m @@ -3,6 +3,7 @@ @implementation CodePushPackage NSString * const StatusFile = @"codepush.json"; +NSString * const UpdateBundleFileName = @"app.jsbundle"; + (NSString *)getCodePushPath { @@ -72,6 +73,17 @@ NSString * const StatusFile = @"codepush.json"; return [self getPackageFolderPath:packageHash]; } ++ (NSString *)getCurrentPackageBundlePath:(NSError **)error +{ + NSString *packageFolder = [self getCurrentPackageFolderPath:error]; + + if(*error) { + return NULL; + } + + return [packageFolder stringByAppendingPathComponent:UpdateBundleFileName]; +} + + (NSString *)getCurrentPackageHash:(NSError **)error { NSDictionary *info = [self getCurrentPackageInfo:error]; @@ -149,50 +161,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"]]; + NSError *error = nil; 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:UpdateBundleFileName]; - [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 diff --git a/Examples/CodePushDemoApp/CodePushDemoApp.xcodeproj/project.pbxproj b/Examples/CodePushDemoApp/CodePushDemoApp.xcodeproj/project.pbxproj index 0d3a903..3bab46c 100644 --- a/Examples/CodePushDemoApp/CodePushDemoApp.xcodeproj/project.pbxproj +++ b/Examples/CodePushDemoApp/CodePushDemoApp.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ 5451ACBA1B86A5B600E2A7DF /* QueryUpdateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 5451ACB81B86A5B600E2A7DF /* QueryUpdateTests.m */; }; 5451ACEC1B86E40A00E2A7DF /* libRCTTest.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5451ACEB1B86E34300E2A7DF /* libRCTTest.a */; }; 54D774BA1B87DAF800F2ABF8 /* ApplyUpdateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 54D774B91B87DAF800F2ABF8 /* ApplyUpdateTests.m */; }; + 54F5F2B41BF6B45D007C3CEA /* DownloadProgressTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 54F5F2B31BF6B45D007C3CEA /* DownloadProgressTests.m */; settings = {ASSET_TAGS = (); }; }; 81551E1B1B3B428000F5B9F1 /* libCodePush.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 81551E0F1B3B427200F5B9F1 /* libCodePush.a */; }; /* End PBXBuildFile section */ @@ -151,6 +152,7 @@ 5451ACB81B86A5B600E2A7DF /* QueryUpdateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QueryUpdateTests.m; sourceTree = ""; }; 5451ACE61B86E34300E2A7DF /* RCTTest.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTTest.xcodeproj; path = "node_modules/react-native/Libraries/RCTTest/RCTTest.xcodeproj"; sourceTree = ""; }; 54D774B91B87DAF800F2ABF8 /* ApplyUpdateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApplyUpdateTests.m; sourceTree = ""; }; + 54F5F2B31BF6B45D007C3CEA /* DownloadProgressTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DownloadProgressTests.m; sourceTree = ""; }; 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = ""; }; 81551E0A1B3B427200F5B9F1 /* CodePush.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = CodePush.xcodeproj; path = ../../CodePush.xcodeproj; sourceTree = ""; }; 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = ""; }; @@ -238,6 +240,7 @@ 00E356EF1AD99517003FC87E /* CodePushDemoAppTests */ = { isa = PBXGroup; children = ( + 54F5F2B31BF6B45D007C3CEA /* DownloadProgressTests.m */, 5451ACB81B86A5B600E2A7DF /* QueryUpdateTests.m */, 54D774B91B87DAF800F2ABF8 /* ApplyUpdateTests.m */, 00E356F01AD99517003FC87E /* Supporting Files */, @@ -608,6 +611,7 @@ buildActionMask = 2147483647; files = ( 5451ACBA1B86A5B600E2A7DF /* QueryUpdateTests.m in Sources */, + 54F5F2B41BF6B45D007C3CEA /* DownloadProgressTests.m in Sources */, 54D774BA1B87DAF800F2ABF8 /* ApplyUpdateTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Examples/CodePushDemoApp/CodePushDemoAppTests/ApplyUpdateTests/DownloadAndApplyUpdateTest.js b/Examples/CodePushDemoApp/CodePushDemoAppTests/ApplyUpdateTests/DownloadAndApplyUpdateTest.js index 5bd9c91..52dcbce 100644 --- a/Examples/CodePushDemoApp/CodePushDemoAppTests/ApplyUpdateTests/DownloadAndApplyUpdateTest.js +++ b/Examples/CodePushDemoApp/CodePushDemoAppTests/ApplyUpdateTests/DownloadAndApplyUpdateTest.js @@ -39,7 +39,7 @@ var DownloadAndApplyUpdateTest = React.createClass({ runTest() { var update = require("./TestPackage"); NativeBridge.downloadUpdate(update).done((downloadedPackage) => { - NativeBridge.applyUpdate(downloadedPackage, 1000); + NativeBridge.applyUpdate(downloadedPackage, /*rollbackTimeout*/ 1000, /*restartImmediately*/ true); }); }, diff --git a/Examples/CodePushDemoApp/CodePushDemoAppTests/DownloadProgressTests.m b/Examples/CodePushDemoApp/CodePushDemoAppTests/DownloadProgressTests.m new file mode 100644 index 0000000..fc23418 --- /dev/null +++ b/Examples/CodePushDemoApp/CodePushDemoAppTests/DownloadProgressTests.m @@ -0,0 +1,36 @@ +#import +#import +#import + +#import "RCTAssert.h" + +#define FB_REFERENCE_IMAGE_DIR "\"$(SOURCE_ROOT)/$(PROJECT_NAME)Tests/ReferenceImages\"" + +@interface DownloadProgressTests : XCTestCase + +@end + +@implementation DownloadProgressTests +{ + RCTTestRunner *_runner; +} + +- (void)setUp +{ +#if __LP64__ + RCTAssert(false, @"Tests should be run on 32-bit device simulators (e.g. iPhone 5)"); +#endif + + NSOperatingSystemVersion version = [[NSProcessInfo processInfo] operatingSystemVersion]; + RCTAssert(version.majorVersion == 8 || version.minorVersion == 3, @"Tests should be run on iOS 8.3, found %zd.%zd.%zd", version.majorVersion, version.minorVersion, version.patchVersion); + _runner = RCTInitRunnerForApp(@"CodePushDemoAppTests/DownloadProgressTests/DownloadProgressTestApp.ios", nil); +} + +#pragma mark Logic Tests +- (void)testDownloadProgress +{ + + [_runner runTest:_cmd module:@"DownloadProgressTest"]; +} + +@end diff --git a/Examples/CodePushDemoApp/CodePushDemoAppTests/DownloadProgressTests/DownloadProgressTest.js b/Examples/CodePushDemoApp/CodePushDemoAppTests/DownloadProgressTests/DownloadProgressTest.js new file mode 100644 index 0000000..c61b4ef --- /dev/null +++ b/Examples/CodePushDemoApp/CodePushDemoAppTests/DownloadProgressTests/DownloadProgressTest.js @@ -0,0 +1,101 @@ +"use strict"; + +var RCTTestModule = require("NativeModules").TestModule; +var React = require("react-native"); +var CodePushSdk = require("react-native-code-push"); +var NativeBridge = require("react-native").NativeModules.CodePush; +var { NativeAppEventEmitter } = require("react-native"); + +var { + Text, + View, +} = React; + +var DownloadProgressTest = React.createClass({ + propTypes: { + shouldThrow: React.PropTypes.bool, + waitOneFrame: React.PropTypes.bool, + }, + + getInitialState() { + return { + done: false, + }; + }, + + componentDidMount() { + if (this.props.waitOneFrame) { + requestAnimationFrame(this.runTest); + } else { + this.runTest(); + } + }, + + checkReceivedAndExpectedBytesEqual() { + if (this.state.progress.receivedBytes !== this.state.progress.totalBytes) { + throw new Error("Bytes do not tally: Received bytes=" + this.state.progress.receivedBytes + " Total bytes=" + this.state.progress.totalBytes); + } + }, + + runTest() { + var downloadProgressSubscription = NativeAppEventEmitter.addListener( + "CodePushDownloadProgress", + (progress) => { + this.setState({ + progress:progress, + done: false, + }); + } + ); + + var updates = require("./TestPackages"); + NativeBridge.downloadUpdate(updates.smallPackage) + .then((smallPackage) => { + if (smallPackage) { + this.checkReceivedAndExpectedBytesEqual(); + return NativeBridge.downloadUpdate(updates.mediumPackage); + } else { + throw new Error("Small package download failed."); + } + }) + .then((mediumPackage) => { + if (mediumPackage) { + this.checkReceivedAndExpectedBytesEqual(); + return NativeBridge.downloadUpdate(updates.largePackage); + } else { + throw new Error("Medium package download failed."); + } + }) + .done((largePackage) => { + if (largePackage) { + this.checkReceivedAndExpectedBytesEqual(); + this.setState({done: true}, RCTTestModule.markTestCompleted); + } else { + throw new Error("Large package download failed."); + } + }); + }, + + render() { + var progressView; + if (this.state.progress) { + progressView = ( + {this.state.progress.receivedBytes} of {this.state.progress.totalBytes} bytes received + ); + } + + return ( + + + {this.constructor.displayName + ": "} + {this.state.done ? "Done" : "Testing..."} + + {progressView} + + ); + } +}); + +DownloadProgressTest.displayName = "DownloadProgressTest"; + +module.exports = DownloadProgressTest; \ No newline at end of file diff --git a/Examples/CodePushDemoApp/CodePushDemoAppTests/DownloadProgressTests/DownloadProgressTestApp.ios.js b/Examples/CodePushDemoApp/CodePushDemoAppTests/DownloadProgressTests/DownloadProgressTestApp.ios.js new file mode 100644 index 0000000..c157ee7 --- /dev/null +++ b/Examples/CodePushDemoApp/CodePushDemoAppTests/DownloadProgressTests/DownloadProgressTestApp.ios.js @@ -0,0 +1,79 @@ +"use strict"; + +var React = require("react-native"); + +var { + AppRegistry, + ScrollView, + StyleSheet, + Text, + TouchableOpacity, + View, +} = React; + +var TESTS = [ + require("./DownloadProgressTest") +]; + +TESTS.forEach( + (test) => AppRegistry.registerComponent(test.displayName, () => test) +); + +var DownloadProgressTestApp = React.createClass({ + getInitialState: function() { + return { + test: null, + }; + }, + render: function() { + if (this.state.test) { + return ( + + + + ); + } + return ( + + + Click on a test to run it in this shell for easier debugging and + development. Run all tests in the testing environment with cmd+U in + Xcode. + + + + {TESTS.map((test) => [ + this.setState({test})} + style={styles.row}> + + {test.displayName} + + , + + ])} + + + ); + } +}); + +var styles = StyleSheet.create({ + container: { + backgroundColor: "white", + marginTop: 40, + margin: 15, + }, + row: { + padding: 10, + }, + testName: { + fontWeight: "500", + }, + separator: { + height: 1, + backgroundColor: "#bbbbbb", + } +}); + +AppRegistry.registerComponent("DownloadProgressTestApp", () => DownloadProgressTestApp); \ No newline at end of file diff --git a/Examples/CodePushDemoApp/CodePushDemoAppTests/DownloadProgressTests/TestPackages.js b/Examples/CodePushDemoApp/CodePushDemoAppTests/DownloadProgressTests/TestPackages.js new file mode 100644 index 0000000..dceefff --- /dev/null +++ b/Examples/CodePushDemoApp/CodePushDemoAppTests/DownloadProgressTests/TestPackages.js @@ -0,0 +1,35 @@ +module.exports = { + smallPackage: { + downloadUrl: "http://localhost:8081/CodePushDemoAppTests/DownloadProgressTests/smallFile", + description: "Angry flappy birds", + appVersion: "1.5.0", + label: "2.4.0", + isMandatory: false, + isAvailable: true, + updateAppVersion: false, + packageHash: "hash240", + packageSize: 1024 + }, + mediumPackage: { + downloadUrl: "http://localhost:8081/CodePushDemoAppTests/DownloadProgressTests/mediumFile", + description: "Angry flappy birds", + appVersion: "1.5.0", + label: "2.4.0", + isMandatory: false, + isAvailable: true, + updateAppVersion: false, + packageHash: "hash240", + packageSize: 1024 + }, + largePackage: { + downloadUrl: "http://localhost:8081/CodePushDemoAppTests/DownloadProgressTests/largeFile", + description: "Angry flappy birds", + appVersion: "1.5.0", + label: "2.4.0", + isMandatory: false, + isAvailable: true, + updateAppVersion: false, + packageHash: "hash240", + packageSize: 1024 + } +}; \ No newline at end of file diff --git a/Examples/CodePushDemoApp/CodePushDemoAppTests/DownloadProgressTests/largeFile b/Examples/CodePushDemoApp/CodePushDemoAppTests/DownloadProgressTests/largeFile new file mode 100644 index 0000000..1a20dba Binary files /dev/null and b/Examples/CodePushDemoApp/CodePushDemoAppTests/DownloadProgressTests/largeFile differ diff --git a/Examples/CodePushDemoApp/CodePushDemoAppTests/DownloadProgressTests/mediumFile b/Examples/CodePushDemoApp/CodePushDemoAppTests/DownloadProgressTests/mediumFile new file mode 100644 index 0000000..3301331 Binary files /dev/null and b/Examples/CodePushDemoApp/CodePushDemoAppTests/DownloadProgressTests/mediumFile differ diff --git a/Examples/CodePushDemoApp/CodePushDemoAppTests/DownloadProgressTests/smallFile b/Examples/CodePushDemoApp/CodePushDemoAppTests/DownloadProgressTests/smallFile new file mode 100644 index 0000000..da1dfb9 Binary files /dev/null and b/Examples/CodePushDemoApp/CodePushDemoAppTests/DownloadProgressTests/smallFile differ diff --git a/Examples/CodePushDemoApp/index.ios.js b/Examples/CodePushDemoApp/index.ios.js index 739b7c7..97343c7 100644 --- a/Examples/CodePushDemoApp/index.ios.js +++ b/Examples/CodePushDemoApp/index.ios.js @@ -32,14 +32,22 @@ 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} diff --git a/README.md b/README.md index 0fbe811..7599cf0 100644 --- a/README.md +++ b/README.md @@ -265,7 +265,7 @@ The `RemotePackage` inherits all of the same properties as the `LocalPackage`, b - __downloadUrl__: The URL at which the package is available for download. (String). This property is only needed for advanced usage, since the `download` method will automatically handle the acquisition of updates for you. ##### Methods -- __download(): Promise__: Downloads the package update from the CodePush service. Returns a Promise that resolves with the `LocalPackage`. +- __download(progressHandler?: Function): Promise__: Downloads the package update from the CodePush service. If a `progressHandler` is specified, it will be called periodically with a `DownloadProgress` object (`{ totalBytes: Number, receivedBytes: Number }`) that reports the progress of the download until the download completes. Returns a Promise that resolves with the `LocalPackage`. --- diff --git a/package-mixins.js b/package-mixins.js index 8590a60..8c576b6 100644 --- a/package-mixins.js +++ b/package-mixins.js @@ -1,20 +1,36 @@ 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")); } + var downloadProgressSubscription; + if (progressHandler) { + // Use event subscription to obtain download progress. + 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 && downloadProgressSubscription.remove(); return extend({}, downloadedPackage, local); + }) + .catch((error) => { + downloadProgressSubscription && downloadProgressSubscription.remove(); + // Rethrow the error for subsequent handlers down the promise chain. + throw error; }); } };