@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.
- package/CodePush.podspec +5 -0
- package/README.md +20 -145
- package/android/app/src/main/java/com/microsoft/codepush/react/ArchiveAttemptLog.java +181 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/{BinaryPatchResult.java → ArchiveRestoreResult.java} +17 -7
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushBinaryPatch.java +20 -20
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushConstants.java +20 -1
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushErrorCode.java +86 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushHttpException.java +29 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushIncompleteDownloadException.java +17 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java +12 -7
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateManager.java +143 -43
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushBinaryPatchTest.java +31 -31
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushErrorCodeTest.java +90 -0
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerBinaryPatchTest.java +159 -19
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerDownloadTest.java +304 -15
- package/cli/commands/createHistoryCommand/createReleaseHistory.test.ts +133 -0
- package/cli/commands/createHistoryCommand/createReleaseHistory.ts +3 -11
- package/cli/commands/releaseCommand/addToReleaseHistory.ts +3 -11
- package/cli/commands/releaseCommand/release.test.ts +55 -13
- package/cli/commands/releaseCommand/release.ts +39 -6
- package/cli/commands/updateHistoryCommand/updateReleaseHistory.ts +3 -11
- package/cli/dist/commands/createHistoryCommand/createReleaseHistory.js +2 -8
- package/cli/dist/commands/createHistoryCommand/createReleaseHistory.test.js +95 -0
- package/cli/dist/commands/releaseCommand/addToReleaseHistory.js +2 -8
- package/cli/dist/commands/releaseCommand/release.js +23 -8
- package/cli/dist/commands/releaseCommand/release.test.js +37 -9
- package/cli/dist/commands/updateHistoryCommand/updateReleaseHistory.js +2 -8
- package/cli/dist/functions/resolveAssetDiffBases.js +7 -2
- package/cli/dist/functions/resolveAssetDiffBases.test.js +26 -12
- package/cli/dist/functions/stageReleaseHistoryFile.js +28 -0
- package/cli/functions/resolveAssetDiffBases.test.ts +28 -14
- package/cli/functions/resolveAssetDiffBases.ts +8 -1
- package/cli/functions/stageReleaseHistoryFile.ts +38 -0
- package/ios/CodePush/CodePush.h +6 -3
- package/ios/CodePush/CodePush.mm +10 -10
- package/ios/CodePush/CodePushBinaryPatch.h +18 -8
- package/ios/CodePush/CodePushBinaryPatch.m +23 -22
- package/ios/CodePush/CodePushDownloadHandler.m +2 -1
- package/ios/CodePush/CodePushErrorUtils.m +55 -0
- package/ios/CodePush/CodePushPackage.m +242 -71
- package/package.json +2 -1
- package/src/CodePush.js +28 -18
- package/src/CodePush.test.js +110 -55
- package/src/package-mixins.js +19 -10
- package/typings/react-native-code-push.d.ts +152 -41
- 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
|
|
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 (
|
|
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.
|
|
398
|
-
CodePushUtils.convertJsonObjectToWritable(
|
|
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 (
|
|
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
|
|
161
|
-
*
|
|
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
|
|
167
|
-
// update. The
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
*
|
|
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
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
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
|
|
197
|
-
*
|
|
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
|
|
200
|
-
|
|
201
|
-
|
|
238
|
+
private boolean tryDownloadArchivePackage(JSONObject updatePackage, String expectedBundleFileName,
|
|
239
|
+
DownloadProgressCallback progressCallback,
|
|
240
|
+
String archiveDownloadUrl, ArchiveAttemptLog patchAttempt)
|
|
241
|
+
throws IOException {
|
|
202
242
|
try {
|
|
203
|
-
|
|
204
|
-
progressCallback,
|
|
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("
|
|
211
|
-
+ ").
|
|
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
|
|
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
|
-
*
|
|
233
|
-
*
|
|
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
|
|
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
|
-
|
|
298
|
+
ArchiveRestoreResult downloadAndInstallPackage(JSONObject updatePackage, String expectedBundleFileName,
|
|
242
299
|
DownloadProgressCallback progressCallback,
|
|
243
300
|
String downloadUrlString, boolean isBinaryPatchUpdate,
|
|
244
|
-
|
|
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
|
-
|
|
317
|
-
|
|
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
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
|
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
|
|
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
|
|
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();
|