9
9
10
10
package com .facebook .react .devsupport ;
11
11
12
+ import android .util .JsonReader ;
13
+ import android .util .JsonToken ;
12
14
import android .util .Log ;
13
- import javax .annotation .Nullable ;
14
-
15
+ import com .facebook .common .logging .FLog ;
16
+ import com .facebook .infer .annotation .Assertions ;
17
+ import com .facebook .react .common .DebugServerException ;
18
+ import com .facebook .react .common .ReactConstants ;
19
+ import com .facebook .react .devsupport .interfaces .DevBundleDownloadListener ;
15
20
import java .io .File ;
21
+ import java .io .FileOutputStream ;
16
22
import java .io .IOException ;
23
+ import java .io .InputStreamReader ;
24
+ import java .util .LinkedHashMap ;
17
25
import java .util .Map ;
18
26
import java .util .regex .Matcher ;
19
27
import java .util .regex .Pattern ;
20
-
21
- import com .facebook .common .logging .FLog ;
22
- import com .facebook .infer .annotation .Assertions ;
23
- import com .facebook .react .common .ReactConstants ;
24
- import com .facebook .react .devsupport .interfaces .DevBundleDownloadListener ;
25
- import com .facebook .react .common .DebugServerException ;
26
-
27
- import org .json .JSONException ;
28
- import org .json .JSONObject ;
29
-
28
+ import javax .annotation .Nullable ;
30
29
import okhttp3 .Call ;
31
30
import okhttp3 .Callback ;
32
31
import okhttp3 .OkHttpClient ;
36
35
import okio .BufferedSource ;
37
36
import okio .Okio ;
38
37
import okio .Sink ;
38
+ import org .json .JSONException ;
39
+ import org .json .JSONObject ;
39
40
40
41
public class BundleDownloader {
41
42
private static final String TAG = "BundleDownloader" ;
@@ -45,6 +46,11 @@ public class BundleDownloader {
45
46
46
47
private final OkHttpClient mClient ;
47
48
49
+ private final LinkedHashMap <Number , byte []> mPreModules = new LinkedHashMap <>();
50
+ private final LinkedHashMap <Number , byte []> mDeltaModules = new LinkedHashMap <>();
51
+ private final LinkedHashMap <Number , byte []> mPostModules = new LinkedHashMap <>();
52
+
53
+ private @ Nullable String mDeltaId ;
48
54
private @ Nullable Call mDownloadBundleFromURLCall ;
49
55
50
56
public static class BundleInfo {
@@ -102,13 +108,22 @@ public void downloadBundleFromURL(
102
108
final File outputFile ,
103
109
final String bundleURL ,
104
110
final @ Nullable BundleInfo bundleInfo ) {
105
- final Request request = new Request .Builder ()
106
- .url (bundleURL )
107
- // FIXME: there is a bug that makes MultipartStreamReader to never find the end of the
108
- // multipart message. This temporarily disables the multipart mode to work around it, but
109
- // it means there is no progress bar displayed in the React Native overlay anymore.
110
- //.addHeader("Accept", "multipart/mixed")
111
- .build ();
111
+
112
+ String finalUrl = bundleURL ;
113
+
114
+ if (isDeltaUrl (bundleURL ) && mDeltaId != null ) {
115
+ finalUrl += "&deltaBundleId=" + mDeltaId ;
116
+ }
117
+
118
+ final Request request =
119
+ new Request .Builder ()
120
+ .url (finalUrl )
121
+ // FIXME: there is a bug that makes MultipartStreamReader to never find the end of the
122
+ // multipart message. This temporarily disables the multipart mode to work around it,
123
+ // but
124
+ // it means there is no progress bar displayed in the React Native overlay anymore.
125
+ // .addHeader("Accept", "multipart/mixed")
126
+ .build ();
112
127
mDownloadBundleFromURLCall = Assertions .assertNotNull (mClient .newCall (request ));
113
128
mDownloadBundleFromURLCall .enqueue (new Callback () {
114
129
@ Override
@@ -161,6 +176,7 @@ public void execute(Map<String, String> headers, Buffer body, boolean finished)
161
176
if (!headers .containsKey ("Content-Type" ) || !headers .get ("Content-Type" ).equals ("application/json" )) {
162
177
return ;
163
178
}
179
+
164
180
try {
165
181
JSONObject progress = new JSONObject (body .readUtf8 ());
166
182
String status = null ;
@@ -202,14 +218,15 @@ public void cancelDownloadBundleFromURL() {
202
218
}
203
219
}
204
220
205
- private static void processBundleResult (
221
+ private void processBundleResult (
206
222
String url ,
207
223
int statusCode ,
208
224
okhttp3 .Headers headers ,
209
225
BufferedSource body ,
210
226
File outputFile ,
211
227
BundleInfo bundleInfo ,
212
- DevBundleDownloadListener callback ) throws IOException {
228
+ DevBundleDownloadListener callback )
229
+ throws IOException {
213
230
// Check for server errors. If the server error has the expected form, fail with more info.
214
231
if (statusCode != 200 ) {
215
232
String bodyString = body .readUtf8 ();
@@ -232,21 +249,135 @@ private static void processBundleResult(
232
249
}
233
250
234
251
File tmpFile = new File (outputFile .getPath () + ".tmp" );
252
+
253
+ boolean bundleUpdated ;
254
+
255
+ if (isDeltaUrl (url )) {
256
+ // If the bundle URL has the delta extension, we need to use the delta patching logic.
257
+ bundleUpdated = storeDeltaInFile (body , tmpFile );
258
+ } else {
259
+ resetDeltaCache ();
260
+ bundleUpdated = storePlainJSInFile (body , tmpFile );
261
+ }
262
+
263
+ if (bundleUpdated ) {
264
+ // If we have received a new bundle from the server, move it to its final destination.
265
+ if (!tmpFile .renameTo (outputFile )) {
266
+ throw new IOException ("Couldn't rename " + tmpFile + " to " + outputFile );
267
+ }
268
+ }
269
+
270
+ callback .onSuccess ();
271
+ }
272
+
273
+ private static boolean storePlainJSInFile (BufferedSource body , File outputFile )
274
+ throws IOException {
235
275
Sink output = null ;
236
276
try {
237
- output = Okio .sink (tmpFile );
277
+ output = Okio .sink (outputFile );
238
278
body .readAll (output );
239
279
} finally {
240
280
if (output != null ) {
241
281
output .close ();
242
282
}
243
283
}
244
284
245
- if (tmpFile .renameTo (outputFile )) {
246
- callback .onSuccess ();
247
- } else {
248
- throw new IOException ("Couldn't rename " + tmpFile + " to " + outputFile );
285
+ return true ;
286
+ }
287
+
288
+ private boolean storeDeltaInFile (BufferedSource body , File outputFile ) throws IOException {
289
+
290
+ JsonReader jsonReader = new JsonReader (new InputStreamReader (body .inputStream ()));
291
+
292
+ jsonReader .beginObject ();
293
+
294
+ int numChangedModules = 0 ;
295
+
296
+ while (jsonReader .hasNext ()) {
297
+ String name = jsonReader .nextName ();
298
+ if (name .equals ("id" )) {
299
+ mDeltaId = jsonReader .nextString ();
300
+ } else if (name .equals ("pre" )) {
301
+ numChangedModules += patchDelta (jsonReader , mPreModules );
302
+ } else if (name .equals ("post" )) {
303
+ numChangedModules += patchDelta (jsonReader , mPostModules );
304
+ } else if (name .equals ("delta" )) {
305
+ numChangedModules += patchDelta (jsonReader , mDeltaModules );
306
+ } else {
307
+ jsonReader .skipValue ();
308
+ }
309
+ }
310
+
311
+ jsonReader .endObject ();
312
+ jsonReader .close ();
313
+
314
+ if (numChangedModules == 0 ) {
315
+ // If we receive an empty delta, we don't need to save the file again (it'll have the
316
+ // same content).
317
+ return false ;
249
318
}
319
+
320
+ FileOutputStream fileOutputStream = new FileOutputStream (outputFile );
321
+
322
+ try {
323
+ for (byte [] code : mPreModules .values ()) {
324
+ fileOutputStream .write (code );
325
+ fileOutputStream .write ('\n' );
326
+ }
327
+
328
+ for (byte [] code : mDeltaModules .values ()) {
329
+ fileOutputStream .write (code );
330
+ fileOutputStream .write ('\n' );
331
+ }
332
+
333
+ for (byte [] code : mPostModules .values ()) {
334
+ fileOutputStream .write (code );
335
+ fileOutputStream .write ('\n' );
336
+ }
337
+ } finally {
338
+ fileOutputStream .flush ();
339
+ fileOutputStream .close ();
340
+ }
341
+
342
+ return true ;
343
+ }
344
+
345
+ private static int patchDelta (JsonReader jsonReader , LinkedHashMap <Number , byte []> map )
346
+ throws IOException {
347
+ jsonReader .beginArray ();
348
+
349
+ int numModules = 0 ;
350
+ while (jsonReader .hasNext ()) {
351
+ jsonReader .beginArray ();
352
+
353
+ int moduleId = jsonReader .nextInt ();
354
+
355
+ if (jsonReader .peek () == JsonToken .NULL ) {
356
+ jsonReader .skipValue ();
357
+ map .remove (moduleId );
358
+ } else {
359
+ map .put (moduleId , jsonReader .nextString ().getBytes ());
360
+ }
361
+
362
+ jsonReader .endArray ();
363
+ numModules ++;
364
+ }
365
+
366
+ jsonReader .endArray ();
367
+
368
+ return numModules ;
369
+ }
370
+
371
+ private void resetDeltaCache () {
372
+ mDeltaId = null ;
373
+
374
+ mDeltaModules .clear ();
375
+ mPreModules .clear ();
376
+ mPostModules .clear ();
377
+ }
378
+
379
+ private static boolean isDeltaUrl (String bundleUrl ) {
380
+ return bundleUrl .indexOf (".delta?" ) != -1 ;
250
381
}
251
382
252
383
private static void populateBundleInfo (String url , okhttp3 .Headers headers , BundleInfo bundleInfo ) {
0 commit comments