Skip to content
This repository was archived by the owner on May 20, 2025. It is now read-only.

Commit 5160ee1

Browse files
committed
Merge pull request #211 from Microsoft/generate-binary-resources-hash
Hash binary resources
2 parents c1c6f3f + c895b4c commit 5160ee1

18 files changed

+479
-121
lines changed

CodePush.js

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { AcquisitionManager as Sdk } from "code-push/script/acquisition-sdk";
22
import { Alert } from "./AlertAdapter";
33
import requestFetchAdapter from "./request-fetch-adapter";
44
import semver from "semver";
5+
import { Platform } from "react-native";
56

67
let NativeCodePush = require("react-native").NativeModules.CodePush;
78
const PackageMixins = require("./package-mixins")(NativeCodePush);
@@ -39,13 +40,20 @@ async function checkForUpdate(deploymentKey = null) {
3940
* to send the app version to the server, since we are interested
4041
* in any updates for current app store version, regardless of hash.
4142
*/
42-
const queryPackage = localPackage && localPackage.appVersion && semver.compare(localPackage.appVersion, config.appVersion) === 0
43-
? localPackage
44-
: { appVersion: config.appVersion };
43+
let queryPackage;
44+
if (localPackage && localPackage.appVersion && semver.compare(localPackage.appVersion, config.appVersion) === 0) {
45+
queryPackage = localPackage;
46+
} else {
47+
queryPackage = { appVersion: config.appVersion };
48+
if (Platform.OS === "ios" && config.packageHash) {
49+
queryPackage.packageHash = config.packageHash;
50+
}
51+
}
52+
4553
const update = await sdk.queryUpdateWithCurrentPackage(queryPackage);
4654

4755
/*
48-
* There are three cases where checkForUpdate will resolve to null:
56+
* There are four cases where checkForUpdate will resolve to null:
4957
* ----------------------------------------------------------------
5058
* 1) The server said there isn't an update. This is the most common case.
5159
* 2) The server said there is an update but it requires a newer binary version.
@@ -56,14 +64,19 @@ async function checkForUpdate(deploymentKey = null) {
5664
* the currently running update. This should _never_ happen, unless there is a
5765
* bug in the server, but we're adding this check just to double-check that the
5866
* client app is resilient to a potential issue with the update check.
67+
* 4) The server said there is an update, but the update's hash is the same as that
68+
* of the binary's currently running version. This should only happen in Android -
69+
* unlike iOS, we don't attach the binary's hash to the updateCheck request
70+
* because we want to avoid having to install diff updates against the binary's
71+
* version, which we can't do yet on Android.
5972
*/
60-
if (!update || update.updateAppVersion || localPackage && (update.packageHash === localPackage.packageHash)) {
73+
if (!update || update.updateAppVersion || localPackage && (update.packageHash === localPackage.packageHash) || !localPackage && config.packageHash === update.packageHash) {
6174
if (update && update.updateAppVersion) {
6275
log("An update is available but it is targeting a newer binary version than you are currently running.");
6376
}
6477

6578
return null;
66-
} else {
79+
} else {
6780
const remotePackage = { ...update, ...PackageMixins.remote(sdk.reportStatusDownload) };
6881
remotePackage.failedInstall = await NativeCodePush.isFailedUpdate(remotePackage.packageHash);
6982
remotePackage.deploymentKey = deploymentKey || nativeConfig.deploymentKey;

Examples/CodePushDemoApp/android/app/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import com.android.build.OutputFile
5858
*/
5959

6060
apply from: "react.gradle"
61+
apply from: "../../node_modules/react-native-code-push/android/codepush.gradle"
6162

6263
/**
6364
* Set this to true to create three separate APKs instead of one:

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,16 @@ In order to integrate CodePush into your Android project, perform the following
153153
...
154154
compile project(':react-native-code-push')
155155
}
156-
```
156+
```
157+
3. (Only needed in v1.7.4+ of the plugin) In your `android/app/build.gradle` file, add the `codepush.gradle` file as an additional build task definition underneath `react.gradle`:
157158
159+
```gradle
160+
...
161+
apply from: "react.gradle"
162+
apply from: "../../node_modules/react-native-code-push/android/codepush.gradle"
163+
...
164+
```
165+
158166
### Plugin Configuration (Android - React Native < v0.18.0)
159167
160168
*NOTE: These instructions are specific to apps that are using React Native v0.15.0-v0.17.0. If you are using v0.18.0+, then skip ahead to the next section.*

android/app/src/main/java/com/microsoft/codepush/react/CodePush.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,9 @@ public class CodePush {
6969
private CodePushTelemetryManager codePushTelemetryManager;
7070

7171
// Config properties.
72-
private String deploymentKey;
7372
private String appVersion;
7473
private int buildVersion;
74+
private String deploymentKey;
7575
private final String serverUrl = "https://codepush.azurewebsites.net/";
7676

7777
private Activity mainActivity;
@@ -402,6 +402,13 @@ public void getConfiguration(Promise promise) {
402402
configMap.putString("clientUniqueId",
403403
Settings.Secure.getString(mainActivity.getContentResolver(),
404404
android.provider.Settings.Secure.ANDROID_ID));
405+
String binaryHash = CodePushUpdateUtils.getHashForBinaryContents(mainActivity, isDebugMode);
406+
if (binaryHash != null) {
407+
// binaryHash will be null if the React Native assets were not bundled into the APK
408+
// (e.g. in Debug builds)
409+
configMap.putString(PACKAGE_HASH_KEY, binaryHash);
410+
}
411+
405412
promise.resolve(configMap);
406413
}
407414

android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateUtils.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.microsoft.codepush.react;
22

3+
import android.app.Activity;
34
import android.util.Base64;
45

56
import com.facebook.react.bridge.ReadableArray;
@@ -21,6 +22,8 @@
2122

2223
public class CodePushUpdateUtils {
2324

25+
private static final String CODE_PUSH_HASH_FILE_NAME = "CodePushHash.json";
26+
2427
private static void addContentsOfFolderToManifest(String folderPath, String pathPrefix, ArrayList<String> manifest) {
2528
File folder = new File(folderPath);
2629
File[] folderFiles = folder.listFiles();
@@ -102,6 +105,20 @@ public static String findJSBundleInUpdateContents(String folderPath) {
102105
return null;
103106
}
104107

108+
public static String getHashForBinaryContents(Activity mainActivity, boolean isDebugMode) {
109+
try {
110+
return CodePushUtils.getStringFromInputStream(mainActivity.getAssets().open(CODE_PUSH_HASH_FILE_NAME));
111+
} catch (IOException e) {
112+
if (!isDebugMode) {
113+
// Only print this message in "Release" mode. In "Debug", we may not have the
114+
// hash if the build skips bundling the files.
115+
CodePushUtils.log("Unable to get the hash of the binary's bundled resources - \"codepush.gradle\" may have not been added to the build definition.");
116+
}
117+
118+
return null;
119+
}
120+
}
121+
105122
public static void verifyHashForDiffUpdate(String folderPath, String expectedHash) {
106123
ArrayList<String> updateContentsManifest = new ArrayList<String>();
107124
addContentsOfFolderToManifest(folderPath, "", updateContentsManifest);

android/app/src/main/java/com/microsoft/codepush/react/CodePushUtils.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.io.FileInputStream;
2222
import java.io.FileOutputStream;
2323
import java.io.IOException;
24+
import java.io.InputStream;
2425
import java.io.InputStreamReader;
2526
import java.io.PrintWriter;
2627
import java.util.Iterator;
@@ -183,6 +184,25 @@ public static JSONObject convertReadableToJsonObject(ReadableMap map) {
183184
return jsonObj;
184185
}
185186

187+
public static String getStringFromInputStream(InputStream inputStream) throws IOException {
188+
BufferedReader bufferedReader = null;
189+
try {
190+
StringBuilder buffer = new StringBuilder();
191+
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
192+
193+
String line;
194+
while ((line = bufferedReader.readLine()) != null) {
195+
buffer.append(line);
196+
buffer.append("\n");
197+
}
198+
199+
return buffer.toString().trim();
200+
} finally {
201+
if (bufferedReader != null) bufferedReader.close();
202+
if (inputStream != null) inputStream.close();
203+
}
204+
}
205+
186206
public static WritableMap getWritableMapFromFile(String filePath) throws IOException {
187207

188208
String content = FileUtils.readFileToString(filePath);

android/codepush.gradle

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Adapted from https://raw.githubusercontent.com/facebook/react-native/master/local-cli/generator-android/templates/src/app/react.gradle
2+
3+
def config = project.hasProperty("react") ? project.react : [];
4+
def bundleAssetName = config.bundleAssetName ?: "index.android.bundle"
5+
6+
def elvisFile(thing) {
7+
return thing ? file(thing) : null;
8+
}
9+
10+
void runBefore(String dependentTaskName, Task task) {
11+
Task dependentTask = tasks.findByPath(dependentTaskName);
12+
if (dependentTask != null) {
13+
dependentTask.dependsOn task
14+
}
15+
}
16+
17+
gradle.projectsEvaluated {
18+
def buildTypes = android.buildTypes.collect { type -> type.name }
19+
def productFlavors = android.productFlavors.collect { flavor -> flavor.name }
20+
if (!productFlavors) productFlavors.add('')
21+
22+
productFlavors.each { productFlavorName ->
23+
buildTypes.each { buildTypeName ->
24+
def targetName = "${productFlavorName.capitalize()}${buildTypeName.capitalize()}"
25+
def targetPath = productFlavorName ?
26+
"${productFlavorName}/${buildTypeName}" :
27+
"${buildTypeName}"
28+
29+
def jsBundleDirConfigName = "jsBundleDir${targetName}"
30+
def assetsDir = "$buildDir/intermediates/assets/${targetPath}"
31+
def jsBundleDir = elvisFile(config."$jsBundleDirConfigName") ?:
32+
file(assetsDir)
33+
34+
def resourcesDirConfigName = "jsBundleDir${targetName}"
35+
def resourcesDir = elvisFile(config."${resourcesDirConfigName}") ?:
36+
file("$buildDir/intermediates/res/merged/${targetPath}")
37+
def jsBundleFile = file("$jsBundleDir/$bundleAssetName")
38+
39+
// Make this task run right before the bundle task
40+
def recordFilesBeforeBundleCommand = tasks.create(
41+
name: "recordFilesBeforeBundleCommand${targetName}",
42+
type: Exec) {
43+
commandLine "node", "../../node_modules/react-native-code-push/scripts/recordFilesBeforeBundleCommand.js", resourcesDir
44+
}
45+
46+
recordFilesBeforeBundleCommand.dependsOn("merge${targetName}Resources")
47+
recordFilesBeforeBundleCommand.dependsOn("merge${targetName}Assets")
48+
runBefore("bundle${targetName}JsAndAssets", recordFilesBeforeBundleCommand)
49+
50+
// Make this task run right after the bundle task
51+
def generateBundledResourcesHash = tasks.create(
52+
name: "generateBundledResourcesHash${targetName}",
53+
type: Exec) {
54+
commandLine "node", "../../node_modules/react-native-code-push/scripts/generateBundledResourcesHash.js", resourcesDir, "$jsBundleDir/$bundleAssetName", assetsDir
55+
}
56+
57+
generateBundledResourcesHash.dependsOn("bundle${targetName}JsAndAssets")
58+
runBefore("processArmeabi-v7a${targetName}Resources", generateBundledResourcesHash)
59+
runBefore("processX86${targetName}Resources", generateBundledResourcesHash)
60+
runBefore("processUniversal${targetName}Resources", generateBundledResourcesHash)
61+
runBefore("process${targetName}Resources", generateBundledResourcesHash)
62+
runBefore("process${targetName}Manifest", generateBundledResourcesHash)
63+
}
64+
}
65+
}

ios/CodePush/CodePush.h

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
@interface CodePush : NSObject <RCTBridgeModule>
44

5+
+ (NSURL *)binaryBundleURL;
56
/*
67
* This method is used to retrieve the URL for the most recent
78
* version of the JavaScript bundle. This could be either the
@@ -68,10 +69,12 @@ failCallback:(void (^)(NSError *err))failCallback;
6869

6970
@interface CodePushPackage : NSObject
7071

71-
+ (void)installPackage:(NSDictionary *)updatePackage
72-
removePendingUpdate:(BOOL)removePendingUpdate
73-
error:(NSError **)error;
72+
+ (void)downloadPackage:(NSDictionary *)updatePackage
73+
progressCallback:(void (^)(long long, long long))progressCallback
74+
doneCallback:(void (^)())doneCallback
75+
failCallback:(void (^)(NSError *err))failCallback;
7476

77+
+ (NSString *)getBinaryAssetsPath;
7578
+ (NSDictionary *)getCurrentPackage:(NSError **)error;
7679
+ (NSString *)getCurrentPackageFolderPath:(NSError **)error;
7780
+ (NSString *)getCurrentPackageBundlePath:(NSError **)error;
@@ -81,18 +84,17 @@ failCallback:(void (^)(NSError *err))failCallback;
8184
error:(NSError **)error;
8285

8386
+ (NSString *)getPackageFolderPath:(NSString *)packageHash;
84-
+ (BOOL)isCodePushError:(NSError *)err;
8587

86-
+ (void)downloadPackage:(NSDictionary *)updatePackage
87-
progressCallback:(void (^)(long long, long long))progressCallback
88-
doneCallback:(void (^)())doneCallback
89-
failCallback:(void (^)(NSError *err))failCallback;
88+
+ (void)installPackage:(NSDictionary *)updatePackage
89+
removePendingUpdate:(BOOL)removePendingUpdate
90+
error:(NSError **)error;
9091

92+
+ (BOOL)isCodePushError:(NSError *)err;
9193
+ (void)rollbackPackage;
9294

9395
// The below methods are only used during tests.
94-
+ (void)downloadAndReplaceCurrentBundle:(NSString *)remoteBundleUrl;
9596
+ (void)clearUpdates;
97+
+ (void)downloadAndReplaceCurrentBundle:(NSString *)remoteBundleUrl;
9698

9799
@end
98100

@@ -109,8 +111,17 @@ failCallback:(void (^)(NSError *err))failCallback;
109111
+ (void)copyEntriesInFolder:(NSString *)sourceFolder
110112
destFolder:(NSString *)destFolder
111113
error:(NSError **)error;
114+
112115
+ (NSString *)findMainBundleInFolder:(NSString *)folderPath
113116
error:(NSError **)error;
117+
118+
+ (NSString *)assetsFolderName;
119+
+ (NSString *)getHashForBinaryContents:(NSURL *)binaryBundleUrl
120+
error:(NSError **)error;
121+
122+
+ (NSString *)manifestFolderPrefix;
123+
+ (NSString *)modifiedDateStringOfFileAtURL:(NSURL *)fileURL;
124+
114125
+ (BOOL)verifyHashForDiffUpdate:(NSString *)finalUpdateFolder
115126
expectedHash:(NSString *)expectedHash
116127
error:(NSError **)error;

0 commit comments

Comments
 (0)