@capawesome/cordova-live-update 0.1.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/LICENSE +21 -0
- package/README.md +1113 -0
- package/dist/docs.json +1654 -0
- package/dist/esm/definitions.d.ts +788 -0
- package/dist/esm/definitions.js +7 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/exec.d.ts +1 -0
- package/dist/esm/exec.js +8 -0
- package/dist/esm/exec.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +46 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/plugin.js +56 -0
- package/dist/plugin.js.map +1 -0
- package/package.json +93 -0
- package/plugin.xml +268 -0
- package/src/android/capawesome-cordova-live-update.gradle +12 -0
- package/src/android/capawesome-live-update.xml +5 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/LiveUpdate.java +1480 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/LiveUpdateConfig.java +105 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/LiveUpdateHttpClient.java +114 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/LiveUpdatePathHandler.java +96 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/LiveUpdatePlugin.java +550 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/LiveUpdatePreferences.java +151 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/classes/Manifest.java +58 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/classes/ManifestItem.java +37 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/classes/api/GetChannelsResponseItem.java +28 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/classes/api/GetLatestBundleResponse.java +74 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/classes/events/DownloadBundleProgressEvent.java +33 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/classes/events/NextBundleSetEvent.java +26 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/classes/options/DeleteBundleOptions.java +18 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/classes/options/DownloadBundleOptions.java +66 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/classes/options/FetchChannelsOptions.java +39 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/classes/options/FetchLatestBundleOptions.java +25 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/classes/options/SetChannelOptions.java +18 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/classes/options/SetConfigOptions.java +20 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/classes/options/SetCustomIdOptions.java +18 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/classes/options/SetNextBundleOptions.java +21 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/classes/options/SyncOptions.java +25 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/classes/results/ChannelResult.java +29 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/classes/results/FetchChannelsResult.java +29 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/classes/results/FetchLatestBundleResult.java +69 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/classes/results/GetBlockedBundlesResult.java +28 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/classes/results/GetBundlesResult.java +28 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/classes/results/GetChannelResult.java +22 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/classes/results/GetConfigResult.java +40 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/classes/results/GetCurrentBundleResult.java +22 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/classes/results/GetCustomIdResult.java +22 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/classes/results/GetDeviceIdResult.java +22 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/classes/results/GetDownloadedBundlesResult.java +28 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/classes/results/GetNextBundleResult.java +22 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/classes/results/GetVersionCodeResult.java +21 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/classes/results/GetVersionNameResult.java +22 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/classes/results/IsSyncingResult.java +27 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/classes/results/ReadyResult.java +32 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/classes/results/SyncResult.java +22 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/enums/ArtifactType.java +6 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/interfaces/Callback.java +5 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/interfaces/DownloadProgressCallback.java +5 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/interfaces/EmptyCallback.java +5 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/interfaces/NonEmptyCallback.java +7 -0
- package/src/android/io/capawesome/cordova/plugins/liveupdate/interfaces/Result.java +8 -0
- package/src/ios/LiveUpdate.swift +895 -0
- package/src/ios/LiveUpdateArtifactType.swift +4 -0
- package/src/ios/LiveUpdateChannelResult.swift +18 -0
- package/src/ios/LiveUpdateConfig.swift +11 -0
- package/src/ios/LiveUpdateDeleteBundleOptions.swift +13 -0
- package/src/ios/LiveUpdateDownloadBundleOptions.swift +41 -0
- package/src/ios/LiveUpdateDownloadBundleProgressEvent.swift +24 -0
- package/src/ios/LiveUpdateError.swift +62 -0
- package/src/ios/LiveUpdateFetchChannelsOptions.swift +25 -0
- package/src/ios/LiveUpdateFetchChannelsResult.swift +15 -0
- package/src/ios/LiveUpdateFetchLatestBundleOptions.swift +17 -0
- package/src/ios/LiveUpdateFetchLatestBundleResult.swift +42 -0
- package/src/ios/LiveUpdateGetBlockedBundlesResult.swift +15 -0
- package/src/ios/LiveUpdateGetBundlesResult.swift +15 -0
- package/src/ios/LiveUpdateGetChannelResult.swift +15 -0
- package/src/ios/LiveUpdateGetChannelsResponseItem.swift +4 -0
- package/src/ios/LiveUpdateGetConfigResult.swift +18 -0
- package/src/ios/LiveUpdateGetCurrentBundleResult.swift +15 -0
- package/src/ios/LiveUpdateGetCustomIdResult.swift +15 -0
- package/src/ios/LiveUpdateGetDeviceIdResult.swift +15 -0
- package/src/ios/LiveUpdateGetDownloadedBundlesResult.swift +15 -0
- package/src/ios/LiveUpdateGetLatestBundleResponse.swift +8 -0
- package/src/ios/LiveUpdateGetNextBundleResult.swift +15 -0
- package/src/ios/LiveUpdateGetVersionCodeResult.swift +15 -0
- package/src/ios/LiveUpdateGetVersionNameResult.swift +15 -0
- package/src/ios/LiveUpdateHttpClient.swift +58 -0
- package/src/ios/LiveUpdateIsSyncingResult.swift +15 -0
- package/src/ios/LiveUpdateManifest.swift +19 -0
- package/src/ios/LiveUpdateManifestItem.swift +5 -0
- package/src/ios/LiveUpdateNextBundleSetEvent.swift +15 -0
- package/src/ios/LiveUpdatePlugin.swift +521 -0
- package/src/ios/LiveUpdatePreferences.swift +116 -0
- package/src/ios/LiveUpdateReadyResult.swift +21 -0
- package/src/ios/LiveUpdateResult.swift +5 -0
- package/src/ios/LiveUpdateSchemeHandler.swift +286 -0
- package/src/ios/LiveUpdateSetChannelOptions.swift +13 -0
- package/src/ios/LiveUpdateSetConfigOptions.swift +13 -0
- package/src/ios/LiveUpdateSetCustomIdOptions.swift +13 -0
- package/src/ios/LiveUpdateSetNextBundleOptions.swift +13 -0
- package/src/ios/LiveUpdateSyncOptions.swift +17 -0
- package/src/ios/LiveUpdateSyncResult.swift +15 -0
|
@@ -0,0 +1,1480 @@
|
|
|
1
|
+
package io.capawesome.cordova.plugins.liveupdate;
|
|
2
|
+
|
|
3
|
+
import android.content.pm.PackageInfo;
|
|
4
|
+
import android.content.pm.PackageManager;
|
|
5
|
+
import android.content.res.AssetManager;
|
|
6
|
+
import android.os.Build;
|
|
7
|
+
import android.os.Handler;
|
|
8
|
+
import android.os.Looper;
|
|
9
|
+
import android.util.Base64;
|
|
10
|
+
import android.util.Log;
|
|
11
|
+
import androidx.annotation.NonNull;
|
|
12
|
+
import androidx.annotation.Nullable;
|
|
13
|
+
import io.capawesome.cordova.plugins.liveupdate.classes.Manifest;
|
|
14
|
+
import io.capawesome.cordova.plugins.liveupdate.classes.ManifestItem;
|
|
15
|
+
import io.capawesome.cordova.plugins.liveupdate.classes.api.GetChannelsResponseItem;
|
|
16
|
+
import io.capawesome.cordova.plugins.liveupdate.classes.api.GetLatestBundleResponse;
|
|
17
|
+
import io.capawesome.cordova.plugins.liveupdate.classes.events.DownloadBundleProgressEvent;
|
|
18
|
+
import io.capawesome.cordova.plugins.liveupdate.classes.events.NextBundleSetEvent;
|
|
19
|
+
import io.capawesome.cordova.plugins.liveupdate.classes.options.DeleteBundleOptions;
|
|
20
|
+
import io.capawesome.cordova.plugins.liveupdate.classes.options.DownloadBundleOptions;
|
|
21
|
+
import io.capawesome.cordova.plugins.liveupdate.classes.options.FetchChannelsOptions;
|
|
22
|
+
import io.capawesome.cordova.plugins.liveupdate.classes.options.FetchLatestBundleOptions;
|
|
23
|
+
import io.capawesome.cordova.plugins.liveupdate.classes.options.SetChannelOptions;
|
|
24
|
+
import io.capawesome.cordova.plugins.liveupdate.classes.options.SetConfigOptions;
|
|
25
|
+
import io.capawesome.cordova.plugins.liveupdate.classes.options.SetCustomIdOptions;
|
|
26
|
+
import io.capawesome.cordova.plugins.liveupdate.classes.options.SetNextBundleOptions;
|
|
27
|
+
import io.capawesome.cordova.plugins.liveupdate.classes.options.SyncOptions;
|
|
28
|
+
import io.capawesome.cordova.plugins.liveupdate.classes.results.ChannelResult;
|
|
29
|
+
import io.capawesome.cordova.plugins.liveupdate.classes.results.FetchChannelsResult;
|
|
30
|
+
import io.capawesome.cordova.plugins.liveupdate.classes.results.FetchLatestBundleResult;
|
|
31
|
+
import io.capawesome.cordova.plugins.liveupdate.classes.results.GetBlockedBundlesResult;
|
|
32
|
+
import io.capawesome.cordova.plugins.liveupdate.classes.results.GetBundlesResult;
|
|
33
|
+
import io.capawesome.cordova.plugins.liveupdate.classes.results.GetChannelResult;
|
|
34
|
+
import io.capawesome.cordova.plugins.liveupdate.classes.results.GetConfigResult;
|
|
35
|
+
import io.capawesome.cordova.plugins.liveupdate.classes.results.GetCurrentBundleResult;
|
|
36
|
+
import io.capawesome.cordova.plugins.liveupdate.classes.results.GetCustomIdResult;
|
|
37
|
+
import io.capawesome.cordova.plugins.liveupdate.classes.results.GetDeviceIdResult;
|
|
38
|
+
import io.capawesome.cordova.plugins.liveupdate.classes.results.GetDownloadedBundlesResult;
|
|
39
|
+
import io.capawesome.cordova.plugins.liveupdate.classes.results.GetNextBundleResult;
|
|
40
|
+
import io.capawesome.cordova.plugins.liveupdate.classes.results.GetVersionCodeResult;
|
|
41
|
+
import io.capawesome.cordova.plugins.liveupdate.classes.results.GetVersionNameResult;
|
|
42
|
+
import io.capawesome.cordova.plugins.liveupdate.classes.results.IsSyncingResult;
|
|
43
|
+
import io.capawesome.cordova.plugins.liveupdate.classes.results.ReadyResult;
|
|
44
|
+
import io.capawesome.cordova.plugins.liveupdate.classes.results.SyncResult;
|
|
45
|
+
import io.capawesome.cordova.plugins.liveupdate.enums.ArtifactType;
|
|
46
|
+
import io.capawesome.cordova.plugins.liveupdate.interfaces.DownloadProgressCallback;
|
|
47
|
+
import io.capawesome.cordova.plugins.liveupdate.interfaces.EmptyCallback;
|
|
48
|
+
import io.capawesome.cordova.plugins.liveupdate.interfaces.NonEmptyCallback;
|
|
49
|
+
import io.capawesome.cordova.plugins.liveupdate.interfaces.Result;
|
|
50
|
+
import java.io.File;
|
|
51
|
+
import java.io.FileInputStream;
|
|
52
|
+
import java.io.FileOutputStream;
|
|
53
|
+
import java.io.IOException;
|
|
54
|
+
import java.io.InputStream;
|
|
55
|
+
import java.security.KeyFactory;
|
|
56
|
+
import java.security.MessageDigest;
|
|
57
|
+
import java.security.PublicKey;
|
|
58
|
+
import java.security.Signature;
|
|
59
|
+
import java.security.spec.X509EncodedKeySpec;
|
|
60
|
+
import java.util.ArrayList;
|
|
61
|
+
import java.util.Arrays;
|
|
62
|
+
import java.util.List;
|
|
63
|
+
import java.util.UUID;
|
|
64
|
+
import java.util.concurrent.CountDownLatch;
|
|
65
|
+
import java.util.concurrent.atomic.AtomicBoolean;
|
|
66
|
+
import java.util.concurrent.atomic.AtomicLong;
|
|
67
|
+
import java.util.concurrent.atomic.AtomicReference;
|
|
68
|
+
import net.lingala.zip4j.ZipFile;
|
|
69
|
+
import net.lingala.zip4j.model.FileHeader;
|
|
70
|
+
import okhttp3.Call;
|
|
71
|
+
import okhttp3.HttpUrl;
|
|
72
|
+
import okhttp3.Response;
|
|
73
|
+
import okhttp3.ResponseBody;
|
|
74
|
+
import okio.Buffer;
|
|
75
|
+
import okio.BufferedSource;
|
|
76
|
+
import okio.Okio;
|
|
77
|
+
import org.json.JSONArray;
|
|
78
|
+
import org.json.JSONObject;
|
|
79
|
+
|
|
80
|
+
public class LiveUpdate {
|
|
81
|
+
|
|
82
|
+
private final long autoUpdateIntervalMs = 15 * 60 * 1000; // 15 minutes
|
|
83
|
+
|
|
84
|
+
@NonNull
|
|
85
|
+
private final LiveUpdateConfig config;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Directory inside the APK where the bundled web assets live. Cordova builds
|
|
89
|
+
* place {@code www/} at the root of the APK assets folder.
|
|
90
|
+
*/
|
|
91
|
+
private final String defaultWebAssetDir = "www";
|
|
92
|
+
|
|
93
|
+
@NonNull
|
|
94
|
+
private final LiveUpdateHttpClient httpClient;
|
|
95
|
+
|
|
96
|
+
@NonNull
|
|
97
|
+
private final LiveUpdatePlugin plugin;
|
|
98
|
+
|
|
99
|
+
@NonNull
|
|
100
|
+
private final LiveUpdatePathHandler pathHandler;
|
|
101
|
+
|
|
102
|
+
@NonNull
|
|
103
|
+
private final LiveUpdatePreferences preferences;
|
|
104
|
+
|
|
105
|
+
private final String bundlesDirectory = "capawesome_live_update_bundles"; // DO NOT CHANGE!
|
|
106
|
+
private final Handler rollbackHandler = new Handler(Looper.getMainLooper());
|
|
107
|
+
private final String manifestFileName = "capawesome-live-update-manifest.json"; // DO NOT CHANGE!
|
|
108
|
+
|
|
109
|
+
private long lastAutoUpdateCheckTimestamp = 0;
|
|
110
|
+
private boolean rollbackPerformed = false;
|
|
111
|
+
private boolean syncInProgress = false;
|
|
112
|
+
|
|
113
|
+
public LiveUpdate(@NonNull LiveUpdateConfig config, @NonNull LiveUpdatePlugin plugin, @NonNull LiveUpdatePathHandler pathHandler)
|
|
114
|
+
throws PackageManager.NameNotFoundException {
|
|
115
|
+
this.config = config;
|
|
116
|
+
this.httpClient = new LiveUpdateHttpClient(config);
|
|
117
|
+
this.plugin = plugin;
|
|
118
|
+
this.pathHandler = pathHandler;
|
|
119
|
+
this.preferences = new LiveUpdatePreferences(plugin.getContext());
|
|
120
|
+
|
|
121
|
+
// Check version and reset config (and the next bundle) if the native app
|
|
122
|
+
// version changed. This must run before promoting the next bundle below so
|
|
123
|
+
// that a stale, now-incompatible bundle is not activated.
|
|
124
|
+
checkAndResetConfigIfVersionChanged();
|
|
125
|
+
|
|
126
|
+
// Promote the persisted next bundle to active for this session (matches
|
|
127
|
+
// Capacitor's launch-time behavior, where the persisted next path becomes
|
|
128
|
+
// the current server base path on each cold start).
|
|
129
|
+
String nextBundleId = preferences.getNextBundleId();
|
|
130
|
+
if (nextBundleId != null && hasBundleById(nextBundleId)) {
|
|
131
|
+
pathHandler.setActiveBundleDir(buildBundleDirectoryFor(nextBundleId));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Set the device ID on the HTTP client (after any potential config reset)
|
|
135
|
+
this.httpClient.setDeviceId(getDeviceId());
|
|
136
|
+
|
|
137
|
+
// Start the rollback timer to rollback to the default bundle
|
|
138
|
+
// if the app is not ready after a certain time
|
|
139
|
+
startRollbackTimer();
|
|
140
|
+
|
|
141
|
+
// Trigger an initial auto-update if configured. Unlike Capacitor we do not
|
|
142
|
+
// need to wait for the WebView's local server to come up; we use OkHttp
|
|
143
|
+
// directly and the path handler is already in place.
|
|
144
|
+
if ("background".equals(config.getAutoUpdateStrategy())) {
|
|
145
|
+
performAutoUpdate();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public void clearBlockedBundles() {
|
|
150
|
+
preferences.setBlockedBundleIds(null);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
public void deleteBundle(@NonNull DeleteBundleOptions options, @NonNull EmptyCallback callback) {
|
|
154
|
+
String bundleId = options.getBundleId();
|
|
155
|
+
|
|
156
|
+
if (!hasBundleById(bundleId)) {
|
|
157
|
+
Exception exception = new Exception(LiveUpdatePlugin.ERROR_BUNDLE_NOT_FOUND);
|
|
158
|
+
callback.error(exception);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
deleteBundleById(bundleId);
|
|
162
|
+
|
|
163
|
+
callback.success();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
public void downloadBundle(@NonNull DownloadBundleOptions options, @NonNull EmptyCallback callback) {
|
|
167
|
+
ArtifactType artifactType = options.getArtifactType();
|
|
168
|
+
String bundleId = options.getBundleId();
|
|
169
|
+
String checksum = options.getChecksum();
|
|
170
|
+
String signature = options.getSignature();
|
|
171
|
+
String url = options.getUrl();
|
|
172
|
+
|
|
173
|
+
// Check if the bundle already exists
|
|
174
|
+
if (hasBundleById(bundleId)) {
|
|
175
|
+
Exception exception = new Exception(LiveUpdatePlugin.ERROR_BUNDLE_EXISTS);
|
|
176
|
+
callback.error(exception);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Download the bundle
|
|
181
|
+
if (artifactType == ArtifactType.MANIFEST) {
|
|
182
|
+
downloadBundleOfTypeManifest(bundleId, url, callback);
|
|
183
|
+
} else {
|
|
184
|
+
downloadBundleOfTypeZip(bundleId, checksum, signature, url, callback);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
public void fetchChannels(@NonNull FetchChannelsOptions options, @NonNull NonEmptyCallback<FetchChannelsResult> callback) {
|
|
189
|
+
try {
|
|
190
|
+
HttpUrl.Builder urlBuilder = new HttpUrl.Builder()
|
|
191
|
+
.scheme("https")
|
|
192
|
+
.host(config.getServerDomain())
|
|
193
|
+
.addPathSegment("v1")
|
|
194
|
+
.addPathSegment("apps")
|
|
195
|
+
.addPathSegment(getAppId())
|
|
196
|
+
.addPathSegment("channels");
|
|
197
|
+
if (options.getLimit() != null) {
|
|
198
|
+
urlBuilder.addQueryParameter("limit", String.valueOf(options.getLimit()));
|
|
199
|
+
}
|
|
200
|
+
if (options.getOffset() != null) {
|
|
201
|
+
urlBuilder.addQueryParameter("offset", String.valueOf(options.getOffset()));
|
|
202
|
+
}
|
|
203
|
+
if (options.getQuery() != null) {
|
|
204
|
+
urlBuilder.addQueryParameter("query", options.getQuery());
|
|
205
|
+
}
|
|
206
|
+
String url = urlBuilder.build().toString();
|
|
207
|
+
|
|
208
|
+
httpClient.enqueue(
|
|
209
|
+
url,
|
|
210
|
+
new NonEmptyCallback<Response>() {
|
|
211
|
+
@Override
|
|
212
|
+
public void success(@NonNull Response response) {
|
|
213
|
+
try {
|
|
214
|
+
String responseBodyString = response.body().string();
|
|
215
|
+
if (response.isSuccessful()) {
|
|
216
|
+
JSONArray responseJsonArray = new JSONArray(responseBodyString);
|
|
217
|
+
ChannelResult[] channels = new ChannelResult[responseJsonArray.length()];
|
|
218
|
+
for (int i = 0; i < responseJsonArray.length(); i++) {
|
|
219
|
+
GetChannelsResponseItem item = new GetChannelsResponseItem(responseJsonArray.getJSONObject(i));
|
|
220
|
+
channels[i] = new ChannelResult(item.getId(), item.getName());
|
|
221
|
+
}
|
|
222
|
+
FetchChannelsResult result = new FetchChannelsResult(channels);
|
|
223
|
+
callback.success(result);
|
|
224
|
+
} else if (response.code() == 401) {
|
|
225
|
+
callback.error(new Exception("Unauthorized. Channel Discovery may not be enabled for this app."));
|
|
226
|
+
} else {
|
|
227
|
+
callback.error(new Exception(responseBodyString));
|
|
228
|
+
}
|
|
229
|
+
} catch (Exception e) {
|
|
230
|
+
callback.error(e);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
@Override
|
|
235
|
+
public void error(@NonNull Exception exception) {
|
|
236
|
+
callback.error(exception);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
);
|
|
240
|
+
} catch (Exception e) {
|
|
241
|
+
callback.error(e);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
public void fetchLatestBundle(@NonNull FetchLatestBundleOptions options, @NonNull NonEmptyCallback<FetchLatestBundleResult> callback) {
|
|
246
|
+
fetchLatestBundleInternal(
|
|
247
|
+
options,
|
|
248
|
+
new NonEmptyCallback<GetLatestBundleResponse>() {
|
|
249
|
+
@Override
|
|
250
|
+
public void success(@Nullable GetLatestBundleResponse response) {
|
|
251
|
+
ArtifactType artifactType = response == null ? null : response.getArtifactType();
|
|
252
|
+
String bundleId = response == null ? null : response.getBundleId();
|
|
253
|
+
String checksum = response == null ? null : response.getChecksum();
|
|
254
|
+
JSONObject customProperties = response == null ? null : response.getCustomProperties();
|
|
255
|
+
String downloadUrl = response == null ? null : response.getUrl();
|
|
256
|
+
String signature = response == null ? null : response.getSignature();
|
|
257
|
+
FetchLatestBundleResult result = new FetchLatestBundleResult(
|
|
258
|
+
artifactType,
|
|
259
|
+
bundleId,
|
|
260
|
+
checksum,
|
|
261
|
+
customProperties,
|
|
262
|
+
downloadUrl,
|
|
263
|
+
signature
|
|
264
|
+
);
|
|
265
|
+
callback.success(result);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
@Override
|
|
269
|
+
public void error(@NonNull Exception exception) {
|
|
270
|
+
callback.error(exception);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
public void getBlockedBundles(@NonNull NonEmptyCallback<GetBlockedBundlesResult> callback) {
|
|
277
|
+
String blockedIds = preferences.getBlockedBundleIds();
|
|
278
|
+
String[] bundleIds;
|
|
279
|
+
if (blockedIds == null || blockedIds.isEmpty()) {
|
|
280
|
+
bundleIds = new String[0];
|
|
281
|
+
} else {
|
|
282
|
+
bundleIds = blockedIds.split(",");
|
|
283
|
+
}
|
|
284
|
+
GetBlockedBundlesResult result = new GetBlockedBundlesResult(bundleIds);
|
|
285
|
+
callback.success(result);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
public void getBundles(@NonNull NonEmptyCallback callback) {
|
|
289
|
+
String[] bundleIds = getDownloadedBundleIds();
|
|
290
|
+
GetBundlesResult result = new GetBundlesResult(bundleIds);
|
|
291
|
+
callback.success(result);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
public void getDownloadedBundles(@NonNull NonEmptyCallback<GetDownloadedBundlesResult> callback) {
|
|
295
|
+
String[] bundleIds = getDownloadedBundleIds();
|
|
296
|
+
GetDownloadedBundlesResult result = new GetDownloadedBundlesResult(bundleIds);
|
|
297
|
+
callback.success(result);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
public void getChannel(@NonNull NonEmptyCallback callback) {
|
|
301
|
+
String channel = getChannel();
|
|
302
|
+
GetChannelResult result = new GetChannelResult(channel);
|
|
303
|
+
callback.success(result);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
public void getConfig(@NonNull NonEmptyCallback<GetConfigResult> callback) {
|
|
307
|
+
String appId = getAppId();
|
|
308
|
+
String autoUpdateStrategy = config.getAutoUpdateStrategy();
|
|
309
|
+
GetConfigResult result = new GetConfigResult(appId, autoUpdateStrategy);
|
|
310
|
+
callback.success(result);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
public void getCurrentBundle(@NonNull NonEmptyCallback<GetCurrentBundleResult> callback) {
|
|
314
|
+
String bundleId = getCurrentBundleId();
|
|
315
|
+
GetCurrentBundleResult result = new GetCurrentBundleResult(bundleId);
|
|
316
|
+
callback.success(result);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
public void getCustomId(@NonNull NonEmptyCallback callback) {
|
|
320
|
+
String customId = preferences.getCustomId();
|
|
321
|
+
GetCustomIdResult result = new GetCustomIdResult(customId);
|
|
322
|
+
callback.success(result);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
public void getDeviceId(@NonNull NonEmptyCallback callback) {
|
|
326
|
+
String deviceId = getDeviceId();
|
|
327
|
+
GetDeviceIdResult result = new GetDeviceIdResult(deviceId);
|
|
328
|
+
callback.success(result);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
public void getNextBundle(@NonNull NonEmptyCallback<GetNextBundleResult> callback) {
|
|
332
|
+
String bundleId = getNextBundleId();
|
|
333
|
+
GetNextBundleResult result = new GetNextBundleResult(bundleId);
|
|
334
|
+
callback.success(result);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
public void getVersionCode(@NonNull NonEmptyCallback callback) throws PackageManager.NameNotFoundException {
|
|
338
|
+
String versionCode = getVersionCodeAsString();
|
|
339
|
+
GetVersionCodeResult result = new GetVersionCodeResult(versionCode);
|
|
340
|
+
callback.success(result);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
public void getVersionName(@NonNull NonEmptyCallback callback) throws PackageManager.NameNotFoundException {
|
|
344
|
+
String versionName = getVersionName();
|
|
345
|
+
GetVersionNameResult result = new GetVersionNameResult(versionName);
|
|
346
|
+
callback.success(result);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
public void isSyncing(@NonNull NonEmptyCallback<IsSyncingResult> callback) {
|
|
350
|
+
IsSyncingResult result = new IsSyncingResult(syncInProgress);
|
|
351
|
+
callback.success(result);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
public void handleOnResume() {
|
|
355
|
+
if ("background".equals(config.getAutoUpdateStrategy())) {
|
|
356
|
+
performAutoUpdate();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
public void ready(@NonNull NonEmptyCallback callback) {
|
|
361
|
+
Log.d(LiveUpdatePlugin.TAG, "App is ready.");
|
|
362
|
+
if (config.getReadyTimeout() <= 0) {
|
|
363
|
+
Log.w(LiveUpdatePlugin.TAG, "Ready timeout is set to 0. Automatic rollback is disabled.");
|
|
364
|
+
}
|
|
365
|
+
// Stop the rollback timer
|
|
366
|
+
stopRollbackTimer();
|
|
367
|
+
// Delete unused bundles
|
|
368
|
+
if (config.getAutoDeleteBundles()) {
|
|
369
|
+
deleteUnusedBundles();
|
|
370
|
+
}
|
|
371
|
+
// Get the current and previous bundle IDs
|
|
372
|
+
String currentBundleId = getCurrentBundleId();
|
|
373
|
+
String previousBundleId = getPreviousBundleId();
|
|
374
|
+
// Block the rolled back bundle if enabled
|
|
375
|
+
if (config.getAutoBlockRolledBackBundles() && rollbackPerformed && previousBundleId != null) {
|
|
376
|
+
addBlockedBundleId(previousBundleId);
|
|
377
|
+
}
|
|
378
|
+
// Return the result
|
|
379
|
+
ReadyResult result = new ReadyResult(currentBundleId, previousBundleId, rollbackPerformed);
|
|
380
|
+
callback.success(result);
|
|
381
|
+
// Set the new previous bundle ID
|
|
382
|
+
setPreviousBundleId(currentBundleId);
|
|
383
|
+
// Reset the rollback flag
|
|
384
|
+
rollbackPerformed = false;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
public void reload() {
|
|
388
|
+
String nextBundleId = getNextBundleId();
|
|
389
|
+
setCurrentBundleById(nextBundleId);
|
|
390
|
+
startRollbackTimer();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
public void reset() {
|
|
394
|
+
setNextBundleById(null);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
public void resetConfig() {
|
|
398
|
+
preferences.setAppId(null);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
public void setChannel(@NonNull SetChannelOptions options, @NonNull EmptyCallback callback) {
|
|
402
|
+
String channel = options.getChannel();
|
|
403
|
+
|
|
404
|
+
preferences.setChannel(channel);
|
|
405
|
+
callback.success();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
public void setConfig(@NonNull SetConfigOptions options) {
|
|
409
|
+
String appId = options.getAppId();
|
|
410
|
+
preferences.setAppId(appId);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
public void setCustomId(@NonNull SetCustomIdOptions options, @NonNull EmptyCallback callback) {
|
|
414
|
+
String customId = options.getCustomId();
|
|
415
|
+
|
|
416
|
+
preferences.setCustomId(customId);
|
|
417
|
+
callback.success();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
public void setNextBundle(@NonNull SetNextBundleOptions options, @NonNull EmptyCallback callback) {
|
|
421
|
+
String bundleId = options.getBundleId();
|
|
422
|
+
|
|
423
|
+
if (bundleId == null) {
|
|
424
|
+
reset();
|
|
425
|
+
} else {
|
|
426
|
+
if (hasBundleById(bundleId)) {
|
|
427
|
+
setNextBundleById(bundleId);
|
|
428
|
+
} else {
|
|
429
|
+
Exception exception = new Exception(LiveUpdatePlugin.ERROR_BUNDLE_NOT_FOUND);
|
|
430
|
+
callback.error(exception);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
callback.success();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
public void sync(@NonNull SyncOptions options, @NonNull NonEmptyCallback<Result> callback) {
|
|
438
|
+
if (syncInProgress) {
|
|
439
|
+
Exception exception = new Exception(LiveUpdatePlugin.ERROR_SYNC_IN_PROGRESS);
|
|
440
|
+
callback.error(exception);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
syncInProgress = true;
|
|
444
|
+
|
|
445
|
+
String channel = options.getChannel();
|
|
446
|
+
// Fetch the latest bundle
|
|
447
|
+
FetchLatestBundleOptions fetchLatestBundleOptions = new FetchLatestBundleOptions(channel);
|
|
448
|
+
fetchLatestBundleInternal(
|
|
449
|
+
fetchLatestBundleOptions,
|
|
450
|
+
new NonEmptyCallback<GetLatestBundleResponse>() {
|
|
451
|
+
@Override
|
|
452
|
+
public void success(@Nullable GetLatestBundleResponse response) {
|
|
453
|
+
try {
|
|
454
|
+
if (response == null) {
|
|
455
|
+
Log.d(LiveUpdatePlugin.TAG, "No update available.");
|
|
456
|
+
syncInProgress = false;
|
|
457
|
+
SyncResult syncResult = new SyncResult(null);
|
|
458
|
+
callback.success(syncResult);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
ArtifactType artifactType = response.getArtifactType();
|
|
462
|
+
String latestBundleId = response.getBundleId();
|
|
463
|
+
String checksum = response.getChecksum();
|
|
464
|
+
String signature = response.getSignature();
|
|
465
|
+
String url = response.getUrl();
|
|
466
|
+
// Check if the bundle is blocked
|
|
467
|
+
if (isBlockedBundleId(latestBundleId)) {
|
|
468
|
+
Log.w(LiveUpdatePlugin.TAG, "Bundle is blocked and will not be downloaded.");
|
|
469
|
+
syncInProgress = false;
|
|
470
|
+
SyncResult syncResult = new SyncResult(null);
|
|
471
|
+
callback.success(syncResult);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
// Check if the bundle already exists
|
|
475
|
+
if (hasBundleById(latestBundleId)) {
|
|
476
|
+
String nextBundleId = null;
|
|
477
|
+
String currentBundleId = getCurrentBundleId();
|
|
478
|
+
if (!latestBundleId.equals(currentBundleId)) {
|
|
479
|
+
// Set the next bundle
|
|
480
|
+
setNextBundleById(latestBundleId);
|
|
481
|
+
nextBundleId = latestBundleId;
|
|
482
|
+
}
|
|
483
|
+
syncInProgress = false;
|
|
484
|
+
SyncResult syncResult = new SyncResult(nextBundleId);
|
|
485
|
+
callback.success(syncResult);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Download the bundle
|
|
490
|
+
EmptyCallback downloadCallback = new EmptyCallback() {
|
|
491
|
+
@Override
|
|
492
|
+
public void success() {
|
|
493
|
+
try {
|
|
494
|
+
// Set the next bundle
|
|
495
|
+
setNextBundleById(latestBundleId);
|
|
496
|
+
syncInProgress = false;
|
|
497
|
+
SyncResult syncResult = new SyncResult(latestBundleId);
|
|
498
|
+
callback.success(syncResult);
|
|
499
|
+
} catch (Exception e) {
|
|
500
|
+
syncInProgress = false;
|
|
501
|
+
callback.error(e);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
@Override
|
|
506
|
+
public void error(@NonNull Exception exception) {
|
|
507
|
+
syncInProgress = false;
|
|
508
|
+
callback.error(exception);
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
if (artifactType == ArtifactType.MANIFEST) {
|
|
513
|
+
downloadBundleOfTypeManifest(latestBundleId, url, downloadCallback);
|
|
514
|
+
} else {
|
|
515
|
+
downloadBundleOfTypeZip(latestBundleId, checksum, signature, url, downloadCallback);
|
|
516
|
+
}
|
|
517
|
+
} catch (Exception e) {
|
|
518
|
+
syncInProgress = false;
|
|
519
|
+
callback.error(e);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
@Override
|
|
524
|
+
public void error(@NonNull Exception exception) {
|
|
525
|
+
syncInProgress = false;
|
|
526
|
+
callback.error(exception);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
private void addBundle(@NonNull String bundleId, @NonNull File sourceDirectory) throws Exception {
|
|
533
|
+
// Search folder with index.html file
|
|
534
|
+
File indexHtmlFile = searchIndexHtmlFile(sourceDirectory);
|
|
535
|
+
if (indexHtmlFile == null) {
|
|
536
|
+
throw new Exception(LiveUpdatePlugin.ERROR_BUNDLE_INDEX_HTML_MISSING);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Create the bundles directory if it does not exist
|
|
540
|
+
createBundlesDirectory();
|
|
541
|
+
|
|
542
|
+
// Move the bundle directory to the bundles directory
|
|
543
|
+
File bundleDirectory = buildBundleDirectoryFor(bundleId);
|
|
544
|
+
indexHtmlFile.getParentFile().renameTo(bundleDirectory);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
private void addBundleOfTypeManifest(@NonNull String bundleId, @NonNull File directory) throws Exception {
|
|
548
|
+
addBundle(bundleId, directory);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
private void addBundleOfTypeZip(@NonNull String bundleId, @NonNull File zipFile) throws Exception {
|
|
552
|
+
// Unzip the file to the bundle directory
|
|
553
|
+
File unzippedDirectory = unzipFile(zipFile);
|
|
554
|
+
// Add the bundle
|
|
555
|
+
addBundle(bundleId, unzippedDirectory);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
private File buildBundlesDirectory() {
|
|
559
|
+
return new File(plugin.getContext().getFilesDir(), bundlesDirectory);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
private File buildBundleDirectoryFor(@NonNull String bundleId) {
|
|
563
|
+
return new File(plugin.getContext().getFilesDir(), bundlesDirectory + "/" + bundleId);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
private File buildTemporaryDirectory() {
|
|
567
|
+
String fileName = UUID.randomUUID().toString();
|
|
568
|
+
return new File(plugin.getContext().getCacheDir(), fileName);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
private File buildTemporaryZipFile() {
|
|
572
|
+
String fileName = UUID.randomUUID().toString() + ".zip";
|
|
573
|
+
return new File(plugin.getContext().getCacheDir(), fileName);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
private void copyCurrentBundleFile(@NonNull ManifestItem fileToCopy, @NonNull File destinationDirectory) throws IOException {
|
|
577
|
+
String href = fileToCopy.getHref();
|
|
578
|
+
String currentBundleId = getCurrentBundleId();
|
|
579
|
+
if (currentBundleId == null) {
|
|
580
|
+
// Create the source input stream
|
|
581
|
+
AssetManager assets = plugin.getContext().getAssets();
|
|
582
|
+
InputStream inputStream = assets.open(defaultWebAssetDir + "/" + href);
|
|
583
|
+
// Create the destination file
|
|
584
|
+
File destination = new File(destinationDirectory, href);
|
|
585
|
+
// Create all destination directories if they do not exist
|
|
586
|
+
destination.getParentFile().mkdirs();
|
|
587
|
+
// Copy the file
|
|
588
|
+
copyFile(inputStream, destination);
|
|
589
|
+
} else {
|
|
590
|
+
File currentBundleDirectory = buildBundleDirectoryFor(currentBundleId);
|
|
591
|
+
// Create the source and destination files
|
|
592
|
+
File source = new File(currentBundleDirectory, href);
|
|
593
|
+
File destination = new File(destinationDirectory, href);
|
|
594
|
+
// Create all destination directories if they do not exist
|
|
595
|
+
destination.getParentFile().mkdirs();
|
|
596
|
+
// Copy the file
|
|
597
|
+
copyFile(source, destination);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
private List<ManifestItem> copyCurrentBundleFilesAndReturnFailures(
|
|
602
|
+
@NonNull List<ManifestItem> filesToCopy,
|
|
603
|
+
@NonNull File destinationDirectory
|
|
604
|
+
) {
|
|
605
|
+
List<ManifestItem> missingItems = new ArrayList<>();
|
|
606
|
+
for (ManifestItem fileToCopy : filesToCopy) {
|
|
607
|
+
boolean success = tryCopyCurrentBundleFile(fileToCopy, destinationDirectory);
|
|
608
|
+
if (!success) {
|
|
609
|
+
Log.w(LiveUpdatePlugin.TAG, "Failed to copy file: " + fileToCopy.getHref());
|
|
610
|
+
// If the file could not be copied, add it to the list of missing items
|
|
611
|
+
missingItems.add(fileToCopy);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return missingItems;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
private void copyFile(File input, File output) throws IOException {
|
|
618
|
+
FileInputStream in = new FileInputStream(input);
|
|
619
|
+
FileOutputStream out = new FileOutputStream(output);
|
|
620
|
+
byte[] buffer = new byte[1024];
|
|
621
|
+
int length;
|
|
622
|
+
while ((length = in.read(buffer)) > 0) {
|
|
623
|
+
out.write(buffer, 0, length);
|
|
624
|
+
}
|
|
625
|
+
out.close();
|
|
626
|
+
in.close();
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
private void copyFile(InputStream input, File output) throws IOException {
|
|
630
|
+
FileOutputStream out = new FileOutputStream(output);
|
|
631
|
+
byte[] buffer = new byte[1024];
|
|
632
|
+
int length;
|
|
633
|
+
while ((length = input.read(buffer)) > 0) {
|
|
634
|
+
out.write(buffer, 0, length);
|
|
635
|
+
}
|
|
636
|
+
out.close();
|
|
637
|
+
input.close();
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
private void createBundlesDirectory() {
|
|
641
|
+
File file = buildBundlesDirectory();
|
|
642
|
+
if (!file.exists()) {
|
|
643
|
+
file.mkdir();
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
private PublicKey createPublicKeyFromString(@NonNull String value) throws Exception {
|
|
648
|
+
try {
|
|
649
|
+
value = value.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "").replace("\n", "");
|
|
650
|
+
byte[] byteKey = Base64.decode(value, Base64.DEFAULT);
|
|
651
|
+
X509EncodedKeySpec X509publicKey = new X509EncodedKeySpec(byteKey);
|
|
652
|
+
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
|
653
|
+
return keyFactory.generatePublic(X509publicKey);
|
|
654
|
+
} catch (Exception exception) {
|
|
655
|
+
Log.e(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
|
|
656
|
+
throw new Exception(LiveUpdatePlugin.ERROR_PUBLIC_KEY_INVALID);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
private File createTemporaryDirectory() {
|
|
661
|
+
File file = buildTemporaryDirectory();
|
|
662
|
+
file.mkdir();
|
|
663
|
+
return file;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
private void deleteBundleById(@NonNull String bundleId) {
|
|
667
|
+
// Delete the bundle directory
|
|
668
|
+
File bundleDirectory = buildBundleDirectoryFor(bundleId);
|
|
669
|
+
deleteFileRecursively(bundleDirectory);
|
|
670
|
+
// Reset the next bundle if it is the deleted bundle
|
|
671
|
+
String nextBundleId = getNextBundleId();
|
|
672
|
+
if (bundleId.equals(nextBundleId)) {
|
|
673
|
+
setNextBundleById(null);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
private void deleteFileRecursively(@NonNull File file) {
|
|
678
|
+
if (file.isDirectory()) {
|
|
679
|
+
File[] children = file.listFiles();
|
|
680
|
+
if (children != null) {
|
|
681
|
+
for (File child : children) {
|
|
682
|
+
deleteFileRecursively(child);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
file.delete();
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
private void deleteUnusedBundles() {
|
|
690
|
+
String[] bundleIds = getDownloadedBundleIds();
|
|
691
|
+
for (String bundleId : bundleIds) {
|
|
692
|
+
if (!isBundleInUse(bundleId)) {
|
|
693
|
+
deleteBundleById(bundleId);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
private Call downloadAndVerifyFile(
|
|
699
|
+
@NonNull String url,
|
|
700
|
+
@NonNull File file,
|
|
701
|
+
@Nullable String checksum,
|
|
702
|
+
@Nullable String signature,
|
|
703
|
+
@Nullable DownloadProgressCallback progressCallback,
|
|
704
|
+
@NonNull EmptyCallback completionCallback
|
|
705
|
+
) {
|
|
706
|
+
return httpClient.enqueue(
|
|
707
|
+
url,
|
|
708
|
+
new NonEmptyCallback<Response>() {
|
|
709
|
+
@Override
|
|
710
|
+
public void success(@NonNull Response response) {
|
|
711
|
+
try {
|
|
712
|
+
if (response.isSuccessful()) {
|
|
713
|
+
ResponseBody responseBody = response.body();
|
|
714
|
+
LiveUpdateHttpClient.writeResponseBodyToFile(responseBody, file, progressCallback);
|
|
715
|
+
|
|
716
|
+
// Extract checksum/signature from headers
|
|
717
|
+
String finalChecksum = checksum == null ? LiveUpdateHttpClient.getChecksumFromResponse(response) : checksum;
|
|
718
|
+
String finalSignature = signature == null ? LiveUpdateHttpClient.getSignatureFromResponse(response) : signature;
|
|
719
|
+
|
|
720
|
+
// Verify file
|
|
721
|
+
verifyFile(file, finalChecksum, finalSignature);
|
|
722
|
+
completionCallback.success();
|
|
723
|
+
} else {
|
|
724
|
+
String errorMessage = response.body().string();
|
|
725
|
+
Exception exception = new Exception(LiveUpdatePlugin.ERROR_DOWNLOAD_FAILED);
|
|
726
|
+
Log.e(LiveUpdatePlugin.TAG, errorMessage, exception);
|
|
727
|
+
completionCallback.error(exception);
|
|
728
|
+
}
|
|
729
|
+
} catch (Exception e) {
|
|
730
|
+
completionCallback.error(e);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
@Override
|
|
735
|
+
public void error(@NonNull Exception exception) {
|
|
736
|
+
completionCallback.error(exception);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
private Call downloadBundleFile(
|
|
743
|
+
@NonNull String baseUrl,
|
|
744
|
+
@NonNull String href,
|
|
745
|
+
@NonNull File destinationDirectory,
|
|
746
|
+
@Nullable DownloadProgressCallback progressCallback,
|
|
747
|
+
@NonNull EmptyCallback completionCallback
|
|
748
|
+
) {
|
|
749
|
+
HttpUrl.Builder urlBuilder = HttpUrl.parse(baseUrl).newBuilder();
|
|
750
|
+
urlBuilder.addQueryParameter("href", href);
|
|
751
|
+
String url = urlBuilder.build().toString();
|
|
752
|
+
|
|
753
|
+
File destinationFile = new File(destinationDirectory, href);
|
|
754
|
+
destinationFile.getParentFile().mkdirs();
|
|
755
|
+
return downloadAndVerifyFile(url, destinationFile, null, null, progressCallback, completionCallback);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
private void downloadBundleFiles(
|
|
759
|
+
@NonNull String baseUrl,
|
|
760
|
+
@NonNull List<ManifestItem> filesToDownload,
|
|
761
|
+
@NonNull File destinationDirectory,
|
|
762
|
+
@Nullable DownloadProgressCallback progressCallback,
|
|
763
|
+
@NonNull EmptyCallback completionCallback
|
|
764
|
+
) {
|
|
765
|
+
if (filesToDownload.isEmpty()) {
|
|
766
|
+
if (progressCallback != null) {
|
|
767
|
+
progressCallback.onProgress(0, 0);
|
|
768
|
+
}
|
|
769
|
+
completionCallback.success();
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Thread-safe progress tracking
|
|
774
|
+
AtomicLong totalBytesDownloaded = new AtomicLong(0);
|
|
775
|
+
long totalBytesToDownload = 0;
|
|
776
|
+
for (ManifestItem item : filesToDownload) {
|
|
777
|
+
totalBytesToDownload += item.getSizeInBytes();
|
|
778
|
+
}
|
|
779
|
+
final long finalTotalBytesToDownload = totalBytesToDownload;
|
|
780
|
+
|
|
781
|
+
// Coordination primitives
|
|
782
|
+
CountDownLatch latch = new CountDownLatch(filesToDownload.size());
|
|
783
|
+
AtomicReference<Exception> firstError = new AtomicReference<>();
|
|
784
|
+
AtomicBoolean completionHandled = new AtomicBoolean(false);
|
|
785
|
+
List<Call> activeCalls = new ArrayList<>();
|
|
786
|
+
|
|
787
|
+
// Start all downloads asynchronously (OkHttp's dispatcher handles parallelization)
|
|
788
|
+
for (ManifestItem item : filesToDownload) {
|
|
789
|
+
Call call = downloadBundleFile(
|
|
790
|
+
baseUrl,
|
|
791
|
+
item.getHref(),
|
|
792
|
+
destinationDirectory,
|
|
793
|
+
(downloadedBytes, totalBytes) -> {
|
|
794
|
+
// Per-file progress
|
|
795
|
+
if (progressCallback != null) {
|
|
796
|
+
long totalProgress = totalBytesDownloaded.get() + downloadedBytes;
|
|
797
|
+
progressCallback.onProgress(totalProgress, finalTotalBytesToDownload);
|
|
798
|
+
}
|
|
799
|
+
},
|
|
800
|
+
new EmptyCallback() {
|
|
801
|
+
@Override
|
|
802
|
+
public void success() {
|
|
803
|
+
// Update total progress
|
|
804
|
+
totalBytesDownloaded.addAndGet(item.getSizeInBytes());
|
|
805
|
+
if (progressCallback != null) {
|
|
806
|
+
progressCallback.onProgress(totalBytesDownloaded.get(), finalTotalBytesToDownload);
|
|
807
|
+
}
|
|
808
|
+
latch.countDown();
|
|
809
|
+
|
|
810
|
+
// Check if this was the last download to complete
|
|
811
|
+
if (latch.getCount() == 0 && completionHandled.compareAndSet(false, true)) {
|
|
812
|
+
Exception error = firstError.get();
|
|
813
|
+
if (error != null) {
|
|
814
|
+
completionCallback.error(error);
|
|
815
|
+
} else {
|
|
816
|
+
// Final progress update
|
|
817
|
+
if (progressCallback != null) {
|
|
818
|
+
progressCallback.onProgress(finalTotalBytesToDownload, finalTotalBytesToDownload);
|
|
819
|
+
}
|
|
820
|
+
completionCallback.success();
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
@Override
|
|
826
|
+
public void error(@NonNull Exception e) {
|
|
827
|
+
// Capture first error and cancel all remaining downloads
|
|
828
|
+
if (firstError.compareAndSet(null, e)) {
|
|
829
|
+
Log.e(LiveUpdatePlugin.TAG, "Failed to download file: " + item.getHref(), e);
|
|
830
|
+
// Cancel all in-flight downloads (fail-fast)
|
|
831
|
+
synchronized (activeCalls) {
|
|
832
|
+
for (Call activeCall : activeCalls) {
|
|
833
|
+
if (!activeCall.isCanceled() && !activeCall.isExecuted()) {
|
|
834
|
+
activeCall.cancel();
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
latch.countDown();
|
|
840
|
+
|
|
841
|
+
// Check if this was the last download to complete
|
|
842
|
+
if (latch.getCount() == 0 && completionHandled.compareAndSet(false, true)) {
|
|
843
|
+
completionCallback.error(firstError.get());
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
);
|
|
848
|
+
synchronized (activeCalls) {
|
|
849
|
+
activeCalls.add(call);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
private void downloadBundleOfTypeManifest(
|
|
855
|
+
@NonNull String bundleId,
|
|
856
|
+
@NonNull String downloadUrl,
|
|
857
|
+
@NonNull EmptyCallback completionCallback
|
|
858
|
+
) {
|
|
859
|
+
try {
|
|
860
|
+
// Create a temporary directory
|
|
861
|
+
File temporaryDirectory = createTemporaryDirectory();
|
|
862
|
+
|
|
863
|
+
// Download the latest manifest
|
|
864
|
+
downloadBundleFile(
|
|
865
|
+
downloadUrl,
|
|
866
|
+
manifestFileName,
|
|
867
|
+
temporaryDirectory,
|
|
868
|
+
null,
|
|
869
|
+
new EmptyCallback() {
|
|
870
|
+
@Override
|
|
871
|
+
public void success() {
|
|
872
|
+
try {
|
|
873
|
+
File latestManifestFile = new File(temporaryDirectory, manifestFileName);
|
|
874
|
+
Manifest latestManifest = loadManifest(latestManifestFile);
|
|
875
|
+
// Load the current manifest
|
|
876
|
+
Manifest currentManifest = loadCurrentManifest();
|
|
877
|
+
// Compare the manifests
|
|
878
|
+
List<ManifestItem> itemsToCopy = new ArrayList<>();
|
|
879
|
+
List<ManifestItem> itemsToDownload = new ArrayList<>();
|
|
880
|
+
if (currentManifest == null) {
|
|
881
|
+
itemsToDownload.addAll(latestManifest.getItems());
|
|
882
|
+
} else {
|
|
883
|
+
itemsToCopy.addAll(Manifest.findDuplicateItems(latestManifest, currentManifest));
|
|
884
|
+
itemsToDownload.addAll(Manifest.findMissingItems(latestManifest, currentManifest));
|
|
885
|
+
}
|
|
886
|
+
// Copy the files
|
|
887
|
+
List<ManifestItem> missingItems = copyCurrentBundleFilesAndReturnFailures(itemsToCopy, temporaryDirectory);
|
|
888
|
+
// If items could not be copied, add them to the list of items to download
|
|
889
|
+
if (!missingItems.isEmpty()) {
|
|
890
|
+
itemsToDownload.addAll(missingItems);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// Download the files
|
|
894
|
+
downloadBundleFiles(
|
|
895
|
+
downloadUrl,
|
|
896
|
+
itemsToDownload,
|
|
897
|
+
temporaryDirectory,
|
|
898
|
+
(downloadedBytes, totalBytes) -> {
|
|
899
|
+
DownloadBundleProgressEvent event = new DownloadBundleProgressEvent(
|
|
900
|
+
bundleId,
|
|
901
|
+
downloadedBytes,
|
|
902
|
+
totalBytes
|
|
903
|
+
);
|
|
904
|
+
notifyDownloadBundleProgressListeners(event);
|
|
905
|
+
},
|
|
906
|
+
new EmptyCallback() {
|
|
907
|
+
@Override
|
|
908
|
+
public void success() {
|
|
909
|
+
try {
|
|
910
|
+
// Add the bundle
|
|
911
|
+
addBundleOfTypeManifest(bundleId, temporaryDirectory);
|
|
912
|
+
completionCallback.success();
|
|
913
|
+
} catch (Exception e) {
|
|
914
|
+
completionCallback.error(e);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
@Override
|
|
919
|
+
public void error(@NonNull Exception exception) {
|
|
920
|
+
completionCallback.error(exception);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
);
|
|
924
|
+
} catch (Exception e) {
|
|
925
|
+
completionCallback.error(e);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
@Override
|
|
930
|
+
public void error(@NonNull Exception exception) {
|
|
931
|
+
completionCallback.error(exception);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
);
|
|
935
|
+
} catch (Exception e) {
|
|
936
|
+
completionCallback.error(e);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
private void downloadBundleOfTypeZip(
|
|
941
|
+
@NonNull String bundleId,
|
|
942
|
+
@Nullable String checksum,
|
|
943
|
+
@Nullable String signature,
|
|
944
|
+
@NonNull String downloadUrl,
|
|
945
|
+
@NonNull EmptyCallback completionCallback
|
|
946
|
+
) {
|
|
947
|
+
File file = buildTemporaryZipFile();
|
|
948
|
+
// Download the bundle
|
|
949
|
+
downloadAndVerifyFile(
|
|
950
|
+
downloadUrl,
|
|
951
|
+
file,
|
|
952
|
+
checksum,
|
|
953
|
+
signature,
|
|
954
|
+
(downloadedBytes, totalBytes) -> {
|
|
955
|
+
DownloadBundleProgressEvent event = new DownloadBundleProgressEvent(bundleId, downloadedBytes, totalBytes);
|
|
956
|
+
notifyDownloadBundleProgressListeners(event);
|
|
957
|
+
},
|
|
958
|
+
new EmptyCallback() {
|
|
959
|
+
@Override
|
|
960
|
+
public void success() {
|
|
961
|
+
try {
|
|
962
|
+
// Add the bundle
|
|
963
|
+
addBundleOfTypeZip(bundleId, file);
|
|
964
|
+
// Delete the temporary file
|
|
965
|
+
file.delete();
|
|
966
|
+
completionCallback.success();
|
|
967
|
+
} catch (Exception e) {
|
|
968
|
+
completionCallback.error(e);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
@Override
|
|
973
|
+
public void error(@NonNull Exception exception) {
|
|
974
|
+
// Delete the temporary file on error
|
|
975
|
+
file.delete();
|
|
976
|
+
completionCallback.error(exception);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
private void fetchLatestBundleInternal(
|
|
983
|
+
@NonNull FetchLatestBundleOptions options,
|
|
984
|
+
@NonNull NonEmptyCallback<GetLatestBundleResponse> callback
|
|
985
|
+
) {
|
|
986
|
+
try {
|
|
987
|
+
String channel = options.getChannel() == null ? getChannel() : options.getChannel();
|
|
988
|
+
String url = new HttpUrl.Builder()
|
|
989
|
+
.scheme("https")
|
|
990
|
+
.host(config.getServerDomain())
|
|
991
|
+
.addPathSegment("v1")
|
|
992
|
+
.addPathSegment("apps")
|
|
993
|
+
.addPathSegment(getAppId())
|
|
994
|
+
.addPathSegment("bundles")
|
|
995
|
+
.addPathSegment("latest")
|
|
996
|
+
.addQueryParameter("appVersionCode", getVersionCodeAsString())
|
|
997
|
+
.addQueryParameter("appVersionName", getVersionName())
|
|
998
|
+
.addQueryParameter("bundleId", getCurrentBundleId())
|
|
999
|
+
.addQueryParameter("channelName", channel)
|
|
1000
|
+
.addQueryParameter("customId", preferences.getCustomId())
|
|
1001
|
+
.addQueryParameter("deviceId", getDeviceId())
|
|
1002
|
+
.addQueryParameter("osVersion", String.valueOf(Build.VERSION.SDK_INT))
|
|
1003
|
+
.addQueryParameter("platform", "0")
|
|
1004
|
+
.addQueryParameter("pluginVersion", LiveUpdatePlugin.VERSION)
|
|
1005
|
+
.addQueryParameter("runtime", "cordova")
|
|
1006
|
+
.build()
|
|
1007
|
+
.toString();
|
|
1008
|
+
Log.d(LiveUpdatePlugin.TAG, "Fetching latest bundle: " + url);
|
|
1009
|
+
|
|
1010
|
+
httpClient.enqueue(
|
|
1011
|
+
url,
|
|
1012
|
+
new NonEmptyCallback<Response>() {
|
|
1013
|
+
@Override
|
|
1014
|
+
public void success(@NonNull Response response) {
|
|
1015
|
+
try {
|
|
1016
|
+
String responseBodyString = response.body().string();
|
|
1017
|
+
Log.d(LiveUpdatePlugin.TAG, "Latest bundle response: " + responseBodyString);
|
|
1018
|
+
if (response.isSuccessful()) {
|
|
1019
|
+
JSONObject responseJson = new JSONObject(responseBodyString);
|
|
1020
|
+
GetLatestBundleResponse result = new GetLatestBundleResponse(responseJson);
|
|
1021
|
+
callback.success(result);
|
|
1022
|
+
} else {
|
|
1023
|
+
callback.success(null);
|
|
1024
|
+
}
|
|
1025
|
+
} catch (Exception e) {
|
|
1026
|
+
callback.error(e);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
@Override
|
|
1031
|
+
public void error(@NonNull Exception exception) {
|
|
1032
|
+
callback.error(exception);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
);
|
|
1036
|
+
} catch (Exception e) {
|
|
1037
|
+
callback.error(e);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
private String[] getDownloadedBundleIds() {
|
|
1042
|
+
File bundlesDirectory = buildBundlesDirectory();
|
|
1043
|
+
File[] bundles = bundlesDirectory.listFiles();
|
|
1044
|
+
if (bundles == null) {
|
|
1045
|
+
return new String[0];
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
String[] bundleIds = new String[bundles.length];
|
|
1049
|
+
for (int i = 0; i < bundles.length; i++) {
|
|
1050
|
+
bundleIds[i] = bundles[i].getName();
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
return bundleIds;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
private byte[] getChecksumForFileAsBytes(@NonNull File file) throws Exception {
|
|
1057
|
+
try {
|
|
1058
|
+
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
1059
|
+
BufferedSource source = Okio.buffer(Okio.source(file));
|
|
1060
|
+
Buffer buffer = new Buffer();
|
|
1061
|
+
for (long bytesRead; (bytesRead = source.read(buffer, 2048)) != -1;) {
|
|
1062
|
+
digest.update(buffer.readByteArray());
|
|
1063
|
+
}
|
|
1064
|
+
source.close();
|
|
1065
|
+
return digest.digest();
|
|
1066
|
+
} catch (IOException exception) {
|
|
1067
|
+
Log.e(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
|
|
1068
|
+
throw new Exception(LiveUpdatePlugin.ERROR_CHECKSUM_CALCULATION_FAILED);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
private String getChecksumForFileAsString(@NonNull File file) throws Exception {
|
|
1073
|
+
byte[] checksumBytes = getChecksumForFileAsBytes(file);
|
|
1074
|
+
StringBuilder checksum = new StringBuilder();
|
|
1075
|
+
for (byte checksumByte : checksumBytes) {
|
|
1076
|
+
checksum.append(Integer.toString((checksumByte & 0xff) + 0x100, 16).substring(1));
|
|
1077
|
+
}
|
|
1078
|
+
return checksum.toString();
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
@Nullable
|
|
1082
|
+
private String getAppId() {
|
|
1083
|
+
String appId = preferences.getAppId();
|
|
1084
|
+
if (appId != null) {
|
|
1085
|
+
return appId;
|
|
1086
|
+
}
|
|
1087
|
+
return config.getAppId();
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
@Nullable
|
|
1091
|
+
private String getChannel() {
|
|
1092
|
+
String channel = null;
|
|
1093
|
+
if (config.getDefaultChannel() != null) {
|
|
1094
|
+
channel = config.getDefaultChannel();
|
|
1095
|
+
}
|
|
1096
|
+
String nativeChannel = getNativeChannel();
|
|
1097
|
+
if (nativeChannel != null) {
|
|
1098
|
+
channel = nativeChannel;
|
|
1099
|
+
}
|
|
1100
|
+
if (preferences.getChannel() != null) {
|
|
1101
|
+
channel = preferences.getChannel();
|
|
1102
|
+
}
|
|
1103
|
+
return channel;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
@Nullable
|
|
1107
|
+
private String getNativeChannel() {
|
|
1108
|
+
// Reads `capawesome_live_update_default_channel` from the merged string
|
|
1109
|
+
// resource namespace. The plugin seeds this name in our own strings file
|
|
1110
|
+
// (see plugin.xml); app developers can override it with `resValue` for
|
|
1111
|
+
// Versioned Channels — `resValue` wins per AGP's merge order.
|
|
1112
|
+
return LiveUpdatePlugin.readStringRes(
|
|
1113
|
+
plugin.getContext().getResources(),
|
|
1114
|
+
plugin.getContext().getPackageName(),
|
|
1115
|
+
"capawesome_live_update_default_channel"
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* @return The current bundle ID or {@code null} if the default bundle is in use.
|
|
1121
|
+
*/
|
|
1122
|
+
@Nullable
|
|
1123
|
+
private String getCurrentBundleId() {
|
|
1124
|
+
File dir = pathHandler.getActiveBundleDir();
|
|
1125
|
+
if (dir == null) {
|
|
1126
|
+
return null;
|
|
1127
|
+
}
|
|
1128
|
+
return dir.getName();
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
@NonNull
|
|
1132
|
+
private String getDeviceId() {
|
|
1133
|
+
String deviceId = preferences.getDeviceIdForApp(getAppId());
|
|
1134
|
+
if (deviceId == null) {
|
|
1135
|
+
deviceId = UUID.randomUUID().toString().toLowerCase();
|
|
1136
|
+
preferences.setDeviceIdForApp(getAppId(), deviceId);
|
|
1137
|
+
}
|
|
1138
|
+
return deviceId;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/**
|
|
1142
|
+
* @return The next bundle ID or {@code null} if the default bundle will be used.
|
|
1143
|
+
*/
|
|
1144
|
+
@Nullable
|
|
1145
|
+
private String getNextBundleId() {
|
|
1146
|
+
return preferences.getNextBundleId();
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* @return The previous bundle ID or {@code null} if the default bundle was used.
|
|
1151
|
+
*/
|
|
1152
|
+
@Nullable
|
|
1153
|
+
private String getPreviousBundleId() {
|
|
1154
|
+
return preferences.getPreviousBundleId();
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
private int getVersionCodeAsInt() throws PackageManager.NameNotFoundException {
|
|
1158
|
+
return getPackageInfo().versionCode;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
private String getVersionCodeAsString() throws PackageManager.NameNotFoundException {
|
|
1162
|
+
return String.valueOf(getVersionCodeAsInt());
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
private String getVersionName() throws PackageManager.NameNotFoundException {
|
|
1166
|
+
return getPackageInfo().versionName;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
private boolean hasBundleById(@NonNull String bundleId) {
|
|
1170
|
+
File bundleDirectory = buildBundleDirectoryFor(bundleId);
|
|
1171
|
+
return bundleDirectory.exists();
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
private boolean isBundleInUse(@NonNull String bundleId) {
|
|
1175
|
+
String currentBundleId = getCurrentBundleId();
|
|
1176
|
+
String nextBundleId = getNextBundleId();
|
|
1177
|
+
return bundleId.equals(currentBundleId) || bundleId.equals(nextBundleId);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
@Nullable
|
|
1181
|
+
private Manifest loadCurrentManifest() throws Exception {
|
|
1182
|
+
String currentBundleId = getCurrentBundleId();
|
|
1183
|
+
if (currentBundleId == null) {
|
|
1184
|
+
AssetManager assets = plugin.getContext().getAssets();
|
|
1185
|
+
boolean manifestFileExists = Arrays.asList(assets.list(defaultWebAssetDir)).contains(manifestFileName);
|
|
1186
|
+
if (manifestFileExists) {
|
|
1187
|
+
InputStream inputStream = assets.open(defaultWebAssetDir + "/" + manifestFileName);
|
|
1188
|
+
BufferedSource source = Okio.buffer(Okio.source(inputStream));
|
|
1189
|
+
return loadManifest(source);
|
|
1190
|
+
} else {
|
|
1191
|
+
return null;
|
|
1192
|
+
}
|
|
1193
|
+
} else {
|
|
1194
|
+
File currentBundleDirectory = buildBundleDirectoryFor(currentBundleId);
|
|
1195
|
+
File manifestFile = new File(currentBundleDirectory, manifestFileName);
|
|
1196
|
+
if (manifestFile.exists()) {
|
|
1197
|
+
return loadManifest(manifestFile);
|
|
1198
|
+
} else {
|
|
1199
|
+
return null;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
private Manifest loadManifest(@NonNull BufferedSource source) throws Exception {
|
|
1205
|
+
String jsonAsString = source.readUtf8();
|
|
1206
|
+
JSONArray jsonArray = new JSONArray(jsonAsString);
|
|
1207
|
+
return new Manifest(jsonArray);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
private Manifest loadManifest(@NonNull File file) throws Exception {
|
|
1211
|
+
BufferedSource source = Okio.buffer(Okio.source(file));
|
|
1212
|
+
return loadManifest(source);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
private void notifyDownloadBundleProgressListeners(@NonNull final DownloadBundleProgressEvent event) {
|
|
1216
|
+
plugin.notifyDownloadBundleProgressListeners(event);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
private void performAutoUpdate() {
|
|
1220
|
+
// Check if enough time has passed since the last check
|
|
1221
|
+
long now = System.currentTimeMillis();
|
|
1222
|
+
if (lastAutoUpdateCheckTimestamp > 0 && (now - lastAutoUpdateCheckTimestamp) < autoUpdateIntervalMs) {
|
|
1223
|
+
Log.d(LiveUpdatePlugin.TAG, "Auto-update skipped. Last check was less than 15 minutes ago.");
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// Skip if appId is not configured
|
|
1228
|
+
String appId = getAppId();
|
|
1229
|
+
if (appId == null || appId.isEmpty()) {
|
|
1230
|
+
Log.d(LiveUpdatePlugin.TAG, "Auto-update skipped. appId is not configured.");
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// Update the timestamp
|
|
1235
|
+
lastAutoUpdateCheckTimestamp = now;
|
|
1236
|
+
|
|
1237
|
+
// Run sync
|
|
1238
|
+
Log.d(LiveUpdatePlugin.TAG, "Auto-update started.");
|
|
1239
|
+
SyncOptions options = new SyncOptions((String) null);
|
|
1240
|
+
NonEmptyCallback<Result> callback = new NonEmptyCallback<>() {
|
|
1241
|
+
@Override
|
|
1242
|
+
public void success(@NonNull Result result) {
|
|
1243
|
+
Log.d(LiveUpdatePlugin.TAG, "Auto-update completed successfully.");
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
@Override
|
|
1247
|
+
public void error(@NonNull Exception exception) {
|
|
1248
|
+
Log.e(LiveUpdatePlugin.TAG, "Auto-update failed: " + exception.getMessage(), exception);
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
sync(options, callback);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
private void rollback() {
|
|
1255
|
+
// Set the rollback flag
|
|
1256
|
+
rollbackPerformed = true;
|
|
1257
|
+
// Set the new previous bundle ID
|
|
1258
|
+
String currentBundleId = getCurrentBundleId();
|
|
1259
|
+
setPreviousBundleId(currentBundleId);
|
|
1260
|
+
// Log the rollback result
|
|
1261
|
+
if (currentBundleId == null) {
|
|
1262
|
+
Log.d(LiveUpdatePlugin.TAG, "App is not ready. Default bundle is already in use.");
|
|
1263
|
+
} else {
|
|
1264
|
+
Log.d(LiveUpdatePlugin.TAG, "App is not ready. Rolling back to default bundle.");
|
|
1265
|
+
// Rollback to the default bundle
|
|
1266
|
+
setNextBundleById(null);
|
|
1267
|
+
setCurrentBundleById(null);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
@Nullable
|
|
1272
|
+
private File searchIndexHtmlFile(@NonNull File directory) {
|
|
1273
|
+
File[] files = directory.listFiles();
|
|
1274
|
+
if (files == null) {
|
|
1275
|
+
return null;
|
|
1276
|
+
}
|
|
1277
|
+
String[] fileNames = new String[files.length];
|
|
1278
|
+
for (int i = 0; i < files.length; i++) {
|
|
1279
|
+
fileNames[i] = files[i].getName();
|
|
1280
|
+
}
|
|
1281
|
+
if (Arrays.asList(fileNames).contains("index.html")) {
|
|
1282
|
+
return new File(directory, "index.html");
|
|
1283
|
+
} else {
|
|
1284
|
+
for (File file : files) {
|
|
1285
|
+
if (file.isDirectory()) {
|
|
1286
|
+
File indexHtmlFile = searchIndexHtmlFile(file);
|
|
1287
|
+
if (indexHtmlFile != null) {
|
|
1288
|
+
return indexHtmlFile;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
return null;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* @param bundleId The bundle ID to set as the current bundle. If {@code null}, the default bundle will be used.
|
|
1298
|
+
*/
|
|
1299
|
+
private void setCurrentBundleById(@Nullable String bundleId) {
|
|
1300
|
+
if (bundleId == null) {
|
|
1301
|
+
pathHandler.setActiveBundleDir(null);
|
|
1302
|
+
} else {
|
|
1303
|
+
pathHandler.setActiveBundleDir(buildBundleDirectoryFor(bundleId));
|
|
1304
|
+
}
|
|
1305
|
+
plugin.reloadWebView();
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
/**
|
|
1309
|
+
* @param bundleId The bundle ID to set as the next bundle. If {@code null}, the default bundle will be used.
|
|
1310
|
+
*/
|
|
1311
|
+
private void setNextBundleById(@Nullable String bundleId) {
|
|
1312
|
+
preferences.setNextBundleId(bundleId);
|
|
1313
|
+
|
|
1314
|
+
// Notify listeners
|
|
1315
|
+
notifyNextBundleSetListeners(bundleId);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
private void notifyNextBundleSetListeners(@Nullable String bundleId) {
|
|
1319
|
+
NextBundleSetEvent event = new NextBundleSetEvent(bundleId);
|
|
1320
|
+
plugin.notifyNextBundleSetListeners(event);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
private void addBlockedBundleId(@NonNull String bundleId) {
|
|
1324
|
+
String blockedIds = preferences.getBlockedBundleIds();
|
|
1325
|
+
List<String> blockedList = new ArrayList<>();
|
|
1326
|
+
|
|
1327
|
+
// Parse existing blocked IDs
|
|
1328
|
+
if (blockedIds != null && !blockedIds.isEmpty()) {
|
|
1329
|
+
String[] ids = blockedIds.split(",");
|
|
1330
|
+
blockedList.addAll(Arrays.asList(ids));
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// Skip if already blocked
|
|
1334
|
+
if (blockedList.contains(bundleId)) {
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// Remove oldest if limit reached
|
|
1339
|
+
if (blockedList.size() >= 100) {
|
|
1340
|
+
blockedList.remove(0);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// Add new bundle
|
|
1344
|
+
blockedList.add(bundleId);
|
|
1345
|
+
|
|
1346
|
+
// Save back to preferences
|
|
1347
|
+
String newBlockedIds = String.join(",", blockedList);
|
|
1348
|
+
preferences.setBlockedBundleIds(newBlockedIds);
|
|
1349
|
+
|
|
1350
|
+
Log.d(LiveUpdatePlugin.TAG, "Bundle blocked: " + bundleId);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
private boolean isBlockedBundleId(@NonNull String bundleId) {
|
|
1354
|
+
String blockedIds = preferences.getBlockedBundleIds();
|
|
1355
|
+
if (blockedIds == null || blockedIds.isEmpty()) {
|
|
1356
|
+
return false;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
String[] ids = blockedIds.split(",");
|
|
1360
|
+
return Arrays.asList(ids).contains(bundleId);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
private void checkAndResetConfigIfVersionChanged() throws PackageManager.NameNotFoundException {
|
|
1364
|
+
int currentVersionCode = getVersionCodeAsInt();
|
|
1365
|
+
int lastVersionCode = preferences.getLastVersionCode();
|
|
1366
|
+
|
|
1367
|
+
if (lastVersionCode == -1 || lastVersionCode != currentVersionCode) {
|
|
1368
|
+
Log.d(
|
|
1369
|
+
LiveUpdatePlugin.TAG,
|
|
1370
|
+
"App version changed (last: " + lastVersionCode + ", current: " + currentVersionCode + "), resetting config."
|
|
1371
|
+
);
|
|
1372
|
+
resetConfig();
|
|
1373
|
+
// Reset the next bundle to the built-in one. The previously persisted
|
|
1374
|
+
// bundle was built for the old native binary and is no longer
|
|
1375
|
+
// compatible. Capacitor gets this for free because its core resets the
|
|
1376
|
+
// server base path on a native update; Cordova has no such mechanism,
|
|
1377
|
+
// so we must do it ourselves.
|
|
1378
|
+
preferences.setNextBundleId(null);
|
|
1379
|
+
preferences.setLastVersionCode(currentVersionCode);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
private void setPreviousBundleId(@Nullable String bundleId) {
|
|
1384
|
+
preferences.setPreviousBundleId(bundleId);
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
private void startRollbackTimer() {
|
|
1388
|
+
if (config.getReadyTimeout() <= 0) {
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
stopRollbackTimer();
|
|
1392
|
+
rollbackHandler.postDelayed(() -> rollback(), config.getReadyTimeout());
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
private void stopRollbackTimer() {
|
|
1396
|
+
rollbackHandler.removeCallbacksAndMessages(null);
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
private boolean tryCopyCurrentBundleFile(@NonNull ManifestItem fileToCopy, @NonNull File destinationDirectory) {
|
|
1400
|
+
try {
|
|
1401
|
+
copyCurrentBundleFile(fileToCopy, destinationDirectory);
|
|
1402
|
+
return true;
|
|
1403
|
+
} catch (IOException exception) {
|
|
1404
|
+
return false;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
private File unzipFile(@NonNull File zipFile) throws IOException {
|
|
1409
|
+
File destination = buildTemporaryDirectory();
|
|
1410
|
+
String destinationPath = destination.getPath();
|
|
1411
|
+
ZipFile zip = new ZipFile(zipFile);
|
|
1412
|
+
// Clear stored Unix permissions to prevent EACCES errors on newer Android versions
|
|
1413
|
+
// where restrictive directory permissions from the zip block file creation.
|
|
1414
|
+
for (FileHeader fileHeader : zip.getFileHeaders()) {
|
|
1415
|
+
fileHeader.setExternalFileAttributes(null);
|
|
1416
|
+
}
|
|
1417
|
+
zip.extractAll(destinationPath);
|
|
1418
|
+
return destination;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
private boolean verifyChecksumForFile(@NonNull File file, @NonNull String checksum) throws Exception {
|
|
1422
|
+
String receivedChecksum = getChecksumForFileAsString(file);
|
|
1423
|
+
return checksum.equals(receivedChecksum);
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
private void verifyFile(@NonNull File file, @Nullable String checksum, @Nullable String signature) throws Exception {
|
|
1427
|
+
String publicKey = config.getPublicKey();
|
|
1428
|
+
if (publicKey != null) {
|
|
1429
|
+
// Verify the signature
|
|
1430
|
+
if (signature == null) {
|
|
1431
|
+
throw new Exception(LiveUpdatePlugin.ERROR_SIGNATURE_MISSING);
|
|
1432
|
+
}
|
|
1433
|
+
// Verify the signature
|
|
1434
|
+
boolean verified = verifySignatureForFile(file, signature, publicKey);
|
|
1435
|
+
if (!verified) {
|
|
1436
|
+
throw new Exception(LiveUpdatePlugin.ERROR_SIGNATURE_VERIFICATION_FAILED);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
// Verify the checksum
|
|
1440
|
+
else if (checksum != null) {
|
|
1441
|
+
// Calculate the checksum
|
|
1442
|
+
boolean verified = verifyChecksumForFile(file, checksum);
|
|
1443
|
+
if (!verified) {
|
|
1444
|
+
throw new Exception(LiveUpdatePlugin.ERROR_CHECKSUM_MISMATCH);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
private boolean verifySignatureForFile(@NonNull File file, @NonNull String signature, @NonNull String keyAsString) throws Exception {
|
|
1450
|
+
PublicKey key = createPublicKeyFromString(keyAsString);
|
|
1451
|
+
return verifySignatureForFile(file, signature, key);
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
private boolean verifySignatureForFile(@NonNull File file, @NonNull String signature, @NonNull PublicKey key) throws Exception {
|
|
1455
|
+
try {
|
|
1456
|
+
byte[] signatureBytes = Base64.decode(signature, Base64.DEFAULT);
|
|
1457
|
+
Signature sig = Signature.getInstance("SHA256withRSA");
|
|
1458
|
+
sig.initVerify(key);
|
|
1459
|
+
BufferedSource source = Okio.buffer(Okio.source(file));
|
|
1460
|
+
Buffer buffer = new Buffer();
|
|
1461
|
+
for (long bytesRead; (bytesRead = source.read(buffer, 2048)) != -1;) {
|
|
1462
|
+
sig.update(buffer.readByteArray());
|
|
1463
|
+
}
|
|
1464
|
+
source.close();
|
|
1465
|
+
return sig.verify(signatureBytes);
|
|
1466
|
+
} catch (Exception exception) {
|
|
1467
|
+
Log.e(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
|
|
1468
|
+
throw new Exception(LiveUpdatePlugin.ERROR_SIGNATURE_VERIFICATION_FAILED);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
private PackageInfo getPackageInfo() throws PackageManager.NameNotFoundException {
|
|
1473
|
+
String packageName = this.plugin.getContext().getPackageName();
|
|
1474
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
1475
|
+
return this.plugin.getContext().getPackageManager().getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0));
|
|
1476
|
+
} else {
|
|
1477
|
+
return this.plugin.getContext().getPackageManager().getPackageInfo(packageName, 0);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
}
|