@capawesome/capacitor-live-update 6.8.0-dev.2e2696e1.1773643381 → 6.9.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/CapawesomeCapacitorLiveUpdate.podspec +2 -2
- package/README.md +435 -27
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +851 -262
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateConfig.java +35 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateHttpClient.java +56 -11
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java +273 -38
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePreferences.java +52 -3
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/api/GetChannelsResponseItem.java +28 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/api/GetLatestBundleResponse.java +7 -7
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/events/DownloadBundleProgressEvent.java +32 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/events/NextBundleSetEvent.java +26 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/FetchChannelsOptions.java +38 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/SetConfigOptions.java +19 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/SyncOptions.java +4 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/ChannelResult.java +28 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/FetchChannelsResult.java +28 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/FetchLatestBundleResult.java +7 -7
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetBlockedBundlesResult.java +27 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetConfigResult.java +40 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetDownloadedBundlesResult.java +27 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/IsSyncingResult.java +26 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/ReadyResult.java +32 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/interfaces/DownloadProgressCallback.java +5 -0
- package/dist/docs.json +845 -54
- package/dist/esm/definitions.d.ts +413 -18
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +10 -2
- package/dist/esm/web.js +24 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +24 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +24 -0
- package/dist/plugin.js.map +1 -1
- package/ios/Plugin/Classes/Events/DownloadBundleProgressEvent.swift +25 -0
- package/ios/Plugin/Classes/Events/NextBundleSetEvent.swift +16 -0
- package/ios/Plugin/Classes/Options/FetchChannelsOptions.swift +26 -0
- package/ios/Plugin/Classes/Options/SetConfigOptions.swift +14 -0
- package/ios/Plugin/Classes/Options/SyncOptions.swift +4 -0
- package/ios/Plugin/Classes/Results/ChannelResult.swift +19 -0
- package/ios/Plugin/Classes/Results/FetchChannelsResult.swift +16 -0
- package/ios/Plugin/Classes/Results/{FetchLatestBundle.swift → FetchLatestBundleResult.swift} +6 -6
- package/ios/Plugin/Classes/Results/GetBlockedBundlesResult.swift +16 -0
- package/ios/Plugin/Classes/Results/GetConfigResult.swift +19 -0
- package/ios/Plugin/Classes/Results/GetDownloadedBundlesResult.swift +16 -0
- package/ios/Plugin/Classes/Results/IsSyncingResult.swift +16 -0
- package/ios/Plugin/Classes/Results/ReadyResult.swift +22 -0
- package/ios/Plugin/LiveUpdate.swift +423 -145
- package/ios/Plugin/LiveUpdateConfig.swift +3 -0
- package/ios/Plugin/LiveUpdateHttpClient.swift +21 -4
- package/ios/Plugin/LiveUpdatePlugin.swift +174 -14
- package/ios/Plugin/LiveUpdatePreferences.swift +54 -12
- package/ios/Plugin/Protocols/Api/GetChannelsResponseItem.swift +4 -0
- package/ios/Plugin/Protocols/Api/GetLatestBundleResponse.swift +1 -1
- package/package.json +5 -5
- package/ios/Plugin/LiveUpdatePlugin.h +0 -10
- package/ios/Plugin/LiveUpdatePlugin.m +0 -27
|
@@ -10,31 +10,45 @@ import android.os.Handler;
|
|
|
10
10
|
import android.util.Base64;
|
|
11
11
|
import androidx.annotation.NonNull;
|
|
12
12
|
import androidx.annotation.Nullable;
|
|
13
|
+
import com.getcapacitor.Bridge;
|
|
13
14
|
import com.getcapacitor.Logger;
|
|
14
15
|
import com.getcapacitor.plugin.WebView;
|
|
15
16
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.Manifest;
|
|
16
17
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.ManifestItem;
|
|
18
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.api.GetChannelsResponseItem;
|
|
17
19
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.api.GetLatestBundleResponse;
|
|
20
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.events.DownloadBundleProgressEvent;
|
|
21
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.events.NextBundleSetEvent;
|
|
18
22
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.DeleteBundleOptions;
|
|
19
23
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.DownloadBundleOptions;
|
|
24
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.FetchChannelsOptions;
|
|
20
25
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.FetchLatestBundleOptions;
|
|
21
26
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetBundleOptions;
|
|
22
27
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetChannelOptions;
|
|
28
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetConfigOptions;
|
|
23
29
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetCustomIdOptions;
|
|
24
30
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetNextBundleOptions;
|
|
25
31
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SyncOptions;
|
|
32
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.ChannelResult;
|
|
33
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.FetchChannelsResult;
|
|
26
34
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.FetchLatestBundleResult;
|
|
35
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetBlockedBundlesResult;
|
|
27
36
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetBundleResult;
|
|
28
37
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetBundlesResult;
|
|
29
38
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetChannelResult;
|
|
39
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetConfigResult;
|
|
30
40
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetCurrentBundleResult;
|
|
31
41
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetCustomIdResult;
|
|
32
42
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetDeviceIdResult;
|
|
43
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetDownloadedBundlesResult;
|
|
33
44
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetNextBundleResult;
|
|
34
45
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetVersionCodeResult;
|
|
35
46
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetVersionNameResult;
|
|
47
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.IsSyncingResult;
|
|
48
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.ReadyResult;
|
|
36
49
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.SyncResult;
|
|
37
50
|
import io.capawesome.capacitorjs.plugins.liveupdate.enums.ArtifactType;
|
|
51
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.DownloadProgressCallback;
|
|
38
52
|
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.EmptyCallback;
|
|
39
53
|
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.NonEmptyCallback;
|
|
40
54
|
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.Result;
|
|
@@ -52,8 +66,12 @@ import java.util.ArrayList;
|
|
|
52
66
|
import java.util.Arrays;
|
|
53
67
|
import java.util.List;
|
|
54
68
|
import java.util.UUID;
|
|
69
|
+
import java.util.concurrent.CountDownLatch;
|
|
70
|
+
import java.util.concurrent.atomic.AtomicBoolean;
|
|
71
|
+
import java.util.concurrent.atomic.AtomicLong;
|
|
55
72
|
import java.util.concurrent.atomic.AtomicReference;
|
|
56
73
|
import net.lingala.zip4j.ZipFile;
|
|
74
|
+
import okhttp3.Call;
|
|
57
75
|
import okhttp3.HttpUrl;
|
|
58
76
|
import okhttp3.Response;
|
|
59
77
|
import okhttp3.ResponseBody;
|
|
@@ -65,13 +83,12 @@ import org.json.JSONObject;
|
|
|
65
83
|
|
|
66
84
|
public class LiveUpdate {
|
|
67
85
|
|
|
86
|
+
private final long autoUpdateIntervalMs = 15 * 60 * 1000; // 15 minutes
|
|
87
|
+
|
|
68
88
|
@NonNull
|
|
69
89
|
private final LiveUpdateConfig config;
|
|
70
90
|
|
|
71
|
-
private final String defaultWebAssetDir;
|
|
72
|
-
|
|
73
|
-
@NonNull
|
|
74
|
-
private final String host;
|
|
91
|
+
private final String defaultWebAssetDir = Bridge.DEFAULT_WEB_ASSET_DIR;
|
|
75
92
|
|
|
76
93
|
@NonNull
|
|
77
94
|
private final LiveUpdateHttpClient httpClient;
|
|
@@ -89,44 +106,57 @@ public class LiveUpdate {
|
|
|
89
106
|
private final Handler rollbackHandler = new Handler();
|
|
90
107
|
private final String manifestFileName = "capawesome-live-update-manifest.json"; // DO NOT CHANGE!
|
|
91
108
|
|
|
109
|
+
private boolean initialPageLoaded = false;
|
|
110
|
+
private long lastAutoUpdateCheckTimestamp = 0;
|
|
111
|
+
private boolean rollbackPerformed = false;
|
|
112
|
+
private boolean syncInProgress = false;
|
|
113
|
+
|
|
92
114
|
public LiveUpdate(@NonNull LiveUpdateConfig config, @NonNull LiveUpdatePlugin plugin) throws PackageManager.NameNotFoundException {
|
|
93
115
|
this.config = config;
|
|
94
|
-
this.defaultWebAssetDir = plugin.getBridge().DEFAULT_WEB_ASSET_DIR;
|
|
95
|
-
if (config.getLocation() != null && config.getLocation().equals("eu")) {
|
|
96
|
-
this.host = "api.cloud.capawesome.eu";
|
|
97
|
-
} else {
|
|
98
|
-
this.host = "api.cloud.capawesome.io";
|
|
99
|
-
}
|
|
100
116
|
this.httpClient = new LiveUpdateHttpClient(config);
|
|
101
117
|
this.plugin = plugin;
|
|
102
118
|
this.preferences = new LiveUpdatePreferences(plugin.getContext());
|
|
103
119
|
this.webViewSettingsEditor = plugin.getContext().getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE).edit();
|
|
104
120
|
|
|
121
|
+
// Set the device ID on the HTTP client
|
|
122
|
+
this.httpClient.setDeviceId(getDeviceId());
|
|
123
|
+
|
|
105
124
|
if (config.getEnabled()) {
|
|
106
|
-
|
|
125
|
+
boolean wasUpdated = wasUpdated();
|
|
126
|
+
if (wasUpdated) {
|
|
127
|
+
// Reset the runtime configuration if the app was updated to a new version
|
|
128
|
+
resetConfig();
|
|
129
|
+
}
|
|
130
|
+
if (wasUpdated && config.getResetOnUpdate()) {
|
|
107
131
|
Logger.debug(LiveUpdatePlugin.TAG, "App was updated. Resetting to default bundle.");
|
|
108
132
|
reset();
|
|
109
133
|
} else {
|
|
134
|
+
// Start the rollback timer to rollback to the default bundle
|
|
135
|
+
// if the app is not ready after a certain time
|
|
110
136
|
startRollbackTimer();
|
|
111
137
|
}
|
|
112
138
|
saveCurrentVersionCode();
|
|
113
139
|
}
|
|
114
140
|
}
|
|
115
141
|
|
|
142
|
+
public void clearBlockedBundles() {
|
|
143
|
+
preferences.setBlockedBundleIds(null);
|
|
144
|
+
}
|
|
145
|
+
|
|
116
146
|
public void deleteBundle(@NonNull DeleteBundleOptions options, @NonNull EmptyCallback callback) {
|
|
117
147
|
String bundleId = options.getBundleId();
|
|
118
148
|
|
|
119
|
-
if (!
|
|
149
|
+
if (!hasBundleById(bundleId)) {
|
|
120
150
|
Exception exception = new Exception(LiveUpdatePlugin.ERROR_BUNDLE_NOT_FOUND);
|
|
121
151
|
callback.error(exception);
|
|
122
152
|
return;
|
|
123
153
|
}
|
|
124
|
-
|
|
154
|
+
deleteBundleById(bundleId);
|
|
125
155
|
|
|
126
156
|
callback.success();
|
|
127
157
|
}
|
|
128
158
|
|
|
129
|
-
public void downloadBundle(@NonNull DownloadBundleOptions options, @NonNull EmptyCallback callback)
|
|
159
|
+
public void downloadBundle(@NonNull DownloadBundleOptions options, @NonNull EmptyCallback callback) {
|
|
130
160
|
ArtifactType artifactType = options.getArtifactType();
|
|
131
161
|
String bundleId = options.getBundleId();
|
|
132
162
|
String checksum = options.getChecksum();
|
|
@@ -134,7 +164,7 @@ public class LiveUpdate {
|
|
|
134
164
|
String url = options.getUrl();
|
|
135
165
|
|
|
136
166
|
// Check if the bundle already exists
|
|
137
|
-
if (
|
|
167
|
+
if (hasBundleById(bundleId)) {
|
|
138
168
|
Exception exception = new Exception(LiveUpdatePlugin.ERROR_BUNDLE_EXISTS);
|
|
139
169
|
callback.error(exception);
|
|
140
170
|
return;
|
|
@@ -142,51 +172,143 @@ public class LiveUpdate {
|
|
|
142
172
|
|
|
143
173
|
// Download the bundle
|
|
144
174
|
if (artifactType == ArtifactType.MANIFEST) {
|
|
145
|
-
downloadBundleOfTypeManifest(bundleId, url);
|
|
175
|
+
downloadBundleOfTypeManifest(bundleId, url, callback);
|
|
146
176
|
} else {
|
|
147
|
-
downloadBundleOfTypeZip(bundleId,
|
|
177
|
+
downloadBundleOfTypeZip(bundleId, checksum, signature, url, callback);
|
|
148
178
|
}
|
|
149
|
-
callback.success();
|
|
150
179
|
}
|
|
151
180
|
|
|
152
|
-
public void
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
181
|
+
public void fetchChannels(@NonNull FetchChannelsOptions options, @NonNull NonEmptyCallback<FetchChannelsResult> callback) {
|
|
182
|
+
try {
|
|
183
|
+
HttpUrl.Builder urlBuilder = new HttpUrl.Builder()
|
|
184
|
+
.scheme("https")
|
|
185
|
+
.host(config.getServerDomain())
|
|
186
|
+
.addPathSegment("v1")
|
|
187
|
+
.addPathSegment("apps")
|
|
188
|
+
.addPathSegment(getAppId())
|
|
189
|
+
.addPathSegment("channels");
|
|
190
|
+
if (options.getLimit() != null) {
|
|
191
|
+
urlBuilder.addQueryParameter("limit", String.valueOf(options.getLimit()));
|
|
192
|
+
}
|
|
193
|
+
if (options.getOffset() != null) {
|
|
194
|
+
urlBuilder.addQueryParameter("offset", String.valueOf(options.getOffset()));
|
|
195
|
+
}
|
|
196
|
+
if (options.getQuery() != null) {
|
|
197
|
+
urlBuilder.addQueryParameter("query", options.getQuery());
|
|
198
|
+
}
|
|
199
|
+
String url = urlBuilder.build().toString();
|
|
200
|
+
|
|
201
|
+
httpClient.enqueue(
|
|
202
|
+
url,
|
|
203
|
+
new NonEmptyCallback<Response>() {
|
|
204
|
+
@Override
|
|
205
|
+
public void success(@NonNull Response response) {
|
|
206
|
+
try {
|
|
207
|
+
String responseBodyString = response.body().string();
|
|
208
|
+
if (response.isSuccessful()) {
|
|
209
|
+
JSONArray responseJsonArray = new JSONArray(responseBodyString);
|
|
210
|
+
ChannelResult[] channels = new ChannelResult[responseJsonArray.length()];
|
|
211
|
+
for (int i = 0; i < responseJsonArray.length(); i++) {
|
|
212
|
+
GetChannelsResponseItem item = new GetChannelsResponseItem(responseJsonArray.getJSONObject(i));
|
|
213
|
+
channels[i] = new ChannelResult(item.getId(), item.getName());
|
|
214
|
+
}
|
|
215
|
+
FetchChannelsResult result = new FetchChannelsResult(channels);
|
|
216
|
+
callback.success(result);
|
|
217
|
+
} else {
|
|
218
|
+
callback.error(new Exception(responseBodyString));
|
|
219
|
+
}
|
|
220
|
+
} catch (Exception e) {
|
|
221
|
+
callback.error(e);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
@Override
|
|
226
|
+
public void error(@NonNull Exception exception) {
|
|
227
|
+
callback.error(exception);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
);
|
|
231
|
+
} catch (Exception e) {
|
|
232
|
+
callback.error(e);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
public void fetchLatestBundle(@NonNull FetchLatestBundleOptions options, @NonNull NonEmptyCallback<FetchLatestBundleResult> callback) {
|
|
237
|
+
fetchLatestBundleInternal(
|
|
238
|
+
options,
|
|
239
|
+
new NonEmptyCallback<GetLatestBundleResponse>() {
|
|
240
|
+
@Override
|
|
241
|
+
public void success(@Nullable GetLatestBundleResponse response) {
|
|
242
|
+
ArtifactType artifactType = response == null ? null : response.getArtifactType();
|
|
243
|
+
String bundleId = response == null ? null : response.getBundleId();
|
|
244
|
+
String checksum = response == null ? null : response.getChecksum();
|
|
245
|
+
JSONObject customProperties = response == null ? null : response.getCustomProperties();
|
|
246
|
+
String downloadUrl = response == null ? null : response.getUrl();
|
|
247
|
+
String signature = response == null ? null : response.getSignature();
|
|
248
|
+
FetchLatestBundleResult result = new FetchLatestBundleResult(
|
|
249
|
+
artifactType,
|
|
250
|
+
bundleId,
|
|
251
|
+
checksum,
|
|
252
|
+
customProperties,
|
|
253
|
+
downloadUrl,
|
|
254
|
+
signature
|
|
255
|
+
);
|
|
256
|
+
callback.success(result);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
@Override
|
|
260
|
+
public void error(@NonNull Exception exception) {
|
|
261
|
+
callback.error(exception);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
public void getBlockedBundles(@NonNull NonEmptyCallback<GetBlockedBundlesResult> callback) {
|
|
268
|
+
String blockedIds = preferences.getBlockedBundleIds();
|
|
269
|
+
String[] bundleIds;
|
|
270
|
+
if (blockedIds == null || blockedIds.isEmpty()) {
|
|
271
|
+
bundleIds = new String[0];
|
|
272
|
+
} else {
|
|
273
|
+
bundleIds = blockedIds.split(",");
|
|
274
|
+
}
|
|
275
|
+
GetBlockedBundlesResult result = new GetBlockedBundlesResult(bundleIds);
|
|
161
276
|
callback.success(result);
|
|
162
277
|
}
|
|
163
278
|
|
|
164
|
-
public void getBundle(@NonNull NonEmptyCallback callback) {
|
|
279
|
+
public void getBundle(@NonNull NonEmptyCallback<GetBundleResult> callback) {
|
|
165
280
|
String bundleId = getCurrentBundleId();
|
|
166
|
-
if (bundleId.equals(defaultWebAssetDir)) {
|
|
167
|
-
bundleId = null;
|
|
168
|
-
}
|
|
169
281
|
GetBundleResult result = new GetBundleResult(bundleId);
|
|
170
282
|
callback.success(result);
|
|
171
283
|
}
|
|
172
284
|
|
|
173
285
|
public void getBundles(@NonNull NonEmptyCallback callback) {
|
|
174
|
-
String[] bundleIds =
|
|
286
|
+
String[] bundleIds = getDownloadedBundleIds();
|
|
175
287
|
GetBundlesResult result = new GetBundlesResult(bundleIds);
|
|
176
288
|
callback.success(result);
|
|
177
289
|
}
|
|
178
290
|
|
|
291
|
+
public void getDownloadedBundles(@NonNull NonEmptyCallback<GetDownloadedBundlesResult> callback) {
|
|
292
|
+
String[] bundleIds = getDownloadedBundleIds();
|
|
293
|
+
GetDownloadedBundlesResult result = new GetDownloadedBundlesResult(bundleIds);
|
|
294
|
+
callback.success(result);
|
|
295
|
+
}
|
|
296
|
+
|
|
179
297
|
public void getChannel(@NonNull NonEmptyCallback callback) {
|
|
180
298
|
String channel = getChannel();
|
|
181
299
|
GetChannelResult result = new GetChannelResult(channel);
|
|
182
300
|
callback.success(result);
|
|
183
301
|
}
|
|
184
302
|
|
|
303
|
+
public void getConfig(@NonNull NonEmptyCallback<GetConfigResult> callback) {
|
|
304
|
+
String appId = getAppId();
|
|
305
|
+
String autoUpdateStrategy = config.getAutoUpdateStrategy();
|
|
306
|
+
GetConfigResult result = new GetConfigResult(appId, autoUpdateStrategy);
|
|
307
|
+
callback.success(result);
|
|
308
|
+
}
|
|
309
|
+
|
|
185
310
|
public void getCurrentBundle(@NonNull NonEmptyCallback<GetCurrentBundleResult> callback) {
|
|
186
311
|
String bundleId = getCurrentBundleId();
|
|
187
|
-
if (bundleId.equals(defaultWebAssetDir)) {
|
|
188
|
-
bundleId = null;
|
|
189
|
-
}
|
|
190
312
|
GetCurrentBundleResult result = new GetCurrentBundleResult(bundleId);
|
|
191
313
|
callback.success(result);
|
|
192
314
|
}
|
|
@@ -205,15 +327,12 @@ public class LiveUpdate {
|
|
|
205
327
|
|
|
206
328
|
public void getNextBundle(@NonNull NonEmptyCallback<GetNextBundleResult> callback) {
|
|
207
329
|
String bundleId = getNextBundleId();
|
|
208
|
-
if (bundleId.equals(defaultWebAssetDir)) {
|
|
209
|
-
bundleId = null;
|
|
210
|
-
}
|
|
211
330
|
GetNextBundleResult result = new GetNextBundleResult(bundleId);
|
|
212
331
|
callback.success(result);
|
|
213
332
|
}
|
|
214
333
|
|
|
215
334
|
public void getVersionCode(@NonNull NonEmptyCallback callback) throws PackageManager.NameNotFoundException {
|
|
216
|
-
String versionCode =
|
|
335
|
+
String versionCode = getVersionCodeAsString();
|
|
217
336
|
GetVersionCodeResult result = new GetVersionCodeResult(versionCode);
|
|
218
337
|
callback.success(result);
|
|
219
338
|
}
|
|
@@ -224,14 +343,51 @@ public class LiveUpdate {
|
|
|
224
343
|
callback.success(result);
|
|
225
344
|
}
|
|
226
345
|
|
|
227
|
-
public void
|
|
346
|
+
public void isSyncing(@NonNull NonEmptyCallback<IsSyncingResult> callback) {
|
|
347
|
+
IsSyncingResult result = new IsSyncingResult(syncInProgress);
|
|
348
|
+
callback.success(result);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
public void handleOnPageLoaded() {
|
|
352
|
+
// Wait for initial page load to perform auto update to make sure the WebViewLocalServer is initialized.
|
|
353
|
+
// Otherwise, a NullPointerException may occur for `com.getcapacitor.WebViewLocalServer.getBasePath()`.
|
|
354
|
+
if ("background".equals(config.getAutoUpdateStrategy()) && !initialPageLoaded) {
|
|
355
|
+
initialPageLoaded = true;
|
|
356
|
+
performAutoUpdate();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
public void handleOnResume() {
|
|
361
|
+
if ("background".equals(config.getAutoUpdateStrategy()) && initialPageLoaded) {
|
|
362
|
+
performAutoUpdate();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
public void ready(@NonNull NonEmptyCallback callback) {
|
|
228
367
|
Logger.debug(LiveUpdatePlugin.TAG, "App is ready.");
|
|
368
|
+
if (config.getReadyTimeout() <= 0) {
|
|
369
|
+
Logger.warn(LiveUpdatePlugin.TAG, "Ready timeout is set to 0. Automatic rollback is disabled.");
|
|
370
|
+
}
|
|
229
371
|
// Stop the rollback timer
|
|
230
372
|
stopRollbackTimer();
|
|
231
373
|
// Delete unused bundles
|
|
232
374
|
if (config.getAutoDeleteBundles()) {
|
|
233
375
|
deleteUnusedBundles();
|
|
234
376
|
}
|
|
377
|
+
// Get the current and previous bundle IDs
|
|
378
|
+
String currentBundleId = getCurrentBundleId();
|
|
379
|
+
String previousBundleId = getPreviousBundleId();
|
|
380
|
+
// Block the rolled back bundle if enabled
|
|
381
|
+
if (config.getAutoBlockRolledBackBundles() && rollbackPerformed && previousBundleId != null) {
|
|
382
|
+
addBlockedBundleId(previousBundleId);
|
|
383
|
+
}
|
|
384
|
+
// Return the result
|
|
385
|
+
ReadyResult result = new ReadyResult(currentBundleId, previousBundleId, rollbackPerformed);
|
|
386
|
+
callback.success(result);
|
|
387
|
+
// Set the new previous bundle ID
|
|
388
|
+
setPreviousBundleId(currentBundleId);
|
|
389
|
+
// Reset the rollback flag
|
|
390
|
+
rollbackPerformed = false;
|
|
235
391
|
}
|
|
236
392
|
|
|
237
393
|
public void reload() {
|
|
@@ -241,19 +397,23 @@ public class LiveUpdate {
|
|
|
241
397
|
}
|
|
242
398
|
|
|
243
399
|
public void reset() {
|
|
244
|
-
|
|
400
|
+
setNextBundleById(null);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
public void resetConfig() {
|
|
404
|
+
preferences.setAppId(null);
|
|
245
405
|
}
|
|
246
406
|
|
|
247
407
|
public void setBundle(@NonNull SetBundleOptions options, @NonNull EmptyCallback callback) {
|
|
248
408
|
String bundleId = options.getBundleId();
|
|
249
409
|
|
|
250
|
-
if (!
|
|
410
|
+
if (!hasBundleById(bundleId)) {
|
|
251
411
|
Exception exception = new Exception(LiveUpdatePlugin.ERROR_BUNDLE_NOT_FOUND);
|
|
252
412
|
callback.error(exception);
|
|
253
413
|
return;
|
|
254
414
|
}
|
|
255
415
|
|
|
256
|
-
|
|
416
|
+
setNextBundleById(bundleId);
|
|
257
417
|
callback.success();
|
|
258
418
|
}
|
|
259
419
|
|
|
@@ -264,6 +424,11 @@ public class LiveUpdate {
|
|
|
264
424
|
callback.success();
|
|
265
425
|
}
|
|
266
426
|
|
|
427
|
+
public void setConfig(@NonNull SetConfigOptions options) {
|
|
428
|
+
String appId = options.getAppId();
|
|
429
|
+
preferences.setAppId(appId);
|
|
430
|
+
}
|
|
431
|
+
|
|
267
432
|
public void setCustomId(@NonNull SetCustomIdOptions options, @NonNull EmptyCallback callback) {
|
|
268
433
|
String customId = options.getCustomId();
|
|
269
434
|
|
|
@@ -277,8 +442,8 @@ public class LiveUpdate {
|
|
|
277
442
|
if (bundleId == null) {
|
|
278
443
|
reset();
|
|
279
444
|
} else {
|
|
280
|
-
if (
|
|
281
|
-
|
|
445
|
+
if (hasBundleById(bundleId)) {
|
|
446
|
+
setNextBundleById(bundleId);
|
|
282
447
|
} else {
|
|
283
448
|
Exception exception = new Exception(LiveUpdatePlugin.ERROR_BUNDLE_NOT_FOUND);
|
|
284
449
|
callback.error(exception);
|
|
@@ -288,45 +453,99 @@ public class LiveUpdate {
|
|
|
288
453
|
callback.success();
|
|
289
454
|
}
|
|
290
455
|
|
|
291
|
-
public void sync(@NonNull SyncOptions options, @NonNull NonEmptyCallback<Result> callback)
|
|
456
|
+
public void sync(@NonNull SyncOptions options, @NonNull NonEmptyCallback<Result> callback) {
|
|
457
|
+
if (syncInProgress) {
|
|
458
|
+
Exception exception = new Exception(LiveUpdatePlugin.ERROR_SYNC_IN_PROGRESS);
|
|
459
|
+
callback.error(exception);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
syncInProgress = true;
|
|
463
|
+
|
|
292
464
|
String channel = options.getChannel();
|
|
293
465
|
// Fetch the latest bundle
|
|
294
466
|
FetchLatestBundleOptions fetchLatestBundleOptions = new FetchLatestBundleOptions(channel);
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
467
|
+
fetchLatestBundleInternal(
|
|
468
|
+
fetchLatestBundleOptions,
|
|
469
|
+
new NonEmptyCallback<GetLatestBundleResponse>() {
|
|
470
|
+
@Override
|
|
471
|
+
public void success(@Nullable GetLatestBundleResponse response) {
|
|
472
|
+
try {
|
|
473
|
+
if (response == null) {
|
|
474
|
+
Logger.debug(LiveUpdatePlugin.TAG, "No update available.");
|
|
475
|
+
syncInProgress = false;
|
|
476
|
+
SyncResult syncResult = new SyncResult(null);
|
|
477
|
+
callback.success(syncResult);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
ArtifactType artifactType = response.getArtifactType();
|
|
481
|
+
String latestBundleId = response.getBundleId();
|
|
482
|
+
String checksum = response.getChecksum();
|
|
483
|
+
String signature = response.getSignature();
|
|
484
|
+
String url = response.getUrl();
|
|
485
|
+
// Check if the bundle is blocked
|
|
486
|
+
if (isBlockedBundleId(latestBundleId)) {
|
|
487
|
+
Logger.warn(LiveUpdatePlugin.TAG, "Bundle is blocked and will not be downloaded.");
|
|
488
|
+
syncInProgress = false;
|
|
489
|
+
SyncResult syncResult = new SyncResult(null);
|
|
490
|
+
callback.success(syncResult);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
// Check if the bundle already exists
|
|
494
|
+
if (hasBundleById(latestBundleId)) {
|
|
495
|
+
String nextBundleId = null;
|
|
496
|
+
String currentBundleId = getCurrentBundleId();
|
|
497
|
+
if (!latestBundleId.equals(currentBundleId)) {
|
|
498
|
+
// Set the next bundle
|
|
499
|
+
setNextBundleById(latestBundleId);
|
|
500
|
+
nextBundleId = latestBundleId;
|
|
501
|
+
}
|
|
502
|
+
syncInProgress = false;
|
|
503
|
+
SyncResult syncResult = new SyncResult(nextBundleId);
|
|
504
|
+
callback.success(syncResult);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Download the bundle
|
|
509
|
+
EmptyCallback downloadCallback = new EmptyCallback() {
|
|
510
|
+
@Override
|
|
511
|
+
public void success() {
|
|
512
|
+
try {
|
|
513
|
+
// Set the next bundle
|
|
514
|
+
setNextBundleById(latestBundleId);
|
|
515
|
+
syncInProgress = false;
|
|
516
|
+
SyncResult syncResult = new SyncResult(latestBundleId);
|
|
517
|
+
callback.success(syncResult);
|
|
518
|
+
} catch (Exception e) {
|
|
519
|
+
syncInProgress = false;
|
|
520
|
+
callback.error(e);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
@Override
|
|
525
|
+
public void error(@NonNull Exception exception) {
|
|
526
|
+
syncInProgress = false;
|
|
527
|
+
callback.error(exception);
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
if (artifactType == ArtifactType.MANIFEST) {
|
|
532
|
+
downloadBundleOfTypeManifest(latestBundleId, url, downloadCallback);
|
|
533
|
+
} else {
|
|
534
|
+
downloadBundleOfTypeZip(latestBundleId, checksum, signature, url, downloadCallback);
|
|
535
|
+
}
|
|
536
|
+
} catch (Exception e) {
|
|
537
|
+
syncInProgress = false;
|
|
538
|
+
callback.error(e);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
@Override
|
|
543
|
+
public void error(@NonNull Exception exception) {
|
|
544
|
+
syncInProgress = false;
|
|
545
|
+
callback.error(exception);
|
|
546
|
+
}
|
|
315
547
|
}
|
|
316
|
-
|
|
317
|
-
callback.success(syncResult);
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
// Download the bundle
|
|
321
|
-
if (artifactType == ArtifactType.MANIFEST) {
|
|
322
|
-
downloadBundleOfTypeManifest(latestBundleId, url);
|
|
323
|
-
} else {
|
|
324
|
-
downloadBundleOfTypeZip(latestBundleId, url, checksum, signature);
|
|
325
|
-
}
|
|
326
|
-
// Set the next bundle
|
|
327
|
-
setNextBundle(latestBundleId);
|
|
328
|
-
SyncResult syncResult = new SyncResult(latestBundleId);
|
|
329
|
-
callback.success(syncResult);
|
|
548
|
+
);
|
|
330
549
|
}
|
|
331
550
|
|
|
332
551
|
private void addBundle(@NonNull String bundleId, @NonNull File sourceDirectory) throws Exception {
|
|
@@ -373,9 +592,10 @@ public class LiveUpdate {
|
|
|
373
592
|
return new File(plugin.getContext().getCacheDir(), fileName);
|
|
374
593
|
}
|
|
375
594
|
|
|
376
|
-
private void copyCurrentBundleFile(@NonNull
|
|
595
|
+
private void copyCurrentBundleFile(@NonNull ManifestItem fileToCopy, @NonNull File destinationDirectory) throws IOException {
|
|
596
|
+
String href = fileToCopy.getHref();
|
|
377
597
|
String currentBundleId = getCurrentBundleId();
|
|
378
|
-
if (currentBundleId
|
|
598
|
+
if (currentBundleId == null) {
|
|
379
599
|
// Create the source input stream
|
|
380
600
|
AssetManager assets = plugin.getContext().getAssets();
|
|
381
601
|
InputStream inputStream = assets.open(defaultWebAssetDir + "/" + href);
|
|
@@ -397,10 +617,20 @@ public class LiveUpdate {
|
|
|
397
617
|
}
|
|
398
618
|
}
|
|
399
619
|
|
|
400
|
-
private
|
|
401
|
-
|
|
402
|
-
|
|
620
|
+
private List<ManifestItem> copyCurrentBundleFilesAndReturnFailures(
|
|
621
|
+
@NonNull List<ManifestItem> filesToCopy,
|
|
622
|
+
@NonNull File destinationDirectory
|
|
623
|
+
) {
|
|
624
|
+
List<ManifestItem> missingItems = new ArrayList<>();
|
|
625
|
+
for (ManifestItem fileToCopy : filesToCopy) {
|
|
626
|
+
boolean success = tryCopyCurrentBundleFile(fileToCopy, destinationDirectory);
|
|
627
|
+
if (!success) {
|
|
628
|
+
Logger.warn(LiveUpdatePlugin.TAG, "Failed to copy file: " + fileToCopy.getHref());
|
|
629
|
+
// If the file could not be copied, add it to the list of missing items
|
|
630
|
+
missingItems.add(fileToCopy);
|
|
631
|
+
}
|
|
403
632
|
}
|
|
633
|
+
return missingItems;
|
|
404
634
|
}
|
|
405
635
|
|
|
406
636
|
private void copyFile(File input, File output) throws IOException {
|
|
@@ -452,191 +682,381 @@ public class LiveUpdate {
|
|
|
452
682
|
return file;
|
|
453
683
|
}
|
|
454
684
|
|
|
455
|
-
private void
|
|
685
|
+
private void deleteBundleById(@NonNull String bundleId) {
|
|
456
686
|
// Delete the bundle directory
|
|
457
687
|
File bundleDirectory = buildBundleDirectoryFor(bundleId);
|
|
458
688
|
deleteFileRecursively(bundleDirectory);
|
|
459
689
|
// Reset the next bundle if it is the deleted bundle
|
|
460
690
|
String nextBundleId = getNextBundleId();
|
|
461
691
|
if (bundleId.equals(nextBundleId)) {
|
|
462
|
-
|
|
692
|
+
setNextBundleById(null);
|
|
463
693
|
}
|
|
464
694
|
}
|
|
465
695
|
|
|
466
696
|
private void deleteFileRecursively(@NonNull File file) {
|
|
467
697
|
if (file.isDirectory()) {
|
|
468
|
-
|
|
469
|
-
|
|
698
|
+
File[] children = file.listFiles();
|
|
699
|
+
if (children != null) {
|
|
700
|
+
for (File child : children) {
|
|
701
|
+
deleteFileRecursively(child);
|
|
702
|
+
}
|
|
470
703
|
}
|
|
471
704
|
}
|
|
472
705
|
file.delete();
|
|
473
706
|
}
|
|
474
707
|
|
|
475
708
|
private void deleteUnusedBundles() {
|
|
476
|
-
String[] bundleIds =
|
|
709
|
+
String[] bundleIds = getDownloadedBundleIds();
|
|
477
710
|
for (String bundleId : bundleIds) {
|
|
478
711
|
if (!isBundleInUse(bundleId)) {
|
|
479
|
-
|
|
712
|
+
deleteBundleById(bundleId);
|
|
480
713
|
}
|
|
481
714
|
}
|
|
482
715
|
}
|
|
483
716
|
|
|
484
|
-
private
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
717
|
+
private Call downloadAndVerifyFile(
|
|
718
|
+
@NonNull String url,
|
|
719
|
+
@NonNull File file,
|
|
720
|
+
@Nullable String checksum,
|
|
721
|
+
@Nullable String signature,
|
|
722
|
+
@Nullable DownloadProgressCallback progressCallback,
|
|
723
|
+
@NonNull EmptyCallback completionCallback
|
|
724
|
+
) {
|
|
725
|
+
return httpClient.enqueue(
|
|
726
|
+
url,
|
|
727
|
+
new NonEmptyCallback<Response>() {
|
|
728
|
+
@Override
|
|
729
|
+
public void success(@NonNull Response response) {
|
|
730
|
+
try {
|
|
731
|
+
if (response.isSuccessful()) {
|
|
732
|
+
ResponseBody responseBody = response.body();
|
|
733
|
+
LiveUpdateHttpClient.writeResponseBodyToFile(responseBody, file, progressCallback);
|
|
734
|
+
|
|
735
|
+
// Extract checksum/signature from headers
|
|
736
|
+
String finalChecksum = checksum == null ? LiveUpdateHttpClient.getChecksumFromResponse(response) : checksum;
|
|
737
|
+
String finalSignature = signature == null ? LiveUpdateHttpClient.getSignatureFromResponse(response) : signature;
|
|
738
|
+
|
|
739
|
+
// Verify file
|
|
740
|
+
verifyFile(file, finalChecksum, finalSignature);
|
|
741
|
+
completionCallback.success();
|
|
742
|
+
} else {
|
|
743
|
+
String errorMessage = response.body().string();
|
|
744
|
+
Exception exception = new Exception(LiveUpdatePlugin.ERROR_DOWNLOAD_FAILED);
|
|
745
|
+
Logger.error(LiveUpdatePlugin.TAG, errorMessage, exception);
|
|
746
|
+
completionCallback.error(exception);
|
|
747
|
+
}
|
|
748
|
+
} catch (Exception e) {
|
|
749
|
+
completionCallback.error(e);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
@Override
|
|
754
|
+
public void error(@NonNull Exception exception) {
|
|
755
|
+
completionCallback.error(exception);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
);
|
|
499
759
|
}
|
|
500
760
|
|
|
501
|
-
private
|
|
761
|
+
private Call downloadBundleFile(
|
|
762
|
+
@NonNull String baseUrl,
|
|
763
|
+
@NonNull String href,
|
|
764
|
+
@NonNull File destinationDirectory,
|
|
765
|
+
@Nullable DownloadProgressCallback progressCallback,
|
|
766
|
+
@NonNull EmptyCallback completionCallback
|
|
767
|
+
) {
|
|
502
768
|
HttpUrl.Builder urlBuilder = HttpUrl.parse(baseUrl).newBuilder();
|
|
503
769
|
urlBuilder.addQueryParameter("href", href);
|
|
504
770
|
String url = urlBuilder.build().toString();
|
|
505
771
|
|
|
506
772
|
File destinationFile = new File(destinationDirectory, href);
|
|
507
773
|
destinationFile.getParentFile().mkdirs();
|
|
508
|
-
downloadAndVerifyFile(url, destinationFile, null, null);
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
774
|
+
return downloadAndVerifyFile(url, destinationFile, null, null, progressCallback, completionCallback);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
private void downloadBundleFiles(
|
|
778
|
+
@NonNull String baseUrl,
|
|
779
|
+
@NonNull List<ManifestItem> filesToDownload,
|
|
780
|
+
@NonNull File destinationDirectory,
|
|
781
|
+
@Nullable DownloadProgressCallback progressCallback,
|
|
782
|
+
@NonNull EmptyCallback completionCallback
|
|
783
|
+
) {
|
|
784
|
+
if (filesToDownload.isEmpty()) {
|
|
785
|
+
if (progressCallback != null) {
|
|
786
|
+
progressCallback.onProgress(0, 0);
|
|
787
|
+
}
|
|
788
|
+
completionCallback.success();
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Thread-safe progress tracking
|
|
793
|
+
AtomicLong totalBytesDownloaded = new AtomicLong(0);
|
|
794
|
+
long totalBytesToDownload = 0;
|
|
795
|
+
for (ManifestItem item : filesToDownload) {
|
|
796
|
+
totalBytesToDownload += item.getSizeInBytes();
|
|
797
|
+
}
|
|
798
|
+
final long finalTotalBytesToDownload = totalBytesToDownload;
|
|
799
|
+
|
|
800
|
+
// Coordination primitives
|
|
801
|
+
CountDownLatch latch = new CountDownLatch(filesToDownload.size());
|
|
802
|
+
AtomicReference<Exception> firstError = new AtomicReference<>();
|
|
803
|
+
AtomicBoolean completionHandled = new AtomicBoolean(false);
|
|
804
|
+
List<Call> activeCalls = new ArrayList<>();
|
|
805
|
+
|
|
806
|
+
// Start all downloads asynchronously (OkHttp's dispatcher handles parallelization)
|
|
807
|
+
for (ManifestItem item : filesToDownload) {
|
|
808
|
+
Call call = downloadBundleFile(
|
|
809
|
+
baseUrl,
|
|
810
|
+
item.getHref(),
|
|
811
|
+
destinationDirectory,
|
|
812
|
+
(downloadedBytes, totalBytes) -> {
|
|
813
|
+
// Per-file progress
|
|
814
|
+
if (progressCallback != null) {
|
|
815
|
+
long totalProgress = totalBytesDownloaded.get() + downloadedBytes;
|
|
816
|
+
progressCallback.onProgress(totalProgress, finalTotalBytesToDownload);
|
|
817
|
+
}
|
|
818
|
+
},
|
|
819
|
+
new EmptyCallback() {
|
|
820
|
+
@Override
|
|
821
|
+
public void success() {
|
|
822
|
+
// Update total progress
|
|
823
|
+
totalBytesDownloaded.addAndGet(item.getSizeInBytes());
|
|
824
|
+
if (progressCallback != null) {
|
|
825
|
+
progressCallback.onProgress(totalBytesDownloaded.get(), finalTotalBytesToDownload);
|
|
826
|
+
}
|
|
827
|
+
latch.countDown();
|
|
828
|
+
|
|
829
|
+
// Check if this was the last download to complete
|
|
830
|
+
if (latch.getCount() == 0 && completionHandled.compareAndSet(false, true)) {
|
|
831
|
+
Exception error = firstError.get();
|
|
832
|
+
if (error != null) {
|
|
833
|
+
completionCallback.error(error);
|
|
834
|
+
} else {
|
|
835
|
+
// Final progress update
|
|
836
|
+
if (progressCallback != null) {
|
|
837
|
+
progressCallback.onProgress(finalTotalBytesToDownload, finalTotalBytesToDownload);
|
|
838
|
+
}
|
|
839
|
+
completionCallback.success();
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
@Override
|
|
845
|
+
public void error(@NonNull Exception e) {
|
|
846
|
+
// Capture first error and cancel all remaining downloads
|
|
847
|
+
if (firstError.compareAndSet(null, e)) {
|
|
848
|
+
Logger.error(LiveUpdatePlugin.TAG, "Failed to download file: " + item.getHref(), e);
|
|
849
|
+
// Cancel all in-flight downloads (fail-fast)
|
|
850
|
+
synchronized (activeCalls) {
|
|
851
|
+
for (Call activeCall : activeCalls) {
|
|
852
|
+
if (!activeCall.isCanceled() && !activeCall.isExecuted()) {
|
|
853
|
+
activeCall.cancel();
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
latch.countDown();
|
|
859
|
+
|
|
860
|
+
// Check if this was the last download to complete
|
|
861
|
+
if (latch.getCount() == 0 && completionHandled.compareAndSet(false, true)) {
|
|
862
|
+
completionCallback.error(firstError.get());
|
|
863
|
+
}
|
|
525
864
|
}
|
|
526
865
|
}
|
|
527
|
-
|
|
528
|
-
|
|
866
|
+
);
|
|
867
|
+
synchronized (activeCalls) {
|
|
868
|
+
activeCalls.add(call);
|
|
529
869
|
}
|
|
530
|
-
// Stop if an exception occurred
|
|
531
|
-
if (exceptionReference.get() != null) {
|
|
532
|
-
break;
|
|
533
|
-
}
|
|
534
|
-
// Start a new thread
|
|
535
|
-
Thread thread = new Thread(() -> {
|
|
536
|
-
try {
|
|
537
|
-
downloadBundleFile(baseUrl, href, destinationDirectory);
|
|
538
|
-
} catch (Exception exception) {
|
|
539
|
-
exceptionReference.set(exception);
|
|
540
|
-
Logger.error(LiveUpdatePlugin.TAG, "Failed to download file: " + href, exception);
|
|
541
|
-
}
|
|
542
|
-
});
|
|
543
|
-
threads.add(thread);
|
|
544
|
-
thread.start();
|
|
545
|
-
}
|
|
546
|
-
// Wait for all threads to finish
|
|
547
|
-
for (Thread thread : threads) {
|
|
548
|
-
thread.join();
|
|
549
|
-
}
|
|
550
|
-
// Throw the exception if one occurred
|
|
551
|
-
Exception exception = exceptionReference.get();
|
|
552
|
-
if (exception != null) {
|
|
553
|
-
throw exception;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
private void downloadBundleOfTypeManifest(@NonNull String bundleId, @NonNull String downloadUrl) throws Exception {
|
|
558
|
-
// Create a temporary directory
|
|
559
|
-
File temporaryDirectory = createTemporaryDirectory();
|
|
560
|
-
// Download the latest manifest
|
|
561
|
-
File latestManifestFile = downloadBundleFile(downloadUrl, manifestFileName, temporaryDirectory);
|
|
562
|
-
Manifest latestManifest = loadManifest(latestManifestFile);
|
|
563
|
-
// Load the current manifest
|
|
564
|
-
Manifest currentManifest = loadCurrentManifest();
|
|
565
|
-
// Compare the manifests
|
|
566
|
-
List<ManifestItem> itemsToCopy = new ArrayList<>();
|
|
567
|
-
List<ManifestItem> itemsToDownload = new ArrayList<>();
|
|
568
|
-
if (currentManifest == null) {
|
|
569
|
-
itemsToDownload.addAll(latestManifest.getItems());
|
|
570
|
-
} else {
|
|
571
|
-
itemsToCopy.addAll(Manifest.findDuplicateItems(latestManifest, currentManifest));
|
|
572
|
-
itemsToDownload.addAll(Manifest.findMissingItems(latestManifest, currentManifest));
|
|
573
|
-
}
|
|
574
|
-
// Copy the files
|
|
575
|
-
List<String> hrefsToCopy = new ArrayList<>();
|
|
576
|
-
for (ManifestItem item : itemsToCopy) {
|
|
577
|
-
hrefsToCopy.add(item.getHref());
|
|
578
870
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
private void downloadBundleOfTypeManifest(
|
|
874
|
+
@NonNull String bundleId,
|
|
875
|
+
@NonNull String downloadUrl,
|
|
876
|
+
@NonNull EmptyCallback completionCallback
|
|
877
|
+
) {
|
|
878
|
+
try {
|
|
879
|
+
// Create a temporary directory
|
|
880
|
+
File temporaryDirectory = createTemporaryDirectory();
|
|
881
|
+
|
|
882
|
+
// Download the latest manifest
|
|
883
|
+
downloadBundleFile(
|
|
884
|
+
downloadUrl,
|
|
885
|
+
manifestFileName,
|
|
886
|
+
temporaryDirectory,
|
|
887
|
+
null,
|
|
888
|
+
new EmptyCallback() {
|
|
889
|
+
@Override
|
|
890
|
+
public void success() {
|
|
891
|
+
try {
|
|
892
|
+
File latestManifestFile = new File(temporaryDirectory, manifestFileName);
|
|
893
|
+
Manifest latestManifest = loadManifest(latestManifestFile);
|
|
894
|
+
// Load the current manifest
|
|
895
|
+
Manifest currentManifest = loadCurrentManifest();
|
|
896
|
+
// Compare the manifests
|
|
897
|
+
List<ManifestItem> itemsToCopy = new ArrayList<>();
|
|
898
|
+
List<ManifestItem> itemsToDownload = new ArrayList<>();
|
|
899
|
+
if (currentManifest == null) {
|
|
900
|
+
itemsToDownload.addAll(latestManifest.getItems());
|
|
901
|
+
} else {
|
|
902
|
+
itemsToCopy.addAll(Manifest.findDuplicateItems(latestManifest, currentManifest));
|
|
903
|
+
itemsToDownload.addAll(Manifest.findMissingItems(latestManifest, currentManifest));
|
|
904
|
+
}
|
|
905
|
+
// Copy the files
|
|
906
|
+
List<ManifestItem> missingItems = copyCurrentBundleFilesAndReturnFailures(itemsToCopy, temporaryDirectory);
|
|
907
|
+
// If items could not be copied, add them to the list of items to download
|
|
908
|
+
if (!missingItems.isEmpty()) {
|
|
909
|
+
itemsToDownload.addAll(missingItems);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Download the files
|
|
913
|
+
downloadBundleFiles(
|
|
914
|
+
downloadUrl,
|
|
915
|
+
itemsToDownload,
|
|
916
|
+
temporaryDirectory,
|
|
917
|
+
(downloadedBytes, totalBytes) -> {
|
|
918
|
+
DownloadBundleProgressEvent event = new DownloadBundleProgressEvent(
|
|
919
|
+
bundleId,
|
|
920
|
+
downloadedBytes,
|
|
921
|
+
totalBytes
|
|
922
|
+
);
|
|
923
|
+
notifyDownloadBundleProgressListeners(event);
|
|
924
|
+
},
|
|
925
|
+
new EmptyCallback() {
|
|
926
|
+
@Override
|
|
927
|
+
public void success() {
|
|
928
|
+
try {
|
|
929
|
+
// Add the bundle
|
|
930
|
+
addBundleOfTypeManifest(bundleId, temporaryDirectory);
|
|
931
|
+
completionCallback.success();
|
|
932
|
+
} catch (Exception e) {
|
|
933
|
+
completionCallback.error(e);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
@Override
|
|
938
|
+
public void error(@NonNull Exception exception) {
|
|
939
|
+
completionCallback.error(exception);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
);
|
|
943
|
+
} catch (Exception e) {
|
|
944
|
+
completionCallback.error(e);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
@Override
|
|
949
|
+
public void error(@NonNull Exception exception) {
|
|
950
|
+
completionCallback.error(exception);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
);
|
|
954
|
+
} catch (Exception e) {
|
|
955
|
+
completionCallback.error(e);
|
|
584
956
|
}
|
|
585
|
-
downloadBundleFiles(downloadUrl, hrefsToDownload, temporaryDirectory);
|
|
586
|
-
// Add the bundle
|
|
587
|
-
addBundleOfTypeManifest(bundleId, temporaryDirectory);
|
|
588
957
|
}
|
|
589
958
|
|
|
590
959
|
private void downloadBundleOfTypeZip(
|
|
591
960
|
@NonNull String bundleId,
|
|
592
|
-
@NonNull String downloadUrl,
|
|
593
961
|
@Nullable String checksum,
|
|
594
|
-
@Nullable String signature
|
|
595
|
-
|
|
962
|
+
@Nullable String signature,
|
|
963
|
+
@NonNull String downloadUrl,
|
|
964
|
+
@NonNull EmptyCallback completionCallback
|
|
965
|
+
) {
|
|
596
966
|
File file = buildTemporaryZipFile();
|
|
597
967
|
// Download the bundle
|
|
598
|
-
downloadAndVerifyFile(
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
968
|
+
downloadAndVerifyFile(
|
|
969
|
+
downloadUrl,
|
|
970
|
+
file,
|
|
971
|
+
checksum,
|
|
972
|
+
signature,
|
|
973
|
+
(downloadedBytes, totalBytes) -> {
|
|
974
|
+
DownloadBundleProgressEvent event = new DownloadBundleProgressEvent(bundleId, downloadedBytes, totalBytes);
|
|
975
|
+
notifyDownloadBundleProgressListeners(event);
|
|
976
|
+
},
|
|
977
|
+
new EmptyCallback() {
|
|
978
|
+
@Override
|
|
979
|
+
public void success() {
|
|
980
|
+
try {
|
|
981
|
+
// Add the bundle
|
|
982
|
+
addBundleOfTypeZip(bundleId, file);
|
|
983
|
+
// Delete the temporary file
|
|
984
|
+
file.delete();
|
|
985
|
+
completionCallback.success();
|
|
986
|
+
} catch (Exception e) {
|
|
987
|
+
completionCallback.error(e);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
@Override
|
|
992
|
+
public void error(@NonNull Exception exception) {
|
|
993
|
+
// Delete the temporary file on error
|
|
994
|
+
file.delete();
|
|
995
|
+
completionCallback.error(exception);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
);
|
|
603
999
|
}
|
|
604
1000
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
.
|
|
611
|
-
.
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
1001
|
+
private void fetchLatestBundleInternal(
|
|
1002
|
+
@NonNull FetchLatestBundleOptions options,
|
|
1003
|
+
@NonNull NonEmptyCallback<GetLatestBundleResponse> callback
|
|
1004
|
+
) {
|
|
1005
|
+
try {
|
|
1006
|
+
String channel = options.getChannel() == null ? getChannel() : options.getChannel();
|
|
1007
|
+
String url = new HttpUrl.Builder()
|
|
1008
|
+
.scheme("https")
|
|
1009
|
+
.host(config.getServerDomain())
|
|
1010
|
+
.addPathSegment("v1")
|
|
1011
|
+
.addPathSegment("apps")
|
|
1012
|
+
.addPathSegment(getAppId())
|
|
1013
|
+
.addPathSegment("bundles")
|
|
1014
|
+
.addPathSegment("latest")
|
|
1015
|
+
.addQueryParameter("appVersionCode", getVersionCodeAsString())
|
|
1016
|
+
.addQueryParameter("appVersionName", getVersionName())
|
|
1017
|
+
.addQueryParameter("bundleId", getCurrentBundleId())
|
|
1018
|
+
.addQueryParameter("channelName", channel)
|
|
1019
|
+
.addQueryParameter("customId", preferences.getCustomId())
|
|
1020
|
+
.addQueryParameter("deviceId", getDeviceId())
|
|
1021
|
+
.addQueryParameter("osVersion", String.valueOf(Build.VERSION.SDK_INT))
|
|
1022
|
+
.addQueryParameter("platform", "0")
|
|
1023
|
+
.addQueryParameter("pluginVersion", LiveUpdatePlugin.VERSION)
|
|
1024
|
+
.build()
|
|
1025
|
+
.toString();
|
|
1026
|
+
Logger.debug(LiveUpdatePlugin.TAG, "Fetching latest bundle: " + url);
|
|
1027
|
+
|
|
1028
|
+
httpClient.enqueue(
|
|
1029
|
+
url,
|
|
1030
|
+
new NonEmptyCallback<Response>() {
|
|
1031
|
+
@Override
|
|
1032
|
+
public void success(@NonNull Response response) {
|
|
1033
|
+
try {
|
|
1034
|
+
String responseBodyString = response.body().string();
|
|
1035
|
+
Logger.debug(LiveUpdatePlugin.TAG, "Latest bundle response: " + responseBodyString);
|
|
1036
|
+
if (response.isSuccessful()) {
|
|
1037
|
+
JSONObject responseJson = new JSONObject(responseBodyString);
|
|
1038
|
+
GetLatestBundleResponse result = new GetLatestBundleResponse(responseJson);
|
|
1039
|
+
callback.success(result);
|
|
1040
|
+
} else {
|
|
1041
|
+
callback.success(null);
|
|
1042
|
+
}
|
|
1043
|
+
} catch (Exception e) {
|
|
1044
|
+
callback.error(e);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
@Override
|
|
1049
|
+
public void error(@NonNull Exception exception) {
|
|
1050
|
+
callback.error(exception);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
);
|
|
1054
|
+
} catch (Exception e) {
|
|
1055
|
+
callback.error(e);
|
|
636
1056
|
}
|
|
637
1057
|
}
|
|
638
1058
|
|
|
639
|
-
private String[]
|
|
1059
|
+
private String[] getDownloadedBundleIds() {
|
|
640
1060
|
File bundlesDirectory = buildBundlesDirectory();
|
|
641
1061
|
File[] bundles = bundlesDirectory.listFiles();
|
|
642
1062
|
if (bundles == null) {
|
|
@@ -676,9 +1096,22 @@ public class LiveUpdate {
|
|
|
676
1096
|
return checksum.toString();
|
|
677
1097
|
}
|
|
678
1098
|
|
|
1099
|
+
@Nullable
|
|
1100
|
+
private String getAppId() {
|
|
1101
|
+
String appId = preferences.getAppId();
|
|
1102
|
+
if (appId != null) {
|
|
1103
|
+
return appId;
|
|
1104
|
+
}
|
|
1105
|
+
return config.getAppId();
|
|
1106
|
+
}
|
|
1107
|
+
|
|
679
1108
|
@Nullable
|
|
680
1109
|
private String getChannel() {
|
|
681
1110
|
String channel = null;
|
|
1111
|
+
String nativeChannel = getNativeChannel();
|
|
1112
|
+
if (nativeChannel != null) {
|
|
1113
|
+
channel = nativeChannel;
|
|
1114
|
+
}
|
|
682
1115
|
if (config.getDefaultChannel() != null) {
|
|
683
1116
|
channel = config.getDefaultChannel();
|
|
684
1117
|
}
|
|
@@ -688,13 +1121,26 @@ public class LiveUpdate {
|
|
|
688
1121
|
return channel;
|
|
689
1122
|
}
|
|
690
1123
|
|
|
1124
|
+
@Nullable
|
|
1125
|
+
private String getNativeChannel() {
|
|
1126
|
+
int resId = plugin
|
|
1127
|
+
.getContext()
|
|
1128
|
+
.getResources()
|
|
1129
|
+
.getIdentifier("capawesome_live_update_default_channel", "string", plugin.getContext().getPackageName());
|
|
1130
|
+
if (resId == 0) {
|
|
1131
|
+
return null;
|
|
1132
|
+
}
|
|
1133
|
+
return plugin.getContext().getResources().getString(resId);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
691
1136
|
/**
|
|
692
|
-
* @return The current bundle ID
|
|
1137
|
+
* @return The current bundle ID or `null` if the default bundle is in use.
|
|
693
1138
|
*/
|
|
1139
|
+
@Nullable
|
|
694
1140
|
private String getCurrentBundleId() {
|
|
695
1141
|
String currentPath = getCurrentCapacitorServerPath();
|
|
696
1142
|
if (currentPath.equals(defaultWebAssetDir)) {
|
|
697
|
-
return
|
|
1143
|
+
return null;
|
|
698
1144
|
}
|
|
699
1145
|
return new File(currentPath).getName();
|
|
700
1146
|
}
|
|
@@ -708,22 +1154,22 @@ public class LiveUpdate {
|
|
|
708
1154
|
|
|
709
1155
|
@NonNull
|
|
710
1156
|
private String getDeviceId() {
|
|
711
|
-
String deviceId = preferences.getDeviceIdForApp(
|
|
1157
|
+
String deviceId = preferences.getDeviceIdForApp(getAppId());
|
|
712
1158
|
if (deviceId == null) {
|
|
713
1159
|
deviceId = UUID.randomUUID().toString().toLowerCase();
|
|
714
|
-
preferences.setDeviceIdForApp(
|
|
1160
|
+
preferences.setDeviceIdForApp(getAppId(), deviceId);
|
|
715
1161
|
}
|
|
716
1162
|
return deviceId;
|
|
717
1163
|
}
|
|
718
1164
|
|
|
719
1165
|
/**
|
|
720
|
-
* @return The next bundle ID
|
|
1166
|
+
* @return The next bundle ID or `null` if the default bundle will be used.
|
|
721
1167
|
*/
|
|
722
|
-
@
|
|
1168
|
+
@Nullable
|
|
723
1169
|
private String getNextBundleId() {
|
|
724
1170
|
String nextPath = getNextCapacitorServerPath();
|
|
725
1171
|
if (nextPath.equals(defaultWebAssetDir)) {
|
|
726
|
-
return
|
|
1172
|
+
return null;
|
|
727
1173
|
}
|
|
728
1174
|
return new File(nextPath).getName();
|
|
729
1175
|
}
|
|
@@ -733,31 +1179,54 @@ public class LiveUpdate {
|
|
|
733
1179
|
*/
|
|
734
1180
|
@NonNull
|
|
735
1181
|
private String getNextCapacitorServerPath() {
|
|
736
|
-
|
|
1182
|
+
String path = plugin
|
|
737
1183
|
.getContext()
|
|
738
1184
|
.getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE)
|
|
739
1185
|
.getString(WebView.CAP_SERVER_PATH, defaultWebAssetDir);
|
|
1186
|
+
// Empty path means default path
|
|
1187
|
+
if (path.isEmpty()) {
|
|
1188
|
+
path = defaultWebAssetDir;
|
|
1189
|
+
}
|
|
1190
|
+
return path;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
/**
|
|
1194
|
+
* @return The previous bundle ID or `null` if the default bundle was used.
|
|
1195
|
+
*/
|
|
1196
|
+
@Nullable
|
|
1197
|
+
private String getPreviousBundleId() {
|
|
1198
|
+
return preferences.getPreviousBundleId();
|
|
740
1199
|
}
|
|
741
1200
|
|
|
742
|
-
private
|
|
743
|
-
return
|
|
1201
|
+
private int getVersionCodeAsInt() throws PackageManager.NameNotFoundException {
|
|
1202
|
+
return getPackageInfo().versionCode;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
private String getVersionCodeAsString() throws PackageManager.NameNotFoundException {
|
|
1206
|
+
return String.valueOf(getVersionCodeAsInt());
|
|
744
1207
|
}
|
|
745
1208
|
|
|
746
1209
|
private String getVersionName() throws PackageManager.NameNotFoundException {
|
|
747
1210
|
return getPackageInfo().versionName;
|
|
748
1211
|
}
|
|
749
1212
|
|
|
750
|
-
private boolean
|
|
1213
|
+
private boolean hasBundleById(@NonNull String bundleId) {
|
|
751
1214
|
File bundleDirectory = buildBundleDirectoryFor(bundleId);
|
|
752
1215
|
return bundleDirectory.exists();
|
|
753
1216
|
}
|
|
754
1217
|
|
|
1218
|
+
private boolean isBundleInUse(@NonNull String bundleId) {
|
|
1219
|
+
String currentBundleId = getCurrentBundleId();
|
|
1220
|
+
String nextBundleId = getNextBundleId();
|
|
1221
|
+
return bundleId.equals(currentBundleId) || bundleId.equals(nextBundleId);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
755
1224
|
@Nullable
|
|
756
1225
|
private Manifest loadCurrentManifest() throws Exception {
|
|
757
1226
|
String currentBundleId = getCurrentBundleId();
|
|
758
|
-
if (currentBundleId
|
|
1227
|
+
if (currentBundleId == null) {
|
|
759
1228
|
AssetManager assets = plugin.getContext().getAssets();
|
|
760
|
-
|
|
1229
|
+
boolean manifestFileExists = Arrays.asList(assets.list(defaultWebAssetDir)).contains(manifestFileName);
|
|
761
1230
|
if (manifestFileExists) {
|
|
762
1231
|
InputStream inputStream = assets.open(defaultWebAssetDir + "/" + manifestFileName);
|
|
763
1232
|
BufferedSource source = Okio.buffer(Okio.source(inputStream));
|
|
@@ -787,25 +1256,53 @@ public class LiveUpdate {
|
|
|
787
1256
|
return loadManifest(source);
|
|
788
1257
|
}
|
|
789
1258
|
|
|
790
|
-
private
|
|
791
|
-
|
|
792
|
-
String nextBundleId = getNextBundleId();
|
|
793
|
-
return bundleId.equals(currentBundleId) || bundleId.equals(nextBundleId);
|
|
1259
|
+
private void notifyDownloadBundleProgressListeners(@NonNull final DownloadBundleProgressEvent event) {
|
|
1260
|
+
plugin.notifyDownloadBundleProgressListeners(event);
|
|
794
1261
|
}
|
|
795
1262
|
|
|
796
|
-
private void
|
|
797
|
-
if
|
|
798
|
-
|
|
1263
|
+
private void performAutoUpdate() {
|
|
1264
|
+
// Check if enough time has passed since the last check
|
|
1265
|
+
long now = System.currentTimeMillis();
|
|
1266
|
+
if (lastAutoUpdateCheckTimestamp > 0 && (now - lastAutoUpdateCheckTimestamp) < autoUpdateIntervalMs) {
|
|
1267
|
+
Logger.debug(LiveUpdatePlugin.TAG, "Auto-update skipped. Last check was less than 15 minutes ago.");
|
|
799
1268
|
return;
|
|
800
1269
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
1270
|
+
|
|
1271
|
+
// Update the timestamp
|
|
1272
|
+
lastAutoUpdateCheckTimestamp = now;
|
|
1273
|
+
|
|
1274
|
+
// Run sync
|
|
1275
|
+
Logger.debug(LiveUpdatePlugin.TAG, "Auto-update started.");
|
|
1276
|
+
SyncOptions options = new SyncOptions((String) null);
|
|
1277
|
+
NonEmptyCallback<Result> callback = new NonEmptyCallback<>() {
|
|
1278
|
+
@Override
|
|
1279
|
+
public void success(@NonNull Result result) {
|
|
1280
|
+
Logger.debug(LiveUpdatePlugin.TAG, "Auto-update completed successfully.");
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
@Override
|
|
1284
|
+
public void error(@NonNull Exception exception) {
|
|
1285
|
+
Logger.error(LiveUpdatePlugin.TAG, "Auto-update failed: " + exception.getMessage(), exception);
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
1288
|
+
sync(options, callback);
|
|
804
1289
|
}
|
|
805
1290
|
|
|
806
|
-
private void
|
|
807
|
-
|
|
808
|
-
|
|
1291
|
+
private void rollback() {
|
|
1292
|
+
// Set the rollback flag
|
|
1293
|
+
rollbackPerformed = true;
|
|
1294
|
+
// Set the new previous bundle ID
|
|
1295
|
+
String currentBundleId = getCurrentBundleId();
|
|
1296
|
+
setPreviousBundleId(currentBundleId);
|
|
1297
|
+
// Log the rollback result
|
|
1298
|
+
if (currentBundleId == null) {
|
|
1299
|
+
Logger.debug(LiveUpdatePlugin.TAG, "App is not ready. Default bundle is already in use.");
|
|
1300
|
+
} else {
|
|
1301
|
+
Logger.debug(LiveUpdatePlugin.TAG, "App is not ready. Rolling back to default bundle.");
|
|
1302
|
+
// Rollback to the default bundle
|
|
1303
|
+
setNextBundleById(null);
|
|
1304
|
+
setCurrentBundleById(null);
|
|
1305
|
+
}
|
|
809
1306
|
}
|
|
810
1307
|
|
|
811
1308
|
@Nullable
|
|
@@ -833,6 +1330,18 @@ public class LiveUpdate {
|
|
|
833
1330
|
return null;
|
|
834
1331
|
}
|
|
835
1332
|
|
|
1333
|
+
/**
|
|
1334
|
+
* @param bundleId The bundle ID to set as the current bundle. If `null`, the default bundle will be used.
|
|
1335
|
+
*/
|
|
1336
|
+
private void setCurrentBundleById(@Nullable String bundleId) {
|
|
1337
|
+
if (bundleId == null) {
|
|
1338
|
+
setCurrentCapacitorServerPath(defaultWebAssetDir);
|
|
1339
|
+
} else {
|
|
1340
|
+
File bundleDirectory = buildBundleDirectoryFor(bundleId);
|
|
1341
|
+
setCurrentCapacitorServerPath(bundleDirectory.getPath());
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
|
|
836
1345
|
private void setCurrentCapacitorServerPath(@NonNull String path) {
|
|
837
1346
|
if (path.equals(defaultWebAssetDir)) {
|
|
838
1347
|
this.plugin.getBridge().setServerAssetPath(path);
|
|
@@ -840,15 +1349,72 @@ public class LiveUpdate {
|
|
|
840
1349
|
this.plugin.getBridge().setServerBasePath(path);
|
|
841
1350
|
}
|
|
842
1351
|
this.plugin.getBridge().reload();
|
|
1352
|
+
// Notify listeners
|
|
1353
|
+
notifyReloadedListeners();
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/**
|
|
1357
|
+
* @param bundleId The bundle ID to set as the next bundle. If `null`, the default bundle will be used.
|
|
1358
|
+
*/
|
|
1359
|
+
private void setNextBundleById(@Nullable String bundleId) {
|
|
1360
|
+
if (bundleId == null) {
|
|
1361
|
+
setNextCapacitorServerPath(defaultWebAssetDir);
|
|
1362
|
+
} else {
|
|
1363
|
+
File bundleDirectory = buildBundleDirectoryFor(bundleId);
|
|
1364
|
+
setNextCapacitorServerPath(bundleDirectory.getPath());
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// Notify listeners
|
|
1368
|
+
notifyNextBundleSetListeners(bundleId);
|
|
843
1369
|
}
|
|
844
1370
|
|
|
845
|
-
private void
|
|
846
|
-
|
|
1371
|
+
private void notifyNextBundleSetListeners(@Nullable String bundleId) {
|
|
1372
|
+
NextBundleSetEvent event = new NextBundleSetEvent(bundleId);
|
|
1373
|
+
plugin.notifyNextBundleSetListeners(event);
|
|
847
1374
|
}
|
|
848
1375
|
|
|
849
|
-
private void
|
|
850
|
-
|
|
851
|
-
|
|
1376
|
+
private void notifyReloadedListeners() {
|
|
1377
|
+
plugin.notifyReloadedListeners();
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
private void addBlockedBundleId(@NonNull String bundleId) {
|
|
1381
|
+
String blockedIds = preferences.getBlockedBundleIds();
|
|
1382
|
+
List<String> blockedList = new ArrayList<>();
|
|
1383
|
+
|
|
1384
|
+
// Parse existing blocked IDs
|
|
1385
|
+
if (blockedIds != null && !blockedIds.isEmpty()) {
|
|
1386
|
+
String[] ids = blockedIds.split(",");
|
|
1387
|
+
blockedList.addAll(Arrays.asList(ids));
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// Skip if already blocked
|
|
1391
|
+
if (blockedList.contains(bundleId)) {
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// Remove oldest if limit reached
|
|
1396
|
+
if (blockedList.size() >= 100) {
|
|
1397
|
+
blockedList.remove(0);
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// Add new bundle
|
|
1401
|
+
blockedList.add(bundleId);
|
|
1402
|
+
|
|
1403
|
+
// Save back to preferences
|
|
1404
|
+
String newBlockedIds = String.join(",", blockedList);
|
|
1405
|
+
preferences.setBlockedBundleIds(newBlockedIds);
|
|
1406
|
+
|
|
1407
|
+
Logger.debug(LiveUpdatePlugin.TAG, "Bundle blocked: " + bundleId);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
private boolean isBlockedBundleId(@NonNull String bundleId) {
|
|
1411
|
+
String blockedIds = preferences.getBlockedBundleIds();
|
|
1412
|
+
if (blockedIds == null || blockedIds.isEmpty()) {
|
|
1413
|
+
return false;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
String[] ids = blockedIds.split(",");
|
|
1417
|
+
return Arrays.asList(ids).contains(bundleId);
|
|
852
1418
|
}
|
|
853
1419
|
|
|
854
1420
|
private void setNextCapacitorServerPath(@NonNull String path) {
|
|
@@ -856,11 +1422,25 @@ public class LiveUpdate {
|
|
|
856
1422
|
this.webViewSettingsEditor.commit();
|
|
857
1423
|
}
|
|
858
1424
|
|
|
859
|
-
private
|
|
860
|
-
|
|
1425
|
+
private boolean wasUpdated() throws PackageManager.NameNotFoundException {
|
|
1426
|
+
int lastVersionCode = preferences.getLastVersionCode();
|
|
1427
|
+
int currentVersionCode = getVersionCodeAsInt();
|
|
1428
|
+
return lastVersionCode != currentVersionCode;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
private void saveCurrentVersionCode() throws PackageManager.NameNotFoundException {
|
|
1432
|
+
int currentVersionCode = getVersionCodeAsInt();
|
|
1433
|
+
preferences.setLastVersionCode(currentVersionCode);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
private void setPreviousBundleId(@Nullable String bundleId) {
|
|
1437
|
+
preferences.setPreviousBundleId(bundleId);
|
|
861
1438
|
}
|
|
862
1439
|
|
|
863
1440
|
private void startRollbackTimer() {
|
|
1441
|
+
if (config.getReadyTimeout() <= 0) {
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
864
1444
|
stopRollbackTimer();
|
|
865
1445
|
rollbackHandler.postDelayed(() -> rollback(), config.getReadyTimeout());
|
|
866
1446
|
}
|
|
@@ -869,10 +1449,25 @@ public class LiveUpdate {
|
|
|
869
1449
|
rollbackHandler.removeCallbacksAndMessages(null);
|
|
870
1450
|
}
|
|
871
1451
|
|
|
1452
|
+
private boolean tryCopyCurrentBundleFile(@NonNull ManifestItem fileToCopy, @NonNull File destinationDirectory) {
|
|
1453
|
+
try {
|
|
1454
|
+
copyCurrentBundleFile(fileToCopy, destinationDirectory);
|
|
1455
|
+
return true;
|
|
1456
|
+
} catch (IOException exception) {
|
|
1457
|
+
return false;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
872
1461
|
private File unzipFile(@NonNull File zipFile) throws IOException {
|
|
873
1462
|
File destination = buildTemporaryDirectory();
|
|
874
1463
|
String destinationPath = destination.getPath();
|
|
875
|
-
new ZipFile(zipFile)
|
|
1464
|
+
ZipFile zip = new ZipFile(zipFile);
|
|
1465
|
+
// Clear stored Unix permissions to prevent EACCES errors on newer Android versions
|
|
1466
|
+
// where restrictive directory permissions from the zip block file creation.
|
|
1467
|
+
for (net.lingala.zip4j.model.FileHeader fileHeader : zip.getFileHeaders()) {
|
|
1468
|
+
fileHeader.setExternalFileAttributes(null);
|
|
1469
|
+
}
|
|
1470
|
+
zip.extractAll(destinationPath);
|
|
876
1471
|
return destination;
|
|
877
1472
|
}
|
|
878
1473
|
|
|
@@ -927,12 +1522,6 @@ public class LiveUpdate {
|
|
|
927
1522
|
}
|
|
928
1523
|
}
|
|
929
1524
|
|
|
930
|
-
private boolean wasUpdated() throws PackageManager.NameNotFoundException {
|
|
931
|
-
int lastVersionCode = preferences.getLastVersionCode();
|
|
932
|
-
int currentVersionCode = getPackageInfo().versionCode;
|
|
933
|
-
return lastVersionCode != currentVersionCode;
|
|
934
|
-
}
|
|
935
|
-
|
|
936
1525
|
private PackageInfo getPackageInfo() throws PackageManager.NameNotFoundException {
|
|
937
1526
|
String packageName = this.plugin.getContext().getPackageName();
|
|
938
1527
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|