Bundle download progress on Android

Summary:
Android equivalent of #15066

Tested that download progress shows up properly when reloading the app.

[ANDROID] [FEATURE] [DevSupport] - Show bundle download progress on Android
Closes https://github.com/facebook/react-native/pull/17809

Differential Revision: D6982823

Pulled By: hramos

fbshipit-source-id: da01e42b8ebb1c603f4407f6bafd68e0b6b3ecba
This commit is contained in:
Janic Duplessis 2018-02-13 20:20:39 -08:00 committed by Facebook Github Bot
parent d220118dbd
commit d06e143420
3 changed files with 87 additions and 31 deletions

View File

@ -146,13 +146,13 @@ public class BundleDownloader {
if (match.find()) {
String boundary = match.group(1);
MultipartStreamReader bodyReader = new MultipartStreamReader(response.body().source(), boundary);
boolean completed = bodyReader.readAllParts(new MultipartStreamReader.ChunkCallback() {
boolean completed = bodyReader.readAllParts(new MultipartStreamReader.ChunkListener() {
@Override
public void execute(Map<String, String> headers, Buffer body, boolean finished) throws IOException {
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
// (finished = true) will be the JS bundle, the other ones will be progress events
// (isLastChunk = true) will be the JS bundle, the other ones will be progress events
// encoded as JSON.
if (finished) {
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")) {
@ -184,6 +184,15 @@ public class BundleDownloader {
}
}
}
@Override
public void onChunkProgress(Map<String, String> headers, long loaded, long total) throws IOException {
if ("application/javascript".equals(headers.get("Content-Type"))) {
callback.onProgress(
"Downloading JavaScript bundle",
(int) (loaded / 1024),
(int) (total / 1024));
}
}
});
if (!completed) {
callback.onFailure(new DebugServerException(

View File

@ -26,9 +26,18 @@ public class MultipartStreamReader {
private final BufferedSource mSource;
private final String mBoundary;
private long mLastProgressEvent;
public interface ChunkCallback {
void execute(Map<String, String> headers, Buffer body, boolean done) throws IOException;
public interface ChunkListener {
/**
* Invoked when a chunk of a multipart response is fully downloaded.
*/
void onChunkComplete(Map<String, String> headers, Buffer body, boolean isLastChunk) throws IOException;
/**
* Invoked as bytes of the current chunk are read.
*/
void onChunkProgress(Map<String, String> headers, long loaded, long total) throws IOException;
}
public MultipartStreamReader(BufferedSource source, String boundary) {
@ -55,34 +64,50 @@ public class MultipartStreamReader {
return headers;
}
private void emitChunk(Buffer chunk, boolean done, ChunkCallback callback) throws IOException {
private void emitChunk(Buffer chunk, boolean done, ChunkListener listener) throws IOException {
ByteString marker = ByteString.encodeUtf8(CRLF + CRLF);
long indexOfMarker = chunk.indexOf(marker);
if (indexOfMarker == -1) {
callback.execute(null, chunk, done);
listener.onChunkComplete(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);
listener.onChunkComplete(parseHeaders(headers), body, done);
}
}
private void emitProgress(Map<String, String> headers, long contentLength, boolean isFinal, ChunkListener listener) throws IOException {
if (headers == null || listener == null) {
return;
}
long currentTime = System.currentTimeMillis();
if (currentTime - mLastProgressEvent > 16 || isFinal) {
mLastProgressEvent = currentTime;
long headersContentLength = headers.get("Content-Length") != null ? Long.parseLong(headers.get("Content-Length")) : 0;
listener.onChunkProgress(headers, contentLength, headersContentLength);
}
}
/**
* Reads all parts of the multipart response and execute the callback for each chunk received.
* @param callback Callback executed when a chunk is received
* Reads all parts of the multipart response and execute the listener for each chunk received.
* @param listener Listener invoked when chunks are received.
* @return If the read was successful
*/
public boolean readAllParts(ChunkCallback callback) throws IOException {
public boolean readAllParts(ChunkListener listener) throws IOException {
ByteString delimiter = ByteString.encodeUtf8(CRLF + "--" + mBoundary + CRLF);
ByteString closeDelimiter = ByteString.encodeUtf8(CRLF + "--" + mBoundary + "--" + CRLF);
ByteString headersDelimiter = ByteString.encodeUtf8(CRLF + CRLF);
int bufferLen = 4 * 1024;
long chunkStart = 0;
long bytesSeen = 0;
Buffer content = new Buffer();
Map<String, String> currentHeaders = null;
long currentHeadersLength = 0;
while (true) {
boolean isCloseDelimiter = false;
@ -98,6 +123,20 @@ public class MultipartStreamReader {
if (indexOfDelimiter == -1) {
bytesSeen = content.size();
if (currentHeaders == null) {
long indexOfHeaders = content.indexOf(headersDelimiter, searchStart);
if (indexOfHeaders >= 0) {
mSource.read(content, indexOfHeaders);
Buffer headers = new Buffer();
content.copyTo(headers, searchStart, indexOfHeaders - searchStart);
currentHeadersLength = headers.size() + headersDelimiter.size();
currentHeaders = parseHeaders(headers);
}
} else {
emitProgress(currentHeaders, content.size() - currentHeadersLength, false, listener);
}
long bytesRead = mSource.read(content, bufferLen);
if (bytesRead <= 0) {
return false;
@ -113,7 +152,10 @@ public class MultipartStreamReader {
Buffer chunk = new Buffer();
content.skip(chunkStart);
content.read(chunk, length);
emitChunk(chunk, isCloseDelimiter, callback);
emitProgress(currentHeaders, chunk.size() - currentHeadersLength, true, listener);
emitChunk(chunk, isCloseDelimiter, listener);
currentHeaders = null;
currentHeadersLength = 0;
} else {
content.skip(chunkEnd);
}

View File

@ -24,14 +24,19 @@ import static org.fest.assertions.api.Assertions.assertThat;
@RunWith(RobolectricTestRunner.class)
public class MultipartStreamReaderTest {
class CallCountTrackingChunkCallback implements MultipartStreamReader.ChunkCallback {
class CallCountTrackingChunkCallback implements MultipartStreamReader.ChunkListener {
private int mCount = 0;
@Override
public void execute(Map<String, String> headers, Buffer body, boolean done) throws IOException {
public void onChunkComplete(Map<String, String> headers, Buffer body, boolean done) throws IOException {
mCount++;
}
@Override
public void onChunkProgress(Map<String, String> headers, long loaded, long total) throws IOException {
}
public int getCallCount() {
return mCount;
}
@ -41,12 +46,12 @@ public class MultipartStreamReaderTest {
public void testSimpleCase() throws IOException {
ByteString response = ByteString.encodeUtf8(
"preable, should be ignored\r\n" +
"--sample_boundary\r\n" +
"Content-Type: application/json; charset=utf-8\r\n" +
"Content-Length: 2\r\n\r\n" +
"{}\r\n" +
"--sample_boundary--\r\n" +
"epilogue, should be ignored");
"--sample_boundary\r\n" +
"Content-Type: application/json; charset=utf-8\r\n" +
"Content-Length: 2\r\n\r\n" +
"{}\r\n" +
"--sample_boundary--\r\n" +
"epilogue, should be ignored");
Buffer source = new Buffer();
source.write(response);
@ -55,8 +60,8 @@ public class MultipartStreamReaderTest {
CallCountTrackingChunkCallback callback = new CallCountTrackingChunkCallback() {
@Override
public void execute(Map<String, String> headers, Buffer body, boolean done) throws IOException {
super.execute(headers, body, done);
public void onChunkComplete(Map<String, String> headers, Buffer body, boolean done) throws IOException {
super.onChunkComplete(headers, body, done);
assertThat(done).isTrue();
assertThat(headers.get("Content-Type")).isEqualTo("application/json; charset=utf-8");
@ -89,8 +94,8 @@ public class MultipartStreamReaderTest {
CallCountTrackingChunkCallback callback = new CallCountTrackingChunkCallback() {
@Override
public void execute(Map<String, String> headers, Buffer body, boolean done) throws IOException {
super.execute(headers, body, done);
public void onChunkComplete(Map<String, String> headers, Buffer body, boolean done) throws IOException {
super.onChunkComplete(headers, body, done);
assertThat(done).isEqualTo(getCallCount() == 3);
assertThat(body.readUtf8()).isEqualTo(String.valueOf(getCallCount()));
@ -122,12 +127,12 @@ public class MultipartStreamReaderTest {
public void testNoCloseDelimiter() throws IOException {
ByteString response = ByteString.encodeUtf8(
"preable, should be ignored\r\n" +
"--sample_boundary\r\n" +
"Content-Type: application/json; charset=utf-8\r\n" +
"Content-Length: 2\r\n\r\n" +
"{}\r\n" +
"--sample_boundary\r\n" +
"incomplete message...");
"--sample_boundary\r\n" +
"Content-Type: application/json; charset=utf-8\r\n" +
"Content-Length: 2\r\n\r\n" +
"{}\r\n" +
"--sample_boundary\r\n" +
"incomplete message...");
Buffer source = new Buffer();
source.write(response);