download-progress

This commit is contained in:
Geoffrey Goh
2015-11-12 20:37:02 -08:00
parent 79e4c5042f
commit 7e0645a939
8 changed files with 209 additions and 68 deletions

View File

@@ -1,12 +1,5 @@
#import "RCTBridgeModule.h"
@interface CodePush : NSObject<RCTBridgeModule>
+ (NSURL *)getBundleUrl;
+ (NSString *)getDocumentsDirectory;
@end
@interface CodePushConfig : NSObject
+ (void)setDeploymentKey:(NSString *)deploymentKey;
@@ -29,6 +22,31 @@
@end
@interface CodePushDownloadHandler : NSObject<NSURLConnectionDelegate>
@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<RCTBridgeModule>
+ (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
@end

View File

@@ -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

View File

@@ -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 = "<group>"; };
13BE3DED1AC21097009241FE /* CodePush.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CodePush.m; sourceTree = "<group>"; };
54FFEDDF1BF550630061DD23 /* CodePushDownloadHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CodePushDownloadHandler.m; sourceTree = "<group>"; };
810D4E6C1B96935000B397E9 /* CodePushPackage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CodePushPackage.m; sourceTree = "<group>"; };
81D51F391B6181C2000DA084 /* CodePushConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CodePushConfig.m; sourceTree = "<group>"; };
/* 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 */,
);

76
CodePushDownloadHandler.m Normal file
View File

@@ -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

View File

@@ -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];
}

View File

@@ -44,6 +44,6 @@
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
<key>CodePushDeploymentKey</key>
<string>deployment-key-here</string>
<string>your-deployment-key</string>
</dict>
</plist>

View File

@@ -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 = (
<Text>{this.state.progress.receivedBytes} of {this.state.progress.totalBytes} bytes received</Text>
);
} else if (this.state.update) {
updateView = (
<View>
<Text>Update Available: {'\n'} {this.state.update.scriptVersion} - {this.state.update.description}</Text>
@@ -49,6 +58,7 @@ var CodePushDemoApp = React.createClass({
</View>
);
};
return (
<View style={styles.container}>
<Text style={styles.welcome}>

View File

@@ -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;
});
}
};