@capawesome/capacitor-live-update 6.4.2 → 6.6.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 +46 -5
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +363 -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 +65 -60
- 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/options/DownloadBundleOptions.java +15 -1
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/FetchLatestBundleResult.java +22 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/enums/ArtifactType.java +6 -0
- package/dist/docs.json +67 -4
- package/dist/esm/definitions.d.ts +42 -3
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +2 -1
- package/dist/esm/web.js +3 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +3 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +3 -0
- package/dist/plugin.js.map +1 -1
- package/ios/Plugin/Classes/Manifest.swift +20 -0
- package/ios/Plugin/Classes/Options/DownloadBundleOptions.swift +11 -1
- package/ios/Plugin/Classes/Results/FetchLatestBundle.swift +16 -0
- package/ios/Plugin/Enums/ArtifactType.swift +4 -0
- package/ios/Plugin/{Classes → Enums}/CustomError.swift +3 -0
- package/ios/Plugin/LiveUpdate.swift +279 -198
- package/ios/Plugin/LiveUpdateHttpClient.swift +42 -0
- package/ios/Plugin/LiveUpdatePlugin.m +1 -0
- package/ios/Plugin/LiveUpdatePlugin.swift +77 -61
- 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,12 +12,15 @@ 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;
|
|
17
20
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetBundleOptions;
|
|
18
21
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetChannelOptions;
|
|
19
22
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetCustomIdOptions;
|
|
23
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.FetchLatestBundleResult;
|
|
20
24
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetBundleResult;
|
|
21
25
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetBundlesResult;
|
|
22
26
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetChannelResult;
|
|
@@ -25,32 +29,33 @@ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetDeviceIdR
|
|
|
25
29
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetVersionCodeResult;
|
|
26
30
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetVersionNameResult;
|
|
27
31
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.SyncResult;
|
|
32
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.enums.ArtifactType;
|
|
28
33
|
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.EmptyCallback;
|
|
29
34
|
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.NonEmptyCallback;
|
|
30
35
|
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.Result;
|
|
31
36
|
import java.io.File;
|
|
37
|
+
import java.io.FileInputStream;
|
|
38
|
+
import java.io.FileOutputStream;
|
|
32
39
|
import java.io.IOException;
|
|
40
|
+
import java.io.InputStream;
|
|
33
41
|
import java.security.KeyFactory;
|
|
34
42
|
import java.security.MessageDigest;
|
|
35
43
|
import java.security.PublicKey;
|
|
36
44
|
import java.security.Signature;
|
|
37
45
|
import java.security.spec.X509EncodedKeySpec;
|
|
46
|
+
import java.util.ArrayList;
|
|
38
47
|
import java.util.Arrays;
|
|
48
|
+
import java.util.List;
|
|
39
49
|
import java.util.UUID;
|
|
40
|
-
import java.util.concurrent.
|
|
50
|
+
import java.util.concurrent.atomic.AtomicReference;
|
|
41
51
|
import net.lingala.zip4j.ZipFile;
|
|
42
|
-
import okhttp3.Call;
|
|
43
|
-
import okhttp3.Callback;
|
|
44
52
|
import okhttp3.HttpUrl;
|
|
45
|
-
import okhttp3.OkHttpClient;
|
|
46
|
-
import okhttp3.Request;
|
|
47
53
|
import okhttp3.Response;
|
|
48
54
|
import okhttp3.ResponseBody;
|
|
49
55
|
import okio.Buffer;
|
|
50
|
-
import okio.BufferedSink;
|
|
51
56
|
import okio.BufferedSource;
|
|
52
57
|
import okio.Okio;
|
|
53
|
-
import org.json.
|
|
58
|
+
import org.json.JSONArray;
|
|
54
59
|
import org.json.JSONObject;
|
|
55
60
|
|
|
56
61
|
public class LiveUpdate {
|
|
@@ -60,6 +65,12 @@ public class LiveUpdate {
|
|
|
60
65
|
|
|
61
66
|
private final String defaultWebAssetDir;
|
|
62
67
|
|
|
68
|
+
@NonNull
|
|
69
|
+
private final String host;
|
|
70
|
+
|
|
71
|
+
@NonNull
|
|
72
|
+
private final LiveUpdateHttpClient httpClient;
|
|
73
|
+
|
|
63
74
|
@NonNull
|
|
64
75
|
private final LiveUpdatePlugin plugin;
|
|
65
76
|
|
|
@@ -69,12 +80,19 @@ public class LiveUpdate {
|
|
|
69
80
|
@NonNull
|
|
70
81
|
private final SharedPreferences.Editor webViewSettingsEditor;
|
|
71
82
|
|
|
72
|
-
private final String bundlesDirectory = "_capacitor_live_update_bundles";
|
|
83
|
+
private final String bundlesDirectory = "_capacitor_live_update_bundles"; // DO NOT CHANGE!
|
|
73
84
|
private final Handler rollbackHandler = new Handler();
|
|
85
|
+
private final String manifestFileName = "capawesome-live-update-manifest.json"; // DO NOT CHANGE!
|
|
74
86
|
|
|
75
87
|
public LiveUpdate(@NonNull LiveUpdateConfig config, @NonNull LiveUpdatePlugin plugin) throws PackageManager.NameNotFoundException {
|
|
76
88
|
this.config = config;
|
|
77
89
|
this.defaultWebAssetDir = plugin.getBridge().DEFAULT_WEB_ASSET_DIR;
|
|
90
|
+
if (config.getLocation() != null && config.getLocation().equals("eu")) {
|
|
91
|
+
this.host = "api.cloud.capawesome.eu";
|
|
92
|
+
} else {
|
|
93
|
+
this.host = "api.cloud.capawesome.io";
|
|
94
|
+
}
|
|
95
|
+
this.httpClient = new LiveUpdateHttpClient(config);
|
|
78
96
|
this.plugin = plugin;
|
|
79
97
|
this.preferences = new LiveUpdatePreferences(plugin.getContext());
|
|
80
98
|
this.webViewSettingsEditor = plugin.getContext().getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE).edit();
|
|
@@ -103,6 +121,7 @@ public class LiveUpdate {
|
|
|
103
121
|
}
|
|
104
122
|
|
|
105
123
|
public void downloadBundle(@NonNull DownloadBundleOptions options, @NonNull EmptyCallback callback) throws Exception {
|
|
124
|
+
ArtifactType artifactType = options.getArtifactType();
|
|
106
125
|
String bundleId = options.getBundleId();
|
|
107
126
|
String checksum = options.getChecksum();
|
|
108
127
|
String url = options.getUrl();
|
|
@@ -115,7 +134,18 @@ public class LiveUpdate {
|
|
|
115
134
|
}
|
|
116
135
|
|
|
117
136
|
// Download the bundle
|
|
118
|
-
|
|
137
|
+
if (artifactType == ArtifactType.MANIFEST) {
|
|
138
|
+
downloadBundleOfTypeManifest(bundleId, url);
|
|
139
|
+
} else {
|
|
140
|
+
downloadBundleOfTypeZip(bundleId, url, checksum);
|
|
141
|
+
}
|
|
142
|
+
callback.success();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
public void fetchLatestBundle(@NonNull NonEmptyCallback callback) throws Exception {
|
|
146
|
+
GetLatestBundleResponse response = fetchLatestBundle();
|
|
147
|
+
FetchLatestBundleResult result = new FetchLatestBundleResult(response == null ? null : response.getBundleId());
|
|
148
|
+
callback.success(result);
|
|
119
149
|
}
|
|
120
150
|
|
|
121
151
|
public void getBundle(@NonNull NonEmptyCallback callback) {
|
|
@@ -165,7 +195,9 @@ public class LiveUpdate {
|
|
|
165
195
|
|
|
166
196
|
public void ready() {
|
|
167
197
|
Logger.debug(LiveUpdatePlugin.TAG, "App is ready.");
|
|
198
|
+
// Stop the rollback timer
|
|
168
199
|
stopRollbackTimer();
|
|
200
|
+
// Delete unused bundles
|
|
169
201
|
if (config.getAutoDeleteBundles()) {
|
|
170
202
|
deleteUnusedBundles();
|
|
171
203
|
}
|
|
@@ -210,73 +242,44 @@ public class LiveUpdate {
|
|
|
210
242
|
|
|
211
243
|
public void sync(@NonNull NonEmptyCallback<Result> callback) throws Exception {
|
|
212
244
|
// 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
|
-
}
|
|
245
|
+
GetLatestBundleResponse response = fetchLatestBundle();
|
|
246
|
+
if (response == null) {
|
|
247
|
+
Logger.debug(LiveUpdatePlugin.TAG, "No update available.");
|
|
248
|
+
SyncResult syncResult = new SyncResult(null);
|
|
249
|
+
callback.success(syncResult);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
ArtifactType artifactType = response.getArtifactType();
|
|
253
|
+
String latestBundleId = response.getBundleId();
|
|
254
|
+
String url = response.getUrl();
|
|
255
|
+
// Check if the bundle already exists
|
|
256
|
+
if (hasBundle(latestBundleId)) {
|
|
257
|
+
String nextBundleId = null;
|
|
258
|
+
String currentBundleId = getCurrentBundleId();
|
|
259
|
+
if (!latestBundleId.equals(currentBundleId)) {
|
|
260
|
+
// Set the next bundle
|
|
261
|
+
setNextBundle(latestBundleId);
|
|
262
|
+
nextBundleId = latestBundleId;
|
|
270
263
|
}
|
|
271
|
-
|
|
264
|
+
SyncResult syncResult = new SyncResult(nextBundleId);
|
|
265
|
+
callback.success(syncResult);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
// Download the bundle
|
|
269
|
+
if (artifactType == ArtifactType.MANIFEST) {
|
|
270
|
+
downloadBundleOfTypeManifest(latestBundleId, url);
|
|
271
|
+
} else {
|
|
272
|
+
downloadBundleOfTypeZip(latestBundleId, url, null);
|
|
273
|
+
}
|
|
274
|
+
// Set the next bundle
|
|
275
|
+
setNextBundle(latestBundleId);
|
|
276
|
+
SyncResult syncResult = new SyncResult(latestBundleId);
|
|
277
|
+
callback.success(syncResult);
|
|
272
278
|
}
|
|
273
279
|
|
|
274
|
-
private void addBundle(@NonNull String bundleId, @NonNull File
|
|
275
|
-
// Unzip the file to the bundle directory
|
|
276
|
-
File unzippedDirectory = unzipFile(zipFile);
|
|
277
|
-
|
|
280
|
+
private void addBundle(@NonNull String bundleId, @NonNull File sourceDirectory) throws Exception {
|
|
278
281
|
// Search folder with index.html file
|
|
279
|
-
File indexHtmlFile = searchIndexHtmlFile(
|
|
282
|
+
File indexHtmlFile = searchIndexHtmlFile(sourceDirectory);
|
|
280
283
|
if (indexHtmlFile == null) {
|
|
281
284
|
throw new Exception(LiveUpdatePlugin.ERROR_BUNDLE_INDEX_HTML_MISSING);
|
|
282
285
|
}
|
|
@@ -289,6 +292,25 @@ public class LiveUpdate {
|
|
|
289
292
|
indexHtmlFile.getParentFile().renameTo(bundleDirectory);
|
|
290
293
|
}
|
|
291
294
|
|
|
295
|
+
private void addBundleOfTypeManifest(@NonNull String bundleId, @NonNull File directory) throws Exception {
|
|
296
|
+
addBundle(bundleId, directory);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private void addBundleOfTypeZip(@NonNull String bundleId, @NonNull File zipFile) throws Exception {
|
|
300
|
+
// Unzip the file to the bundle directory
|
|
301
|
+
File unzippedDirectory = unzipFile(zipFile);
|
|
302
|
+
// Add the bundle
|
|
303
|
+
addBundle(bundleId, unzippedDirectory);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private File buildBundlesDirectory() {
|
|
307
|
+
return new File(plugin.getContext().getFilesDir(), bundlesDirectory);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private File buildBundleDirectoryFor(@NonNull String bundleId) {
|
|
311
|
+
return new File(plugin.getContext().getFilesDir(), bundlesDirectory + "/" + bundleId);
|
|
312
|
+
}
|
|
313
|
+
|
|
292
314
|
private File buildTemporaryDirectory() {
|
|
293
315
|
String fileName = UUID.randomUUID().toString();
|
|
294
316
|
return new File(plugin.getContext().getCacheDir(), fileName);
|
|
@@ -299,43 +321,63 @@ public class LiveUpdate {
|
|
|
299
321
|
return new File(plugin.getContext().getCacheDir(), fileName);
|
|
300
322
|
}
|
|
301
323
|
|
|
302
|
-
private File
|
|
303
|
-
|
|
324
|
+
private void copyCurrentBundleFile(@NonNull String href, @NonNull File destinationDirectory) throws IOException {
|
|
325
|
+
String currentBundleId = getCurrentBundleId();
|
|
326
|
+
if (currentBundleId.equals(defaultWebAssetDir)) {
|
|
327
|
+
// Create the source input stream
|
|
328
|
+
AssetManager assets = plugin.getContext().getAssets();
|
|
329
|
+
InputStream inputStream = assets.open(defaultWebAssetDir + "/" + href);
|
|
330
|
+
// Create the destination file
|
|
331
|
+
File destination = new File(destinationDirectory, href);
|
|
332
|
+
// Create all destination directories if they do not exist
|
|
333
|
+
destination.getParentFile().mkdirs();
|
|
334
|
+
// Copy the file
|
|
335
|
+
copyFile(inputStream, destination);
|
|
336
|
+
} else {
|
|
337
|
+
File currentBundleDirectory = buildBundleDirectoryFor(currentBundleId);
|
|
338
|
+
// Create the source and destination files
|
|
339
|
+
File source = new File(currentBundleDirectory, href);
|
|
340
|
+
File destination = new File(destinationDirectory, href);
|
|
341
|
+
// Create all destination directories if they do not exist
|
|
342
|
+
destination.getParentFile().mkdirs();
|
|
343
|
+
// Copy the file
|
|
344
|
+
copyFile(source, destination);
|
|
345
|
+
}
|
|
304
346
|
}
|
|
305
347
|
|
|
306
|
-
private
|
|
307
|
-
|
|
348
|
+
private void copyCurrentBundleFiles(@NonNull List<String> hrefs, @NonNull File destinationDirectory) throws IOException {
|
|
349
|
+
for (String href : hrefs) {
|
|
350
|
+
copyCurrentBundleFile(href, destinationDirectory);
|
|
351
|
+
}
|
|
308
352
|
}
|
|
309
353
|
|
|
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);
|
|
354
|
+
private void copyFile(File input, File output) throws IOException {
|
|
355
|
+
FileInputStream in = new FileInputStream(input);
|
|
356
|
+
FileOutputStream out = new FileOutputStream(output);
|
|
357
|
+
byte[] buffer = new byte[1024];
|
|
358
|
+
int length;
|
|
359
|
+
while ((length = in.read(buffer)) > 0) {
|
|
360
|
+
out.write(buffer, 0, length);
|
|
323
361
|
}
|
|
362
|
+
out.close();
|
|
363
|
+
in.close();
|
|
324
364
|
}
|
|
325
365
|
|
|
326
|
-
private
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
366
|
+
private void copyFile(InputStream input, File output) throws IOException {
|
|
367
|
+
FileOutputStream out = new FileOutputStream(output);
|
|
368
|
+
byte[] buffer = new byte[1024];
|
|
369
|
+
int length;
|
|
370
|
+
while ((length = input.read(buffer)) > 0) {
|
|
371
|
+
out.write(buffer, 0, length);
|
|
331
372
|
}
|
|
332
|
-
|
|
373
|
+
out.close();
|
|
374
|
+
input.close();
|
|
333
375
|
}
|
|
334
376
|
|
|
335
377
|
private void createBundlesDirectory() {
|
|
336
|
-
File
|
|
337
|
-
if (!
|
|
338
|
-
|
|
378
|
+
File file = buildBundlesDirectory();
|
|
379
|
+
if (!file.exists()) {
|
|
380
|
+
file.mkdir();
|
|
339
381
|
}
|
|
340
382
|
}
|
|
341
383
|
|
|
@@ -352,14 +394,19 @@ public class LiveUpdate {
|
|
|
352
394
|
}
|
|
353
395
|
}
|
|
354
396
|
|
|
397
|
+
private File createTemporaryDirectory() {
|
|
398
|
+
File file = buildTemporaryDirectory();
|
|
399
|
+
file.mkdir();
|
|
400
|
+
return file;
|
|
401
|
+
}
|
|
402
|
+
|
|
355
403
|
private void deleteBundle(@NonNull String bundleId) {
|
|
356
404
|
// Delete the bundle directory
|
|
357
405
|
File bundleDirectory = buildBundleDirectoryFor(bundleId);
|
|
358
406
|
deleteFileRecursively(bundleDirectory);
|
|
359
407
|
// Reset the next bundle if it is the deleted bundle
|
|
360
|
-
String currentBundleId = getCurrentBundleId();
|
|
361
408
|
String nextBundleId = getNextBundleId();
|
|
362
|
-
if (bundleId.equals(
|
|
409
|
+
if (bundleId.equals(nextBundleId)) {
|
|
363
410
|
setNextCapacitorServerPathToDefaultWebAssetDir();
|
|
364
411
|
}
|
|
365
412
|
}
|
|
@@ -382,124 +429,125 @@ public class LiveUpdate {
|
|
|
382
429
|
}
|
|
383
430
|
}
|
|
384
431
|
|
|
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
|
-
}
|
|
432
|
+
private void downloadAndVerifyFile(@NonNull String url, @NonNull File file, @Nullable String checksum) throws Exception {
|
|
433
|
+
Response response = httpClient.execute(url);
|
|
434
|
+
ResponseBody responseBody = response.body();
|
|
435
|
+
if (response.isSuccessful()) {
|
|
436
|
+
LiveUpdateHttpClient.writeResponseBodyToFile(responseBody, file);
|
|
437
|
+
// Verify the file
|
|
438
|
+
checksum = checksum == null ? LiveUpdateHttpClient.getChecksumFromResponse(response) : checksum;
|
|
439
|
+
String signature = LiveUpdateHttpClient.getSignatureFromResponse(response);
|
|
440
|
+
verifyFile(file, checksum, signature);
|
|
441
|
+
} else {
|
|
442
|
+
Exception exception = new Exception(responseBody.string());
|
|
443
|
+
Logger.error(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
|
|
444
|
+
throw new Exception(LiveUpdatePlugin.ERROR_DOWNLOAD_FAILED);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
404
447
|
|
|
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);
|
|
448
|
+
private File downloadBundleFile(@NonNull String baseUrl, @NonNull String href, @NonNull File destinationDirectory) throws Exception {
|
|
449
|
+
HttpUrl.Builder urlBuilder = HttpUrl.parse(baseUrl).newBuilder();
|
|
450
|
+
urlBuilder.addQueryParameter("href", href);
|
|
451
|
+
String url = urlBuilder.build().toString();
|
|
452
|
+
|
|
453
|
+
File destinationFile = new File(destinationDirectory, href);
|
|
454
|
+
destinationFile.getParentFile().mkdirs();
|
|
455
|
+
downloadAndVerifyFile(url, destinationFile, null);
|
|
456
|
+
return destinationFile;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private void downloadBundleFiles(@NonNull String baseUrl, @NonNull List<String> hrefs, @NonNull File destinationDirectory)
|
|
460
|
+
throws Exception {
|
|
461
|
+
// Limit the number of threads to twice the number of processors
|
|
462
|
+
int maxThreads = Runtime.getRuntime().availableProcessors() * 2;
|
|
463
|
+
List<Thread> threads = new ArrayList<>();
|
|
464
|
+
AtomicReference<Exception> exceptionReference = new AtomicReference<>();
|
|
465
|
+
for (String href : hrefs) {
|
|
466
|
+
// Wait until a thread is available
|
|
467
|
+
while (threads.size() >= maxThreads) {
|
|
468
|
+
for (Thread thread : threads) {
|
|
469
|
+
if (!thread.isAlive()) {
|
|
470
|
+
threads.remove(thread);
|
|
471
|
+
break;
|
|
439
472
|
}
|
|
440
473
|
}
|
|
474
|
+
// Wait a bit before checking again
|
|
475
|
+
Thread.sleep(10);
|
|
441
476
|
}
|
|
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
|
-
}
|
|
477
|
+
// Stop if an exception occurred
|
|
478
|
+
if (exceptionReference.get() != null) {
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
// Start a new thread
|
|
482
|
+
Thread thread = new Thread(() -> {
|
|
483
|
+
try {
|
|
484
|
+
downloadBundleFile(baseUrl, href, destinationDirectory);
|
|
485
|
+
} catch (Exception exception) {
|
|
486
|
+
exceptionReference.set(exception);
|
|
487
|
+
Logger.error(LiveUpdatePlugin.TAG, "Failed to download file: " + href, exception);
|
|
491
488
|
}
|
|
492
|
-
);
|
|
489
|
+
});
|
|
490
|
+
threads.add(thread);
|
|
491
|
+
thread.start();
|
|
492
|
+
}
|
|
493
|
+
// Wait for all threads to finish
|
|
494
|
+
for (Thread thread : threads) {
|
|
495
|
+
thread.join();
|
|
496
|
+
}
|
|
497
|
+
// Throw the exception if one occurred
|
|
498
|
+
Exception exception = exceptionReference.get();
|
|
499
|
+
if (exception != null) {
|
|
500
|
+
throw exception;
|
|
501
|
+
}
|
|
493
502
|
}
|
|
494
503
|
|
|
495
|
-
@
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
504
|
+
private void downloadBundleOfTypeManifest(@NonNull String bundleId, @NonNull String downloadUrl) throws Exception {
|
|
505
|
+
// Create a temporary directory
|
|
506
|
+
File temporaryDirectory = createTemporaryDirectory();
|
|
507
|
+
// Download the latest manifest
|
|
508
|
+
File latestManifestFile = downloadBundleFile(downloadUrl, manifestFileName, temporaryDirectory);
|
|
509
|
+
Manifest latestManifest = loadManifest(latestManifestFile);
|
|
510
|
+
// Load the current manifest
|
|
511
|
+
Manifest currentManifest = loadCurrentManifest();
|
|
512
|
+
// Compare the manifests
|
|
513
|
+
List<ManifestItem> itemsToCopy = new ArrayList<>();
|
|
514
|
+
List<ManifestItem> itemsToDownload = new ArrayList<>();
|
|
515
|
+
if (currentManifest == null) {
|
|
516
|
+
itemsToDownload.addAll(latestManifest.getItems());
|
|
517
|
+
} else {
|
|
518
|
+
itemsToCopy.addAll(Manifest.findDuplicateItems(latestManifest, currentManifest));
|
|
519
|
+
itemsToDownload.addAll(Manifest.findMissingItems(latestManifest, currentManifest));
|
|
501
520
|
}
|
|
502
|
-
|
|
521
|
+
// Copy the files
|
|
522
|
+
List<String> hrefsToCopy = new ArrayList<>();
|
|
523
|
+
for (ManifestItem item : itemsToCopy) {
|
|
524
|
+
hrefsToCopy.add(item.getHref());
|
|
525
|
+
}
|
|
526
|
+
copyCurrentBundleFiles(hrefsToCopy, temporaryDirectory);
|
|
527
|
+
// Download the files
|
|
528
|
+
List<String> hrefsToDownload = new ArrayList<>();
|
|
529
|
+
for (ManifestItem item : itemsToDownload) {
|
|
530
|
+
hrefsToDownload.add(item.getHref());
|
|
531
|
+
}
|
|
532
|
+
downloadBundleFiles(downloadUrl, hrefsToDownload, temporaryDirectory);
|
|
533
|
+
// Add the bundle
|
|
534
|
+
addBundleOfTypeManifest(bundleId, temporaryDirectory);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
private void downloadBundleOfTypeZip(@NonNull String bundleId, @NonNull String downloadUrl, @Nullable String checksum)
|
|
538
|
+
throws Exception {
|
|
539
|
+
File file = buildTemporaryZipFile();
|
|
540
|
+
// Download the bundle
|
|
541
|
+
downloadAndVerifyFile(downloadUrl, file, checksum);
|
|
542
|
+
// Add the bundle
|
|
543
|
+
addBundleOfTypeZip(bundleId, file);
|
|
544
|
+
// Delete the temporary file
|
|
545
|
+
file.delete();
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
@Nullable
|
|
549
|
+
private GetLatestBundleResponse fetchLatestBundle() throws Exception {
|
|
550
|
+
String url = new HttpUrl.Builder()
|
|
503
551
|
.scheme("https")
|
|
504
552
|
.host(host)
|
|
505
553
|
.addPathSegment("v1")
|
|
@@ -516,47 +564,18 @@ public class LiveUpdate {
|
|
|
516
564
|
.addQueryParameter("osVersion", String.valueOf(Build.VERSION.SDK_INT))
|
|
517
565
|
.addQueryParameter("platform", "0")
|
|
518
566
|
.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
|
-
);
|
|
567
|
+
.build()
|
|
568
|
+
.toString();
|
|
569
|
+
Logger.debug(LiveUpdatePlugin.TAG, "Fetching latest bundle: " + url);
|
|
570
|
+
Response response = httpClient.execute(url);
|
|
571
|
+
String responseBodyString = response.body().string();
|
|
572
|
+
Logger.debug(LiveUpdatePlugin.TAG, "Latest bundle response: " + responseBodyString);
|
|
573
|
+
if (response.isSuccessful()) {
|
|
574
|
+
JSONObject responseJson = new JSONObject(responseBodyString);
|
|
575
|
+
return new GetLatestBundleResponse(responseJson);
|
|
576
|
+
} else {
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
560
579
|
}
|
|
561
580
|
|
|
562
581
|
private String[] getBundleIds() {
|
|
@@ -574,6 +593,31 @@ public class LiveUpdate {
|
|
|
574
593
|
return bundleIds;
|
|
575
594
|
}
|
|
576
595
|
|
|
596
|
+
private byte[] getChecksumForFileAsBytes(@NonNull File file) throws Exception {
|
|
597
|
+
try {
|
|
598
|
+
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
599
|
+
BufferedSource source = Okio.buffer(Okio.source(file));
|
|
600
|
+
Buffer buffer = new Buffer();
|
|
601
|
+
for (long bytesRead; (bytesRead = source.read(buffer, 2048)) != -1;) {
|
|
602
|
+
digest.update(buffer.readByteArray());
|
|
603
|
+
}
|
|
604
|
+
source.close();
|
|
605
|
+
return digest.digest();
|
|
606
|
+
} catch (IOException exception) {
|
|
607
|
+
Logger.error(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
|
|
608
|
+
throw new Exception(LiveUpdatePlugin.ERROR_CHECKSUM_CALCULATION_FAILED);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
private String getChecksumForFileAsString(@NonNull File file) throws Exception {
|
|
613
|
+
byte[] checksumBytes = getChecksumForFileAsBytes(file);
|
|
614
|
+
StringBuilder checksum = new StringBuilder();
|
|
615
|
+
for (byte checksumByte : checksumBytes) {
|
|
616
|
+
checksum.append(Integer.toString((checksumByte & 0xff) + 0x100, 16).substring(1));
|
|
617
|
+
}
|
|
618
|
+
return checksum.toString();
|
|
619
|
+
}
|
|
620
|
+
|
|
577
621
|
@Nullable
|
|
578
622
|
private String getChannel() {
|
|
579
623
|
String channel = null;
|
|
@@ -650,6 +694,41 @@ public class LiveUpdate {
|
|
|
650
694
|
return bundleDirectory.exists();
|
|
651
695
|
}
|
|
652
696
|
|
|
697
|
+
@Nullable
|
|
698
|
+
private Manifest loadCurrentManifest() throws Exception {
|
|
699
|
+
String currentBundleId = getCurrentBundleId();
|
|
700
|
+
if (currentBundleId.equals(defaultWebAssetDir)) {
|
|
701
|
+
AssetManager assets = plugin.getContext().getAssets();
|
|
702
|
+
Boolean manifestFileExists = Arrays.asList(assets.list(defaultWebAssetDir)).contains(manifestFileName);
|
|
703
|
+
if (manifestFileExists) {
|
|
704
|
+
InputStream inputStream = assets.open(defaultWebAssetDir + "/" + manifestFileName);
|
|
705
|
+
BufferedSource source = Okio.buffer(Okio.source(inputStream));
|
|
706
|
+
return loadManifest(source);
|
|
707
|
+
} else {
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
710
|
+
} else {
|
|
711
|
+
File currentBundleDirectory = buildBundleDirectoryFor(currentBundleId);
|
|
712
|
+
File manifestFile = new File(currentBundleDirectory, manifestFileName);
|
|
713
|
+
if (manifestFile.exists()) {
|
|
714
|
+
return loadManifest(manifestFile);
|
|
715
|
+
} else {
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
private Manifest loadManifest(@NonNull BufferedSource source) throws Exception {
|
|
722
|
+
String jsonAsString = source.readUtf8();
|
|
723
|
+
JSONArray jsonArray = new JSONArray(jsonAsString);
|
|
724
|
+
return new Manifest(jsonArray);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
private Manifest loadManifest(@NonNull File file) throws Exception {
|
|
728
|
+
BufferedSource source = Okio.buffer(Okio.source(file));
|
|
729
|
+
return loadManifest(source);
|
|
730
|
+
}
|
|
731
|
+
|
|
653
732
|
private boolean isBundleInUse(@NonNull String bundleId) {
|
|
654
733
|
String currentBundleId = getCurrentBundleId();
|
|
655
734
|
String nextBundleId = getNextBundleId();
|
|
@@ -739,6 +818,39 @@ public class LiveUpdate {
|
|
|
739
818
|
return destination;
|
|
740
819
|
}
|
|
741
820
|
|
|
821
|
+
private boolean verifyChecksumForFile(@NonNull File file, @NonNull String checksum) throws Exception {
|
|
822
|
+
String receivedChecksum = getChecksumForFileAsString(file);
|
|
823
|
+
return checksum.equals(receivedChecksum);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
private void verifyFile(@NonNull File file, @Nullable String checksum, @Nullable String signature) throws Exception {
|
|
827
|
+
String publicKey = config.getPublicKey();
|
|
828
|
+
if (publicKey != null) {
|
|
829
|
+
// Verify the signature
|
|
830
|
+
if (signature == null) {
|
|
831
|
+
throw new Exception(LiveUpdatePlugin.ERROR_SIGNATURE_MISSING);
|
|
832
|
+
}
|
|
833
|
+
// Verify the signature
|
|
834
|
+
boolean verified = verifySignatureForFile(file, signature, publicKey);
|
|
835
|
+
if (!verified) {
|
|
836
|
+
throw new Exception(LiveUpdatePlugin.ERROR_SIGNATURE_VERIFICATION_FAILED);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
// Verify the checksum
|
|
840
|
+
else if (checksum != null) {
|
|
841
|
+
// Calculate the checksum
|
|
842
|
+
boolean verified = verifyChecksumForFile(file, checksum);
|
|
843
|
+
if (!verified) {
|
|
844
|
+
throw new Exception(LiveUpdatePlugin.ERROR_CHECKSUM_MISMATCH);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
private boolean verifySignatureForFile(@NonNull File file, @NonNull String signature, @NonNull String keyAsString) throws Exception {
|
|
850
|
+
PublicKey key = createPublicKeyFromString(keyAsString);
|
|
851
|
+
return verifySignatureForFile(file, signature, key);
|
|
852
|
+
}
|
|
853
|
+
|
|
742
854
|
private boolean verifySignatureForFile(@NonNull File file, @NonNull String signature, @NonNull PublicKey key) throws Exception {
|
|
743
855
|
try {
|
|
744
856
|
byte[] signatureBytes = Base64.decode(signature, Base64.DEFAULT);
|