From a383d9f930cfa5c6c75a28338e301dcc1bde52cb Mon Sep 17 00:00:00 2001 From: Jonathan Carter Date: Tue, 21 Jun 2016 17:01:16 -0700 Subject: [PATCH 01/26] Adding link to getting started video --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index aaaf797..208987a 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ As new core components are released, which support referencing assets, we'll upd ## Getting Started + Once you've followed the general-purpose ["getting started"](http://codepush.tools/docs/getting-started.html) instructions for setting up your CodePush account, you can start CodePush-ifying your React Native app by running the following command from within your app's root directory: ```shell @@ -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) 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 From d46c0f89eae9fb048c8b892f7d800dbb81917ed4 Mon Sep 17 00:00:00 2001 From: Jonathan Carter Date: Tue, 21 Jun 2016 18:01:37 -0700 Subject: [PATCH 02/26] Fixing links --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 208987a..56d1585 100644 --- a/README.md +++ b/README.md @@ -79,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. 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). +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 From 59fb8256d3d70d07e69815b2165ba1cdcd7dd0db Mon Sep 17 00:00:00 2001 From: Jonathan Carter Date: Tue, 21 Jun 2016 18:02:39 -0700 Subject: [PATCH 03/26] Correcting hyperlink --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 56d1585..875f559 100644 --- a/README.md +++ b/README.md @@ -79,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--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). +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 From c563b71739a10cabbe1ccad00ba5d0faf0fafffc Mon Sep 17 00:00:00 2001 From: Jonathan Carter Date: Tue, 21 Jun 2016 18:28:06 -0700 Subject: [PATCH 04/26] Simplify RNPM instructions --- README.md | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/README.md b/README.md index 875f559..9cee8e9 100644 --- a/README.md +++ b/README.md @@ -101,17 +101,7 @@ In order to accomodate as many developer preferences as possible, the CodePush p *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 - -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) From ac32063a4d5868921c1ab3cb044a8018b99bcd15 Mon Sep 17 00:00:00 2001 From: Jonathan Carter Date: Wed, 22 Jun 2016 07:17:05 -0700 Subject: [PATCH 05/26] Fixing typos --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9cee8e9..e62da3e 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ 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. @@ -199,7 +199,7 @@ 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. @@ -450,7 +450,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 @@ -1045,7 +1045,7 @@ The Objective-C API is made available by importing the `CodePush.h` header into 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 :) From 507a3dc33a15d8f006f54ea15c21ccc8e00cdcd0 Mon Sep 17 00:00:00 2001 From: Jonathan Carter Date: Wed, 22 Jun 2016 10:54:48 -0700 Subject: [PATCH 06/26] Add comment about react-native link --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e62da3e..9146c08 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,6 @@ As new core components are released, which support referencing assets, we'll upd ## Getting Started - Once you've followed the general-purpose ["getting started"](http://codepush.tools/docs/getting-started.html) instructions for setting up your CodePush account, you can start CodePush-ifying your React Native app by running the following command from within your app's root directory: ```shell @@ -99,7 +98,7 @@ In order to accommodate as many developer preferences as possible, the CodePush 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.* + *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. Alternatively, if you're using React Native 0.28+, you can run `react-native link` instead, since RNPM was merged into React Native starting in that version.* And that's it! Isn't RNPM awesome? :) @@ -209,7 +208,7 @@ In order to accommodate as many developer preferences as possible, the CodePush 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.* + *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. Alternatively, if you're using React Native 0.28+, you can run `react-native link` instead, since RNPM was merged into React Native starting in that version.* 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. From e4a8d6146b6fab086c8dcda0982f82d588aca3f3 Mon Sep 17 00:00:00 2001 From: Geoffrey Goh Date: Wed, 22 Jun 2016 11:22:37 -0700 Subject: [PATCH 07/26] update react.gradle location --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9146c08..b66f6e8 100644 --- a/README.md +++ b/README.md @@ -216,7 +216,7 @@ In order to accommodate as many developer preferences as possible, the CodePush ```gradle ... - apply from: "react.gradle" + apply from: "../../node_modules/react-native/react.gradle" apply from: "../../node_modules/react-native-code-push/android/codepush.gradle" ... ``` From 76a26bed95510ca0d29e327eaa8edfdc33a2f268 Mon Sep 17 00:00:00 2001 From: Jonathan Carter Date: Wed, 22 Jun 2016 12:03:00 -0700 Subject: [PATCH 08/26] Removing react-native link comment --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9146c08..af3d8bc 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ In order to accommodate as many developer preferences as possible, the CodePush 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. Alternatively, if you're using React Native 0.28+, you can run `react-native link` instead, since RNPM was merged into React Native starting in that version.* + *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.* And that's it! Isn't RNPM awesome? :) @@ -208,7 +208,7 @@ In order to accommodate as many developer preferences as possible, the CodePush 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. Alternatively, if you're using React Native 0.28+, you can run `react-native link` instead, since RNPM was merged into React Native starting in that version.* + *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. From d919ed8eade227c138747e18a1cce1d7676dc7a6 Mon Sep 17 00:00:00 2001 From: Ruben Rios Date: Wed, 22 Jun 2016 12:09:13 -0700 Subject: [PATCH 09/26] Updating with Code of Conduct info. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index af3d8bc..bc7bb79 100644 --- a/README.md +++ b/README.md @@ -1141,3 +1141,5 @@ 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) + +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. From 21ef0f4df4f0bdd4252ec2a770630778d6d0c924 Mon Sep 17 00:00:00 2001 From: Jonathan Carter Date: Thu, 23 Jun 2016 07:56:58 -0700 Subject: [PATCH 10/26] Fix Android docs --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e2c3cc1..e9182bd 100644 --- a/README.md +++ b/README.md @@ -467,13 +467,13 @@ To set this up, perform the following steps: buildTypes { debug { ... - buildConfigField "String", "CODEPUSH_KEY", "" + buildConfigField 'String', 'CODEPUSH_KEY', '""' ... } release { ... - buildConfigField "String", "CODEPUSH_KEY", "" + buildConfigField 'String', 'CODEPUSH_KEY', '""' ... } } @@ -502,7 +502,7 @@ If you want to be able to install both debug and release builds simultaneously o ```groovy buildTypes { debug { - applicationIdSuffix ".debug" + applicationIdSuffix '.debug' } } ``` From 4901346716cda98d13782cacbdeada90a8ac9ecd Mon Sep 17 00:00:00 2001 From: Jonathan Carter Date: Thu, 23 Jun 2016 08:00:28 -0700 Subject: [PATCH 11/26] Fixing quotes --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e9182bd..d2031fb 100644 --- a/README.md +++ b/README.md @@ -467,13 +467,13 @@ To set this up, perform the following steps: buildTypes { debug { ... - buildConfigField 'String', 'CODEPUSH_KEY', '""' + buildConfigField "String", "CODEPUSH_KEY", '""' ... } release { ... - buildConfigField 'String', 'CODEPUSH_KEY', '""' + buildConfigField "String", "CODEPUSH_KEY", '""' ... } } @@ -502,7 +502,7 @@ If you want to be able to install both debug and release builds simultaneously o ```groovy buildTypes { debug { - applicationIdSuffix '.debug' + applicationIdSuffix ".debug" } } ``` From 8c432e4595d6c9d6349e2f3f83970629dcd14823 Mon Sep 17 00:00:00 2001 From: Ian MacLeod Date: Thu, 23 Jun 2016 10:52:35 -0700 Subject: [PATCH 12/26] Bug fix for #379 where status report can be undefined on notifyApplicationReady I introduced a crash on subsequent launches after an update is applied, where `getNewStatusReport()` returns nothing. This fixes it! --- CodePush.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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; } From 2b628007e0298909e0063e5906cb890cd6ae40aa Mon Sep 17 00:00:00 2001 From: Ian MacLeod Date: Thu, 23 Jun 2016 10:59:00 -0700 Subject: [PATCH 13/26] Indicate that StatusReports are optional, too --- typings/react-native-code-push.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. From 65b906d96a68c8f65abde475b8a17c5eb1aef61c Mon Sep 17 00:00:00 2001 From: Jonathan Carter Date: Thu, 23 Jun 2016 18:45:46 -0700 Subject: [PATCH 14/26] Adding ZeeMee mention --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index d2031fb..055504e 100644 --- a/README.md +++ b/README.md @@ -1142,4 +1142,8 @@ 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). + +--- + 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. From ec5faffab72ad9748b2291e03fd0636a38ace871 Mon Sep 17 00:00:00 2001 From: Cory Smith Date: Fri, 24 Jun 2016 15:34:58 -0600 Subject: [PATCH 15/26] Fixing some spelling mistakes in Readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 055504e..df90e00 100644 --- a/README.md +++ b/README.md @@ -437,7 +437,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) @@ -483,7 +483,7 @@ 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); From fcc05c65f8766069398413f954b5f7db04a15ca5 Mon Sep 17 00:00:00 2001 From: Jonathan Carter Date: Fri, 24 Jun 2016 14:47:31 -0700 Subject: [PATCH 16/26] Binary hash ios --- ios/CodePush/CodePushUpdateUtils.m | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) 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 From eb510663e5f23bd5d2338dc8e7fbc660f61d3f57 Mon Sep 17 00:00:00 2001 From: Geoffrey Goh Date: Fri, 24 Jun 2016 19:40:32 -0700 Subject: [PATCH 17/26] Binary Hashing (Android) (#401) binary hashing android --- scripts/generateBundledResourcesHash.js | 89 ++++++++++++++----------- 1 file changed, 49 insertions(+), 40 deletions(-) diff --git a/scripts/generateBundledResourcesHash.js b/scripts/generateBundledResourcesHash.js index 81a6b4a..2eb46c9 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,53 @@ 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) { + // Generate hash for JS bundle + addFileToManifest(path.dirname(jsBundleFilePath), path.basename(jsBundleFilePath), manifest, function() { + // ...and the JS bundle "meta" + 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"); - 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); - }); - } - }); + var savedResourcesManifestPath = assetsDir + "/" + CODE_PUSH_HASH_FILE_NAME; + fs.writeFileSync(savedResourcesManifestPath, finalHash); + }); + }); + } + }); }); } +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); \ No newline at end of file From e8005b26dfa725df416239030d2e938d871e68aa Mon Sep 17 00:00:00 2001 From: Jonathan Carter Date: Mon, 27 Jun 2016 09:01:12 -0700 Subject: [PATCH 18/26] Adding TS mention --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index df90e00..f858a80 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? @@ -1144,6 +1145,10 @@ In addition to being able to use the CodePush CLI to "manually" release updates, 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. From e0719383485ba6a7eafccf25c6cbbfa963f36706 Mon Sep 17 00:00:00 2001 From: Jonathan Carter Date: Mon, 27 Jun 2016 09:02:32 -0700 Subject: [PATCH 19/26] Fixing hyperlink in TOC --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f858a80..32bb1f6 100644 --- a/README.md +++ b/README.md @@ -20,7 +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) +* [TypeScript Consumption](#typescript-consumption) ## How does it work? From 38a52dc7a64050a6140491152d12bb95ab191043 Mon Sep 17 00:00:00 2001 From: Geoffrey Goh Date: Mon, 27 Jun 2016 09:27:31 -0700 Subject: [PATCH 20/26] 0.29 compat + refactoring android (#398) 0.29 compat --- .../codepushdemoapp/MainActivity.java | 2 +- README.md | 233 +++--- android/app/build.gradle | 4 +- .../microsoft/codepush/react/CodePush.java | 719 +++--------------- .../codepush/react/CodePushConstants.java | 28 + .../codepush/react/CodePushDialog.java | 16 +- .../codepush/react/CodePushNativeModule.java | 477 ++++++++++++ .../react/CodePushTelemetryManager.java | 24 +- ...ackage.java => CodePushUpdateManager.java} | 78 +- .../codepush/react/CodePushUpdateUtils.java | 4 +- .../codepush/react/CodePushUtils.java | 4 +- .../codepush/react/DownloadProgress.java | 20 +- .../codepush/react/SettingsManager.java | 125 +++ 13 files changed, 940 insertions(+), 794 deletions(-) create mode 100644 android/app/src/main/java/com/microsoft/codepush/react/CodePushConstants.java create mode 100644 android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java rename android/app/src/main/java/com/microsoft/codepush/react/{CodePushPackage.java => CodePushUpdateManager.java} (81%) create mode 100644 android/app/src/main/java/com/microsoft/codepush/react/SettingsManager.java 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 32bb1f6..4c624b9 100644 --- a/README.md +++ b/README.md @@ -28,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).* @@ -52,7 +52,7 @@ We try our best to maintain backwards compatability of our plugin with previous 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` | @@ -89,7 +89,7 @@ Once you've acquired the CodePush plugin, you need to integrate it into the Xcod 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. @@ -100,7 +100,7 @@ In order to accommodate as many developer preferences as possible, the CodePush 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.* - + And that's it! Isn't RNPM awesome? :) #### Plugin Installation (iOS - CocoaPods) @@ -110,20 +110,20 @@ And that's it! Isn't RNPM awesome? :) ```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 @@ -138,12 +138,12 @@ And that's it! Isn't RNPM awesome? :) ![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. @@ -164,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 @@ -175,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; @@ -186,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 @@ -201,7 +201,7 @@ In order to integrate CodePush into your Android project, perform the following 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. @@ -210,31 +210,31 @@ In order to accommodate as many developer preferences as possible, the CodePush 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: "../../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 { @@ -242,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" @@ -256,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 @@ -270,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(), @@ -301,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 @@ -311,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 @@ -339,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 @@ -396,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`). @@ -407,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 @@ -461,17 +500,17 @@ 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", '""' ... } - + release { ... buildConfigField "String", "CODEPUSH_KEY", '""' @@ -483,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 `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. + +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.* @@ -533,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 @@ -545,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. @@ -606,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. @@ -664,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?"); } @@ -697,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(); } - - ... + + ... } ``` @@ -739,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() @@ -766,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 @@ -790,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? } }); ``` @@ -807,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 @@ -835,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(); @@ -860,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: @@ -907,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 }); @@ -920,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: @@ -939,8 +978,8 @@ codePush.sync({ updateDialog: true }, break; } }, - ({ receivedBytes, totalBytes, }) => { - /* Update download modal progress */ + ({ receivedBytes, totalBytes, }) => { + /* Update download modal progress */ } ); ``` @@ -983,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 @@ -1010,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 @@ -1025,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 @@ -1034,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) @@ -1043,7 +1082,7 @@ 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 accommodate the following scenarios: @@ -1055,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 @@ -1082,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. 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 bd7585e..e11ae7b 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.getBundleUrl(); + } + + 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,510 +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) { - // If this is an unconditional restart request, or there - // is current pending update, then reload the app. - if (!onlyIfUpdateIsPending || CodePush.this.isPendingUpdate(null)) { - loadBundle(); - } - } - - @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..761a94c --- /dev/null +++ b/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java @@ -0,0 +1,477 @@ +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) { + // 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); + } + } + +} From 006a868db115cac22d0814af3de1bbf388086770 Mon Sep 17 00:00:00 2001 From: Jonathan Carter Date: Mon, 27 Jun 2016 10:14:52 -0700 Subject: [PATCH 21/26] Version bump --- README.md | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4c624b9..15ec0a8 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,8 @@ 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 diff --git a/package.json b/package.json index 722d1ce..fd1a34a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-code-push", - "version": "1.12.2-beta", + "version": "1.13.0-beta", "description": "React Native plugin for the CodePush service", "main": "CodePush.js", "typings": "typings/react-native-code-push.d.ts", From 416b82be13ba1d27c28a663d121901fef3582a52 Mon Sep 17 00:00:00 2001 From: Geoffrey Goh Date: Mon, 27 Jun 2016 12:41:18 -0700 Subject: [PATCH 22/26] Update CodePush.java --- .../src/main/java/com/microsoft/codepush/react/CodePush.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e11ae7b..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 @@ -143,7 +143,7 @@ public class CodePush implements ReactPackage { } public static String getJSBundleFile() { - return CodePush.getBundleUrl(); + return CodePush.getJSBundleFile(CodePushConstants.DEFAULT_JS_BUNDLE_NAME); } public static String getJSBundleFile(String assetsBundleFileName) { From c51865b7b77ed3ad550a76866504a9be780ed9b3 Mon Sep 17 00:00:00 2001 From: Geoffrey Goh Date: Mon, 27 Jun 2016 12:41:33 -0700 Subject: [PATCH 23/26] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fd1a34a..53e5463 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-code-push", - "version": "1.13.0-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", From 6532c4aee46b922d68dc8e5d7623f2fada15895f Mon Sep 17 00:00:00 2001 From: Geoffrey Goh Date: Mon, 27 Jun 2016 14:01:03 -0700 Subject: [PATCH 24/26] Update generateBundledResourcesHash.js --- scripts/generateBundledResourcesHash.js | 36 ++++++++++++++----------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/scripts/generateBundledResourcesHash.js b/scripts/generateBundledResourcesHash.js index 2eb46c9..c4afc2d 100644 --- a/scripts/generateBundledResourcesHash.js +++ b/scripts/generateBundledResourcesHash.js @@ -51,23 +51,29 @@ if (bundleGeneratedAssetFiles.length) { // Generate hash for each asset file addFileToManifest(resourcesDir, assetFile, manifest, function() { if (manifest.length === bundleGeneratedAssetFiles.length) { - // Generate hash for JS bundle - addFileToManifest(path.dirname(jsBundleFilePath), path.basename(jsBundleFilePath), manifest, function() { - // ...and the JS bundle "meta" - 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"); - - var savedResourcesManifestPath = assetsDir + "/" + CODE_PUSH_HASH_FILE_NAME; - fs.writeFileSync(savedResourcesManifestPath, finalHash); - }); - }); + addJsBundleAndMetaToManifest(); } }); }); +} else { + addJsBundleAndMetaToManifest(); +} + +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); + }); + }); } function addFileToManifest(folder, assetFile, manifest, done) { @@ -98,4 +104,4 @@ function fileExists(file) { catch (e) { return false; } } -fs.unlinkSync(TEMP_FILE_PATH); \ No newline at end of file +fs.unlinkSync(TEMP_FILE_PATH); From 2d337b3bb8f13a3d59ec7e40a9941cdcb3bc0589 Mon Sep 17 00:00:00 2001 From: Geoffrey Goh Date: Mon, 27 Jun 2016 17:47:37 -0700 Subject: [PATCH 25/26] CPLog --- ios/CodePush.xcodeproj/project.pbxproj | 4 ++++ ios/CodePush/CodePush.h | 2 ++ ios/CodePush/CodePush.m | 12 ++++++------ ios/CodePush/CodePushPackage.m | 12 ++++++------ ios/CodePush/CodePushUtils.m | 9 +++++++++ 5 files changed, 27 insertions(+), 12 deletions(-) create mode 100644 ios/CodePush/CodePushUtils.m 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 c703092..0acfd26 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; } 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/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 From 7d18c26b728f82147a3a6beff2768d8397f92153 Mon Sep 17 00:00:00 2001 From: Jonathan Carter Date: Tue, 28 Jun 2016 10:34:53 -0700 Subject: [PATCH 26/26] Adding RNPM version mention --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 15ec0a8..fe8b2f7 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ In order to accommodate as many developer preferences as possible, the CodePush 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.* + *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.* And that's it! Isn't RNPM awesome? :)