mirror of
https://github.com/zhigang1992/react-native-code-push.git
synced 2026-06-10 07:10:36 +08:00
Merge pull request #113 from Microsoft/isPending
isPending property on package objects
This commit is contained in:
44
CodePush.m
44
CodePush.m
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
};
|
||||
Reference in New Issue
Block a user