diff --git a/CodePush.m b/CodePush.m index 6c25d58..61d1dd8 100644 --- a/CodePush.m +++ b/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 NSUserDdefaults 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 metada +// 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 \ No newline at end of file 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 74fa96b..a0ac474 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 @@ -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 getConstants() { final Map constants = new HashMap<>(); @@ -474,5 +498,4 @@ public class CodePush { return new ArrayList(); } } - -} +} \ No newline at end of file diff --git a/package-mixins.js b/package-mixins.js index 3a61d02..8cdd1f0 100644 --- a/package-mixins.js +++ b/package-mixins.js @@ -1,11 +1,15 @@ -var { Platform, DeviceEventEmitter } = require("react-native"); +import { DeviceEventEmitter } from "react-native"; -module.exports = (NativeCodePush) => { - var remote = { - abortDownload: function abortDownload() { +// 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 = function PackageMixinFactory(NativeCodePush) { + 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 }; }; \ No newline at end of file