mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Migrate to Kotlin - BundleDownloader (#50557)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/50557 This diff migrates the following file to Kotlin - BundleDownloader as part of our ongoing effort of migrating the codebase to Kotlin Changelog: [Internal] [Changed] - BundleDownloader to Kotlin Reviewed By: javache Differential Revision: D72627995 fbshipit-source-id: 9d65473c4d4a0962f78b0f71fc24530415a0e908
This commit is contained in:
committed by
Facebook GitHub Bot
parent
655a86c348
commit
f866a8d800
@@ -1947,18 +1947,28 @@ public final class com/facebook/react/devsupport/BridgeDevSupportManager : com/f
|
||||
public fun loadSplitBundleFromServer (Ljava/lang/String;Lcom/facebook/react/devsupport/interfaces/DevSplitBundleCallback;)V
|
||||
}
|
||||
|
||||
public class com/facebook/react/devsupport/BundleDownloader {
|
||||
public final class com/facebook/react/devsupport/BundleDownloader {
|
||||
public static final field Companion Lcom/facebook/react/devsupport/BundleDownloader$Companion;
|
||||
public fun <init> (Lokhttp3/OkHttpClient;)V
|
||||
public fun downloadBundleFromURL (Lcom/facebook/react/devsupport/interfaces/DevBundleDownloadListener;Ljava/io/File;Ljava/lang/String;Lcom/facebook/react/devsupport/BundleDownloader$BundleInfo;)V
|
||||
public fun downloadBundleFromURL (Lcom/facebook/react/devsupport/interfaces/DevBundleDownloadListener;Ljava/io/File;Ljava/lang/String;Lcom/facebook/react/devsupport/BundleDownloader$BundleInfo;Lokhttp3/Request$Builder;)V
|
||||
public final fun downloadBundleFromURL (Lcom/facebook/react/devsupport/interfaces/DevBundleDownloadListener;Ljava/io/File;Ljava/lang/String;Lcom/facebook/react/devsupport/BundleDownloader$BundleInfo;)V
|
||||
public final fun downloadBundleFromURL (Lcom/facebook/react/devsupport/interfaces/DevBundleDownloadListener;Ljava/io/File;Ljava/lang/String;Lcom/facebook/react/devsupport/BundleDownloader$BundleInfo;Lokhttp3/Request$Builder;)V
|
||||
public static synthetic fun downloadBundleFromURL$default (Lcom/facebook/react/devsupport/BundleDownloader;Lcom/facebook/react/devsupport/interfaces/DevBundleDownloadListener;Ljava/io/File;Ljava/lang/String;Lcom/facebook/react/devsupport/BundleDownloader$BundleInfo;Lokhttp3/Request$Builder;ILjava/lang/Object;)V
|
||||
}
|
||||
|
||||
public class com/facebook/react/devsupport/BundleDownloader$BundleInfo {
|
||||
public final class com/facebook/react/devsupport/BundleDownloader$BundleInfo {
|
||||
public static final field Companion Lcom/facebook/react/devsupport/BundleDownloader$BundleInfo$Companion;
|
||||
public fun <init> ()V
|
||||
public static fun fromJSONString (Ljava/lang/String;)Lcom/facebook/react/devsupport/BundleDownloader$BundleInfo;
|
||||
public fun getFilesChangedCount ()I
|
||||
public fun getUrl ()Ljava/lang/String;
|
||||
public fun toJSONString ()Ljava/lang/String;
|
||||
public static final fun fromJSONString (Ljava/lang/String;)Lcom/facebook/react/devsupport/BundleDownloader$BundleInfo;
|
||||
public final fun getFilesChangedCount ()I
|
||||
public final fun getUrl ()Ljava/lang/String;
|
||||
public final fun toJSONString ()Ljava/lang/String;
|
||||
}
|
||||
|
||||
public final class com/facebook/react/devsupport/BundleDownloader$BundleInfo$Companion {
|
||||
public final fun fromJSONString (Ljava/lang/String;)Lcom/facebook/react/devsupport/BundleDownloader$BundleInfo;
|
||||
}
|
||||
|
||||
public final class com/facebook/react/devsupport/BundleDownloader$Companion {
|
||||
}
|
||||
|
||||
public final class com/facebook/react/devsupport/DefaultDevLoadingViewImplementation : com/facebook/react/devsupport/interfaces/DevLoadingViewManager {
|
||||
|
||||
-332
@@ -1,332 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.react.devsupport;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.infer.annotation.Assertions;
|
||||
import com.facebook.infer.annotation.Nullsafe;
|
||||
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.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.Callback;
|
||||
import okhttp3.Headers;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import okio.Buffer;
|
||||
import okio.BufferedSource;
|
||||
import okio.Okio;
|
||||
import okio.Sink;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
@Nullsafe(Nullsafe.Mode.LOCAL)
|
||||
public class BundleDownloader {
|
||||
private static final String TAG = "BundleDownloader";
|
||||
|
||||
// Should be kept in sync with constants in RCTJavaScriptLoader.h
|
||||
private static final int FILES_CHANGED_COUNT_NOT_BUILT_BY_BUNDLER = -2;
|
||||
|
||||
private final OkHttpClient mClient;
|
||||
|
||||
private @Nullable Call mDownloadBundleFromURLCall;
|
||||
|
||||
public static class BundleInfo {
|
||||
private @Nullable String mUrl;
|
||||
private int mFilesChangedCount;
|
||||
|
||||
public static @Nullable BundleInfo fromJSONString(String jsonStr) {
|
||||
if (jsonStr == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
BundleInfo info = new BundleInfo();
|
||||
|
||||
try {
|
||||
JSONObject obj = new JSONObject(jsonStr);
|
||||
info.mUrl = obj.getString("url");
|
||||
info.mFilesChangedCount = obj.getInt("filesChangedCount");
|
||||
} catch (JSONException e) {
|
||||
FLog.e(TAG, "Invalid bundle info: ", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
public @Nullable String toJSONString() {
|
||||
JSONObject obj = new JSONObject();
|
||||
|
||||
try {
|
||||
obj.put("url", mUrl);
|
||||
obj.put("filesChangedCount", mFilesChangedCount);
|
||||
} catch (JSONException e) {
|
||||
FLog.e(TAG, "Can't serialize bundle info: ", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
return obj.toString();
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return mUrl != null ? mUrl : "unknown";
|
||||
}
|
||||
|
||||
public int getFilesChangedCount() {
|
||||
return mFilesChangedCount;
|
||||
}
|
||||
}
|
||||
|
||||
public BundleDownloader(OkHttpClient client) {
|
||||
mClient = client;
|
||||
}
|
||||
|
||||
public void downloadBundleFromURL(
|
||||
final DevBundleDownloadListener callback,
|
||||
final File outputFile,
|
||||
final String bundleURL,
|
||||
final @Nullable BundleInfo bundleInfo) {
|
||||
downloadBundleFromURL(callback, outputFile, bundleURL, bundleInfo, new Request.Builder());
|
||||
}
|
||||
|
||||
public void downloadBundleFromURL(
|
||||
final DevBundleDownloadListener callback,
|
||||
final File outputFile,
|
||||
final String bundleURL,
|
||||
final @Nullable BundleInfo bundleInfo,
|
||||
Request.Builder requestBuilder) {
|
||||
final Request request =
|
||||
requestBuilder.url(bundleURL).addHeader("Accept", "multipart/mixed").build();
|
||||
mDownloadBundleFromURLCall = Assertions.assertNotNull(mClient.newCall(request));
|
||||
mDownloadBundleFromURLCall.enqueue(
|
||||
new Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
// ignore callback if call was cancelled
|
||||
if (mDownloadBundleFromURLCall == null || mDownloadBundleFromURLCall.isCanceled()) {
|
||||
mDownloadBundleFromURLCall = null;
|
||||
return;
|
||||
}
|
||||
mDownloadBundleFromURLCall = null;
|
||||
|
||||
String url = call.request().url().toString();
|
||||
|
||||
callback.onFailure(
|
||||
DebugServerException.makeGeneric(
|
||||
url, "Could not connect to development server.", "URL: " + url, e));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, final Response response) throws IOException {
|
||||
try (Response r = response) {
|
||||
// ignore callback if call was cancelled
|
||||
if (mDownloadBundleFromURLCall == null || mDownloadBundleFromURLCall.isCanceled()) {
|
||||
mDownloadBundleFromURLCall = null;
|
||||
return;
|
||||
}
|
||||
mDownloadBundleFromURLCall = null;
|
||||
|
||||
final String url = response.request().url().toString();
|
||||
// Make sure the result is a multipart response and parse the boundary.
|
||||
String contentType = response.header("content-type");
|
||||
if (contentType == null) {
|
||||
// fallback to empty string for nullability
|
||||
contentType = "";
|
||||
}
|
||||
Pattern regex = Pattern.compile("multipart/mixed;.*boundary=\"([^\"]+)\"");
|
||||
Matcher match = regex.matcher(contentType);
|
||||
if (!contentType.isEmpty() && match.find()) {
|
||||
String boundary = Assertions.assertNotNull(match.group(1));
|
||||
processMultipartResponse(url, r, boundary, outputFile, bundleInfo, callback);
|
||||
} else {
|
||||
// In case the server doesn't support multipart/mixed responses, fallback to normal
|
||||
// download.
|
||||
try (ResponseBody body = r.body()) {
|
||||
if (body != null) {
|
||||
processBundleResult(
|
||||
url,
|
||||
r.code(),
|
||||
r.headers(),
|
||||
body.source(),
|
||||
outputFile,
|
||||
bundleInfo,
|
||||
callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void processMultipartResponse(
|
||||
final String url,
|
||||
final Response response,
|
||||
String boundary,
|
||||
final File outputFile,
|
||||
@Nullable final BundleInfo bundleInfo,
|
||||
final DevBundleDownloadListener callback)
|
||||
throws IOException {
|
||||
if (response.body() == null) {
|
||||
callback.onFailure(
|
||||
new DebugServerException(
|
||||
"Error while reading multipart response.\n\nResponse body was empty: "
|
||||
+ response.code()
|
||||
+ "\n\n"
|
||||
+ "URL: "
|
||||
+ url.toString()
|
||||
+ "\n\n"));
|
||||
return;
|
||||
}
|
||||
MultipartStreamReader bodyReader =
|
||||
new MultipartStreamReader(response.body().source(), boundary);
|
||||
boolean completed =
|
||||
bodyReader.readAllParts(
|
||||
new MultipartStreamReader.ChunkListener() {
|
||||
@Override
|
||||
public void onChunkComplete(
|
||||
Map<String, String> headers, Buffer body, boolean isLastChunk)
|
||||
throws IOException {
|
||||
// This will get executed for every chunk of the multipart response. The last chunk
|
||||
// (isLastChunk = true) will be the JS bundle, the other ones will be progress
|
||||
// events
|
||||
// encoded as JSON.
|
||||
if (isLastChunk) {
|
||||
// The http status code for each separate chunk is in the X-Http-Status header.
|
||||
int status = response.code();
|
||||
if (headers.containsKey("X-Http-Status")) {
|
||||
status = Integer.parseInt(headers.get("X-Http-Status"));
|
||||
}
|
||||
processBundleResult(
|
||||
url, status, Headers.of(headers), body, outputFile, bundleInfo, callback);
|
||||
} else {
|
||||
if (!headers.containsKey("Content-Type")
|
||||
|| !headers.get("Content-Type").equals("application/json")) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
JSONObject progress = new JSONObject(body.readUtf8());
|
||||
String status =
|
||||
progress.has("status") ? progress.getString("status") : "Bundling";
|
||||
Integer done = null;
|
||||
if (progress.has("done")) {
|
||||
done = progress.getInt("done");
|
||||
}
|
||||
Integer total = null;
|
||||
if (progress.has("total")) {
|
||||
total = progress.getInt("total");
|
||||
}
|
||||
callback.onProgress(status, done, total);
|
||||
} catch (JSONException e) {
|
||||
FLog.e(ReactConstants.TAG, "Error parsing progress JSON. " + e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChunkProgress(Map<String, String> headers, long loaded, long total) {
|
||||
if ("application/javascript".equals(headers.get("Content-Type"))) {
|
||||
callback.onProgress("Downloading", (int) (loaded / 1024), (int) (total / 1024));
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!completed) {
|
||||
callback.onFailure(
|
||||
new DebugServerException(
|
||||
"Error while reading multipart response.\n\nResponse code: "
|
||||
+ response.code()
|
||||
+ "\n\n"
|
||||
+ "URL: "
|
||||
+ url.toString()
|
||||
+ "\n\n"));
|
||||
}
|
||||
}
|
||||
|
||||
private void processBundleResult(
|
||||
String url,
|
||||
int statusCode,
|
||||
Headers headers,
|
||||
BufferedSource body,
|
||||
File outputFile,
|
||||
@Nullable BundleInfo bundleInfo,
|
||||
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();
|
||||
DebugServerException debugServerException = DebugServerException.parse(url, bodyString);
|
||||
if (debugServerException != null) {
|
||||
callback.onFailure(debugServerException);
|
||||
} else {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("The development server returned response error code: ")
|
||||
.append(statusCode)
|
||||
.append("\n\n")
|
||||
.append("URL: ")
|
||||
.append(url)
|
||||
.append("\n\n")
|
||||
.append("Body:\n")
|
||||
.append(bodyString);
|
||||
callback.onFailure(new DebugServerException(sb.toString()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (bundleInfo != null) {
|
||||
populateBundleInfo(url, headers, bundleInfo);
|
||||
}
|
||||
|
||||
File tmpFile = new File(outputFile.getPath() + ".tmp");
|
||||
|
||||
if (storePlainJSInFile(body, tmpFile)) {
|
||||
// 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(outputFile);
|
||||
body.readAll(output);
|
||||
} finally {
|
||||
if (output != null) {
|
||||
output.close();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void populateBundleInfo(String url, Headers headers, BundleInfo bundleInfo) {
|
||||
bundleInfo.mUrl = url;
|
||||
|
||||
String filesChangedCountStr = headers.get("X-Metro-Files-Changed-Count");
|
||||
if (filesChangedCountStr != null) {
|
||||
try {
|
||||
bundleInfo.mFilesChangedCount = Integer.parseInt(filesChangedCountStr);
|
||||
} catch (NumberFormatException e) {
|
||||
bundleInfo.mFilesChangedCount = FILES_CHANGED_COUNT_NOT_BUILT_BY_BUNDLER;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+323
@@ -0,0 +1,323 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
@file:Suppress("DEPRECATION_ERROR") // Conflicting okhttp versions
|
||||
|
||||
package com.facebook.react.devsupport
|
||||
|
||||
import com.facebook.common.logging.FLog
|
||||
import com.facebook.infer.annotation.Assertions
|
||||
import com.facebook.react.common.DebugServerException
|
||||
import com.facebook.react.common.DebugServerException.Companion.makeGeneric
|
||||
import com.facebook.react.common.DebugServerException.Companion.parse
|
||||
import com.facebook.react.common.ReactConstants
|
||||
import com.facebook.react.devsupport.MultipartStreamReader.ChunkListener
|
||||
import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.regex.Pattern
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okio.Buffer
|
||||
import okio.BufferedSource
|
||||
import okio.Okio
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
public class BundleDownloader public constructor(private val client: OkHttpClient) {
|
||||
private var downloadBundleFromURLCall: Call? = null
|
||||
|
||||
public class BundleInfo public constructor() {
|
||||
internal var _url: String? = null
|
||||
public val url: String
|
||||
get() = _url ?: "unknown"
|
||||
|
||||
public var filesChangedCount: Int = 0
|
||||
internal set
|
||||
|
||||
public fun toJSONString(): String? =
|
||||
try {
|
||||
JSONObject()
|
||||
.apply {
|
||||
put("url", _url)
|
||||
put("filesChangedCount", filesChangedCount)
|
||||
}
|
||||
.toString()
|
||||
} catch (e: JSONException) {
|
||||
FLog.e(TAG, "Can't serialize bundle info: ", e)
|
||||
null
|
||||
}
|
||||
|
||||
public companion object {
|
||||
@JvmStatic
|
||||
public fun fromJSONString(jsonStr: String?): BundleInfo? {
|
||||
if (jsonStr == null) {
|
||||
return null
|
||||
}
|
||||
return try {
|
||||
val obj = JSONObject(jsonStr)
|
||||
BundleInfo().apply {
|
||||
_url = obj.getString("url")
|
||||
filesChangedCount = obj.getInt("filesChangedCount")
|
||||
}
|
||||
} catch (e: JSONException) {
|
||||
FLog.e(TAG, "Invalid bundle info: ", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
public fun downloadBundleFromURL(
|
||||
callback: DevBundleDownloadListener,
|
||||
outputFile: File,
|
||||
bundleURL: String?,
|
||||
bundleInfo: BundleInfo?,
|
||||
requestBuilder: Request.Builder = Request.Builder()
|
||||
) {
|
||||
checkNotNull(bundleURL)
|
||||
val request = requestBuilder.url(bundleURL).addHeader("Accept", "multipart/mixed").build()
|
||||
downloadBundleFromURLCall = client.newCall(request)
|
||||
checkNotNull(downloadBundleFromURLCall)
|
||||
.enqueue(
|
||||
object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
// ignore callback if call was cancelled
|
||||
if (downloadBundleFromURLCall == null ||
|
||||
downloadBundleFromURLCall?.isCanceled() == true) {
|
||||
downloadBundleFromURLCall = null
|
||||
return
|
||||
}
|
||||
downloadBundleFromURLCall = null
|
||||
val url = call.request().url().toString()
|
||||
callback.onFailure(
|
||||
makeGeneric(url, "Could not connect to development server.", "URL: $url", e))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
response.use { resp ->
|
||||
// ignore callback if call was cancelled
|
||||
if (downloadBundleFromURLCall == null ||
|
||||
downloadBundleFromURLCall?.isCanceled() == true) {
|
||||
downloadBundleFromURLCall = null
|
||||
return
|
||||
}
|
||||
downloadBundleFromURLCall = null
|
||||
|
||||
val url = resp.request().url().toString()
|
||||
// Make sure the result is a multipart response and parse the boundary.
|
||||
var contentType = resp.header("content-type")
|
||||
if (contentType == null) {
|
||||
// fallback to empty string for nullability
|
||||
contentType = ""
|
||||
}
|
||||
val regex = Pattern.compile("multipart/mixed;.*boundary=\"([^\"]+)\"")
|
||||
val match = regex.matcher(contentType)
|
||||
if (contentType.isNotEmpty() && match.find()) {
|
||||
val boundary = Assertions.assertNotNull(match.group(1))
|
||||
processMultipartResponse(url, resp, boundary, outputFile, bundleInfo, callback)
|
||||
} else {
|
||||
// In case the server doesn't support multipart/mixed responses, fallback to
|
||||
// normal
|
||||
// download.
|
||||
resp.body().use { body ->
|
||||
if (body != null) {
|
||||
processBundleResult(
|
||||
url,
|
||||
resp.code(),
|
||||
resp.headers(),
|
||||
body.source(),
|
||||
outputFile,
|
||||
bundleInfo,
|
||||
callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun processMultipartResponse(
|
||||
url: String,
|
||||
response: Response,
|
||||
boundary: String,
|
||||
outputFile: File,
|
||||
bundleInfo: BundleInfo?,
|
||||
callback: DevBundleDownloadListener
|
||||
) {
|
||||
if (response.body() == null) {
|
||||
callback.onFailure(
|
||||
DebugServerException(
|
||||
("""
|
||||
Error while reading multipart response.
|
||||
|
||||
Response body was empty: ${response.code()}
|
||||
|
||||
URL: $url
|
||||
|
||||
|
||||
"""
|
||||
.trimIndent())))
|
||||
return
|
||||
}
|
||||
val source = checkNotNull(response.body()?.source())
|
||||
val bodyReader = MultipartStreamReader(source, boundary)
|
||||
val completed =
|
||||
bodyReader.readAllParts(
|
||||
object : ChunkListener {
|
||||
@Throws(IOException::class)
|
||||
override fun onChunkComplete(
|
||||
headers: Map<String, String>,
|
||||
body: Buffer,
|
||||
isLastChunk: Boolean
|
||||
) {
|
||||
// This will get executed for every chunk of the multipart response. The last chunk
|
||||
// (isLastChunk = true) will be the JS bundle, the other ones will be progress
|
||||
// events
|
||||
// encoded as JSON.
|
||||
if (isLastChunk) {
|
||||
// The http status code for each separate chunk is in the X-Http-Status header.
|
||||
var status = response.code()
|
||||
if (headers.containsKey("X-Http-Status")) {
|
||||
status = headers.getOrDefault("X-Http-Status", "0").toInt()
|
||||
}
|
||||
processBundleResult(
|
||||
url, status, Headers.of(headers), body, outputFile, bundleInfo, callback)
|
||||
} else {
|
||||
if (!headers.containsKey("Content-Type") ||
|
||||
headers["Content-Type"] != "application/json") {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val progress = JSONObject(body.readUtf8())
|
||||
val status =
|
||||
if (progress.has("status")) progress.getString("status") else "Bundling"
|
||||
var done: Int? = null
|
||||
if (progress.has("done")) {
|
||||
done = progress.getInt("done")
|
||||
}
|
||||
var total: Int? = null
|
||||
if (progress.has("total")) {
|
||||
total = progress.getInt("total")
|
||||
}
|
||||
callback.onProgress(status, done, total)
|
||||
} catch (e: JSONException) {
|
||||
FLog.e(ReactConstants.TAG, "Error parsing progress JSON. $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChunkProgress(
|
||||
headers: Map<String, String>,
|
||||
loaded: Long,
|
||||
total: Long
|
||||
) {
|
||||
if ("application/javascript" == headers["Content-Type"]) {
|
||||
callback.onProgress(
|
||||
"Downloading", (loaded / 1024).toInt(), (total / 1024).toInt())
|
||||
}
|
||||
}
|
||||
})
|
||||
if (!completed) {
|
||||
callback.onFailure(
|
||||
DebugServerException(
|
||||
("""
|
||||
Error while reading multipart response.
|
||||
|
||||
Response code: ${response.code()}
|
||||
|
||||
URL: $url
|
||||
|
||||
|
||||
"""
|
||||
.trimIndent())))
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun processBundleResult(
|
||||
url: String,
|
||||
statusCode: Int,
|
||||
headers: Headers,
|
||||
body: BufferedSource,
|
||||
outputFile: File,
|
||||
bundleInfo: BundleInfo?,
|
||||
callback: DevBundleDownloadListener
|
||||
) {
|
||||
// Check for server errors. If the server error has the expected form, fail with more info.
|
||||
if (statusCode != 200) {
|
||||
val bodyString = body.readUtf8()
|
||||
val debugServerException = parse(url, bodyString)
|
||||
if (debugServerException != null) {
|
||||
callback.onFailure(debugServerException)
|
||||
} else {
|
||||
val sb = StringBuilder()
|
||||
sb.append("The development server returned response error code: ")
|
||||
.append(statusCode)
|
||||
.append("\n\n")
|
||||
.append("URL: ")
|
||||
.append(url)
|
||||
.append("\n\n")
|
||||
.append("Body:\n")
|
||||
.append(bodyString)
|
||||
callback.onFailure(DebugServerException(sb.toString()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (bundleInfo != null) {
|
||||
populateBundleInfo(url, headers, bundleInfo)
|
||||
}
|
||||
|
||||
val tmpFile = File(outputFile.path + ".tmp")
|
||||
|
||||
if (storePlainJSInFile(body, tmpFile)) {
|
||||
// If we have received a new bundle from the server, move it to its final destination.
|
||||
if (!tmpFile.renameTo(outputFile)) {
|
||||
throw IOException("Couldn't rename $tmpFile to $outputFile")
|
||||
}
|
||||
}
|
||||
|
||||
callback.onSuccess()
|
||||
}
|
||||
|
||||
public companion object {
|
||||
private const val TAG = "BundleDownloader"
|
||||
|
||||
// Should be kept in sync with constants in RCTJavaScriptLoader.h
|
||||
private const val FILES_CHANGED_COUNT_NOT_BUILT_BY_BUNDLER = -2
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun storePlainJSInFile(body: BufferedSource, outputFile: File): Boolean {
|
||||
Okio.sink(outputFile).use { it -> body.readAll(it) }
|
||||
return true
|
||||
}
|
||||
|
||||
private fun populateBundleInfo(url: String, headers: Headers, bundleInfo: BundleInfo) {
|
||||
bundleInfo._url = url
|
||||
|
||||
val filesChangedCountStr = headers["X-Metro-Files-Changed-Count"]
|
||||
if (filesChangedCountStr != null) {
|
||||
try {
|
||||
bundleInfo.filesChangedCount = filesChangedCountStr.toInt()
|
||||
} catch (e: NumberFormatException) {
|
||||
bundleInfo.filesChangedCount = FILES_CHANGED_COUNT_NOT_BUILT_BY_BUNDLER
|
||||
FLog.e(TAG, "Can't populate bundle info: ", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user