Skip to content
This repository was archived by the owner on May 20, 2025. It is now read-only.

Commit 92112ab

Browse files
committed
Merge pull request #49 from Microsoft/download-progress
Download progress
2 parents 79e4c50 + 0eb3ea6 commit 92112ab

File tree

17 files changed

+469
-63
lines changed

17 files changed

+469
-63
lines changed

CodePush.h

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,32 @@
2929

3030
@end
3131

32+
@interface CodePushDownloadHandler : NSObject<NSURLConnectionDelegate>
33+
34+
@property (strong) NSOutputStream *outputFileStream;
35+
@property long expectedContentLength;
36+
@property long receivedContentLength;
37+
@property (copy) void (^progressCallback)(long, long);
38+
@property (copy) void (^doneCallback)();
39+
@property (copy) void (^failCallback)(NSError *err);
40+
41+
- (id)init:(NSString *)downloadFilePath
42+
progressCallback:(void (^)(long, long))progressCallback
43+
doneCallback:(void (^)())doneCallback
44+
failCallback:(void (^)(NSError *err))failCallback;
45+
46+
- (void)download:(NSString*)url;
47+
48+
@end
49+
3250
@interface CodePushPackage : NSObject
3351

3452
+ (void)applyPackage:(NSDictionary *)updatePackage
3553
error:(NSError **)error;
3654

3755
+ (NSDictionary *)getCurrentPackage:(NSError **)error;
3856
+ (NSString *)getCurrentPackageFolderPath:(NSError **)error;
57+
+ (NSString *)getCurrentPackageBundlePath:(NSError **)error;
3958
+ (NSString *)getCurrentPackageHash:(NSError **)error;
4059

4160
+ (NSDictionary *)getPackage:(NSString *)packageHash
@@ -45,7 +64,9 @@
4564

4665

4766
+ (void)downloadPackage:(NSDictionary *)updatePackage
48-
error:(NSError **)error;
67+
progressCallback:(void (^)(long, long))progressCallback
68+
doneCallback:(void (^)())doneCallback
69+
failCallback:(void (^)(NSError *err))failCallback;
4970

5071
+ (void)rollbackPackage;
5172

CodePush.m

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#import "RCTBridgeModule.h"
2+
#import "RCTEventDispatcher.h"
23
#import "RCTRootView.h"
34
#import "RCTUtils.h"
45
#import "CodePush.h"
@@ -13,7 +14,6 @@ @implementation CodePush
1314

1415
NSString * const FailedUpdatesKey = @"CODE_PUSH_FAILED_UPDATES";
1516
NSString * const PendingUpdateKey = @"CODE_PUSH_PENDING_UPDATE";
16-
NSString * const UpdateBundleFileName = @"app.jsbundle";
1717

1818
@synthesize bridge = _bridge;
1919

@@ -27,16 +27,14 @@ + (NSString *)getDocumentsDirectory
2727
+ (NSURL *)getBundleUrl
2828
{
2929
NSError *error;
30-
NSString *packageFolder = [CodePushPackage getCurrentPackageFolderPath:&error];
30+
NSString *packageFile = [CodePushPackage getCurrentPackageBundlePath:&error];
3131
NSURL *binaryJsBundleUrl = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
3232

33-
if (error || !packageFolder)
33+
if (error || !packageFile)
3434
{
3535
return binaryJsBundleUrl;
3636
}
3737

38-
NSString *packageFile = [packageFolder stringByAppendingPathComponent:UpdateBundleFileName];
39-
4038
NSDictionary *binaryFileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[binaryJsBundleUrl path] error:nil];
4139
NSDictionary *appFileAttribs = [[NSFileManager defaultManager] attributesOfItemAtPath:packageFile error:nil];
4240
NSDate *binaryDate = [binaryFileAttributes objectForKey:NSFileModificationDate];
@@ -198,24 +196,30 @@ - (void)startRollbackTimer:(int)rollbackTimeout
198196
resolver:(RCTPromiseResolveBlock)resolve
199197
rejecter:(RCTPromiseRejectBlock)reject)
200198
{
201-
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
202-
NSError *err;
203-
[CodePushPackage downloadPackage:updatePackage
204-
error:&err];
205-
206-
if (err) {
207-
return reject(err);
208-
}
209-
210-
NSDictionary *newPackage = [CodePushPackage getPackage:updatePackage[@"packageHash"]
211-
error:&err];
212-
213-
if (err) {
214-
return reject(err);
215-
}
216-
217-
resolve(newPackage);
218-
});
199+
[CodePushPackage downloadPackage:updatePackage
200+
progressCallback:^(long expectedContentLength, long receivedContentLength) {
201+
[self.bridge.eventDispatcher
202+
sendAppEventWithName:@"CodePushDownloadProgress"
203+
body:@{
204+
@"totalBytes":[NSNumber numberWithLong:expectedContentLength],
205+
@"receivedBytes":[NSNumber numberWithLong:receivedContentLength]
206+
}];
207+
}
208+
doneCallback:^{
209+
NSError *err;
210+
NSDictionary *newPackage = [CodePushPackage
211+
getPackage:updatePackage[@"packageHash"]
212+
error:&err];
213+
214+
if (err) {
215+
return reject(err);
216+
}
217+
218+
resolve(newPackage);
219+
}
220+
failCallback:^(NSError *err) {
221+
reject(err);
222+
}];
219223
}
220224

221225
RCT_EXPORT_METHOD(getConfiguration:(RCTPromiseResolveBlock)resolve

CodePush.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
/* Begin PBXBuildFile section */
1010
13BE3DEE1AC21097009241FE /* CodePush.m in Sources */ = {isa = PBXBuildFile; fileRef = 13BE3DED1AC21097009241FE /* CodePush.m */; };
11+
54FFEDE01BF550630061DD23 /* CodePushDownloadHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 54FFEDDF1BF550630061DD23 /* CodePushDownloadHandler.m */; settings = {ASSET_TAGS = (); }; };
1112
810D4E6D1B96935000B397E9 /* CodePushPackage.m in Sources */ = {isa = PBXBuildFile; fileRef = 810D4E6C1B96935000B397E9 /* CodePushPackage.m */; };
1213
81D51F3A1B6181C2000DA084 /* CodePushConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 81D51F391B6181C2000DA084 /* CodePushConfig.m */; };
1314
/* End PBXBuildFile section */
@@ -28,6 +29,7 @@
2829
134814201AA4EA6300B7C361 /* libCodePush.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libCodePush.a; sourceTree = BUILT_PRODUCTS_DIR; };
2930
13BE3DEC1AC21097009241FE /* CodePush.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CodePush.h; sourceTree = "<group>"; };
3031
13BE3DED1AC21097009241FE /* CodePush.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CodePush.m; sourceTree = "<group>"; };
32+
54FFEDDF1BF550630061DD23 /* CodePushDownloadHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CodePushDownloadHandler.m; sourceTree = "<group>"; };
3133
810D4E6C1B96935000B397E9 /* CodePushPackage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CodePushPackage.m; sourceTree = "<group>"; };
3234
81D51F391B6181C2000DA084 /* CodePushConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CodePushConfig.m; sourceTree = "<group>"; };
3335
/* End PBXFileReference section */
@@ -54,6 +56,7 @@
5456
58B511D21A9E6C8500147676 = {
5557
isa = PBXGroup;
5658
children = (
59+
54FFEDDF1BF550630061DD23 /* CodePushDownloadHandler.m */,
5760
810D4E6C1B96935000B397E9 /* CodePushPackage.m */,
5861
81D51F391B6181C2000DA084 /* CodePushConfig.m */,
5962
13BE3DEC1AC21097009241FE /* CodePush.h */,
@@ -119,6 +122,7 @@
119122
buildActionMask = 2147483647;
120123
files = (
121124
81D51F3A1B6181C2000DA084 /* CodePushConfig.m in Sources */,
125+
54FFEDE01BF550630061DD23 /* CodePushDownloadHandler.m in Sources */,
122126
13BE3DEE1AC21097009241FE /* CodePush.m in Sources */,
123127
810D4E6D1B96935000B397E9 /* CodePushPackage.m in Sources */,
124128
);

CodePushDownloadHandler.m

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#import "CodePush.h"
2+
3+
@implementation CodePushDownloadHandler
4+
5+
- (id)init:(NSString *)downloadFilePath
6+
progressCallback:(void (^)(long, long))progressCallback
7+
doneCallback:(void (^)())doneCallback
8+
failCallback:(void (^)(NSError *err))failCallback {
9+
self.outputFileStream = [NSOutputStream outputStreamToFileAtPath:downloadFilePath
10+
append:NO];
11+
self.receivedContentLength = 0;
12+
self.progressCallback = progressCallback;
13+
self.doneCallback = doneCallback;
14+
self.failCallback = failCallback;
15+
return self;
16+
}
17+
18+
-(void)download:(NSString*)url {
19+
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]
20+
cachePolicy:NSURLRequestUseProtocolCachePolicy
21+
timeoutInterval:60.0];
22+
23+
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request
24+
delegate:self
25+
startImmediately:NO];
26+
[connection scheduleInRunLoop:[NSRunLoop mainRunLoop]
27+
forMode:NSDefaultRunLoopMode];
28+
[connection start];
29+
}
30+
31+
#pragma mark NSURLConnection Delegate Methods
32+
33+
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection
34+
willCacheResponse:(NSCachedURLResponse*)cachedResponse {
35+
// Return nil to indicate not necessary to store a cached response for this connection
36+
return nil;
37+
}
38+
39+
-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
40+
self.expectedContentLength = response.expectedContentLength;
41+
[self.outputFileStream open];
42+
}
43+
44+
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
45+
self.receivedContentLength = self.receivedContentLength + [data length];
46+
47+
NSInteger bytesLeft = [data length];
48+
49+
do {
50+
NSInteger bytesWritten = [self.outputFileStream write:[data bytes]
51+
maxLength:bytesLeft];
52+
if (bytesWritten == -1) {
53+
break;
54+
}
55+
56+
bytesLeft -= bytesWritten;
57+
} while (bytesLeft > 0);
58+
59+
self.progressCallback(self.expectedContentLength, self.receivedContentLength);
60+
61+
// bytesLeft should not be negative.
62+
assert(bytesLeft >= 0);
63+
64+
if (bytesLeft) {
65+
[self.outputFileStream close];
66+
[connection cancel];
67+
self.failCallback([self.outputFileStream streamError]);
68+
}
69+
}
70+
71+
- (void)connection:(NSURLConnection*)connection didFailWithError:(NSError*)error
72+
{
73+
[self.outputFileStream close];
74+
self.failCallback(error);
75+
}
76+
77+
-(void)connectionDidFinishLoading:(NSURLConnection *)connection {
78+
// We should have received all of the bytes if this is called.
79+
assert(self.receivedContentLength == self.expectedContentLength);
80+
81+
[self.outputFileStream close];
82+
self.doneCallback();
83+
}
84+
85+
@end

CodePushPackage.m

Lines changed: 47 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
@implementation CodePushPackage
44

55
NSString * const StatusFile = @"codepush.json";
6+
NSString * const UpdateBundleFileName = @"app.jsbundle";
67

78
+ (NSString *)getCodePushPath
89
{
@@ -72,6 +73,17 @@ + (NSString *)getCurrentPackageFolderPath:(NSError **)error
7273
return [self getPackageFolderPath:packageHash];
7374
}
7475

76+
+ (NSString *)getCurrentPackageBundlePath:(NSError **)error
77+
{
78+
NSString *packageFolder = [self getCurrentPackageFolderPath:error];
79+
80+
if(*error) {
81+
return NULL;
82+
}
83+
84+
return [packageFolder stringByAppendingPathComponent:UpdateBundleFileName];
85+
}
86+
7587
+ (NSString *)getCurrentPackageHash:(NSError **)error
7688
{
7789
NSDictionary *info = [self getCurrentPackageInfo:error];
@@ -149,50 +161,51 @@ + (NSString *)getPackageFolderPath:(NSString *)packageHash
149161
}
150162

151163
+ (void)downloadPackage:(NSDictionary *)updatePackage
152-
error:(NSError **)error
164+
progressCallback:(void (^)(long, long))progressCallback
165+
doneCallback:(void (^)())doneCallback
166+
failCallback:(void (^)(NSError *err))failCallback
153167
{
154168
NSString *packageFolderPath = [self getPackageFolderPath:updatePackage[@"packageHash"]];
169+
NSError *error = nil;
155170

156171
if (![[NSFileManager defaultManager] fileExistsAtPath:packageFolderPath]) {
157172
[[NSFileManager defaultManager] createDirectoryAtPath:packageFolderPath
158173
withIntermediateDirectories:YES
159174
attributes:nil
160-
error:error];
161-
}
162-
163-
if (*error) {
164-
return;
165-
}
166-
167-
NSURL *url = [[NSURL alloc] initWithString:updatePackage[@"downloadUrl"]];
168-
NSString *updateContents = [[NSString alloc] initWithContentsOfURL:url
169-
encoding:NSUTF8StringEncoding
170-
error:error];
171-
if (*error) {
172-
return;
175+
error:&error];
173176
}
174177

175-
[updateContents writeToFile:[packageFolderPath stringByAppendingPathComponent:@"app.jsbundle"]
176-
atomically:YES
177-
encoding:NSUTF8StringEncoding
178-
error:error];
179-
if (*error) {
180-
return;
181-
}
182-
183-
NSData *updateSerializedData = [NSJSONSerialization dataWithJSONObject:updatePackage
184-
options:0
185-
error:error];
186-
187-
if (*error) {
188-
return;
189-
}
178+
if (error) {
179+
return failCallback(error);
180+
}
181+
182+
NSString *downloadFilePath = [packageFolderPath stringByAppendingPathComponent:UpdateBundleFileName];
183+
184+
CodePushDownloadHandler *downloadHandler = [[CodePushDownloadHandler alloc]
185+
init:downloadFilePath
186+
progressCallback:progressCallback
187+
doneCallback:^{
188+
NSError *error;
189+
NSData *updateSerializedData = [NSJSONSerialization
190+
dataWithJSONObject:updatePackage
191+
options:0
192+
error:&error];
193+
NSString *packageJsonString = [[NSString alloc]
194+
initWithData:updateSerializedData encoding:NSUTF8StringEncoding];
195+
196+
[packageJsonString writeToFile:[packageFolderPath stringByAppendingPathComponent:@"app.json"]
197+
atomically:YES
198+
encoding:NSUTF8StringEncoding
199+
error:&error];
200+
if (error) {
201+
failCallback(error);
202+
} else {
203+
doneCallback();
204+
}
205+
}
206+
failCallback:failCallback];
190207

191-
NSString *packageJsonString = [[NSString alloc] initWithData:updateSerializedData encoding:NSUTF8StringEncoding];
192-
[packageJsonString writeToFile:[packageFolderPath stringByAppendingPathComponent:@"app.json"]
193-
atomically:YES
194-
encoding:NSUTF8StringEncoding
195-
error:error];
208+
[downloadHandler download:updatePackage[@"downloadUrl"]];
196209
}
197210

198211
+ (void)applyPackage:(NSDictionary *)updatePackage

Examples/CodePushDemoApp/CodePushDemoApp.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
5451ACBA1B86A5B600E2A7DF /* QueryUpdateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 5451ACB81B86A5B600E2A7DF /* QueryUpdateTests.m */; };
2626
5451ACEC1B86E40A00E2A7DF /* libRCTTest.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5451ACEB1B86E34300E2A7DF /* libRCTTest.a */; };
2727
54D774BA1B87DAF800F2ABF8 /* ApplyUpdateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 54D774B91B87DAF800F2ABF8 /* ApplyUpdateTests.m */; };
28+
54F5F2B41BF6B45D007C3CEA /* DownloadProgressTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 54F5F2B31BF6B45D007C3CEA /* DownloadProgressTests.m */; settings = {ASSET_TAGS = (); }; };
2829
81551E1B1B3B428000F5B9F1 /* libCodePush.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 81551E0F1B3B427200F5B9F1 /* libCodePush.a */; };
2930
/* End PBXBuildFile section */
3031

@@ -151,6 +152,7 @@
151152
5451ACB81B86A5B600E2A7DF /* QueryUpdateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QueryUpdateTests.m; sourceTree = "<group>"; };
152153
5451ACE61B86E34300E2A7DF /* RCTTest.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTTest.xcodeproj; path = "node_modules/react-native/Libraries/RCTTest/RCTTest.xcodeproj"; sourceTree = "<group>"; };
153154
54D774B91B87DAF800F2ABF8 /* ApplyUpdateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApplyUpdateTests.m; sourceTree = "<group>"; };
155+
54F5F2B31BF6B45D007C3CEA /* DownloadProgressTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DownloadProgressTests.m; sourceTree = "<group>"; };
154156
78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = "<group>"; };
155157
81551E0A1B3B427200F5B9F1 /* CodePush.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = CodePush.xcodeproj; path = ../../CodePush.xcodeproj; sourceTree = "<group>"; };
156158
832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = "<group>"; };
@@ -238,6 +240,7 @@
238240
00E356EF1AD99517003FC87E /* CodePushDemoAppTests */ = {
239241
isa = PBXGroup;
240242
children = (
243+
54F5F2B31BF6B45D007C3CEA /* DownloadProgressTests.m */,
241244
5451ACB81B86A5B600E2A7DF /* QueryUpdateTests.m */,
242245
54D774B91B87DAF800F2ABF8 /* ApplyUpdateTests.m */,
243246
00E356F01AD99517003FC87E /* Supporting Files */,
@@ -608,6 +611,7 @@
608611
buildActionMask = 2147483647;
609612
files = (
610613
5451ACBA1B86A5B600E2A7DF /* QueryUpdateTests.m in Sources */,
614+
54F5F2B41BF6B45D007C3CEA /* DownloadProgressTests.m in Sources */,
611615
54D774BA1B87DAF800F2ABF8 /* ApplyUpdateTests.m in Sources */,
612616
);
613617
runOnlyForDeploymentPostprocessing = 0;

Examples/CodePushDemoApp/CodePushDemoAppTests/ApplyUpdateTests/DownloadAndApplyUpdateTest.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ var DownloadAndApplyUpdateTest = React.createClass({
3939
runTest() {
4040
var update = require("./TestPackage");
4141
NativeBridge.downloadUpdate(update).done((downloadedPackage) => {
42-
NativeBridge.applyUpdate(downloadedPackage, 1000);
42+
NativeBridge.applyUpdate(downloadedPackage, /*rollbackTimeout*/ 1000, /*restartImmediately*/ true);
4343
});
4444
},
4545

0 commit comments

Comments
 (0)