Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bundle download progress on Android #17809

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -146,13 +146,13 @@ public void onResponse(Call call, final Response response) throws IOException {
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")) {
Expand Down Expand Up @@ -184,6 +184,15 @@ public void execute(Map<String, String> headers, Buffer body, boolean finished)
}
}
}
@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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,18 @@ public class MultipartStreamReader {

private final BufferedSource mSource;
private final String mBoundary;

public interface ChunkCallback {
void execute(Map<String, String> headers, Buffer body, boolean done) throws IOException;
private long mLastProgressEvent;

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) {
Expand All @@ -55,34 +64,50 @@ private Map<String, String> parseHeaders(Buffer data) {
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;
Expand All @@ -98,6 +123,20 @@ public boolean readAllParts(ChunkCallback callback) throws IOException {

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;
Expand All @@ -113,7 +152,10 @@ public boolean readAllParts(ChunkCallback callback) throws IOException {
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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,19 @@
@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;
}
Expand All @@ -41,12 +46,12 @@ public int getCallCount() {
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);
Expand All @@ -55,8 +60,8 @@ public void testSimpleCase() throws IOException {

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");
Expand Down Expand Up @@ -89,8 +94,8 @@ public void testMultipleParts() throws IOException {

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()));
Expand Down Expand Up @@ -122,12 +127,12 @@ public void testNoDelimiter() throws IOException {
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);
Expand Down