From a65f97ff24fcfaa76c911389b893b7894e3931ed Mon Sep 17 00:00:00 2001 From: Jonathan Carter Date: Tue, 8 Dec 2015 15:03:53 -0800 Subject: [PATCH 01/12] Adding logging to unknown error case --- CodePush.js | 1 + 1 file changed, 1 insertion(+) diff --git a/CodePush.js b/CodePush.js index 6b95b1a..9d1433a 100644 --- a/CodePush.js +++ b/CodePush.js @@ -294,6 +294,7 @@ function sync(options = {}, syncStatusChangeCallback, downloadProgressCallback) }) .catch((error) => { syncStatusChangeCallback(CodePush.SyncStatus.UNKNOWN_ERROR); + log(error.message); reject(error); }) .done(); From d4ff75d41506dd9d1ddd8bea88cae9b5ddfb353a Mon Sep 17 00:00:00 2001 From: Jonathan Carter Date: Tue, 8 Dec 2015 18:09:54 -0800 Subject: [PATCH 02/12] Adding logging to download and install --- CodePush.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/CodePush.js b/CodePush.js index 9d1433a..cadd8d1 100644 --- a/CodePush.js +++ b/CodePush.js @@ -219,6 +219,12 @@ function sync(options = {}, syncStatusChangeCallback, downloadProgressCallback) }; return new Promise((resolve, reject) => { + var rejectPromise = (error) => { + syncStatusChangeCallback(CodePush.SyncStatus.UNKNOWN_ERROR); + log(error.message); + reject(error); + }; + CodePush.notifyApplicationReady() .then(() => { syncStatusChangeCallback(CodePush.SyncStatus.CHECKING_FOR_UPDATE); @@ -235,7 +241,7 @@ function sync(options = {}, syncStatusChangeCallback, downloadProgressCallback) resolve(CodePush.SyncStatus.UPDATE_INSTALLED); }); }) - .catch(reject) + .catch(rejectPromise) .done(); } @@ -292,11 +298,7 @@ function sync(options = {}, syncStatusChangeCallback, downloadProgressCallback) doDownloadAndInstall(); } }) - .catch((error) => { - syncStatusChangeCallback(CodePush.SyncStatus.UNKNOWN_ERROR); - log(error.message); - reject(error); - }) + .catch(rejectPromise) .done(); }); }; From e29ca3ac3123e226e126e5fd8d2a28f1e4a84de6 Mon Sep 17 00:00:00 2001 From: Jonathan Carter Date: Tue, 8 Dec 2015 23:10:51 -0800 Subject: [PATCH 03/12] Version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index effd15e..b8ae6de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-code-push", - "version": "1.4.1-beta", + "version": "1.4.2-beta", "description": "React Native plugin for the CodePush service", "main": "CodePush.js", "homepage": "https://microsoft.github.io/code-push", From 3be5a9ac72dce3889590ba9e81c178e24da12f28 Mon Sep 17 00:00:00 2001 From: Jonathan Carter Date: Tue, 8 Dec 2015 23:32:45 -0800 Subject: [PATCH 04/12] Fixing comment to reflect API name change We renamed `failedApply` to `failedInstall` but forgot to update this comment to reflect that change. --- CodePush.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CodePush.m b/CodePush.m index fa05255..e4498ef 100644 --- a/CodePush.m +++ b/CodePush.m @@ -356,7 +356,7 @@ RCT_EXPORT_METHOD(installUpdate:(NSDictionary*)updatePackage /* * This method isn't publicly exposed via the "react-native-code-push" - * module, and is only used internally to populate the RemotePackage.failedApply property. + * module, and is only used internally to populate the RemotePackage.failedInstall property. */ RCT_EXPORT_METHOD(isFailedUpdate:(NSString *)packageHash resolve:(RCTPromiseResolveBlock)resolve @@ -406,4 +406,4 @@ RCT_EXPORT_METHOD(setUsingTestFolder:(BOOL)shouldUseTestFolder) usingTestFolder = shouldUseTestFolder; } -@end \ No newline at end of file +@end From 8fe9951c8178255680a4a4b06d7b25d48fc3b35b Mon Sep 17 00:00:00 2001 From: Jonathan Carter Date: Thu, 10 Dec 2015 18:14:50 -0800 Subject: [PATCH 05/12] Doc updates --- README.md | 93 +++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 76 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index d499ccb..1f4b5b9 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,9 @@ This plugin provides client-side integration for the [CodePush service](https:// * [Releasing Updates (JavaScript-only)](#releasing-updates-javascript-only) * [Releasing Updates (JavaScript + images)](#releasing-updates-javascript--images) * [API Reference](#api-reference) + * [JavaScript API](#javascript-api-reference) + * [Objective-C API Reference (iOS)](#objective-c-api-reference-ios) + * [Java API Reference (Android)](#java-api-reference-android) ## How does it work? @@ -27,7 +30,7 @@ The CodePush plugin helps get product improvements in front of your end users in ## Supported React Native platforms - iOS -- Android +- Android (asset support coming soon!) *Note: CodePush v1.3.0 requires v0.14.0+ of React Native, and CodePush v1.4.0 requires v0.15.0+ of React Native, so make sure you are using the right version of the CodePush plugin.* @@ -236,12 +239,22 @@ If you are using the new React Native [assets system](https://facebook.github.io Additionally, the CodePush client supports differential updates, so even though you are releasing your JS bundle and assets on every update, your end users will only actually download the files they need. The service handles this automatically so that you can focus on creating awesome apps and we can worry about optimizing end user downloads. -*Note: Releasing assets via CodePush is currently only supported on iOS, and requires that you're using React Native v0.15.0+.* +*Note: Releasing assets via CodePush is currently only supported on iOS, and requires that you're using React Native v0.15.0+ and CodePush 1.4.0+. If you are using assets and an older version of the CodePush plugin, you should not release updates via CodePush, because it will break your app's ability to load images from the binary. Please test and release appropriately!* --- ## API Reference +The CodePush plugin is made up of two components: + +1. A JavaScript module, which can be imported/required, and allows the app to interact with the service during runtime (e.g. check for updates, inspect the metadata about the currently running app update). + +2. A native API (Objective-C and Java) which allows the React Native app host to bootstrap itself with the right JS bundle location. + +The following sections describe the shape and behavior of these APIs in detail: + +### JavaScript API Reference + When you require `react-native-code-push`, the module object provides the following top-level methods: * [checkForUpdate](#codepushcheckforupdate): Asks the CodePush service whether the configured app deployment has an update available. @@ -254,7 +267,7 @@ When you require `react-native-code-push`, the module object provides the follow * [sync](#codepushsync): Allows checking for an update, downloading it and installing it, all with a single call. Unless you need custom UI and/or behavior, we recommend most developers to use this method when integrating CodePush into their apps -### codePush.checkForUpdate +#### codePush.checkForUpdate ```javascript codePush.checkForUpdate(deploymentKey: String = null): Promise; @@ -280,7 +293,7 @@ codePush.checkForUpdate() }); ``` -### codePush.getCurrentPackage +#### codePush.getCurrentPackage ```javascript codePush.getCurrentPackage(): Promise; @@ -304,7 +317,7 @@ codePush.getCurrentPackage() }); ``` -### codePush.notifyApplicationReady +#### codePush.notifyApplicationReady ```javascript codePush.notifyApplicationReady(): Promise; @@ -314,7 +327,7 @@ Notifies the CodePush runtime that a freshly installed update should be consider If you are using the `sync` function, and doing your update check on app start, then you don't need to manually call `notifyApplicationReady` since `sync` will call it for you. This behavior exists due to the assumption that the point at which `sync` is called in your app represents a good approximation of a successful startup. -### codePush.restartApp +#### codePush.restartApp ```javascript codePush.restartApp(): void; @@ -325,7 +338,7 @@ Immediately restarts the app. If there is an update pending, it will be presente 1. Your app is specifying an install mode value of `ON_NEXT_RESTART` or `ON_NEXT_RESUME` when calling the `sync` or `LocalPackage.install` methods. This has the effect of not applying your update until the app has been restarted (by either the end user or OS) or resumed, and therefore, the update won't be immediately displayed to the end user . 2. You have an app-specific user event (e.g. the end user navigated back to the app's home route) that allows you to apply the update in an unobtrusive way, and potentially gets the update in front of the end user sooner then waiting until the next restart or resume. -### codePush.sync +#### codePush.sync ```javascript codePush.sync(options: Object, syncStatusChangeCallback: function(syncStatus: Number), downloadProgressCallback: function(progress: DownloadProgress)): Promise; @@ -432,18 +445,18 @@ If the update check and/or the subsequent download fails for any reason, the `Pr The `sync` method can be called anywhere you'd like to check for an update. That could be in the `componentWillMount` lifecycle event of your root component, the onPress handler of a `` component, in the callback of a periodic timer, or whatever else makes sense for your needs. Just like the `checkForUpdate` method, it will perform the network request to check for an update in the background, so it won't impact your UI thread and/or JavaScript thread's responsiveness. -### Package objects +#### Package objects The `checkForUpdate` and `getCurrentPackage` methods return promises, that when resolved, provide acces to "package" objects. The package represents your code update as well as any extra metadata (e.g. description, mandatory?). The CodePush API has the distinction between the following types of packages: * [LocalPackage](#localpackage): Represents a downloaded update package that is either already running, or has been installed and is pending an app restart. * [RemotePackage](#remotepackage): Represents an available update on the CodePush server that hasn't been downloaded yet. -#### LocalPackage +##### LocalPackage Contains details about an update that has been downloaded locally or already installed. You can get a reference to an instance of this object either by calling the module-level `getCurrentPackage` method, or as the value of the promise returned by the `RemotePackage.download` method. -##### Properties +###### Properties - __appVersion__: The app binary version that this update is dependent on. This is the value that was specified via the `appStoreVersion` parameter when calling the CLI's `release` command. *(String)* - __deploymentKey__: The deployment key that was used to originally download this update. *(String)* - __description__: The description of the update. This is the same value that you specified in the CLI when you released the update. *(String)* @@ -454,27 +467,27 @@ Contains details about an update that has been downloaded locally or already ins - __packageHash__: The SHA hash value of the update. *(String)* - __packageSize__: The size of the code contained within the update, in bytes. *(Number)* -##### Methods +###### Methods - __install(installMode: CodePush.InstallMode = CodePush.InstallMode.ON_NEXT_RESTART): Promise<void>__: Installs the update by saving it to the location on disk where the runtime expects to find the latest version of the app. The `installMode` parameter controls when the changes are actually presented to the end user. The default value is to wait until the next app restart to display the changes, but you can refer to the [`InstallMode`](#installmode) enum reference for a description of the available options and what they do. -#### RemotePackage +##### RemotePackage Contains details about an update that is available for download from the CodePush server. You get a reference to an instance of this object by calling the `checkForUpdate` method when an update is available. If you are using the `sync` API, you don't need to worry about the `RemotePackage`, since it will handle the download and installation process automatically for you. -##### Properties +###### Properties The `RemotePackage` inherits all of the same properties as the `LocalPackage`, but includes one additional one: - __downloadUrl__: The URL at which the package is available for download. This property is only needed for advanced usage, since the `download` method will automatically handle the acquisition of updates for you. *(String)* -##### Methods +###### Methods - __download(downloadProgressCallback?: Function): Promise<LocalPackage>__: Downloads the available update from the CodePush service. If a `downloadProgressCallback` is specified, it will be called periodically with a `DownloadProgress` object (`{ totalBytes: Number, receivedBytes: Number }`) that reports the progress of the download until it completes. Returns a Promise that resolves with the `LocalPackage`. -### Enums +#### Enums The CodePush API includes the following enums which can be used to customize the update experience: -#### InstallMode +##### InstallMode This enum specified when you would like an installed update to actually be applied, and can be passed to either the `sync` or `LocalPackage.install` methods. It includes the following values: * __CodePush.InstallMode.IMMEDIATE__ *(0)* - Indicates that you want to install the update and restart the app immediately. This value is appropriate for debugging scenarios as well as when displaying an update prompt to the user, since they would expect to see the changes immediately after accepting the installation. @@ -483,7 +496,7 @@ This enum specified when you would like an installed update to actually be appli * __CodePush.InstallMode.ON_NEXT_RESUME__ *(2)* - Indicates that you want to install the update, but don't want to restart the app until the next time the end user resumes it from the background. This way, you don't disrupt their current session, but you can get the update in front of them sooner then having to wait for the next natural restart. This value is appropriate for silent installs that can be applied on resume in a non-invasive way. -#### SyncStatus +##### SyncStatus This enum is provided to the `syncStatusChangedCallback` function that can be passed to the `sync` method, in order to hook into the overall update process. It includes the following values: @@ -495,3 +508,49 @@ This enum is provided to the `syncStatusChangedCallback` function that can be pa * __CodePush.SyncStatus.UPDATE_IGNORED__ *(5)* - The app has an optional update, which the end user chose to ignore. (This is only applicable when the `updateDialog` is used) * __CodePush.SyncStatus.UPDATE_INSTALLED__ *(6)* - An available update has been installed and will be run either immediately after the `syncStatusChangedCallback` function returns or the next time the app resumes/restarts, depending on the `InstallMode` specified in `SyncOptions`. * __CodePush.SyncStatus.UNKNOWN_ERROR__ *(-1)* - The sync operation encountered an unknown error. + +### Objective-C API Reference (iOS) + +The Objective-C API is made available by importing the `CodePush.h` header into your `AppDelegate.m` file, and consists of a single public class named `CodePush`. + +#### CodePush + +Contains static methods for retreiving the `NSURL` that represents the most recent JavaScript bundle file, and can be passed to the `RCTRootView`'s `initWithBundleURL` method when bootstrapping your app in the `AppDelegate.m` file. + +The `CodePush` class' methods can be thought of as composite resolvers which always load the appropriate bundle, in order to accomodate the following scenarios: + +1. When an end-user installs your app from the store (e.g. `1.0.0`), they will get the JS bundle that is contained within the binary. This is the behavior you would get without using CodePush, but we make sure it doesn't break :) + +2. As soon as you begin releasing CodePush updates, your end-users will get the JS bundle that represents the latest release for the configured deployment. This is the behavior that allows you to iterate beyond what you shipped to the store. + +3. As soon as you release an update to the app store (e.g. `1.1.0`), and your end-users update it, they will once again get the JS bundle that is contained within the binary. This behavior ensures that CodePush updates that targetted a previous app store version aren't used (since we don't know it they would work), and your end-users always have a working version of your app. + +4. Repeat #2 and #3 as the CodePush releases and app store releases continue on into infinity (and beyond?) + +Because of this behavior, you can safely deploy updates to both the app store(s) and CodePush as neccesary, and rest assured that your end-users will always get the most recent version. + +##### Methods + +- __(NSURL \*)bundleURL__ - Returns the most recent JS bundle `NSURL` as described above. This method assumes that the name of the JS bundle contained within your app binary is `main.jsbundle`. + +- __(NSURL \*)bundleURLForResource:(NSString \*)resourceName__ - Equivalent to the `bundleURL` method, but also allows customizing the name of the JS bundle that is looked for within the app binary. This is useful if you aren't naming this file `main` (which is the default convention). This method assumes that the JS bundle's extension is `*.jsbundle`. + +- __(NSURL \*)bundleURLForResource:(NSString \*)resourceName withExtension:(NSString \*)resourceExtension__: Equivalent to the `bundleURLForResource:` method, but also allows customizing the extension used by the JS bundle that is looked for within the app binary. This is useful if you aren't naming this file `*.jsbundle` (which is the default convention). + +### Java API Reference (Android) + +The Java API is made available by importing the `com.microsoft.codepush.react.CodePush` class into your `MainActivity.java` file, and consists of a single public class named `CodePush`. + +#### CodePush + +Constructs the CodePush client runtime and includes methods for integrating CodePush into your app's `ReactInstanceManager`. + +##### Constructors + +- __CodePush(String deploymentKey, Activity mainActivity)__ - Creates a new instance of the CodePush runtime, that will be used to query the service for updates via the provided deployment key. The `mainActivity` parameter should always be set to `this` when configuring your `ReactInstanceManager` inside the `MainActivity` class. + +##### Methods + +- __getBundleUrl(String bundleName)__ - Returns the path to the most recent version of your app's JS bundle file, using the specified resource name (e.g. `index.android.bundle`). This method has the same resolution behavior as the Objective-C equivalent described above. + +- __getReactPackage()__ - Returns a `ReactPackage` object that should be added to your `ReactInstanceManager` via its `addPackage` method. Without this, the `react-native-code-push` JS module won't be available to your script. \ No newline at end of file From f15bce41ca754370db851104bcfeb9e314379fd9 Mon Sep 17 00:00:00 2001 From: Jonathan Carter Date: Thu, 10 Dec 2015 18:21:53 -0800 Subject: [PATCH 06/12] Adding comment about activity import --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1f4b5b9..6dd7afb 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,7 @@ After installing the plugin and syncing your Android Studio project with Gradle, import com.microsoft.codepush.react.CodePush; // 2. Optional: extend FragmentActivity if you intend to show a dialog prompting users about updates. + // If you do this, make sure to also add `import android.support.v4.app.FragmentActivity` to the file. public class MainActivity extends FragmentActivity implements DefaultHardwareBackBtnHandler { ... From a9b79f370e6cc82181072fe777cd15e832625713 Mon Sep 17 00:00:00 2001 From: Jonathan Carter Date: Thu, 10 Dec 2015 18:23:22 -0800 Subject: [PATCH 07/12] Adding more detail --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6dd7afb..6f7e210 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ After installing the plugin and syncing your Android Studio project with Gradle, import com.microsoft.codepush.react.CodePush; // 2. Optional: extend FragmentActivity if you intend to show a dialog prompting users about updates. - // If you do this, make sure to also add `import android.support.v4.app.FragmentActivity` to the file. + // If you do this, make sure to also add "import android.support.v4.app.FragmentActivity" below #1. public class MainActivity extends FragmentActivity implements DefaultHardwareBackBtnHandler { ... From 9c1a20f05b4cac46e46a3f929224148432a89ee8 Mon Sep 17 00:00:00 2001 From: Geoffrey Goh Date: Thu, 10 Dec 2015 23:41:02 -0800 Subject: [PATCH 08/12] Add comment --- .../app/src/main/java/com/microsoft/codepush/react/CodePush.java | 1 + 1 file changed, 1 insertion(+) 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 fe311d9..db21af7 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 @@ -53,6 +53,7 @@ public class CodePush { private final String CODE_PUSH_TAG = "CodePush"; private final String DOWNLOAD_PROGRESS_EVENT_NAME = "CodePushDownloadProgress"; private final String RESOURCES_BUNDLE = "resources.arsc"; + // 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 CodePushPackage codePushPackage; From 7d6339e5cefb3db3c3a1291a430c7b6ea3b62b7f Mon Sep 17 00:00:00 2001 From: Jonathan Carter Date: Sat, 12 Dec 2015 12:51:37 -0800 Subject: [PATCH 09/12] Version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b8ae6de..acf1dfc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-code-push", - "version": "1.4.2-beta", + "version": "1.5.0-beta", "description": "React Native plugin for the CodePush service", "main": "CodePush.js", "homepage": "https://microsoft.github.io/code-push", From 4694db558cb16ba01b551eb893a898954332d6ca Mon Sep 17 00:00:00 2001 From: Jonathan Carter Date: Wed, 16 Dec 2015 20:49:27 -0800 Subject: [PATCH 10/12] Adding isPending --- CodePush.m | 44 ++- .../microsoft/codepush/react/CodePush.java | 285 ++++++++++-------- package-mixins.js | 38 ++- 3 files changed, 214 insertions(+), 153 deletions(-) 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 From 4a378140ae1a8a8dc984d47ad60bd6ce9217a76e Mon Sep 17 00:00:00 2001 From: Jonathan Carter Date: Wed, 16 Dec 2015 20:51:36 -0800 Subject: [PATCH 11/12] Fixing comment typos --- CodePush.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CodePush.m b/CodePush.m index 61d1dd8..2d5e15d 100644 --- a/CodePush.m +++ b/CodePush.m @@ -15,7 +15,7 @@ RCT_EXPORT_MODULE() static BOOL usingTestFolder = NO; -// These keys represent the names we use to store data in NSUserDdefaults +// 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,7 +24,7 @@ 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 +// 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"; From a16f0b150161597cba370f972b7235dfe6c741d8 Mon Sep 17 00:00:00 2001 From: Jonathan Carter Date: Wed, 16 Dec 2015 21:06:54 -0800 Subject: [PATCH 12/12] Changing function to arrow --- package-mixins.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-mixins.js b/package-mixins.js index 8cdd1f0..d539ab9 100644 --- a/package-mixins.js +++ b/package-mixins.js @@ -3,7 +3,7 @@ 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 = function PackageMixinFactory(NativeCodePush) { +module.exports = (NativeCodePush) => { const remote = { abortDownload() { return NativeCodePush.abortDownload(this);