diff --git a/CodePush.js b/CodePush.js index 7baa35b..8d8244a 100644 --- a/CodePush.js +++ b/CodePush.js @@ -62,6 +62,7 @@ async function checkForUpdate(deploymentKey = null) { } else { const remotePackage = { ...update, ...PackageMixins.remote }; remotePackage.failedInstall = await NativeCodePush.isFailedUpdate(remotePackage.packageHash); + remotePackage.deploymentKey = deploymentKey || nativeConfig.deploymentKey; return remotePackage; } } @@ -102,6 +103,18 @@ function getPromisifiedSdk(requestFetchAdapter, config) { }); }; + sdk.reportStatus = (package, status) => { + return new Promise((resolve, reject) => { + module.exports.AcquisitionSdk.prototype.reportStatusDeploy.call(sdk, package, status, (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + }; + return sdk; } @@ -110,6 +123,23 @@ 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) { + config.appVersion = statusReport.appVersion; + const sdk = getPromisifiedSdk(requestFetchAdapter, config); + sdk.reportStatus(); + } else { + config.deploymentKey = statusReport.package.deploymentKey; + const sdk = getPromisifiedSdk(requestFetchAdapter, config); + sdk.reportStatus(statusReport.package, statusReport.status); + } + } +} + function restartApp(onlyIfUpdateIsPending = false) { NativeCodePush.restartApp(onlyIfUpdateIsPending); } @@ -269,7 +299,7 @@ const CodePush = { getConfiguration, getCurrentPackage, log, - notifyApplicationReady: NativeCodePush.notifyApplicationReady, + notifyApplicationReady, restartApp, setUpTestDependencies, sync, diff --git a/CodePush.m b/CodePush.m index bba73ed..b886eea 100644 --- a/CodePush.m +++ b/CodePush.m @@ -15,9 +15,14 @@ RCT_EXPORT_MODULE() static BOOL testConfigurationFlag = NO; +// These constants represent valid deployment statuses +static NSString *const DeploymentSucceeded = @"DeploymentSucceeded"; +static NSString *const DeploymentFailed = @"DeploymentFailed"; + // These keys represent the names we use to store data in NSUserDefaults static NSString *const FailedUpdatesKey = @"CODE_PUSH_FAILED_UPDATES"; static NSString *const PendingUpdateKey = @"CODE_PUSH_PENDING_UPDATE"; +static NSString *const StatusReportsKey = @"CODE_PUSH_STATUS_REPORTS"; // These keys are already "namespaced" by the PendingUpdateKey, so // their values don't need to be obfuscated to prevent collision with app data @@ -26,6 +31,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"; @@ -70,7 +77,7 @@ static NSString *const PackageIsPendingKey = @"isPending"; 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); @@ -148,6 +155,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]; @@ -185,6 +205,13 @@ static NSString *const PackageIsPendingKey = @"isPending"; } } +- (BOOL)isDeploymentStatusNotYetReported:(NSString *)appVersionOrPackageIdentifier +{ + NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults]; + NSDictionary *sentStatusReports = [preferences objectForKey:StatusReportsKey]; + return sentStatusReports == nil || [sentStatusReports objectForKey:appVersionOrPackageIdentifier] == nil; +} + /* * This method checks to see whether a specific package hash * has previously failed installation. @@ -193,7 +220,25 @@ static NSString *const PackageIsPendingKey = @"isPending"; { NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults]; NSMutableArray *failedUpdates = [preferences objectForKey:FailedUpdatesKey]; - return (failedUpdates != nil && [failedUpdates containsObject:packageHash]); + if (failedUpdates == 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 +282,22 @@ static NSString *const PackageIsPendingKey = @"isPending"; }); } +- (void)recordDeploymentStatusReported:(NSString *)appVersionOrPackageIdentifier + status:(NSString *)status +{ + NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults]; + NSMutableDictionary *sentStatusReports = [preferences objectForKey:StatusReportsKey]; + if (sentStatusReports == nil) { + sentStatusReports = [NSMutableDictionary dictionary]; + } else { + sentStatusReports = [sentStatusReports mutableCopy]; + } + + [sentStatusReports setValue:status forKey:appVersionOrPackageIdentifier]; + [preferences setValue:sentStatusReports forKey:StatusReportsKey]; + [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 +308,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 +324,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 +336,7 @@ static NSString *const PackageIsPendingKey = @"isPending"; failedUpdates = [failedUpdates mutableCopy]; } - [failedUpdates addObject:packageHash]; + [failedUpdates addObject:failedPackage]; [preferences setObject:failedUpdates forKey:FailedUpdatesKey]; [preferences synchronize]; } @@ -467,6 +528,55 @@ 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) +{ + // Check if the current appVersion has been reported. + NSString *appVersion = [[CodePushConfig current] appVersion]; + if ([self isDeploymentStatusNotYetReported:appVersion]) { + [self recordDeploymentStatusReported:appVersion + status:DeploymentSucceeded]; + resolve(@{ @"appVersion": appVersion }); + return; + } + + // Check if there was a rollback that was not yet reported + 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 + status:DeploymentFailed]; + resolve(@{ @"package": lastFailedPackage, @"status": DeploymentFailed }); + return; + } + } + } + + // Check if the current CodePush package has been reported + NSError *error; + NSDictionary* currentPackage = [CodePushPackage getCurrentPackage:&error]; + if (currentPackage) { + NSString* currentPackageIdentifier = [self getPackageStatusReportIdentifier:currentPackage]; + if (currentPackageIdentifier && [self isDeploymentStatusNotYetReported:currentPackageIdentifier]) { + [self recordDeploymentStatusReported:currentPackageIdentifier + status:DeploymentSucceeded]; + resolve(@{ @"package": currentPackage, @"status": DeploymentSucceeded }); + return; + } + } + + resolve([NSNull null]); + return; +} + /* * This method is the native side of the CodePush.restartApp() method. */ diff --git a/CodePushConfig.m b/CodePushConfig.m index 0de94de..31884fc 100644 --- a/CodePushConfig.m +++ b/CodePushConfig.m @@ -1,4 +1,5 @@ #import "CodePush.h" +#import @implementation CodePushConfig { NSMutableDictionary *_configDictionary; @@ -10,6 +11,7 @@ static NSString * const AppVersionConfigKey = @"appVersion"; static NSString * const BuildVdersionConfigKey = @"buildVersion"; static NSString * const DeploymentKeyConfigKey = @"deploymentKey"; static NSString * const ServerURLConfigKey = @"serverUrl"; +static NSString * const ClientUniqueIDConfigKey = @"clientUniqueId"; + (instancetype)current { @@ -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/"; } @@ -40,6 +50,7 @@ static NSString * const ServerURLConfigKey = @"serverUrl"; buildVersion,BuildVdersionConfigKey, serverURL,ServerURLConfigKey, deploymentKey,DeploymentKeyConfigKey, + clientUniqueId,ClientUniqueIDConfigKey, nil]; return self; @@ -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]; diff --git a/Examples/CodePushDemoApp/CodePushDemoAppTests/CheckForUpdateTests/testcases/SwitchDeploymentKeyTest.js b/Examples/CodePushDemoApp/CodePushDemoAppTests/CheckForUpdateTests/testcases/SwitchDeploymentKeyTest.js index 25e9568..7427d72 100644 --- a/Examples/CodePushDemoApp/CodePushDemoAppTests/CheckForUpdateTests/testcases/SwitchDeploymentKeyTest.js +++ b/Examples/CodePushDemoApp/CodePushDemoAppTests/CheckForUpdateTests/testcases/SwitchDeploymentKeyTest.js @@ -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"); } ); diff --git a/Examples/CodePushDemoApp/CodePushDemoAppTests/InstallUpdateTests/resources/remotePackage.js b/Examples/CodePushDemoApp/CodePushDemoAppTests/InstallUpdateTests/resources/remotePackage.js index 5d3e57a..d974260 100644 --- a/Examples/CodePushDemoApp/CodePushDemoAppTests/InstallUpdateTests/resources/remotePackage.js +++ b/Examples/CodePushDemoApp/CodePushDemoAppTests/InstallUpdateTests/resources/remotePackage.js @@ -1,4 +1,5 @@ export default { + deploymentKey: "myKey123", description: "Angry flappy birds", appVersion: "1.5.0", label: "2.4.0", diff --git a/Examples/CodePushDemoApp/CodePushDemoAppTests/utils/mockAcquisitionSdk.js b/Examples/CodePushDemoApp/CodePushDemoAppTests/utils/mockAcquisitionSdk.js index 253d497..aebc467 100644 --- a/Examples/CodePushDemoApp/CodePushDemoAppTests/utils/mockAcquisitionSdk.js +++ b/Examples/CodePushDemoApp/CodePushDemoAppTests/utils/mockAcquisitionSdk.js @@ -13,6 +13,11 @@ function createMockAcquisitionSdk(serverPackage, localPackage, expectedDeploymen callback(/*err:*/ null, serverPackage); }; + AcquisitionManager.prototype.reportStatus = (status, message, callback) => { + // No-op and return success. + callback(null, null); + }; + return AcquisitionManager; } diff --git a/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java b/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java index a9743d8..ae74487 100644 --- a/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java +++ b/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java @@ -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; @@ -43,17 +45,24 @@ public class CodePush { 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 STATUS_REPORTS_KEY = "CODE_PUSH_STATUS_REPORTS"; + // 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; @@ -145,7 +154,7 @@ public class CodePush { String pacakgeAppVersion = CodePushUtils.tryGetString(packageMetadata, "appVersion"); if (binaryModifiedDateDuringPackageInstall != null && binaryModifiedDateDuringPackageInstall == binaryResourcesModifiedTime && - this.appVersion.equals(pacakgeAppVersion)) { + (this.isUsingTestConfiguration() || this.appVersion.equals(pacakgeAppVersion))) { CodePushUtils.logBundleUrl(packageFilePath); return packageFilePath; } else { @@ -165,6 +174,36 @@ public class CodePush { } } + 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); @@ -217,22 +256,39 @@ 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 sentStatusReportsString = settings.getString(STATUS_REPORTS_KEY, null); + if (sentStatusReportsString == null) { + return true; + } else { + try { + JSONObject sentStatusReports = new JSONObject(sentStatusReportsString); + return !sentStatusReports.has(appVersionOrPackageIdentifier); + } catch (JSONException e) { + throw new CodePushUnknownException("Unable to parse sent status reports information " + + sentStatusReportsString + " stored in SharedPreferences.", e); + } + } + } + + private boolean isFailedHash(String packageHash) { + JSONArray failedUpdates = getFailedUpdates(); + 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 +305,24 @@ public class CodePush { } } + private void recordDeploymentStatusReported(String appVersionOrPackageIdentifier, String status) { + SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0); + String sentStatusReportsString = settings.getString(STATUS_REPORTS_KEY, null); + JSONObject sentStatusReports; + try { + if (sentStatusReportsString == null) { + sentStatusReports = new JSONObject(); + } else { + sentStatusReports = new JSONObject(sentStatusReportsString); + } + + sentStatusReports.put(appVersionOrPackageIdentifier, status); + settings.edit().putString(STATUS_REPORTS_KEY, sentStatusReports.toString()).commit(); + } catch (JSONException e) { + throw new CodePushUnknownException("Unable to save new entry in SharedPreferences under " + STATUS_REPORTS_KEY + ".", e); + } + } + private void removeFailedUpdates() { SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0); settings.edit().remove(FAILED_UPDATES_KEY).commit(); @@ -261,8 +335,8 @@ public class CodePush { private void rollbackPackage() { try { - String packageHash = codePushPackage.getCurrentPackageHash(); - saveFailedUpdate(packageHash); + WritableMap failedPackage = codePushPackage.getCurrentPackage(); + saveFailedUpdate(failedPackage); } catch (IOException e) { throw new CodePushUnknownException("Attempted a rollback without having a current downloaded package", e); } @@ -276,29 +350,25 @@ public class CodePush { 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 +447,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); } @@ -408,7 +481,60 @@ public class CodePush { asyncTask.execute(); } - + + @ReactMethod + public void getNewStatusReport(Promise promise) { + // Check if the current appVersion has been reported. + if (isDeploymentStatusNotYetReported(appVersion)) { + recordDeploymentStatusReported(appVersion, DEPLOYMENT_SUCCEEDED_STATUS); + WritableNativeMap reportMap = new WritableNativeMap(); + reportMap.putString("appVersion", appVersion); + promise.resolve(reportMap); + return; + } + + // Check if there was a rollback that was not yet reported + 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, DEPLOYMENT_FAILED_STATUS); + 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); + } + } + + // Check if the current CodePush package has been reported + try { + WritableMap currentPackage = codePushPackage.getCurrentPackage(); + if (currentPackage != null) { + String currentPackageIdentifier = getPackageStatusReportIdentifier(currentPackage); + if (currentPackageIdentifier != null && isDeploymentStatusNotYetReported(currentPackageIdentifier)) { + recordDeploymentStatusReported(currentPackageIdentifier, DEPLOYMENT_SUCCEEDED_STATUS); + WritableNativeMap reportMap = new WritableNativeMap(); + reportMap.putMap("package", currentPackage); + reportMap.putString("status", DEPLOYMENT_SUCCEEDED_STATUS); + promise.resolve(reportMap); + return; + } + } + } catch (IOException e) { + // No current package, resolve no report. + promise.resolve(""); + } + + promise.resolve(""); + } + @ReactMethod public void installUpdate(final ReadableMap updatePackage, final int installMode, final Promise promise) { AsyncTask asyncTask = new AsyncTask() { diff --git a/request-fetch-adapter.js b/request-fetch-adapter.js index 6254def..b18a8cf 100644 --- a/request-fetch-adapter.js +++ b/request-fetch-adapter.js @@ -16,7 +16,7 @@ module.exports = { try { const response = await fetch(url, { - method: verb, + method: getHttpMethodName(verb), headers: headers, body: body }); @@ -28,4 +28,18 @@ module.exports = { callback(err); } } -}; \ No newline at end of file +}; + +function getHttpMethodName(verb) { + return [ + "GET", + "HEAD", + "POST", + "PUT", + "DELETE", + "TRACE", + "OPTIONS", + "CONNECT", + "PATCH" + ][verb]; +} \ No newline at end of file