Merge pull request #150 from Microsoft/report-acquisition-status

Report Acquisition Status
This commit is contained in:
Geoffrey Goh
2016-01-22 14:28:49 -08:00
24 changed files with 438 additions and 139 deletions

View File

@@ -60,8 +60,9 @@ async function checkForUpdate(deploymentKey = null) {
if (!update || update.updateAppVersion || (update.packageHash === localPackage.packageHash)) {
return null;
} else {
const remotePackage = { ...update, ...PackageMixins.remote };
const remotePackage = { ...update, ...PackageMixins.remote(sdk.reportStatusDownload) };
remotePackage.failedInstall = await NativeCodePush.isFailedUpdate(remotePackage.packageHash);
remotePackage.deploymentKey = deploymentKey || nativeConfig.deploymentKey;
return remotePackage;
}
}
@@ -101,7 +102,31 @@ function getPromisifiedSdk(requestFetchAdapter, config) {
});
});
};
sdk.reportStatusDeploy = (deployedPackage, status) => {
return new Promise((resolve, reject) => {
module.exports.AcquisitionSdk.prototype.reportStatusDeploy.call(sdk, deployedPackage, status, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
};
sdk.reportStatusDownload = (downloadedPackage) => {
return new Promise((resolve, reject) => {
module.exports.AcquisitionSdk.prototype.reportStatusDownload.call(sdk, downloadedPackage, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
};
return sdk;
}
@@ -110,6 +135,22 @@ function log(message) {
console.log(`[CodePush] ${message}`)
}
async function notifyApplicationReady() {
await NativeCodePush.notifyApplicationReady();
const statusReport = await NativeCodePush.getNewStatusReport();
if (statusReport) {
const config = await getConfiguration();
if (statusReport.appVersion) {
const sdk = getPromisifiedSdk(requestFetchAdapter, config);
sdk.reportStatusDeploy();
} else {
config.deploymentKey = statusReport.package.deploymentKey;
const sdk = getPromisifiedSdk(requestFetchAdapter, config);
sdk.reportStatusDeploy(statusReport.package, statusReport.status);
}
}
}
function restartApp(onlyIfUpdateIsPending = false) {
NativeCodePush.restartApp(onlyIfUpdateIsPending);
}
@@ -269,7 +310,7 @@ const CodePush = {
getConfiguration,
getCurrentPackage,
log,
notifyApplicationReady: NativeCodePush.notifyApplicationReady,
notifyApplicationReady,
restartApp,
setUpTestDependencies,
sync,

View File

@@ -13,10 +13,17 @@
RCT_EXPORT_MODULE()
static BOOL needToReportRollback = NO;
static BOOL isRunningBinaryVersion = NO;
static BOOL testConfigurationFlag = NO;
// These constants represent valid deployment statuses
static NSString *const DeploymentFailed = @"DeploymentFailed";
static NSString *const DeploymentSucceeded = @"DeploymentSucceeded";
// These keys represent the names we use to store data in NSUserDefaults
static NSString *const FailedUpdatesKey = @"CODE_PUSH_FAILED_UPDATES";
static NSString *const LastDeploymentReportKey = @"CODE_PUSH_LAST_DEPLOYMENT_REPORT";
static NSString *const PendingUpdateKey = @"CODE_PUSH_PENDING_UPDATE";
// These keys are already "namespaced" by the PendingUpdateKey, so
@@ -26,6 +33,8 @@ static NSString *const PendingUpdateIsLoadingKey = @"isLoading";
// These keys are used to inspect/augment the metadata
// that is associated with an update's package.
static NSString *const DeploymentKeyKey = @"deploymentKey";
static NSString *const LabelKey = @"label";
static NSString *const PackageHashKey = @"packageHash";
static NSString *const PackageIsPendingKey = @"isPending";
@@ -54,6 +63,7 @@ static NSString *const PackageIsPendingKey = @"isPending";
if (error || !packageFile) {
NSLog(logMessageFormat, binaryJsBundleUrl);
isRunningBinaryVersion = YES;
return binaryJsBundleUrl;
}
@@ -65,15 +75,17 @@ static NSString *const PackageIsPendingKey = @"isPending";
NSDictionary *currentPackageMetadata = [CodePushPackage getCurrentPackage:&error];
if (error || !currentPackageMetadata) {
NSLog(logMessageFormat, binaryJsBundleUrl);
isRunningBinaryVersion = YES;
return binaryJsBundleUrl;
}
NSString *packageAppVersion = [currentPackageMetadata objectForKey:@"appVersion"];
if ([binaryDate compare:packageDate] == NSOrderedAscending && [binaryAppVersion isEqualToString:packageAppVersion]) {
if ([binaryDate compare:packageDate] == NSOrderedAscending && ([CodePush isUsingTestConfiguration] ||[binaryAppVersion isEqualToString:packageAppVersion])) {
// Return package file because it is newer than the app store binary's JS bundle
NSURL *packageUrl = [[NSURL alloc] initFileURLWithPath:packageFile];
NSLog(logMessageFormat, packageUrl);
isRunningBinaryVersion = NO;
return packageUrl;
} else {
#ifndef DEBUG
@@ -81,6 +93,7 @@ static NSString *const PackageIsPendingKey = @"isPending";
#endif
NSLog(logMessageFormat, binaryJsBundleUrl);
isRunningBinaryVersion = YES;
return binaryJsBundleUrl;
}
}
@@ -148,6 +161,19 @@ static NSString *const PackageIsPendingKey = @"isPending";
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (NSString *)getPackageStatusReportIdentifier:(NSDictionary *)package
{
// Because deploymentKeys can be dynamically switched, we use a
// combination of the deploymentKey and label as the packageIdentifier.
NSString *deploymentKey = [package objectForKey:DeploymentKeyKey];
NSString *label = [package objectForKey:LabelKey];
if (deploymentKey && label) {
return [[deploymentKey stringByAppendingString:@":"] stringByAppendingString:label];
} else {
return nil;
}
}
- (instancetype)init
{
self = [super init];
@@ -175,6 +201,7 @@ static NSString *const PackageIsPendingKey = @"isPending";
// Pending update was initialized, but notifyApplicationReady was not called.
// Therefore, deduce that it is a broken update and rollback.
NSLog(@"Update did not finish loading the last time, rolling back to a previous version.");
needToReportRollback = YES;
[self rollbackPackage];
} else {
// Mark that we tried to initialize the new update, so that if it crashes,
@@ -185,6 +212,13 @@ static NSString *const PackageIsPendingKey = @"isPending";
}
}
- (BOOL)isDeploymentStatusNotYetReported:(NSString *)appVersionOrPackageIdentifier
{
NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
NSString *sentStatusReportIdentifier = [preferences objectForKey:LastDeploymentReportKey];
return sentStatusReportIdentifier == nil || ![sentStatusReportIdentifier isEqualToString:appVersionOrPackageIdentifier];
}
/*
* This method checks to see whether a specific package hash
* has previously failed installation.
@@ -193,7 +227,25 @@ static NSString *const PackageIsPendingKey = @"isPending";
{
NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
NSMutableArray *failedUpdates = [preferences objectForKey:FailedUpdatesKey];
return (failedUpdates != nil && [failedUpdates containsObject:packageHash]);
if (failedUpdates == nil || packageHash == nil) {
return NO;
} else {
for (NSDictionary *failedPackage in failedUpdates)
{
// Type check is needed for backwards compatibility, where we used to just store
// the failed package hash instead of the metadata. This only impacts "dev"
// scenarios, since in production we clear out old information whenever a new
// binary is applied.
if ([failedPackage isKindOfClass:[NSDictionary class]]) {
NSString *failedPackageHash = [failedPackage objectForKey:PackageHashKey];
if ([packageHash isEqualToString:failedPackageHash]) {
return YES;
}
}
}
return NO;
}
}
/*
@@ -237,6 +289,13 @@ static NSString *const PackageIsPendingKey = @"isPending";
});
}
- (void)recordDeploymentStatusReported:(NSString *)appVersionOrPackageIdentifier
{
NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
[preferences setValue:appVersionOrPackageIdentifier forKey:LastDeploymentReportKey];
[preferences synchronize];
}
/*
* This method is used when an update has failed installation
* and the app needs to be rolled back to the previous bundle.
@@ -247,10 +306,10 @@ static NSString *const PackageIsPendingKey = @"isPending";
- (void)rollbackPackage
{
NSError *error;
NSString *packageHash = [CodePushPackage getCurrentPackageHash:&error];
NSDictionary *failedPackage = [CodePushPackage getCurrentPackage:&error];
// Write the current package's hash to the "failed list"
[self saveFailedUpdate:packageHash];
// Write the current package's metadata to the "failed list"
[self saveFailedUpdate:failedPackage];
// Rollback to the previous version and de-register the new update
[CodePushPackage rollbackPackage];
@@ -263,7 +322,7 @@ static NSString *const PackageIsPendingKey = @"isPending";
* to store its hash so that it can be ignored on future
* attempts to check the server for an update.
*/
- (void)saveFailedUpdate:(NSString *)packageHash
- (void)saveFailedUpdate:(NSDictionary *)failedPackage
{
NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
NSMutableArray *failedUpdates = [preferences objectForKey:FailedUpdatesKey];
@@ -275,7 +334,7 @@ static NSString *const PackageIsPendingKey = @"isPending";
failedUpdates = [failedUpdates mutableCopy];
}
[failedUpdates addObject:packageHash];
[failedUpdates addObject:failedPackage];
[preferences setObject:failedUpdates forKey:FailedUpdatesKey];
[preferences synchronize];
}
@@ -467,6 +526,54 @@ RCT_EXPORT_METHOD(notifyApplicationReady:(RCTPromiseResolveBlock)resolve
resolve([NSNull null]);
}
/*
* This method is checks if a new status update exists (new version was installed,
* or an update failed) and return its details (version label, status).
*/
RCT_EXPORT_METHOD(getNewStatusReport:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
if (needToReportRollback) {
// Check if there was a rollback that was not yet reported
needToReportRollback = NO;
NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
NSMutableArray *failedUpdates = [preferences objectForKey:FailedUpdatesKey];
if (failedUpdates) {
NSDictionary *lastFailedPackage = [failedUpdates lastObject];
if (lastFailedPackage) {
NSString *lastFailedPackageIdentifier = [self getPackageStatusReportIdentifier:lastFailedPackage];
if (lastFailedPackageIdentifier && [self isDeploymentStatusNotYetReported:lastFailedPackageIdentifier]) {
[self recordDeploymentStatusReported:lastFailedPackageIdentifier];
resolve(@{ @"package": lastFailedPackage, @"status": DeploymentFailed });
return;
}
}
}
} else if (_isFirstRunAfterUpdate) {
// Check if the current CodePush package has been reported
NSError *error;
NSDictionary *currentPackage = [CodePushPackage getCurrentPackage:&error];
if (!error && currentPackage) {
NSString *currentPackageIdentifier = [self getPackageStatusReportIdentifier:currentPackage];
if (currentPackageIdentifier && [self isDeploymentStatusNotYetReported:currentPackageIdentifier]) {
[self recordDeploymentStatusReported:currentPackageIdentifier];
resolve(@{ @"package": currentPackage, @"status": DeploymentSucceeded });
return;
}
}
} else if (isRunningBinaryVersion || [_bridge.bundleURL.scheme hasPrefix:@"http"]) {
// Check if the current appVersion has been reported.
NSString *appVersion = [[CodePushConfig current] appVersion];
if ([self isDeploymentStatusNotYetReported:appVersion]) {
[self recordDeploymentStatusReported:appVersion];
resolve(@{ @"appVersion": appVersion });
return;
}
}
resolve([NSNull null]);
}
/*
* This method is the native side of the CodePush.restartApp() method.
*/

View File

@@ -1,4 +1,5 @@
#import "CodePush.h"
#import <UIKit/UIKit.h>
@implementation CodePushConfig {
NSMutableDictionary *_configDictionary;
@@ -8,6 +9,7 @@ static CodePushConfig *_currentConfig;
static NSString * const AppVersionConfigKey = @"appVersion";
static NSString * const BuildVdersionConfigKey = @"buildVersion";
static NSString * const ClientUniqueIDConfigKey = @"clientUniqueId";
static NSString * const DeploymentKeyConfigKey = @"deploymentKey";
static NSString * const ServerURLConfigKey = @"serverUrl";
@@ -31,6 +33,14 @@ static NSString * const ServerURLConfigKey = @"serverUrl";
NSString *deploymentKey = [infoDictionary objectForKey:@"CodePushDeploymentKey"];
NSString *serverURL = [infoDictionary objectForKey:@"CodePushServerURL"];
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
NSString *clientUniqueId = [userDefaults stringForKey:ClientUniqueIDConfigKey];
if (clientUniqueId == nil) {
clientUniqueId = [[[UIDevice currentDevice] identifierForVendor] UUIDString];
[userDefaults setObject:clientUniqueId forKey:ClientUniqueIDConfigKey];
[userDefaults synchronize];
}
if (!serverURL) {
serverURL = @"https://codepush.azurewebsites.net/";
}
@@ -39,6 +49,7 @@ static NSString * const ServerURLConfigKey = @"serverUrl";
appVersion,AppVersionConfigKey,
buildVersion,BuildVdersionConfigKey,
serverURL,ServerURLConfigKey,
clientUniqueId,ClientUniqueIDConfigKey,
deploymentKey,DeploymentKeyConfigKey,
nil];
@@ -70,6 +81,11 @@ static NSString * const ServerURLConfigKey = @"serverUrl";
return [_configDictionary objectForKey:ServerURLConfigKey];
}
- (NSString *)clientUniqueId
{
return [_configDictionary objectForKey:ClientUniqueIDConfigKey];
}
- (void)setDeploymentKey:(NSString *)deploymentKey
{
[_configDictionary setValue:deploymentKey forKey:DeploymentKeyConfigKey];

View File

@@ -24,7 +24,7 @@ let FirstUpdateTest = createTestCaseComponent(
},
async () => {
let update = await CodePush.checkForUpdate();
assert.equal(JSON.stringify(update), JSON.stringify({ ...serverPackage, ...PackageMixins.remote, failedInstall: false }), "checkForUpdate did not return the update from the server");
assert.equal(JSON.stringify(update), JSON.stringify({ ...serverPackage, ...PackageMixins.remote(), failedInstall: false }), "checkForUpdate did not return the update from the server");
}
);

View File

@@ -23,7 +23,7 @@ let NewUpdateTest = createTestCaseComponent(
},
async () => {
let update = await CodePush.checkForUpdate();
assert.equal(JSON.stringify(update), JSON.stringify({ ...serverPackage, ...PackageMixins.remote, failedInstall: false }), "checkForUpdate did not return the update from the server");
assert.equal(JSON.stringify(update), JSON.stringify({ ...serverPackage, ...PackageMixins.remote(), failedInstall: false }), "checkForUpdate did not return the update from the server");
}
);

View File

@@ -25,7 +25,7 @@ let SwitchDeploymentKeyTest = createTestCaseComponent(
},
async () => {
let update = await CodePush.checkForUpdate(deploymentKey);
assert.equal(JSON.stringify(update), JSON.stringify({ ...serverPackage, ...PackageMixins.remote, failedInstall: false }), "checkForUpdate did not return the update from the server");
assert.equal(JSON.stringify(update), JSON.stringify({ ...serverPackage, ...PackageMixins.remote(), failedInstall: false, deploymentKey }), "checkForUpdate did not return the update from the server");
}
);

View File

@@ -28,7 +28,7 @@ let DownloadProgressTest = createTestCaseComponent(
"should successfully download all the bytes contained in the test packages",
() => {
testPackages.forEach((aPackage, index) => {
testPackages[index] = Object.assign(aPackage, PackageMixins.remote);
testPackages[index] = Object.assign(aPackage, PackageMixins.remote());
});
return Promise.resolve();
},

View File

@@ -27,7 +27,7 @@ let RollbackTest = React.createClass({
await NativeCodePush.downloadAndReplaceCurrentBundle("http://localhost:8081/CodePushDemoAppTests/InstallUpdateTests/resources/RollbackTestBundleV1Pass.includeRequire.runModule.bundle?platform=ios&dev=true");
}
remotePackage = Object.assign(remotePackage, PackageMixins.remote);
remotePackage = Object.assign(remotePackage, PackageMixins.remote());
let localPackage = await remotePackage.download();
return await localPackage.install(NativeCodePush.codePushInstallModeImmediate);

View File

@@ -1,4 +1,5 @@
export default {
deploymentKey: "myKey123",
description: "Angry flappy birds",
appVersion: "1.5.0",
label: "2.4.0",

View File

@@ -21,7 +21,7 @@ let InstallModeImmediateTest = createTestCaseComponent(
remotePackage.downloadUrl = "http://localhost:8081/CodePushDemoAppTests/InstallUpdateTests/resources/PassInstallModeImmediateTest.includeRequire.runModule.bundle?platform=ios&dev=true"
}
remotePackage = Object.assign(remotePackage, PackageMixins.remote);
remotePackage = Object.assign(remotePackage, PackageMixins.remote());
},
async () => {
let localPackage = await remotePackage.download();

View File

@@ -22,7 +22,7 @@ let InstallModeOnNextRestartTest = createTestCaseComponent(
remotePackage.downloadUrl = "http://localhost:8081/CodePushDemoAppTests/InstallUpdateTests/resources/PassInstallModeOnNextRestartTest.includeRequire.runModule.bundle?platform=ios&dev=true"
}
remotePackage = Object.assign(remotePackage, PackageMixins.remote);
remotePackage = Object.assign(remotePackage, PackageMixins.remote());
},
async () => {
let localPackage = await remotePackage.download();

View File

@@ -21,7 +21,7 @@ let InstallModeOnNextResumeTest = createTestCaseComponent(
remotePackage.downloadUrl = "http://localhost:8081/CodePushDemoAppTests/InstallUpdateTests/resources/PassInstallModeOnNextResumeTest.includeRequire.runModule.bundle?platform=ios&dev=true"
}
remotePackage = Object.assign(remotePackage, PackageMixins.remote);
remotePackage = Object.assign(remotePackage, PackageMixins.remote());
},
async () => {
let localPackage = await remotePackage.download()

View File

@@ -21,7 +21,7 @@ let IsFailedUpdateTest = createTestCaseComponent(
remotePackage.downloadUrl = "http://localhost:8081/CodePushDemoAppTests/InstallUpdateTests/resources/IsFailedUpdateTestBundleV1.includeRequire.runModule.bundle?platform=ios&dev=true"
}
remotePackage = Object.assign(remotePackage, PackageMixins.remote);
remotePackage = Object.assign(remotePackage, PackageMixins.remote());
},
async () => {
let localPackage = await remotePackage.download();

View File

@@ -20,7 +20,7 @@ let IsFirstRunTest = createTestCaseComponent(
remotePackage.downloadUrl = "http://localhost:8081/CodePushDemoAppTests/InstallUpdateTests/resources/CheckIsFirstRunAndPassTest.includeRequire.runModule.bundle?platform=ios&dev=true"
}
remotePackage = Object.assign(remotePackage, PackageMixins.remote);
remotePackage = Object.assign(remotePackage, PackageMixins.remote());
},
async () => {
let localPackage = await remotePackage.download();

View File

@@ -21,7 +21,7 @@ let IsPendingTest = createTestCaseComponent(
remotePackage.downloadUrl = "http://localhost:8081/CodePushDemoAppTests/InstallUpdateTests/resources/CheckIsFirstRunAndPassTest.includeRequire.runModule.bundle?platform=ios&dev=true"
}
remotePackage = Object.assign(remotePackage, PackageMixins.remote);
remotePackage = Object.assign(remotePackage, PackageMixins.remote());
},
async () => {
let localPackage = await remotePackage.download();

View File

@@ -21,7 +21,7 @@ let NotifyApplicationReadyTest = createTestCaseComponent(
remotePackage.downloadUrl = "http://localhost:8081/CodePushDemoAppTests/InstallUpdateTests/resources/NotifyApplicationReadyAndRestart.includeRequire.runModule.bundle?platform=ios&dev=true"
}
remotePackage = Object.assign(remotePackage, PackageMixins.remote);
remotePackage = Object.assign(remotePackage, PackageMixins.remote());
},
async () => {
let localPackage = await remotePackage.download()

View File

@@ -21,7 +21,7 @@ let RollbackTest = createTestCaseComponent(
remotePackage.downloadUrl = "http://localhost:8081/CodePushDemoAppTests/InstallUpdateTests/resources/RollbackTestBundleV1.includeRequire.runModule.bundle?platform=ios&dev=true"
}
remotePackage = Object.assign(remotePackage, PackageMixins.remote);
remotePackage = Object.assign(remotePackage, PackageMixins.remote());
},
async () => {
let localPackage = await remotePackage.download()

View File

@@ -13,6 +13,16 @@ function createMockAcquisitionSdk(serverPackage, localPackage, expectedDeploymen
callback(/*err:*/ null, serverPackage);
};
AcquisitionManager.prototype.reportStatusDeploy = (package, status, callback) => {
// No-op and return success.
callback(null, null);
};
AcquisitionManager.prototype.reportStatusDownload = (package, callback) => {
// No-op and return success.
callback(null, null);
};
return AcquisitionManager;
}

View File

@@ -81,6 +81,10 @@ let CodePushDemoApp = React.createClass({
}
},
componentDidMount() {
CodePush.notifyApplicationReady();
},
getInitialState() {
return { };
},

View File

@@ -23,7 +23,9 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.AsyncTask;
import android.provider.Settings;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@@ -38,22 +40,32 @@ import java.util.zip.ZipFile;
public class CodePush {
private static boolean needToReportRollback = false;
private static boolean isRunningBinaryVersion = false;
private static boolean testConfigurationFlag = false;
private boolean didUpdate = false;
private String assetsBundleFileName;
private final String ASSETS_BUNDLE_PREFIX = "assets://";
private final String BINARY_MODIFIED_TIME_KEY = "binaryModifiedTime";
private final String CODE_PUSH_PREFERENCES = "CodePush";
private final String DEPLOYMENT_FAILED_STATUS = "DeploymentFailed";
private final String DEPLOYMENT_KEY_KEY = "deploymentKey";
private final String DEPLOYMENT_SUCCEEDED_STATUS = "DeploymentSucceeded";
private final String DOWNLOAD_PROGRESS_EVENT_NAME = "CodePushDownloadProgress";
private final String FAILED_UPDATES_KEY = "CODE_PUSH_FAILED_UPDATES";
private final String PENDING_UPDATE_KEY = "CODE_PUSH_PENDING_UPDATE";
private final String LABEL_KEY = "label";
private final String PACKAGE_HASH_KEY = "packageHash";
private final String PENDING_UPDATE_HASH_KEY = "hash";
private final String PENDING_UPDATE_IS_LOADING_KEY = "isLoading";
private final String ASSETS_BUNDLE_PREFIX = "assets://";
private final String CODE_PUSH_PREFERENCES = "CodePush";
private final String DOWNLOAD_PROGRESS_EVENT_NAME = "CodePushDownloadProgress";
private final String PENDING_UPDATE_KEY = "CODE_PUSH_PENDING_UPDATE";
private final String RESOURCES_BUNDLE = "resources.arsc";
private final String LAST_DEPLOYMENT_REPORT_KEY = "CODE_PUSH_LAST_DEPLOYMENT_REPORT";
// This needs to be kept in sync with https://github.com/facebook/react-native/blob/master/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java#L78
private final String REACT_DEV_BUNDLE_CACHE_FILE_NAME = "ReactNativeDevBundle.js";
private final String BINARY_MODIFIED_TIME_KEY = "binaryModifiedTime";
private CodePushPackage codePushPackage;
private CodePushReactPackage codePushReactPackage;
@@ -132,6 +144,7 @@ public class CodePush {
if (packageFilePath == null) {
// There has not been any downloaded updates.
CodePushUtils.logBundleUrl(binaryJsBundleUrl);
isRunningBinaryVersion = true;
return binaryJsBundleUrl;
}
@@ -142,11 +155,12 @@ public class CodePush {
binaryModifiedDateDuringPackageInstall = Long.parseLong(binaryModifiedDateDuringPackageInstallString);
}
String pacakgeAppVersion = CodePushUtils.tryGetString(packageMetadata, "appVersion");
String packageAppVersion = CodePushUtils.tryGetString(packageMetadata, "appVersion");
if (binaryModifiedDateDuringPackageInstall != null &&
binaryModifiedDateDuringPackageInstall == binaryResourcesModifiedTime &&
this.appVersion.equals(pacakgeAppVersion)) {
(this.isUsingTestConfiguration() || this.appVersion.equals(packageAppVersion))) {
CodePushUtils.logBundleUrl(packageFilePath);
isRunningBinaryVersion = false;
return packageFilePath;
} else {
// The binary version is newer.
@@ -156,15 +170,44 @@ public class CodePush {
}
CodePushUtils.logBundleUrl(binaryJsBundleUrl);
isRunningBinaryVersion = true;
return binaryJsBundleUrl;
}
} catch (IOException e) {
throw new CodePushUnknownException("Error in getting current package bundle path", e);
} catch (NumberFormatException e) {
throw new CodePushUnknownException("Error in reading binary modified date from package metadata", e);
}
}
private String getPackageStatusReportIdentifier(WritableMap updatePackage) {
// Because deploymentKeys can be dynamically switched, we use a
// combination of the deploymentKey and label as the packageIdentifier.
String deploymentKey = CodePushUtils.tryGetString(updatePackage, DEPLOYMENT_KEY_KEY);
String label = CodePushUtils.tryGetString(updatePackage, LABEL_KEY);
if (deploymentKey != null && label != null) {
return deploymentKey + ":" + label;
} else {
return null;
}
}
private JSONArray getFailedUpdates() {
SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0);
String failedUpdatesString = settings.getString(FAILED_UPDATES_KEY, null);
if (failedUpdatesString == null) {
return new JSONArray();
}
try {
JSONArray failedUpdates = new JSONArray(failedUpdatesString);
return failedUpdates;
} catch (JSONException e) {
// Unrecognized data format, clear and replace with expected format.
JSONArray emptyArray = new JSONArray();
settings.edit().putString(FAILED_UPDATES_KEY, emptyArray.toString()).commit();
return emptyArray;
}
}
private JSONObject getPendingUpdate() {
SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0);
String pendingUpdateString = settings.getString(PENDING_UPDATE_KEY, null);
@@ -200,6 +243,7 @@ public class CodePush {
// Pending update was initialized, but notifyApplicationReady was not called.
// Therefore, deduce that it is a broken update and rollback.
CodePushUtils.log("Update did not finish loading the last time, rolling back to a previous version.");
needToReportRollback = true;
rollbackPackage();
} else {
// Clear the React dev bundle cache so that new updates can be loaded.
@@ -217,22 +261,35 @@ public class CodePush {
}
}
}
private boolean isFailedHash(String packageHash) {
private boolean isDeploymentStatusNotYetReported(String appVersionOrPackageIdentifier) {
SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0);
String failedUpdatesString = settings.getString(FAILED_UPDATES_KEY, null);
if (failedUpdatesString == null) {
return false;
String lastDeploymentReportIdentifier = settings.getString(LAST_DEPLOYMENT_REPORT_KEY, null);
if (lastDeploymentReportIdentifier == null) {
return true;
} else {
return !lastDeploymentReportIdentifier.equals(appVersionOrPackageIdentifier);
}
}
private boolean isFailedHash(String packageHash) {
JSONArray failedUpdates = getFailedUpdates();
if (packageHash != null) {
for (int i = 0; i < failedUpdates.length(); i++) {
JSONObject failedPackage = null;
try {
failedPackage = failedUpdates.getJSONObject(i);
String failedPackageHash = failedPackage.getString(PACKAGE_HASH_KEY);
if (packageHash.equals(failedPackageHash)) {
return true;
}
} catch (JSONException e) {
throw new CodePushUnknownException("Unable to read failedUpdates data stored in SharedPreferences.", e);
}
}
}
try {
JSONObject failedUpdates = new JSONObject(failedUpdatesString);
return failedUpdates.has(packageHash);
} catch (JSONException e) {
// Should not happen.
throw new CodePushUnknownException("Unable to parse failed updates information " +
failedUpdatesString + " stored in SharedPreferences", e);
}
return false;
}
private boolean isPendingUpdate(String packageHash) {
@@ -249,6 +306,11 @@ public class CodePush {
}
}
private void recordDeploymentStatusReported(String appVersionOrPackageIdentifier) {
SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0);
settings.edit().putString(LAST_DEPLOYMENT_REPORT_KEY, appVersionOrPackageIdentifier).commit();
}
private void removeFailedUpdates() {
SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0);
settings.edit().remove(FAILED_UPDATES_KEY).commit();
@@ -260,45 +322,31 @@ public class CodePush {
}
private void rollbackPackage() {
try {
String packageHash = codePushPackage.getCurrentPackageHash();
saveFailedUpdate(packageHash);
} catch (IOException e) {
throw new CodePushUnknownException("Attempted a rollback without having a current downloaded package", e);
}
try {
codePushPackage.rollbackPackage();
} catch (IOException e) {
throw new CodePushUnknownException("Error in rolling back package", e);
}
WritableMap failedPackage = codePushPackage.getCurrentPackage();
saveFailedUpdate(failedPackage);
codePushPackage.rollbackPackage();
removePendingUpdate();
}
private void saveFailedUpdate(String packageHash) {
private void saveFailedUpdate(WritableMap failedPackage) {
SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0);
String failedUpdatesString = settings.getString(FAILED_UPDATES_KEY, null);
JSONObject failedUpdates;
JSONArray failedUpdates;
if (failedUpdatesString == null) {
failedUpdates = new JSONObject();
failedUpdates = new JSONArray();
} else {
try {
failedUpdates = new JSONObject(failedUpdatesString);
failedUpdates = new JSONArray(failedUpdatesString);
} catch (JSONException e) {
// Should not happen.
throw new CodePushMalformedDataException("Unable to parse failed updates information " +
failedUpdatesString + " stored in SharedPreferences", e);
}
}
try {
failedUpdates.put(packageHash, true);
settings.edit().putString(FAILED_UPDATES_KEY, failedUpdates.toString()).commit();
} catch (JSONException e) {
// Should not happen unless the packageHash is null.
throw new CodePushUnknownException("Unable to save package hash " +
packageHash + " as a failed update", e);
}
JSONObject failedPackageJSON = CodePushUtils.convertReadableToJsonObject(failedPackage);
failedUpdates.put(failedPackageJSON);
settings.edit().putString(FAILED_UPDATES_KEY, failedUpdates.toString()).commit();
}
private void savePendingUpdate(String packageHash, boolean isLoading) {
@@ -377,6 +425,9 @@ public class CodePush {
configMap.putInt("buildVersion", buildVersion);
configMap.putString("deploymentKey", deploymentKey);
configMap.putString("serverUrl", serverUrl);
configMap.putString("clientUniqueId",
Settings.Secure.getString(mainActivity.getContentResolver(),
android.provider.Settings.Secure.ANDROID_ID));
promise.resolve(configMap);
}
@@ -385,30 +436,76 @@ public class CodePush {
AsyncTask asyncTask = new AsyncTask() {
@Override
protected Void doInBackground(Object... params) {
try {
WritableMap currentPackage = codePushPackage.getCurrentPackage();
WritableMap currentPackage = codePushPackage.getCurrentPackage();
Boolean isPendingUpdate = false;
Boolean isPendingUpdate = false;
if (currentPackage.hasKey(codePushPackage.PACKAGE_HASH_KEY)) {
String currentHash = currentPackage.getString(codePushPackage.PACKAGE_HASH_KEY);
isPendingUpdate = CodePush.this.isPendingUpdate(currentHash);
}
currentPackage.putBoolean("isPending", isPendingUpdate);
promise.resolve(currentPackage);
} catch (IOException e) {
e.printStackTrace();
promise.reject(e.getMessage());
if (currentPackage.hasKey(codePushPackage.PACKAGE_HASH_KEY)) {
String currentHash = currentPackage.getString(codePushPackage.PACKAGE_HASH_KEY);
isPendingUpdate = CodePush.this.isPendingUpdate(currentHash);
}
currentPackage.putBoolean("isPending", isPendingUpdate);
promise.resolve(currentPackage);
return null;
}
};
asyncTask.execute();
}
@ReactMethod
public void getNewStatusReport(Promise promise) {
if (needToReportRollback) {
// Check if there was a rollback that was not yet reported
needToReportRollback = false;
JSONArray failedUpdates = getFailedUpdates();
if (failedUpdates != null && failedUpdates.length() > 0) {
try {
JSONObject lastFailedPackageJSON = failedUpdates.getJSONObject(failedUpdates.length() - 1);
WritableMap lastFailedPackage = CodePushUtils.convertJsonObjectToWriteable(lastFailedPackageJSON);
String lastFailedPackageIdentifier = getPackageStatusReportIdentifier(lastFailedPackage);
if (lastFailedPackage != null && isDeploymentStatusNotYetReported(lastFailedPackageIdentifier)) {
recordDeploymentStatusReported(lastFailedPackageIdentifier);
WritableNativeMap reportMap = new WritableNativeMap();
reportMap.putMap("package", lastFailedPackage);
reportMap.putString("status", DEPLOYMENT_FAILED_STATUS);
promise.resolve(reportMap);
return;
}
} catch (JSONException e) {
throw new CodePushUnknownException("Unable to read failed updates information stored in SharedPreferences.", e);
}
}
} else if (didUpdate) {
// Check if the current CodePush package has been reported
WritableMap currentPackage = codePushPackage.getCurrentPackage();
if (currentPackage != null) {
String currentPackageIdentifier = getPackageStatusReportIdentifier(currentPackage);
if (currentPackageIdentifier != null && isDeploymentStatusNotYetReported(currentPackageIdentifier)) {
recordDeploymentStatusReported(currentPackageIdentifier);
WritableNativeMap reportMap = new WritableNativeMap();
reportMap.putMap("package", currentPackage);
reportMap.putString("status", DEPLOYMENT_SUCCEEDED_STATUS);
promise.resolve(reportMap);
return;
}
}
} else if (isRunningBinaryVersion) {
// Check if the current appVersion has been reported.
String binaryIdentifier = "" + getBinaryResourcesModifiedTime();
if (isDeploymentStatusNotYetReported(binaryIdentifier)) {
recordDeploymentStatusReported(binaryIdentifier);
WritableNativeMap reportMap = new WritableNativeMap();
reportMap.putString("appVersion", appVersion);
promise.resolve(reportMap);
return;
}
}
promise.resolve("");
}
@ReactMethod
public void installUpdate(final ReadableMap updatePackage, final int installMode, final Promise promise) {
AsyncTask asyncTask = new AsyncTask() {
@@ -464,16 +561,11 @@ public class CodePush {
@ReactMethod
public void isFirstRun(String packageHash, Promise promise) {
try {
boolean isFirstRun = didUpdate
&& packageHash != null
&& packageHash.length() > 0
&& packageHash.equals(codePushPackage.getCurrentPackageHash());
promise.resolve(isFirstRun);
} catch (IOException e) {
e.printStackTrace();
promise.reject(e.getMessage());
}
boolean isFirstRun = didUpdate
&& packageHash != null
&& packageHash.length() > 0
&& packageHash.equals(codePushPackage.getCurrentPackageHash());
promise.resolve(isFirstRun);
}
@ReactMethod

View File

@@ -50,20 +50,28 @@ public class CodePushPackage {
return CodePushUtils.appendPathComponent(getCodePushPath(), STATUS_FILE);
}
public WritableMap getCurrentPackageInfo() throws IOException {
public WritableMap getCurrentPackageInfo() {
String statusFilePath = getStatusFilePath();
if (!CodePushUtils.fileAtPathExists(statusFilePath)) {
return new WritableNativeMap();
}
return CodePushUtils.getWritableMapFromFile(statusFilePath);
try {
return CodePushUtils.getWritableMapFromFile(statusFilePath);
} catch (IOException e) {
throw new CodePushUnknownException("Error getting current package info" , e);
}
}
public void updateCurrentPackageInfo(ReadableMap packageInfo) throws IOException {
CodePushUtils.writeReadableMapToFile(packageInfo, getStatusFilePath());
public void updateCurrentPackageInfo(ReadableMap packageInfo) {
try {
CodePushUtils.writeReadableMapToFile(packageInfo, getStatusFilePath());
} catch (IOException e) {
throw new CodePushUnknownException("Error updating current package info" , e);
}
}
public String getCurrentPackageFolderPath() throws IOException {
public String getCurrentPackageFolderPath() {
WritableMap info = getCurrentPackageInfo();
String packageHash = CodePushUtils.tryGetString(info, CURRENT_PACKAGE_KEY);
if (packageHash == null) {
@@ -73,7 +81,7 @@ public class CodePushPackage {
return getPackageFolderPath(packageHash);
}
public String getCurrentPackageBundlePath() throws IOException {
public String getCurrentPackageBundlePath() {
String packageFolder = getCurrentPackageFolderPath();
if (packageFolder == null) {
return null;
@@ -86,17 +94,17 @@ public class CodePushPackage {
return CodePushUtils.appendPathComponent(getCodePushPath(), packageHash);
}
public String getCurrentPackageHash() throws IOException {
public String getCurrentPackageHash() {
WritableMap info = getCurrentPackageInfo();
return CodePushUtils.tryGetString(info, CURRENT_PACKAGE_KEY);
}
public String getPreviousPackageHash() throws IOException {
public String getPreviousPackageHash() {
WritableMap info = getCurrentPackageInfo();
return CodePushUtils.tryGetString(info, PREVIOUS_PACKAGE_KEY);
}
public WritableMap getCurrentPackage() throws IOException {
public WritableMap getCurrentPackage() {
String folderPath = getCurrentPackageFolderPath();
if (folderPath == null) {
return new WritableNativeMap();
@@ -111,7 +119,7 @@ public class CodePushPackage {
}
}
public WritableMap getPackage(String packageHash) throws IOException {
public WritableMap getPackage(String packageHash) {
String folderPath = getPackageFolderPath(packageHash);
String packageFilePath = CodePushUtils.appendPathComponent(folderPath, PACKAGE_FILE_NAME);
try {
@@ -185,7 +193,7 @@ public class CodePushPackage {
updateCurrentPackageInfo(info);
}
public void rollbackPackage() throws IOException {
public void rollbackPackage() {
WritableMap info = getCurrentPackageInfo();
String currentPackageFolderPath = getCurrentPackageFolderPath();
CodePushUtils.deleteDirectoryAtPath(currentPackageFolderPath);

View File

@@ -1,35 +1,39 @@
import { AcquisitionManager as Sdk } from "code-push/script/acquisition-sdk";
import { DeviceEventEmitter } from "react-native";
// This function is used to augment remote and local
// package objects with additional functionality/properties
// beyond what is included in the metadata sent by the server.
module.exports = (NativeCodePush) => {
const remote = {
async download(downloadProgressCallback) {
if (!this.downloadUrl) {
throw new Error("Cannot download an update without a download url");
}
const remote = (reportStatusDownload) => {
return {
async download(downloadProgressCallback) {
if (!this.downloadUrl) {
throw new Error("Cannot download an update without a download url");
}
let downloadProgressSubscription;
if (downloadProgressCallback) {
// Use event subscription to obtain download progress.
downloadProgressSubscription = DeviceEventEmitter.addListener(
"CodePushDownloadProgress",
downloadProgressCallback
);
}
let downloadProgressSubscription;
if (downloadProgressCallback) {
// 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);
return { ...downloadedPackage, ...local };
} finally {
downloadProgressSubscription && downloadProgressSubscription.remove();
}
},
// 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);
reportStatusDownload && reportStatusDownload(this);
return { ...downloadedPackage, ...local };
} finally {
downloadProgressSubscription && downloadProgressSubscription.remove();
}
},
isPending: false // A remote package could never be in a pending state
isPending: false // A remote package could never be in a pending state
};
};
const local = {

View File

@@ -16,7 +16,7 @@
"url": "https://github.com/Microsoft/react-native-code-push"
},
"dependencies": {
"code-push": "^1.1.1-beta",
"code-push": "1.5.1-beta",
"semver": "^5.1.0"
},
"devDependencies": {

View File

@@ -1,8 +1,8 @@
module.exports = {
async request(verb, url, body, callback) {
if (typeof body === "function") {
callback = body;
body = null;
async request(verb, url, requestBody, callback) {
if (typeof requestBody === "function") {
callback = requestBody;
requestBody = null;
}
var headers = {
@@ -10,15 +10,15 @@ module.exports = {
"Content-Type": "application/json"
};
if (body && typeof body === "object") {
body = JSON.stringify(body);
if (requestBody && typeof requestBody === "object") {
requestBody = JSON.stringify(requestBody);
}
try {
const response = await fetch(url, {
method: verb,
method: getHttpMethodName(verb),
headers: headers,
body: body
body: requestBody
});
const statusCode = response.status;
@@ -28,4 +28,20 @@ module.exports = {
callback(err);
}
}
};
};
function getHttpMethodName(verb) {
// Note: This should stay in sync with the enum definition in
// https://github.com/Microsoft/code-push/blob/master/sdk/script/acquisition-sdk.ts#L6
return [
"GET",
"HEAD",
"POST",
"PUT",
"DELETE",
"TRACE",
"OPTIONS",
"CONNECT",
"PATCH"
][verb];
}