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:
parent
d220118dbd
commit
d06e143420
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue