@capawesome/capacitor-live-update 6.4.1 → 6.5.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.
- package/README.md +2 -1
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +351 -251
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateHttpClient.java +71 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java +42 -59
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/Manifest.java +58 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/ManifestItem.java +37 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/api/GetLatestBundleResponse.java +19 -27
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetChannelResult.java +2 -1
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/enums/ArtifactType.java +6 -0
- package/dist/docs.json +1 -1
- package/dist/esm/definitions.d.ts +1 -1
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Plugin/Classes/Manifest.swift +20 -0
- package/ios/Plugin/Enums/ArtifactType.swift +4 -0
- package/ios/Plugin/{Classes → Enums}/CustomError.swift +3 -0
- package/ios/Plugin/LiveUpdate.swift +270 -199
- package/ios/Plugin/LiveUpdateHttpClient.swift +42 -0
- package/ios/Plugin/LiveUpdatePlugin.swift +65 -63
- package/ios/Plugin/Protocols/Api/GetLatestBundleResponse.swift +2 -3
- package/ios/Plugin/Protocols/Api/ManifestItem.swift +5 -0
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import android.app.Activity;
|
|
|
4
4
|
import android.content.SharedPreferences;
|
|
5
5
|
import android.content.pm.PackageInfo;
|
|
6
6
|
import android.content.pm.PackageManager;
|
|
7
|
+
import android.content.res.AssetManager;
|
|
7
8
|
import android.os.Build;
|
|
8
9
|
import android.os.Handler;
|
|
9
10
|
import android.util.Base64;
|
|
@@ -11,6 +12,8 @@ import androidx.annotation.NonNull;
|
|
|
11
12
|
import androidx.annotation.Nullable;
|
|
12
13
|
import com.getcapacitor.Logger;
|
|
13
14
|
import com.getcapacitor.plugin.WebView;
|
|
15
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.Manifest;
|
|
16
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.ManifestItem;
|
|
14
17
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.api.GetLatestBundleResponse;
|
|
15
18
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.DeleteBundleOptions;
|
|
16
19
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.DownloadBundleOptions;
|
|
@@ -25,32 +28,33 @@ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetDeviceIdR
|
|
|
25
28
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetVersionCodeResult;
|
|
26
29
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetVersionNameResult;
|
|
27
30
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.SyncResult;
|
|
31
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.enums.ArtifactType;
|
|
28
32
|
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.EmptyCallback;
|
|
29
33
|
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.NonEmptyCallback;
|
|
30
34
|
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.Result;
|
|
31
35
|
import java.io.File;
|
|
36
|
+
import java.io.FileInputStream;
|
|
37
|
+
import java.io.FileOutputStream;
|
|
32
38
|
import java.io.IOException;
|
|
39
|
+
import java.io.InputStream;
|
|
33
40
|
import java.security.KeyFactory;
|
|
34
41
|
import java.security.MessageDigest;
|
|
35
42
|
import java.security.PublicKey;
|
|
36
43
|
import java.security.Signature;
|
|
37
44
|
import java.security.spec.X509EncodedKeySpec;
|
|
45
|
+
import java.util.ArrayList;
|
|
38
46
|
import java.util.Arrays;
|
|
47
|
+
import java.util.List;
|
|
39
48
|
import java.util.UUID;
|
|
40
|
-
import java.util.concurrent.
|
|
49
|
+
import java.util.concurrent.atomic.AtomicReference;
|
|
41
50
|
import net.lingala.zip4j.ZipFile;
|
|
42
|
-
import okhttp3.Call;
|
|
43
|
-
import okhttp3.Callback;
|
|
44
51
|
import okhttp3.HttpUrl;
|
|
45
|
-
import okhttp3.OkHttpClient;
|
|
46
|
-
import okhttp3.Request;
|
|
47
52
|
import okhttp3.Response;
|
|
48
53
|
import okhttp3.ResponseBody;
|
|
49
54
|
import okio.Buffer;
|
|
50
|
-
import okio.BufferedSink;
|
|
51
55
|
import okio.BufferedSource;
|
|
52
56
|
import okio.Okio;
|
|
53
|
-
import org.json.
|
|
57
|
+
import org.json.JSONArray;
|
|
54
58
|
import org.json.JSONObject;
|
|
55
59
|
|
|
56
60
|
public class LiveUpdate {
|
|
@@ -60,6 +64,12 @@ public class LiveUpdate {
|
|
|
60
64
|
|
|
61
65
|
private final String defaultWebAssetDir;
|
|
62
66
|
|
|
67
|
+
@NonNull
|
|
68
|
+
private final String host;
|
|
69
|
+
|
|
70
|
+
@NonNull
|
|
71
|
+
private final LiveUpdateHttpClient httpClient;
|
|
72
|
+
|
|
63
73
|
@NonNull
|
|
64
74
|
private final LiveUpdatePlugin plugin;
|
|
65
75
|
|
|
@@ -69,12 +79,19 @@ public class LiveUpdate {
|
|
|
69
79
|
@NonNull
|
|
70
80
|
private final SharedPreferences.Editor webViewSettingsEditor;
|
|
71
81
|
|
|
72
|
-
private final String bundlesDirectory = "_capacitor_live_update_bundles";
|
|
82
|
+
private final String bundlesDirectory = "_capacitor_live_update_bundles"; // DO NOT CHANGE!
|
|
73
83
|
private final Handler rollbackHandler = new Handler();
|
|
84
|
+
private final String manifestFileName = "capawesome-live-update-manifest.json"; // DO NOT CHANGE!
|
|
74
85
|
|
|
75
86
|
public LiveUpdate(@NonNull LiveUpdateConfig config, @NonNull LiveUpdatePlugin plugin) throws PackageManager.NameNotFoundException {
|
|
76
87
|
this.config = config;
|
|
77
88
|
this.defaultWebAssetDir = plugin.getBridge().DEFAULT_WEB_ASSET_DIR;
|
|
89
|
+
if (config.getLocation() != null && config.getLocation().equals("eu")) {
|
|
90
|
+
this.host = "api.cloud.capawesome.eu";
|
|
91
|
+
} else {
|
|
92
|
+
this.host = "api.cloud.capawesome.io";
|
|
93
|
+
}
|
|
94
|
+
this.httpClient = new LiveUpdateHttpClient(config);
|
|
78
95
|
this.plugin = plugin;
|
|
79
96
|
this.preferences = new LiveUpdatePreferences(plugin.getContext());
|
|
80
97
|
this.webViewSettingsEditor = plugin.getContext().getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE).edit();
|
|
@@ -115,7 +132,8 @@ public class LiveUpdate {
|
|
|
115
132
|
}
|
|
116
133
|
|
|
117
134
|
// Download the bundle
|
|
118
|
-
|
|
135
|
+
downloadBundleOfTypeZip(bundleId, url, checksum);
|
|
136
|
+
callback.success();
|
|
119
137
|
}
|
|
120
138
|
|
|
121
139
|
public void getBundle(@NonNull NonEmptyCallback callback) {
|
|
@@ -165,7 +183,9 @@ public class LiveUpdate {
|
|
|
165
183
|
|
|
166
184
|
public void ready() {
|
|
167
185
|
Logger.debug(LiveUpdatePlugin.TAG, "App is ready.");
|
|
186
|
+
// Stop the rollback timer
|
|
168
187
|
stopRollbackTimer();
|
|
188
|
+
// Delete unused bundles
|
|
169
189
|
if (config.getAutoDeleteBundles()) {
|
|
170
190
|
deleteUnusedBundles();
|
|
171
191
|
}
|
|
@@ -210,73 +230,44 @@ public class LiveUpdate {
|
|
|
210
230
|
|
|
211
231
|
public void sync(@NonNull NonEmptyCallback<Result> callback) throws Exception {
|
|
212
232
|
// Fetch the latest bundle
|
|
213
|
-
fetchLatestBundle(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
String checksum = result.getChecksum();
|
|
232
|
-
String signature = result.getSignature();
|
|
233
|
-
String url = result.getUrl();
|
|
234
|
-
|
|
235
|
-
// Check if the bundle already exists
|
|
236
|
-
if (hasBundle(latestBundleId)) {
|
|
237
|
-
String nextBundleId = null;
|
|
238
|
-
String currentBundleId = getCurrentBundleId();
|
|
239
|
-
if (!latestBundleId.equals(currentBundleId)) {
|
|
240
|
-
// Set the next bundle
|
|
241
|
-
setNextBundle(latestBundleId);
|
|
242
|
-
nextBundleId = latestBundleId;
|
|
243
|
-
}
|
|
244
|
-
SyncResult syncResult = new SyncResult(nextBundleId);
|
|
245
|
-
callback.success(syncResult);
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
// Download and unzip the bundle
|
|
249
|
-
downloadBundle(
|
|
250
|
-
latestBundleId,
|
|
251
|
-
checksum,
|
|
252
|
-
signature,
|
|
253
|
-
url,
|
|
254
|
-
new EmptyCallback() {
|
|
255
|
-
@Override
|
|
256
|
-
public void success() {
|
|
257
|
-
// Set the next bundle
|
|
258
|
-
setNextBundle(latestBundleId);
|
|
259
|
-
SyncResult syncResult = new SyncResult(latestBundleId);
|
|
260
|
-
callback.success(syncResult);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
@Override
|
|
264
|
-
public void error(Exception exception) {
|
|
265
|
-
callback.error(exception);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
);
|
|
269
|
-
}
|
|
233
|
+
GetLatestBundleResponse response = fetchLatestBundle();
|
|
234
|
+
if (response == null) {
|
|
235
|
+
Logger.debug(LiveUpdatePlugin.TAG, "No update available.");
|
|
236
|
+
SyncResult syncResult = new SyncResult(null);
|
|
237
|
+
callback.success(syncResult);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
ArtifactType artifactType = response.getArtifactType();
|
|
241
|
+
String latestBundleId = response.getBundleId();
|
|
242
|
+
String url = response.getUrl();
|
|
243
|
+
// Check if the bundle already exists
|
|
244
|
+
if (hasBundle(latestBundleId)) {
|
|
245
|
+
String nextBundleId = null;
|
|
246
|
+
String currentBundleId = getCurrentBundleId();
|
|
247
|
+
if (!latestBundleId.equals(currentBundleId)) {
|
|
248
|
+
// Set the next bundle
|
|
249
|
+
setNextBundle(latestBundleId);
|
|
250
|
+
nextBundleId = latestBundleId;
|
|
270
251
|
}
|
|
271
|
-
|
|
252
|
+
SyncResult syncResult = new SyncResult(nextBundleId);
|
|
253
|
+
callback.success(syncResult);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
// Download the bundle
|
|
257
|
+
if (artifactType == ArtifactType.MANIFEST) {
|
|
258
|
+
downloadBundleOfTypeManifest(latestBundleId, url);
|
|
259
|
+
} else {
|
|
260
|
+
downloadBundleOfTypeZip(latestBundleId, url, null);
|
|
261
|
+
}
|
|
262
|
+
// Set the next bundle
|
|
263
|
+
setNextBundle(latestBundleId);
|
|
264
|
+
SyncResult syncResult = new SyncResult(latestBundleId);
|
|
265
|
+
callback.success(syncResult);
|
|
272
266
|
}
|
|
273
267
|
|
|
274
|
-
private void addBundle(@NonNull String bundleId, @NonNull File
|
|
275
|
-
// Unzip the file to the bundle directory
|
|
276
|
-
File unzippedDirectory = unzipFile(zipFile);
|
|
277
|
-
|
|
268
|
+
private void addBundle(@NonNull String bundleId, @NonNull File sourceDirectory) throws Exception {
|
|
278
269
|
// Search folder with index.html file
|
|
279
|
-
File indexHtmlFile = searchIndexHtmlFile(
|
|
270
|
+
File indexHtmlFile = searchIndexHtmlFile(sourceDirectory);
|
|
280
271
|
if (indexHtmlFile == null) {
|
|
281
272
|
throw new Exception(LiveUpdatePlugin.ERROR_BUNDLE_INDEX_HTML_MISSING);
|
|
282
273
|
}
|
|
@@ -289,6 +280,25 @@ public class LiveUpdate {
|
|
|
289
280
|
indexHtmlFile.getParentFile().renameTo(bundleDirectory);
|
|
290
281
|
}
|
|
291
282
|
|
|
283
|
+
private void addBundleOfTypeManifest(@NonNull String bundleId, @NonNull File directory) throws Exception {
|
|
284
|
+
addBundle(bundleId, directory);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private void addBundleOfTypeZip(@NonNull String bundleId, @NonNull File zipFile) throws Exception {
|
|
288
|
+
// Unzip the file to the bundle directory
|
|
289
|
+
File unzippedDirectory = unzipFile(zipFile);
|
|
290
|
+
// Add the bundle
|
|
291
|
+
addBundle(bundleId, unzippedDirectory);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private File buildBundlesDirectory() {
|
|
295
|
+
return new File(plugin.getContext().getFilesDir(), bundlesDirectory);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private File buildBundleDirectoryFor(@NonNull String bundleId) {
|
|
299
|
+
return new File(plugin.getContext().getFilesDir(), bundlesDirectory + "/" + bundleId);
|
|
300
|
+
}
|
|
301
|
+
|
|
292
302
|
private File buildTemporaryDirectory() {
|
|
293
303
|
String fileName = UUID.randomUUID().toString();
|
|
294
304
|
return new File(plugin.getContext().getCacheDir(), fileName);
|
|
@@ -299,43 +309,63 @@ public class LiveUpdate {
|
|
|
299
309
|
return new File(plugin.getContext().getCacheDir(), fileName);
|
|
300
310
|
}
|
|
301
311
|
|
|
302
|
-
private File
|
|
303
|
-
|
|
312
|
+
private void copyCurrentBundleFile(@NonNull String href, @NonNull File destinationDirectory) throws IOException {
|
|
313
|
+
String currentBundleId = getCurrentBundleId();
|
|
314
|
+
if (currentBundleId.equals(defaultWebAssetDir)) {
|
|
315
|
+
// Create the source input stream
|
|
316
|
+
AssetManager assets = plugin.getContext().getAssets();
|
|
317
|
+
InputStream inputStream = assets.open(defaultWebAssetDir + "/" + href);
|
|
318
|
+
// Create the destination file
|
|
319
|
+
File destination = new File(destinationDirectory, href);
|
|
320
|
+
// Create all destination directories if they do not exist
|
|
321
|
+
destination.getParentFile().mkdirs();
|
|
322
|
+
// Copy the file
|
|
323
|
+
copyFile(inputStream, destination);
|
|
324
|
+
} else {
|
|
325
|
+
File currentBundleDirectory = buildBundleDirectoryFor(currentBundleId);
|
|
326
|
+
// Create the source and destination files
|
|
327
|
+
File source = new File(currentBundleDirectory, href);
|
|
328
|
+
File destination = new File(destinationDirectory, href);
|
|
329
|
+
// Create all destination directories if they do not exist
|
|
330
|
+
destination.getParentFile().mkdirs();
|
|
331
|
+
// Copy the file
|
|
332
|
+
copyFile(source, destination);
|
|
333
|
+
}
|
|
304
334
|
}
|
|
305
335
|
|
|
306
|
-
private
|
|
307
|
-
|
|
336
|
+
private void copyCurrentBundleFiles(@NonNull List<String> hrefs, @NonNull File destinationDirectory) throws IOException {
|
|
337
|
+
for (String href : hrefs) {
|
|
338
|
+
copyCurrentBundleFile(href, destinationDirectory);
|
|
339
|
+
}
|
|
308
340
|
}
|
|
309
341
|
|
|
310
|
-
private
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
318
|
-
source.close();
|
|
319
|
-
return digest.digest();
|
|
320
|
-
} catch (IOException exception) {
|
|
321
|
-
Logger.error(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
|
|
322
|
-
throw new Exception(LiveUpdatePlugin.ERROR_CHECKSUM_CALCULATION_FAILED);
|
|
342
|
+
private void copyFile(File input, File output) throws IOException {
|
|
343
|
+
FileInputStream in = new FileInputStream(input);
|
|
344
|
+
FileOutputStream out = new FileOutputStream(output);
|
|
345
|
+
byte[] buffer = new byte[1024];
|
|
346
|
+
int length;
|
|
347
|
+
while ((length = in.read(buffer)) > 0) {
|
|
348
|
+
out.write(buffer, 0, length);
|
|
323
349
|
}
|
|
350
|
+
out.close();
|
|
351
|
+
in.close();
|
|
324
352
|
}
|
|
325
353
|
|
|
326
|
-
private
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
354
|
+
private void copyFile(InputStream input, File output) throws IOException {
|
|
355
|
+
FileOutputStream out = new FileOutputStream(output);
|
|
356
|
+
byte[] buffer = new byte[1024];
|
|
357
|
+
int length;
|
|
358
|
+
while ((length = input.read(buffer)) > 0) {
|
|
359
|
+
out.write(buffer, 0, length);
|
|
331
360
|
}
|
|
332
|
-
|
|
361
|
+
out.close();
|
|
362
|
+
input.close();
|
|
333
363
|
}
|
|
334
364
|
|
|
335
365
|
private void createBundlesDirectory() {
|
|
336
|
-
File
|
|
337
|
-
if (!
|
|
338
|
-
|
|
366
|
+
File file = buildBundlesDirectory();
|
|
367
|
+
if (!file.exists()) {
|
|
368
|
+
file.mkdir();
|
|
339
369
|
}
|
|
340
370
|
}
|
|
341
371
|
|
|
@@ -352,14 +382,19 @@ public class LiveUpdate {
|
|
|
352
382
|
}
|
|
353
383
|
}
|
|
354
384
|
|
|
385
|
+
private File createTemporaryDirectory() {
|
|
386
|
+
File file = buildTemporaryDirectory();
|
|
387
|
+
file.mkdir();
|
|
388
|
+
return file;
|
|
389
|
+
}
|
|
390
|
+
|
|
355
391
|
private void deleteBundle(@NonNull String bundleId) {
|
|
356
392
|
// Delete the bundle directory
|
|
357
393
|
File bundleDirectory = buildBundleDirectoryFor(bundleId);
|
|
358
394
|
deleteFileRecursively(bundleDirectory);
|
|
359
395
|
// Reset the next bundle if it is the deleted bundle
|
|
360
|
-
String currentBundleId = getCurrentBundleId();
|
|
361
396
|
String nextBundleId = getNextBundleId();
|
|
362
|
-
if (bundleId.equals(
|
|
397
|
+
if (bundleId.equals(nextBundleId)) {
|
|
363
398
|
setNextCapacitorServerPathToDefaultWebAssetDir();
|
|
364
399
|
}
|
|
365
400
|
}
|
|
@@ -382,124 +417,125 @@ public class LiveUpdate {
|
|
|
382
417
|
}
|
|
383
418
|
}
|
|
384
419
|
|
|
385
|
-
private void
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
|
-
callback.error(exception);
|
|
403
|
-
}
|
|
420
|
+
private void downloadAndVerifyFile(@NonNull String url, @NonNull File file, @Nullable String checksum) throws Exception {
|
|
421
|
+
Response response = httpClient.execute(url);
|
|
422
|
+
ResponseBody responseBody = response.body();
|
|
423
|
+
if (response.isSuccessful()) {
|
|
424
|
+
LiveUpdateHttpClient.writeResponseBodyToFile(responseBody, file);
|
|
425
|
+
// Verify the file
|
|
426
|
+
checksum = checksum == null ? LiveUpdateHttpClient.getChecksumFromResponse(response) : checksum;
|
|
427
|
+
String signature = LiveUpdateHttpClient.getSignatureFromResponse(response);
|
|
428
|
+
verifyFile(file, checksum, signature);
|
|
429
|
+
} else {
|
|
430
|
+
Exception exception = new Exception(responseBody.string());
|
|
431
|
+
Logger.error(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
|
|
432
|
+
throw new Exception(LiveUpdatePlugin.ERROR_DOWNLOAD_FAILED);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
404
435
|
|
|
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
|
-
// Add the bundle
|
|
431
|
-
addBundle(bundleId, result);
|
|
432
|
-
|
|
433
|
-
// Delete the temporary file
|
|
434
|
-
result.delete();
|
|
435
|
-
|
|
436
|
-
callback.success();
|
|
437
|
-
} catch (Exception exception) {
|
|
438
|
-
callback.error(exception);
|
|
436
|
+
private File downloadBundleFile(@NonNull String baseUrl, @NonNull String href, @NonNull File destinationDirectory) throws Exception {
|
|
437
|
+
HttpUrl.Builder urlBuilder = HttpUrl.parse(baseUrl).newBuilder();
|
|
438
|
+
urlBuilder.addQueryParameter("href", href);
|
|
439
|
+
String url = urlBuilder.build().toString();
|
|
440
|
+
|
|
441
|
+
File destinationFile = new File(destinationDirectory, href);
|
|
442
|
+
destinationFile.getParentFile().mkdirs();
|
|
443
|
+
downloadAndVerifyFile(url, destinationFile, null);
|
|
444
|
+
return destinationFile;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private void downloadBundleFiles(@NonNull String baseUrl, @NonNull List<String> hrefs, @NonNull File destinationDirectory)
|
|
448
|
+
throws Exception {
|
|
449
|
+
// Limit the number of threads to twice the number of processors
|
|
450
|
+
int maxThreads = Runtime.getRuntime().availableProcessors() * 2;
|
|
451
|
+
List<Thread> threads = new ArrayList<>();
|
|
452
|
+
AtomicReference<Exception> exceptionReference = new AtomicReference<>();
|
|
453
|
+
for (String href : hrefs) {
|
|
454
|
+
// Wait until a thread is available
|
|
455
|
+
while (threads.size() >= maxThreads) {
|
|
456
|
+
for (Thread thread : threads) {
|
|
457
|
+
if (!thread.isAlive()) {
|
|
458
|
+
threads.remove(thread);
|
|
459
|
+
break;
|
|
439
460
|
}
|
|
440
461
|
}
|
|
462
|
+
// Wait a bit before checking again
|
|
463
|
+
Thread.sleep(10);
|
|
441
464
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
okHttpClient
|
|
454
|
-
.newCall(request)
|
|
455
|
-
.enqueue(
|
|
456
|
-
new Callback() {
|
|
457
|
-
@Override
|
|
458
|
-
public void onFailure(@NonNull Call call, @NonNull IOException exception) {
|
|
459
|
-
callback.error(exception);
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
@Override
|
|
463
|
-
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
|
|
464
|
-
ResponseBody responseBody = response.body();
|
|
465
|
-
if (response.isSuccessful()) {
|
|
466
|
-
File destinationFile = buildTemporaryZipFile();
|
|
467
|
-
|
|
468
|
-
long contentLength = responseBody.contentLength();
|
|
469
|
-
BufferedSource source = responseBody.source();
|
|
470
|
-
|
|
471
|
-
BufferedSink sink = Okio.buffer(Okio.sink(destinationFile));
|
|
472
|
-
Buffer sinkBuffer = sink.getBuffer();
|
|
473
|
-
|
|
474
|
-
long totalBytesRead = 0;
|
|
475
|
-
int bufferSize = 8 * 1024;
|
|
476
|
-
for (long bytesRead; (bytesRead = source.read(sinkBuffer, bufferSize)) != -1;) {
|
|
477
|
-
sink.emit();
|
|
478
|
-
totalBytesRead += bytesRead;
|
|
479
|
-
int progress = (int) ((totalBytesRead * 100) / contentLength);
|
|
480
|
-
}
|
|
481
|
-
sink.flush();
|
|
482
|
-
sink.close();
|
|
483
|
-
source.close();
|
|
484
|
-
callback.success(destinationFile);
|
|
485
|
-
} else {
|
|
486
|
-
Exception exception = new Exception(responseBody.string());
|
|
487
|
-
Logger.error(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
|
|
488
|
-
callback.error(new Exception(LiveUpdatePlugin.ERROR_DOWNLOAD_FAILED));
|
|
489
|
-
}
|
|
490
|
-
}
|
|
465
|
+
// Stop if an exception occurred
|
|
466
|
+
if (exceptionReference.get() != null) {
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
// Start a new thread
|
|
470
|
+
Thread thread = new Thread(() -> {
|
|
471
|
+
try {
|
|
472
|
+
downloadBundleFile(baseUrl, href, destinationDirectory);
|
|
473
|
+
} catch (Exception exception) {
|
|
474
|
+
exceptionReference.set(exception);
|
|
475
|
+
Logger.error(LiveUpdatePlugin.TAG, "Failed to download file: " + href, exception);
|
|
491
476
|
}
|
|
492
|
-
);
|
|
477
|
+
});
|
|
478
|
+
threads.add(thread);
|
|
479
|
+
thread.start();
|
|
480
|
+
}
|
|
481
|
+
// Wait for all threads to finish
|
|
482
|
+
for (Thread thread : threads) {
|
|
483
|
+
thread.join();
|
|
484
|
+
}
|
|
485
|
+
// Throw the exception if one occurred
|
|
486
|
+
Exception exception = exceptionReference.get();
|
|
487
|
+
if (exception != null) {
|
|
488
|
+
throw exception;
|
|
489
|
+
}
|
|
493
490
|
}
|
|
494
491
|
|
|
495
|
-
@
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
492
|
+
private void downloadBundleOfTypeManifest(@NonNull String bundleId, @NonNull String downloadUrl) throws Exception {
|
|
493
|
+
// Create a temporary directory
|
|
494
|
+
File temporaryDirectory = createTemporaryDirectory();
|
|
495
|
+
// Download the latest manifest
|
|
496
|
+
File latestManifestFile = downloadBundleFile(downloadUrl, manifestFileName, temporaryDirectory);
|
|
497
|
+
Manifest latestManifest = loadManifest(latestManifestFile);
|
|
498
|
+
// Load the current manifest
|
|
499
|
+
Manifest currentManifest = loadCurrentManifest();
|
|
500
|
+
// Compare the manifests
|
|
501
|
+
List<ManifestItem> itemsToCopy = new ArrayList<>();
|
|
502
|
+
List<ManifestItem> itemsToDownload = new ArrayList<>();
|
|
503
|
+
if (currentManifest == null) {
|
|
504
|
+
itemsToDownload.addAll(latestManifest.getItems());
|
|
505
|
+
} else {
|
|
506
|
+
itemsToCopy.addAll(Manifest.findDuplicateItems(latestManifest, currentManifest));
|
|
507
|
+
itemsToDownload.addAll(Manifest.findMissingItems(latestManifest, currentManifest));
|
|
508
|
+
}
|
|
509
|
+
// Copy the files
|
|
510
|
+
List<String> hrefsToCopy = new ArrayList<>();
|
|
511
|
+
for (ManifestItem item : itemsToCopy) {
|
|
512
|
+
hrefsToCopy.add(item.getHref());
|
|
501
513
|
}
|
|
502
|
-
|
|
514
|
+
copyCurrentBundleFiles(hrefsToCopy, temporaryDirectory);
|
|
515
|
+
// Download the files
|
|
516
|
+
List<String> hrefsToDownload = new ArrayList<>();
|
|
517
|
+
for (ManifestItem item : itemsToDownload) {
|
|
518
|
+
hrefsToDownload.add(item.getHref());
|
|
519
|
+
}
|
|
520
|
+
downloadBundleFiles(downloadUrl, hrefsToDownload, temporaryDirectory);
|
|
521
|
+
// Add the bundle
|
|
522
|
+
addBundleOfTypeManifest(bundleId, temporaryDirectory);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
private void downloadBundleOfTypeZip(@NonNull String bundleId, @NonNull String downloadUrl, @Nullable String checksum)
|
|
526
|
+
throws Exception {
|
|
527
|
+
File file = buildTemporaryZipFile();
|
|
528
|
+
// Download the bundle
|
|
529
|
+
downloadAndVerifyFile(downloadUrl, file, checksum);
|
|
530
|
+
// Add the bundle
|
|
531
|
+
addBundleOfTypeZip(bundleId, file);
|
|
532
|
+
// Delete the temporary file
|
|
533
|
+
file.delete();
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
@Nullable
|
|
537
|
+
private GetLatestBundleResponse fetchLatestBundle() throws Exception {
|
|
538
|
+
String url = new HttpUrl.Builder()
|
|
503
539
|
.scheme("https")
|
|
504
540
|
.host(host)
|
|
505
541
|
.addPathSegment("v1")
|
|
@@ -516,47 +552,18 @@ public class LiveUpdate {
|
|
|
516
552
|
.addQueryParameter("osVersion", String.valueOf(Build.VERSION.SDK_INT))
|
|
517
553
|
.addQueryParameter("platform", "0")
|
|
518
554
|
.addQueryParameter("pluginVersion", LiveUpdatePlugin.VERSION)
|
|
519
|
-
.build()
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
.enqueue(
|
|
532
|
-
new okhttp3.Callback() {
|
|
533
|
-
@Override
|
|
534
|
-
public void onFailure(@NonNull okhttp3.Call call, @NonNull IOException exception) {
|
|
535
|
-
Logger.error(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
|
|
536
|
-
callback.error(exception);
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
@Override
|
|
540
|
-
public void onResponse(@NonNull okhttp3.Call call, @NonNull okhttp3.Response response) throws IOException {
|
|
541
|
-
ResponseBody responseBody = response.body();
|
|
542
|
-
if (response.isSuccessful()) {
|
|
543
|
-
String responseBodyString = responseBody.string();
|
|
544
|
-
JSONObject responseJson = null;
|
|
545
|
-
try {
|
|
546
|
-
responseJson = new JSONObject(responseBodyString);
|
|
547
|
-
} catch (JSONException exception) {
|
|
548
|
-
callback.error(exception);
|
|
549
|
-
}
|
|
550
|
-
GetLatestBundleResponse getLatestBundleResponse = new GetLatestBundleResponse(responseJson);
|
|
551
|
-
callback.success(getLatestBundleResponse);
|
|
552
|
-
} else {
|
|
553
|
-
Exception exception = new Exception(responseBody.string());
|
|
554
|
-
Logger.error(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
|
|
555
|
-
callback.error(exception);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
);
|
|
555
|
+
.build()
|
|
556
|
+
.toString();
|
|
557
|
+
Logger.debug(LiveUpdatePlugin.TAG, "Fetching latest bundle: " + url);
|
|
558
|
+
Response response = httpClient.execute(url);
|
|
559
|
+
String responseBodyString = response.body().string();
|
|
560
|
+
Logger.debug(LiveUpdatePlugin.TAG, "Latest bundle response: " + responseBodyString);
|
|
561
|
+
if (response.isSuccessful()) {
|
|
562
|
+
JSONObject responseJson = new JSONObject(responseBodyString);
|
|
563
|
+
return new GetLatestBundleResponse(responseJson);
|
|
564
|
+
} else {
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
560
567
|
}
|
|
561
568
|
|
|
562
569
|
private String[] getBundleIds() {
|
|
@@ -574,6 +581,31 @@ public class LiveUpdate {
|
|
|
574
581
|
return bundleIds;
|
|
575
582
|
}
|
|
576
583
|
|
|
584
|
+
private byte[] getChecksumForFileAsBytes(@NonNull File file) throws Exception {
|
|
585
|
+
try {
|
|
586
|
+
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
587
|
+
BufferedSource source = Okio.buffer(Okio.source(file));
|
|
588
|
+
Buffer buffer = new Buffer();
|
|
589
|
+
for (long bytesRead; (bytesRead = source.read(buffer, 2048)) != -1;) {
|
|
590
|
+
digest.update(buffer.readByteArray());
|
|
591
|
+
}
|
|
592
|
+
source.close();
|
|
593
|
+
return digest.digest();
|
|
594
|
+
} catch (IOException exception) {
|
|
595
|
+
Logger.error(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
|
|
596
|
+
throw new Exception(LiveUpdatePlugin.ERROR_CHECKSUM_CALCULATION_FAILED);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
private String getChecksumForFileAsString(@NonNull File file) throws Exception {
|
|
601
|
+
byte[] checksumBytes = getChecksumForFileAsBytes(file);
|
|
602
|
+
StringBuilder checksum = new StringBuilder();
|
|
603
|
+
for (byte checksumByte : checksumBytes) {
|
|
604
|
+
checksum.append(Integer.toString((checksumByte & 0xff) + 0x100, 16).substring(1));
|
|
605
|
+
}
|
|
606
|
+
return checksum.toString();
|
|
607
|
+
}
|
|
608
|
+
|
|
577
609
|
@Nullable
|
|
578
610
|
private String getChannel() {
|
|
579
611
|
String channel = null;
|
|
@@ -650,6 +682,41 @@ public class LiveUpdate {
|
|
|
650
682
|
return bundleDirectory.exists();
|
|
651
683
|
}
|
|
652
684
|
|
|
685
|
+
@Nullable
|
|
686
|
+
private Manifest loadCurrentManifest() throws Exception {
|
|
687
|
+
String currentBundleId = getCurrentBundleId();
|
|
688
|
+
if (currentBundleId.equals(defaultWebAssetDir)) {
|
|
689
|
+
AssetManager assets = plugin.getContext().getAssets();
|
|
690
|
+
Boolean manifestFileExists = Arrays.asList(assets.list(defaultWebAssetDir)).contains(manifestFileName);
|
|
691
|
+
if (manifestFileExists) {
|
|
692
|
+
InputStream inputStream = assets.open(defaultWebAssetDir + "/" + manifestFileName);
|
|
693
|
+
BufferedSource source = Okio.buffer(Okio.source(inputStream));
|
|
694
|
+
return loadManifest(source);
|
|
695
|
+
} else {
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
} else {
|
|
699
|
+
File currentBundleDirectory = buildBundleDirectoryFor(currentBundleId);
|
|
700
|
+
File manifestFile = new File(currentBundleDirectory, manifestFileName);
|
|
701
|
+
if (manifestFile.exists()) {
|
|
702
|
+
return loadManifest(manifestFile);
|
|
703
|
+
} else {
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
private Manifest loadManifest(@NonNull BufferedSource source) throws Exception {
|
|
710
|
+
String jsonAsString = source.readUtf8();
|
|
711
|
+
JSONArray jsonArray = new JSONArray(jsonAsString);
|
|
712
|
+
return new Manifest(jsonArray);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
private Manifest loadManifest(@NonNull File file) throws Exception {
|
|
716
|
+
BufferedSource source = Okio.buffer(Okio.source(file));
|
|
717
|
+
return loadManifest(source);
|
|
718
|
+
}
|
|
719
|
+
|
|
653
720
|
private boolean isBundleInUse(@NonNull String bundleId) {
|
|
654
721
|
String currentBundleId = getCurrentBundleId();
|
|
655
722
|
String nextBundleId = getNextBundleId();
|
|
@@ -739,6 +806,39 @@ public class LiveUpdate {
|
|
|
739
806
|
return destination;
|
|
740
807
|
}
|
|
741
808
|
|
|
809
|
+
private boolean verifyChecksumForFile(@NonNull File file, @NonNull String checksum) throws Exception {
|
|
810
|
+
String receivedChecksum = getChecksumForFileAsString(file);
|
|
811
|
+
return checksum.equals(receivedChecksum);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
private void verifyFile(@NonNull File file, @Nullable String checksum, @Nullable String signature) throws Exception {
|
|
815
|
+
String publicKey = config.getPublicKey();
|
|
816
|
+
if (publicKey != null) {
|
|
817
|
+
// Verify the signature
|
|
818
|
+
if (signature == null) {
|
|
819
|
+
throw new Exception(LiveUpdatePlugin.ERROR_SIGNATURE_MISSING);
|
|
820
|
+
}
|
|
821
|
+
// Verify the signature
|
|
822
|
+
boolean verified = verifySignatureForFile(file, signature, publicKey);
|
|
823
|
+
if (!verified) {
|
|
824
|
+
throw new Exception(LiveUpdatePlugin.ERROR_SIGNATURE_VERIFICATION_FAILED);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
// Verify the checksum
|
|
828
|
+
else if (checksum != null) {
|
|
829
|
+
// Calculate the checksum
|
|
830
|
+
boolean verified = verifyChecksumForFile(file, checksum);
|
|
831
|
+
if (!verified) {
|
|
832
|
+
throw new Exception(LiveUpdatePlugin.ERROR_CHECKSUM_MISMATCH);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
private boolean verifySignatureForFile(@NonNull File file, @NonNull String signature, @NonNull String keyAsString) throws Exception {
|
|
838
|
+
PublicKey key = createPublicKeyFromString(keyAsString);
|
|
839
|
+
return verifySignatureForFile(file, signature, key);
|
|
840
|
+
}
|
|
841
|
+
|
|
742
842
|
private boolean verifySignatureForFile(@NonNull File file, @NonNull String signature, @NonNull PublicKey key) throws Exception {
|
|
743
843
|
try {
|
|
744
844
|
byte[] signatureBytes = Base64.decode(signature, Base64.DEFAULT);
|