1717import java .util .Locale ;
1818import java .util .Map ;
1919import java .util .concurrent .TimeUnit ;
20+ import java .util .regex .Matcher ;
21+ import java .util .regex .Pattern ;
2022
2123import android .content .Context ;
2224import android .os .AsyncTask ;
3234import com .facebook .react .modules .systeminfo .AndroidInfoHelpers ;
3335import com .facebook .react .packagerconnection .JSPackagerClient ;
3436
37+ import org .json .JSONException ;
38+ import org .json .JSONObject ;
39+
3540import okhttp3 .Call ;
3641import okhttp3 .Callback ;
3742import okhttp3 .ConnectionPool ;
3843import okhttp3 .OkHttpClient ;
3944import okhttp3 .Request ;
4045import okhttp3 .Response ;
4146import okhttp3 .ResponseBody ;
47+ import okio .Buffer ;
48+ import okio .BufferedSource ;
4249import okio .Okio ;
4350import okio .Sink ;
4451
@@ -78,6 +85,7 @@ public class DevServerHelper {
7885
7986 public interface BundleDownloadCallback {
8087 void onSuccess ();
88+ void onProgress (@ Nullable String status , @ Nullable Integer done , @ Nullable Integer total );
8189 void onFailure (Exception cause );
8290 }
8391
@@ -297,6 +305,7 @@ public void downloadBundleFromURL(
297305 final String bundleURL ) {
298306 final Request request = new Request .Builder ()
299307 .url (bundleURL )
308+ .addHeader ("Accept" , "multipart/mixed" )
300309 .build ();
301310 mDownloadBundleFromURLCall = Assertions .assertNotNull (mClient .newCall (request ));
302311 mDownloadBundleFromURLCall .enqueue (new Callback () {
@@ -316,45 +325,109 @@ public void onFailure(Call call, IOException e) {
316325 }
317326
318327 @ Override
319- public void onResponse (Call call , Response response ) throws IOException {
328+ public void onResponse (Call call , final Response response ) throws IOException {
320329 // ignore callback if call was cancelled
321330 if (mDownloadBundleFromURLCall == null || mDownloadBundleFromURLCall .isCanceled ()) {
322331 mDownloadBundleFromURLCall = null ;
323332 return ;
324333 }
325334 mDownloadBundleFromURLCall = null ;
326335
327- // Check for server errors. If the server error has the expected form, fail with more info.
328- if (!response .isSuccessful ()) {
329- String body = response .body ().string ();
330- DebugServerException debugServerException = DebugServerException .parse (body );
331- if (debugServerException != null ) {
332- callback .onFailure (debugServerException );
333- } else {
334- StringBuilder sb = new StringBuilder ();
335- sb .append ("The development server returned response error code: " ).append (response .code ()).append ("\n \n " )
336- .append ("URL: " ).append (call .request ().url ().toString ()).append ("\n \n " )
337- .append ("Body:\n " )
338- .append (body );
339- callback .onFailure (new DebugServerException (sb .toString ()));
340- }
341- return ;
342- }
336+ final String url = response .request ().url ().toString ();
343337
344- Sink output = null ;
345- try {
346- output = Okio .sink (outputFile );
347- Okio .buffer (response .body ().source ()).readAll (output );
348- callback .onSuccess ();
349- } finally {
350- if (output != null ) {
351- output .close ();
338+ // Make sure the result is a multipart response and parse the boundary.
339+ String contentType = response .header ("content-type" );
340+ Pattern regex = Pattern .compile ("multipart/mixed;.*boundary=\" ([^\" ]+)\" " );
341+ Matcher match = regex .matcher (contentType );
342+ if (match .find ()) {
343+ String boundary = match .group (1 );
344+ MultipartStreamReader bodyReader = new MultipartStreamReader (response .body ().source (), boundary );
345+ boolean completed = bodyReader .readAllParts (new MultipartStreamReader .ChunkCallback () {
346+ @ Override
347+ public void execute (Map <String , String > headers , Buffer body , boolean finished ) throws IOException {
348+ // This will get executed for every chunk of the multipart response. The last chunk
349+ // (finished = true) will be the JS bundle, the other ones will be progress events
350+ // encoded as JSON.
351+ if (finished ) {
352+ // The http status code for each separate chunk is in the X-Http-Status header.
353+ int status = response .code ();
354+ if (headers .containsKey ("X-Http-Status" )) {
355+ status = Integer .parseInt (headers .get ("X-Http-Status" ));
356+ }
357+ processBundleResult (url , status , body , outputFile , callback );
358+ } else {
359+ if (!headers .containsKey ("Content-Type" ) || !headers .get ("Content-Type" ).equals ("application/json" )) {
360+ return ;
361+ }
362+ try {
363+ JSONObject progress = new JSONObject (body .readUtf8 ());
364+ String status = null ;
365+ if (progress .has ("status" )) {
366+ status = progress .getString ("status" );
367+ }
368+ Integer done = null ;
369+ if (progress .has ("done" )) {
370+ done = progress .getInt ("done" );
371+ }
372+ Integer total = null ;
373+ if (progress .has ("total" )) {
374+ total = progress .getInt ("total" );
375+ }
376+ callback .onProgress (status , done , total );
377+ } catch (JSONException e ) {
378+ FLog .e (ReactConstants .TAG , "Error parsing progress JSON. " + e .toString ());
379+ }
380+ }
381+ }
382+ });
383+ if (!completed ) {
384+ callback .onFailure (new DebugServerException (
385+ "Error while reading multipart response.\n \n Response code: " + response .code () + "\n \n " +
386+ "URL: " + call .request ().url ().toString () + "\n \n " ));
352387 }
388+ } else {
389+ // In case the server doesn't support multipart/mixed responses, fallback to normal download.
390+ processBundleResult (url , response .code (), Okio .buffer (response .body ().source ()), outputFile , callback );
353391 }
354392 }
355393 });
356394 }
357395
396+ private void processBundleResult (
397+ String url ,
398+ int statusCode ,
399+ BufferedSource body ,
400+ File outputFile ,
401+ BundleDownloadCallback callback ) throws IOException {
402+ // Check for server errors. If the server error has the expected form, fail with more info.
403+ if (statusCode != 200 ) {
404+ String bodyString = body .readUtf8 ();
405+ DebugServerException debugServerException = DebugServerException .parse (bodyString );
406+ if (debugServerException != null ) {
407+ callback .onFailure (debugServerException );
408+ } else {
409+ StringBuilder sb = new StringBuilder ();
410+ sb .append ("The development server returned response error code: " ).append (statusCode ).append ("\n \n " )
411+ .append ("URL: " ).append (url ).append ("\n \n " )
412+ .append ("Body:\n " )
413+ .append (bodyString );
414+ callback .onFailure (new DebugServerException (sb .toString ()));
415+ }
416+ return ;
417+ }
418+
419+ Sink output = null ;
420+ try {
421+ output = Okio .sink (outputFile );
422+ body .readAll (output );
423+ callback .onSuccess ();
424+ } finally {
425+ if (output != null ) {
426+ output .close ();
427+ }
428+ }
429+ }
430+
358431 public void cancelDownloadBundleFromURL () {
359432 if (mDownloadBundleFromURLCall != null ) {
360433 mDownloadBundleFromURLCall .cancel ();
0 commit comments