@bravemobile/react-native-code-push 13.1.0-beta.3 → 13.1.0-beta.5

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 (46) hide show
  1. package/CodePush.podspec +5 -0
  2. package/README.md +20 -145
  3. package/android/app/src/main/java/com/microsoft/codepush/react/ArchiveAttemptLog.java +181 -0
  4. package/android/app/src/main/java/com/microsoft/codepush/react/{BinaryPatchResult.java → ArchiveRestoreResult.java} +17 -7
  5. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushBinaryPatch.java +20 -20
  6. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushConstants.java +20 -1
  7. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushErrorCode.java +86 -0
  8. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushHttpException.java +29 -0
  9. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushIncompleteDownloadException.java +17 -0
  10. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java +12 -7
  11. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateManager.java +143 -43
  12. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushBinaryPatchTest.java +31 -31
  13. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushErrorCodeTest.java +90 -0
  14. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerBinaryPatchTest.java +159 -19
  15. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerDownloadTest.java +304 -15
  16. package/cli/commands/createHistoryCommand/createReleaseHistory.test.ts +133 -0
  17. package/cli/commands/createHistoryCommand/createReleaseHistory.ts +3 -11
  18. package/cli/commands/releaseCommand/addToReleaseHistory.ts +3 -11
  19. package/cli/commands/releaseCommand/release.test.ts +55 -13
  20. package/cli/commands/releaseCommand/release.ts +39 -6
  21. package/cli/commands/updateHistoryCommand/updateReleaseHistory.ts +3 -11
  22. package/cli/dist/commands/createHistoryCommand/createReleaseHistory.js +2 -8
  23. package/cli/dist/commands/createHistoryCommand/createReleaseHistory.test.js +95 -0
  24. package/cli/dist/commands/releaseCommand/addToReleaseHistory.js +2 -8
  25. package/cli/dist/commands/releaseCommand/release.js +23 -8
  26. package/cli/dist/commands/releaseCommand/release.test.js +37 -9
  27. package/cli/dist/commands/updateHistoryCommand/updateReleaseHistory.js +2 -8
  28. package/cli/dist/functions/resolveAssetDiffBases.js +7 -2
  29. package/cli/dist/functions/resolveAssetDiffBases.test.js +26 -12
  30. package/cli/dist/functions/stageReleaseHistoryFile.js +28 -0
  31. package/cli/functions/resolveAssetDiffBases.test.ts +28 -14
  32. package/cli/functions/resolveAssetDiffBases.ts +8 -1
  33. package/cli/functions/stageReleaseHistoryFile.ts +38 -0
  34. package/ios/CodePush/CodePush.h +6 -3
  35. package/ios/CodePush/CodePush.mm +10 -10
  36. package/ios/CodePush/CodePushBinaryPatch.h +18 -8
  37. package/ios/CodePush/CodePushBinaryPatch.m +23 -22
  38. package/ios/CodePush/CodePushDownloadHandler.m +2 -1
  39. package/ios/CodePush/CodePushErrorUtils.m +55 -0
  40. package/ios/CodePush/CodePushPackage.m +242 -71
  41. package/package.json +2 -1
  42. package/src/CodePush.js +28 -18
  43. package/src/CodePush.test.js +110 -55
  44. package/src/package-mixins.js +19 -10
  45. package/typings/react-native-code-push.d.ts +152 -41
  46. package/android/app/src/main/java/com/microsoft/codepush/react/BinaryPatchAttempt.java +0 -92
@@ -0,0 +1,86 @@
1
+ package com.microsoft.codepush.react;
2
+
3
+ import java.net.SocketException;
4
+ import java.net.SocketTimeoutException;
5
+ import java.net.UnknownHostException;
6
+
7
+ import javax.net.ssl.SSLException;
8
+
9
+ /**
10
+ * What kind of failure a download ended in, in the words JS reads it by.
11
+ *
12
+ * React Native fills the code of a promise rejected with a bare throwable with
13
+ * `EUNSPECIFIED`, which left the message the only thing telling one failure apart from
14
+ * another - and a message is written for a person, not for a report to group by.
15
+ *
16
+ * The categories are the ones that ask for different things to happen next, which is why
17
+ * there are four of them rather than one per way a download can fail. A category no
18
+ * caller would act differently on is a category that only makes the reports wider.
19
+ */
20
+ public class CodePushErrorCode {
21
+
22
+ /** The network did not carry the download: the socket dropped, timed out, or never opened. */
23
+ public static final String NETWORK = "CODE_PUSH_NETWORK";
24
+
25
+ /** The server answered, with a status that is not a body to install. */
26
+ public static final String HTTP = "CODE_PUSH_HTTP";
27
+
28
+ /**
29
+ * The downloaded contents do not hash to the release's package hash, or hold no JS
30
+ * bundle by the name the app looks for. Downloading them again cannot help.
31
+ */
32
+ public static final String INTEGRITY = "CODE_PUSH_INTEGRITY";
33
+
34
+ /** Nothing here has a word for it, and inventing one would only be a guess. */
35
+ public static final String UNKNOWN = "CODE_PUSH_UNKNOWN";
36
+
37
+ private CodePushErrorCode() {
38
+ }
39
+
40
+ /**
41
+ * The category of a failure, read through its causes: the download wraps some of what it
42
+ * catches, and the wrapper is never the part that says what went wrong.
43
+ */
44
+ public static String of(Throwable error) {
45
+ for (Throwable cause = error; cause != null; cause = cause.getCause()) {
46
+ if (cause instanceof CodePushHttpException) {
47
+ return HTTP;
48
+ }
49
+
50
+ if (cause instanceof CodePushInvalidUpdateException) {
51
+ return INTEGRITY;
52
+ }
53
+
54
+ if (isTransportFailure(cause)) {
55
+ return NETWORK;
56
+ }
57
+ }
58
+
59
+ return UNKNOWN;
60
+ }
61
+
62
+ /**
63
+ * Whether the download failed because the network did not carry it.
64
+ *
65
+ * A server that answered is not this, however it answered: the connection worked, and
66
+ * asking a different URL over it is worth doing. A connection that never opened or that
67
+ * dropped is, and every URL behind it is equally out of reach.
68
+ */
69
+ public static boolean isNetworkFailure(Throwable error) {
70
+ return NETWORK.equals(of(error));
71
+ }
72
+
73
+ private static boolean isTransportFailure(Throwable error) {
74
+ // `SocketException` is the one that covers the reported majority - the connection
75
+ // reset and the connection aborted an app being backgrounded mid-download leaves
76
+ // behind - along with the connection that was refused or had no route.
77
+ //
78
+ // A download that stopped short belongs here too. The socket raised nothing for it,
79
+ // so only the byte count says the network dropped the rest of the body.
80
+ return error instanceof SocketTimeoutException
81
+ || error instanceof SocketException
82
+ || error instanceof UnknownHostException
83
+ || error instanceof SSLException
84
+ || error instanceof CodePushIncompleteDownloadException;
85
+ }
86
+ }
@@ -0,0 +1,29 @@
1
+ package com.microsoft.codepush.react;
2
+
3
+ import java.io.IOException;
4
+
5
+ /**
6
+ * The server answered a download with a status that is not a body to install.
7
+ *
8
+ * An I/O exception rather than one of this package's unchecked ones, because every caller
9
+ * of a download already handles `IOException` and this is one more way a download does not
10
+ * arrive - and because the alternative, an unchecked exception, would escape the download
11
+ * path uncaught.
12
+ *
13
+ * The message reads the way the other platform's does, so one release answered with the
14
+ * same status reads the same in both platforms' reports.
15
+ */
16
+ public class CodePushHttpException extends IOException {
17
+
18
+ private final int mStatusCode;
19
+
20
+ public CodePushHttpException(String url, int statusCode) {
21
+ super("Received " + statusCode + " response from " + url);
22
+ mStatusCode = statusCode;
23
+ }
24
+
25
+ /** The status the server answered with. */
26
+ public int getStatusCode() {
27
+ return mStatusCode;
28
+ }
29
+ }
@@ -0,0 +1,17 @@
1
+ package com.microsoft.codepush.react;
2
+
3
+ import java.io.IOException;
4
+
5
+ /**
6
+ * The response stopped before it had delivered the length it declared.
7
+ *
8
+ * A type of its own rather than one more unnamed I/O error, because this is a network
9
+ * failure the socket never raised one for: the bytes simply stopped arriving, and only the
10
+ * count says so. Everywhere a network failure is acted on has to act on this one too.
11
+ */
12
+ public class CodePushIncompleteDownloadException extends IOException {
13
+
14
+ public CodePushIncompleteDownloadException(long receivedBytes, long declaredBytes) {
15
+ super("Received " + receivedBytes + " bytes, expected " + declaredBytes);
16
+ }
17
+ }
@@ -336,7 +336,7 @@ public class CodePushNativeModule extends NativeCodePushSpec {
336
336
  public void run() {
337
337
  try {
338
338
  JSONObject mutableUpdatePackage = CodePushUtils.convertReadableToJsonObject(updatePackage);
339
- JSONObject binaryPatchResult = mUpdateManager.downloadPackage(mutableUpdatePackage, mCodePush.getAssetsBundleFileName(), new DownloadProgressCallback() {
339
+ JSONObject updateArchiveResult = mUpdateManager.downloadPackage(mutableUpdatePackage, mCodePush.getAssetsBundleFileName(), new DownloadProgressCallback() {
340
340
  private volatile boolean hasScheduledNextFrame = false;
341
341
  private final DownloadProgressEventDispatcher progressEventDispatcher = new DownloadProgressEventDispatcher(
342
342
  new DownloadProgressEventDispatcher.EventEmitter() {
@@ -389,23 +389,28 @@ public class CodePushNativeModule extends NativeCodePushSpec {
389
389
 
390
390
  JSONObject newPackage = mUpdateManager.getPackage(CodePushUtils.tryGetString(updatePackage, CodePushConstants.PACKAGE_HASH_KEY));
391
391
  WritableMap downloadedPackage = CodePushUtils.convertJsonObjectToWritable(newPackage);
392
- if (binaryPatchResult != null) {
392
+ if (updateArchiveResult != null) {
393
393
  // The result describes this download rather than the update, so it is
394
394
  // put on what this call resolves with and nowhere else: the metadata
395
395
  // the update manager has already written knows nothing about it, and
396
396
  // whoever asked for the download decides what the result is worth.
397
- downloadedPackage.putMap(CodePushConstants.BINARY_PATCH_RESULT_KEY,
398
- CodePushUtils.convertJsonObjectToWritable(binaryPatchResult));
397
+ downloadedPackage.putMap(CodePushConstants.UPDATE_ARCHIVE_RESULT_KEY,
398
+ CodePushUtils.convertJsonObjectToWritable(updateArchiveResult));
399
399
  }
400
400
 
401
401
  promise.resolve(downloadedPackage);
402
402
  } catch (CodePushInvalidUpdateException e) {
403
403
  CodePushUtils.log(e);
404
404
  mSettingsManager.saveFailedUpdate(CodePushUtils.convertReadableToJsonObject(updatePackage));
405
- promise.reject(e);
406
- } catch (IOException | CodePushUnknownException e) {
405
+ promise.reject(CodePushErrorCode.of(e), e.getMessage(), e);
406
+ } catch (Throwable e) {
407
+ // Anything at all, because a promise is waiting on this. A failure that
408
+ // leaves this method without settling it leaves JS waiting on a download
409
+ // that is no longer running, with nothing to time it out. The narrower
410
+ // catch this replaces let two through: a download URL that is not a URL,
411
+ // and an archive naming a path outside the folder it unpacks into.
407
412
  CodePushUtils.log(e);
408
- promise.reject(e);
413
+ promise.reject(CodePushErrorCode.of(e), e.getMessage(), e);
409
414
  }
410
415
  }
411
416
  });
@@ -157,20 +157,46 @@ public class CodePushUpdateManager {
157
157
  }
158
158
 
159
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
160
+ * @return what the attempts at installing the update from its patch archives ended in,
161
+ * or null when the update had no such archive to attempt. The result belongs
162
162
  * to this download alone: the metadata written for the update never carries it.
163
163
  */
164
164
  public JSONObject downloadPackage(JSONObject updatePackage, String expectedBundleFileName,
165
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,
166
+ // A release that was published with a binary patch offers up to three archives of
167
+ // the same update. The asset diff is the smallest and is tried first, the patch
168
+ // archive stands in when the diff fails on its asset side, and the full archive is
169
+ // always there when none of it works out.
170
+ String binaryPatchDownloadUrl = optArchiveDownloadUrl(updatePackage, CodePushConstants.BINARY_PATCH_DOWNLOAD_URL_KEY);
171
+ String assetDiffDownloadUrl = optArchiveDownloadUrl(updatePackage, CodePushConstants.ASSET_DIFF_DOWNLOAD_URL_KEY);
172
+
173
+ ArchiveAttemptLog patchAttempt = null;
174
+ boolean patchArchiveWorthTrying = binaryPatchDownloadUrl != null;
175
+ if (assetDiffDownloadUrl != null) {
176
+ patchAttempt = new ArchiveAttemptLog();
177
+ patchAttempt.beginAttempt(ArchiveAttemptLog.ARCHIVE_ASSET_DIFF);
178
+ if (tryDownloadArchivePackage(updatePackage, expectedBundleFileName, progressCallback,
179
+ assetDiffDownloadUrl, patchAttempt)) {
180
+ return patchAttempt.result();
181
+ }
182
+
183
+ // The patch archive is worth trying when nothing about how the diff failed
184
+ // implicates it. A diff that failed after restoring its bundle failed on its
185
+ // asset side, which the patch archive does not share; a diff the server never
186
+ // served is a verdict on one URL, and the patch archive is at another. Anything
187
+ // else failed in the bundle patch both archives carry byte for byte, so the
188
+ // patch archive would fail the same way and trying it would only put a second
189
+ // doomed download in front of the full one.
190
+ patchArchiveWorthTrying = patchArchiveWorthTrying
191
+ && (patchAttempt.currentAttemptRestoredBundle() || patchAttempt.currentAttemptWasNotServed());
192
+ }
193
+
194
+ if (patchArchiveWorthTrying) {
195
+ if (patchAttempt == null) {
196
+ patchAttempt = new ArchiveAttemptLog();
197
+ }
198
+ patchAttempt.beginAttempt(ArchiveAttemptLog.ARCHIVE_BINARY_PATCH);
199
+ if (tryDownloadArchivePackage(updatePackage, expectedBundleFileName, progressCallback,
174
200
  binaryPatchDownloadUrl, patchAttempt)) {
175
201
  return patchAttempt.result();
176
202
  }
@@ -183,40 +209,71 @@ public class CodePushUpdateManager {
183
209
  }
184
210
 
185
211
  /**
186
- * Installs the update from its binary patch archive.
212
+ * @return the URL one of the update's archives is offered at, or null when the update
213
+ * does not offer that archive. An empty string is not a URL: it stands for the
214
+ * same absent archive as a missing key, which is what iOS makes of it too.
215
+ */
216
+ private static String optArchiveDownloadUrl(JSONObject updatePackage, String downloadUrlKey) {
217
+ String downloadUrl = updatePackage.optString(downloadUrlKey, null);
218
+ return downloadUrl == null || downloadUrl.isEmpty() ? null : downloadUrl;
219
+ }
220
+
221
+ /**
222
+ * Installs the update from one of its patch archives.
187
223
  *
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.
224
+ * Every verdict on the archive ends the same way, with the caller moving on to the next
225
+ * one, so none of it is reported to the caller as an error. A network that did not carry
226
+ * the archive is not a verdict on it and is raised instead. The ladder cannot loop:
227
+ * which archive comes next is the caller's decision alone, and the full archive at its
228
+ * end is downloaded by a call that is not allowed to take the patch path, so it has no
229
+ * failure of its own to fall back from.
193
230
  *
194
231
  * @param patchAttempt records what the attempt ended in, for the caller to hand to
195
232
  * 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
233
+ * @return true when the update was installed, false when the caller has to move on to
234
+ * the next archive
235
+ * @throws IOException when the network did not carry the archive, which is not a verdict
236
+ * on the archive and so is not a reason to try another one
198
237
  */
199
- private boolean tryDownloadBinaryPatchPackage(JSONObject updatePackage, String expectedBundleFileName,
200
- DownloadProgressCallback progressCallback,
201
- String binaryPatchDownloadUrl, BinaryPatchAttempt patchAttempt) {
238
+ private boolean tryDownloadArchivePackage(JSONObject updatePackage, String expectedBundleFileName,
239
+ DownloadProgressCallback progressCallback,
240
+ String archiveDownloadUrl, ArchiveAttemptLog patchAttempt)
241
+ throws IOException {
202
242
  try {
203
- BinaryPatchResult patchResult = downloadAndInstallPackage(updatePackage, expectedBundleFileName,
204
- progressCallback, binaryPatchDownloadUrl, true, patchAttempt);
243
+ ArchiveRestoreResult patchResult = downloadAndInstallPackage(updatePackage, expectedBundleFileName,
244
+ progressCallback, archiveDownloadUrl, true, patchAttempt);
205
245
  if (patchResult.succeeded()) {
206
246
  return true;
207
247
  }
208
248
 
209
249
  patchAttempt.recordFallback(patchResult.getFailureReason());
210
- CodePushUtils.log("Binary patch update failed (" + patchResult.getFailureReason()
211
- + "). Downloading the full update instead.");
250
+ CodePushUtils.log("The " + patchAttempt.currentArchive() + " archive failed ("
251
+ + patchResult.getFailureReason() + "). Falling back.");
212
252
  } catch (Exception | OutOfMemoryError e) {
253
+ if (CodePushErrorCode.isNetworkFailure(e)) {
254
+ // The network is what failed, not the archive, and the full archive is behind
255
+ // the same network - only larger, and started over from nothing. Falling back
256
+ // here would spend a second download to reach the failure already in hand.
257
+ CodePushUtils.log("The " + patchAttempt.currentArchive()
258
+ + " archive could not be downloaded. Giving up on the download.");
259
+ if (e instanceof IOException) {
260
+ throw (IOException) e;
261
+ }
262
+
263
+ // A network failure read out of a wrapper this package raised, which the
264
+ // caller catches by its own type rather than by `IOException`.
265
+ throw new CodePushUnknownException(
266
+ "The " + patchAttempt.currentArchive() + " archive could not be downloaded.", e);
267
+ }
268
+
213
269
  // Applying a patch is the one path that holds a whole bundle in memory, so
214
270
  // running out of it is a failure this has to absorb like any other: by the time
215
271
  // it lands here the arrays are unreachable, and the full archive is downloaded
216
272
  // to disk in chunks rather than held.
217
- patchAttempt.recordFallbackAfterError();
273
+ patchAttempt.recordFallbackAfterError(e);
218
274
  CodePushUtils.log(e);
219
- CodePushUtils.log("The binary patch update could not be completed. Downloading the full update instead.");
275
+ CodePushUtils.log("The " + patchAttempt.currentArchive()
276
+ + " archive could not be applied. Falling back.");
220
277
  } finally {
221
278
  FileUtils.deleteDirectoryAtPath(getBinaryPatchFolderPath());
222
279
  }
@@ -229,19 +286,19 @@ public class CodePushUpdateManager {
229
286
  *
230
287
  * @param isBinaryPatchUpdate whether the archive holds a binary patch of the JS bundle,
231
288
  * 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.
289
+ * Both the asset diff and the patch archive are downloaded
290
+ * that way; the full archive never is, so an update being
291
+ * downloaded in full can never end up on the patch path.
235
292
  * @param patchAttempt the record the patch is timed into, or null for a full download,
236
293
  * which has no patch to time
237
294
  * @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
295
+ * and the caller has to move on to the next archive. Downloading the full
239
296
  * archive always succeeds or throws.
240
297
  */
241
- BinaryPatchResult downloadAndInstallPackage(JSONObject updatePackage, String expectedBundleFileName,
298
+ ArchiveRestoreResult downloadAndInstallPackage(JSONObject updatePackage, String expectedBundleFileName,
242
299
  DownloadProgressCallback progressCallback,
243
300
  String downloadUrlString, boolean isBinaryPatchUpdate,
244
- BinaryPatchAttempt patchAttempt) throws IOException {
301
+ ArchiveAttemptLog patchAttempt) throws IOException {
245
302
  String newUpdateHash = updatePackage.optString(CodePushConstants.PACKAGE_HASH_KEY, null);
246
303
  String newUpdateFolderPath = getPackageFolderPath(newUpdateHash);
247
304
  String newUpdateMetadataPath = CodePushUtils.appendPathComponent(newUpdateFolderPath, CodePushConstants.PACKAGE_FILE_NAME);
@@ -262,6 +319,8 @@ public class CodePushUpdateManager {
262
319
  try {
263
320
  URL downloadUrl = new URL(downloadUrlString);
264
321
  connection = (HttpURLConnection) (downloadUrl.openConnection());
322
+ connection.setConnectTimeout(CodePushConstants.DOWNLOAD_CONNECT_TIMEOUT_IN_MS);
323
+ connection.setReadTimeout(CodePushConstants.DOWNLOAD_READ_TIMEOUT_IN_MS);
265
324
 
266
325
  if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP &&
267
326
  downloadUrl.toString().startsWith("https")) {
@@ -273,6 +332,15 @@ public class CodePushUpdateManager {
273
332
  }
274
333
 
275
334
  connection.setRequestProperty("Accept-Encoding", "identity");
335
+
336
+ // Read before the body is: an error status carries a body of its own, and
337
+ // asking `getInputStream()` for it first turns some statuses into a stream and
338
+ // others into an exception that says nothing about which status it was.
339
+ int responseCode = connection.getResponseCode();
340
+ if (responseCode >= 400) {
341
+ throw new CodePushHttpException(downloadUrlString, responseCode);
342
+ }
343
+
276
344
  bin = new BufferedInputStream(connection.getInputStream());
277
345
 
278
346
  // Announced only once the response is flowing, so a connection that fails to
@@ -313,8 +381,11 @@ public class CodePushUpdateManager {
313
381
  progressCallback.call(new DownloadProgress(totalBytes, receivedBytes));
314
382
  }
315
383
 
316
- if (totalBytes != receivedBytes) {
317
- throw new CodePushUnknownException("Received " + receivedBytes + " bytes, expected " + totalBytes);
384
+ // Only against a length the server declared. `getContentLength()` answers -1
385
+ // for a body sent without one, which no read total matches - so comparing anyway
386
+ // would fail every download a server chooses to send that way.
387
+ if (totalBytes >= 0 && totalBytes != receivedBytes) {
388
+ throw new CodePushIncompleteDownloadException(receivedBytes, totalBytes);
318
389
  }
319
390
 
320
391
  isZip = ByteBuffer.wrap(header).getInt() == 0x504b0304;
@@ -341,7 +412,7 @@ public class CodePushUpdateManager {
341
412
  // contents identical to the ones the full archive would have delivered.
342
413
  if (isBinaryPatchUpdate) {
343
414
  long patchStartTime = System.currentTimeMillis();
344
- BinaryPatchResult patchResult = mBinaryPatch.restoreBundle(unzippedFolderPath,
415
+ ArchiveRestoreResult patchResult = mBinaryPatch.restoreBundle(unzippedFolderPath,
345
416
  getBinaryPatchFolderPath(), expectedBundleFileName);
346
417
  if (!patchResult.succeeded()) {
347
418
  return patchResult;
@@ -357,10 +428,31 @@ public class CodePushUpdateManager {
357
428
  CodePushConstants.DIFF_MANIFEST_FILE_NAME);
358
429
  boolean isDiffUpdate = FileUtils.fileAtPathExists(diffManifestFilePath);
359
430
  if (isDiffUpdate) {
360
- String currentPackageFolderPath = getCurrentPackageFolderPath();
361
- CodePushUpdateUtils.copyNecessaryFilesFromCurrentPackage(diffManifestFilePath, currentPackageFolderPath, newUpdateFolderPath);
362
- File diffManifestFile = new File(diffManifestFilePath);
363
- diffManifestFile.delete();
431
+ try {
432
+ String currentPackageFolderPath = getCurrentPackageFolderPath();
433
+ if (currentPackageFolderPath != null && !FileUtils.fileAtPathExists(currentPackageFolderPath)) {
434
+ // The copy below skips a package that is gone and lets the merge
435
+ // "complete" without it, which would report the missing package as a
436
+ // verification failure of the merged contents. The other platform's
437
+ // merge fails reading the missing files, so it is refused here too,
438
+ // for the two platforms to report one reason for one state.
439
+ throw new IOException("The installed package the diff merges into is gone from " + currentPackageFolderPath);
440
+ }
441
+
442
+ CodePushUpdateUtils.copyNecessaryFilesFromCurrentPackage(diffManifestFilePath, currentPackageFolderPath, newUpdateFolderPath);
443
+ File diffManifestFile = new File(diffManifestFilePath);
444
+ diffManifestFile.delete();
445
+ } catch (Exception e) {
446
+ // A merge that cannot complete has a word of its own, because it says
447
+ // the diff went wrong on its asset side - the one failure the patch
448
+ // archive, which carries every asset, is not implicated in.
449
+ if (isBinaryPatchUpdate) {
450
+ CodePushUtils.log(e);
451
+ return ArchiveRestoreResult.failure(ArchiveRestoreResult.REASON_ASSET_MERGE_FAILED);
452
+ }
453
+
454
+ throw e;
455
+ }
364
456
  }
365
457
 
366
458
  FileUtils.copyDirectoryContents(unzippedFolderPath, newUpdateFolderPath);
@@ -389,11 +481,11 @@ public class CodePushUpdateManager {
389
481
  }
390
482
  } else {
391
483
  if (isBinaryPatchUpdate) {
392
- // Whatever the patch URL served, it is not a patch archive - an error page
484
+ // Whatever the archive URL served, it is not an update archive - an error page
393
485
  // answered with a 200 looks like this too. Moving it into place would
394
486
  // install bytes no hash has ever been checked against, so the full archive
395
487
  // is downloaded instead.
396
- return BinaryPatchResult.failure(BinaryPatchResult.REASON_INVALID_MANIFEST);
488
+ return ArchiveRestoreResult.failure(ArchiveRestoreResult.REASON_INVALID_MANIFEST);
397
489
  }
398
490
 
399
491
  // File is a jsbundle, move it to a folder with the packageHash as its name
@@ -403,7 +495,7 @@ public class CodePushUpdateManager {
403
495
  // Save metadata to the folder.
404
496
  CodePushUtils.writeJsonToFile(updatePackage, newUpdateMetadataPath);
405
497
 
406
- return BinaryPatchResult.success();
498
+ return ArchiveRestoreResult.success();
407
499
  }
408
500
 
409
501
  public void installPackage(JSONObject updatePackage, boolean removePendingUpdate) {
@@ -452,6 +544,14 @@ public class CodePushUpdateManager {
452
544
  try {
453
545
  downloadUrl = new URL(remoteBundleUrl);
454
546
  connection = (HttpURLConnection) (downloadUrl.openConnection());
547
+ connection.setConnectTimeout(CodePushConstants.DOWNLOAD_CONNECT_TIMEOUT_IN_MS);
548
+ connection.setReadTimeout(CodePushConstants.DOWNLOAD_READ_TIMEOUT_IN_MS);
549
+
550
+ int responseCode = connection.getResponseCode();
551
+ if (responseCode >= 400) {
552
+ throw new CodePushHttpException(remoteBundleUrl, responseCode);
553
+ }
554
+
455
555
  bin = new BufferedInputStream(connection.getInputStream());
456
556
  File downloadFile = new File(getCurrentPackageBundlePath(bundleFileName));
457
557
  downloadFile.delete();