@bravemobile/react-native-code-push 12.4.1 → 13.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/CodePush.podspec +47 -2
  2. package/README.md +118 -3
  3. package/THIRD-PARTY-NOTICES +96 -0
  4. package/android/app/build.gradle +26 -0
  5. package/android/app/proguard-rules.pro +5 -0
  6. package/android/app/src/main/cpp/CMakeLists.txt +38 -0
  7. package/android/app/src/main/cpp/binarypatch_jni.c +181 -0
  8. package/android/app/src/main/java/com/microsoft/codepush/react/BinaryPatchAttempt.java +92 -0
  9. package/android/app/src/main/java/com/microsoft/codepush/react/BinaryPatchResult.java +64 -0
  10. package/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java +46 -11
  11. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushBinaryPatch.java +293 -0
  12. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushConstants.java +25 -0
  13. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java +79 -88
  14. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateManager.java +159 -4
  15. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateUtils.java +13 -0
  16. package/android/app/src/main/java/com/microsoft/codepush/react/HDiffPatchNative.java +52 -0
  17. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushBinaryPatchTest.java +393 -0
  18. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerBinaryPatchTest.java +207 -0
  19. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerDownloadTest.java +449 -0
  20. package/android/codepush-export.gradle +115 -0
  21. package/cli/commands/bundleCommand/bundleCodePush.ts +39 -7
  22. package/cli/commands/bundleCommand/index.ts +9 -1
  23. package/cli/commands/releaseCommand/addToReleaseHistory.ts +7 -0
  24. package/cli/commands/releaseCommand/index.test.ts +118 -0
  25. package/cli/commands/releaseCommand/index.ts +20 -2
  26. package/cli/commands/releaseCommand/release.test.ts +582 -0
  27. package/cli/commands/releaseCommand/release.ts +232 -15
  28. package/cli/dist/commands/bundleCommand/bundleCodePush.js +26 -7
  29. package/cli/dist/commands/bundleCommand/index.js +5 -1
  30. package/cli/dist/commands/releaseCommand/addToReleaseHistory.js +6 -1
  31. package/cli/dist/commands/releaseCommand/index.js +8 -1
  32. package/cli/dist/commands/releaseCommand/index.test.js +96 -0
  33. package/cli/dist/commands/releaseCommand/release.js +165 -17
  34. package/cli/dist/commands/releaseCommand/release.test.js +367 -0
  35. package/cli/dist/functions/makeBinaryPatchBundle.js +248 -0
  36. package/cli/dist/functions/makeBinaryPatchBundle.test.js +321 -0
  37. package/cli/dist/functions/makeCodePushBundle.js +4 -2
  38. package/cli/dist/functions/runHermesEmitBinaryCommand.js +98 -27
  39. package/cli/dist/functions/runHermesEmitBinaryCommand.test.js +100 -0
  40. package/cli/dist/jest.globalSetup.js +38 -0
  41. package/cli/dist/utils/binaryPatch.js +86 -0
  42. package/cli/dist/utils/binaryPatch.test.js +190 -0
  43. package/cli/functions/makeBinaryPatchBundle.test.ts +410 -0
  44. package/cli/functions/makeBinaryPatchBundle.ts +367 -0
  45. package/cli/functions/makeCodePushBundle.ts +4 -2
  46. package/cli/functions/runHermesEmitBinaryCommand.test.ts +124 -0
  47. package/cli/functions/runHermesEmitBinaryCommand.ts +112 -26
  48. package/cli/jest.globalSetup.ts +43 -0
  49. package/cli/utils/binaryPatch.test.ts +234 -0
  50. package/cli/utils/binaryPatch.ts +103 -0
  51. package/cpp/binarypatch/binarypatch_zstd_decompressor.c +187 -0
  52. package/cpp/binarypatch/binarypatch_zstd_decompressor.h +47 -0
  53. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/checksum_plugin.h +52 -0
  54. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/hpatch_mt/hpatch_mt.h +147 -0
  55. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch.c +3315 -0
  56. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch.h +268 -0
  57. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch_private.h +297 -0
  58. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch_types.h +434 -0
  59. package/cpp/binarypatch/vendor/zstd/common/allocations.h +55 -0
  60. package/cpp/binarypatch/vendor/zstd/common/bits.h +205 -0
  61. package/cpp/binarypatch/vendor/zstd/common/bitstream.h +454 -0
  62. package/cpp/binarypatch/vendor/zstd/common/compiler.h +464 -0
  63. package/cpp/binarypatch/vendor/zstd/common/cpu.h +249 -0
  64. package/cpp/binarypatch/vendor/zstd/common/debug.c +30 -0
  65. package/cpp/binarypatch/vendor/zstd/common/debug.h +107 -0
  66. package/cpp/binarypatch/vendor/zstd/common/entropy_common.c +340 -0
  67. package/cpp/binarypatch/vendor/zstd/common/error_private.c +64 -0
  68. package/cpp/binarypatch/vendor/zstd/common/error_private.h +158 -0
  69. package/cpp/binarypatch/vendor/zstd/common/fse.h +625 -0
  70. package/cpp/binarypatch/vendor/zstd/common/fse_decompress.c +315 -0
  71. package/cpp/binarypatch/vendor/zstd/common/huf.h +277 -0
  72. package/cpp/binarypatch/vendor/zstd/common/mem.h +422 -0
  73. package/cpp/binarypatch/vendor/zstd/common/pool.c +371 -0
  74. package/cpp/binarypatch/vendor/zstd/common/pool.h +81 -0
  75. package/cpp/binarypatch/vendor/zstd/common/portability_macros.h +171 -0
  76. package/cpp/binarypatch/vendor/zstd/common/threading.c +182 -0
  77. package/cpp/binarypatch/vendor/zstd/common/threading.h +142 -0
  78. package/cpp/binarypatch/vendor/zstd/common/xxhash.c +18 -0
  79. package/cpp/binarypatch/vendor/zstd/common/xxhash.h +7094 -0
  80. package/cpp/binarypatch/vendor/zstd/common/zstd_common.c +48 -0
  81. package/cpp/binarypatch/vendor/zstd/common/zstd_deps.h +123 -0
  82. package/cpp/binarypatch/vendor/zstd/common/zstd_internal.h +324 -0
  83. package/cpp/binarypatch/vendor/zstd/common/zstd_trace.h +156 -0
  84. package/cpp/binarypatch/vendor/zstd/decompress/huf_decompress.c +1944 -0
  85. package/cpp/binarypatch/vendor/zstd/decompress/zstd_ddict.c +244 -0
  86. package/cpp/binarypatch/vendor/zstd/decompress/zstd_ddict.h +44 -0
  87. package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress.c +2410 -0
  88. package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_block.c +2209 -0
  89. package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_block.h +73 -0
  90. package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_internal.h +240 -0
  91. package/cpp/binarypatch/vendor/zstd/zstd.h +3206 -0
  92. package/cpp/binarypatch/vendor/zstd/zstd_errors.h +107 -0
  93. package/ios/CodePush/CodePush.h +58 -9
  94. package/ios/CodePush/{CodePush.m → CodePush.mm} +77 -63
  95. package/ios/CodePush/CodePushBinaryPatch.h +80 -0
  96. package/ios/CodePush/CodePushBinaryPatch.m +473 -0
  97. package/ios/CodePush/CodePushPackage.m +183 -2
  98. package/ios/CodePush/CodePushUpdateUtils.m +49 -1
  99. package/ios/CodePush.xcodeproj/project.pbxproj +6 -6
  100. package/package.json +28 -4
  101. package/react-native.config.js +3 -1
  102. package/scripts/binary-patch/build-hdiffpatch.sh +86 -0
  103. package/scripts/export-embedded-bundle.sh +76 -0
  104. package/src/CodePush.js +190 -96
  105. package/src/CodePush.test.js +382 -0
  106. package/src/native/NativeCodePush.ts +162 -0
  107. package/src/package-mixins.js +24 -11
  108. package/src/specs/NativeCodePush.ts +43 -0
  109. package/typings/react-native-code-push.d.ts +77 -0
  110. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushDialog.java +0 -102
  111. package/src/AlertAdapter.js +0 -24
@@ -2,7 +2,6 @@ package com.microsoft.codepush.react;
2
2
 
3
3
  import android.app.Activity;
4
4
  import android.content.SharedPreferences;
5
- import android.os.AsyncTask;
6
5
  import android.os.Handler;
7
6
  import android.os.Looper;
8
7
  import android.view.View;
@@ -20,11 +19,11 @@ import com.facebook.react.bridge.JSBundleLoader;
20
19
  import com.facebook.react.bridge.LifecycleEventListener;
21
20
  import com.facebook.react.bridge.Promise;
22
21
  import com.facebook.react.bridge.ReactApplicationContext;
23
- import com.facebook.react.bridge.ReactContextBaseJavaModule;
24
22
  import com.facebook.react.bridge.ReactMethod;
25
23
  import com.facebook.react.bridge.ReadableMap;
26
24
  import com.facebook.react.bridge.WritableMap;
27
25
  import com.facebook.react.common.annotations.UnstableReactNativeAPI;
26
+ import com.facebook.react.module.annotations.ReactModule;
28
27
  import com.facebook.react.modules.core.DeviceEventManagerModule;
29
28
  import com.facebook.react.modules.core.ReactChoreographer;
30
29
  import com.facebook.react.runtime.ReactHostDelegate;
@@ -37,15 +36,19 @@ import java.io.IOException;
37
36
  import java.lang.reflect.Field;
38
37
  import java.util.ArrayList;
39
38
  import java.util.Date;
40
- import java.util.HashMap;
41
- import java.util.Map;
42
39
  import java.util.UUID;
40
+ import java.util.concurrent.ExecutorService;
41
+ import java.util.concurrent.Executors;
42
+
43
+ @ReactModule(name = CodePushNativeModule.NAME)
44
+ public class CodePushNativeModule extends NativeCodePushSpec {
45
+ public static final String NAME = "CodePush";
43
46
 
44
- public class CodePushNativeModule extends ReactContextBaseJavaModule {
45
47
  private String mBinaryContentsHash = null;
46
48
  private String mClientUniqueId = null;
47
49
  private LifecycleEventListener mLifecycleEventListener = null;
48
50
  private int mMinimumBackgroundDuration = 0;
51
+ private final ExecutorService mBackgroundExecutor = Executors.newSingleThreadExecutor();
49
52
 
50
53
  private CodePush mCodePush;
51
54
  private SettingsManager mSettingsManager;
@@ -76,24 +79,15 @@ public class CodePushNativeModule extends ReactContextBaseJavaModule {
76
79
  }
77
80
 
78
81
  @Override
79
- public Map<String, Object> getConstants() {
80
- final Map<String, Object> constants = new HashMap<>();
81
-
82
- constants.put("codePushInstallModeImmediate", CodePushInstallMode.IMMEDIATE.getValue());
83
- constants.put("codePushInstallModeOnNextRestart", CodePushInstallMode.ON_NEXT_RESTART.getValue());
84
- constants.put("codePushInstallModeOnNextResume", CodePushInstallMode.ON_NEXT_RESUME.getValue());
85
- constants.put("codePushInstallModeOnNextSuspend", CodePushInstallMode.ON_NEXT_SUSPEND.getValue());
86
-
87
- constants.put("codePushUpdateStateRunning", CodePushUpdateState.RUNNING.getValue());
88
- constants.put("codePushUpdateStatePending", CodePushUpdateState.PENDING.getValue());
89
- constants.put("codePushUpdateStateLatest", CodePushUpdateState.LATEST.getValue());
90
-
91
- return constants;
82
+ public String getName() {
83
+ return NAME;
92
84
  }
93
85
 
94
86
  @Override
95
- public String getName() {
96
- return "CodePush";
87
+ public void invalidate() {
88
+ clearLifecycleEventListener();
89
+ mBackgroundExecutor.shutdownNow();
90
+ super.invalidate();
97
91
  }
98
92
 
99
93
  private void loadBundleLegacy() {
@@ -265,6 +259,21 @@ public class CodePushNativeModule extends ReactContextBaseJavaModule {
265
259
  return null;
266
260
  }
267
261
 
262
+ private void executeInBackground(Runnable runnable) {
263
+ mBackgroundExecutor.execute(runnable);
264
+ }
265
+
266
+ private void emitDownloadProgressEvent(DownloadProgress downloadProgress) {
267
+ if (mEventEmitterCallback != null) {
268
+ emitOnDownloadProgress(downloadProgress.createWritableMap());
269
+ return;
270
+ }
271
+
272
+ getReactApplicationContext()
273
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
274
+ .emit(CodePushConstants.DOWNLOAD_PROGRESS_EVENT_NAME, downloadProgress.createWritableMap());
275
+ }
276
+
268
277
  private void restartAppInternal(boolean onlyIfUpdateIsPending) {
269
278
  if (this._restartInProgress) {
270
279
  CodePushUtils.log("Restart request queued until the current restart is completed");
@@ -292,7 +301,7 @@ public class CodePushNativeModule extends ReactContextBaseJavaModule {
292
301
  }
293
302
 
294
303
  @ReactMethod
295
- public void allow(Promise promise) {
304
+ public void allow() {
296
305
  CodePushUtils.log("Re-allowing restarts");
297
306
  this._allowed = true;
298
307
 
@@ -302,54 +311,39 @@ public class CodePushNativeModule extends ReactContextBaseJavaModule {
302
311
  this._restartQueue.remove(0);
303
312
  this.restartAppInternal(buf);
304
313
  }
305
-
306
- promise.resolve(null);
307
- return;
308
314
  }
309
315
 
310
316
  @ReactMethod
311
- public void clearPendingRestart(Promise promise) {
317
+ public void clearPendingRestart() {
312
318
  this._restartQueue.clear();
313
- promise.resolve(null);
314
- return;
315
319
  }
316
320
 
317
321
  @ReactMethod
318
- public void disallow(Promise promise) {
322
+ public void disallow() {
319
323
  CodePushUtils.log("Disallowing restarts");
320
324
  this._allowed = false;
321
- promise.resolve(null);
322
- return;
323
325
  }
324
326
 
325
327
  @ReactMethod
326
- public void restartApp(boolean onlyIfUpdateIsPending, Promise promise) {
327
- try {
328
- restartAppInternal(onlyIfUpdateIsPending);
329
- promise.resolve(null);
330
- } catch(CodePushUnknownException e) {
331
- CodePushUtils.log(e);
332
- promise.reject(e);
333
- }
328
+ public void restartApp(boolean onlyIfUpdateIsPending) {
329
+ restartAppInternal(onlyIfUpdateIsPending);
334
330
  }
335
331
 
336
332
  @ReactMethod
337
333
  public void downloadUpdate(final ReadableMap updatePackage, final boolean notifyProgress, final Promise promise) {
338
- AsyncTask<Void, Void, Void> asyncTask = new AsyncTask<Void, Void, Void>() {
334
+ executeInBackground(new Runnable() {
339
335
  @Override
340
- protected Void doInBackground(Void... params) {
336
+ public void run() {
341
337
  try {
342
338
  JSONObject mutableUpdatePackage = CodePushUtils.convertReadableToJsonObject(updatePackage);
343
- mUpdateManager.downloadPackage(mutableUpdatePackage, mCodePush.getAssetsBundleFileName(), new DownloadProgressCallback() {
339
+ JSONObject binaryPatchResult = mUpdateManager.downloadPackage(mutableUpdatePackage, mCodePush.getAssetsBundleFileName(), new DownloadProgressCallback() {
344
340
  private volatile boolean hasScheduledNextFrame = false;
345
341
  private volatile DownloadProgress latestDownloadProgress = null;
346
342
  private final DownloadProgressEventDispatcher progressEventDispatcher = new DownloadProgressEventDispatcher(
347
343
  new DownloadProgressEventDispatcher.EventEmitter() {
348
344
  @Override
349
345
  public void emit(DownloadProgress downloadProgress) {
350
- getReactApplicationContext()
351
- .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
352
- .emit(CodePushConstants.DOWNLOAD_PROGRESS_EVENT_NAME, downloadProgress.createWritableMap());
346
+ emitDownloadProgressEvent(downloadProgress);
353
347
  }
354
348
  });
355
349
 
@@ -390,7 +384,17 @@ public class CodePushNativeModule extends ReactContextBaseJavaModule {
390
384
  });
391
385
 
392
386
  JSONObject newPackage = mUpdateManager.getPackage(CodePushUtils.tryGetString(updatePackage, CodePushConstants.PACKAGE_HASH_KEY));
393
- promise.resolve(CodePushUtils.convertJsonObjectToWritable(newPackage));
387
+ WritableMap downloadedPackage = CodePushUtils.convertJsonObjectToWritable(newPackage);
388
+ if (binaryPatchResult != null) {
389
+ // The result describes this download rather than the update, so it is
390
+ // put on what this call resolves with and nowhere else: the metadata
391
+ // the update manager has already written knows nothing about it, and
392
+ // whoever asked for the download decides what the result is worth.
393
+ downloadedPackage.putMap(CodePushConstants.BINARY_PATCH_RESULT_KEY,
394
+ CodePushUtils.convertJsonObjectToWritable(binaryPatchResult));
395
+ }
396
+
397
+ promise.resolve(downloadedPackage);
394
398
  } catch (CodePushInvalidUpdateException e) {
395
399
  CodePushUtils.log(e);
396
400
  mSettingsManager.saveFailedUpdate(CodePushUtils.convertReadableToJsonObject(updatePackage));
@@ -399,12 +403,8 @@ public class CodePushNativeModule extends ReactContextBaseJavaModule {
399
403
  CodePushUtils.log(e);
400
404
  promise.reject(e);
401
405
  }
402
-
403
- return null;
404
406
  }
405
- };
406
-
407
- asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
407
+ });
408
408
  }
409
409
 
410
410
  @ReactMethod
@@ -429,16 +429,17 @@ public class CodePushNativeModule extends ReactContextBaseJavaModule {
429
429
  }
430
430
 
431
431
  @ReactMethod
432
- public void getUpdateMetadata(final int updateState, final Promise promise) {
433
- AsyncTask<Void, Void, Void> asyncTask = new AsyncTask<Void, Void, Void>() {
432
+ public void getUpdateMetadata(final double updateStateValue, final Promise promise) {
433
+ executeInBackground(new Runnable() {
434
434
  @Override
435
- protected Void doInBackground(Void... params) {
435
+ public void run() {
436
436
  try {
437
+ int updateState = (int) updateStateValue;
437
438
  JSONObject currentPackage = mUpdateManager.getCurrentPackage();
438
439
 
439
440
  if (currentPackage == null) {
440
441
  promise.resolve(null);
441
- return null;
442
+ return;
442
443
  }
443
444
 
444
445
  Boolean currentUpdateIsPending = false;
@@ -459,7 +460,7 @@ public class CodePushNativeModule extends ReactContextBaseJavaModule {
459
460
 
460
461
  if (previousPackage == null) {
461
462
  promise.resolve(null);
462
- return null;
463
+ return;
463
464
  }
464
465
 
465
466
  promise.resolve(CodePushUtils.convertJsonObjectToWritable(previousPackage));
@@ -488,19 +489,15 @@ public class CodePushNativeModule extends ReactContextBaseJavaModule {
488
489
  CodePushUtils.log(e);
489
490
  promise.reject(e);
490
491
  }
491
-
492
- return null;
493
492
  }
494
- };
495
-
496
- asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
493
+ });
497
494
  }
498
495
 
499
496
  @ReactMethod
500
497
  public void getNewStatusReport(final Promise promise) {
501
- AsyncTask<Void, Void, Void> asyncTask = new AsyncTask<Void, Void, Void>() {
498
+ executeInBackground(new Runnable() {
502
499
  @Override
503
- protected Void doInBackground(Void... params) {
500
+ public void run() {
504
501
  try {
505
502
  if (mCodePush.needToReportRollback()) {
506
503
  mCodePush.setNeedToReportRollback(false);
@@ -512,7 +509,7 @@ public class CodePushNativeModule extends ReactContextBaseJavaModule {
512
509
  WritableMap failedStatusReport = mTelemetryManager.getRollbackReport(lastFailedPackage);
513
510
  if (failedStatusReport != null) {
514
511
  promise.resolve(failedStatusReport);
515
- return null;
512
+ return;
516
513
  }
517
514
  } catch (JSONException e) {
518
515
  throw new CodePushUnknownException("Unable to read failed updates information stored in SharedPreferences.", e);
@@ -524,41 +521,40 @@ public class CodePushNativeModule extends ReactContextBaseJavaModule {
524
521
  WritableMap newPackageStatusReport = mTelemetryManager.getUpdateReport(CodePushUtils.convertJsonObjectToWritable(currentPackage));
525
522
  if (newPackageStatusReport != null) {
526
523
  promise.resolve(newPackageStatusReport);
527
- return null;
524
+ return;
528
525
  }
529
526
  }
530
527
  } else if (mCodePush.isRunningBinaryVersion()) {
531
528
  WritableMap newAppVersionStatusReport = mTelemetryManager.getBinaryUpdateReport(mCodePush.getAppVersion());
532
529
  if (newAppVersionStatusReport != null) {
533
530
  promise.resolve(newAppVersionStatusReport);
534
- return null;
531
+ return;
535
532
  }
536
533
  } else {
537
534
  WritableMap retryStatusReport = mTelemetryManager.getRetryStatusReport();
538
535
  if (retryStatusReport != null) {
539
536
  promise.resolve(retryStatusReport);
540
- return null;
537
+ return;
541
538
  }
542
539
  }
543
540
 
544
- promise.resolve("");
541
+ promise.resolve(null);
545
542
  } catch(CodePushUnknownException e) {
546
543
  CodePushUtils.log(e);
547
544
  promise.reject(e);
548
545
  }
549
- return null;
550
546
  }
551
- };
552
-
553
- asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
547
+ });
554
548
  }
555
549
 
556
550
  @ReactMethod
557
- public void installUpdate(final ReadableMap updatePackage, final int installMode, final int minimumBackgroundDuration, final Promise promise) {
558
- AsyncTask<Void, Void, Void> asyncTask = new AsyncTask<Void, Void, Void>() {
551
+ public void installUpdate(final ReadableMap updatePackage, final double installModeValue, final double minimumBackgroundDurationValue, final Promise promise) {
552
+ executeInBackground(new Runnable() {
559
553
  @Override
560
- protected Void doInBackground(Void... params) {
554
+ public void run() {
561
555
  try {
556
+ int installMode = (int) installModeValue;
557
+ int minimumBackgroundDuration = (int) minimumBackgroundDurationValue;
562
558
  mUpdateManager.installPackage(CodePushUtils.convertReadableToJsonObject(updatePackage), mSettingsManager.isPendingUpdate(null));
563
559
 
564
560
  String pendingHash = CodePushUtils.tryGetString(updatePackage, CodePushConstants.PACKAGE_HASH_KEY);
@@ -568,13 +564,12 @@ public class CodePushNativeModule extends ReactContextBaseJavaModule {
568
564
  mSettingsManager.savePendingUpdate(pendingHash, /* isLoading */false);
569
565
  }
570
566
 
571
- if (installMode == CodePushInstallMode.ON_NEXT_RESUME.getValue() ||
572
- // We also add the resume listener if the installMode is IMMEDIATE, because
573
- // if the current activity is backgrounded, we want to reload the bundle when
574
- // it comes back into the foreground.
575
- installMode == CodePushInstallMode.IMMEDIATE.getValue() ||
576
- installMode == CodePushInstallMode.ON_NEXT_SUSPEND.getValue()) {
577
-
567
+ if (installMode == CodePushInstallMode.ON_NEXT_RESUME.getValue()
568
+ // We also add the resume listener if the installMode is IMMEDIATE, because
569
+ // if the current activity is backgrounded, we want to reload the bundle when
570
+ // it comes back into the foreground.
571
+ || installMode == CodePushInstallMode.IMMEDIATE.getValue()
572
+ || installMode == CodePushInstallMode.ON_NEXT_SUSPEND.getValue()) {
578
573
  // Store the minimum duration on the native module as an instance
579
574
  // variable instead of relying on a closure below, so that any
580
575
  // subsequent resume-based installs could override it.
@@ -628,17 +623,13 @@ public class CodePushNativeModule extends ReactContextBaseJavaModule {
628
623
  }
629
624
  }
630
625
 
631
- promise.resolve("");
626
+ promise.resolve(null);
632
627
  } catch(CodePushUnknownException e) {
633
628
  CodePushUtils.log(e);
634
629
  promise.reject(e);
635
630
  }
636
-
637
- return null;
638
631
  }
639
- };
640
-
641
- asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
632
+ });
642
633
  }
643
634
 
644
635
  @ReactMethod
@@ -695,7 +686,7 @@ public class CodePushNativeModule extends ReactContextBaseJavaModule {
695
686
  public void notifyApplicationReady(Promise promise) {
696
687
  try {
697
688
  mSettingsManager.removePendingUpdate();
698
- promise.resolve("");
689
+ promise.resolve(null);
699
690
  } catch(CodePushUnknownException e) {
700
691
  CodePushUtils.log(e);
701
692
  promise.reject(e);
@@ -1,14 +1,17 @@
1
1
  package com.microsoft.codepush.react;
2
2
 
3
+ import android.content.Context;
3
4
  import android.os.Build;
4
5
 
5
6
  import org.json.JSONObject;
6
7
 
7
8
  import java.io.BufferedInputStream;
8
9
  import java.io.BufferedOutputStream;
10
+ import java.io.ByteArrayOutputStream;
9
11
  import java.io.File;
10
12
  import java.io.FileOutputStream;
11
13
  import java.io.IOException;
14
+ import java.io.InputStream;
12
15
  import java.net.HttpURLConnection;
13
16
  import java.net.MalformedURLException;
14
17
  import java.net.URL;
@@ -19,9 +22,15 @@ import javax.net.ssl.HttpsURLConnection;
19
22
  public class CodePushUpdateManager {
20
23
 
21
24
  private String mDocumentsDirectory;
25
+ private final CodePushBinaryPatch mBinaryPatch;
22
26
 
23
- public CodePushUpdateManager(String documentsDirectory) {
27
+ public CodePushUpdateManager(String documentsDirectory, Context context) {
28
+ this(documentsDirectory, new CodePushBinaryPatch(new AssetsBaseBundleProvider(context), new HDiffPatchNative()));
29
+ }
30
+
31
+ CodePushUpdateManager(String documentsDirectory, CodePushBinaryPatch binaryPatch) {
24
32
  mDocumentsDirectory = documentsDirectory;
33
+ mBinaryPatch = binaryPatch;
25
34
  }
26
35
 
27
36
  private String getDownloadFilePath() {
@@ -32,6 +41,10 @@ public class CodePushUpdateManager {
32
41
  return CodePushUtils.appendPathComponent(getCodePushPath(), CodePushConstants.UNZIPPED_FOLDER_NAME);
33
42
  }
34
43
 
44
+ private String getBinaryPatchFolderPath() {
45
+ return CodePushUtils.appendPathComponent(getCodePushPath(), CodePushConstants.BINARY_PATCH_FOLDER_NAME);
46
+ }
47
+
35
48
  private String getDocumentsDirectory() {
36
49
  return mDocumentsDirectory;
37
50
  }
@@ -143,8 +156,92 @@ public class CodePushUpdateManager {
143
156
  }
144
157
  }
145
158
 
146
- public void downloadPackage(JSONObject updatePackage, String expectedBundleFileName,
147
- DownloadProgressCallback progressCallback) throws IOException {
159
+ /**
160
+ * @return what the attempt at installing the update from its binary patch archive ended
161
+ * in, or null when the update had no patch archive to attempt. The result belongs
162
+ * to this download alone: the metadata written for the update never carries it.
163
+ */
164
+ public JSONObject downloadPackage(JSONObject updatePackage, String expectedBundleFileName,
165
+ DownloadProgressCallback progressCallback) throws IOException {
166
+ // A release that was published with a binary patch offers two archives of the same
167
+ // update. The patch is worth trying because it is a fraction of the size, and the
168
+ // full archive is always there when it does not work out.
169
+ String binaryPatchDownloadUrl = updatePackage.optString(CodePushConstants.BINARY_PATCH_DOWNLOAD_URL_KEY, null);
170
+ BinaryPatchAttempt patchAttempt = null;
171
+ if (binaryPatchDownloadUrl != null) {
172
+ patchAttempt = new BinaryPatchAttempt();
173
+ if (tryDownloadBinaryPatchPackage(updatePackage, expectedBundleFileName, progressCallback,
174
+ binaryPatchDownloadUrl, patchAttempt)) {
175
+ return patchAttempt.result();
176
+ }
177
+ }
178
+
179
+ downloadAndInstallPackage(updatePackage, expectedBundleFileName, progressCallback,
180
+ updatePackage.optString(CodePushConstants.DOWNLOAD_URL_KEY, null), false, null);
181
+
182
+ return patchAttempt == null ? null : patchAttempt.result();
183
+ }
184
+
185
+ /**
186
+ * Installs the update from its binary patch archive.
187
+ *
188
+ * Every way this can fail ends the same way, with the update being downloaded in full
189
+ * instead, so none of them is reported to the caller as an error. The fallback happens
190
+ * exactly once without anything having to count it: the full archive is downloaded by
191
+ * a call that is not allowed to take the patch path, so it has no failure of its own to
192
+ * fall back from.
193
+ *
194
+ * @param patchAttempt records what the attempt ended in, for the caller to hand to
195
+ * whoever asked for the download
196
+ * @return true when the update was installed, false when the caller has to download the
197
+ * full archive instead
198
+ */
199
+ private boolean tryDownloadBinaryPatchPackage(JSONObject updatePackage, String expectedBundleFileName,
200
+ DownloadProgressCallback progressCallback,
201
+ String binaryPatchDownloadUrl, BinaryPatchAttempt patchAttempt) {
202
+ try {
203
+ BinaryPatchResult patchResult = downloadAndInstallPackage(updatePackage, expectedBundleFileName,
204
+ progressCallback, binaryPatchDownloadUrl, true, patchAttempt);
205
+ if (patchResult.succeeded()) {
206
+ return true;
207
+ }
208
+
209
+ patchAttempt.recordFallback(patchResult.getFailureReason());
210
+ CodePushUtils.log("Binary patch update failed (" + patchResult.getFailureReason()
211
+ + "). Downloading the full update instead.");
212
+ } catch (Exception | OutOfMemoryError e) {
213
+ // Applying a patch is the one path that holds a whole bundle in memory, so
214
+ // running out of it is a failure this has to absorb like any other: by the time
215
+ // it lands here the arrays are unreachable, and the full archive is downloaded
216
+ // to disk in chunks rather than held.
217
+ patchAttempt.recordFallbackAfterError();
218
+ CodePushUtils.log(e);
219
+ CodePushUtils.log("The binary patch update could not be completed. Downloading the full update instead.");
220
+ } finally {
221
+ FileUtils.deleteDirectoryAtPath(getBinaryPatchFolderPath());
222
+ }
223
+
224
+ return false;
225
+ }
226
+
227
+ /**
228
+ * Downloads an update from one of its archives and installs it.
229
+ *
230
+ * @param isBinaryPatchUpdate whether the archive holds a binary patch of the JS bundle,
231
+ * which has to be applied before the contents are the update.
232
+ * Only an archive downloaded from the binary patch URL is
233
+ * treated that way, so an update being downloaded in full can
234
+ * never end up on the patch path.
235
+ * @param patchAttempt the record the patch is timed into, or null for a full download,
236
+ * which has no patch to time
237
+ * @return the outcome of the patch: a failed result means the update was not installed
238
+ * and the caller has to fall back to the full archive. Downloading the full
239
+ * archive always succeeds or throws.
240
+ */
241
+ BinaryPatchResult downloadAndInstallPackage(JSONObject updatePackage, String expectedBundleFileName,
242
+ DownloadProgressCallback progressCallback,
243
+ String downloadUrlString, boolean isBinaryPatchUpdate,
244
+ BinaryPatchAttempt patchAttempt) throws IOException {
148
245
  String newUpdateHash = updatePackage.optString(CodePushConstants.PACKAGE_HASH_KEY, null);
149
246
  String newUpdateFolderPath = getPackageFolderPath(newUpdateHash);
150
247
  String newUpdateMetadataPath = CodePushUtils.appendPathComponent(newUpdateFolderPath, CodePushConstants.PACKAGE_FILE_NAME);
@@ -154,7 +251,6 @@ public class CodePushUpdateManager {
154
251
  FileUtils.deleteDirectoryAtPath(newUpdateFolderPath);
155
252
  }
156
253
 
157
- String downloadUrlString = updatePackage.optString(CodePushConstants.DOWNLOAD_URL_KEY, null);
158
254
  HttpURLConnection connection = null;
159
255
  BufferedInputStream bin = null;
160
256
  FileOutputStream fos = null;
@@ -232,6 +328,21 @@ public class CodePushUpdateManager {
232
328
  FileUtils.unzipFile(downloadFile, unzippedFolderPath);
233
329
  FileUtils.deleteFileOrFolderSilently(downloadFile);
234
330
 
331
+ // Rebuild the JS bundle the archive only carries a patch of, which leaves the
332
+ // contents identical to the ones the full archive would have delivered.
333
+ if (isBinaryPatchUpdate) {
334
+ long patchStartTime = System.currentTimeMillis();
335
+ BinaryPatchResult patchResult = mBinaryPatch.restoreBundle(unzippedFolderPath,
336
+ getBinaryPatchFolderPath(), expectedBundleFileName);
337
+ if (!patchResult.succeeded()) {
338
+ return patchResult;
339
+ }
340
+
341
+ long applyDurationMs = System.currentTimeMillis() - patchStartTime;
342
+ patchAttempt.recordBundleRestored(applyDurationMs);
343
+ CodePushUtils.log("Restored the update from its binary patch in " + applyDurationMs + " ms.");
344
+ }
345
+
235
346
  // Merge contents with current update based on the manifest
236
347
  String diffManifestFilePath = CodePushUtils.appendPathComponent(unzippedFolderPath,
237
348
  CodePushConstants.DIFF_MANIFEST_FILE_NAME);
@@ -268,12 +379,22 @@ public class CodePushUpdateManager {
268
379
  CodePushUtils.setJSONValueForKey(updatePackage, CodePushConstants.RELATIVE_BUNDLE_PATH_KEY, relativeBundlePath);
269
380
  }
270
381
  } else {
382
+ if (isBinaryPatchUpdate) {
383
+ // Whatever the patch URL served, it is not a patch archive - an error page
384
+ // answered with a 200 looks like this too. Moving it into place would
385
+ // install bytes no hash has ever been checked against, so the full archive
386
+ // is downloaded instead.
387
+ return BinaryPatchResult.failure(BinaryPatchResult.REASON_INVALID_MANIFEST);
388
+ }
389
+
271
390
  // File is a jsbundle, move it to a folder with the packageHash as its name
272
391
  FileUtils.moveFile(downloadFile, newUpdateFolderPath, expectedBundleFileName);
273
392
  }
274
393
 
275
394
  // Save metadata to the folder.
276
395
  CodePushUtils.writeJsonToFile(updatePackage, newUpdateMetadataPath);
396
+
397
+ return BinaryPatchResult.success();
277
398
  }
278
399
 
279
400
  public void installPackage(JSONObject updatePackage, boolean removePendingUpdate) {
@@ -349,4 +470,38 @@ public class CodePushUpdateManager {
349
470
  public void clearUpdates() {
350
471
  FileUtils.deleteDirectoryAtPath(getCodePushPath());
351
472
  }
473
+
474
+ /** Reads the JS bundle that shipped inside the app binary out of the APK's assets. */
475
+ private static class AssetsBaseBundleProvider implements CodePushBinaryPatch.BaseBundleProvider {
476
+
477
+ private final Context mContext;
478
+
479
+ AssetsBaseBundleProvider(Context context) {
480
+ mContext = context;
481
+ }
482
+
483
+ @Override
484
+ public byte[] readBaseBundle(String bundleFileName) throws IOException {
485
+ // The bundle is stored uncompressed in the APK, so it is read straight into
486
+ // memory: a copy on disk would buy nothing and cost the space the restored
487
+ // bundle needs.
488
+ InputStream assetStream = mContext.getAssets().open(bundleFileName);
489
+ try {
490
+ // An uncompressed asset knows its whole length up front, so the buffer is
491
+ // sized for it: growing one would repeatedly hold two copies of a bundle
492
+ // that is already the largest allocation on this path.
493
+ ByteArrayOutputStream bundleBytes = new ByteArrayOutputStream(
494
+ Math.max(assetStream.available(), CodePushConstants.DOWNLOAD_BUFFER_SIZE));
495
+ byte[] buffer = new byte[CodePushConstants.DOWNLOAD_BUFFER_SIZE];
496
+ int bytesRead;
497
+ while ((bytesRead = assetStream.read(buffer)) > 0) {
498
+ bundleBytes.write(buffer, 0, bytesRead);
499
+ }
500
+
501
+ return bundleBytes.toByteArray();
502
+ } finally {
503
+ assetStream.close();
504
+ }
505
+ }
506
+ }
352
507
  }
@@ -86,6 +86,19 @@ public class CodePushUpdateUtils {
86
86
  return String.format("%064x", new java.math.BigInteger(1, hash));
87
87
  }
88
88
 
89
+ /**
90
+ * SHA-256 of a single file's bytes, as lowercase hex - the way a binary patch manifest
91
+ * records the bundle it was computed against and the bundle it produces.
92
+ */
93
+ public static String computeHashForFile(File file) throws IOException {
94
+ return computeHash(new FileInputStream(file));
95
+ }
96
+
97
+ /** SHA-256 of the bytes in memory, in the same form as {@link #computeHashForFile}. */
98
+ public static String computeHashForBytes(byte[] data) {
99
+ return computeHash(new ByteArrayInputStream(data));
100
+ }
101
+
89
102
  public static void copyNecessaryFilesFromCurrentPackage(String diffManifestFilePath, String currentPackageFolderPath, String newPackageFolderPath) throws IOException {
90
103
  if (currentPackageFolderPath == null || !new File(currentPackageFolderPath).exists()) {
91
104
  CodePushUtils.log("Unable to copy files from current package during diff update, because currentPackageFolderPath is invalid.");
@@ -0,0 +1,52 @@
1
+ package com.microsoft.codepush.react;
2
+
3
+ /**
4
+ * The native HDiffPatch applier, which is what actually turns the bundle inside the app
5
+ * binary into the bundle an update wants to run.
6
+ *
7
+ * The library is loaded on first use rather than when the class is loaded, and a load
8
+ * failure is reported as a result code instead of an error: an app whose build did not
9
+ * produce the library still has to install its updates, it just has to download them in
10
+ * full.
11
+ */
12
+ public class HDiffPatchNative implements CodePushBinaryPatch.PatchApplier {
13
+
14
+ private static final String LIBRARY_NAME = "codepush-binarypatch";
15
+
16
+ private static boolean sLibraryLoadAttempted = false;
17
+ private static boolean sLibraryLoaded = false;
18
+
19
+ @Override
20
+ public int apply(byte[] base, byte[] patch, String outputPath, long expectedTargetSize) {
21
+ if (!loadLibrary()) {
22
+ return RESULT_LIBRARY_UNAVAILABLE;
23
+ }
24
+
25
+ return applyPatch(base, patch, outputPath, expectedTargetSize);
26
+ }
27
+
28
+ private static synchronized boolean loadLibrary() {
29
+ if (!sLibraryLoadAttempted) {
30
+ sLibraryLoadAttempted = true;
31
+ try {
32
+ System.loadLibrary(LIBRARY_NAME);
33
+ sLibraryLoaded = true;
34
+ } catch (UnsatisfiedLinkError e) {
35
+ CodePushUtils.log("Unable to load the binary patch library: " + e.getMessage());
36
+ }
37
+ }
38
+
39
+ return sLibraryLoaded;
40
+ }
41
+
42
+ /**
43
+ * Writes the bundle that `patch` produces from `base` to `outputPath`.
44
+ *
45
+ * The base bundle and the patch are passed as arrays because the applier needs random
46
+ * access to both, while the restored bundle is written straight to a file, so the two
47
+ * bundles are never in memory at the same time.
48
+ *
49
+ * @return one of the RESULT_* codes of {@link CodePushBinaryPatch.PatchApplier}
50
+ */
51
+ private static native int applyPatch(byte[] base, byte[] patch, String outputPath, long expectedTargetSize);
52
+ }