@capawesome/capacitor-live-update 6.8.0 → 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 +429 -20
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +851 -261
- 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 +11 -2
- 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 +8 -0
- 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 +853 -50
- package/dist/esm/definitions.d.ts +411 -1
- 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 -1
- 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 -0
- 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,50 +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
|
-
|
|
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);
|
|
160
276
|
callback.success(result);
|
|
161
277
|
}
|
|
162
278
|
|
|
163
|
-
public void getBundle(@NonNull NonEmptyCallback callback) {
|
|
279
|
+
public void getBundle(@NonNull NonEmptyCallback<GetBundleResult> callback) {
|
|
164
280
|
String bundleId = getCurrentBundleId();
|
|
165
|
-
if (bundleId.equals(defaultWebAssetDir)) {
|
|
166
|
-
bundleId = null;
|
|
167
|
-
}
|
|
168
281
|
GetBundleResult result = new GetBundleResult(bundleId);
|
|
169
282
|
callback.success(result);
|
|
170
283
|
}
|
|
171
284
|
|
|
172
285
|
public void getBundles(@NonNull NonEmptyCallback callback) {
|
|
173
|
-
String[] bundleIds =
|
|
286
|
+
String[] bundleIds = getDownloadedBundleIds();
|
|
174
287
|
GetBundlesResult result = new GetBundlesResult(bundleIds);
|
|
175
288
|
callback.success(result);
|
|
176
289
|
}
|
|
177
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
|
+
|
|
178
297
|
public void getChannel(@NonNull NonEmptyCallback callback) {
|
|
179
298
|
String channel = getChannel();
|
|
180
299
|
GetChannelResult result = new GetChannelResult(channel);
|
|
181
300
|
callback.success(result);
|
|
182
301
|
}
|
|
183
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
|
+
|
|
184
310
|
public void getCurrentBundle(@NonNull NonEmptyCallback<GetCurrentBundleResult> callback) {
|
|
185
311
|
String bundleId = getCurrentBundleId();
|
|
186
|
-
if (bundleId.equals(defaultWebAssetDir)) {
|
|
187
|
-
bundleId = null;
|
|
188
|
-
}
|
|
189
312
|
GetCurrentBundleResult result = new GetCurrentBundleResult(bundleId);
|
|
190
313
|
callback.success(result);
|
|
191
314
|
}
|
|
@@ -204,15 +327,12 @@ public class LiveUpdate {
|
|
|
204
327
|
|
|
205
328
|
public void getNextBundle(@NonNull NonEmptyCallback<GetNextBundleResult> callback) {
|
|
206
329
|
String bundleId = getNextBundleId();
|
|
207
|
-
if (bundleId.equals(defaultWebAssetDir)) {
|
|
208
|
-
bundleId = null;
|
|
209
|
-
}
|
|
210
330
|
GetNextBundleResult result = new GetNextBundleResult(bundleId);
|
|
211
331
|
callback.success(result);
|
|
212
332
|
}
|
|
213
333
|
|
|
214
334
|
public void getVersionCode(@NonNull NonEmptyCallback callback) throws PackageManager.NameNotFoundException {
|
|
215
|
-
String versionCode =
|
|
335
|
+
String versionCode = getVersionCodeAsString();
|
|
216
336
|
GetVersionCodeResult result = new GetVersionCodeResult(versionCode);
|
|
217
337
|
callback.success(result);
|
|
218
338
|
}
|
|
@@ -223,14 +343,51 @@ public class LiveUpdate {
|
|
|
223
343
|
callback.success(result);
|
|
224
344
|
}
|
|
225
345
|
|
|
226
|
-
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) {
|
|
227
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
|
+
}
|
|
228
371
|
// Stop the rollback timer
|
|
229
372
|
stopRollbackTimer();
|
|
230
373
|
// Delete unused bundles
|
|
231
374
|
if (config.getAutoDeleteBundles()) {
|
|
232
375
|
deleteUnusedBundles();
|
|
233
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;
|
|
234
391
|
}
|
|
235
392
|
|
|
236
393
|
public void reload() {
|
|
@@ -240,19 +397,23 @@ public class LiveUpdate {
|
|
|
240
397
|
}
|
|
241
398
|
|
|
242
399
|
public void reset() {
|
|
243
|
-
|
|
400
|
+
setNextBundleById(null);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
public void resetConfig() {
|
|
404
|
+
preferences.setAppId(null);
|
|
244
405
|
}
|
|
245
406
|
|
|
246
407
|
public void setBundle(@NonNull SetBundleOptions options, @NonNull EmptyCallback callback) {
|
|
247
408
|
String bundleId = options.getBundleId();
|
|
248
409
|
|
|
249
|
-
if (!
|
|
410
|
+
if (!hasBundleById(bundleId)) {
|
|
250
411
|
Exception exception = new Exception(LiveUpdatePlugin.ERROR_BUNDLE_NOT_FOUND);
|
|
251
412
|
callback.error(exception);
|
|
252
413
|
return;
|
|
253
414
|
}
|
|
254
415
|
|
|
255
|
-
|
|
416
|
+
setNextBundleById(bundleId);
|
|
256
417
|
callback.success();
|
|
257
418
|
}
|
|
258
419
|
|
|
@@ -263,6 +424,11 @@ public class LiveUpdate {
|
|
|
263
424
|
callback.success();
|
|
264
425
|
}
|
|
265
426
|
|
|
427
|
+
public void setConfig(@NonNull SetConfigOptions options) {
|
|
428
|
+
String appId = options.getAppId();
|
|
429
|
+
preferences.setAppId(appId);
|
|
430
|
+
}
|
|
431
|
+
|
|
266
432
|
public void setCustomId(@NonNull SetCustomIdOptions options, @NonNull EmptyCallback callback) {
|
|
267
433
|
String customId = options.getCustomId();
|
|
268
434
|
|
|
@@ -276,8 +442,8 @@ public class LiveUpdate {
|
|
|
276
442
|
if (bundleId == null) {
|
|
277
443
|
reset();
|
|
278
444
|
} else {
|
|
279
|
-
if (
|
|
280
|
-
|
|
445
|
+
if (hasBundleById(bundleId)) {
|
|
446
|
+
setNextBundleById(bundleId);
|
|
281
447
|
} else {
|
|
282
448
|
Exception exception = new Exception(LiveUpdatePlugin.ERROR_BUNDLE_NOT_FOUND);
|
|
283
449
|
callback.error(exception);
|
|
@@ -287,45 +453,99 @@ public class LiveUpdate {
|
|
|
287
453
|
callback.success();
|
|
288
454
|
}
|
|
289
455
|
|
|
290
|
-
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
|
+
|
|
291
464
|
String channel = options.getChannel();
|
|
292
465
|
// Fetch the latest bundle
|
|
293
466
|
FetchLatestBundleOptions fetchLatestBundleOptions = new FetchLatestBundleOptions(channel);
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
+
}
|
|
314
547
|
}
|
|
315
|
-
|
|
316
|
-
callback.success(syncResult);
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
// Download the bundle
|
|
320
|
-
if (artifactType == ArtifactType.MANIFEST) {
|
|
321
|
-
downloadBundleOfTypeManifest(latestBundleId, url);
|
|
322
|
-
} else {
|
|
323
|
-
downloadBundleOfTypeZip(latestBundleId, url, checksum, signature);
|
|
324
|
-
}
|
|
325
|
-
// Set the next bundle
|
|
326
|
-
setNextBundle(latestBundleId);
|
|
327
|
-
SyncResult syncResult = new SyncResult(latestBundleId);
|
|
328
|
-
callback.success(syncResult);
|
|
548
|
+
);
|
|
329
549
|
}
|
|
330
550
|
|
|
331
551
|
private void addBundle(@NonNull String bundleId, @NonNull File sourceDirectory) throws Exception {
|
|
@@ -372,9 +592,10 @@ public class LiveUpdate {
|
|
|
372
592
|
return new File(plugin.getContext().getCacheDir(), fileName);
|
|
373
593
|
}
|
|
374
594
|
|
|
375
|
-
private void copyCurrentBundleFile(@NonNull
|
|
595
|
+
private void copyCurrentBundleFile(@NonNull ManifestItem fileToCopy, @NonNull File destinationDirectory) throws IOException {
|
|
596
|
+
String href = fileToCopy.getHref();
|
|
376
597
|
String currentBundleId = getCurrentBundleId();
|
|
377
|
-
if (currentBundleId
|
|
598
|
+
if (currentBundleId == null) {
|
|
378
599
|
// Create the source input stream
|
|
379
600
|
AssetManager assets = plugin.getContext().getAssets();
|
|
380
601
|
InputStream inputStream = assets.open(defaultWebAssetDir + "/" + href);
|
|
@@ -396,10 +617,20 @@ public class LiveUpdate {
|
|
|
396
617
|
}
|
|
397
618
|
}
|
|
398
619
|
|
|
399
|
-
private
|
|
400
|
-
|
|
401
|
-
|
|
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
|
+
}
|
|
402
632
|
}
|
|
633
|
+
return missingItems;
|
|
403
634
|
}
|
|
404
635
|
|
|
405
636
|
private void copyFile(File input, File output) throws IOException {
|
|
@@ -451,191 +682,381 @@ public class LiveUpdate {
|
|
|
451
682
|
return file;
|
|
452
683
|
}
|
|
453
684
|
|
|
454
|
-
private void
|
|
685
|
+
private void deleteBundleById(@NonNull String bundleId) {
|
|
455
686
|
// Delete the bundle directory
|
|
456
687
|
File bundleDirectory = buildBundleDirectoryFor(bundleId);
|
|
457
688
|
deleteFileRecursively(bundleDirectory);
|
|
458
689
|
// Reset the next bundle if it is the deleted bundle
|
|
459
690
|
String nextBundleId = getNextBundleId();
|
|
460
691
|
if (bundleId.equals(nextBundleId)) {
|
|
461
|
-
|
|
692
|
+
setNextBundleById(null);
|
|
462
693
|
}
|
|
463
694
|
}
|
|
464
695
|
|
|
465
696
|
private void deleteFileRecursively(@NonNull File file) {
|
|
466
697
|
if (file.isDirectory()) {
|
|
467
|
-
|
|
468
|
-
|
|
698
|
+
File[] children = file.listFiles();
|
|
699
|
+
if (children != null) {
|
|
700
|
+
for (File child : children) {
|
|
701
|
+
deleteFileRecursively(child);
|
|
702
|
+
}
|
|
469
703
|
}
|
|
470
704
|
}
|
|
471
705
|
file.delete();
|
|
472
706
|
}
|
|
473
707
|
|
|
474
708
|
private void deleteUnusedBundles() {
|
|
475
|
-
String[] bundleIds =
|
|
709
|
+
String[] bundleIds = getDownloadedBundleIds();
|
|
476
710
|
for (String bundleId : bundleIds) {
|
|
477
711
|
if (!isBundleInUse(bundleId)) {
|
|
478
|
-
|
|
712
|
+
deleteBundleById(bundleId);
|
|
479
713
|
}
|
|
480
714
|
}
|
|
481
715
|
}
|
|
482
716
|
|
|
483
|
-
private
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
+
);
|
|
498
759
|
}
|
|
499
760
|
|
|
500
|
-
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
|
+
) {
|
|
501
768
|
HttpUrl.Builder urlBuilder = HttpUrl.parse(baseUrl).newBuilder();
|
|
502
769
|
urlBuilder.addQueryParameter("href", href);
|
|
503
770
|
String url = urlBuilder.build().toString();
|
|
504
771
|
|
|
505
772
|
File destinationFile = new File(destinationDirectory, href);
|
|
506
773
|
destinationFile.getParentFile().mkdirs();
|
|
507
|
-
downloadAndVerifyFile(url, destinationFile, null, null);
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
+
}
|
|
524
864
|
}
|
|
525
865
|
}
|
|
526
|
-
|
|
527
|
-
|
|
866
|
+
);
|
|
867
|
+
synchronized (activeCalls) {
|
|
868
|
+
activeCalls.add(call);
|
|
528
869
|
}
|
|
529
|
-
// Stop if an exception occurred
|
|
530
|
-
if (exceptionReference.get() != null) {
|
|
531
|
-
break;
|
|
532
|
-
}
|
|
533
|
-
// Start a new thread
|
|
534
|
-
Thread thread = new Thread(() -> {
|
|
535
|
-
try {
|
|
536
|
-
downloadBundleFile(baseUrl, href, destinationDirectory);
|
|
537
|
-
} catch (Exception exception) {
|
|
538
|
-
exceptionReference.set(exception);
|
|
539
|
-
Logger.error(LiveUpdatePlugin.TAG, "Failed to download file: " + href, exception);
|
|
540
|
-
}
|
|
541
|
-
});
|
|
542
|
-
threads.add(thread);
|
|
543
|
-
thread.start();
|
|
544
|
-
}
|
|
545
|
-
// Wait for all threads to finish
|
|
546
|
-
for (Thread thread : threads) {
|
|
547
|
-
thread.join();
|
|
548
|
-
}
|
|
549
|
-
// Throw the exception if one occurred
|
|
550
|
-
Exception exception = exceptionReference.get();
|
|
551
|
-
if (exception != null) {
|
|
552
|
-
throw exception;
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
private void downloadBundleOfTypeManifest(@NonNull String bundleId, @NonNull String downloadUrl) throws Exception {
|
|
557
|
-
// Create a temporary directory
|
|
558
|
-
File temporaryDirectory = createTemporaryDirectory();
|
|
559
|
-
// Download the latest manifest
|
|
560
|
-
File latestManifestFile = downloadBundleFile(downloadUrl, manifestFileName, temporaryDirectory);
|
|
561
|
-
Manifest latestManifest = loadManifest(latestManifestFile);
|
|
562
|
-
// Load the current manifest
|
|
563
|
-
Manifest currentManifest = loadCurrentManifest();
|
|
564
|
-
// Compare the manifests
|
|
565
|
-
List<ManifestItem> itemsToCopy = new ArrayList<>();
|
|
566
|
-
List<ManifestItem> itemsToDownload = new ArrayList<>();
|
|
567
|
-
if (currentManifest == null) {
|
|
568
|
-
itemsToDownload.addAll(latestManifest.getItems());
|
|
569
|
-
} else {
|
|
570
|
-
itemsToCopy.addAll(Manifest.findDuplicateItems(latestManifest, currentManifest));
|
|
571
|
-
itemsToDownload.addAll(Manifest.findMissingItems(latestManifest, currentManifest));
|
|
572
|
-
}
|
|
573
|
-
// Copy the files
|
|
574
|
-
List<String> hrefsToCopy = new ArrayList<>();
|
|
575
|
-
for (ManifestItem item : itemsToCopy) {
|
|
576
|
-
hrefsToCopy.add(item.getHref());
|
|
577
870
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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);
|
|
583
956
|
}
|
|
584
|
-
downloadBundleFiles(downloadUrl, hrefsToDownload, temporaryDirectory);
|
|
585
|
-
// Add the bundle
|
|
586
|
-
addBundleOfTypeManifest(bundleId, temporaryDirectory);
|
|
587
957
|
}
|
|
588
958
|
|
|
589
959
|
private void downloadBundleOfTypeZip(
|
|
590
960
|
@NonNull String bundleId,
|
|
591
|
-
@NonNull String downloadUrl,
|
|
592
961
|
@Nullable String checksum,
|
|
593
|
-
@Nullable String signature
|
|
594
|
-
|
|
962
|
+
@Nullable String signature,
|
|
963
|
+
@NonNull String downloadUrl,
|
|
964
|
+
@NonNull EmptyCallback completionCallback
|
|
965
|
+
) {
|
|
595
966
|
File file = buildTemporaryZipFile();
|
|
596
967
|
// Download the bundle
|
|
597
|
-
downloadAndVerifyFile(
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
+
);
|
|
602
999
|
}
|
|
603
1000
|
|
|
604
|
-
|
|
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
|
-
|
|
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);
|
|
635
1056
|
}
|
|
636
1057
|
}
|
|
637
1058
|
|
|
638
|
-
private String[]
|
|
1059
|
+
private String[] getDownloadedBundleIds() {
|
|
639
1060
|
File bundlesDirectory = buildBundlesDirectory();
|
|
640
1061
|
File[] bundles = bundlesDirectory.listFiles();
|
|
641
1062
|
if (bundles == null) {
|
|
@@ -675,9 +1096,22 @@ public class LiveUpdate {
|
|
|
675
1096
|
return checksum.toString();
|
|
676
1097
|
}
|
|
677
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
|
+
|
|
678
1108
|
@Nullable
|
|
679
1109
|
private String getChannel() {
|
|
680
1110
|
String channel = null;
|
|
1111
|
+
String nativeChannel = getNativeChannel();
|
|
1112
|
+
if (nativeChannel != null) {
|
|
1113
|
+
channel = nativeChannel;
|
|
1114
|
+
}
|
|
681
1115
|
if (config.getDefaultChannel() != null) {
|
|
682
1116
|
channel = config.getDefaultChannel();
|
|
683
1117
|
}
|
|
@@ -687,13 +1121,26 @@ public class LiveUpdate {
|
|
|
687
1121
|
return channel;
|
|
688
1122
|
}
|
|
689
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
|
+
|
|
690
1136
|
/**
|
|
691
|
-
* @return The current bundle ID
|
|
1137
|
+
* @return The current bundle ID or `null` if the default bundle is in use.
|
|
692
1138
|
*/
|
|
1139
|
+
@Nullable
|
|
693
1140
|
private String getCurrentBundleId() {
|
|
694
1141
|
String currentPath = getCurrentCapacitorServerPath();
|
|
695
1142
|
if (currentPath.equals(defaultWebAssetDir)) {
|
|
696
|
-
return
|
|
1143
|
+
return null;
|
|
697
1144
|
}
|
|
698
1145
|
return new File(currentPath).getName();
|
|
699
1146
|
}
|
|
@@ -707,22 +1154,22 @@ public class LiveUpdate {
|
|
|
707
1154
|
|
|
708
1155
|
@NonNull
|
|
709
1156
|
private String getDeviceId() {
|
|
710
|
-
String deviceId = preferences.getDeviceIdForApp(
|
|
1157
|
+
String deviceId = preferences.getDeviceIdForApp(getAppId());
|
|
711
1158
|
if (deviceId == null) {
|
|
712
1159
|
deviceId = UUID.randomUUID().toString().toLowerCase();
|
|
713
|
-
preferences.setDeviceIdForApp(
|
|
1160
|
+
preferences.setDeviceIdForApp(getAppId(), deviceId);
|
|
714
1161
|
}
|
|
715
1162
|
return deviceId;
|
|
716
1163
|
}
|
|
717
1164
|
|
|
718
1165
|
/**
|
|
719
|
-
* @return The next bundle ID
|
|
1166
|
+
* @return The next bundle ID or `null` if the default bundle will be used.
|
|
720
1167
|
*/
|
|
721
|
-
@
|
|
1168
|
+
@Nullable
|
|
722
1169
|
private String getNextBundleId() {
|
|
723
1170
|
String nextPath = getNextCapacitorServerPath();
|
|
724
1171
|
if (nextPath.equals(defaultWebAssetDir)) {
|
|
725
|
-
return
|
|
1172
|
+
return null;
|
|
726
1173
|
}
|
|
727
1174
|
return new File(nextPath).getName();
|
|
728
1175
|
}
|
|
@@ -732,31 +1179,54 @@ public class LiveUpdate {
|
|
|
732
1179
|
*/
|
|
733
1180
|
@NonNull
|
|
734
1181
|
private String getNextCapacitorServerPath() {
|
|
735
|
-
|
|
1182
|
+
String path = plugin
|
|
736
1183
|
.getContext()
|
|
737
1184
|
.getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE)
|
|
738
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();
|
|
739
1199
|
}
|
|
740
1200
|
|
|
741
|
-
private
|
|
742
|
-
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());
|
|
743
1207
|
}
|
|
744
1208
|
|
|
745
1209
|
private String getVersionName() throws PackageManager.NameNotFoundException {
|
|
746
1210
|
return getPackageInfo().versionName;
|
|
747
1211
|
}
|
|
748
1212
|
|
|
749
|
-
private boolean
|
|
1213
|
+
private boolean hasBundleById(@NonNull String bundleId) {
|
|
750
1214
|
File bundleDirectory = buildBundleDirectoryFor(bundleId);
|
|
751
1215
|
return bundleDirectory.exists();
|
|
752
1216
|
}
|
|
753
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
|
+
|
|
754
1224
|
@Nullable
|
|
755
1225
|
private Manifest loadCurrentManifest() throws Exception {
|
|
756
1226
|
String currentBundleId = getCurrentBundleId();
|
|
757
|
-
if (currentBundleId
|
|
1227
|
+
if (currentBundleId == null) {
|
|
758
1228
|
AssetManager assets = plugin.getContext().getAssets();
|
|
759
|
-
|
|
1229
|
+
boolean manifestFileExists = Arrays.asList(assets.list(defaultWebAssetDir)).contains(manifestFileName);
|
|
760
1230
|
if (manifestFileExists) {
|
|
761
1231
|
InputStream inputStream = assets.open(defaultWebAssetDir + "/" + manifestFileName);
|
|
762
1232
|
BufferedSource source = Okio.buffer(Okio.source(inputStream));
|
|
@@ -786,25 +1256,53 @@ public class LiveUpdate {
|
|
|
786
1256
|
return loadManifest(source);
|
|
787
1257
|
}
|
|
788
1258
|
|
|
789
|
-
private
|
|
790
|
-
|
|
791
|
-
String nextBundleId = getNextBundleId();
|
|
792
|
-
return bundleId.equals(currentBundleId) || bundleId.equals(nextBundleId);
|
|
1259
|
+
private void notifyDownloadBundleProgressListeners(@NonNull final DownloadBundleProgressEvent event) {
|
|
1260
|
+
plugin.notifyDownloadBundleProgressListeners(event);
|
|
793
1261
|
}
|
|
794
1262
|
|
|
795
|
-
private void
|
|
796
|
-
if
|
|
797
|
-
|
|
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.");
|
|
798
1268
|
return;
|
|
799
1269
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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);
|
|
803
1289
|
}
|
|
804
1290
|
|
|
805
|
-
private void
|
|
806
|
-
|
|
807
|
-
|
|
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
|
+
}
|
|
808
1306
|
}
|
|
809
1307
|
|
|
810
1308
|
@Nullable
|
|
@@ -832,6 +1330,18 @@ public class LiveUpdate {
|
|
|
832
1330
|
return null;
|
|
833
1331
|
}
|
|
834
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
|
+
|
|
835
1345
|
private void setCurrentCapacitorServerPath(@NonNull String path) {
|
|
836
1346
|
if (path.equals(defaultWebAssetDir)) {
|
|
837
1347
|
this.plugin.getBridge().setServerAssetPath(path);
|
|
@@ -839,15 +1349,72 @@ public class LiveUpdate {
|
|
|
839
1349
|
this.plugin.getBridge().setServerBasePath(path);
|
|
840
1350
|
}
|
|
841
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);
|
|
842
1369
|
}
|
|
843
1370
|
|
|
844
|
-
private void
|
|
845
|
-
|
|
1371
|
+
private void notifyNextBundleSetListeners(@Nullable String bundleId) {
|
|
1372
|
+
NextBundleSetEvent event = new NextBundleSetEvent(bundleId);
|
|
1373
|
+
plugin.notifyNextBundleSetListeners(event);
|
|
846
1374
|
}
|
|
847
1375
|
|
|
848
|
-
private void
|
|
849
|
-
|
|
850
|
-
|
|
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);
|
|
851
1418
|
}
|
|
852
1419
|
|
|
853
1420
|
private void setNextCapacitorServerPath(@NonNull String path) {
|
|
@@ -855,11 +1422,25 @@ public class LiveUpdate {
|
|
|
855
1422
|
this.webViewSettingsEditor.commit();
|
|
856
1423
|
}
|
|
857
1424
|
|
|
858
|
-
private
|
|
859
|
-
|
|
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);
|
|
860
1438
|
}
|
|
861
1439
|
|
|
862
1440
|
private void startRollbackTimer() {
|
|
1441
|
+
if (config.getReadyTimeout() <= 0) {
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
863
1444
|
stopRollbackTimer();
|
|
864
1445
|
rollbackHandler.postDelayed(() -> rollback(), config.getReadyTimeout());
|
|
865
1446
|
}
|
|
@@ -868,10 +1449,25 @@ public class LiveUpdate {
|
|
|
868
1449
|
rollbackHandler.removeCallbacksAndMessages(null);
|
|
869
1450
|
}
|
|
870
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
|
+
|
|
871
1461
|
private File unzipFile(@NonNull File zipFile) throws IOException {
|
|
872
1462
|
File destination = buildTemporaryDirectory();
|
|
873
1463
|
String destinationPath = destination.getPath();
|
|
874
|
-
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);
|
|
875
1471
|
return destination;
|
|
876
1472
|
}
|
|
877
1473
|
|
|
@@ -926,12 +1522,6 @@ public class LiveUpdate {
|
|
|
926
1522
|
}
|
|
927
1523
|
}
|
|
928
1524
|
|
|
929
|
-
private boolean wasUpdated() throws PackageManager.NameNotFoundException {
|
|
930
|
-
int lastVersionCode = preferences.getLastVersionCode();
|
|
931
|
-
int currentVersionCode = getPackageInfo().versionCode;
|
|
932
|
-
return lastVersionCode != currentVersionCode;
|
|
933
|
-
}
|
|
934
|
-
|
|
935
1525
|
private PackageInfo getPackageInfo() throws PackageManager.NameNotFoundException {
|
|
936
1526
|
String packageName = this.plugin.getContext().getPackageName();
|
|
937
1527
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|