Skip to content

Commit d06e143

Browse files
janicduplessisfacebook-github-bot
authored andcommitted
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 #17809 Differential Revision: D6982823 Pulled By: hramos fbshipit-source-id: da01e42b8ebb1c603f4407f6bafd68e0b6b3ecba
1 parent d220118 commit d06e143

File tree

3 files changed

+88
-32
lines changed

3 files changed

+88
-32
lines changed

ReactAndroid/src/main/java/com/facebook/react/devsupport/BundleDownloader.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,13 +146,13 @@ public void onResponse(Call call, final Response response) throws IOException {
146146
if (match.find()) {
147147
String boundary = match.group(1);
148148
MultipartStreamReader bodyReader = new MultipartStreamReader(response.body().source(), boundary);
149-
boolean completed = bodyReader.readAllParts(new MultipartStreamReader.ChunkCallback() {
149+
boolean completed = bodyReader.readAllParts(new MultipartStreamReader.ChunkListener() {
150150
@Override
151-
public void execute(Map<String, String> headers, Buffer body, boolean finished) throws IOException {
151+
public void onChunkComplete(Map<String, String> headers, Buffer body, boolean isLastChunk) throws IOException {
152152
// This will get executed for every chunk of the multipart response. The last chunk
153-
// (finished = true) will be the JS bundle, the other ones will be progress events
153+
// (isLastChunk = true) will be the JS bundle, the other ones will be progress events
154154
// encoded as JSON.
155-
if (finished) {
155+
if (isLastChunk) {
156156
// The http status code for each separate chunk is in the X-Http-Status header.
157157
int status = response.code();
158158
if (headers.containsKey("X-Http-Status")) {
@@ -184,6 +184,15 @@ public void execute(Map<String, String> headers, Buffer body, boolean finished)
184184
}
185185
}
186186
}
187+
@Override
188+
public void onChunkProgress(Map<String, String> headers, long loaded, long total) throws IOException {
189+
if ("application/javascript".equals(headers.get("Content-Type"))) {
190+
callback.onProgress(
191+
"Downloading JavaScript bundle",
192+
(int) (loaded / 1024),
193+
(int) (total / 1024));
194+
}
195+
}
187196
});
188197
if (!completed) {
189198
callback.onFailure(new DebugServerException(

ReactAndroid/src/main/java/com/facebook/react/devsupport/MultipartStreamReader.java

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,18 @@ public class MultipartStreamReader {
2626

2727
private final BufferedSource mSource;
2828
private final String mBoundary;
29-
30-
public interface ChunkCallback {
31-
void execute(Map<String, String> headers, Buffer body, boolean done) throws IOException;
29+
private long mLastProgressEvent;
30+
31+
public interface ChunkListener {
32+
/**
33+
* Invoked when a chunk of a multipart response is fully downloaded.
34+
*/
35+
void onChunkComplete(Map<String, String> headers, Buffer body, boolean isLastChunk) throws IOException;
36+
37+
/**
38+
* Invoked as bytes of the current chunk are read.
39+
*/
40+
void onChunkProgress(Map<String, String> headers, long loaded, long total) throws IOException;
3241
}
3342

3443
public MultipartStreamReader(BufferedSource source, String boundary) {
@@ -55,34 +64,50 @@ private Map<String, String> parseHeaders(Buffer data) {
5564
return headers;
5665
}
5766

58-
private void emitChunk(Buffer chunk, boolean done, ChunkCallback callback) throws IOException {
67+
private void emitChunk(Buffer chunk, boolean done, ChunkListener listener) throws IOException {
5968
ByteString marker = ByteString.encodeUtf8(CRLF + CRLF);
6069
long indexOfMarker = chunk.indexOf(marker);
6170
if (indexOfMarker == -1) {
62-
callback.execute(null, chunk, done);
71+
listener.onChunkComplete(null, chunk, done);
6372
} else {
6473
Buffer headers = new Buffer();
6574
Buffer body = new Buffer();
6675
chunk.read(headers, indexOfMarker);
6776
chunk.skip(marker.size());
6877
chunk.readAll(body);
69-
callback.execute(parseHeaders(headers), body, done);
78+
listener.onChunkComplete(parseHeaders(headers), body, done);
79+
}
80+
}
81+
82+
private void emitProgress(Map<String, String> headers, long contentLength, boolean isFinal, ChunkListener listener) throws IOException {
83+
if (headers == null || listener == null) {
84+
return;
85+
}
86+
87+
long currentTime = System.currentTimeMillis();
88+
if (currentTime - mLastProgressEvent > 16 || isFinal) {
89+
mLastProgressEvent = currentTime;
90+
long headersContentLength = headers.get("Content-Length") != null ? Long.parseLong(headers.get("Content-Length")) : 0;
91+
listener.onChunkProgress(headers, contentLength, headersContentLength);
7092
}
7193
}
7294

7395
/**
74-
* Reads all parts of the multipart response and execute the callback for each chunk received.
75-
* @param callback Callback executed when a chunk is received
96+
* Reads all parts of the multipart response and execute the listener for each chunk received.
97+
* @param listener Listener invoked when chunks are received.
7698
* @return If the read was successful
7799
*/
78-
public boolean readAllParts(ChunkCallback callback) throws IOException {
100+
public boolean readAllParts(ChunkListener listener) throws IOException {
79101
ByteString delimiter = ByteString.encodeUtf8(CRLF + "--" + mBoundary + CRLF);
80102
ByteString closeDelimiter = ByteString.encodeUtf8(CRLF + "--" + mBoundary + "--" + CRLF);
103+
ByteString headersDelimiter = ByteString.encodeUtf8(CRLF + CRLF);
81104

82105
int bufferLen = 4 * 1024;
83106
long chunkStart = 0;
84107
long bytesSeen = 0;
85108
Buffer content = new Buffer();
109+
Map<String, String> currentHeaders = null;
110+
long currentHeadersLength = 0;
86111

87112
while (true) {
88113
boolean isCloseDelimiter = false;
@@ -98,6 +123,20 @@ public boolean readAllParts(ChunkCallback callback) throws IOException {
98123

99124
if (indexOfDelimiter == -1) {
100125
bytesSeen = content.size();
126+
127+
if (currentHeaders == null) {
128+
long indexOfHeaders = content.indexOf(headersDelimiter, searchStart);
129+
if (indexOfHeaders >= 0) {
130+
mSource.read(content, indexOfHeaders);
131+
Buffer headers = new Buffer();
132+
content.copyTo(headers, searchStart, indexOfHeaders - searchStart);
133+
currentHeadersLength = headers.size() + headersDelimiter.size();
134+
currentHeaders = parseHeaders(headers);
135+
}
136+
} else {
137+
emitProgress(currentHeaders, content.size() - currentHeadersLength, false, listener);
138+
}
139+
101140
long bytesRead = mSource.read(content, bufferLen);
102141
if (bytesRead <= 0) {
103142
return false;
@@ -113,7 +152,10 @@ public boolean readAllParts(ChunkCallback callback) throws IOException {
113152
Buffer chunk = new Buffer();
114153
content.skip(chunkStart);
115154
content.read(chunk, length);
116-
emitChunk(chunk, isCloseDelimiter, callback);
155+
emitProgress(currentHeaders, chunk.size() - currentHeadersLength, true, listener);
156+
emitChunk(chunk, isCloseDelimiter, listener);
157+
currentHeaders = null;
158+
currentHeadersLength = 0;
117159
} else {
118160
content.skip(chunkEnd);
119161
}

ReactAndroid/src/test/java/com/facebook/react/devsupport/MultipartStreamReaderTest.java

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,19 @@
2424
@RunWith(RobolectricTestRunner.class)
2525
public class MultipartStreamReaderTest {
2626

27-
class CallCountTrackingChunkCallback implements MultipartStreamReader.ChunkCallback {
27+
class CallCountTrackingChunkCallback implements MultipartStreamReader.ChunkListener {
2828
private int mCount = 0;
2929

3030
@Override
31-
public void execute(Map<String, String> headers, Buffer body, boolean done) throws IOException {
31+
public void onChunkComplete(Map<String, String> headers, Buffer body, boolean done) throws IOException {
3232
mCount++;
3333
}
3434

35+
@Override
36+
public void onChunkProgress(Map<String, String> headers, long loaded, long total) throws IOException {
37+
38+
}
39+
3540
public int getCallCount() {
3641
return mCount;
3742
}
@@ -41,12 +46,12 @@ public int getCallCount() {
4146
public void testSimpleCase() throws IOException {
4247
ByteString response = ByteString.encodeUtf8(
4348
"preable, should be ignored\r\n" +
44-
"--sample_boundary\r\n" +
45-
"Content-Type: application/json; charset=utf-8\r\n" +
46-
"Content-Length: 2\r\n\r\n" +
47-
"{}\r\n" +
48-
"--sample_boundary--\r\n" +
49-
"epilogue, should be ignored");
49+
"--sample_boundary\r\n" +
50+
"Content-Type: application/json; charset=utf-8\r\n" +
51+
"Content-Length: 2\r\n\r\n" +
52+
"{}\r\n" +
53+
"--sample_boundary--\r\n" +
54+
"epilogue, should be ignored");
5055

5156
Buffer source = new Buffer();
5257
source.write(response);
@@ -55,8 +60,8 @@ public void testSimpleCase() throws IOException {
5560

5661
CallCountTrackingChunkCallback callback = new CallCountTrackingChunkCallback() {
5762
@Override
58-
public void execute(Map<String, String> headers, Buffer body, boolean done) throws IOException {
59-
super.execute(headers, body, done);
63+
public void onChunkComplete(Map<String, String> headers, Buffer body, boolean done) throws IOException {
64+
super.onChunkComplete(headers, body, done);
6065

6166
assertThat(done).isTrue();
6267
assertThat(headers.get("Content-Type")).isEqualTo("application/json; charset=utf-8");
@@ -89,8 +94,8 @@ public void testMultipleParts() throws IOException {
8994

9095
CallCountTrackingChunkCallback callback = new CallCountTrackingChunkCallback() {
9196
@Override
92-
public void execute(Map<String, String> headers, Buffer body, boolean done) throws IOException {
93-
super.execute(headers, body, done);
97+
public void onChunkComplete(Map<String, String> headers, Buffer body, boolean done) throws IOException {
98+
super.onChunkComplete(headers, body, done);
9499

95100
assertThat(done).isEqualTo(getCallCount() == 3);
96101
assertThat(body.readUtf8()).isEqualTo(String.valueOf(getCallCount()));
@@ -122,12 +127,12 @@ public void testNoDelimiter() throws IOException {
122127
public void testNoCloseDelimiter() throws IOException {
123128
ByteString response = ByteString.encodeUtf8(
124129
"preable, should be ignored\r\n" +
125-
"--sample_boundary\r\n" +
126-
"Content-Type: application/json; charset=utf-8\r\n" +
127-
"Content-Length: 2\r\n\r\n" +
128-
"{}\r\n" +
129-
"--sample_boundary\r\n" +
130-
"incomplete message...");
130+
"--sample_boundary\r\n" +
131+
"Content-Type: application/json; charset=utf-8\r\n" +
132+
"Content-Length: 2\r\n\r\n" +
133+
"{}\r\n" +
134+
"--sample_boundary\r\n" +
135+
"incomplete message...");
131136

132137
Buffer source = new Buffer();
133138
source.write(response);

0 commit comments

Comments
 (0)