Merge pull request #113 from Microsoft/isPending

isPending property on package objects
This commit is contained in:
Jonathan Carter
2015-12-17 20:24:06 -10:00
3 changed files with 213 additions and 152 deletions

View File

@@ -13,9 +13,9 @@
RCT_EXPORT_MODULE()
static NSTimer *_timer;
static BOOL usingTestFolder = NO;
// 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";
@@ -24,6 +24,11 @@ static NSString *const PendingUpdateKey = @"CODE_PUSH_PENDING_UPDATE";
static NSString *const PendingUpdateHashKey = @"hash";
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 PackageHashKey = @"packageHash";
static NSString *const PackageIsPendingKey = @"isPending";
@synthesize bridge = _bridge;
// Public Obj-C API (see header for method comments)
@@ -141,6 +146,25 @@ static NSString *const PendingUpdateIsLoadingKey = @"isLoading";
return (failedUpdates != nil && [failedUpdates containsObject:packageHash]);
}
/*
* This method checks to see whether a specific package hash
* represents a downloaded and installed update, that hasn't
* been applied yet via an app restart.
*/
- (BOOL)isPendingUpdate:(NSString*)packageHash
{
NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
NSDictionary *pendingUpdate = [preferences objectForKey:PendingUpdateKey];
// If there is a pending update, whose hash is equal to the one
// specified, and its "state" isn't loading, then we consider it "pending".
BOOL updateIsPending = pendingUpdate &&
[pendingUpdate[PendingUpdateIsLoadingKey] boolValue] == NO &&
[pendingUpdate[PendingUpdateHashKey] isEqualToString:packageHash];
return updateIsPending;
}
/*
* This method updates the React Native bridge's bundle URL
* to point at the latest CodePush update, and then restarts
@@ -259,7 +283,7 @@ RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage
// The download completed
doneCallback:^{
NSError *err;
NSDictionary *newPackage = [CodePushPackage getPackage:updatePackage[@"packageHash"] error:&err];
NSDictionary *newPackage = [CodePushPackage getPackage:updatePackage[PackageHashKey] error:&err];
if (err) {
return reject(err);
@@ -293,12 +317,18 @@ RCT_EXPORT_METHOD(getCurrentPackage:(RCTPromiseResolveBlock)resolve
{
dispatch_async(dispatch_get_main_queue(), ^{
NSError *error;
NSDictionary *package = [CodePushPackage getCurrentPackage:&error];
NSMutableDictionary *package = [[CodePushPackage getCurrentPackage:&error] mutableCopy];
if (error) {
reject(error);
} else {
resolve(package);
}
// Add the "isPending" virtual property to the package at this point, so that
// the script-side doesn't need to immediately call back into native to populate it.
BOOL isPendingUpdate = [self isPendingUpdate:[package objectForKey:PackageHashKey]];
[package setObject:@(isPendingUpdate) forKey:PackageIsPendingKey];
resolve(package);
});
}
@@ -318,7 +348,7 @@ RCT_EXPORT_METHOD(installUpdate:(NSDictionary*)updatePackage
if (error) {
reject(error);
} else {
[self savePendingUpdate:updatePackage[@"packageHash"]
[self savePendingUpdate:updatePackage[PackageHashKey]
isLoading:NO];
if (installMode == CodePushInstallModeImmediate) {
@@ -393,4 +423,4 @@ RCT_EXPORT_METHOD(setUsingTestFolder:(BOOL)shouldUseTestFolder)
usingTestFolder = shouldUseTestFolder;
}
@end
@end

View File

@@ -38,7 +38,6 @@ import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public class CodePush {
private boolean didUpdate = false;
private boolean usingTestFolder = false;
@@ -89,13 +88,13 @@ public class CodePush {
initializeUpdateAfterRestart();
}
public ReactPackage getReactPackage() {
if (codePushReactPackage == null) {
codePushReactPackage = new CodePushReactPackage();
private void clearReactDevBundleCache() {
File cachedDevBundle = new File(this.applicationContext.getFilesDir(), REACT_DEV_BUNDLE_CACHE_FILE_NAME);
if (cachedDevBundle.exists()) {
cachedDevBundle.delete();
}
return codePushReactPackage;
}
public long getBinaryResourcesModifiedTime() {
ApplicationInfo ai = null;
ZipFile applicationFile = null;
@@ -148,6 +147,57 @@ public class CodePush {
}
}
private JSONObject getPendingUpdate() {
SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0);
String pendingUpdateString = settings.getString(PENDING_UPDATE_KEY, null);
if (pendingUpdateString == null) {
return null;
}
try {
JSONObject pendingUpdate = new JSONObject(pendingUpdateString);
return pendingUpdate;
} catch (JSONException e) {
// Should not happen.
CodePushUtils.log("Unable to parse pending update metadata " + pendingUpdateString +
" stored in SharedPreferences");
return null;
}
}
public ReactPackage getReactPackage() {
if (codePushReactPackage == null) {
codePushReactPackage = new CodePushReactPackage();
}
return codePushReactPackage;
}
private void initializeUpdateAfterRestart() {
JSONObject pendingUpdate = getPendingUpdate();
if (pendingUpdate != null) {
didUpdate = true;
try {
boolean updateIsLoading = pendingUpdate.getBoolean(PENDING_UPDATE_IS_LOADING_KEY);
if (updateIsLoading) {
// 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.");
rollbackPackage();
} else {
// Clear the React dev bundle cache so that new updates can be loaded.
clearReactDevBundleCache();
// 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),
/* isLoading */true);
}
} catch (JSONException e) {
// Should not happen.
throw new CodePushUnknownException("Unable to read pending update metadata stored in SharedPreferences", e);
}
}
}
private boolean isFailedHash(String packageHash) {
SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0);
String failedUpdatesString = settings.getString(FAILED_UPDATES_KEY, null);
@@ -165,6 +215,26 @@ public class CodePush {
}
}
private boolean isPendingUpdate(String packageHash) {
JSONObject pendingUpdate = getPendingUpdate();
try {
boolean updateIsPending = pendingUpdate != null &&
pendingUpdate.getBoolean(PENDING_UPDATE_IS_LOADING_KEY) == false &&
pendingUpdate.getString(PENDING_UPDATE_HASH_KEY).equals(packageHash);
return updateIsPending;
}
catch (JSONException e) {
throw new CodePushUnknownException("Unable to read pending update metadata in isPendingUpdate.", e);
}
}
private void removePendingUpdate() {
SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0);
settings.edit().remove(PENDING_UPDATE_KEY).commit();
}
private void rollbackPackage() {
try {
String packageHash = codePushPackage.getCurrentPackageHash();
@@ -207,11 +277,6 @@ public class CodePush {
}
}
private void removePendingUpdate() {
SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0);
settings.edit().remove(PENDING_UPDATE_KEY).commit();
}
private void savePendingUpdate(String packageHash, boolean isLoading) {
SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0);
JSONObject pendingUpdate = new JSONObject();
@@ -225,57 +290,6 @@ public class CodePush {
}
}
private JSONObject getPendingUpdate() {
SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0);
String pendingUpdateString = settings.getString(PENDING_UPDATE_KEY, null);
if (pendingUpdateString == null) {
return null;
}
try {
JSONObject pendingUpdate = new JSONObject(pendingUpdateString);
return pendingUpdate;
} catch (JSONException e) {
// Should not happen.
CodePushUtils.log("Unable to parse pending update metadata " + pendingUpdateString +
" stored in SharedPreferences");
return null;
}
}
private void initializeUpdateAfterRestart() {
JSONObject pendingUpdate = getPendingUpdate();
if (pendingUpdate != null) {
didUpdate = true;
try {
boolean updateIsLoading = pendingUpdate.getBoolean(PENDING_UPDATE_IS_LOADING_KEY);
if (updateIsLoading) {
// 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.");
rollbackPackage();
} else {
// Clear the React dev bundle cache so that new updates can be loaded.
clearReactDevBundleCache();
// 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),
/* isLoading */true);
}
} catch (JSONException e) {
// Should not happen.
throw new CodePushUnknownException("Unable to read pending update metadata stored in SharedPreferences", e);
}
}
}
private void clearReactDevBundleCache() {
File cachedDevBundle = new File(this.applicationContext.getFilesDir(), REACT_DEV_BUNDLE_CACHE_FILE_NAME);
if (cachedDevBundle.exists()) {
cachedDevBundle.delete();
}
}
private class CodePushNativeModule extends ReactContextBaseJavaModule {
private LifecycleEventListener lifecycleEventListener = null;
@@ -286,11 +300,81 @@ public class CodePush {
mainActivity.startActivity(intent);
}
@ReactMethod
public void downloadUpdate(final ReadableMap updatePackage, final Promise promise) {
AsyncTask asyncTask = new AsyncTask() {
@Override
protected Void doInBackground(Object... params) {
try {
WritableMap mutableUpdatePackage = CodePushUtils.convertReadableMapToWritableMap(updatePackage);
mutableUpdatePackage.putString(BINARY_MODIFIED_TIME_KEY, "" + getBinaryResourcesModifiedTime());
codePushPackage.downloadPackage(applicationContext, mutableUpdatePackage, new DownloadProgressCallback() {
@Override
public void call(DownloadProgress downloadProgress) {
getReactApplicationContext()
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(DOWNLOAD_PROGRESS_EVENT_NAME, downloadProgress.createWritableMap());
}
});
WritableMap newPackage = codePushPackage.getPackage(CodePushUtils.tryGetString(updatePackage, codePushPackage.PACKAGE_HASH_KEY));
promise.resolve(newPackage);
} catch (IOException e) {
e.printStackTrace();
promise.reject(e.getMessage());
}
return null;
}
};
asyncTask.execute();
}
@ReactMethod
public void getConfiguration(Promise promise) {
WritableNativeMap configMap = new WritableNativeMap();
configMap.putString("appVersion", appVersion);
configMap.putInt("buildVersion", buildVersion);
configMap.putString("deploymentKey", deploymentKey);
configMap.putString("serverUrl", serverUrl);
promise.resolve(configMap);
}
@ReactMethod
public void getCurrentPackage(final Promise promise) {
AsyncTask asyncTask = new AsyncTask() {
@Override
protected Void doInBackground(Object... params) {
try {
WritableMap currentPackage = codePushPackage.getCurrentPackage();
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());
}
return null;
}
};
asyncTask.execute();
}
@ReactMethod
public void installUpdate(final ReadableMap updatePackage, final int installMode, final Promise promise) {
AsyncTask asyncTask = new AsyncTask() {
@Override
protected Void doInBackground(Object[] params) {
protected Void doInBackground(Object... params) {
try {
codePushPackage.installPackage(updatePackage);
@@ -336,67 +420,7 @@ public class CodePush {
asyncTask.execute();
}
@ReactMethod
public void downloadUpdate(final ReadableMap updatePackage, final Promise promise) {
AsyncTask asyncTask = new AsyncTask() {
@Override
protected Void doInBackground(Object[] params) {
try {
WritableMap mutableUpdatePackage = CodePushUtils.convertReadableMapToWritableMap(updatePackage);
mutableUpdatePackage.putString(BINARY_MODIFIED_TIME_KEY, "" + getBinaryResourcesModifiedTime());
codePushPackage.downloadPackage(applicationContext, mutableUpdatePackage, new DownloadProgressCallback() {
@Override
public void call(DownloadProgress downloadProgress) {
getReactApplicationContext()
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(DOWNLOAD_PROGRESS_EVENT_NAME, downloadProgress.createWritableMap());
}
});
WritableMap newPackage = codePushPackage.getPackage(CodePushUtils.tryGetString(updatePackage, codePushPackage.PACKAGE_HASH_KEY));
promise.resolve(newPackage);
} catch (IOException e) {
e.printStackTrace();
promise.reject(e.getMessage());
}
return null;
}
};
asyncTask.execute();
}
@ReactMethod
public void getConfiguration(Promise promise) {
WritableNativeMap configMap = new WritableNativeMap();
configMap.putString("appVersion", appVersion);
configMap.putInt("buildVersion", buildVersion);
configMap.putString("deploymentKey", deploymentKey);
configMap.putString("serverUrl", serverUrl);
promise.resolve(configMap);
}
@ReactMethod
public void getCurrentPackage(final Promise promise) {
AsyncTask asyncTask = new AsyncTask() {
@Override
protected Void doInBackground(Object[] params) {
try {
promise.resolve(codePushPackage.getCurrentPackage());
} catch (IOException e) {
e.printStackTrace();
promise.reject(e.getMessage());
}
return null;
}
};
asyncTask.execute();
}
@ReactMethod
public void isFailedUpdate(String packageHash, Promise promise) {
promise.resolve(isFailedHash(packageHash));
@@ -421,17 +445,17 @@ public class CodePush {
removePendingUpdate();
promise.resolve("");
}
@ReactMethod
public void restartApp() {
loadBundle();
}
@ReactMethod
public void setUsingTestFolder(boolean shouldUseTestFolder) {
usingTestFolder = shouldUseTestFolder;
}
@ReactMethod
public void restartApp() {
loadBundle();
}
@Override
public Map<String, Object> getConstants() {
final Map<String, Object> constants = new HashMap<>();
@@ -474,5 +498,4 @@ public class CodePush {
return new ArrayList();
}
}
}
}

View File

@@ -1,11 +1,15 @@
var { Platform, DeviceEventEmitter } = require("react-native");
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) => {
var remote = {
abortDownload: function abortDownload() {
const remote = {
abortDownload() {
return NativeCodePush.abortDownload(this);
},
download: function download(downloadProgressCallback) {
download(downloadProgressCallback) {
if (!this.downloadUrl) {
return Promise.reject(new Error("Cannot download an update without a download url"));
}
@@ -31,23 +35,27 @@ module.exports = (NativeCodePush) => {
// Rethrow the error for subsequent handlers down the promise chain.
throw error;
});
}
},
isPending: false // A remote package could never be in a pending state
};
var local = {
install: function install(installMode = NativeCodePush.codePushInstallModeOnNextRestart, updateInstalledCallback) {
const local = {
install(installMode = NativeCodePush.codePushInstallModeOnNextRestart, updateInstalledCallback) {
let localPackage = this;
return NativeCodePush.installUpdate(this, installMode)
.then(function() {
.then(() => {
updateInstalledCallback && updateInstalledCallback();
if (installMode == NativeCodePush.codePushInstallModeImmediate) {
NativeCodePush.restartApp();
};
} else {
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
};
return {
remote: remote,
local: local
};
return { local, remote };
};