forked from microsoft/react-native-code-push
-
Notifications
You must be signed in to change notification settings - Fork 25
Expand file tree
/
Copy pathCodePushUpdateManager.java
More file actions
616 lines (529 loc) · 29.3 KB
/
Copy pathCodePushUpdateManager.java
File metadata and controls
616 lines (529 loc) · 29.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
package com.microsoft.codepush.react;
import android.content.Context;
import android.os.Build;
import org.json.JSONObject;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.ByteBuffer;
import javax.net.ssl.HttpsURLConnection;
public class CodePushUpdateManager {
private String mDocumentsDirectory;
private final CodePushBinaryPatch mBinaryPatch;
public CodePushUpdateManager(String documentsDirectory, Context context) {
this(documentsDirectory, new CodePushBinaryPatch(new AssetsBaseBundleProvider(context), new HDiffPatchNative()));
}
CodePushUpdateManager(String documentsDirectory, CodePushBinaryPatch binaryPatch) {
mDocumentsDirectory = documentsDirectory;
mBinaryPatch = binaryPatch;
}
private String getDownloadFilePath() {
return CodePushUtils.appendPathComponent(getCodePushPath(), CodePushConstants.DOWNLOAD_FILE_NAME);
}
private String getUnzippedFolderPath() {
return CodePushUtils.appendPathComponent(getCodePushPath(), CodePushConstants.UNZIPPED_FOLDER_NAME);
}
private String getBinaryPatchFolderPath() {
return CodePushUtils.appendPathComponent(getCodePushPath(), CodePushConstants.BINARY_PATCH_FOLDER_NAME);
}
private String getDocumentsDirectory() {
return mDocumentsDirectory;
}
private String getCodePushPath() {
String codePushPath = CodePushUtils.appendPathComponent(getDocumentsDirectory(), CodePushConstants.CODE_PUSH_FOLDER_PREFIX);
if (CodePush.isUsingTestConfiguration()) {
codePushPath = CodePushUtils.appendPathComponent(codePushPath, "TestPackages");
}
return codePushPath;
}
private String getStatusFilePath() {
return CodePushUtils.appendPathComponent(getCodePushPath(), CodePushConstants.STATUS_FILE);
}
public JSONObject getCurrentPackageInfo() {
String statusFilePath = getStatusFilePath();
if (!FileUtils.fileAtPathExists(statusFilePath)) {
return new JSONObject();
}
try {
return CodePushUtils.getJsonObjectFromFile(statusFilePath);
} catch (IOException e) {
// Should not happen.
throw new CodePushUnknownException("Error getting current package info", e);
}
}
public void updateCurrentPackageInfo(JSONObject packageInfo) {
try {
CodePushUtils.writeJsonToFile(packageInfo, getStatusFilePath());
} catch (IOException e) {
// Should not happen.
throw new CodePushUnknownException("Error updating current package info", e);
}
}
public String getCurrentPackageFolderPath() {
JSONObject info = getCurrentPackageInfo();
String packageHash = info.optString(CodePushConstants.CURRENT_PACKAGE_KEY, null);
if (packageHash == null) {
return null;
}
return getPackageFolderPath(packageHash);
}
public String getCurrentPackageBundlePath(String bundleFileName) {
String packageFolder = getCurrentPackageFolderPath();
if (packageFolder == null) {
return null;
}
JSONObject currentPackage = getCurrentPackage();
if (currentPackage == null) {
return null;
}
String relativeBundlePath = currentPackage.optString(CodePushConstants.RELATIVE_BUNDLE_PATH_KEY, null);
if (relativeBundlePath == null) {
return CodePushUtils.appendPathComponent(packageFolder, bundleFileName);
} else {
return CodePushUtils.appendPathComponent(packageFolder, relativeBundlePath);
}
}
public String getPackageFolderPath(String packageHash) {
return CodePushUtils.appendPathComponent(getCodePushPath(), packageHash);
}
public String getCurrentPackageHash() {
JSONObject info = getCurrentPackageInfo();
return info.optString(CodePushConstants.CURRENT_PACKAGE_KEY, null);
}
public String getPreviousPackageHash() {
JSONObject info = getCurrentPackageInfo();
return info.optString(CodePushConstants.PREVIOUS_PACKAGE_KEY, null);
}
public JSONObject getCurrentPackage() {
String packageHash = getCurrentPackageHash();
if (packageHash == null) {
return null;
}
return getPackage(packageHash);
}
public JSONObject getPreviousPackage() {
String packageHash = getPreviousPackageHash();
if (packageHash == null) {
return null;
}
return getPackage(packageHash);
}
public JSONObject getPackage(String packageHash) {
String folderPath = getPackageFolderPath(packageHash);
String packageFilePath = CodePushUtils.appendPathComponent(folderPath, CodePushConstants.PACKAGE_FILE_NAME);
try {
return CodePushUtils.getJsonObjectFromFile(packageFilePath);
} catch (IOException e) {
return null;
}
}
/**
* @return what the attempts at installing the update from its patch archives ended in,
* or null when the update had no such archive to attempt. The result belongs
* to this download alone: the metadata written for the update never carries it.
*/
public JSONObject downloadPackage(JSONObject updatePackage, String expectedBundleFileName,
DownloadProgressCallback progressCallback) throws IOException {
// A release that was published with a binary patch offers up to three archives of
// the same update. The asset diff is the smallest and is tried first, the patch
// archive stands in when the diff fails on its asset side, and the full archive is
// always there when none of it works out.
String binaryPatchDownloadUrl = optArchiveDownloadUrl(updatePackage, CodePushConstants.BINARY_PATCH_DOWNLOAD_URL_KEY);
String assetDiffDownloadUrl = optArchiveDownloadUrl(updatePackage, CodePushConstants.ASSET_DIFF_DOWNLOAD_URL_KEY);
ArchiveAttemptLog patchAttempt = null;
boolean patchArchiveWorthTrying = binaryPatchDownloadUrl != null;
if (assetDiffDownloadUrl != null) {
patchAttempt = new ArchiveAttemptLog();
patchAttempt.beginAttempt(ArchiveAttemptLog.ARCHIVE_ASSET_DIFF);
if (tryDownloadArchivePackage(updatePackage, expectedBundleFileName, progressCallback,
assetDiffDownloadUrl, patchAttempt)) {
return patchAttempt.result();
}
// The patch archive is worth trying when nothing about how the diff failed
// implicates it. A diff that failed after restoring its bundle failed on its
// asset side, which the patch archive does not share; a diff the server never
// served is a verdict on one URL, and the patch archive is at another. Anything
// else failed in the bundle patch both archives carry byte for byte, so the
// patch archive would fail the same way and trying it would only put a second
// doomed download in front of the full one.
patchArchiveWorthTrying = patchArchiveWorthTrying
&& (patchAttempt.currentAttemptRestoredBundle() || patchAttempt.currentAttemptWasNotServed());
}
if (patchArchiveWorthTrying) {
if (patchAttempt == null) {
patchAttempt = new ArchiveAttemptLog();
}
patchAttempt.beginAttempt(ArchiveAttemptLog.ARCHIVE_BINARY_PATCH);
if (tryDownloadArchivePackage(updatePackage, expectedBundleFileName, progressCallback,
binaryPatchDownloadUrl, patchAttempt)) {
return patchAttempt.result();
}
}
downloadAndInstallPackage(updatePackage, expectedBundleFileName, progressCallback,
updatePackage.optString(CodePushConstants.DOWNLOAD_URL_KEY, null), false, null);
return patchAttempt == null ? null : patchAttempt.result();
}
/**
* @return the URL one of the update's archives is offered at, or null when the update
* does not offer that archive. An empty string is not a URL: it stands for the
* same absent archive as a missing key, which is what iOS makes of it too.
*/
private static String optArchiveDownloadUrl(JSONObject updatePackage, String downloadUrlKey) {
String downloadUrl = updatePackage.optString(downloadUrlKey, null);
return downloadUrl == null || downloadUrl.isEmpty() ? null : downloadUrl;
}
/**
* Installs the update from one of its patch archives.
*
* Every verdict on the archive ends the same way, with the caller moving on to the next
* one, so none of it is reported to the caller as an error. A network that did not carry
* the archive is not a verdict on it and is raised instead. The ladder cannot loop:
* which archive comes next is the caller's decision alone, and the full archive at its
* end is downloaded by a call that is not allowed to take the patch path, so it has no
* failure of its own to fall back from.
*
* @param patchAttempt records what the attempt ended in, for the caller to hand to
* whoever asked for the download
* @return true when the update was installed, false when the caller has to move on to
* the next archive
* @throws IOException when the network did not carry the archive, which is not a verdict
* on the archive and so is not a reason to try another one
*/
private boolean tryDownloadArchivePackage(JSONObject updatePackage, String expectedBundleFileName,
DownloadProgressCallback progressCallback,
String archiveDownloadUrl, ArchiveAttemptLog patchAttempt)
throws IOException {
try {
ArchiveRestoreResult patchResult = downloadAndInstallPackage(updatePackage, expectedBundleFileName,
progressCallback, archiveDownloadUrl, true, patchAttempt);
if (patchResult.succeeded()) {
return true;
}
patchAttempt.recordFallback(patchResult.getFailureReason());
CodePushUtils.log("The " + patchAttempt.currentArchive() + " archive failed ("
+ patchResult.getFailureReason() + "). Falling back.");
} catch (Exception | OutOfMemoryError e) {
if (CodePushErrorCode.isNetworkFailure(e)) {
// The network is what failed, not the archive, and the full archive is behind
// the same network - only larger, and started over from nothing. Falling back
// here would spend a second download to reach the failure already in hand.
CodePushUtils.log("The " + patchAttempt.currentArchive()
+ " archive could not be downloaded. Giving up on the download.");
if (e instanceof IOException) {
throw (IOException) e;
}
// A network failure read out of a wrapper this package raised, which the
// caller catches by its own type rather than by `IOException`.
throw new CodePushUnknownException(
"The " + patchAttempt.currentArchive() + " archive could not be downloaded.", e);
}
// Applying a patch is the one path that holds a whole bundle in memory, so
// running out of it is a failure this has to absorb like any other: by the time
// it lands here the arrays are unreachable, and the full archive is downloaded
// to disk in chunks rather than held.
patchAttempt.recordFallbackAfterError(e);
CodePushUtils.log(e);
CodePushUtils.log("The " + patchAttempt.currentArchive()
+ " archive could not be applied. Falling back.");
} finally {
FileUtils.deleteDirectoryAtPath(getBinaryPatchFolderPath());
}
return false;
}
/**
* Downloads an update from one of its archives and installs it.
*
* @param isBinaryPatchUpdate whether the archive holds a binary patch of the JS bundle,
* which has to be applied before the contents are the update.
* Both the asset diff and the patch archive are downloaded
* that way; the full archive never is, so an update being
* downloaded in full can never end up on the patch path.
* @param patchAttempt the record the patch is timed into, or null for a full download,
* which has no patch to time
* @return the outcome of the patch: a failed result means the update was not installed
* and the caller has to move on to the next archive. Downloading the full
* archive always succeeds or throws.
*/
ArchiveRestoreResult downloadAndInstallPackage(JSONObject updatePackage, String expectedBundleFileName,
DownloadProgressCallback progressCallback,
String downloadUrlString, boolean isBinaryPatchUpdate,
ArchiveAttemptLog patchAttempt) throws IOException {
String newUpdateHash = updatePackage.optString(CodePushConstants.PACKAGE_HASH_KEY, null);
String newUpdateFolderPath = getPackageFolderPath(newUpdateHash);
String newUpdateMetadataPath = CodePushUtils.appendPathComponent(newUpdateFolderPath, CodePushConstants.PACKAGE_FILE_NAME);
if (FileUtils.fileAtPathExists(newUpdateFolderPath)) {
// This removes any stale data in newPackageFolderPath that could have been left
// uncleared due to a crash or error during the download or install process.
FileUtils.deleteDirectoryAtPath(newUpdateFolderPath);
}
HttpURLConnection connection = null;
BufferedInputStream bin = null;
FileOutputStream fos = null;
BufferedOutputStream bout = null;
File downloadFile = null;
boolean isZip = false;
// Download the file while checking if it is a zip and notifying client of progress.
try {
URL downloadUrl = new URL(downloadUrlString);
connection = (HttpURLConnection) (downloadUrl.openConnection());
connection.setConnectTimeout(CodePushConstants.DOWNLOAD_CONNECT_TIMEOUT_IN_MS);
connection.setReadTimeout(CodePushConstants.DOWNLOAD_READ_TIMEOUT_IN_MS);
if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP &&
downloadUrl.toString().startsWith("https")) {
try {
((HttpsURLConnection)connection).setSSLSocketFactory(new TLSSocketFactory());
} catch (Exception e) {
throw new CodePushUnknownException("Error set SSLSocketFactory. ", e);
}
}
connection.setRequestProperty("Accept-Encoding", "identity");
// Read before the body is: an error status carries a body of its own, and
// asking `getInputStream()` for it first turns some statuses into a stream and
// others into an exception that says nothing about which status it was.
int responseCode = connection.getResponseCode();
if (responseCode >= 400) {
throw new CodePushHttpException(downloadUrlString, responseCode);
}
bin = new BufferedInputStream(connection.getInputStream());
// Announced only once the response is flowing, so a connection that fails to
// open never announces an empty stream.
progressCallback.onDownloadStart();
long totalBytes = connection.getContentLength();
long receivedBytes = 0;
if (totalBytes > 0) {
// The zero-received event hands the listener this download's total up front,
// so a fallback download shows as restarting rather than running backwards.
progressCallback.call(new DownloadProgress(totalBytes, 0));
}
File downloadFolder = new File(getCodePushPath());
downloadFolder.mkdirs();
downloadFile = new File(downloadFolder, CodePushConstants.DOWNLOAD_FILE_NAME);
fos = new FileOutputStream(downloadFile);
bout = new BufferedOutputStream(fos, CodePushConstants.DOWNLOAD_BUFFER_SIZE);
byte[] data = new byte[CodePushConstants.DOWNLOAD_BUFFER_SIZE];
byte[] header = new byte[4];
int numBytesRead = 0;
while ((numBytesRead = bin.read(data, 0, CodePushConstants.DOWNLOAD_BUFFER_SIZE)) >= 0) {
if (receivedBytes < 4) {
for (int i = 0; i < numBytesRead; i++) {
int headerOffset = (int) (receivedBytes) + i;
if (headerOffset >= 4) {
break;
}
header[headerOffset] = data[i];
}
}
receivedBytes += numBytesRead;
bout.write(data, 0, numBytesRead);
progressCallback.call(new DownloadProgress(totalBytes, receivedBytes));
}
// Only against a length the server declared. `getContentLength()` answers -1
// for a body sent without one, which no read total matches - so comparing anyway
// would fail every download a server chooses to send that way.
if (totalBytes >= 0 && totalBytes != receivedBytes) {
throw new CodePushIncompleteDownloadException(receivedBytes, totalBytes);
}
isZip = ByteBuffer.wrap(header).getInt() == 0x504b0304;
} catch (MalformedURLException e) {
throw new CodePushMalformedDataException(downloadUrlString, e);
} finally {
try {
if (bout != null) bout.close();
if (fos != null) fos.close();
if (bin != null) bin.close();
if (connection != null) connection.disconnect();
} catch (IOException e) {
throw new CodePushUnknownException("Error closing IO resources.", e);
}
}
if (isZip) {
// Unzip the downloaded file and then delete the zip
String unzippedFolderPath = getUnzippedFolderPath();
FileUtils.unzipFile(downloadFile, unzippedFolderPath);
FileUtils.deleteFileOrFolderSilently(downloadFile);
// Rebuild the JS bundle the archive only carries a patch of, which leaves the
// contents identical to the ones the full archive would have delivered.
if (isBinaryPatchUpdate) {
long patchStartTime = System.currentTimeMillis();
ArchiveRestoreResult patchResult = mBinaryPatch.restoreBundle(unzippedFolderPath,
getBinaryPatchFolderPath(), expectedBundleFileName);
if (!patchResult.succeeded()) {
return patchResult;
}
long applyDurationMs = System.currentTimeMillis() - patchStartTime;
patchAttempt.recordBundleRestored(applyDurationMs);
CodePushUtils.log("Restored the update from its binary patch in " + applyDurationMs + " ms.");
}
// Merge contents with current update based on the manifest
String diffManifestFilePath = CodePushUtils.appendPathComponent(unzippedFolderPath,
CodePushConstants.DIFF_MANIFEST_FILE_NAME);
boolean isDiffUpdate = FileUtils.fileAtPathExists(diffManifestFilePath);
if (isDiffUpdate) {
try {
String currentPackageFolderPath = getCurrentPackageFolderPath();
if (currentPackageFolderPath != null && !FileUtils.fileAtPathExists(currentPackageFolderPath)) {
// The copy below skips a package that is gone and lets the merge
// "complete" without it, which would report the missing package as a
// verification failure of the merged contents. The other platform's
// merge fails reading the missing files, so it is refused here too,
// for the two platforms to report one reason for one state.
throw new IOException("The installed package the diff merges into is gone from " + currentPackageFolderPath);
}
CodePushUpdateUtils.copyNecessaryFilesFromCurrentPackage(diffManifestFilePath, currentPackageFolderPath, newUpdateFolderPath);
File diffManifestFile = new File(diffManifestFilePath);
diffManifestFile.delete();
} catch (Exception e) {
// A merge that cannot complete has a word of its own, because it says
// the diff went wrong on its asset side - the one failure the patch
// archive, which carries every asset, is not implicated in.
if (isBinaryPatchUpdate) {
CodePushUtils.log(e);
return ArchiveRestoreResult.failure(ArchiveRestoreResult.REASON_ASSET_MERGE_FAILED);
}
throw e;
}
}
FileUtils.copyDirectoryContents(unzippedFolderPath, newUpdateFolderPath);
FileUtils.deleteFileAtPathSilently(unzippedFolderPath);
// For zip updates, we need to find the relative path to the jsBundle and save it in the
// metadata so that we can find and run it easily the next time.
String relativeBundlePath = CodePushUpdateUtils.findJSBundleInUpdateContents(newUpdateFolderPath, expectedBundleFileName);
if (relativeBundlePath == null) {
throw new CodePushInvalidUpdateException("Update is invalid - A JS bundle file named \"" + expectedBundleFileName + "\" could not be found within the downloaded contents. Please check that you are releasing your CodePush updates using the exact same JS bundle file name that was shipped with your app's binary.");
} else {
if (FileUtils.fileAtPathExists(newUpdateMetadataPath)) {
File metadataFileFromOldUpdate = new File(newUpdateMetadataPath);
metadataFileFromOldUpdate.delete();
}
if (isDiffUpdate) {
CodePushUtils.log("Applying diff update.");
} else {
CodePushUtils.log("Applying full update.");
}
CodePushUpdateUtils.verifyFolderHash(newUpdateFolderPath, newUpdateHash);
CodePushUtils.setJSONValueForKey(updatePackage, CodePushConstants.RELATIVE_BUNDLE_PATH_KEY, relativeBundlePath);
}
} else {
if (isBinaryPatchUpdate) {
// Whatever the archive URL served, it is not an update archive - an error page
// answered with a 200 looks like this too. Moving it into place would
// install bytes no hash has ever been checked against, so the full archive
// is downloaded instead.
return ArchiveRestoreResult.failure(ArchiveRestoreResult.REASON_INVALID_MANIFEST);
}
// File is a jsbundle, move it to a folder with the packageHash as its name
FileUtils.moveFile(downloadFile, newUpdateFolderPath, expectedBundleFileName);
}
// Save metadata to the folder.
CodePushUtils.writeJsonToFile(updatePackage, newUpdateMetadataPath);
return ArchiveRestoreResult.success();
}
public void installPackage(JSONObject updatePackage, boolean removePendingUpdate) {
String packageHash = updatePackage.optString(CodePushConstants.PACKAGE_HASH_KEY, null);
JSONObject info = getCurrentPackageInfo();
String currentPackageHash = info.optString(CodePushConstants.CURRENT_PACKAGE_KEY, null);
if (packageHash != null && packageHash.equals(currentPackageHash)) {
// The current package is already the one being installed, so we should no-op.
return;
}
if (removePendingUpdate) {
String currentPackageFolderPath = getCurrentPackageFolderPath();
if (currentPackageFolderPath != null) {
FileUtils.deleteDirectoryAtPath(currentPackageFolderPath);
}
} else {
String previousPackageHash = getPreviousPackageHash();
if (previousPackageHash != null && !previousPackageHash.equals(packageHash)) {
FileUtils.deleteDirectoryAtPath(getPackageFolderPath(previousPackageHash));
}
CodePushUtils.setJSONValueForKey(info, CodePushConstants.PREVIOUS_PACKAGE_KEY, info.optString(CodePushConstants.CURRENT_PACKAGE_KEY, null));
}
CodePushUtils.setJSONValueForKey(info, CodePushConstants.CURRENT_PACKAGE_KEY, packageHash);
updateCurrentPackageInfo(info);
}
public void rollbackPackage() {
JSONObject info = getCurrentPackageInfo();
String currentPackageFolderPath = getCurrentPackageFolderPath();
FileUtils.deleteDirectoryAtPath(currentPackageFolderPath);
CodePushUtils.setJSONValueForKey(info, CodePushConstants.CURRENT_PACKAGE_KEY, info.optString(CodePushConstants.PREVIOUS_PACKAGE_KEY, null));
CodePushUtils.setJSONValueForKey(info, CodePushConstants.PREVIOUS_PACKAGE_KEY, null);
updateCurrentPackageInfo(info);
}
public void downloadAndReplaceCurrentBundle(String remoteBundleUrl, String bundleFileName) throws IOException {
URL downloadUrl;
HttpURLConnection connection = null;
BufferedInputStream bin = null;
FileOutputStream fos = null;
BufferedOutputStream bout = null;
try {
downloadUrl = new URL(remoteBundleUrl);
connection = (HttpURLConnection) (downloadUrl.openConnection());
connection.setConnectTimeout(CodePushConstants.DOWNLOAD_CONNECT_TIMEOUT_IN_MS);
connection.setReadTimeout(CodePushConstants.DOWNLOAD_READ_TIMEOUT_IN_MS);
int responseCode = connection.getResponseCode();
if (responseCode >= 400) {
throw new CodePushHttpException(remoteBundleUrl, responseCode);
}
bin = new BufferedInputStream(connection.getInputStream());
File downloadFile = new File(getCurrentPackageBundlePath(bundleFileName));
downloadFile.delete();
fos = new FileOutputStream(downloadFile);
bout = new BufferedOutputStream(fos, CodePushConstants.DOWNLOAD_BUFFER_SIZE);
byte[] data = new byte[CodePushConstants.DOWNLOAD_BUFFER_SIZE];
int numBytesRead = 0;
while ((numBytesRead = bin.read(data, 0, CodePushConstants.DOWNLOAD_BUFFER_SIZE)) >= 0) {
bout.write(data, 0, numBytesRead);
}
} catch (MalformedURLException e) {
throw new CodePushMalformedDataException(remoteBundleUrl, e);
} finally {
try {
if (bout != null) bout.close();
if (fos != null) fos.close();
if (bin != null) bin.close();
if (connection != null) connection.disconnect();
} catch (IOException e) {
throw new CodePushUnknownException("Error closing IO resources.", e);
}
}
}
public void clearUpdates() {
FileUtils.deleteDirectoryAtPath(getCodePushPath());
}
/** Reads the JS bundle that shipped inside the app binary out of the APK's assets. */
private static class AssetsBaseBundleProvider implements CodePushBinaryPatch.BaseBundleProvider {
private final Context mContext;
AssetsBaseBundleProvider(Context context) {
mContext = context;
}
@Override
public byte[] readBaseBundle(String bundleFileName) throws IOException {
// The bundle is stored uncompressed in the APK, so it is read straight into
// memory: a copy on disk would buy nothing and cost the space the restored
// bundle needs.
InputStream assetStream = mContext.getAssets().open(bundleFileName);
try {
// An uncompressed asset knows its whole length up front, so the buffer is
// sized for it: growing one would repeatedly hold two copies of a bundle
// that is already the largest allocation on this path.
ByteArrayOutputStream bundleBytes = new ByteArrayOutputStream(
Math.max(assetStream.available(), CodePushConstants.DOWNLOAD_BUFFER_SIZE));
byte[] buffer = new byte[CodePushConstants.DOWNLOAD_BUFFER_SIZE];
int bytesRead;
while ((bytesRead = assetStream.read(buffer)) > 0) {
bundleBytes.write(buffer, 0, bytesRead);
}
return bundleBytes.toByteArray();
} finally {
assetStream.close();
}
}
}
}