diff --git a/CodePush.js b/CodePush.js index f2aac01..288c254 100644 --- a/CodePush.js +++ b/CodePush.js @@ -173,7 +173,7 @@ const notifyApplicationReady = (() => { async function notifyApplicationReadyInternal() { await NativeCodePush.notifyApplicationReady(); const statusReport = await NativeCodePush.getNewStatusReport(); - tryReportStatus(statusReport); // Don't wait for this to complete. + statusReport && tryReportStatus(statusReport); // Don't wait for this to complete. return statusReport; } diff --git a/Examples/CodePushDemoApp/android/app/src/main/java/com/microsoft/codepushdemoapp/MainActivity.java b/Examples/CodePushDemoApp/android/app/src/main/java/com/microsoft/codepushdemoapp/MainActivity.java index 2b2a36f..aa4d814 100644 --- a/Examples/CodePushDemoApp/android/app/src/main/java/com/microsoft/codepushdemoapp/MainActivity.java +++ b/Examples/CodePushDemoApp/android/app/src/main/java/com/microsoft/codepushdemoapp/MainActivity.java @@ -12,7 +12,7 @@ public class MainActivity extends ReactActivity { @Override protected String getJSBundleFile() { - return CodePush.getBundleUrl("index.android.bundle"); + return CodePush.getJSBundleFile(); } /** diff --git a/README.md b/README.md index aaaf797..fe8b2f7 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ This plugin provides client-side integration for the [CodePush service](http://c * [Debugging / Troubleshooting](#debugging--troubleshooting) * [Example Apps / Starters](#example-apps--starters) * [Continuous Integration / Delivery](#continuous-integration--delivery) +* [TypeScript Consumption](#typescript-consumption) ## How does it work? @@ -27,7 +28,7 @@ A React Native app is composed of JavaScript files and any accompanying [images] The CodePush plugin helps get product improvements in front of your end users instantly, by keeping your JavaScript and images synchronized with updates you release to the CodePush server. This way, your app gets the benefits of an offline mobile experience, as well as the "web-like" agility of side-loading updates as soon as they are available. It's a win-win! -In order to ensure that your end users always have a functioning version of your app, the CodePush plugin maintains a copy of the previous update, so that in the event that you accidentally push an update which includes a crash, it can automatically roll back. This way, you can rest assured that your newfound release agility won't result in users becoming blocked before you have a chance to [roll back](http://microsoft.github.io/code-push/docs/cli.html#link-10) on the server. It's a win-win-win! +In order to ensure that your end users always have a functioning version of your app, the CodePush plugin maintains a copy of the previous update, so that in the event that you accidentally push an update which includes a crash, it can automatically roll back. This way, you can rest assured that your newfound release agility won't result in users becoming blocked before you have a chance to [roll back](http://microsoft.github.io/code-push/docs/cli.html#link-10) on the server. It's a win-win-win! *Note: Any product changes which touch native code (e.g. modifying your `AppDelegate.m`/`MainActivity.java` file, adding a new plugin) cannot be distributed via CodePush, and therefore, must be updated via the appropriate store(s).* @@ -44,14 +45,14 @@ We try our best to maintain backwards compatability of our plugin with previous | <0.14.0 | **Unsupported** | | v0.14.0 | v1.3.0 *(introduced Android support)* | | v0.15.0-v0.18.0 | v1.4.0-v1.6.0 *(introduced iOS asset support)* | -| v0.19.0-v0.28.0 | v1.7.0+ *(introduced Android asset support)* | -| v0.29.0+ | TBD :) We work hard to respond to new RN releases, but they do occasionally break us. We will update this chart with each RN release, so that users can check to see what our "official" support is. +| v0.19.0-v0.29.0 | v1.7.0+ *(introduced Android asset support)* | +| v0.30.0+ | TBD :) We work hard to respond to new RN releases, but they do occasionally break us. We will update this chart with each RN release, so that users can check to see what our "official" support is. ## Supported Components When using the React Native assets sytem (i.e. using the `require("./foo.png")` syntax), the following list represents the set of core components (and props) that support having their referenced images updated via CodePush: -| Component | Prop(s) | +| Component | Prop(s) | |-------------------------------------------------|------------------------------------------| | `Image` | `source` | | `MapView.Marker`
*(Requires [react-native-maps](https://github.com/lelandrichardson/react-native-maps) `>=O.3.2`)* | `image` | @@ -78,7 +79,7 @@ npm install --save react-native-code-push@latest As with all other React Native plugins, the integration experience is different for iOS and Android, so perform the following setup steps depending on which platform(s) you are targetting. -If you want to see how other projects have integrated with CodePush, you can check out the excellent [example apps](#example-apps) provided by the community. +If you want to see how other projects have integrated with CodePush, you can check out the excellent [example apps](#example-apps--starters) provided by the community. Additionally, if you'd like to quickly familiarize yourself with CodePush + React Native, you can check out the awesome [getting started video](https://www.youtube.com/watch?v=uN0FRWk-YW8&feature=youtu.be) shot by [Bilal Budhani](https://twitter.com/BilalBudhani). ## iOS Setup @@ -86,9 +87,9 @@ Once you've acquired the CodePush plugin, you need to integrate it into the Xcod ### Plugin Installation (iOS) -In order to accomodate as many developer preferences as possible, the CodePush plugin supports iOS installation via three mechanisms: +In order to accommodate as many developer preferences as possible, the CodePush plugin supports iOS installation via three mechanisms: -1. [**RNPM**](#plugin-installation-ios---rnpm) - [React Native Package Manager (RNPM)](https://github.com/rnpm/rnpm) is an awesome tool that provides the simplest installation experience possible for React Native plugins. If you're already using it, or you want to use it, then we recommend this approach. +1. [**RNPM**](#plugin-installation-ios---rnpm) - [React Native Package Manager (RNPM)](https://github.com/rnpm/rnpm) is an awesome tool that provides the simplest installation experience possible for React Native plugins. If you're already using it, or you want to use it, then we recommend this approach. 2. [**CocoaPods**](#plugin-installation-ios---cocoapods) - If you're building a native iOS app that is embedding React Native into it, or you simply prefer using [CocoaPods](https://cocoapods.org), then we recommend using the Podspec file that we ship as part of our plugin. @@ -98,19 +99,9 @@ In order to accomodate as many developer preferences as possible, the CodePush p 1. Run `rnpm link react-native-code-push` - *Note: If you don't already have RNPM installed, you can do so by simply running `npm i -g rnpm` and then executing the above command.* - -2. Open your app's Xcode project + *Note: If you don't already have RNPM installed, you can do so by simply running `npm i -g rnpm` and then executing the above command. If you already have RNPM installed, make sure you have v1.9.0+ in order to benefit from this one step install.* -3. Select the project node in Xcode and select the "Build Phases" tab of your project configuration. - -4. Click the plus sign underneath the "Link Binary With Libraries" list and select the `libz.tbd` library underneath the `iOS` node that matches your target version. - - ![Libz reference](https://cloud.githubusercontent.com/assets/116461/11605042/6f786e64-9aaa-11e5-8ca7-14b852f808b1.png) - - *Note: Alternatively, if you prefer, you can add the `-lz` flag to the `Other Linker Flags` field in the `Linking` section of the `Build Settings`.* - -We hope to eventually remove the need for steps #2-4, but in the meantime, RNPM doesn't support automating them. +And that's it! Isn't RNPM awesome? :) #### Plugin Installation (iOS - CocoaPods) @@ -119,20 +110,20 @@ We hope to eventually remove the need for steps #2-4, but in the meantime, RNPM ```ruby pod 'CodePush', :path => './node_modules/react-native-code-push' ``` - + CodePush depends on an internal copy of the `SSZipArchive` library, so if your project already includes it (either directly or via a transitive dependency), then you can install a version of CodePush which excludes it by depending specificaly on the `Core` subspec: - + ```ruby pod 'CodePush', :path => './node_modules/react-native-code-push', :subspecs => ['Core'] ``` - + *NOTE: The above paths needs to be relative to your app's `Podfile`, so adjust it as nec cessary.* - + 2. Run `pod install` *NOTE: The CodePush `.podspec` depends on the `React` pod, and so in order to ensure that it can correctly use the version of React Native that your app is built with, please make sure to define the `React` dependency in your app's `Podfile` as explained [here](http://facebook.github.io/react-native/docs/embedded-app-ios.html#install-react-native-using-cocoapods).* - + #### Plugin Installation (iOS - Manual) 1. Open your app's Xcode project @@ -147,12 +138,12 @@ We hope to eventually remove the need for steps #2-4, but in the meantime, RNPM ![Link CodePush during build](https://cloud.githubusercontent.com/assets/516559/10322221/a75ea066-6c31-11e5-9d88-ff6f6a4d6968.png) -5. Click the plus sign underneath the "Link Binary With Libraries" list and select the `libz.tbd` library underneath the `iOS 9.1` node. +5. Click the plus sign underneath the "Link Binary With Libraries" list and select the `libz.tbd` library underneath the `iOS 9.1` node. ![Libz reference](https://cloud.githubusercontent.com/assets/116461/11605042/6f786e64-9aaa-11e5-8ca7-14b852f808b1.png) - + *Note: Alternatively, if you prefer, you can add the `-lz` flag to the `Other Linker Flags` field in the `Linking` section of the `Build Settings`.* - + 6. Under the "Build Settings" tab of your project configuration, find the "Header Search Paths" section and edit the value. Add a new value, `$(SRCROOT)/../node_modules/react-native-code-push` and select "recursive" in the dropdown. @@ -173,7 +164,7 @@ Once your Xcode project has been setup to build/link the CodePush plugin, you ne ```objective-c jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; ``` - + 3. Replace it with this line: ```objective-c @@ -184,7 +175,7 @@ This change configures your app to always load the most recent version of your a *NOTE: The `bundleURL` method assumes your app's JS bundle is named `main.jsbundle`. If you have configured your app to use a different file name, simply call the `bundleURLForResource:` method (which assumes you're using the `.jsbundle` extension) or `bundleURLForResource:withExtension:` method instead, in order to overwrite that default behavior* -Typically, you're only going to want to use CodePush to resolve your JS bundle location within release builds, and therefore, we recommend using the `DEBUG` pre-processor macro to dynamically switch between using the packager server and CodePush, depending on whether you are debugging or not. This will make it much simpler to ensure you get the right behavior you want in production, while still being able to use the Chrome Dev Tools, live reload, etc. at debug-time. +Typically, you're only going to want to use CodePush to resolve your JS bundle location within release builds, and therefore, we recommend using the `DEBUG` pre-processor macro to dynamically switch between using the packager server and CodePush, depending on whether you are debugging or not. This will make it much simpler to ensure you get the right behavior you want in production, while still being able to use the Chrome Dev Tools, live reload, etc. at debug-time. ```objective-c NSURL *jsCodeLocation; @@ -195,11 +186,11 @@ NSURL *jsCodeLocation; jsCodeLocation = [CodePush bundleURL]; #endif ``` - + To let the CodePush runtime know which deployment it should query for updates against, open your app's `Info.plist` file and add a new entry named `CodePushDeploymentKey`, whose value is the key of the deployment you want to configure this app against (e.g. the key for the `Staging` deployment for the `FooBar` app). You can retrieve this value by running `code-push deployment ls -k` in the CodePush CLI (the `-k` flag is necessary since keys aren't displayed by default) and copying the value of the `Deployment Key` column which corresponds to the deployment you want to use (see below). Note that using the deployment's name (e.g. Staging) will not work. That "friendly name" is intended only for authenticated management usage from the CLI, and not for public consumption within your app. ![Deployment list](https://cloud.githubusercontent.com/assets/116461/11601733/13011d5e-9a8a-11e5-9ce2-b100498ffb34.png) - + In order to effectively make use of the `Staging` and `Production` deployments that were created along with your CodePush app, refer to the [multi-deployment testing](#multi-deployment-testing) docs below before actually moving your app's usage of CodePush into production. ## Android Setup @@ -208,9 +199,9 @@ In order to integrate CodePush into your Android project, perform the following ### Plugin Installation (Android) -In order to accomodate as many developer preferences as possible, the CodePush plugin supports Android installation via two mechanisms: +In order to accommodate as many developer preferences as possible, the CodePush plugin supports Android installation via two mechanisms: -1. [**RNPM**](#plugin-installation-android---rnpm) - [React Native Package Manager (RNPM)](https://github.com/rnpm/rnpm) is an awesome tool that provides the simplest installation experience possible for React Native plugins. If you're already using it, or you want to use it, then we recommend this approach. +1. [**RNPM**](#plugin-installation-android---rnpm) - [React Native Package Manager (RNPM)](https://github.com/rnpm/rnpm) is an awesome tool that provides the simplest installation experience possible for React Native plugins. If you're already using it, or you want to use it, then we recommend this approach. 2. [**"Manual"**](#plugin-installation-android---manual) - If you don't want to depend on any additional tools or are fine with a few extra installation steps (it's a one-time thing), then go with this approach. @@ -219,31 +210,31 @@ In order to accomodate as many developer preferences as possible, the CodePush p 1. Run `rnpm link react-native-code-push` *Note: If you don't already have RNPM installed, you can do so by simply running `npm i -g rnpm` and then executing the above command.* - + 2. If you're using RNPM >=1.6.0, you will be prompted for the deployment key you'd like to use. If you don't already have it, you can retreive this value by running `code-push deployment ls -k`, or you can choose to ignore it (by simply hitting ``) and add it in later. To get started, we would recommend just using your `Staging` deployment key, so that you can test out the CodePush end-to-end. 3. (Only needed in v1.8.0+ of the plugin) In your `android/app/build.gradle` file, add the `codepush.gradle` file as an additional build task definition underneath `react.gradle`: - + ```gradle ... - apply from: "react.gradle" + apply from: "../../node_modules/react-native/react.gradle" apply from: "../../node_modules/react-native-code-push/android/codepush.gradle" ... ``` - + And that's it for installation using RNPM! Continue below to the [Plugin Configuration](#plugin-configuration-android) section to complete the setup. #### Plugin Installation (Android - Manual) 1. In your `android/settings.gradle` file, make the following additions: - + ```gradle include ':app', ':react-native-code-push' project(':react-native-code-push').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-code-push/android/app') ``` - + 2. In your `android/app/build.gradle` file, add the `:react-native-code-push` project as a compile-time dependency: - + ```gradle ... dependencies { @@ -251,9 +242,9 @@ And that's it for installation using RNPM! Continue below to the [Plugin Configu compile project(':react-native-code-push') } ``` - + 3. (Only needed in v1.8.0+ of the plugin) In your `android/app/build.gradle` file, add the `codepush.gradle` file as an additional build task definition underneath `react.gradle`: - + ```gradle ... apply from: "react.gradle" @@ -265,8 +256,47 @@ And that's it for installation using RNPM! Continue below to the [Plugin Configu *Note: If you are using an older version (<=1.9.0-beta) of the CodePush plugin, please refer to [these docs](https://github.com/Microsoft/react-native-code-push/tree/e717eb024fe9d1810ac21c40c097f7bc165ea5f1#plugin-configuration-android---react-native--v0180) instead.* -After installing the plugin and syncing your Android Studio project with Gradle, you need to configure your app to consult CodePush for the location of your JS bundle, since it will "take control" of managing the current and all future versions. To do this, update the `MainActivity.java` file to use CodePush via the following changes: - +After installing the plugin and syncing your Android Studio project with Gradle, you need to configure your app to consult CodePush for the location of your JS bundle, since it will "take control" of managing the current and all future versions. To do this: + +**For React Native >= v0.29** + +Update the `MainApplication.java` file to use CodePush via the following changes: + +```java +... +// 1. Import the plugin class. +import com.microsoft.codepush.react.CodePush; + +public class MainApplication extends Application implements ReactApplication { + + private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { + ... + // 2. Override the getJSBundleFile method in order to let + // the CodePush runtime determine where to get the JS + // bundle location from on each app start + @Override + protected String getJSBundleFile() { + return CodePush.getJSBundleFile(); + } + + @Override + protected List getPackages() { + // 3. Instantiate an instance of the CodePush runtime and add it to the list of + // existing packages, specifying the right deployment key. If you don't already + // have it, you can run "code-push deployment ls -k" to retrieve your key. + return Arrays.asList( + new MainReactPackage(), + new CodePush("deployment-key-here", MainApplication.this, BuildConfig.DEBUG) + ); + } + }; +} +``` + +**For React Native v0.19 - v0.28** + +Update the `MainActivity.java` file to use CodePush via the following changes: + ```java ... // 1. Import the plugin class (if you used RNPM to install the plugin, this @@ -279,13 +309,13 @@ public class MainActivity extends ReactActivity { // bundle location from on each app start @Override protected String getJSBundleFile() { - return CodePush.getBundleUrl(); + return CodePush.getJSBundleFile(); } @Override protected List getPackages() { // 3. Instantiate an instance of the CodePush runtime and add it to the list of - // existing packages, specifying the right deployment key. If you don't already + // existing packages, specifying the right deployment key. If you don't already // have it, you can run "code-push deployment ls -k" to retrieve your key. return Arrays.asList( new MainReactPackage(), @@ -310,7 +340,7 @@ Once you've acquired the CodePush plugin, you need to integrate it into the Visu 2. Right-click the solution node in the `Solution Explorer` window and select the `Add -> Existing Project...` menu item ![Add Project](https://cloud.githubusercontent.com/assets/116461/14467164/ddf6312e-008e-11e6-8a10-44a8b44b5dfc.PNG) - + 3. Browse to the `node_modules\react-native-code-push\windows` directory, select the `CodePush.csproj` file and click `OK` 4. Back in the `Solution Explorer`, right-click the project node that is named after your app, and select the `Add -> Reference...` menu item @@ -320,23 +350,23 @@ Once you've acquired the CodePush plugin, you need to integrate it into the Visu 5. Select the `Projects` tab on the left hand side, check the `CodePush` item and then click `OK` ![Add Reference Dialog](https://cloud.githubusercontent.com/assets/116461/14467147/cb805b6e-008e-11e6-964f-f856c59b65af.PNG) - + ### Plugin Configuration (Windows) After installing the plugin, you need to configure your app to consult CodePush for the location of your JS bundle, since it will "take control" of managing the current and all future versions. To do this, update the `AppReactPage.cs` file to use CodePush via the following changes: ```c# ... -// 1. Import the CodePush namespace +// 1. Import the CodePush namespace using CodePush.ReactNative; ... class AppReactPage : ReactPage { // 2. Declare a private instance variable for the CodePushModule instance. private CodePushReactPackage codePushReactPackage; - + // 3. Update the JavaScriptBundleFile property to initalize the CodePush runtime, - // specifying the right deployment key, then use it to return the bundle URL from + // specifying the right deployment key, then use it to return the bundle URL from // CodePush instead of statically from the binary. If you don't already have your // deployment key, you can run "code-push deployment ls -k" to retrieve it. public override string JavaScriptBundleFile @@ -348,7 +378,7 @@ class AppReactPage : ReactPage } } - // 4. Add the codePushReactPackage instance to the list of existing packages. + // 4. Add the codePushReactPackage instance to the list of existing packages. public override List Packages { get @@ -405,7 +435,7 @@ Additionally, if you would like to display an update confirmation dialog (an "ac ## Releasing Updates -Once your app has been configured and distributed to your users, and you've made some JS and/or asset changes, it's time to instantly release them! The simplest (and recommended) way to do this is to use the `release-react` command in the CodePush CLI, which will handle bundling your JavaScript and asset files and releasing the update to the CodePush server. +Once your app has been configured and distributed to your users, and you've made some JS and/or asset changes, it's time to instantly release them! The simplest (and recommended) way to do this is to use the `release-react` command in the CodePush CLI, which will handle bundling your JavaScript and asset files and releasing the update to the CodePush server. In it's most basic form, this command only requires two parameters: your app name and the platform you are bundling the update for (either `ios` or `android`). @@ -416,7 +446,7 @@ code-push release-react MyApp ios code-push release-react MyApp-Android android ``` -The `release-react` command enables such a simple workflow because it provides many sensible defaults (e.g. generating a release bundle, assuming your app's entry file on iOS is either `index.ios.js` or `index.js`). However, all of these defaults can be customized to allow incremental flexibility as necessary, which makes it a good fit for most scenarios. +The `release-react` command enables such a simple workflow because it provides many sensible defaults (e.g. generating a release bundle, assuming your app's entry file on iOS is either `index.ios.js` or `index.js`). However, all of these defaults can be customized to allow incremental flexibility as necessary, which makes it a good fit for most scenarios. ```shell # Release a mandatory update with a changelog @@ -447,7 +477,7 @@ In our [getting started](#getting-started) docs, we illustrated how to configure *NOTE: Our client-side rollback feature can help unblock users after installing a release that resulted in a crash, and server-side rollbacks (i.e. `code-push rollback`) allow you to prevent additional users from installing a bad release once it's been identified. However, it's obviously better if you can prevent an erroneous update from being broadly released in the first place.* -Taking advantage of the `Staging` and `Production` deployments allows you to acheive a workflow like the following (feel free to customize!): +Taking advantage of the `Staging` and `Production` deployments allows you to achieve a workflow like the following (feel free to customize!): 1. Release a CodePush update to your `Staging` deployment using the `code-push release-react` command (or `code-push release` if you need more control) @@ -459,7 +489,7 @@ Taking advantage of the `Staging` and `Production` deployments allows you to ach *NOTE: If you want to get really fancy, you can even choose to perform a "staged rollout" as part of #3, which allows you to mitigate additional potential risk with the update (e.g. did your testing in #2 touch all possible devices/conditions?) by only making the production update available to a percentage of your users (e.g. `code-push promote Staging Production -r 20%`). Then, after waiting for a reasonable amount of time to see if any crash reports or customer feedback comes in, you can expand it to your entire audience by running `code-push patch Production -r 100%`.* -You'll notice that the above steps refer to a "staging build" and "production build" of your app. If your build process already generates distinct binaries per "environment", then you don't need to read any further, since swapping out CodePush deployment keys is just like handling environment-specific config for any other service your app uses (e.g. Facebook). However, if you're looking for examples on how to setup your build process to accomodate this, then refer to the following sections, depending on the platform(s) your app is targeting. +You'll notice that the above steps refer to a "staging build" and "production build" of your app. If your build process already generates distinct binaries per "environment", then you don't need to read any further, since swapping out CodePush deployment keys is just like handling environment-specific config for any other service your app uses (e.g. Facebook). However, if you're looking for examples on how to setup your build process to accommodate this, then refer to the following sections, depending on the platform(s) your app is targeting. ### Android @@ -470,20 +500,20 @@ To set this up, perform the following steps: 1. Open your app's `build.gradle` file (e.g. `android/app/build.gradle` in standard React Native projects) 2. Find the `android { buildTypes {} }` section and define `buildConfigField` entries for both your `debug` and `release` build types, which reference your `Staging` and `Production` deployment keys respectively. If you prefer, you can define the key literals in your `gradle.properties` file, and then reference them here. Either way will work, and it's just a matter of personal preference. - + ```groovy android { - ... + ... buildTypes { debug { ... - buildConfigField "String", "CODEPUSH_KEY", "" + buildConfigField "String", "CODEPUSH_KEY", '""' ... } - + release { ... - buildConfigField "String", "CODEPUSH_KEY", "" + buildConfigField "String", "CODEPUSH_KEY", '""' ... } } @@ -492,15 +522,15 @@ To set this up, perform the following steps: ``` *NOTE: As a reminder, you can retrieve these keys by running `code-push deployment ls -k` from your terminal.* - -4. Open up your `MainAtivity.java` file and change the `CodePush` constructor to pass the deployment key in via the build config you just defined, as opposed to a string literal. + +4. Open up your `MainActivity.java` file and change the `CodePush` constructor to pass the deployment key in via the build config you just defined, as opposed to a string literal. ```java new CodePush(BuildConfig.CODEPUSH_KEY, this, BuildConfig.DEBUG); ``` - + *Note: If you gave your build setting a different name in your Gradle file, simply make sure to reflect that in your Java code.* - + And that's it! Now when you run or build your app, your debug builds will automatically be configured to sync with your `Staging` deployment, and your release builds will be configured to sync with your `Production` deployment. *NOTE: By default, the `react-native run-android` command builds and deploys the debug version of your app, so if you want to test out a release/production build, simply run `react-native run-android --variant release. Refer to the [React Native docs](http://facebook.github.io/react-native/docs/signed-apk-android.html#conten) for details about how to configure and create release builds for your Android apps.* @@ -542,7 +572,7 @@ To set this up, perform the following steps: 4. Click the `+` button within the `Configurations` section and select `Duplicate "Release" Configuration` ![Configuration](https://cloud.githubusercontent.com/assets/116461/16101597/088714c0-331c-11e6-9504-5469d9a59d74.png) - + 5. Name the new configuration `Staging` (or whatever you prefer) 6. Select the `Build Settings` tab @@ -554,13 +584,13 @@ To set this up, perform the following steps: 8. Name this new setting something like `CODEPUSH_KEY`, expand it, and specify your `Staging` deployment key for the `Staging` config and your `Production` deployment key for the `Release` config. ![Setting Keys](https://cloud.githubusercontent.com/assets/116461/15764230/106c245c-28de-11e6-96fe-2615f9220b07.png) - + *NOTE: As a reminder, you can retrieve these keys by running `code-push deployment ls -k` from your terminal.* - + 9. Open your project's `Info.plist` file and change the value of your `CodePushDeploymentKey` entry to `$(CODEPUSH_KEY)` ![Infoplist](https://cloud.githubusercontent.com/assets/116461/15764252/3ac8aed2-28de-11e6-8c19-2270ae9857a7.png) - + And that's it! Now when you run or build your app, your staging builds will automatically be configured to sync with your `Staging` deployment, and your release builds will be configured to sync with your `Production` deployment. Additionally, if you want to give them seperate names and/or icons, you can modify the `Product Name` and `Asset Catalog App Icon Set Name` build settings, which will allow your staging builds to be distinguishable from release builds when installed on the same device. @@ -615,7 +645,7 @@ When you require `react-native-code-push`, the module object provides the follow * [allowRestart](#codepushallowrestart): Re-allows programmatic restarts to occur as a result of an update being installed, and optionally, immediately restarts the app if a pending update had attempted to restart the app while restarts were disallowed. This is an advanced API and is only necessary if your app explicitly disallowed restarts via the `disallowRestart` method. -* [checkForUpdate](#codepushcheckforupdate): Asks the CodePush service whether the configured app deployment has an update available. +* [checkForUpdate](#codepushcheckforupdate): Asks the CodePush service whether the configured app deployment has an update available. * [disallowRestart](#codepushdisallowrestart): Temporarily disallows any programmatic restarts to occur as a result of a CodePush update being installed. This is an advanced API, and is useful when a component within your app (e.g. an onboarding process) needs to ensure that no end-user interruptions can occur during its lifetime. @@ -673,13 +703,13 @@ This method returns a `Promise` which resolves to one of two possible values: 2. A [`RemotePackage`](#remotepackage) instance which represents an available update that can be inspected and/or subsequently downloaded. -Example Usage: +Example Usage: ```javascript codePush.checkForUpdate() .then((update) => { if (!update) { - console.log("The app is up to date!"); + console.log("The app is up to date!"); } else { console.log("An update is available! Should we download it?"); } @@ -706,26 +736,26 @@ This is an advanced API, and is primarily useful when individual components with As an alternative, you could also choose to simply use `InstallMode.ON_NEXT_RESTART` whenever calling `sync` (which will never attempt to programmatically restart the app), and then explicity calling `restartApp` at points in your app that you know it is "safe" to do so. `disallowRestart` provides an alternative approach to this when the code that synchronizes with the CodePush server is separate from the code/components that want to enforce a no-restart policy. -Example Usage: +Example Usage: ```javascript class OnboardingProcess extends Component { ... - + componentWillMount() { // Ensure that any CodePush updates which are // synchronized in the background can't trigger // a restart while this component is mounted. - codePush.disallowRestart(); + codePush.disallowRestart(); } - + componentWillUnmount() { // Reallow restarts, and optionally trigger // a restart if one was currently pending. codePush.allowRestart(); } - - ... + + ... } ``` @@ -748,7 +778,7 @@ This method returns a `Promise` which resolves to one of two possible values: 2. A [`LocalPackage`](#localpackage) instance which represents the metadata for the currently running CodePush update. -Example Usage: +Example Usage: ```javascript codePush.getCurrentPackage() @@ -775,16 +805,16 @@ This method returns a `Promise` which resolves to one of two possible values: 1. `null` if an update with the specified state doesn't currently exist. This occurs in the following scenarios: 1. The end-user hasn't installed any CodePush updates yet, and therefore, no metadata is available for any updates, regardless what you specify as the `updateState` parameter. - + 2. The end-user installed an update of the binary (e.g. from the store), which cleared away the old CodePush updates, and gave precedence back to the JS binary in the binary. Therefore, it would exhibit the same behavior as #1 - + 3. The `updateState` parameter is set to `UpdateState.RUNNING`, but the app isn't currently running a CodePush update. There may be a pending update, but the app hasn't been restarted yet in order to make it active. - + 4. The `updateState` parameter is set to `UpdateState.PENDING`, but the app doesn't have any currently pending updates. 2. A [`LocalPackage`](#localpackage) instance which represents the metadata for the currently requested CodePush update (either the running or pending). -Example Usage: +Example Usage: ```javascript // Check if there is currently a CodePush update running, and if @@ -799,7 +829,7 @@ codePush.getUpdateMetadata().then((update) => { // Check to see if there is still an update pending. codePush.getUpdateMetadata(UpdateState.PENDING).then((update) => { if (update) { - // There's a pending update, do we want to force a restart? + // There's a pending update, do we want to force a restart? } }); ``` @@ -816,19 +846,19 @@ If you are using the `sync` function, and doing your update check on app start, *NOTE: This method is also aliased as `notifyApplicationReady` (for backwards compatibility).* -#### codePush.restartApp - -```javascript -codePush.restartApp(onlyIfUpdateIsPending: Boolean = false): void; -``` - +#### codePush.restartApp + +```javascript +codePush.restartApp(onlyIfUpdateIsPending: Boolean = false): void; +``` + Immediately restarts the app. If a truthy value is provided to the `onlyIfUpdateIsPending` parameter, then the app will only restart if there is actually a pending update waiting to be applied. -This method is for advanced scenarios, and is primarily useful when the following conditions are true: - +This method is for advanced scenarios, and is primarily useful when the following conditions are true: + 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. +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 @@ -844,11 +874,11 @@ This method provides support for two different (but customizable) "modes" to eas 2. **Active mode**, which when an update is available, prompts the end user for permission before downloading it, and then immediately applies the update. If an update was released using the `mandatory` flag, the end user would still be notified about the update, but they wouldn't have the choice to ignore it. -Example Usage: +Example Usage: ```javascript // Fully silent update which keeps the app in -// sync with the server, without ever +// sync with the server, without ever // interrupting the end user codePush.sync(); @@ -869,25 +899,25 @@ While the `sync` method tries to make it easy to perform silent and active updat * __mandatoryInstallMode__ *(codePush.InstallMode)* - Specifies when you would like to install updates which are marked as mandatory. Defaults to `codePush.InstallMode.IMMEDIATE`. Refer to the [`InstallMode`](#installmode) enum reference for a description of the available options and what they do. * __minimumBackgroundDuration__ *(Number)* - Specifies the minimum number of seconds that the app needs to have been in the background before restarting the app. This property only applies to updates which are installed using `InstallMode.ON_NEXT_RESUME`, and can be useful for getting your update in front of end users sooner, without being too obtrusive. Defaults to `0`, which has the effect of applying the update immediately after a resume, regardless how long it was in the background. - + * __updateDialog__ *(UpdateDialogOptions)* - An "options" object used to determine whether a confirmation dialog should be displayed to the end user when an update is available, and if so, what strings to use. Defaults to `null`, which has the effect of disabling the dialog completely. Setting this to any truthy value will enable the dialog with the default strings, and passing an object to this parameter allows enabling the dialog as well as overriding one or more of the default strings. Before enabling this option within an App Store-distributed app, please refer to [this note](#user-content-apple-note). The following list represents the available options and their defaults: - + * __appendReleaseDescription__ *(Boolean)* - Indicates whether you would like to append the description of an available release to the notification message which is displayed to the end user. Defaults to `false`. - + * __descriptionPrefix__ *(String)* - Indicates the string you would like to prefix the release description with, if any, when displaying the update notification to the end user. Defaults to `" Description: "` - + * __mandatoryContinueButtonLabel__ *(String)* - The text to use for the button the end user must press in order to install a mandatory update. Defaults to `"Continue"`. - + * __mandatoryUpdateMessage__ *(String)* - The text used as the body of an update notification, when the update is specified as mandatory. Defaults to `"An update is available that must be installed."`. - + * __optionalIgnoreButtonLabel__ *(String)* - The text to use for the button the end user can press in order to ignore an optional update that is available. Defaults to `"Ignore"`. - + * __optionalInstallButtonLabel__ *(String)* - The text to use for the button the end user can press in order to install an optional update. Defaults to `"Install"`. - + * __optionalUpdateMessage__ *(String)* - The text used as the body of an update notification, when the update is optional. Defaults to `"An update is available. Would you like to install it?"`. - + * __title__ *(String)* - The text used as the header of an update notification that is displayed to the end user. Defaults to `"Update available"`. Example Usage: @@ -916,7 +946,7 @@ codePush.sync({ updateDialog: { title: "An update is available!" } }); codePush.sync({ updateDialog: { appendReleaseDescription: true, - descriptionPrefix: "\n\nChange log:\n" + descriptionPrefix: "\n\nChange log:\n" }, installMode: codePush.InstallMode.IMMEDIATE }); @@ -929,15 +959,15 @@ In addition to the options, the `sync` method also accepts two optional function * __downloadProgressCallback__ *((progress: DownloadProgress) => void)* - Called periodically when an available update is being downloaded from the CodePush server. The method is called with a `DownloadProgress` object, which contains the following two properties: * __totalBytes__ *(Number)* - The total number of bytes expected to be received for this update (i.e. the size of the set of files which changed from the previous release). - + * __receivedBytes__ *(Number)* - The number of bytes downloaded thus far, which can be used to track download progress. Example Usage: ```javascript // Prompt the user when an update is available -// and then display a "downloading" modal -codePush.sync({ updateDialog: true }, +// and then display a "downloading" modal +codePush.sync({ updateDialog: true }, (status) => { switch (status) { case codePush.SyncStatus.DOWNLOADING_PACKAGE: @@ -948,8 +978,8 @@ codePush.sync({ updateDialog: true }, break; } }, - ({ receivedBytes, totalBytes, }) => { - /* Update download modal progress */ + ({ receivedBytes, totalBytes, }) => { + /* Update download modal progress */ } ); ``` @@ -992,7 +1022,7 @@ Contains details about an update that has been downloaded locally or already ins ###### Methods -- __install(installMode: codePush.InstallMode = codePush.InstallMode.ON_NEXT_RESTART, minimumBackgroundDuration = 0): 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. If the `installMode` parameter is set to `InstallMode.ON_NEXT_RESUME`, then the `minimumBackgroundDuration` parameter allows you to control how long the app must have been in the background before forcing the install after it is resumed. +- __install(installMode: codePush.InstallMode = codePush.InstallMode.ON_NEXT_RESTART, minimumBackgroundDuration = 0): 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. If the `installMode` parameter is set to `InstallMode.ON_NEXT_RESUME`, then the `minimumBackgroundDuration` parameter allows you to control how long the app must have been in the background before forcing the install after it is resumed. ##### RemotePackage @@ -1019,7 +1049,7 @@ This enum specifies when you would like an installed update to actually be appli * __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. Additionally, this mode can be used to enforce mandatory updates, since it removes the potentially undesired latency between the update installation and the next time the end user restarts or resumes the app. * __codePush.InstallMode.ON_NEXT_RESTART__ *(1)* - Indicates that you want to install the update, but not forcibly restart the app. When the app is "naturally" restarted (due the OS or end user killing it), the update will be seamlessly picked up. This value is appropriate when performing silent updates, since it would likely be disruptive to the end user if the app suddenly restarted out of nowhere, since they wouldn't have realized an update was even downloaded. This is the default mode used for both the `sync` and `LocalPackage.install` methods. - + * __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 @@ -1034,7 +1064,7 @@ 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.SYNC_IN_PROGRESS__ *(7)* - There is an ongoing `sync` operation running which prevents the current call from being executed. -* __codePush.SyncStatus.UNKNOWN_ERROR__ *(-1)* - The sync operation encountered an unknown error. +* __codePush.SyncStatus.UNKNOWN_ERROR__ *(-1)* - The sync operation encountered an unknown error. ##### UpdateState @@ -1043,7 +1073,7 @@ This enum specifies the state that an update is currently in, and can be specifi * __codePush.UpdateState.RUNNING__ *(0)* - Indicates that an update represents the version of the app that is currently running. This can be useful for identifying attributes about the app, for scenarios such as displaying the release description in a "what's new?" dialog or reporting the latest version to an analytics and/or crash reporting service. * __codePush.UpdateState.PENDING__ *(1)* - Indicates than an update has been installed, but the app hasn't been restarted yet in order to apply it. This can be useful for determining whether there is a pending update, which you may want to force a programmatic restart (via `restartApp`) in order to apply. - + * __codePush.UpdateState.LATEST__ *(2)* - Indicates than an update represents the latest available release, and can be either currently running or pending. ### Objective-C API Reference (iOS) @@ -1052,9 +1082,9 @@ The Objective-C API is made available by importing the `CodePush.h` header into #### 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. +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: +The `CodePush` class' methods can be thought of as composite resolvers which always load the appropriate bundle, in order to accommodate 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 :) @@ -1064,7 +1094,7 @@ The `CodePush` class' methods can be thought of as composite resolvers which alw 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 necesary, and rest assured that your end-users will always get the most recent version. +Because of this behavior, you can safely deploy updates to both the app store(s) and CodePush as necesary, and rest assured that your end-users will always get the most recent version. ##### Methods @@ -1091,9 +1121,9 @@ Constructs the CodePush client runtime and represents the `ReactPackage` instanc - __CodePush(String deploymentKey, Activity mainActivity, bool isDebugMode)__ - Equivalent to the previous constructor, but allows you to specify whether you want the CodePush runtime to be in debug mode or not. When using this constructor, the `isDebugMode` parameter should always be set to `BuildConfig.DEBUG` in order to stay synchronized with your build type. When putting CodePush into debug mode, the following behaviors are enabled: 1. Old CodePush updates aren't deleted from storage whenever a new binary is deployed to the emulator/device. This behavior enables you to deploy new binaries, without bumping the version during development, and without continuously getting the same update every time your app calls `sync`. - + 2. The local cache that the React Native runtime maintains in debug mode is deleted whenever a CodePush update is installed. This ensures that when the app is restarted after an update is applied, you will see the expected changes. As soon as [this PR](https://github.com/facebook/react-native/pull/4738) is merged, we won't need to do this anymore. - + ##### Static Methods - __getBundleUrl()__ - Returns the path to the most recent version of your app's JS bundle file, assuming that the resource name is `index.android.bundle`. If your app is using a different bundle name, then use the overloaded version of this method which allows specifying it. This method has the same resolution behavior as the Objective-C equivalent described above. @@ -1151,3 +1181,13 @@ In addition to being able to use the CodePush CLI to "manually" release updates, * [Visual Studio Team Services](https://marketplace.visualstudio.com/items?itemName=ms-vsclient.code-push) - *NOTE: VSTS also has extensions for publishing to [HockeyApp](https://marketplace.visualstudio.com/items?itemName=ms.hockeyapp) and the [Google Play](https://github.com/Microsoft/google-play-vsts-extension) store, so it provides a pretty great mobile CD solution in general.* * [Travis CI](https://github.com/mondora/code-push-travis-cli) + +Additionally, if you'd like more details of what a complete mobile CI/CD workflow can look like, which includes CodePush, check out this [excellent article](https://zeemee.engineering/zeemee-engineering-and-the-quest-for-the-holy-mobile-dev-grail-1310be4953d1#.zfwaxtbco) by the [ZeeMee engineering team](https://zeemee.engineering). + +## TypeScript Consumption + +This module ships its `*.d.ts` file as part of its NPM package, which allows you to simply `import` it, and receive intellisense in supporting editors (e.g. Visual Studio Code), as well as compile-time type checking if you're using TypeScript. For the most part, this behavior should just work out of the box, however, if you've specified `es6` as the value for either the `target` or `module` [compiler option](http://www.typescriptlang.org/docs/handbook/compiler-options.html) in your [`tsconfig.json`](http://www.typescriptlang.org/docs/handbook/tsconfig-json.html) file, then just make sure that you also set the `moduleResolution` option to `node`. This ensures that the TypeScript compiler will look within the `node_modules` for the type definitions of imported modules. Otherwise, you'll get an error like the following: `error TS2307: Cannot find module 'react-native-code-push'`. + +--- + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. diff --git a/android/app/build.gradle b/android/app/build.gradle index 7f0b932..799bd48 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -10,7 +10,7 @@ android { versionCode 1 versionName "1.0" } - + buildTypes { release { minifyEnabled false @@ -22,5 +22,5 @@ android { dependencies { compile fileTree(dir: "libs", include: ["*.jar"]) compile "com.android.support:appcompat-v7:23.0.1" - compile "com.facebook.react:react-native:0.19.+" + compile "com.facebook.react:react-native:+" } \ 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 911ceb7..bf8e330 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 @@ -1,137 +1,115 @@ package com.microsoft.codepush.react; -import com.facebook.react.ReactActivity; import com.facebook.react.ReactPackage; import com.facebook.react.bridge.JavaScriptModule; -import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReactContextBaseJavaModule; -import com.facebook.react.bridge.ReactMethod; -import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableMap; -import com.facebook.react.bridge.WritableNativeMap; -import com.facebook.react.modules.core.DeviceEventManagerModule; -import com.facebook.react.uimanager.ReactChoreographer; import com.facebook.react.uimanager.ViewManager; import com.facebook.soloader.SoLoader; -import android.app.Activity; import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; -import android.os.AsyncTask; -import android.provider.Settings; -import android.view.Choreographer; -import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import java.lang.reflect.Field; -import java.lang.reflect.Method; - import java.io.File; import java.io.IOException; import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; public class CodePush implements ReactPackage { - private static boolean needToReportRollback = false; - private static boolean isRunningBinaryVersion = false; - private static boolean testConfigurationFlag = false; - private boolean didUpdate = false; + private static boolean sIsRunningBinaryVersion = false; + private static boolean sNeedToReportRollback = false; + private static boolean sTestConfigurationFlag = false; - private String assetsBundleFileName; + private boolean mDidUpdate = false; - private static final String ASSETS_BUNDLE_PREFIX = "assets://"; - private static final String BINARY_MODIFIED_TIME_KEY = "binaryModifiedTime"; - private final String CODE_PUSH_PREFERENCES = "CodePush"; - private static final String DEFAULT_JS_BUNDLE_NAME = "index.android.bundle"; - private final String DOWNLOAD_PROGRESS_EVENT_NAME = "CodePushDownloadProgress"; - private final String FAILED_UPDATES_KEY = "CODE_PUSH_FAILED_UPDATES"; - private final String PACKAGE_HASH_KEY = "packageHash"; - private final String PENDING_UPDATE_HASH_KEY = "hash"; - private final String PENDING_UPDATE_IS_LOADING_KEY = "isLoading"; - private final String PENDING_UPDATE_KEY = "CODE_PUSH_PENDING_UPDATE"; - private final String RESOURCES_BUNDLE = "resources.arsc"; + private String mAssetsBundleFileName; // Helper classes. - private CodePushNativeModule codePushNativeModule; - private CodePushPackage codePushPackage; - private CodePushTelemetryManager codePushTelemetryManager; + private CodePushNativeModule mNativeModule; + private CodePushUpdateManager mUpdateManager; + private CodePushTelemetryManager mTelemetryManager; + private SettingsManager mSettingsManager; // Config properties. - private String appVersion; - private int buildVersion; - private String deploymentKey; - private String serverUrl = "https://codepush.azurewebsites.net/"; + private String mAppVersion; + private String mDeploymentKey; + private String mServerUrl = "https://codepush.azurewebsites.net/"; - private Activity mainActivity; - private Context applicationContext; - private final boolean isDebugMode; + private Context mContext; + private final boolean mIsDebugMode; - private static CodePush currentInstance; + private static CodePush mCurrentInstance; - public CodePush(String deploymentKey, Activity mainActivity) { - this(deploymentKey, mainActivity, false); + public CodePush(String deploymentKey, Context context) { + this(deploymentKey, context, false); } - public CodePush(String deploymentKey, Activity mainActivity, boolean isDebugMode) { - SoLoader.init(mainActivity, false); - this.applicationContext = mainActivity.getApplicationContext(); - this.codePushPackage = new CodePushPackage(mainActivity.getFilesDir().getAbsolutePath()); - this.codePushTelemetryManager = new CodePushTelemetryManager(this.applicationContext, CODE_PUSH_PREFERENCES); - this.deploymentKey = deploymentKey; - this.isDebugMode = isDebugMode; - this.mainActivity = mainActivity; + public CodePush(String deploymentKey, Context context, boolean isDebugMode) { + SoLoader.init(context, false); + mContext = context.getApplicationContext(); + + mUpdateManager = new CodePushUpdateManager(context.getFilesDir().getAbsolutePath()); + mTelemetryManager = new CodePushTelemetryManager(mContext); + mDeploymentKey = deploymentKey; + mIsDebugMode = isDebugMode; + mSettingsManager = new SettingsManager(mContext); try { - PackageInfo pInfo = applicationContext.getPackageManager().getPackageInfo(applicationContext.getPackageName(), 0); - appVersion = pInfo.versionName; - buildVersion = pInfo.versionCode; + PackageInfo pInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0); + mAppVersion = pInfo.versionName; } catch (PackageManager.NameNotFoundException e) { - throw new CodePushUnknownException("Unable to get package info for " + applicationContext.getPackageName(), e); + throw new CodePushUnknownException("Unable to get package info for " + mContext.getPackageName(), e); } - currentInstance = this; + mCurrentInstance = this; clearDebugCacheIfNeeded(); initializeUpdateAfterRestart(); } - // USED FOR TESTING SO THAT IT CAN CONNECT TO DEBUG SERVER - public CodePush(String deploymentKey, Activity mainActivity, boolean isDebugMode, String serverUrl) { - this(deploymentKey, mainActivity, isDebugMode); - this.serverUrl = serverUrl; + public CodePush(String deploymentKey, Context context, boolean isDebugMode, String serverUrl) { + this(deploymentKey, context, isDebugMode); + mServerUrl = serverUrl; } - private void clearDebugCacheIfNeeded() { - if (isDebugMode && isPendingUpdate(null)) { + public void clearDebugCacheIfNeeded() { + if (mIsDebugMode && mSettingsManager.isPendingUpdate(null)) { // 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 - File cachedDevBundle = new File(applicationContext.getFilesDir(), "ReactNativeDevBundle.js"); + File cachedDevBundle = new File(mContext.getFilesDir(), "ReactNativeDevBundle.js"); if (cachedDevBundle.exists()) { cachedDevBundle.delete(); } } } - private long getBinaryResourcesModifiedTime() { + public boolean didUpdate() { + return mDidUpdate; + } + + public String getAppVersion() { + return mAppVersion; + } + + public String getAssetsBundleFileName() { + return mAssetsBundleFileName; + } + + long getBinaryResourcesModifiedTime() { ZipFile applicationFile = null; try { - ApplicationInfo ai = applicationContext.getPackageManager().getApplicationInfo(applicationContext.getPackageName(), 0); + ApplicationInfo ai = this.mContext.getPackageManager().getApplicationInfo(this.mContext.getPackageName(), 0); applicationFile = new ZipFile(ai.sourceDir); - ZipEntry classesDexEntry = applicationFile.getEntry(RESOURCES_BUNDLE); + ZipEntry classesDexEntry = applicationFile.getEntry(CodePushConstants.RESOURCES_BUNDLE); return classesDexEntry.getTime(); } catch (PackageManager.NameNotFoundException | IOException e) { throw new CodePushUnknownException("Error in getting file information about compiled resources", e); @@ -146,35 +124,53 @@ public class CodePush implements ReactPackage { } } + @Deprecated public static String getBundleUrl() { - return getBundleUrl(DEFAULT_JS_BUNDLE_NAME); + return getJSBundleFile(); } + @Deprecated public static String getBundleUrl(String assetsBundleFileName) { - if (currentInstance == null) { + return getJSBundleFile(assetsBundleFileName); + } + + public Context getContext() { + return mContext; + } + + public String getDeploymentKey() { + return mDeploymentKey; + } + + public static String getJSBundleFile() { + return CodePush.getJSBundleFile(CodePushConstants.DEFAULT_JS_BUNDLE_NAME); + } + + public static String getJSBundleFile(String assetsBundleFileName) { + if (mCurrentInstance == null) { throw new CodePushNotInitializedException("A CodePush instance has not been created yet. Have you added it to your app's list of ReactPackages?"); } - return currentInstance.getBundleUrlInternal(assetsBundleFileName); + return mCurrentInstance.getJSBundleFileInternal(assetsBundleFileName); } - public String getBundleUrlInternal(String assetsBundleFileName) { - this.assetsBundleFileName = assetsBundleFileName; - String binaryJsBundleUrl = ASSETS_BUNDLE_PREFIX + assetsBundleFileName; + public String getJSBundleFileInternal(String assetsBundleFileName) { + this.mAssetsBundleFileName = assetsBundleFileName; + String binaryJsBundleUrl = CodePushConstants.ASSETS_BUNDLE_PREFIX + assetsBundleFileName; long binaryResourcesModifiedTime = this.getBinaryResourcesModifiedTime(); try { - String packageFilePath = codePushPackage.getCurrentPackageBundlePath(this.assetsBundleFileName); + String packageFilePath = mUpdateManager.getCurrentPackageBundlePath(this.mAssetsBundleFileName); if (packageFilePath == null) { // There has not been any downloaded updates. CodePushUtils.logBundleUrl(binaryJsBundleUrl); - isRunningBinaryVersion = true; + sIsRunningBinaryVersion = true; return binaryJsBundleUrl; } - ReadableMap packageMetadata = this.codePushPackage.getCurrentPackage(); + ReadableMap packageMetadata = this.mUpdateManager.getCurrentPackage(); Long binaryModifiedDateDuringPackageInstall = null; - String binaryModifiedDateDuringPackageInstallString = CodePushUtils.tryGetString(packageMetadata, BINARY_MODIFIED_TIME_KEY); + String binaryModifiedDateDuringPackageInstallString = CodePushUtils.tryGetString(packageMetadata, CodePushConstants.BINARY_MODIFIED_TIME_KEY); if (binaryModifiedDateDuringPackageInstallString != null) { binaryModifiedDateDuringPackageInstall = Long.parseLong(binaryModifiedDateDuringPackageInstallString); } @@ -182,19 +178,19 @@ public class CodePush implements ReactPackage { String packageAppVersion = CodePushUtils.tryGetString(packageMetadata, "appVersion"); if (binaryModifiedDateDuringPackageInstall != null && binaryModifiedDateDuringPackageInstall == binaryResourcesModifiedTime && - (isUsingTestConfiguration() || this.appVersion.equals(packageAppVersion))) { + (isUsingTestConfiguration() || this.mAppVersion.equals(packageAppVersion))) { CodePushUtils.logBundleUrl(packageFilePath); - isRunningBinaryVersion = false; + sIsRunningBinaryVersion = false; return packageFilePath; } else { // The binary version is newer. - this.didUpdate = false; - if (!this.isDebugMode || !this.appVersion.equals(packageAppVersion)) { + this.mDidUpdate = false; + if (!this.mIsDebugMode || !this.mAppVersion.equals(packageAppVersion)) { this.clearUpdates(); } CodePushUtils.logBundleUrl(binaryJsBundleUrl); - isRunningBinaryVersion = true; + sIsRunningBinaryVersion = true; return binaryJsBundleUrl; } } catch (NumberFormatException e) { @@ -202,63 +198,33 @@ public class CodePush implements ReactPackage { } } - private JSONArray getFailedUpdates() { - SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0); - String failedUpdatesString = settings.getString(FAILED_UPDATES_KEY, null); - if (failedUpdatesString == null) { - return new JSONArray(); - } - - try { - return new JSONArray(failedUpdatesString); - } catch (JSONException e) { - // Unrecognized data format, clear and replace with expected format. - JSONArray emptyArray = new JSONArray(); - settings.edit().putString(FAILED_UPDATES_KEY, emptyArray.toString()).commit(); - return emptyArray; - } + public String getServerUrl() { + return mServerUrl; } - 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 { - return new JSONObject(pendingUpdateString); - } catch (JSONException e) { - // Should not happen. - CodePushUtils.log("Unable to parse pending update metadata " + pendingUpdateString + - " stored in SharedPreferences"); - return null; - } - } - - private void initializeUpdateAfterRestart() { + void initializeUpdateAfterRestart() { // Reset the state which indicates that // the app was just freshly updated. - didUpdate = false; + mDidUpdate = false; - JSONObject pendingUpdate = getPendingUpdate(); + JSONObject pendingUpdate = mSettingsManager.getPendingUpdate(); if (pendingUpdate != null) { try { - boolean updateIsLoading = pendingUpdate.getBoolean(PENDING_UPDATE_IS_LOADING_KEY); + boolean updateIsLoading = pendingUpdate.getBoolean(CodePushConstants.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."); - needToReportRollback = true; + sNeedToReportRollback = true; rollbackPackage(); } else { // There is in fact a new update running for the first // time, so update the local state to ensure the client knows. - didUpdate = true; + mDidUpdate = true; // 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), + mSettingsManager.savePendingUpdate(pendingUpdate.getString(CodePushConstants.PENDING_UPDATE_HASH_KEY), /* isLoading */true); } } catch (JSONException e) { @@ -268,512 +234,55 @@ public class CodePush implements ReactPackage { } } - private boolean isFailedHash(String packageHash) { - JSONArray failedUpdates = getFailedUpdates(); - if (packageHash != null) { - for (int i = 0; i < failedUpdates.length(); i++) { - try { - JSONObject failedPackage = failedUpdates.getJSONObject(i); - String failedPackageHash = failedPackage.getString(PACKAGE_HASH_KEY); - if (packageHash.equals(failedPackageHash)) { - return true; - } - } catch (JSONException e) { - throw new CodePushUnknownException("Unable to read failedUpdates data stored in SharedPreferences.", e); - } - } - } - - return false; + void invalidateCurrentInstance() { + mCurrentInstance = null; } - private boolean isPendingUpdate(String packageHash) { - JSONObject pendingUpdate = getPendingUpdate(); - - try { - return pendingUpdate != null && - !pendingUpdate.getBoolean(PENDING_UPDATE_IS_LOADING_KEY) && - (packageHash == null || pendingUpdate.getString(PENDING_UPDATE_HASH_KEY).equals(packageHash)); - } - catch (JSONException e) { - throw new CodePushUnknownException("Unable to read pending update metadata in isPendingUpdate.", e); - } + boolean isDebugMode() { + return mIsDebugMode; } - private void removeFailedUpdates() { - SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0); - settings.edit().remove(FAILED_UPDATES_KEY).commit(); + boolean isRunningBinaryVersion() { + return sIsRunningBinaryVersion; } - private void removePendingUpdate() { - SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0); - settings.edit().remove(PENDING_UPDATE_KEY).commit(); + boolean needToReportRollback() { + return sNeedToReportRollback; } private void rollbackPackage() { - WritableMap failedPackage = codePushPackage.getCurrentPackage(); - saveFailedUpdate(failedPackage); - codePushPackage.rollbackPackage(); - removePendingUpdate(); + WritableMap failedPackage = mUpdateManager.getCurrentPackage(); + mSettingsManager.saveFailedUpdate(failedPackage); + mUpdateManager.rollbackPackage(); + mSettingsManager.removePendingUpdate(); } - private void saveFailedUpdate(ReadableMap failedPackage) { - SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0); - String failedUpdatesString = settings.getString(FAILED_UPDATES_KEY, null); - JSONArray failedUpdates; - if (failedUpdatesString == null) { - failedUpdates = new JSONArray(); - } else { - try { - failedUpdates = new JSONArray(failedUpdatesString); - } catch (JSONException e) { - // Should not happen. - throw new CodePushMalformedDataException("Unable to parse failed updates information " + - failedUpdatesString + " stored in SharedPreferences", e); - } - } - - JSONObject failedPackageJSON = CodePushUtils.convertReadableToJsonObject(failedPackage); - failedUpdates.put(failedPackageJSON); - settings.edit().putString(FAILED_UPDATES_KEY, failedUpdates.toString()).commit(); - } - - private void savePendingUpdate(String packageHash, boolean isLoading) { - SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0); - JSONObject pendingUpdate = new JSONObject(); - try { - pendingUpdate.put(PENDING_UPDATE_HASH_KEY, packageHash); - pendingUpdate.put(PENDING_UPDATE_IS_LOADING_KEY, isLoading); - settings.edit().putString(PENDING_UPDATE_KEY, pendingUpdate.toString()).commit(); - } catch (JSONException e) { - // Should not happen. - throw new CodePushUnknownException("Unable to save pending update.", e); - } + public void setNeedToReportRollback(boolean needToReportRollback) { + CodePush.sNeedToReportRollback = needToReportRollback; } /* The below 3 methods are used for running tests.*/ public static boolean isUsingTestConfiguration() { - return testConfigurationFlag; + return sTestConfigurationFlag; } public static void setUsingTestConfiguration(boolean shouldUseTestConfiguration) { - testConfigurationFlag = shouldUseTestConfiguration; + sTestConfigurationFlag = shouldUseTestConfiguration; } public void clearUpdates() { - codePushPackage.clearUpdates(); - removePendingUpdate(); - removeFailedUpdates(); - } - - private class CodePushNativeModule extends ReactContextBaseJavaModule { - private LifecycleEventListener lifecycleEventListener = null; - private int minimumBackgroundDuration = 0; - - public CodePushNativeModule(ReactApplicationContext reactContext) { - super(reactContext); - } - - @Override - public Map getConstants() { - final Map constants = new HashMap<>(); - - constants.put("codePushInstallModeImmediate", CodePushInstallMode.IMMEDIATE.getValue()); - constants.put("codePushInstallModeOnNextRestart", CodePushInstallMode.ON_NEXT_RESTART.getValue()); - constants.put("codePushInstallModeOnNextResume", CodePushInstallMode.ON_NEXT_RESUME.getValue()); - - constants.put("codePushUpdateStateRunning", CodePushUpdateState.RUNNING.getValue()); - constants.put("codePushUpdateStatePending", CodePushUpdateState.PENDING.getValue()); - constants.put("codePushUpdateStateLatest", CodePushUpdateState.LATEST.getValue()); - - return constants; - } - - @Override - public String getName() { - return "CodePush"; - } - - private void loadBundleLegacy() { - Intent intent = mainActivity.getIntent(); - mainActivity.finish(); - mainActivity.startActivity(intent); - - currentInstance = null; - } - - private void loadBundle() { - CodePush.this.clearDebugCacheIfNeeded(); - - // Our preferred reload logic relies on the user's Activity inheriting from the - // core ReactActivity class, so if it doesn't, we fallback early to our legacy behavior. - if (!ReactActivity.class.isInstance(mainActivity)) { - loadBundleLegacy(); - return; - } - - try { - // #1) Get the private ReactInstanceManager, which is what includes - // the logic to reload the current React context. - Field instanceManagerField = ReactActivity.class.getDeclaredField("mReactInstanceManager"); - instanceManagerField.setAccessible(true); // Make a private field accessible - final Object instanceManager = instanceManagerField.get(mainActivity); - - // #2) Update the locally stored JS bundle file path - String latestJSBundleFile = CodePush.this.getBundleUrlInternal(CodePush.this.assetsBundleFileName); - Field jsBundleField = instanceManager.getClass().getDeclaredField("mJSBundleFile"); - jsBundleField.setAccessible(true); - jsBundleField.set(instanceManager, latestJSBundleFile); - - // #3) Get the context creation method and fire it on the UI thread (which RN enforces) - final Method recreateMethod = instanceManager.getClass().getMethod("recreateReactContextInBackground"); - mainActivity.runOnUiThread(new Runnable() { - @Override - public void run() { - try { - recreateMethod.invoke(instanceManager); - initializeUpdateAfterRestart(); - } - catch (Exception e) { - // The recreation method threw an unknown exception - // so just simply fallback to restarting the Activity - loadBundleLegacy(); - } - } - }); - } - catch (Exception e) { - // Our reflection logic failed somewhere - // so fall back to restarting the Activity - loadBundleLegacy(); - } - } - - @ReactMethod - public void downloadUpdate(final ReadableMap updatePackage, final boolean notifyProgress, final Promise promise) { - AsyncTask asyncTask = new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - try { - WritableMap mutableUpdatePackage = CodePushUtils.convertReadableMapToWritableMap(updatePackage); - mutableUpdatePackage.putString(BINARY_MODIFIED_TIME_KEY, "" + getBinaryResourcesModifiedTime()); - codePushPackage.downloadPackage(mutableUpdatePackage, CodePush.this.assetsBundleFileName, new DownloadProgressCallback() { - private boolean hasScheduledNextFrame = false; - private DownloadProgress latestDownloadProgress = null; - - @Override - public void call(DownloadProgress downloadProgress) { - if (!notifyProgress) { - return; - } - - latestDownloadProgress = downloadProgress; - // If the download is completed, synchronously send the last event. - if (latestDownloadProgress.isCompleted()) { - dispatchDownloadProgressEvent(); - return; - } - - if (hasScheduledNextFrame) { - return; - } - - hasScheduledNextFrame = true; - getReactApplicationContext().runOnUiQueueThread(new Runnable() { - @Override - public void run() { - ReactChoreographer.getInstance().postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, new Choreographer.FrameCallback() { - @Override - public void doFrame(long frameTimeNanos) { - if (!latestDownloadProgress.isCompleted()) { - dispatchDownloadProgressEvent(); - } - - hasScheduledNextFrame = false; - } - }); - } - }); - } - - public void dispatchDownloadProgressEvent() { - getReactApplicationContext() - .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit(DOWNLOAD_PROGRESS_EVENT_NAME, latestDownloadProgress.createWritableMap()); - } - }); - - WritableMap newPackage = codePushPackage.getPackage(CodePushUtils.tryGetString(updatePackage, PACKAGE_HASH_KEY)); - promise.resolve(newPackage); - } catch (IOException e) { - e.printStackTrace(); - promise.reject(e); - } catch (CodePushInvalidUpdateException e) { - e.printStackTrace(); - saveFailedUpdate(updatePackage); - promise.reject(e); - } catch (CodePushMalformedDataException e) { - e.printStackTrace(); - saveFailedUpdate(updatePackage); - promise.reject(e); - } - - 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); - configMap.putString("clientUniqueId", - Settings.Secure.getString(mainActivity.getContentResolver(), - android.provider.Settings.Secure.ANDROID_ID)); - String binaryHash = CodePushUpdateUtils.getHashForBinaryContents(mainActivity, isDebugMode); - if (binaryHash != null) { - // binaryHash will be null if the React Native assets were not bundled into the APK - // (e.g. in Debug builds) - configMap.putString(PACKAGE_HASH_KEY, binaryHash); - } - - promise.resolve(configMap); - } - - @ReactMethod - public void getUpdateMetadata(final int updateState, final Promise promise) { - AsyncTask asyncTask = new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - WritableMap currentPackage = codePushPackage.getCurrentPackage(); - - if (currentPackage == null) { - promise.resolve(""); - return null; - } - - Boolean currentUpdateIsPending = false; - - if (currentPackage.hasKey(PACKAGE_HASH_KEY)) { - String currentHash = currentPackage.getString(PACKAGE_HASH_KEY); - currentUpdateIsPending = CodePush.this.isPendingUpdate(currentHash); - } - - if (updateState == CodePushUpdateState.PENDING.getValue() && !currentUpdateIsPending) { - // The caller wanted a pending update - // but there isn't currently one. - promise.resolve(""); - } else if (updateState == CodePushUpdateState.RUNNING.getValue() && currentUpdateIsPending) { - // The caller wants the running update, but the current - // one is pending, so we need to grab the previous. - promise.resolve(codePushPackage.getPreviousPackage()); - } else { - // The current package satisfies the request: - // 1) Caller wanted a pending, and there is a pending update - // 2) Caller wanted the running update, and there isn't a pending - // 3) Caller wants the latest update, regardless if it's pending or not - if (isRunningBinaryVersion) { - // This only matters in Debug builds. Since we do not clear "outdated" updates, - // we need to indicate to the JS side that somehow we have a current update on - // disk that is not actually running. - currentPackage.putBoolean("_isDebugOnly", true); - } - - // Enable differentiating pending vs. non-pending updates - currentPackage.putBoolean("isPending", currentUpdateIsPending); - promise.resolve(currentPackage); - } - - return null; - } - }; - - asyncTask.execute(); - } - - @ReactMethod - public void getNewStatusReport(final Promise promise) { - AsyncTask asyncTask = new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - if (needToReportRollback) { - needToReportRollback = false; - JSONArray failedUpdates = getFailedUpdates(); - if (failedUpdates != null && failedUpdates.length() > 0) { - try { - JSONObject lastFailedPackageJSON = failedUpdates.getJSONObject(failedUpdates.length() - 1); - WritableMap lastFailedPackage = CodePushUtils.convertJsonObjectToWritable(lastFailedPackageJSON); - WritableMap failedStatusReport = codePushTelemetryManager.getRollbackReport(lastFailedPackage); - if (failedStatusReport != null) { - promise.resolve(failedStatusReport); - return null; - } - } catch (JSONException e) { - throw new CodePushUnknownException("Unable to read failed updates information stored in SharedPreferences.", e); - } - } - } else if (didUpdate) { - WritableMap currentPackage = codePushPackage.getCurrentPackage(); - if (currentPackage != null) { - WritableMap newPackageStatusReport = codePushTelemetryManager.getUpdateReport(currentPackage); - if (newPackageStatusReport != null) { - promise.resolve(newPackageStatusReport); - return null; - } - } - } else if (isRunningBinaryVersion) { - WritableMap newAppVersionStatusReport = codePushTelemetryManager.getBinaryUpdateReport(appVersion); - if (newAppVersionStatusReport != null) { - promise.resolve(newAppVersionStatusReport); - return null; - } - } else { - WritableMap retryStatusReport = codePushTelemetryManager.getRetryStatusReport(); - if (retryStatusReport != null) { - promise.resolve(retryStatusReport); - return null; - } - } - - promise.resolve(""); - return null; - } - }; - - asyncTask.execute(); - } - - @ReactMethod - public void installUpdate(final ReadableMap updatePackage, final int installMode, final int minimumBackgroundDuration, final Promise promise) { - AsyncTask asyncTask = new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - codePushPackage.installPackage(updatePackage, isPendingUpdate(null)); - - String pendingHash = CodePushUtils.tryGetString(updatePackage, PACKAGE_HASH_KEY); - if (pendingHash == null) { - throw new CodePushUnknownException("Update package to be installed has no hash."); - } else { - savePendingUpdate(pendingHash, /* isLoading */false); - } - - if (installMode == CodePushInstallMode.ON_NEXT_RESUME.getValue()) { - // Store the minimum duration on the native module as an instance - // variable instead of relying on a closure below, so that any - // subsequent resume-based installs could override it. - CodePushNativeModule.this.minimumBackgroundDuration = minimumBackgroundDuration; - - if (lifecycleEventListener == null) { - // Ensure we do not add the listener twice. - lifecycleEventListener = new LifecycleEventListener() { - private Date lastPausedDate = null; - - @Override - public void onHostResume() { - // Determine how long the app was in the background and ensure - // that it meets the minimum duration amount of time. - long durationInBackground = 0; - if (lastPausedDate != null) { - durationInBackground = (new Date().getTime() - lastPausedDate.getTime()) / 1000; - } - - if (durationInBackground >= CodePushNativeModule.this.minimumBackgroundDuration) { - loadBundle(); - } - } - - @Override - public void onHostPause() { - // Save the current time so that when the app is later - // resumed, we can detect how long it was in the background. - lastPausedDate = new Date(); - } - - @Override - public void onHostDestroy() { - } - }; - - getReactApplicationContext().addLifecycleEventListener(lifecycleEventListener); - } - } - - promise.resolve(""); - - return null; - } - }; - - asyncTask.execute(); - } - - @ReactMethod - public void isFailedUpdate(String packageHash, Promise promise) { - promise.resolve(isFailedHash(packageHash)); - } - - @ReactMethod - public void isFirstRun(String packageHash, Promise promise) { - boolean isFirstRun = didUpdate - && packageHash != null - && packageHash.length() > 0 - && packageHash.equals(codePushPackage.getCurrentPackageHash()); - promise.resolve(isFirstRun); - } - - @ReactMethod - public void notifyApplicationReady(Promise promise) { - removePendingUpdate(); - promise.resolve(""); - } - - @ReactMethod - public void recordStatusReported(ReadableMap statusReport) { - codePushTelemetryManager.recordStatusReported(statusReport); - } - - @ReactMethod - public void restartApp(boolean onlyIfUpdateIsPending, Promise promise) { - // If this is an unconditional restart request, or there - // is current pending update, then reload the app. - if (!onlyIfUpdateIsPending || CodePush.this.isPendingUpdate(null)) { - loadBundle(); - promise.resolve(true); - } - promise.resolve(false); - } - - @ReactMethod - public void saveStatusReportForRetry(ReadableMap statusReport) { - codePushTelemetryManager.saveStatusReportForRetry(statusReport); - } - - @ReactMethod - // Replaces the current bundle with the one downloaded from removeBundleUrl. - // It is only to be used during tests. No-ops if the test configuration flag is not set. - public void downloadAndReplaceCurrentBundle(String remoteBundleUrl) { - if (isUsingTestConfiguration()) { - try { - codePushPackage.downloadAndReplaceCurrentBundle(remoteBundleUrl, assetsBundleFileName); - } catch (IOException e) { - throw new CodePushUnknownException("Unable to replace current bundle", e); - } - } - } + mUpdateManager.clearUpdates(); + mSettingsManager.removePendingUpdate(); + mSettingsManager.removeFailedUpdates(); } @Override public List createNativeModules(ReactApplicationContext reactApplicationContext) { List nativeModules = new ArrayList<>(); - this.codePushNativeModule = new CodePushNativeModule(reactApplicationContext); - CodePushDialog dialogModule = new CodePushDialog(reactApplicationContext, mainActivity); + mNativeModule = new CodePushNativeModule(reactApplicationContext, this, mUpdateManager, mTelemetryManager, mSettingsManager); + CodePushDialog dialogModule = new CodePushDialog(reactApplicationContext); - nativeModules.add(this.codePushNativeModule); + nativeModules.add(mNativeModule); nativeModules.add(dialogModule); return nativeModules; diff --git a/android/app/src/main/java/com/microsoft/codepush/react/CodePushConstants.java b/android/app/src/main/java/com/microsoft/codepush/react/CodePushConstants.java new file mode 100644 index 0000000..898d264 --- /dev/null +++ b/android/app/src/main/java/com/microsoft/codepush/react/CodePushConstants.java @@ -0,0 +1,28 @@ +package com.microsoft.codepush.react; + +public class CodePushConstants { + public static final String ASSETS_BUNDLE_PREFIX = "assets://"; + public static final String BINARY_MODIFIED_TIME_KEY = "binaryModifiedTime"; + public static final String CODE_PUSH_FOLDER_PREFIX = "CodePush"; + public static final String CODE_PUSH_HASH_FILE_NAME = "CodePushHash.json"; + public static final String CODE_PUSH_PREFERENCES = "CodePush"; + public static final String CURRENT_PACKAGE_KEY = "currentPackage"; + public static final String DEFAULT_JS_BUNDLE_NAME = "index.android.bundle"; + public static final String DIFF_MANIFEST_FILE_NAME = "hotcodepush.json"; + public static final int DOWNLOAD_BUFFER_SIZE = 1024 * 256; + public static final String DOWNLOAD_FILE_NAME = "download.zip"; + public static final String DOWNLOAD_PROGRESS_EVENT_NAME = "CodePushDownloadProgress"; + public static final String DOWNLOAD_URL_KEY = "downloadUrl"; + public static final String FAILED_UPDATES_KEY = "CODE_PUSH_FAILED_UPDATES"; + public static final String PACKAGE_FILE_NAME = "app.json"; + public static final String PACKAGE_HASH_KEY = "packageHash"; + public static final String PENDING_UPDATE_HASH_KEY = "hash"; + public static final String PENDING_UPDATE_IS_LOADING_KEY = "isLoading"; + public static final String PENDING_UPDATE_KEY = "CODE_PUSH_PENDING_UPDATE"; + public static final String PREVIOUS_PACKAGE_KEY = "previousPackage"; + public static final String REACT_NATIVE_LOG_TAG = "ReactNative"; + public static final String RELATIVE_BUNDLE_PATH_KEY = "bundlePath"; + public static final String RESOURCES_BUNDLE = "resources.arsc"; + public static final String STATUS_FILE = "codepush.json"; + public static final String UNZIPPED_FOLDER_NAME = "unzipped"; +} diff --git a/android/app/src/main/java/com/microsoft/codepush/react/CodePushDialog.java b/android/app/src/main/java/com/microsoft/codepush/react/CodePushDialog.java index 14fd613..788aee4 100644 --- a/android/app/src/main/java/com/microsoft/codepush/react/CodePushDialog.java +++ b/android/app/src/main/java/com/microsoft/codepush/react/CodePushDialog.java @@ -11,30 +11,20 @@ import com.facebook.react.bridge.ReactMethod; public class CodePushDialog extends ReactContextBaseJavaModule{ - private Activity mainActivity; - - public CodePushDialog(ReactApplicationContext reactContext, Activity mainActivity) { + public CodePushDialog(ReactApplicationContext reactContext) { super(reactContext); - this.mainActivity = mainActivity; } @ReactMethod public void showDialog(String title, String message, String button1Text, String button2Text, final Callback successCallback, Callback errorCallback) { - AlertDialog.Builder builder = new AlertDialog.Builder(mainActivity); + AlertDialog.Builder builder = new AlertDialog.Builder(getCurrentActivity()); builder.setCancelable(false); DialogInterface.OnClickListener clickListener = new DialogInterface.OnClickListener() { - private boolean callbackConsumed = false; - @Override - public synchronized void onClick(DialogInterface dialog, int which) { - if (callbackConsumed) { - return; - } - - callbackConsumed = true; + public void onClick(DialogInterface dialog, int which) { dialog.cancel(); switch (which) { case DialogInterface.BUTTON_POSITIVE: diff --git a/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java b/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java new file mode 100644 index 0000000..95459d9 --- /dev/null +++ b/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java @@ -0,0 +1,490 @@ +package com.microsoft.codepush.react; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.AsyncTask; +import android.provider.Settings; +import android.view.Choreographer; + +import com.facebook.react.ReactActivity; +import com.facebook.react.ReactInstanceManager; +import com.facebook.react.bridge.LifecycleEventListener; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeMap; +import com.facebook.react.modules.core.DeviceEventManagerModule; +import com.facebook.react.uimanager.ReactChoreographer; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +public class CodePushNativeModule extends ReactContextBaseJavaModule { + private LifecycleEventListener mLifecycleEventListener = null; + private int mMinimumBackgroundDuration = 0; + private CodePush mCodePush; + private CodePushUpdateManager mUpdateManager; + private CodePushTelemetryManager mTelemetryManager; + private SettingsManager mSettingsManager; + + private static final String REACT_APPLICATION_CLASS_NAME = "com.facebook.react.ReactApplication"; + private static final String REACT_NATIVE_HOST_CLASS_NAME = "com.facebook.react.ReactNativeHost"; + + public CodePushNativeModule(ReactApplicationContext reactContext, CodePush codePush, CodePushUpdateManager codePushUpdateManager, CodePushTelemetryManager codePushTelemetryManager, SettingsManager settingsManager) { + super(reactContext); + mCodePush = codePush; + mUpdateManager = codePushUpdateManager; + mTelemetryManager = codePushTelemetryManager; + mSettingsManager = settingsManager; + } + + @Override + public Map getConstants() { + final Map constants = new HashMap<>(); + + constants.put("codePushInstallModeImmediate", CodePushInstallMode.IMMEDIATE.getValue()); + constants.put("codePushInstallModeOnNextRestart", CodePushInstallMode.ON_NEXT_RESTART.getValue()); + constants.put("codePushInstallModeOnNextResume", CodePushInstallMode.ON_NEXT_RESUME.getValue()); + + constants.put("codePushUpdateStateRunning", CodePushUpdateState.RUNNING.getValue()); + constants.put("codePushUpdateStatePending", CodePushUpdateState.PENDING.getValue()); + constants.put("codePushUpdateStateLatest", CodePushUpdateState.LATEST.getValue()); + + return constants; + } + + @Override + public String getName() { + return "CodePush"; + } + + private boolean isReactApplication(Context context) { + Class reactApplicationClass = tryGetClass(REACT_APPLICATION_CLASS_NAME); + if (reactApplicationClass != null && reactApplicationClass.isInstance(context)) { + return true; + } + + return false; + } + + private void loadBundleLegacy() { + Activity currentActivity = getCurrentActivity(); + Intent intent = currentActivity.getIntent(); + currentActivity.finish(); + currentActivity.startActivity(intent); + + mCodePush.invalidateCurrentInstance(); + } + + private void loadBundle() { + mCodePush.clearDebugCacheIfNeeded(); + Activity currentActivity = getCurrentActivity(); + + if (!ReactActivity.class.isInstance(currentActivity)) { + // Our preferred reload logic relies on the user's Activity inheriting + // from the core ReactActivity class, so if it doesn't, we fallback + // early to our legacy behavior. + loadBundleLegacy(); + } else { + try { + ReactActivity reactActivity = (ReactActivity)currentActivity; + ReactInstanceManager instanceManager; + + // #1) Get the ReactInstanceManager instance, which is what includes the + // logic to reload the current React context. + try { + // In RN 0.29, the "mReactInstanceManager" field yields a null value, so we try + // to get the instance manager via the ReactNativeHost, which only exists in 0.29. + Method getApplicationMethod = ReactActivity.class.getMethod("getApplication"); + Object reactApplication = getApplicationMethod.invoke(reactActivity); + Class reactApplicationClass = tryGetClass(REACT_APPLICATION_CLASS_NAME); + Method getReactNativeHostMethod = reactApplicationClass.getMethod("getReactNativeHost"); + Object reactNativeHost = getReactNativeHostMethod.invoke(reactApplication); + Class reactNativeHostClass = tryGetClass(REACT_NATIVE_HOST_CLASS_NAME); + Method getReactInstanceManagerMethod = reactNativeHostClass.getMethod("getReactInstanceManager"); + instanceManager = (ReactInstanceManager)getReactInstanceManagerMethod.invoke(reactNativeHost); + } catch (Exception e) { + // The React Native version might be older than 0.29, so we try to get the + // instance manager via the "mReactInstanceManager" field. + Field instanceManagerField = ReactActivity.class.getDeclaredField("mReactInstanceManager"); + instanceManagerField.setAccessible(true); + instanceManager = (ReactInstanceManager)instanceManagerField.get(reactActivity); + } + + String latestJSBundleFile = mCodePush.getJSBundleFileInternal(mCodePush.getAssetsBundleFileName()); + + // #2) Update the locally stored JS bundle file path + Field jsBundleField = instanceManager.getClass().getDeclaredField("mJSBundleFile"); + jsBundleField.setAccessible(true); + jsBundleField.set(instanceManager, latestJSBundleFile); + + // #3) Get the context creation method and fire it on the UI thread (which RN enforces) + final Method recreateMethod = instanceManager.getClass().getMethod("recreateReactContextInBackground"); + + final ReactInstanceManager finalizedInstanceManager = instanceManager; + reactActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + try { + recreateMethod.invoke(finalizedInstanceManager); + mCodePush.initializeUpdateAfterRestart(); + } + catch (Exception e) { + // The recreation method threw an unknown exception + // so just simply fallback to restarting the Activity + loadBundleLegacy(); + } + } + }); + } catch (Exception e) { + // Our reflection logic failed somewhere + // so fall back to restarting the Activity + loadBundleLegacy(); + } + } + } + + private Class tryGetClass(String className) { + try { + return Class.forName(className); + } catch (ClassNotFoundException e) { + return null; + } + } + + @ReactMethod + public void downloadUpdate(final ReadableMap updatePackage, final boolean notifyProgress, final Promise promise) { + AsyncTask asyncTask = new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + try { + WritableMap mutableUpdatePackage = CodePushUtils.convertReadableMapToWritableMap(updatePackage); + mutableUpdatePackage.putString(CodePushConstants.BINARY_MODIFIED_TIME_KEY, "" + mCodePush.getBinaryResourcesModifiedTime()); + mUpdateManager.downloadPackage(mutableUpdatePackage, mCodePush.getAssetsBundleFileName(), new DownloadProgressCallback() { + private boolean hasScheduledNextFrame = false; + private DownloadProgress latestDownloadProgress = null; + + @Override + public void call(DownloadProgress downloadProgress) { + if (!notifyProgress) { + return; + } + + latestDownloadProgress = downloadProgress; + // If the download is completed, synchronously send the last event. + if (latestDownloadProgress.isCompleted()) { + dispatchDownloadProgressEvent(); + return; + } + + if (hasScheduledNextFrame) { + return; + } + + hasScheduledNextFrame = true; + getReactApplicationContext().runOnUiQueueThread(new Runnable() { + @Override + public void run() { + ReactChoreographer.getInstance().postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, new Choreographer.FrameCallback() { + @Override + public void doFrame(long frameTimeNanos) { + if (!latestDownloadProgress.isCompleted()) { + dispatchDownloadProgressEvent(); + } + + hasScheduledNextFrame = false; + } + }); + } + }); + } + + public void dispatchDownloadProgressEvent() { + getReactApplicationContext() + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit(CodePushConstants.DOWNLOAD_PROGRESS_EVENT_NAME, latestDownloadProgress.createWritableMap()); + } + }); + + WritableMap newPackage = mUpdateManager.getPackage(CodePushUtils.tryGetString(updatePackage, CodePushConstants.PACKAGE_HASH_KEY)); + promise.resolve(newPackage); + } catch (IOException e) { + e.printStackTrace(); + promise.reject(e); + } catch (CodePushInvalidUpdateException e) { + e.printStackTrace(); + mSettingsManager.saveFailedUpdate(updatePackage); + promise.reject(e); + } + + return null; + } + }; + + asyncTask.execute(); + } + + @ReactMethod + public void getConfiguration(Promise promise) { + Activity currentActivity = getCurrentActivity(); + WritableNativeMap configMap = new WritableNativeMap(); + configMap.putString("appVersion", mCodePush.getAppVersion()); + configMap.putString("deploymentKey", mCodePush.getDeploymentKey()); + configMap.putString("serverUrl", mCodePush.getServerUrl()); + configMap.putString("clientUniqueId", + Settings.Secure.getString(currentActivity.getContentResolver(), + android.provider.Settings.Secure.ANDROID_ID)); + String binaryHash = CodePushUpdateUtils.getHashForBinaryContents(currentActivity, mCodePush.isDebugMode()); + if (binaryHash != null) { + // binaryHash will be null if the React Native assets were not bundled into the APK + // (e.g. in Debug builds) + configMap.putString(CodePushConstants.PACKAGE_HASH_KEY, binaryHash); + } + + promise.resolve(configMap); + } + + @ReactMethod + public void getUpdateMetadata(final int updateState, final Promise promise) { + AsyncTask asyncTask = new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + WritableMap currentPackage = mUpdateManager.getCurrentPackage(); + + if (currentPackage == null) { + promise.resolve(""); + return null; + } + + Boolean currentUpdateIsPending = false; + + if (currentPackage.hasKey(CodePushConstants.PACKAGE_HASH_KEY)) { + String currentHash = currentPackage.getString(CodePushConstants.PACKAGE_HASH_KEY); + currentUpdateIsPending = mSettingsManager.isPendingUpdate(currentHash); + } + + if (updateState == CodePushUpdateState.PENDING.getValue() && !currentUpdateIsPending) { + // The caller wanted a pending update + // but there isn't currently one. + promise.resolve(""); + } else if (updateState == CodePushUpdateState.RUNNING.getValue() && currentUpdateIsPending) { + // The caller wants the running update, but the current + // one is pending, so we need to grab the previous. + promise.resolve(mUpdateManager.getPreviousPackage()); + } else { + // The current package satisfies the request: + // 1) Caller wanted a pending, and there is a pending update + // 2) Caller wanted the running update, and there isn't a pending + // 3) Caller wants the latest update, regardless if it's pending or not + if (mCodePush.isRunningBinaryVersion()) { + // This only matters in Debug builds. Since we do not clear "outdated" updates, + // we need to indicate to the JS side that somehow we have a current update on + // disk that is not actually running. + currentPackage.putBoolean("_isDebugOnly", true); + } + + // Enable differentiating pending vs. non-pending updates + currentPackage.putBoolean("isPending", currentUpdateIsPending); + promise.resolve(currentPackage); + } + + return null; + } + }; + + asyncTask.execute(); + } + + @ReactMethod + public void getNewStatusReport(final Promise promise) { + AsyncTask asyncTask = new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + if (mCodePush.needToReportRollback()) { + mCodePush.setNeedToReportRollback(false); + JSONArray failedUpdates = mSettingsManager.getFailedUpdates(); + if (failedUpdates != null && failedUpdates.length() > 0) { + try { + JSONObject lastFailedPackageJSON = failedUpdates.getJSONObject(failedUpdates.length() - 1); + WritableMap lastFailedPackage = CodePushUtils.convertJsonObjectToWritable(lastFailedPackageJSON); + WritableMap failedStatusReport = mTelemetryManager.getRollbackReport(lastFailedPackage); + if (failedStatusReport != null) { + promise.resolve(failedStatusReport); + return null; + } + } catch (JSONException e) { + throw new CodePushUnknownException("Unable to read failed updates information stored in SharedPreferences.", e); + } + } + } else if (mCodePush.didUpdate()) { + WritableMap currentPackage = mUpdateManager.getCurrentPackage(); + if (currentPackage != null) { + WritableMap newPackageStatusReport = mTelemetryManager.getUpdateReport(currentPackage); + if (newPackageStatusReport != null) { + promise.resolve(newPackageStatusReport); + return null; + } + } + } else if (mCodePush.isRunningBinaryVersion()) { + WritableMap newAppVersionStatusReport = mTelemetryManager.getBinaryUpdateReport(mCodePush.getAppVersion()); + if (newAppVersionStatusReport != null) { + promise.resolve(newAppVersionStatusReport); + return null; + } + } else { + WritableMap retryStatusReport = mTelemetryManager.getRetryStatusReport(); + if (retryStatusReport != null) { + promise.resolve(retryStatusReport); + return null; + } + } + + promise.resolve(""); + return null; + } + }; + + asyncTask.execute(); + } + + @ReactMethod + public void installUpdate(final ReadableMap updatePackage, final int installMode, final int minimumBackgroundDuration, final Promise promise) { + AsyncTask asyncTask = new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + mUpdateManager.installPackage(updatePackage, mSettingsManager.isPendingUpdate(null)); + + String pendingHash = CodePushUtils.tryGetString(updatePackage, CodePushConstants.PACKAGE_HASH_KEY); + if (pendingHash == null) { + throw new CodePushUnknownException("Update package to be installed has no hash."); + } else { + mSettingsManager.savePendingUpdate(pendingHash, /* isLoading */false); + } + + if (installMode == CodePushInstallMode.ON_NEXT_RESUME.getValue()) { + // Store the minimum duration on the native module as an instance + // variable instead of relying on a closure below, so that any + // subsequent resume-based installs could override it. + CodePushNativeModule.this.mMinimumBackgroundDuration = minimumBackgroundDuration; + + if (mLifecycleEventListener == null) { + // Ensure we do not add the listener twice. + mLifecycleEventListener = new LifecycleEventListener() { + private Date lastPausedDate = null; + + @Override + public void onHostResume() { + // Determine how long the app was in the background and ensure + // that it meets the minimum duration amount of time. + long durationInBackground = 0; + if (lastPausedDate != null) { + durationInBackground = (new Date().getTime() - lastPausedDate.getTime()) / 1000; + } + + if (durationInBackground >= CodePushNativeModule.this.mMinimumBackgroundDuration) { + loadBundle(); + } + } + + @Override + public void onHostPause() { + // Save the current time so that when the app is later + // resumed, we can detect how long it was in the background. + lastPausedDate = new Date(); + } + + @Override + public void onHostDestroy() { + } + }; + + getReactApplicationContext().addLifecycleEventListener(mLifecycleEventListener); + } + } + + promise.resolve(""); + + return null; + } + }; + + asyncTask.execute(); + } + + @ReactMethod + public void isFailedUpdate(String packageHash, Promise promise) { + promise.resolve(mSettingsManager.isFailedHash(packageHash)); + } + + @ReactMethod + public void isFirstRun(String packageHash, Promise promise) { + boolean isFirstRun = mCodePush.didUpdate() + && packageHash != null + && packageHash.length() > 0 + && packageHash.equals(mUpdateManager.getCurrentPackageHash()); + promise.resolve(isFirstRun); + } + + @ReactMethod + public void notifyApplicationReady(Promise promise) { + mSettingsManager.removePendingUpdate(); + promise.resolve(""); + } + + @ReactMethod + public void recordStatusReported(ReadableMap statusReport) { + mTelemetryManager.recordStatusReported(statusReport); + } + + @ReactMethod + public void restartApp(boolean onlyIfUpdateIsPending, Promise promise) { + // If this is an unconditional restart request, or there + // is current pending update, then reload the app. + if (!onlyIfUpdateIsPending || CodePush.this.isPendingUpdate(null)) { + loadBundle(); + promise.resolve(true); + return; + } + + promise.resolve(false); + } + + @ReactMethod + public void restartApp(boolean onlyIfUpdateIsPending) { + // If this is an unconditional restart request, or there + // is current pending update, then reload the app. + if (!onlyIfUpdateIsPending || mSettingsManager.isPendingUpdate(null)) { + loadBundle(); + } + } + + @ReactMethod + public void saveStatusReportForRetry(ReadableMap statusReport) { + mTelemetryManager.saveStatusReportForRetry(statusReport); + } + + @ReactMethod + // Replaces the current bundle with the one downloaded from removeBundleUrl. + // It is only to be used during tests. No-ops if the test configuration flag is not set. + public void downloadAndReplaceCurrentBundle(String remoteBundleUrl) { + if (mCodePush.isUsingTestConfiguration()) { + try { + mUpdateManager.downloadAndReplaceCurrentBundle(remoteBundleUrl, mCodePush.getAssetsBundleFileName()); + } catch (IOException e) { + throw new CodePushUnknownException("Unable to replace current bundle", e); + } + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/microsoft/codepush/react/CodePushTelemetryManager.java b/android/app/src/main/java/com/microsoft/codepush/react/CodePushTelemetryManager.java index 3227907..d5c50ac 100644 --- a/android/app/src/main/java/com/microsoft/codepush/react/CodePushTelemetryManager.java +++ b/android/app/src/main/java/com/microsoft/codepush/react/CodePushTelemetryManager.java @@ -11,10 +11,8 @@ import org.json.JSONException; import org.json.JSONObject; public class CodePushTelemetryManager { - - private Context applicationContext; + private SharedPreferences mSettings; private final String APP_VERSION_KEY = "appVersion"; - private final String CODE_PUSH_PREFERENCES; private final String DEPLOYMENT_FAILED_STATUS = "DeploymentFailed"; private final String DEPLOYMENT_KEY_KEY = "deploymentKey"; private final String DEPLOYMENT_SUCCEEDED_STATUS = "DeploymentSucceeded"; @@ -26,9 +24,8 @@ public class CodePushTelemetryManager { private final String RETRY_DEPLOYMENT_REPORT_KEY = "CODE_PUSH_RETRY_DEPLOYMENT_REPORT"; private final String STATUS_KEY = "status"; - public CodePushTelemetryManager(Context applicationContext, String codePushPreferencesKey) { - this.applicationContext = applicationContext; - this.CODE_PUSH_PREFERENCES = codePushPreferencesKey; + public CodePushTelemetryManager(Context applicationContext) { + mSettings = applicationContext.getSharedPreferences(CodePushConstants.CODE_PUSH_PREFERENCES, 0); } public WritableMap getBinaryUpdateReport(String appVersion) { @@ -58,8 +55,7 @@ public class CodePushTelemetryManager { } public WritableMap getRetryStatusReport() { - SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0); - String retryStatusReportString = settings.getString(RETRY_DEPLOYMENT_REPORT_KEY, null); + String retryStatusReportString = mSettings.getString(RETRY_DEPLOYMENT_REPORT_KEY, null); if (retryStatusReportString != null) { clearRetryStatusReport(); try { @@ -127,14 +123,12 @@ public class CodePushTelemetryManager { } public void saveStatusReportForRetry(ReadableMap statusReport) { - SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0); JSONObject statusReportJSON = CodePushUtils.convertReadableToJsonObject(statusReport); - settings.edit().putString(RETRY_DEPLOYMENT_REPORT_KEY, statusReportJSON.toString()).commit(); + mSettings.edit().putString(RETRY_DEPLOYMENT_REPORT_KEY, statusReportJSON.toString()).commit(); } private void clearRetryStatusReport() { - SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0); - settings.edit().remove(RETRY_DEPLOYMENT_REPORT_KEY).commit(); + mSettings.edit().remove(RETRY_DEPLOYMENT_REPORT_KEY).commit(); } private String getDeploymentKeyFromStatusReportIdentifier(String statusReportIdentifier) { @@ -159,8 +153,7 @@ public class CodePushTelemetryManager { } private String getPreviousStatusReportIdentifier() { - SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0); - return settings.getString(LAST_DEPLOYMENT_REPORT_KEY, null); + return mSettings.getString(LAST_DEPLOYMENT_REPORT_KEY, null); } private String getVersionLabelFromStatusReportIdentifier(String statusReportIdentifier) { @@ -177,7 +170,6 @@ public class CodePushTelemetryManager { } private void saveStatusReportedForIdentifier(String appVersionOrPackageIdentifier) { - SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0); - settings.edit().putString(LAST_DEPLOYMENT_REPORT_KEY, appVersionOrPackageIdentifier).commit(); + mSettings.edit().putString(LAST_DEPLOYMENT_REPORT_KEY, appVersionOrPackageIdentifier).commit(); } } diff --git a/android/app/src/main/java/com/microsoft/codepush/react/CodePushPackage.java b/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateManager.java similarity index 81% rename from android/app/src/main/java/com/microsoft/codepush/react/CodePushPackage.java rename to android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateManager.java index 3354e5d..294d06c 100644 --- a/android/app/src/main/java/com/microsoft/codepush/react/CodePushPackage.java +++ b/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateManager.java @@ -17,40 +17,28 @@ import java.net.MalformedURLException; import java.net.URL; import java.nio.ByteBuffer; -public class CodePushPackage { - private final String CODE_PUSH_FOLDER_PREFIX = "CodePush"; - private final String CURRENT_PACKAGE_KEY = "currentPackage"; - private final String DIFF_MANIFEST_FILE_NAME = "hotcodepush.json"; - private final int DOWNLOAD_BUFFER_SIZE = 1024 * 256; - private final String DOWNLOAD_FILE_NAME = "download.zip"; - private final String DOWNLOAD_URL_KEY = "downloadUrl"; - private final String PACKAGE_FILE_NAME = "app.json"; - private final String PACKAGE_HASH_KEY = "packageHash"; - private final String PREVIOUS_PACKAGE_KEY = "previousPackage"; - private final String RELATIVE_BUNDLE_PATH_KEY = "bundlePath"; - private final String STATUS_FILE = "codepush.json"; - private final String UNZIPPED_FOLDER_NAME = "unzipped"; +public class CodePushUpdateManager { - private String documentsDirectory; + private String mDocumentsDirectory; - public CodePushPackage(String documentsDirectory) { - this.documentsDirectory = documentsDirectory; + public CodePushUpdateManager(String documentsDirectory) { + mDocumentsDirectory = documentsDirectory; } private String getDownloadFilePath() { - return CodePushUtils.appendPathComponent(getCodePushPath(), DOWNLOAD_FILE_NAME); + return CodePushUtils.appendPathComponent(getCodePushPath(), CodePushConstants.DOWNLOAD_FILE_NAME); } private String getUnzippedFolderPath() { - return CodePushUtils.appendPathComponent(getCodePushPath(), UNZIPPED_FOLDER_NAME); + return CodePushUtils.appendPathComponent(getCodePushPath(), CodePushConstants.UNZIPPED_FOLDER_NAME); } private String getDocumentsDirectory() { - return documentsDirectory; + return mDocumentsDirectory; } private String getCodePushPath() { - String codePushPath = CodePushUtils.appendPathComponent(getDocumentsDirectory(), CODE_PUSH_FOLDER_PREFIX); + String codePushPath = CodePushUtils.appendPathComponent(getDocumentsDirectory(), CodePushConstants.CODE_PUSH_FOLDER_PREFIX); if (CodePush.isUsingTestConfiguration()) { codePushPath = CodePushUtils.appendPathComponent(codePushPath, "TestPackages"); } @@ -59,7 +47,7 @@ public class CodePushPackage { } private String getStatusFilePath() { - return CodePushUtils.appendPathComponent(getCodePushPath(), STATUS_FILE); + return CodePushUtils.appendPathComponent(getCodePushPath(), CodePushConstants.STATUS_FILE); } public WritableMap getCurrentPackageInfo() { @@ -87,7 +75,7 @@ public class CodePushPackage { public String getCurrentPackageFolderPath() { WritableMap info = getCurrentPackageInfo(); - String packageHash = CodePushUtils.tryGetString(info, CURRENT_PACKAGE_KEY); + String packageHash = CodePushUtils.tryGetString(info, CodePushConstants.CURRENT_PACKAGE_KEY); if (packageHash == null) { return null; } @@ -106,7 +94,7 @@ public class CodePushPackage { return null; } - String relativeBundlePath = CodePushUtils.tryGetString(currentPackage, RELATIVE_BUNDLE_PATH_KEY); + String relativeBundlePath = CodePushUtils.tryGetString(currentPackage, CodePushConstants.RELATIVE_BUNDLE_PATH_KEY); if (relativeBundlePath == null) { return CodePushUtils.appendPathComponent(packageFolder, bundleFileName); } else { @@ -120,12 +108,12 @@ public class CodePushPackage { public String getCurrentPackageHash() { WritableMap info = getCurrentPackageInfo(); - return CodePushUtils.tryGetString(info, CURRENT_PACKAGE_KEY); + return CodePushUtils.tryGetString(info, CodePushConstants.CURRENT_PACKAGE_KEY); } public String getPreviousPackageHash() { WritableMap info = getCurrentPackageInfo(); - return CodePushUtils.tryGetString(info, PREVIOUS_PACKAGE_KEY); + return CodePushUtils.tryGetString(info, CodePushConstants.PREVIOUS_PACKAGE_KEY); } public WritableMap getCurrentPackage() { @@ -148,7 +136,7 @@ public class CodePushPackage { public WritableMap getPackage(String packageHash) { String folderPath = getPackageFolderPath(packageHash); - String packageFilePath = CodePushUtils.appendPathComponent(folderPath, PACKAGE_FILE_NAME); + String packageFilePath = CodePushUtils.appendPathComponent(folderPath, CodePushConstants.PACKAGE_FILE_NAME); try { return CodePushUtils.getWritableMapFromFile(packageFilePath); } catch (IOException e) { @@ -158,16 +146,16 @@ public class CodePushPackage { public void downloadPackage(ReadableMap updatePackage, String expectedBundleFileName, DownloadProgressCallback progressCallback) throws IOException { - String newUpdateHash = CodePushUtils.tryGetString(updatePackage, PACKAGE_HASH_KEY); + String newUpdateHash = CodePushUtils.tryGetString(updatePackage, CodePushConstants.PACKAGE_HASH_KEY); String newUpdateFolderPath = getPackageFolderPath(newUpdateHash); - String newUpdateMetadataPath = CodePushUtils.appendPathComponent(newUpdateFolderPath, PACKAGE_FILE_NAME); + String newUpdateMetadataPath = CodePushUtils.appendPathComponent(newUpdateFolderPath, CodePushConstants.PACKAGE_FILE_NAME); if (FileUtils.fileAtPathExists(newUpdateFolderPath)) { // This removes any stale data in newPackageFolderPath that could have been left // uncleared due to a crash or error during the download or install process. FileUtils.deleteDirectoryAtPath(newUpdateFolderPath); } - String downloadUrlString = CodePushUtils.tryGetString(updatePackage, DOWNLOAD_URL_KEY); + String downloadUrlString = CodePushUtils.tryGetString(updatePackage, CodePushConstants.DOWNLOAD_URL_KEY); HttpURLConnection connection = null; BufferedInputStream bin = null; FileOutputStream fos = null; @@ -186,14 +174,14 @@ public class CodePushPackage { bin = new BufferedInputStream(connection.getInputStream()); File downloadFolder = new File(getCodePushPath()); downloadFolder.mkdirs(); - downloadFile = new File(downloadFolder, DOWNLOAD_FILE_NAME); + downloadFile = new File(downloadFolder, CodePushConstants.DOWNLOAD_FILE_NAME); fos = new FileOutputStream(downloadFile); - bout = new BufferedOutputStream(fos, DOWNLOAD_BUFFER_SIZE); - byte[] data = new byte[DOWNLOAD_BUFFER_SIZE]; + bout = new BufferedOutputStream(fos, CodePushConstants.DOWNLOAD_BUFFER_SIZE); + byte[] data = new byte[CodePushConstants.DOWNLOAD_BUFFER_SIZE]; byte[] header = new byte[4]; int numBytesRead = 0; - while ((numBytesRead = bin.read(data, 0, DOWNLOAD_BUFFER_SIZE)) >= 0) { + while ((numBytesRead = bin.read(data, 0, CodePushConstants.DOWNLOAD_BUFFER_SIZE)) >= 0) { if (receivedBytes < 4) { for (int i = 0; i < numBytesRead; i++) { int headerOffset = (int)(receivedBytes) + i; @@ -236,7 +224,7 @@ public class CodePushPackage { // Merge contents with current update based on the manifest String diffManifestFilePath = CodePushUtils.appendPathComponent(unzippedFolderPath, - DIFF_MANIFEST_FILE_NAME); + CodePushConstants.DIFF_MANIFEST_FILE_NAME); boolean isDiffUpdate = FileUtils.fileAtPathExists(diffManifestFilePath); if (isDiffUpdate) { String currentPackageFolderPath = getCurrentPackageFolderPath(); @@ -253,7 +241,7 @@ public class CodePushPackage { String relativeBundlePath = CodePushUpdateUtils.findJSBundleInUpdateContents(newUpdateFolderPath, expectedBundleFileName); if (relativeBundlePath == null) { - throw new CodePushInvalidUpdateException("Update is invalid - A JS bundle file named \"" + expectedBundleFileName + "\" could not be found within the downloaded contents. Please ensure that your app is syncing with the correct deployment and that you are releasing your CodePush updates using the exact same JS bundle file name that was shipped with your app's binary."); + throw new CodePushInvalidUpdateException("Update is invalid - A JS bundle file named \"" + expectedBundleFileName + "\" could not be found within the downloaded contents. Please check that you are releasing your CodePush updates using the exact same JS bundle file name that was shipped with your app's binary."); } else { if (FileUtils.fileAtPathExists(newUpdateMetadataPath)) { File metadataFileFromOldUpdate = new File(newUpdateMetadataPath); @@ -266,10 +254,10 @@ public class CodePushPackage { JSONObject updatePackageJSON = CodePushUtils.convertReadableToJsonObject(updatePackage); try { - updatePackageJSON.put(RELATIVE_BUNDLE_PATH_KEY, relativeBundlePath); + updatePackageJSON.put(CodePushConstants.RELATIVE_BUNDLE_PATH_KEY, relativeBundlePath); } catch (JSONException e) { throw new CodePushUnknownException("Unable to set key " + - RELATIVE_BUNDLE_PATH_KEY + " to value " + relativeBundlePath + + CodePushConstants.RELATIVE_BUNDLE_PATH_KEY + " to value " + relativeBundlePath + " in update package.", e); } @@ -285,7 +273,7 @@ public class CodePushPackage { } public void installPackage(ReadableMap updatePackage, boolean removePendingUpdate) { - String packageHash = CodePushUtils.tryGetString(updatePackage, PACKAGE_HASH_KEY); + String packageHash = CodePushUtils.tryGetString(updatePackage, CodePushConstants.PACKAGE_HASH_KEY); WritableMap info = getCurrentPackageInfo(); if (removePendingUpdate) { String currentPackageFolderPath = getCurrentPackageFolderPath(); @@ -298,10 +286,10 @@ public class CodePushPackage { FileUtils.deleteDirectoryAtPath(getPackageFolderPath(previousPackageHash)); } - info.putString(PREVIOUS_PACKAGE_KEY, CodePushUtils.tryGetString(info, CURRENT_PACKAGE_KEY)); + info.putString(CodePushConstants.PREVIOUS_PACKAGE_KEY, CodePushUtils.tryGetString(info, CodePushConstants.CURRENT_PACKAGE_KEY)); } - info.putString(CURRENT_PACKAGE_KEY, packageHash); + info.putString(CodePushConstants.CURRENT_PACKAGE_KEY, packageHash); updateCurrentPackageInfo(info); } @@ -309,8 +297,8 @@ public class CodePushPackage { WritableMap info = getCurrentPackageInfo(); String currentPackageFolderPath = getCurrentPackageFolderPath(); FileUtils.deleteDirectoryAtPath(currentPackageFolderPath); - info.putString(CURRENT_PACKAGE_KEY, CodePushUtils.tryGetString(info, PREVIOUS_PACKAGE_KEY)); - info.putNull(PREVIOUS_PACKAGE_KEY); + info.putString(CodePushConstants.CURRENT_PACKAGE_KEY, CodePushUtils.tryGetString(info, CodePushConstants.PREVIOUS_PACKAGE_KEY)); + info.putNull(CodePushConstants.PREVIOUS_PACKAGE_KEY); updateCurrentPackageInfo(info); } @@ -327,10 +315,10 @@ public class CodePushPackage { File downloadFile = new File(getCurrentPackageBundlePath(bundleFileName)); downloadFile.delete(); fos = new FileOutputStream(downloadFile); - bout = new BufferedOutputStream(fos, DOWNLOAD_BUFFER_SIZE); - byte[] data = new byte[DOWNLOAD_BUFFER_SIZE]; + bout = new BufferedOutputStream(fos, CodePushConstants.DOWNLOAD_BUFFER_SIZE); + byte[] data = new byte[CodePushConstants.DOWNLOAD_BUFFER_SIZE]; int numBytesRead = 0; - while ((numBytesRead = bin.read(data, 0, DOWNLOAD_BUFFER_SIZE)) >= 0) { + while ((numBytesRead = bin.read(data, 0, CodePushConstants.DOWNLOAD_BUFFER_SIZE)) >= 0) { bout.write(data, 0, numBytesRead); } } catch (MalformedURLException e) { diff --git a/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateUtils.java b/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateUtils.java index ee6d78d..f83bf98 100644 --- a/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateUtils.java +++ b/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateUtils.java @@ -21,8 +21,6 @@ import java.util.Collections; public class CodePushUpdateUtils { - private static final String CODE_PUSH_HASH_FILE_NAME = "CodePushHash.json"; - private static void addContentsOfFolderToManifest(String folderPath, String pathPrefix, ArrayList manifest) { File folder = new File(folderPath); File[] folderFiles = folder.listFiles(); @@ -102,7 +100,7 @@ public class CodePushUpdateUtils { public static String getHashForBinaryContents(Activity mainActivity, boolean isDebugMode) { try { - return CodePushUtils.getStringFromInputStream(mainActivity.getAssets().open(CODE_PUSH_HASH_FILE_NAME)); + return CodePushUtils.getStringFromInputStream(mainActivity.getAssets().open(CodePushConstants.CODE_PUSH_HASH_FILE_NAME)); } catch (IOException e) { if (!isDebugMode) { // Only print this message in "Release" mode. In "Debug", we may not have the diff --git a/android/app/src/main/java/com/microsoft/codepush/react/CodePushUtils.java b/android/app/src/main/java/com/microsoft/codepush/react/CodePushUtils.java index 55752a6..225e1e9 100644 --- a/android/app/src/main/java/com/microsoft/codepush/react/CodePushUtils.java +++ b/android/app/src/main/java/com/microsoft/codepush/react/CodePushUtils.java @@ -24,8 +24,6 @@ import java.util.Iterator; public class CodePushUtils { - public static final String REACT_NATIVE_LOG_TAG = "ReactNative"; - public static String appendPathComponent(String basePath, String appendPathComponent) { return new File(basePath, appendPathComponent).getAbsolutePath(); } @@ -208,7 +206,7 @@ public class CodePushUtils { } public static void log(String message) { - Log.d(REACT_NATIVE_LOG_TAG, "[CodePush] " + message); + Log.d(CodePushConstants.REACT_NATIVE_LOG_TAG, "[CodePush] " + message); } public static void logBundleUrl(String path) { diff --git a/android/app/src/main/java/com/microsoft/codepush/react/DownloadProgress.java b/android/app/src/main/java/com/microsoft/codepush/react/DownloadProgress.java index 45b5026..ecad8dd 100644 --- a/android/app/src/main/java/com/microsoft/codepush/react/DownloadProgress.java +++ b/android/app/src/main/java/com/microsoft/codepush/react/DownloadProgress.java @@ -4,27 +4,27 @@ import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeMap; class DownloadProgress { - private long totalBytes; - private long receivedBytes; + private long mTotalBytes; + private long mReceivedBytes; public DownloadProgress (long totalBytes, long receivedBytes){ - this.totalBytes = totalBytes; - this.receivedBytes = receivedBytes; + mTotalBytes = totalBytes; + mReceivedBytes = receivedBytes; } public WritableMap createWritableMap() { WritableMap map = new WritableNativeMap(); - if (totalBytes < Integer.MAX_VALUE) { - map.putInt("totalBytes", (int) totalBytes); - map.putInt("receivedBytes", (int) receivedBytes); + if (mTotalBytes < Integer.MAX_VALUE) { + map.putInt("totalBytes", (int) mTotalBytes); + map.putInt("receivedBytes", (int) mReceivedBytes); } else { - map.putDouble("totalBytes", totalBytes); - map.putDouble("receivedBytes", receivedBytes); + map.putDouble("totalBytes", mTotalBytes); + map.putDouble("receivedBytes", mReceivedBytes); } return map; } public boolean isCompleted() { - return this.totalBytes == this.receivedBytes; + return mTotalBytes == mReceivedBytes; } } diff --git a/android/app/src/main/java/com/microsoft/codepush/react/SettingsManager.java b/android/app/src/main/java/com/microsoft/codepush/react/SettingsManager.java new file mode 100644 index 0000000..ac50bf8 --- /dev/null +++ b/android/app/src/main/java/com/microsoft/codepush/react/SettingsManager.java @@ -0,0 +1,125 @@ +package com.microsoft.codepush.react; + +import android.content.Context; +import android.content.SharedPreferences; + +import com.facebook.react.bridge.ReadableMap; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +public class SettingsManager { + + private SharedPreferences mSettings; + + public SettingsManager(Context applicationContext) { + mSettings = applicationContext.getSharedPreferences(CodePushConstants.CODE_PUSH_PREFERENCES, 0); + } + + public JSONArray getFailedUpdates() { + String failedUpdatesString = mSettings.getString(CodePushConstants.FAILED_UPDATES_KEY, null); + if (failedUpdatesString == null) { + return new JSONArray(); + } + + try { + return new JSONArray(failedUpdatesString); + } catch (JSONException e) { + // Unrecognized data format, clear and replace with expected format. + JSONArray emptyArray = new JSONArray(); + mSettings.edit().putString(CodePushConstants.FAILED_UPDATES_KEY, emptyArray.toString()).commit(); + return emptyArray; + } + } + + public JSONObject getPendingUpdate() { + String pendingUpdateString = mSettings.getString(CodePushConstants.PENDING_UPDATE_KEY, null); + if (pendingUpdateString == null) { + return null; + } + + try { + return new JSONObject(pendingUpdateString); + } catch (JSONException e) { + // Should not happen. + CodePushUtils.log("Unable to parse pending update metadata " + pendingUpdateString + + " stored in SharedPreferences"); + return null; + } + } + + + public boolean isFailedHash(String packageHash) { + JSONArray failedUpdates = getFailedUpdates(); + if (packageHash != null) { + for (int i = 0; i < failedUpdates.length(); i++) { + try { + JSONObject failedPackage = failedUpdates.getJSONObject(i); + String failedPackageHash = failedPackage.getString(CodePushConstants.PACKAGE_HASH_KEY); + if (packageHash.equals(failedPackageHash)) { + return true; + } + } catch (JSONException e) { + throw new CodePushUnknownException("Unable to read failedUpdates data stored in SharedPreferences.", e); + } + } + } + + return false; + } + + public boolean isPendingUpdate(String packageHash) { + JSONObject pendingUpdate = getPendingUpdate(); + + try { + return pendingUpdate != null && + !pendingUpdate.getBoolean(CodePushConstants.PENDING_UPDATE_IS_LOADING_KEY) && + (packageHash == null || pendingUpdate.getString(CodePushConstants.PENDING_UPDATE_HASH_KEY).equals(packageHash)); + } + catch (JSONException e) { + throw new CodePushUnknownException("Unable to read pending update metadata in isPendingUpdate.", e); + } + } + + public void removeFailedUpdates() { + mSettings.edit().remove(CodePushConstants.FAILED_UPDATES_KEY).commit(); + } + + public void removePendingUpdate() { + mSettings.edit().remove(CodePushConstants.PENDING_UPDATE_KEY).commit(); + } + + public void saveFailedUpdate(ReadableMap failedPackage) { + String failedUpdatesString = mSettings.getString(CodePushConstants.FAILED_UPDATES_KEY, null); + JSONArray failedUpdates; + if (failedUpdatesString == null) { + failedUpdates = new JSONArray(); + } else { + try { + failedUpdates = new JSONArray(failedUpdatesString); + } catch (JSONException e) { + // Should not happen. + throw new CodePushMalformedDataException("Unable to parse failed updates information " + + failedUpdatesString + " stored in SharedPreferences", e); + } + } + + JSONObject failedPackageJSON = CodePushUtils.convertReadableToJsonObject(failedPackage); + failedUpdates.put(failedPackageJSON); + mSettings.edit().putString(CodePushConstants.FAILED_UPDATES_KEY, failedUpdates.toString()).commit(); + } + + public void savePendingUpdate(String packageHash, boolean isLoading) { + JSONObject pendingUpdate = new JSONObject(); + try { + pendingUpdate.put(CodePushConstants.PENDING_UPDATE_HASH_KEY, packageHash); + pendingUpdate.put(CodePushConstants.PENDING_UPDATE_IS_LOADING_KEY, isLoading); + mSettings.edit().putString(CodePushConstants.PENDING_UPDATE_KEY, pendingUpdate.toString()).commit(); + } catch (JSONException e) { + // Should not happen. + throw new CodePushUnknownException("Unable to save pending update.", e); + } + } + +} diff --git a/ios/CodePush.xcodeproj/project.pbxproj b/ios/CodePush.xcodeproj/project.pbxproj index eed3c75..4403e95 100644 --- a/ios/CodePush.xcodeproj/project.pbxproj +++ b/ios/CodePush.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 1BCC09A71CC19EB700DDC0DD /* RCTConvert+CodePushUpdateState.m in Sources */ = {isa = PBXBuildFile; fileRef = 1BCC09A61CC19EB700DDC0DD /* RCTConvert+CodePushUpdateState.m */; }; 540D20121C7684FE00D6EF41 /* CodePushUpdateUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 540D20111C7684FE00D6EF41 /* CodePushUpdateUtils.m */; }; 5421FE311C58AD5A00986A55 /* CodePushTelemetryManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5421FE301C58AD5A00986A55 /* CodePushTelemetryManager.m */; }; + 5498D8F61D21F14100B5EB43 /* CodePushUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 5498D8F51D21F14100B5EB43 /* CodePushUtils.m */; }; 54A0026C1C0E2880004C3CEC /* aescrypt.c in Sources */ = {isa = PBXBuildFile; fileRef = 54A0024C1C0E2880004C3CEC /* aescrypt.c */; }; 54A0026D1C0E2880004C3CEC /* aeskey.c in Sources */ = {isa = PBXBuildFile; fileRef = 54A0024D1C0E2880004C3CEC /* aeskey.c */; }; 54A0026E1C0E2880004C3CEC /* aestab.c in Sources */ = {isa = PBXBuildFile; fileRef = 54A0024F1C0E2880004C3CEC /* aestab.c */; }; @@ -53,6 +54,7 @@ 1BCC09A61CC19EB700DDC0DD /* RCTConvert+CodePushUpdateState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "RCTConvert+CodePushUpdateState.m"; path = "CodePush/RCTConvert+CodePushUpdateState.m"; sourceTree = ""; }; 540D20111C7684FE00D6EF41 /* CodePushUpdateUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CodePushUpdateUtils.m; path = CodePush/CodePushUpdateUtils.m; sourceTree = ""; }; 5421FE301C58AD5A00986A55 /* CodePushTelemetryManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CodePushTelemetryManager.m; path = CodePush/CodePushTelemetryManager.m; sourceTree = ""; }; + 5498D8F51D21F14100B5EB43 /* CodePushUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CodePushUtils.m; path = CodePush/CodePushUtils.m; sourceTree = ""; }; 54A0024A1C0E2880004C3CEC /* aes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = aes.h; sourceTree = ""; }; 54A0024B1C0E2880004C3CEC /* aes_via_ace.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = aes_via_ace.h; sourceTree = ""; }; 54A0024C1C0E2880004C3CEC /* aescrypt.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = aescrypt.c; sourceTree = ""; }; @@ -170,6 +172,7 @@ 58B511D21A9E6C8500147676 = { isa = PBXGroup; children = ( + 5498D8F51D21F14100B5EB43 /* CodePushUtils.m */, 13BE3DEC1AC21097009241FE /* CodePush.h */, 13BE3DED1AC21097009241FE /* CodePush.m */, 81D51F391B6181C2000DA084 /* CodePushConfig.m */, @@ -263,6 +266,7 @@ 54A0026C1C0E2880004C3CEC /* aescrypt.c in Sources */, 1B762E901C9A5E9A006EF800 /* CodePushErrorUtils.m in Sources */, 54A002701C0E2880004C3CEC /* fileenc.c in Sources */, + 5498D8F61D21F14100B5EB43 /* CodePushUtils.m in Sources */, 810D4E6D1B96935000B397E9 /* CodePushPackage.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ios/CodePush/CodePush.h b/ios/CodePush/CodePush.h index 782b5b9..22affb1 100644 --- a/ios/CodePush/CodePush.h +++ b/ios/CodePush/CodePush.h @@ -143,6 +143,8 @@ failCallback:(void (^)(NSError *err))failCallback; @end +void CPLog(NSString *formatString, ...); + typedef NS_ENUM(NSInteger, CodePushInstallMode) { CodePushInstallModeImmediate, CodePushInstallModeOnNextRestart, diff --git a/ios/CodePush/CodePush.m b/ios/CodePush/CodePush.m index 6e2299e..3931cc1 100644 --- a/ios/CodePush/CodePush.m +++ b/ios/CodePush/CodePush.m @@ -91,7 +91,7 @@ static NSString *bundleResourceName = @"main"; NSURL *binaryBundleURL = [self binaryBundleURL]; if (error || !packageFile) { - NSLog(logMessageFormat, binaryBundleURL); + CPLog(logMessageFormat, binaryBundleURL); isRunningBinaryVersion = YES; return binaryBundleURL; } @@ -99,7 +99,7 @@ static NSString *bundleResourceName = @"main"; NSString *binaryAppVersion = [[CodePushConfig current] appVersion]; NSDictionary *currentPackageMetadata = [CodePushPackage getCurrentPackage:&error]; if (error || !currentPackageMetadata) { - NSLog(logMessageFormat, binaryBundleURL); + CPLog(logMessageFormat, binaryBundleURL); isRunningBinaryVersion = YES; return binaryBundleURL; } @@ -110,7 +110,7 @@ static NSString *bundleResourceName = @"main"; if ([[CodePushUpdateUtils modifiedDateStringOfFileAtURL:binaryBundleURL] isEqualToString:packageDate] && ([CodePush isUsingTestConfiguration] ||[binaryAppVersion isEqualToString:packageAppVersion])) { // Return package file because it is newer than the app store binary's JS bundle NSURL *packageUrl = [[NSURL alloc] initFileURLWithPath:packageFile]; - NSLog(logMessageFormat, packageUrl); + CPLog(logMessageFormat, packageUrl); isRunningBinaryVersion = NO; return packageUrl; } else { @@ -123,7 +123,7 @@ static NSString *bundleResourceName = @"main"; [CodePush clearUpdates]; } - NSLog(logMessageFormat, binaryBundleURL); + CPLog(logMessageFormat, binaryBundleURL); isRunningBinaryVersion = YES; return binaryBundleURL; } @@ -310,7 +310,7 @@ static NSString *bundleResourceName = @"main"; if (updateIsLoading) { // Pending update was initialized, but notifyApplicationReady was not called. // Therefore, deduce that it is a broken update and rollback. - NSLog(@"Update did not finish loading the last time, rolling back to a previous version."); + CPLog(@"Update did not finish loading the last time, rolling back to a previous version."); needToReportRollback = YES; [self rollbackPackage]; } else { @@ -582,7 +582,7 @@ RCT_EXPORT_METHOD(getConfiguration:(RCTPromiseResolveBlock)resolve // isRunningBinaryVersion will not get set to "YES" if running against the packager. NSString *binaryHash = [CodePushUpdateUtils getHashForBinaryContents:[CodePush binaryBundleURL] error:&error]; if (error) { - NSLog(@"Error obtaining hash for binary contents: %@", error); + CPLog(@"Error obtaining hash for binary contents: %@", error); resolve(configuration); return; } @@ -749,7 +749,9 @@ RCT_EXPORT_METHOD(restartApp:(BOOL)onlyIfUpdateIsPending if (!onlyIfUpdateIsPending || [self isPendingUpdate:nil]) { [self loadBundle]; resolve(@(YES)); + return; } + resolve(@(NO)); } diff --git a/ios/CodePush/CodePushPackage.m b/ios/CodePush/CodePushPackage.m index e2ae37c..e5d830f 100644 --- a/ios/CodePush/CodePushPackage.m +++ b/ios/CodePush/CodePushPackage.m @@ -29,7 +29,7 @@ static NSString *const UnzippedFolderName = @"unzipped"; error:&error]; if (error) { - NSLog(@"Error downloading from URL %@", remoteBundleUrl); + CPLog(@"Error downloading from URL %@", remoteBundleUrl); } else { NSString *currentPackageBundlePath = [self getCurrentPackageBundlePath:&error]; [downloadedBundle writeToFile:currentPackageBundlePath @@ -101,7 +101,7 @@ static NSString *const UnzippedFolderName = @"unzipped"; [[NSFileManager defaultManager] removeItemAtPath:downloadFilePath error:&nonFailingError]; if (nonFailingError) { - NSLog(@"Error deleting downloaded file: %@", nonFailingError); + CPLog(@"Error deleting downloaded file: %@", nonFailingError); nonFailingError = nil; } @@ -198,7 +198,7 @@ static NSString *const UnzippedFolderName = @"unzipped"; [[NSFileManager defaultManager] removeItemAtPath:unzippedFolderPath error:&nonFailingError]; if (nonFailingError) { - NSLog(@"Error deleting downloaded file: %@", nonFailingError); + CPLog(@"Error deleting downloaded file: %@", nonFailingError); nonFailingError = nil; } @@ -462,7 +462,7 @@ static NSString *const UnzippedFolderName = @"unzipped"; [[NSFileManager defaultManager] removeItemAtPath:currentPackageFolderPath error:&deleteError]; if (deleteError) { - NSLog(@"Error deleting pending package: %@", deleteError); + CPLog(@"Error deleting pending package: %@", deleteError); } } } else { @@ -474,7 +474,7 @@ static NSString *const UnzippedFolderName = @"unzipped"; [[NSFileManager defaultManager] removeItemAtPath:previousPackageFolderPath error:&deleteError]; if (deleteError) { - NSLog(@"Error deleting old package: %@", deleteError); + CPLog(@"Error deleting old package: %@", deleteError); } } [info setValue:info[@"currentPackage"] forKey:@"previousPackage"]; @@ -503,7 +503,7 @@ static NSString *const UnzippedFolderName = @"unzipped"; [[NSFileManager defaultManager] removeItemAtPath:currentPackageFolderPath error:&deleteError]; if (deleteError) { - NSLog(@"Error deleting current package contents at %@", currentPackageFolderPath); + CPLog(@"Error deleting current package contents at %@", currentPackageFolderPath); } [info setValue:info[@"previousPackage"] forKey:@"currentPackage"]; diff --git a/ios/CodePush/CodePushUpdateUtils.m b/ios/CodePush/CodePushUpdateUtils.m index 334ea87..48270e0 100644 --- a/ios/CodePush/CodePushUpdateUtils.m +++ b/ios/CodePush/CodePushUpdateUtils.m @@ -40,6 +40,16 @@ NSString * const ManifestFolderPrefix = @"CodePush"; } } ++ (void)addFileToManifest:(NSURL *)fileURL + manifest:(NSMutableArray *)manifest +{ + if ([[NSFileManager defaultManager] fileExistsAtPath:[fileURL path]]) { + NSData *fileContents = [NSData dataWithContentsOfURL:fileURL]; + NSString *fileContentsHash = [self computeHashForData:fileContents]; + [manifest addObject:[NSString stringWithFormat:@"%@/%@:%@", [self manifestFolderPrefix], [fileURL lastPathComponent], fileContentsHash]]; + } +} + + (NSString *)computeFinalHashFromManifest:(NSMutableArray *)manifest error:(NSError **)error { @@ -190,9 +200,9 @@ NSString * const ManifestFolderPrefix = @"CodePush"; } } - NSData *jsBundleContents = [NSData dataWithContentsOfURL:binaryBundleUrl]; - NSString *jsBundleContentsHash = [self computeHashForData:jsBundleContents]; - [manifest addObject:[[NSString stringWithFormat:@"%@/%@:", [self manifestFolderPrefix], [binaryBundleUrl lastPathComponent]] stringByAppendingString:jsBundleContentsHash]]; + [self addFileToManifest:binaryBundleUrl manifest:manifest]; + [self addFileToManifest:[binaryBundleUrl URLByAppendingPathExtension:@"meta"] manifest:manifest]; + binaryHash = [self computeFinalHashFromManifest:manifest error:error]; // Cache the hash in user preferences. This assumes that the modified date for the diff --git a/ios/CodePush/CodePushUtils.m b/ios/CodePush/CodePushUtils.m new file mode 100644 index 0000000..4b5d477 --- /dev/null +++ b/ios/CodePush/CodePushUtils.m @@ -0,0 +1,9 @@ +#import "CodePush.h" + +void CPLog(NSString *formatString, ...) { + va_list args; + va_start(args, formatString); + NSString *prependedFormatString = [NSString stringWithFormat:@"\n[CodePush] %@", formatString]; + NSLogv(prependedFormatString, args); + va_end(args); +} \ No newline at end of file diff --git a/package.json b/package.json index 722d1ce..53e5463 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-code-push", - "version": "1.12.2-beta", + "version": "1.13.1-beta", "description": "React Native plugin for the CodePush service", "main": "CodePush.js", "typings": "typings/react-native-code-push.d.ts", diff --git a/scripts/generateBundledResourcesHash.js b/scripts/generateBundledResourcesHash.js index 81a6b4a..c4afc2d 100644 --- a/scripts/generateBundledResourcesHash.js +++ b/scripts/generateBundledResourcesHash.js @@ -1,10 +1,10 @@ /* * This script generates a hash of all the React Native bundled assets and writes it into - * into the APK. The hash in "updateCheck" requests to prevent downloading an identical + * into the APK. The hash in "updateCheck" requests to prevent downloading an identical * update to the one already present in the binary. - * - * It first creates a snapshot of the contents in the resource directory by creating - * a map with the modified time of all the files in the directory. It then compares this + * + * It first creates a snapshot of the contents in the resource directory by creating + * a map with the modified time of all the files in the directory. It then compares this * snapshot with the one saved earlier in "recordFilesBeforeBundleCommand.js" to figure * out which files were generated by the "react-native bundle" command. It then computes * the hash for each file to generate a manifest, and then computes a hash over the entire @@ -49,44 +49,59 @@ var manifest = []; if (bundleGeneratedAssetFiles.length) { bundleGeneratedAssetFiles.forEach(function(assetFile) { // Generate hash for each asset file - var readStream = fs.createReadStream(resourcesDir + assetFile); - var hashStream = crypto.createHash(HASH_ALGORITHM); + addFileToManifest(resourcesDir, assetFile, manifest, function() { + if (manifest.length === bundleGeneratedAssetFiles.length) { + addJsBundleAndMetaToManifest(); + } + }); + }); +} else { + addJsBundleAndMetaToManifest(); +} - readStream.pipe(hashStream) - .on("error", function(error) { - throw error; - }) - .on("finish", function() { - hashStream.end(); - var buffer = hashStream.read(); - var fileHash = buffer.toString("hex"); - manifest.push(CODE_PUSH_FOLDER_PREFIX + assetFile.replace(/\\/g, "/") + ":" + fileHash); - - if (manifest.length === bundleGeneratedAssetFiles.length) { - // Generate hash for JS bundle - readStream = fs.createReadStream(jsBundleFilePath); - hashStream = crypto.createHash(HASH_ALGORITHM); - readStream.pipe(hashStream) - .on("error", function(error) { - throw error; - }) - .on("finish", function() { - hashStream.end(); - var buffer = hashStream.read(); - var fileHash = buffer.toString("hex"); - manifest.push(CODE_PUSH_FOLDER_PREFIX + "/" + path.basename(jsBundleFilePath) + ":" + fileHash); - manifest = manifest.sort(); - - var finalHash = crypto.createHash(HASH_ALGORITHM) - .update(JSON.stringify(manifest)) - .digest("hex"); - - var savedResourcesManifestPath = assetsDir + "/" + CODE_PUSH_HASH_FILE_NAME; - fs.writeFileSync(savedResourcesManifestPath, finalHash); - }); - } - }); +function addJsBundleAndMetaToManifest() { + addFileToManifest(path.dirname(jsBundleFilePath), path.basename(jsBundleFilePath), manifest, function() { + var jsBundleMetaFilePath = jsBundleFilePath + ".meta"; + addFileToManifest(path.dirname(jsBundleMetaFilePath), path.basename(jsBundleMetaFilePath), manifest, function() { + manifest = manifest.sort(); + var finalHash = crypto.createHash(HASH_ALGORITHM) + .update(JSON.stringify(manifest)) + .digest("hex"); + + console.log(finalHash); + + var savedResourcesManifestPath = assetsDir + "/" + CODE_PUSH_HASH_FILE_NAME; + fs.writeFileSync(savedResourcesManifestPath, finalHash); + }); }); } -fs.unlinkSync(TEMP_FILE_PATH); \ No newline at end of file +function addFileToManifest(folder, assetFile, manifest, done) { + var fullFilePath = path.join(folder, assetFile); + if (!fileExists(fullFilePath)) { + done(); + return; + } + + var readStream = fs.createReadStream(path.join(folder, assetFile)); + var hashStream = crypto.createHash(HASH_ALGORITHM); + + readStream.pipe(hashStream) + .on("error", function(error) { + throw error; + }) + .on("finish", function() { + hashStream.end(); + var buffer = hashStream.read(); + var fileHash = buffer.toString("hex"); + manifest.push(path.join(CODE_PUSH_FOLDER_PREFIX, assetFile).replace(/\\/g, "/") + ":" + fileHash); + done(); + }); +} + +function fileExists(file) { + try { return fs.statSync(file).isFile(); } + catch (e) { return false; } +} + +fs.unlinkSync(TEMP_FILE_PATH); diff --git a/typings/react-native-code-push.d.ts b/typings/react-native-code-push.d.ts index 7867921..cf3bacc 100644 --- a/typings/react-native-code-push.d.ts +++ b/typings/react-native-code-push.d.ts @@ -243,7 +243,7 @@ declare namespace CodePush { /** * Notifies the CodePush runtime that an installed update is considered successful. */ - function notifyAppReady(): Promise; + function notifyAppReady(): Promise; /** * Allow CodePush to restart the app.