Permalink
Browse files

Add end to end Delta support to Android devices

Reviewed By: davidaurelio

Differential Revision: D6338677

fbshipit-source-id: 8fa8f618bf8d6cb2291ce4405093cad23bd47fc3
  • Loading branch information...
rafeca authored and facebook-github-bot committed Nov 17, 2017
1 parent 0ac5a52 commit 231c7a03043b9fb3c4bf81251ad099bab0ba05c2
@@ -33,10 +33,12 @@ const HMRClient = {
? `${host}:${port}`
: host;
bundleEntry = bundleEntry.replace(/\.(bundle|delta)/, '.js');
// Build the websocket url
const wsUrl = `ws://${wsHostPort}/hot?` +
`platform=${platform}&` +
`bundleEntry=${bundleEntry.replace('.bundle', '.js')}`;
`bundleEntry=${bundleEntry}`;
const activeWS = new WebSocket(wsUrl);
activeWS.onerror = (e) => {
@@ -9,24 +9,23 @@
package com.facebook.react.devsupport;
import android.util.JsonReader;
import android.util.JsonToken;
import android.util.Log;
import javax.annotation.Nullable;
import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.common.DebugServerException;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener;
import com.facebook.react.common.DebugServerException;
import org.json.JSONException;
import org.json.JSONObject;
import javax.annotation.Nullable;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
@@ -36,6 +35,8 @@
import okio.BufferedSource;
import okio.Okio;
import okio.Sink;
import org.json.JSONException;
import org.json.JSONObject;
public class BundleDownloader {
private static final String TAG = "BundleDownloader";
@@ -45,6 +46,11 @@
private final OkHttpClient mClient;
private final LinkedHashMap<Number, byte[]> mPreModules = new LinkedHashMap<>();
private final LinkedHashMap<Number, byte[]> mDeltaModules = new LinkedHashMap<>();
private final LinkedHashMap<Number, byte[]> mPostModules = new LinkedHashMap<>();
private @Nullable String mDeltaId;
private @Nullable Call mDownloadBundleFromURLCall;
public static class BundleInfo {
@@ -102,13 +108,22 @@ public void downloadBundleFromURL(
final File outputFile,
final String bundleURL,
final @Nullable BundleInfo bundleInfo) {
final Request request = new Request.Builder()
.url(bundleURL)
// FIXME: there is a bug that makes MultipartStreamReader to never find the end of the
// multipart message. This temporarily disables the multipart mode to work around it, but
// it means there is no progress bar displayed in the React Native overlay anymore.
//.addHeader("Accept", "multipart/mixed")
.build();
String finalUrl = bundleURL;
if (isDeltaUrl(bundleURL) && mDeltaId != null) {
finalUrl += "&deltaBundleId=" + mDeltaId;
}
final Request request =
new Request.Builder()
.url(finalUrl)
// FIXME: there is a bug that makes MultipartStreamReader to never find the end of the
// multipart message. This temporarily disables the multipart mode to work around it,
// but
// it means there is no progress bar displayed in the React Native overlay anymore.
// .addHeader("Accept", "multipart/mixed")
.build();
mDownloadBundleFromURLCall = Assertions.assertNotNull(mClient.newCall(request));
mDownloadBundleFromURLCall.enqueue(new Callback() {
@Override
@@ -161,6 +176,7 @@ public void execute(Map<String, String> headers, Buffer body, boolean finished)
if (!headers.containsKey("Content-Type") || !headers.get("Content-Type").equals("application/json")) {
return;
}
try {
JSONObject progress = new JSONObject(body.readUtf8());
String status = null;
@@ -202,14 +218,15 @@ public void cancelDownloadBundleFromURL() {
}
}
private static void processBundleResult(
private void processBundleResult(
String url,
int statusCode,
okhttp3.Headers headers,
BufferedSource body,
File outputFile,
BundleInfo bundleInfo,
DevBundleDownloadListener callback) throws IOException {
DevBundleDownloadListener callback)
throws IOException {
// Check for server errors. If the server error has the expected form, fail with more info.
if (statusCode != 200) {
String bodyString = body.readUtf8();
@@ -232,21 +249,135 @@ private static void processBundleResult(
}
File tmpFile = new File(outputFile.getPath() + ".tmp");
boolean bundleUpdated;
if (isDeltaUrl(url)) {
// If the bundle URL has the delta extension, we need to use the delta patching logic.
bundleUpdated = storeDeltaInFile(body, tmpFile);
} else {
resetDeltaCache();
bundleUpdated = storePlainJSInFile(body, tmpFile);
}
if (bundleUpdated) {
// If we have received a new bundle from the server, move it to its final destination.
if (!tmpFile.renameTo(outputFile)) {
throw new IOException("Couldn't rename " + tmpFile + " to " + outputFile);
}
}
callback.onSuccess();
}
private static boolean storePlainJSInFile(BufferedSource body, File outputFile)
throws IOException {
Sink output = null;
try {
output = Okio.sink(tmpFile);
output = Okio.sink(outputFile);
body.readAll(output);
} finally {
if (output != null) {
output.close();
}
}
if (tmpFile.renameTo(outputFile)) {
callback.onSuccess();
} else {
throw new IOException("Couldn't rename " + tmpFile + " to " + outputFile);
return true;
}
private boolean storeDeltaInFile(BufferedSource body, File outputFile) throws IOException {
JsonReader jsonReader = new JsonReader(new InputStreamReader(body.inputStream()));
jsonReader.beginObject();
int numChangedModules = 0;
while (jsonReader.hasNext()) {
String name = jsonReader.nextName();
if (name.equals("id")) {
mDeltaId = jsonReader.nextString();
} else if (name.equals("pre")) {
numChangedModules += patchDelta(jsonReader, mPreModules);
} else if (name.equals("post")) {
numChangedModules += patchDelta(jsonReader, mPostModules);
} else if (name.equals("delta")) {
numChangedModules += patchDelta(jsonReader, mDeltaModules);
} else {
jsonReader.skipValue();
}
}
jsonReader.endObject();
jsonReader.close();
if (numChangedModules == 0) {
// If we receive an empty delta, we don't need to save the file again (it'll have the
// same content).
return false;
}
FileOutputStream fileOutputStream = new FileOutputStream(outputFile);
try {
for (byte[] code : mPreModules.values()) {
fileOutputStream.write(code);
fileOutputStream.write('\n');
}
for (byte[] code : mDeltaModules.values()) {
fileOutputStream.write(code);
fileOutputStream.write('\n');
}
for (byte[] code : mPostModules.values()) {
fileOutputStream.write(code);
fileOutputStream.write('\n');
}
} finally {
fileOutputStream.flush();
fileOutputStream.close();
}
return true;
}
private static int patchDelta(JsonReader jsonReader, LinkedHashMap<Number, byte[]> map)
throws IOException {
jsonReader.beginArray();
int numModules = 0;
while (jsonReader.hasNext()) {
jsonReader.beginArray();
int moduleId = jsonReader.nextInt();
if (jsonReader.peek() == JsonToken.NULL) {
jsonReader.skipValue();
map.remove(moduleId);
} else {
map.put(moduleId, jsonReader.nextString().getBytes());
}
jsonReader.endArray();
numModules++;
}
jsonReader.endArray();
return numModules;
}
private void resetDeltaCache() {
mDeltaId = null;
mDeltaModules.clear();
mPreModules.clear();
mPostModules.clear();
}
private static boolean isDeltaUrl(String bundleUrl) {
return bundleUrl.indexOf(".delta?") != -1;
}
private static void populateBundleInfo(String url, okhttp3.Headers headers, BundleInfo bundleInfo) {
@@ -9,12 +9,10 @@
package com.facebook.react.devsupport;
import javax.annotation.Nullable;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.modules.debug.interfaces.DeveloperSettings;
import com.facebook.react.packagerconnection.PackagerConnectionSettings;
@@ -32,6 +30,7 @@
private static final String PREFS_FPS_DEBUG_KEY = "fps_debug";
private static final String PREFS_JS_DEV_MODE_DEBUG_KEY = "js_dev_mode_debug";
private static final String PREFS_JS_MINIFY_DEBUG_KEY = "js_minify_debug";
private static final String PREFS_JS_BUNDLE_DELTAS_KEY = "js_bundle_deltas";
private static final String PREFS_ANIMATIONS_DEBUG_KEY = "animations_debug";
private static final String PREFS_RELOAD_ON_JS_CHANGE_KEY = "reload_on_js_change";
private static final String PREFS_INSPECTOR_DEBUG_KEY = "inspector_debug";
@@ -81,10 +80,11 @@ public boolean isJSMinifyEnabled() {
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (mListener != null) {
if (PREFS_FPS_DEBUG_KEY.equals(key) ||
PREFS_RELOAD_ON_JS_CHANGE_KEY.equals(key) ||
PREFS_JS_DEV_MODE_DEBUG_KEY.equals(key) ||
PREFS_JS_MINIFY_DEBUG_KEY.equals(key)) {
if (PREFS_FPS_DEBUG_KEY.equals(key)
|| PREFS_RELOAD_ON_JS_CHANGE_KEY.equals(key)
|| PREFS_JS_DEV_MODE_DEBUG_KEY.equals(key)
|| PREFS_JS_BUNDLE_DELTAS_KEY.equals(key)
|| PREFS_JS_MINIFY_DEBUG_KEY.equals(key)) {
mListener.onInternalSettingsChanged();
}
}
@@ -114,6 +114,16 @@ public void setElementInspectorEnabled(boolean enabled) {
mPreferences.edit().putBoolean(PREFS_INSPECTOR_DEBUG_KEY, enabled).apply();
}
@SuppressLint("SharedPreferencesUse")
public boolean isBundleDeltasEnabled() {
return mPreferences.getBoolean(PREFS_JS_BUNDLE_DELTAS_KEY, false);
}
@SuppressLint("SharedPreferencesUse")
public void setBundleDeltasEnabled(boolean enabled) {
mPreferences.edit().putBoolean(PREFS_JS_BUNDLE_DELTAS_KEY, enabled).apply();
}
@Override
public boolean isRemoteJSDebugEnabled() {
return mPreferences.getBoolean(PREFS_REMOTE_JS_DEBUG_KEY, false);
Oops, something went wrong.

0 comments on commit 231c7a0

Please sign in to comment.