mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-05-01 14:25:08 +08:00
Show bundle loading progress on Android
Summary: This implements a loading banner like on iOS that shows the progress of the packager.  **Test plan** - Tested that it displays similar messages as it does on iOS and also that is show the right message when waiting for the remote debugger. - Tested that errors are still shown properly. - Tested that it works with packagers that don't support multipart response (add && false in https://github.com/facebook/react-native/blob/master/packager/src/Server/MultipartResponse.js#L81). - Run new unit tests. - Tested that backgrounding / foregrounding the app hides / show the banner properly. Closes https://github.com/facebook/react-native/pull/12674 Differential Revision: D4673638 Pulled By: mkonicek fbshipit-source-id: b2a1163de3d0792cf481d7111231a065f80a9594
This commit is contained in:
committed by
Facebook Github Bot
parent
e5ebdd8458
commit
231bf7c68b
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
package com.facebook.react.devsupport;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.react.R;
|
||||
import com.facebook.react.bridge.UiThreadUtil;
|
||||
import com.facebook.react.common.ReactConstants;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Controller to display loading messages on top of the screen. All methods are thread safe.
|
||||
*/
|
||||
public class DevLoadingViewController {
|
||||
private static final int COLOR_DARK_GREEN = Color.parseColor("#035900");
|
||||
|
||||
private static boolean sEnabled = true;
|
||||
private final Context mContext;
|
||||
private final WindowManager mWindowManager;
|
||||
private TextView mDevLoadingView;
|
||||
private boolean mIsVisible = false;
|
||||
|
||||
public static void setDevLoadingEnabled(boolean enabled) {
|
||||
sEnabled = enabled;
|
||||
}
|
||||
|
||||
public DevLoadingViewController(Context context) {
|
||||
mContext = context;
|
||||
mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
|
||||
LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
mDevLoadingView = (TextView) inflater.inflate(R.layout.dev_loading_view, null);
|
||||
}
|
||||
|
||||
public void showMessage(final String message, final int color, final int backgroundColor) {
|
||||
if (!sEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
UiThreadUtil.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mDevLoadingView.setBackgroundColor(backgroundColor);
|
||||
mDevLoadingView.setText(message);
|
||||
mDevLoadingView.setTextColor(color);
|
||||
|
||||
setVisible(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void showForUrl(String url) {
|
||||
URL parsedURL;
|
||||
try {
|
||||
parsedURL = new URL(url);
|
||||
} catch (MalformedURLException e) {
|
||||
FLog.e(ReactConstants.TAG, "Bundle url format is invalid. \n\n" + e.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
showMessage(
|
||||
mContext.getString(R.string.catalyst_loading_from_url, parsedURL.getHost() + ":" + parsedURL.getPort()),
|
||||
Color.WHITE,
|
||||
COLOR_DARK_GREEN);
|
||||
}
|
||||
|
||||
public void showForRemoteJSEnabled() {
|
||||
showMessage(mContext.getString(R.string.catalyst_remotedbg_message), Color.WHITE, COLOR_DARK_GREEN);
|
||||
}
|
||||
|
||||
public void updateProgress(final @Nullable String status, final @Nullable Integer done, final @Nullable Integer total) {
|
||||
if (!sEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
UiThreadUtil.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
StringBuilder message = new StringBuilder();
|
||||
message.append(status != null ? status : "Loading");
|
||||
if (done != null && total != null && total > 0) {
|
||||
message.append(String.format(Locale.getDefault(), " %.1f%% (%d/%d)", (float) done / total * 100, done, total));
|
||||
}
|
||||
message.append("\u2026"); // `...` character
|
||||
|
||||
mDevLoadingView.setText(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void show() {
|
||||
if (!sEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
UiThreadUtil.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
setVisible(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
if (!sEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
UiThreadUtil.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
setVisible(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setVisible(boolean visible) {
|
||||
if (visible && !mIsVisible) {
|
||||
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
|
||||
WindowManager.LayoutParams.MATCH_PARENT,
|
||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||
WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
PixelFormat.TRANSLUCENT);
|
||||
params.gravity = Gravity.TOP;
|
||||
mWindowManager.addView(mDevLoadingView, params);
|
||||
} else if (!visible && mIsVisible) {
|
||||
mWindowManager.removeView(mDevLoadingView);
|
||||
}
|
||||
mIsVisible = visible;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@ import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
@@ -32,6 +34,9 @@ import com.facebook.react.devsupport.interfaces.PackagerStatusCallback;
|
||||
import com.facebook.react.modules.systeminfo.AndroidInfoHelpers;
|
||||
import com.facebook.react.packagerconnection.JSPackagerClient;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import okhttp3.Call;
|
||||
import okhttp3.Callback;
|
||||
import okhttp3.ConnectionPool;
|
||||
@@ -39,6 +44,8 @@ import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import okio.Buffer;
|
||||
import okio.BufferedSource;
|
||||
import okio.Okio;
|
||||
import okio.Sink;
|
||||
|
||||
@@ -78,6 +85,7 @@ public class DevServerHelper {
|
||||
|
||||
public interface BundleDownloadCallback {
|
||||
void onSuccess();
|
||||
void onProgress(@Nullable String status, @Nullable Integer done, @Nullable Integer total);
|
||||
void onFailure(Exception cause);
|
||||
}
|
||||
|
||||
@@ -297,6 +305,7 @@ public class DevServerHelper {
|
||||
final String bundleURL) {
|
||||
final Request request = new Request.Builder()
|
||||
.url(bundleURL)
|
||||
.addHeader("Accept", "multipart/mixed")
|
||||
.build();
|
||||
mDownloadBundleFromURLCall = Assertions.assertNotNull(mClient.newCall(request));
|
||||
mDownloadBundleFromURLCall.enqueue(new Callback() {
|
||||
@@ -316,7 +325,7 @@ public class DevServerHelper {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, Response response) throws IOException {
|
||||
public void onResponse(Call call, final Response response) throws IOException {
|
||||
// ignore callback if call was cancelled
|
||||
if (mDownloadBundleFromURLCall == null || mDownloadBundleFromURLCall.isCanceled()) {
|
||||
mDownloadBundleFromURLCall = null;
|
||||
@@ -324,37 +333,101 @@ public class DevServerHelper {
|
||||
}
|
||||
mDownloadBundleFromURLCall = null;
|
||||
|
||||
// Check for server errors. If the server error has the expected form, fail with more info.
|
||||
if (!response.isSuccessful()) {
|
||||
String body = response.body().string();
|
||||
DebugServerException debugServerException = DebugServerException.parse(body);
|
||||
if (debugServerException != null) {
|
||||
callback.onFailure(debugServerException);
|
||||
} else {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("The development server returned response error code: ").append(response.code()).append("\n\n")
|
||||
.append("URL: ").append(call.request().url().toString()).append("\n\n")
|
||||
.append("Body:\n")
|
||||
.append(body);
|
||||
callback.onFailure(new DebugServerException(sb.toString()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
final String url = response.request().url().toString();
|
||||
|
||||
Sink output = null;
|
||||
try {
|
||||
output = Okio.sink(outputFile);
|
||||
Okio.buffer(response.body().source()).readAll(output);
|
||||
callback.onSuccess();
|
||||
} finally {
|
||||
if (output != null) {
|
||||
output.close();
|
||||
// Make sure the result is a multipart response and parse the boundary.
|
||||
String contentType = response.header("content-type");
|
||||
Pattern regex = Pattern.compile("multipart/mixed;.*boundary=\"([^\"]+)\"");
|
||||
Matcher match = regex.matcher(contentType);
|
||||
if (match.find()) {
|
||||
String boundary = match.group(1);
|
||||
MultipartStreamReader bodyReader = new MultipartStreamReader(response.body().source(), boundary);
|
||||
boolean completed = bodyReader.readAllParts(new MultipartStreamReader.ChunkCallback() {
|
||||
@Override
|
||||
public void execute(Map<String, String> headers, Buffer body, boolean finished) throws IOException {
|
||||
// This will get executed for every chunk of the multipart response. The last chunk
|
||||
// (finished = true) will be the JS bundle, the other ones will be progress events
|
||||
// encoded as JSON.
|
||||
if (finished) {
|
||||
// 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, body, outputFile, callback);
|
||||
} else {
|
||||
if (!headers.containsKey("Content-Type") || !headers.get("Content-Type").equals("application/json")) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
JSONObject progress = new JSONObject(body.readUtf8());
|
||||
String status = null;
|
||||
if (progress.has("status")) {
|
||||
status = progress.getString("status");
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!completed) {
|
||||
callback.onFailure(new DebugServerException(
|
||||
"Error while reading multipart response.\n\nResponse code: " + response.code() + "\n\n" +
|
||||
"URL: " + call.request().url().toString() + "\n\n"));
|
||||
}
|
||||
} else {
|
||||
// In case the server doesn't support multipart/mixed responses, fallback to normal download.
|
||||
processBundleResult(url, response.code(), Okio.buffer(response.body().source()), outputFile, callback);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void processBundleResult(
|
||||
String url,
|
||||
int statusCode,
|
||||
BufferedSource body,
|
||||
File outputFile,
|
||||
BundleDownloadCallback 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(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;
|
||||
}
|
||||
|
||||
Sink output = null;
|
||||
try {
|
||||
output = Okio.sink(outputFile);
|
||||
body.readAll(output);
|
||||
callback.onSuccess();
|
||||
} finally {
|
||||
if (output != null) {
|
||||
output.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void cancelDownloadBundleFromURL() {
|
||||
if (mDownloadBundleFromURLCall != null) {
|
||||
mDownloadBundleFromURLCall.cancel();
|
||||
|
||||
@@ -9,19 +9,6 @@
|
||||
|
||||
package com.facebook.react.devsupport;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.BroadcastReceiver;
|
||||
@@ -58,6 +45,19 @@ import com.facebook.react.devsupport.interfaces.StackFrame;
|
||||
import com.facebook.react.modules.debug.interfaces.DeveloperSettings;
|
||||
import com.facebook.react.packagerconnection.JSPackagerClient;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
@@ -115,10 +115,12 @@ public class DevSupportManagerImpl implements
|
||||
private final @Nullable String mJSAppBundleName;
|
||||
private final File mJSBundleTempFile;
|
||||
private final DefaultNativeModuleCallExceptionHandler mDefaultNativeModuleCallExceptionHandler;
|
||||
private final DevLoadingViewController mDevLoadingViewController;
|
||||
|
||||
private @Nullable RedBoxDialog mRedBoxDialog;
|
||||
private @Nullable AlertDialog mDevOptionsDialog;
|
||||
private @Nullable DebugOverlayController mDebugOverlayController;
|
||||
private boolean mDevLoadingViewVisible = false;
|
||||
private @Nullable ReactContext mCurrentContext;
|
||||
private DevInternalSettings mDevSettings;
|
||||
private boolean mIsReceiverRegistered = false;
|
||||
@@ -226,6 +228,7 @@ public class DevSupportManagerImpl implements
|
||||
setDevSupportEnabled(enableOnCreate);
|
||||
|
||||
mRedBoxHandler = redBoxHandler;
|
||||
mDevLoadingViewController = new DevLoadingViewController(applicationContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -641,7 +644,9 @@ public class DevSupportManagerImpl implements
|
||||
}
|
||||
|
||||
if (mDevSettings.isRemoteJSDebugEnabled()) {
|
||||
reloadJSInProxyMode(showProgressDialog());
|
||||
mDevLoadingViewController.showForRemoteJSEnabled();
|
||||
mDevLoadingViewVisible = true;
|
||||
reloadJSInProxyMode();
|
||||
} else {
|
||||
String bundleURL =
|
||||
mDevServerHelper.getDevServerBundleURL(Assertions.assertNotNull(mJSAppBundleName));
|
||||
@@ -748,7 +753,7 @@ public class DevSupportManagerImpl implements
|
||||
mLastErrorType = errorType;
|
||||
}
|
||||
|
||||
private void reloadJSInProxyMode(final AlertDialog progressDialog) {
|
||||
private void reloadJSInProxyMode() {
|
||||
// When using js proxy, there is no need to fetch JS bundle as proxy executor will do that
|
||||
// anyway
|
||||
mDevServerHelper.launchJSDevtools();
|
||||
@@ -760,7 +765,7 @@ public class DevSupportManagerImpl implements
|
||||
SimpleSettableFuture<Boolean> future = new SimpleSettableFuture<>();
|
||||
executor.connect(
|
||||
mDevServerHelper.getWebsocketProxyURL(),
|
||||
getExecutorConnectCallback(progressDialog, future));
|
||||
getExecutorConnectCallback(future));
|
||||
// TODO(t9349129) Don't use timeout
|
||||
try {
|
||||
future.get(90, TimeUnit.SECONDS);
|
||||
@@ -776,18 +781,19 @@ public class DevSupportManagerImpl implements
|
||||
}
|
||||
|
||||
private WebsocketJavaScriptExecutor.JSExecutorConnectCallback getExecutorConnectCallback(
|
||||
final AlertDialog progressDialog,
|
||||
final SimpleSettableFuture<Boolean> future) {
|
||||
return new WebsocketJavaScriptExecutor.JSExecutorConnectCallback() {
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
future.set(true);
|
||||
progressDialog.dismiss();
|
||||
mDevLoadingViewController.hide();
|
||||
mDevLoadingViewVisible = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(final Throwable cause) {
|
||||
progressDialog.dismiss();
|
||||
mDevLoadingViewController.hide();
|
||||
mDevLoadingViewVisible = false;
|
||||
FLog.e(ReactConstants.TAG, "Unable to connect to remote debugger", cause);
|
||||
future.setException(
|
||||
new IOException(
|
||||
@@ -796,27 +802,16 @@ public class DevSupportManagerImpl implements
|
||||
};
|
||||
}
|
||||
|
||||
private AlertDialog showProgressDialog() {
|
||||
AlertDialog dialog = new AlertDialog.Builder(mApplicationContext)
|
||||
.setTitle(R.string.catalyst_jsload_title)
|
||||
.setMessage(mApplicationContext.getString(
|
||||
mDevSettings.isRemoteJSDebugEnabled() ?
|
||||
R.string.catalyst_remotedbg_message :
|
||||
R.string.catalyst_jsload_message))
|
||||
.create();
|
||||
dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
|
||||
dialog.show();
|
||||
return dialog;
|
||||
}
|
||||
|
||||
public void reloadJSFromServer(final String bundleURL) {
|
||||
final AlertDialog progressDialog = showProgressDialog();
|
||||
mDevLoadingViewController.showForUrl(bundleURL);
|
||||
mDevLoadingViewVisible = true;
|
||||
|
||||
mDevServerHelper.downloadBundleFromURL(
|
||||
new DevServerHelper.BundleDownloadCallback() {
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
progressDialog.dismiss();
|
||||
mDevLoadingViewController.hide();
|
||||
mDevLoadingViewVisible = false;
|
||||
UiThreadUtil.runOnUiThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
@@ -826,9 +821,15 @@ public class DevSupportManagerImpl implements
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgress(@Nullable final String status, @Nullable final Integer done, @Nullable final Integer total) {
|
||||
mDevLoadingViewController.updateProgress(status, done, total);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(final Exception cause) {
|
||||
progressDialog.dismiss();
|
||||
mDevLoadingViewController.hide();
|
||||
mDevLoadingViewVisible = false;
|
||||
FLog.e(ReactConstants.TAG, "Unable to download JS bundle", cause);
|
||||
UiThreadUtil.runOnUiThread(
|
||||
new Runnable() {
|
||||
@@ -848,13 +849,6 @@ public class DevSupportManagerImpl implements
|
||||
},
|
||||
mJSBundleTempFile,
|
||||
bundleURL);
|
||||
progressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
mDevServerHelper.cancelDownloadBundleFromURL();
|
||||
}
|
||||
});
|
||||
progressDialog.setCancelable(true);
|
||||
}
|
||||
|
||||
private void reload() {
|
||||
@@ -880,6 +874,11 @@ public class DevSupportManagerImpl implements
|
||||
mIsReceiverRegistered = true;
|
||||
}
|
||||
|
||||
// show the dev loading if it should be
|
||||
if (mDevLoadingViewVisible) {
|
||||
mDevLoadingViewController.show();
|
||||
}
|
||||
|
||||
mDevServerHelper.openPackagerConnection(this);
|
||||
mDevServerHelper.openInspectorConnection();
|
||||
if (mDevSettings.isReloadOnJSChangeEnabled()) {
|
||||
@@ -921,6 +920,9 @@ public class DevSupportManagerImpl implements
|
||||
mDevOptionsDialog.dismiss();
|
||||
}
|
||||
|
||||
// hide loading view
|
||||
mDevLoadingViewController.hide();
|
||||
|
||||
mDevServerHelper.closePackagerConnection();
|
||||
mDevServerHelper.closeInspectorConnection();
|
||||
mDevServerHelper.stopPollingOnChangeEndpoint();
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
package com.facebook.react.devsupport;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import okio.Buffer;
|
||||
import okio.BufferedSource;
|
||||
import okio.ByteString;
|
||||
|
||||
/**
|
||||
* Utility class to parse the body of a response of type multipart/mixed.
|
||||
*/
|
||||
public class MultipartStreamReader {
|
||||
// Standard line separator for HTTP.
|
||||
private static final String CRLF = "\r\n";
|
||||
|
||||
private final BufferedSource mSource;
|
||||
private final String mBoundary;
|
||||
|
||||
public interface ChunkCallback {
|
||||
void execute(Map<String, String> headers, Buffer body, boolean done) throws IOException;
|
||||
}
|
||||
|
||||
public MultipartStreamReader(BufferedSource source, String boundary) {
|
||||
mSource = source;
|
||||
mBoundary = boundary;
|
||||
}
|
||||
|
||||
private Map<String, String> parseHeaders(Buffer data) {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
|
||||
String text = data.readUtf8();
|
||||
String[] lines = text.split(CRLF);
|
||||
for (String line : lines) {
|
||||
int indexOfSeparator = line.indexOf(":");
|
||||
if (indexOfSeparator == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String key = line.substring(0, indexOfSeparator).trim();
|
||||
String value = line.substring(indexOfSeparator + 1).trim();
|
||||
headers.put(key, value);
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
private void emitChunk(Buffer chunk, boolean done, ChunkCallback callback) throws IOException {
|
||||
ByteString marker = ByteString.encodeUtf8(CRLF + CRLF);
|
||||
long indexOfMarker = chunk.indexOf(marker);
|
||||
if (indexOfMarker == -1) {
|
||||
callback.execute(null, chunk, done);
|
||||
} else {
|
||||
Buffer headers = new Buffer();
|
||||
Buffer body = new Buffer();
|
||||
chunk.read(headers, indexOfMarker);
|
||||
chunk.skip(marker.size());
|
||||
chunk.readAll(body);
|
||||
callback.execute(parseHeaders(headers), body, done);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads all parts of the multipart response and execute the callback for each chunk received.
|
||||
* @param callback Callback executed when a chunk is received
|
||||
* @return If the read was successful
|
||||
*/
|
||||
public boolean readAllParts(ChunkCallback callback) throws IOException {
|
||||
ByteString delimiter = ByteString.encodeUtf8(CRLF + "--" + mBoundary + CRLF);
|
||||
ByteString closeDelimiter = ByteString.encodeUtf8(CRLF + "--" + mBoundary + "--" + CRLF);
|
||||
|
||||
int bufferLen = 4 * 1024;
|
||||
long chunkStart = 0;
|
||||
long bytesSeen = 0;
|
||||
Buffer content = new Buffer();
|
||||
|
||||
while (true) {
|
||||
boolean isCloseDelimiter = false;
|
||||
|
||||
// Search only a subset of chunk that we haven't seen before + few bytes
|
||||
// to allow for the edge case when the delimiter is cut by read call.
|
||||
long searchStart = Math.max(bytesSeen - closeDelimiter.size(), chunkStart);
|
||||
long indexOfDelimiter = content.indexOf(delimiter, searchStart);
|
||||
if (indexOfDelimiter == -1) {
|
||||
isCloseDelimiter = true;
|
||||
indexOfDelimiter = content.indexOf(closeDelimiter, searchStart);
|
||||
}
|
||||
|
||||
if (indexOfDelimiter == -1) {
|
||||
bytesSeen = content.size();
|
||||
long bytesRead = mSource.read(content, bufferLen);
|
||||
if (bytesRead <= 0) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
long chunkEnd = indexOfDelimiter;
|
||||
long length = chunkEnd - chunkStart;
|
||||
|
||||
// Ignore preamble
|
||||
if (chunkStart > 0) {
|
||||
Buffer chunk = new Buffer();
|
||||
content.skip(chunkStart);
|
||||
content.read(chunk, length);
|
||||
emitChunk(chunk, isCloseDelimiter, callback);
|
||||
} else {
|
||||
content.skip(chunkEnd);
|
||||
}
|
||||
|
||||
if (isCloseDelimiter) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bytesSeen = chunkStart = delimiter.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user