Merge branch 'master' of github.com:Microsoft/react-native-code-push

This commit is contained in:
Jonathan Carter
2016-04-28 18:03:33 -07:00
8 changed files with 182 additions and 98 deletions

View File

@@ -9,7 +9,7 @@ const PackageMixins = require("./package-mixins")(NativeCodePush);
async function checkForUpdate(deploymentKey = null) {
/*
* Before we ask the server if an update exists, we
* need to retrieve three pieces of information from the
* need to retrieve three pieces of information from the
* native side: deployment key, app version (e.g. 1.0.1)
* and the hash of the currently running update (if there is one).
* This allows the client to only receive updates which are targetted
@@ -17,7 +17,7 @@ async function checkForUpdate(deploymentKey = null) {
* different from the CodePush update they have already installed.
*/
const nativeConfig = await getConfiguration();
/*
* If a deployment key was explicitly provided,
* then let's override the one we retrieved
@@ -30,7 +30,7 @@ async function checkForUpdate(deploymentKey = null) {
// Use dynamically overridden getCurrentPackage() during tests.
const localPackage = await module.exports.getCurrentPackage();
/*
* If the app has a previously installed update, and that update
* was targetted at the same app version that is currently running,
@@ -48,9 +48,9 @@ async function checkForUpdate(deploymentKey = null) {
queryPackage.packageHash = config.packageHash;
}
}
const update = await sdk.queryUpdateWithCurrentPackage(queryPackage);
/*
* There are four cases where checkForUpdate will resolve to null:
* ----------------------------------------------------------------
@@ -69,13 +69,13 @@ async function checkForUpdate(deploymentKey = null) {
* because we want to avoid having to install diff updates against the binary's
* version, which we can't do yet on Android.
*/
if (!update || update.updateAppVersion ||
localPackage && (update.packageHash === localPackage.packageHash) ||
if (!update || update.updateAppVersion ||
localPackage && (update.packageHash === localPackage.packageHash) ||
(!localPackage || localPackage._isDebugOnly) && config.packageHash === update.packageHash) {
if (update && update.updateAppVersion) {
log("An update is available but it is targeting a newer binary version than you are currently running.");
}
return null;
} else {
const remotePackage = { ...update, ...PackageMixins.remote(sdk.reportStatusDownload) };
@@ -93,7 +93,7 @@ const getConfiguration = (() => {
} else if (testConfig) {
return testConfig;
} else {
config = await NativeCodePush.getConfiguration();
config = await NativeCodePush.getConfiguration();
return config;
}
}
@@ -123,7 +123,7 @@ function getPromisifiedSdk(requestFetchAdapter, config) {
} else {
resolve(update);
}
});
});
});
};
@@ -135,7 +135,7 @@ function getPromisifiedSdk(requestFetchAdapter, config) {
} else {
resolve();
}
});
});
});
};
@@ -147,7 +147,7 @@ function getPromisifiedSdk(requestFetchAdapter, config) {
} else {
resolve();
}
});
});
});
};
@@ -159,7 +159,7 @@ function log(message) {
console.log(`[CodePush] ${message}`)
}
// This ensures that notifyApplicationReadyInternal is only called once
// This ensures that notifyApplicationReadyInternal is only called once
// in the lifetime of this module instance.
const notifyApplicationReady = (() => {
let notifyApplicationReadyPromise;
@@ -167,13 +167,13 @@ const notifyApplicationReady = (() => {
if (!notifyApplicationReadyPromise) {
notifyApplicationReadyPromise = notifyApplicationReadyInternal();
}
return notifyApplicationReadyPromise;
};
})();
async function notifyApplicationReadyInternal() {
await NativeCodePush.notifyApplicationReady();
await NativeCodePush.notifyApplicationReady();
const statusReport = await NativeCodePush.getNewStatusReport();
if (statusReport) {
const config = await getConfiguration();
@@ -208,15 +208,15 @@ function setUpTestDependencies(testSdk, providedTestConfig, testNativeBridge) {
const sync = (() => {
let syncInProgress = false;
const setSyncCompleted = () => { syncInProgress = false; };
return (options = {}, syncStatusChangeCallback, downloadProgressCallback) => {
if (syncInProgress) {
typeof syncStatusChangeCallback === "function"
? syncStatusChangeCallback(CodePush.SyncStatus.SYNC_IN_PROGRESS)
: log("Sync already in progress.");
return Promise.resolve(CodePush.SyncStatus.SYNC_IN_PROGRESS);
}
}
syncInProgress = true;
const syncPromise = syncInternal(options, syncStatusChangeCallback, downloadProgressCallback);
syncPromise
@@ -230,7 +230,7 @@ const sync = (() => {
/*
* The syncInternal method provides a simple, one-line experience for
* incorporating the check, download and installation of an update.
*
*
* It simply composes the existing API methods together and adds additional
* support for respecting mandatory updates, ignoring previously failed
* releases, and displaying a standard confirmation UI to the end-user
@@ -245,9 +245,9 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
mandatoryInstallMode: CodePush.InstallMode.IMMEDIATE,
minimumBackgroundDuration: 0,
updateDialog: null,
...options
...options
};
syncStatusChangeCallback = typeof syncStatusChangeCallback === "function"
? syncStatusChangeCallback
: (syncStatus) => {
@@ -271,7 +271,7 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
log("User cancelled the update.");
break;
case CodePush.SyncStatus.UPDATE_INSTALLED:
/*
/*
* If the install mode is IMMEDIATE, this will not get returned as the
* app will be restarted to a new Javascript context.
*/
@@ -290,40 +290,34 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
break;
}
};
downloadProgressCallback = typeof downloadProgressCallback === "function"
? downloadProgressCallback
: (downloadProgress) => {
log(`Expecting ${downloadProgress.totalBytes} bytes, received ${downloadProgress.receivedBytes} bytes.`);
};
try {
await CodePush.notifyApplicationReady();
syncStatusChangeCallback(CodePush.SyncStatus.CHECKING_FOR_UPDATE);
const remotePackage = await checkForUpdate(syncOptions.deploymentKey);
const doDownloadAndInstall = async () => {
syncStatusChangeCallback(CodePush.SyncStatus.DOWNLOADING_PACKAGE);
const localPackage = await remotePackage.download(downloadProgressCallback);
// Determine the correct install mode based on whether the update is mandatory or not.
resolvedInstallMode = localPackage.isMandatory ? syncOptions.mandatoryInstallMode : syncOptions.installMode;
syncStatusChangeCallback(CodePush.SyncStatus.INSTALLING_UPDATE);
await localPackage.install(resolvedInstallMode, syncOptions.minimumBackgroundDuration, () => {
syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_INSTALLED);
});
return CodePush.SyncStatus.UPDATE_INSTALLED;
};
const updateShouldBeIgnored = remotePackage && (remotePackage.failedInstall && syncOptions.ignoreFailedUpdates);
if (!remotePackage || updateShouldBeIgnored) {
if (updateShouldBeIgnored) {
log("An update is available, but it is being ignored due to having been previously rolled back.");
}
syncStatusChangeCallback(CodePush.SyncStatus.UP_TO_DATE);
return CodePush.SyncStatus.UP_TO_DATE;
} else if (syncOptions.updateDialog) {
@@ -334,24 +328,24 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
} else {
syncOptions.updateDialog = { ...CodePush.DEFAULT_UPDATE_DIALOG, ...syncOptions.updateDialog };
}
return await new Promise((resolve, reject) => {
return await new Promise((resolve, reject) => {
let message = null;
const dialogButtons = [{
text: null,
onPress: async () => {
onPress: async () => {
resolve(await doDownloadAndInstall());
}
}];
if (remotePackage.isMandatory) {
message = syncOptions.updateDialog.mandatoryUpdateMessage;
dialogButtons[0].text = syncOptions.updateDialog.mandatoryContinueButtonLabel;
} else {
message = syncOptions.updateDialog.optionalUpdateMessage;
dialogButtons[0].text = syncOptions.updateDialog.optionalInstallButtonLabel;
dialogButtons[0].text = syncOptions.updateDialog.optionalInstallButtonLabel;
// Since this is an optional update, add another button
// to allow the end-user to ignore it
// to allow the end-user to ignore it
dialogButtons.push({
text: syncOptions.updateDialog.optionalIgnoreButtonLabel,
onPress: () => {
@@ -360,13 +354,13 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
}
});
}
// If the update has a description, and the developer
// explicitly chose to display it, then set that as the message
if (syncOptions.updateDialog.appendReleaseDescription && remotePackage.description) {
message += `${syncOptions.updateDialog.descriptionPrefix} ${remotePackage.description}`;
message += `${syncOptions.updateDialog.descriptionPrefix} ${remotePackage.description}`;
}
syncStatusChangeCallback(CodePush.SyncStatus.AWAITING_USER_ACTION);
Alert.alert(syncOptions.updateDialog.title, message, dialogButtons);
});
@@ -375,16 +369,16 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
}
} catch (error) {
syncStatusChangeCallback(CodePush.SyncStatus.UNKNOWN_ERROR);
log(error.message);
log(error.message);
throw error;
}
}
};
let CodePush;
// If the "NativeCodePush" variable isn't defined, then
// If the "NativeCodePush" variable isn't defined, then
// the app didn't properly install the native module,
// and therefore, it doesn't make sense initializing
// and therefore, it doesn't make sense initializing
// the JS interface when it wouldn't work anyways.
if (NativeCodePush) {
CodePush = {

View File

@@ -13,6 +13,7 @@ import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.uimanager.ReactChoreographer;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.soloader.SoLoader;
@@ -25,6 +26,7 @@ import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.AsyncTask;
import android.provider.Settings;
import android.view.Choreographer;
import org.json.JSONArray;
import org.json.JSONException;
@@ -236,7 +238,7 @@ public class CodePush implements ReactPackage {
// Reset the state which indicates that
// the app was just freshly updated.
didUpdate = false;
JSONObject pendingUpdate = getPendingUpdate();
if (pendingUpdate != null) {
try {
@@ -251,7 +253,7 @@ public class CodePush implements ReactPackage {
// There is in fact a new update running for the first
// time, so update the local state to ensure the client knows.
didUpdate = true;
// Mark that we tried to initialize the new update, so that if it crashes,
// we will know that we need to rollback when the app next starts.
savePendingUpdate(pendingUpdate.getString(PENDING_UPDATE_HASH_KEY),
@@ -439,7 +441,7 @@ public class CodePush implements ReactPackage {
}
@ReactMethod
public void downloadUpdate(final ReadableMap updatePackage, final Promise promise) {
public void downloadUpdate(final ReadableMap updatePackage, final boolean notifyProgress, final Promise promise) {
AsyncTask<Void, Void, Void> asyncTask = new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
@@ -447,11 +449,48 @@ public class CodePush implements ReactPackage {
WritableMap mutableUpdatePackage = CodePushUtils.convertReadableMapToWritableMap(updatePackage);
mutableUpdatePackage.putString(BINARY_MODIFIED_TIME_KEY, "" + getBinaryResourcesModifiedTime());
codePushPackage.downloadPackage(mutableUpdatePackage, CodePush.this.assetsBundleFileName, new DownloadProgressCallback() {
private boolean hasScheduledNextFrame = false;
private DownloadProgress latestDownloadProgress = null;
@Override
public void call(DownloadProgress downloadProgress) {
if (!notifyProgress) {
return;
}
latestDownloadProgress = downloadProgress;
// If the download is completed, synchronously send the last event.
if (latestDownloadProgress.isCompleted()) {
dispatchDownloadProgressEvent();
return;
}
if (hasScheduledNextFrame) {
return;
}
hasScheduledNextFrame = true;
getReactApplicationContext().runOnUiQueueThread(new Runnable() {
@Override
public void run() {
ReactChoreographer.getInstance().postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
if (!latestDownloadProgress.isCompleted()) {
dispatchDownloadProgressEvent();
}
hasScheduledNextFrame = false;
}
});
}
});
}
public void dispatchDownloadProgressEvent() {
getReactApplicationContext()
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(DOWNLOAD_PROGRESS_EVENT_NAME, downloadProgress.createWritableMap());
.emit(DOWNLOAD_PROGRESS_EVENT_NAME, latestDownloadProgress.createWritableMap());
}
});

View File

@@ -23,4 +23,8 @@ class DownloadProgress {
}
return map;
}
public boolean isCompleted() {
return this.totalBytes == this.receivedBytes;
}
}

View File

@@ -54,11 +54,13 @@
@property (strong) NSOutputStream *outputFileStream;
@property long long expectedContentLength;
@property long long receivedContentLength;
@property dispatch_queue_t operationQueue;
@property (copy) void (^progressCallback)(long long, long long);
@property (copy) void (^doneCallback)(BOOL);
@property (copy) void (^failCallback)(NSError *err);
- (id)init:(NSString *)downloadFilePath
operationQueue:(dispatch_queue_t)operationQueue
progressCallback:(void (^)(long long, long long))progressCallback
doneCallback:(void (^)(BOOL))doneCallback
failCallback:(void (^)(NSError *err))failCallback;
@@ -78,6 +80,7 @@ failCallback:(void (^)(NSError *err))failCallback;
+ (void)downloadPackage:(NSDictionary *)updatePackage
expectedBundleFileName:(NSString *)expectedBundleFileName
operationQueue:(dispatch_queue_t)operationQueue
progressCallback:(void (^)(long long, long long))progressCallback
doneCallback:(void (^)())doneCallback
failCallback:(void (^)(NSError *err))failCallback;

View File

@@ -7,7 +7,7 @@
#import "CodePush.h"
@interface CodePush () <RCTBridgeModule>
@interface CodePush () <RCTBridgeModule, RCTFrameUpdateObserver>
@end
@implementation CodePush {
@@ -15,6 +15,11 @@
BOOL _isFirstRunAfterUpdate;
int _minimumBackgroundDuration;
NSDate *_lastResignedDate;
// Used to coordinate the dispatching of download progress events to JS.
long long _latestExpectedContentLength;
long long _latestReceivedConentLength;
BOOL _didUpdateProgress;
}
RCT_EXPORT_MODULE()
@@ -171,6 +176,8 @@ static NSString *bundleResourceName = @"main";
@synthesize bridge = _bridge;
@synthesize methodQueue = _methodQueue;
@synthesize pauseCallback = _pauseCallback;
@synthesize paused = _paused;
/*
* This method is used to clear updates that are installed
@@ -223,6 +230,17 @@ static NSString *bundleResourceName = @"main";
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)dispatchDownloadProgressEvent
{
// Notify the script-side about the progress
[self.bridge.eventDispatcher
sendDeviceEventWithName:@"CodePushDownloadProgress"
body:@{
@"totalBytes":[NSNumber numberWithLongLong:_latestExpectedContentLength],
@"receivedBytes":[NSNumber numberWithLongLong:_latestReceivedConentLength]
}];
}
/*
* This method ensures that the app was packaged with a JS bundle
* file, and if not, it throws the appropriate exception.
@@ -273,7 +291,7 @@ static NSString *bundleResourceName = @"main";
#ifdef DEBUG
[self clearDebugUpdates];
#endif
_paused = YES;
NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
NSDictionary *pendingUpdate = [preferences objectForKey:PendingUpdateKey];
if (pendingUpdate) {
@@ -479,6 +497,7 @@ static NSString *bundleResourceName = @"main";
* This is native-side of the RemotePackage.download method
*/
RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage
notifyProgress:(BOOL)notifyProgress
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
@@ -488,44 +507,53 @@ RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage
[mutableUpdatePackage setValue:[CodePushUpdateUtils modifiedDateStringOfFileAtURL:binaryBundleURL]
forKey:BinaryBundleDateKey];
}
if (notifyProgress) {
// Set up and unpause the frame observer so that it can emit
// progress events every frame if the progress is updated.
_didUpdateProgress = NO;
_paused = NO;
}
[CodePushPackage
downloadPackage:mutableUpdatePackage
expectedBundleFileName:[bundleResourceName stringByAppendingPathExtension:bundleResourceExtension]
operationQueue:_methodQueue
// The download is progressing forward
progressCallback:^(long long expectedContentLength, long long receivedContentLength) {
dispatch_async(_methodQueue, ^{
// Notify the script-side about the progress
[self.bridge.eventDispatcher
sendDeviceEventWithName:@"CodePushDownloadProgress"
body:@{
@"totalBytes":[NSNumber numberWithLongLong:expectedContentLength],
@"receivedBytes":[NSNumber numberWithLongLong:receivedContentLength]
}];
});
// Update the download progress so that the frame observer can notify the JS side
_latestExpectedContentLength = expectedContentLength;
_latestReceivedConentLength = receivedContentLength;
_didUpdateProgress = YES;
// If the download is completed, stop observing frame
// updates and synchronously send the last event.
if (expectedContentLength == receivedContentLength) {
_didUpdateProgress = NO;
_paused = YES;
[self dispatchDownloadProgressEvent];
}
}
// The download completed
doneCallback:^{
dispatch_async(_methodQueue, ^{
NSError *err;
NSDictionary *newPackage = [CodePushPackage getPackage:mutableUpdatePackage[PackageHashKey] error:&err];
NSError *err;
NSDictionary *newPackage = [CodePushPackage getPackage:mutableUpdatePackage[PackageHashKey] error:&err];
if (err) {
return reject([NSString stringWithFormat: @"%lu", (long)err.code], err.localizedDescription, err);
}
resolve(newPackage);
});
if (err) {
return reject([NSString stringWithFormat: @"%lu", (long)err.code], err.localizedDescription, err);
}
resolve(newPackage);
}
// The download failed
failCallback:^(NSError *err) {
dispatch_async(_methodQueue, ^{
if ([CodePushErrorUtils isCodePushError:err]) {
[self saveFailedUpdate:mutableUpdatePackage];
}
reject([NSString stringWithFormat: @"%lu", (long)err.code], err.localizedDescription, err);
});
if ([CodePushErrorUtils isCodePushError:err]) {
[self saveFailedUpdate:mutableUpdatePackage];
}
// Stop observing frame updates if the download fails.
_didUpdateProgress = NO;
_paused = YES;
reject([NSString stringWithFormat: @"%lu", (long)err.code], err.localizedDescription, err);
}];
}
@@ -760,4 +788,16 @@ RCT_EXPORT_METHOD(getNewStatusReport:(RCTPromiseResolveBlock)resolve
resolve(nil);
}
#pragma mark - RCTFrameUpdateObserver Methods
- (void)didUpdateFrame:(RCTFrameUpdate *)update
{
if (!_didUpdateProgress) {
return;
}
[self dispatchDownloadProgressEvent];
_didUpdateProgress = NO;
}
@end

View File

@@ -6,12 +6,14 @@
}
- (id)init:(NSString *)downloadFilePath
operationQueue:(dispatch_queue_t)operationQueue
progressCallback:(void (^)(long long, long long))progressCallback
doneCallback:(void (^)(BOOL))doneCallback
failCallback:(void (^)(NSError *err))failCallback {
self.outputFileStream = [NSOutputStream outputStreamToFileAtPath:downloadFilePath
append:NO];
self.receivedContentLength = 0;
self.operationQueue = operationQueue;
self.progressCallback = progressCallback;
self.doneCallback = doneCallback;
self.failCallback = failCallback;
@@ -22,12 +24,12 @@ failCallback:(void (^)(NSError *err))failCallback {
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];
NSOperationQueue *delegateQueue = [NSOperationQueue new];
delegateQueue.underlyingQueue = self.operationQueue;
[connection setDelegateQueue:delegateQueue];
[connection start];
}
@@ -51,31 +53,31 @@ failCallback:(void (^)(NSError *err))failCallback {
if (headerOffset >= 4) {
break;
}
const char *bytes = [data bytes];
_header[headerOffset] = bytes[i];
}
}
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];

View File

@@ -41,6 +41,7 @@ static NSString *const UnzippedFolderName = @"unzipped";
+ (void)downloadPackage:(NSDictionary *)updatePackage
expectedBundleFileName:(NSString *)expectedBundleFileName
operationQueue:(dispatch_queue_t)operationQueue
progressCallback:(void (^)(long long, long long))progressCallback
doneCallback:(void (^)())doneCallback
failCallback:(void (^)(NSError *err))failCallback
@@ -76,6 +77,7 @@ static NSString *const UnzippedFolderName = @"unzipped";
CodePushDownloadHandler *downloadHandler = [[CodePushDownloadHandler alloc]
init:downloadFilePath
operationQueue:operationQueue
progressCallback:progressCallback
doneCallback:^(BOOL isZip) {
NSError *error = nil;

View File

@@ -14,24 +14,24 @@ module.exports = (NativeCodePush) => {
let downloadProgressSubscription;
if (downloadProgressCallback) {
// Use event subscription to obtain download progress.
// Use event subscription to obtain download progress.
downloadProgressSubscription = DeviceEventEmitter.addListener(
"CodePushDownloadProgress",
downloadProgressCallback
);
}
// Use the downloaded package info. Native code will save the package info
// so that the client knows what the current package version is.
try {
const downloadedPackage = await NativeCodePush.downloadUpdate(this);
try {
const downloadedPackage = await NativeCodePush.downloadUpdate(this, !!downloadProgressCallback);
reportStatusDownload && reportStatusDownload(this);
return { ...downloadedPackage, ...local };
} finally {
downloadProgressSubscription && downloadProgressSubscription.remove();
}
},
isPending: false // A remote package could never be in a pending state
};
};
@@ -47,7 +47,7 @@ module.exports = (NativeCodePush) => {
localPackage.isPending = true; // Mark the package as pending since it hasn't been applied yet
}
},
isPending: false // A local package wouldn't be pending until it was installed
};