@capgo/capacitor-webview-version-checker 8.0.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/CapgoCapacitorWebviewVersionChecker.podspec +17 -0
- package/LICENSE +373 -0
- package/Package.swift +28 -0
- package/README.md +393 -0
- package/android/build.gradle +61 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/app/capgo/webviewversionchecker/WebviewVersionChecker.java +75 -0
- package/android/src/main/java/app/capgo/webviewversionchecker/WebviewVersionCheckerPlugin.java +751 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/dist/docs.json +560 -0
- package/dist/esm/definitions.d.ts +294 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +18 -0
- package/dist/esm/web.js +159 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +173 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +176 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/WebviewVersionCheckerPlugin/WebviewVersionChecker.swift +142 -0
- package/ios/Sources/WebviewVersionCheckerPlugin/WebviewVersionCheckerPlugin.swift +394 -0
- package/ios/Tests/WebviewVersionCheckerPluginTests/WebviewVersionCheckerPluginTests.swift +22 -0
- package/package.json +89 -0
package/android/src/main/java/app/capgo/webviewversionchecker/WebviewVersionCheckerPlugin.java
ADDED
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
package app.capgo.webviewversionchecker;
|
|
2
|
+
|
|
3
|
+
import android.app.Activity;
|
|
4
|
+
import android.content.ActivityNotFoundException;
|
|
5
|
+
import android.content.Intent;
|
|
6
|
+
import android.content.pm.PackageInfo;
|
|
7
|
+
import android.net.Uri;
|
|
8
|
+
import android.os.Build;
|
|
9
|
+
import android.webkit.WebView;
|
|
10
|
+
import androidx.annotation.Nullable;
|
|
11
|
+
import androidx.appcompat.app.AlertDialog;
|
|
12
|
+
import androidx.webkit.WebViewCompat;
|
|
13
|
+
import com.getcapacitor.JSObject;
|
|
14
|
+
import com.getcapacitor.Plugin;
|
|
15
|
+
import com.getcapacitor.PluginCall;
|
|
16
|
+
import com.getcapacitor.PluginConfig;
|
|
17
|
+
import com.getcapacitor.PluginMethod;
|
|
18
|
+
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
19
|
+
import java.io.BufferedReader;
|
|
20
|
+
import java.io.IOException;
|
|
21
|
+
import java.io.InputStream;
|
|
22
|
+
import java.io.InputStreamReader;
|
|
23
|
+
import java.net.HttpURLConnection;
|
|
24
|
+
import java.net.URL;
|
|
25
|
+
import java.nio.charset.StandardCharsets;
|
|
26
|
+
import java.text.SimpleDateFormat;
|
|
27
|
+
import java.util.Date;
|
|
28
|
+
import java.util.Locale;
|
|
29
|
+
import java.util.TimeZone;
|
|
30
|
+
import org.json.JSONArray;
|
|
31
|
+
import org.json.JSONObject;
|
|
32
|
+
|
|
33
|
+
@CapacitorPlugin(name = "WebviewVersionChecker")
|
|
34
|
+
public class WebviewVersionCheckerPlugin extends Plugin {
|
|
35
|
+
|
|
36
|
+
private static final String DEFAULT_ANDROID_LATEST_VERSION_API_URL =
|
|
37
|
+
"https://versionhistory.googleapis.com/v1/chrome/platforms/android/channels/stable/versions?page_size=1";
|
|
38
|
+
private static final String DEFAULT_PROMPT_TITLE = "WebView update available";
|
|
39
|
+
private static final String DEFAULT_PROMPT_UPDATE_BUTTON = "Update";
|
|
40
|
+
private static final String DEFAULT_PROMPT_CANCEL_BUTTON = "Later";
|
|
41
|
+
private static final long LATEST_VERSION_CACHE_TTL_MS = 6L * 60L * 60L * 1000L;
|
|
42
|
+
|
|
43
|
+
private static final ThreadLocal<SimpleDateFormat> ISO_FORMAT = ThreadLocal.withInitial(() -> {
|
|
44
|
+
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
|
|
45
|
+
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
|
|
46
|
+
return sdf;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
private final WebviewVersionChecker versionChecker = new WebviewVersionChecker();
|
|
50
|
+
|
|
51
|
+
private RuntimeOptions runtimeOptions = new RuntimeOptions();
|
|
52
|
+
private boolean monitoringEnabled;
|
|
53
|
+
private boolean checkOnResume = true;
|
|
54
|
+
|
|
55
|
+
@Nullable
|
|
56
|
+
private volatile JSObject lastStatus;
|
|
57
|
+
|
|
58
|
+
@Nullable
|
|
59
|
+
private String lastPromptFingerprint;
|
|
60
|
+
|
|
61
|
+
@Nullable
|
|
62
|
+
private String cachedLatestVersion;
|
|
63
|
+
|
|
64
|
+
@Nullable
|
|
65
|
+
private String cachedLatestVersionUrl;
|
|
66
|
+
|
|
67
|
+
private long cachedLatestVersionTimestamp;
|
|
68
|
+
|
|
69
|
+
@Override
|
|
70
|
+
public void load() {
|
|
71
|
+
super.load();
|
|
72
|
+
runtimeOptions = RuntimeOptions.fromConfig(getConfig());
|
|
73
|
+
monitoringEnabled = runtimeOptions.autoCheckOnResume;
|
|
74
|
+
checkOnResume = runtimeOptions.autoCheckOnResume;
|
|
75
|
+
|
|
76
|
+
if (runtimeOptions.autoCheckOnLoad) {
|
|
77
|
+
scheduleCheck("load", runtimeOptions, null, false);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@Override
|
|
82
|
+
protected void handleOnResume() {
|
|
83
|
+
super.handleOnResume();
|
|
84
|
+
if (!monitoringEnabled || !checkOnResume) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
scheduleCheck("resume", runtimeOptions, null, false);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@PluginMethod
|
|
91
|
+
public void check(PluginCall call) {
|
|
92
|
+
RuntimeOptions merged = runtimeOptions.merge(call);
|
|
93
|
+
String source = call.getString("source", "manual");
|
|
94
|
+
scheduleCheck(source, merged, call, merged.showPromptOnOutdated);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@PluginMethod
|
|
98
|
+
public void startMonitoring(PluginCall call) {
|
|
99
|
+
runtimeOptions = runtimeOptions.merge(call);
|
|
100
|
+
monitoringEnabled = true;
|
|
101
|
+
checkOnResume = call.getBoolean("checkOnResume", runtimeOptions.autoCheckOnResume);
|
|
102
|
+
|
|
103
|
+
if (call.getBoolean("checkOnStart", true)) {
|
|
104
|
+
scheduleCheck("startMonitoring", runtimeOptions, null, runtimeOptions.showPromptOnOutdated);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
call.resolve(buildMonitoringStatePayload());
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@PluginMethod
|
|
111
|
+
public void stopMonitoring(PluginCall call) {
|
|
112
|
+
monitoringEnabled = false;
|
|
113
|
+
call.resolve(buildMonitoringStatePayload());
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@PluginMethod
|
|
117
|
+
public void getLastStatus(PluginCall call) {
|
|
118
|
+
JSObject payload = new JSObject();
|
|
119
|
+
payload.put("status", lastStatus != null ? lastStatus : JSONObject.NULL);
|
|
120
|
+
call.resolve(payload);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@PluginMethod
|
|
124
|
+
public void showUpdatePrompt(PluginCall call) {
|
|
125
|
+
PromptOptions promptOptions = PromptOptions.fromCall(call, runtimeOptions);
|
|
126
|
+
showUpdatePrompt(promptOptions, true, (shown, opened) -> {
|
|
127
|
+
JSObject payload = new JSObject();
|
|
128
|
+
payload.put("shown", shown);
|
|
129
|
+
payload.put("openedUpdatePage", opened);
|
|
130
|
+
call.resolve(payload);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@PluginMethod
|
|
135
|
+
public void openUpdatePage(PluginCall call) {
|
|
136
|
+
String url = resolveUpdateUrl(call.getString("updateUrl"), lastStatus);
|
|
137
|
+
if (isBlank(url)) {
|
|
138
|
+
JSObject payload = new JSObject();
|
|
139
|
+
payload.put("opened", false);
|
|
140
|
+
payload.put("url", "");
|
|
141
|
+
call.resolve(payload);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
bridge.executeOnMainThread(() -> {
|
|
146
|
+
boolean opened = openUpdateUrl(url);
|
|
147
|
+
JSObject payload = new JSObject();
|
|
148
|
+
payload.put("opened", opened);
|
|
149
|
+
payload.put("url", url);
|
|
150
|
+
call.resolve(payload);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private void scheduleCheck(String source, RuntimeOptions options, @Nullable PluginCall call, boolean forcePrompt) {
|
|
155
|
+
execute(() -> {
|
|
156
|
+
JSObject status = evaluateStatus(source, options);
|
|
157
|
+
lastStatus = status;
|
|
158
|
+
|
|
159
|
+
notifyStatus(status);
|
|
160
|
+
maybeAutoPrompt(status, options, forcePrompt);
|
|
161
|
+
|
|
162
|
+
if (call != null) {
|
|
163
|
+
bridge.executeOnMainThread(() -> call.resolve(status));
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private JSObject evaluateStatus(String source, RuntimeOptions options) {
|
|
169
|
+
CurrentWebViewInfo current = resolveCurrentWebViewInfo();
|
|
170
|
+
LatestVersionResolution latestResolution = resolveLatestVersion(options);
|
|
171
|
+
|
|
172
|
+
JSObject status = new JSObject();
|
|
173
|
+
status.put("platform", "android");
|
|
174
|
+
status.put("checkedAt", isoNow());
|
|
175
|
+
status.put("source", source);
|
|
176
|
+
status.put("providerPackage", current.packageName);
|
|
177
|
+
status.put("currentVersion", current.versionName);
|
|
178
|
+
|
|
179
|
+
Integer currentMajorVersion = versionChecker.parseMajorVersion(current.versionName);
|
|
180
|
+
if (currentMajorVersion != null) {
|
|
181
|
+
status.put("currentMajorVersion", currentMajorVersion);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
String latestVersion = latestResolution.version;
|
|
185
|
+
if (!isBlank(latestVersion)) {
|
|
186
|
+
status.put("latestVersion", latestVersion);
|
|
187
|
+
Integer latestMajorVersion = versionChecker.parseMajorVersion(latestVersion);
|
|
188
|
+
if (latestMajorVersion != null) {
|
|
189
|
+
status.put("latestMajorVersion", latestMajorVersion);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
String updateUrl = resolveUpdateUrl(options.updateUrl, status);
|
|
194
|
+
if (!isBlank(updateUrl)) {
|
|
195
|
+
status.put("updateUrl", updateUrl);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (isBlank(current.versionName)) {
|
|
199
|
+
status.put("state", "unknown");
|
|
200
|
+
status.put("isLatest", false);
|
|
201
|
+
status.put("reason", "Could not detect the active Android WebView package version.");
|
|
202
|
+
return status;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!isBlank(latestVersion)) {
|
|
206
|
+
int cmp = versionChecker.compareVersions(current.versionName, latestVersion);
|
|
207
|
+
boolean isLatest = cmp >= 0;
|
|
208
|
+
status.put("state", isLatest ? "latest" : "outdated");
|
|
209
|
+
status.put("isLatest", isLatest);
|
|
210
|
+
status.put(
|
|
211
|
+
"reason",
|
|
212
|
+
isLatest
|
|
213
|
+
? "Installed WebView version is at or above the latest resolved version."
|
|
214
|
+
: "Installed WebView version is behind the latest resolved version."
|
|
215
|
+
);
|
|
216
|
+
return status;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
Integer minimumMajorVersion = options.minimumMajorVersion;
|
|
220
|
+
if (minimumMajorVersion != null && currentMajorVersion != null) {
|
|
221
|
+
boolean isLatest = currentMajorVersion >= minimumMajorVersion;
|
|
222
|
+
status.put("state", isLatest ? "latest" : "outdated");
|
|
223
|
+
status.put("isLatest", isLatest);
|
|
224
|
+
status.put(
|
|
225
|
+
"reason",
|
|
226
|
+
isLatest
|
|
227
|
+
? "Installed WebView major version satisfies the minimum configured major version."
|
|
228
|
+
: "Installed WebView major version is below the configured minimum major version."
|
|
229
|
+
);
|
|
230
|
+
return status;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
status.put("state", "unknown");
|
|
234
|
+
status.put("isLatest", false);
|
|
235
|
+
if (!isBlank(latestResolution.errorMessage)) {
|
|
236
|
+
status.put("reason", latestResolution.errorMessage);
|
|
237
|
+
} else {
|
|
238
|
+
status.put("reason", "Unable to resolve latest WebView version. Configure latestVersion or minimumMajorVersion.");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return status;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private void notifyStatus(JSObject status) {
|
|
245
|
+
bridge.executeOnMainThread(() -> {
|
|
246
|
+
notifyListeners("statusChanged", status);
|
|
247
|
+
String state = status.optString("state", "unknown");
|
|
248
|
+
if ("latest".equals(state)) {
|
|
249
|
+
notifyListeners("webViewLatest", status);
|
|
250
|
+
} else if ("outdated".equals(state)) {
|
|
251
|
+
notifyListeners("webViewOutdated", status);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private void maybeAutoPrompt(JSObject status, RuntimeOptions options, boolean forcePrompt) {
|
|
257
|
+
String state = status.optString("state", "unknown");
|
|
258
|
+
boolean shouldPrompt = "outdated".equals(state) && (forcePrompt || options.autoPromptOnOutdated);
|
|
259
|
+
if (!shouldPrompt) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
PromptOptions promptOptions = PromptOptions.fromRuntime(options);
|
|
264
|
+
showUpdatePrompt(promptOptions, forcePrompt, null);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private void showUpdatePrompt(PromptOptions promptOptions, boolean force, @Nullable PromptResultCallback callback) {
|
|
268
|
+
Activity activity = getActivity();
|
|
269
|
+
if (activity == null || activity.isFinishing()) {
|
|
270
|
+
if (callback != null) {
|
|
271
|
+
callback.onResult(false, false);
|
|
272
|
+
}
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
String updateUrl = resolveUpdateUrl(promptOptions.updateUrl, lastStatus);
|
|
277
|
+
if (isBlank(updateUrl)) {
|
|
278
|
+
if (callback != null) {
|
|
279
|
+
callback.onResult(false, false);
|
|
280
|
+
}
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
String fingerprint = buildPromptFingerprint(lastStatus, updateUrl);
|
|
285
|
+
if (!force && !isBlank(fingerprint) && fingerprint.equals(lastPromptFingerprint)) {
|
|
286
|
+
if (callback != null) {
|
|
287
|
+
callback.onResult(false, false);
|
|
288
|
+
}
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!isBlank(fingerprint)) {
|
|
293
|
+
lastPromptFingerprint = fingerprint;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
String title = !isBlank(promptOptions.title) ? promptOptions.title : DEFAULT_PROMPT_TITLE;
|
|
297
|
+
String message = !isBlank(promptOptions.message) ? promptOptions.message : buildDefaultPromptMessage(lastStatus);
|
|
298
|
+
String updateButtonText = !isBlank(promptOptions.updateButtonText) ? promptOptions.updateButtonText : DEFAULT_PROMPT_UPDATE_BUTTON;
|
|
299
|
+
String cancelButtonText = !isBlank(promptOptions.cancelButtonText) ? promptOptions.cancelButtonText : DEFAULT_PROMPT_CANCEL_BUTTON;
|
|
300
|
+
|
|
301
|
+
bridge.executeOnMainThread(() -> {
|
|
302
|
+
final boolean[] resolved = { false };
|
|
303
|
+
AlertDialog dialog = new AlertDialog.Builder(activity)
|
|
304
|
+
.setTitle(title)
|
|
305
|
+
.setMessage(message)
|
|
306
|
+
.setPositiveButton(updateButtonText, (dialogInterface, which) -> {
|
|
307
|
+
boolean opened = openUpdateUrl(updateUrl);
|
|
308
|
+
if (callback != null && !resolved[0]) {
|
|
309
|
+
resolved[0] = true;
|
|
310
|
+
callback.onResult(true, opened);
|
|
311
|
+
}
|
|
312
|
+
})
|
|
313
|
+
.setNegativeButton(cancelButtonText, (dialogInterface, which) -> {
|
|
314
|
+
if (callback != null && !resolved[0]) {
|
|
315
|
+
resolved[0] = true;
|
|
316
|
+
callback.onResult(true, false);
|
|
317
|
+
}
|
|
318
|
+
})
|
|
319
|
+
.setOnCancelListener(dialogInterface -> {
|
|
320
|
+
if (callback != null && !resolved[0]) {
|
|
321
|
+
resolved[0] = true;
|
|
322
|
+
callback.onResult(true, false);
|
|
323
|
+
}
|
|
324
|
+
})
|
|
325
|
+
.create();
|
|
326
|
+
dialog.show();
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private JSObject buildMonitoringStatePayload() {
|
|
331
|
+
JSObject payload = new JSObject();
|
|
332
|
+
payload.put("monitoring", monitoringEnabled);
|
|
333
|
+
payload.put("checkOnResume", checkOnResume);
|
|
334
|
+
payload.put("autoPromptOnOutdated", runtimeOptions.autoPromptOnOutdated);
|
|
335
|
+
return payload;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private CurrentWebViewInfo resolveCurrentWebViewInfo() {
|
|
339
|
+
try {
|
|
340
|
+
PackageInfo packageInfo = WebViewCompat.getCurrentWebViewPackage(getContext());
|
|
341
|
+
if (packageInfo != null) {
|
|
342
|
+
return new CurrentWebViewInfo(packageInfo.packageName, packageInfo.versionName);
|
|
343
|
+
}
|
|
344
|
+
} catch (Throwable ignored) {
|
|
345
|
+
// Fall through to platform APIs.
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
349
|
+
try {
|
|
350
|
+
PackageInfo packageInfo = WebView.getCurrentWebViewPackage();
|
|
351
|
+
if (packageInfo != null) {
|
|
352
|
+
return new CurrentWebViewInfo(packageInfo.packageName, packageInfo.versionName);
|
|
353
|
+
}
|
|
354
|
+
} catch (Throwable ignored) {
|
|
355
|
+
// Fall through to unknown state.
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return new CurrentWebViewInfo("", "");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private LatestVersionResolution resolveLatestVersion(RuntimeOptions options) {
|
|
363
|
+
if (!isBlank(options.latestVersion)) {
|
|
364
|
+
return new LatestVersionResolution(options.latestVersion, null);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
String apiUrl = !isBlank(options.latestVersionApiUrl) ? options.latestVersionApiUrl : DEFAULT_ANDROID_LATEST_VERSION_API_URL;
|
|
368
|
+
|
|
369
|
+
if (
|
|
370
|
+
!isBlank(cachedLatestVersion) &&
|
|
371
|
+
!isBlank(cachedLatestVersionUrl) &&
|
|
372
|
+
apiUrl.equals(cachedLatestVersionUrl) &&
|
|
373
|
+
(System.currentTimeMillis() - cachedLatestVersionTimestamp) < LATEST_VERSION_CACHE_TTL_MS
|
|
374
|
+
) {
|
|
375
|
+
return new LatestVersionResolution(cachedLatestVersion, null);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
String payload = readUrl(apiUrl);
|
|
380
|
+
String version = extractLatestVersion(payload);
|
|
381
|
+
if (isBlank(version)) {
|
|
382
|
+
return new LatestVersionResolution(null, "Latest version API returned no version string.");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
cachedLatestVersion = version;
|
|
386
|
+
cachedLatestVersionUrl = apiUrl;
|
|
387
|
+
cachedLatestVersionTimestamp = System.currentTimeMillis();
|
|
388
|
+
|
|
389
|
+
return new LatestVersionResolution(version, null);
|
|
390
|
+
} catch (Exception error) {
|
|
391
|
+
return new LatestVersionResolution(null, "Could not resolve latest WebView version: " + error.getMessage());
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private String readUrl(String urlValue) throws IOException {
|
|
396
|
+
HttpURLConnection connection = (HttpURLConnection) new URL(urlValue).openConnection();
|
|
397
|
+
connection.setRequestMethod("GET");
|
|
398
|
+
connection.setConnectTimeout(5000);
|
|
399
|
+
connection.setReadTimeout(5000);
|
|
400
|
+
connection.setRequestProperty("Accept", "application/json");
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
InputStream stream = connection.getResponseCode() >= 400 ? connection.getErrorStream() : connection.getInputStream();
|
|
404
|
+
if (stream == null) {
|
|
405
|
+
throw new IOException("No response body.");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8));
|
|
409
|
+
StringBuilder out = new StringBuilder();
|
|
410
|
+
String line;
|
|
411
|
+
while ((line = reader.readLine()) != null) {
|
|
412
|
+
out.append(line);
|
|
413
|
+
}
|
|
414
|
+
return out.toString();
|
|
415
|
+
} finally {
|
|
416
|
+
connection.disconnect();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
@Nullable
|
|
421
|
+
private String extractLatestVersion(String payload) throws Exception {
|
|
422
|
+
JSONObject json = new JSONObject(payload);
|
|
423
|
+
|
|
424
|
+
String version = normalizeVersion(json.optString("version", null));
|
|
425
|
+
if (!isBlank(version)) {
|
|
426
|
+
return version;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
version = normalizeVersion(json.optString("latestVersion", null));
|
|
430
|
+
if (!isBlank(version)) {
|
|
431
|
+
return version;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
JSONArray versions = json.optJSONArray("versions");
|
|
435
|
+
if (versions != null && versions.length() > 0) {
|
|
436
|
+
JSONObject first = versions.optJSONObject(0);
|
|
437
|
+
if (first != null) {
|
|
438
|
+
version = normalizeVersion(first.optString("version", null));
|
|
439
|
+
if (!isBlank(version)) {
|
|
440
|
+
return version;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
String name = first.optString("name", null);
|
|
444
|
+
if (!isBlank(name)) {
|
|
445
|
+
int slash = name.lastIndexOf('/');
|
|
446
|
+
if (slash >= 0 && slash + 1 < name.length()) {
|
|
447
|
+
version = normalizeVersion(name.substring(slash + 1));
|
|
448
|
+
if (!isBlank(version)) {
|
|
449
|
+
return version;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
@Nullable
|
|
460
|
+
private String normalizeVersion(@Nullable String value) {
|
|
461
|
+
if (isBlank(value)) {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
return value.trim();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
private String resolveUpdateUrl(@Nullable String explicitUrl, @Nullable JSObject status) {
|
|
468
|
+
if (!isBlank(explicitUrl)) {
|
|
469
|
+
return explicitUrl;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (status != null) {
|
|
473
|
+
String statusUrl = status.optString("updateUrl", null);
|
|
474
|
+
if (!isBlank(statusUrl)) {
|
|
475
|
+
return statusUrl;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
String providerPackage = status.optString("providerPackage", defaultProviderPackageForSdk());
|
|
479
|
+
return buildStoreUrl(providerPackage);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return buildStoreUrl(defaultProviderPackageForSdk());
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
private String buildStoreUrl(@Nullable String packageName) {
|
|
486
|
+
String resolvedPackage = isBlank(packageName) ? defaultProviderPackageForSdk() : packageName;
|
|
487
|
+
return "market://details?id=" + resolvedPackage;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
private String defaultProviderPackageForSdk() {
|
|
491
|
+
int sdk = Build.VERSION.SDK_INT;
|
|
492
|
+
if (sdk >= Build.VERSION_CODES.N && sdk <= Build.VERSION_CODES.P) {
|
|
493
|
+
return "com.android.chrome";
|
|
494
|
+
}
|
|
495
|
+
return "com.google.android.webview";
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private boolean openUpdateUrl(String url) {
|
|
499
|
+
Activity activity = getActivity();
|
|
500
|
+
if (activity == null) {
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
try {
|
|
505
|
+
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
|
506
|
+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
507
|
+
activity.startActivity(intent);
|
|
508
|
+
return true;
|
|
509
|
+
} catch (ActivityNotFoundException error) {
|
|
510
|
+
if (url.startsWith("market://")) {
|
|
511
|
+
Uri uri = Uri.parse(url);
|
|
512
|
+
String packageName = uri.getQueryParameter("id");
|
|
513
|
+
if (!isBlank(packageName)) {
|
|
514
|
+
String fallbackUrl = "https://play.google.com/store/apps/details?id=" + packageName;
|
|
515
|
+
try {
|
|
516
|
+
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(fallbackUrl));
|
|
517
|
+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
518
|
+
activity.startActivity(intent);
|
|
519
|
+
return true;
|
|
520
|
+
} catch (ActivityNotFoundException ignored) {
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
private String buildDefaultPromptMessage(@Nullable JSObject status) {
|
|
530
|
+
if (status == null) {
|
|
531
|
+
return "A newer WebView version is available. Please update for best compatibility.";
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
String current = status.optString("currentVersion", "current version");
|
|
535
|
+
String latest = status.optString("latestVersion", "the latest available version");
|
|
536
|
+
return "Your WebView version (" + current + ") is outdated. Please update to " + latest + ".";
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
private String buildPromptFingerprint(@Nullable JSObject status, String updateUrl) {
|
|
540
|
+
if (status == null) {
|
|
541
|
+
return updateUrl;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
String current = status.optString("currentVersion", "");
|
|
545
|
+
String latest = status.optString("latestVersion", "");
|
|
546
|
+
String provider = status.optString("providerPackage", "");
|
|
547
|
+
return current + "|" + latest + "|" + provider + "|" + updateUrl;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
private boolean isBlank(@Nullable String value) {
|
|
551
|
+
return value == null || value.trim().isEmpty();
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
private String isoNow() {
|
|
555
|
+
return ISO_FORMAT.get().format(new Date());
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
private interface PromptResultCallback {
|
|
559
|
+
void onResult(boolean shown, boolean openedUpdatePage);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
private static final class CurrentWebViewInfo {
|
|
563
|
+
|
|
564
|
+
private final String packageName;
|
|
565
|
+
private final String versionName;
|
|
566
|
+
|
|
567
|
+
private CurrentWebViewInfo(String packageName, String versionName) {
|
|
568
|
+
this.packageName = packageName != null ? packageName : "";
|
|
569
|
+
this.versionName = versionName != null ? versionName : "";
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
private static final class LatestVersionResolution {
|
|
574
|
+
|
|
575
|
+
@Nullable
|
|
576
|
+
private final String version;
|
|
577
|
+
|
|
578
|
+
@Nullable
|
|
579
|
+
private final String errorMessage;
|
|
580
|
+
|
|
581
|
+
private LatestVersionResolution(@Nullable String version, @Nullable String errorMessage) {
|
|
582
|
+
this.version = version;
|
|
583
|
+
this.errorMessage = errorMessage;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
private static final class PromptOptions {
|
|
588
|
+
|
|
589
|
+
@Nullable
|
|
590
|
+
private String title;
|
|
591
|
+
|
|
592
|
+
@Nullable
|
|
593
|
+
private String message;
|
|
594
|
+
|
|
595
|
+
@Nullable
|
|
596
|
+
private String updateButtonText;
|
|
597
|
+
|
|
598
|
+
@Nullable
|
|
599
|
+
private String cancelButtonText;
|
|
600
|
+
|
|
601
|
+
@Nullable
|
|
602
|
+
private String updateUrl;
|
|
603
|
+
|
|
604
|
+
static PromptOptions fromRuntime(RuntimeOptions options) {
|
|
605
|
+
PromptOptions out = new PromptOptions();
|
|
606
|
+
out.title = options.promptTitle;
|
|
607
|
+
out.message = options.promptMessage;
|
|
608
|
+
out.updateButtonText = options.promptUpdateButtonText;
|
|
609
|
+
out.cancelButtonText = options.promptCancelButtonText;
|
|
610
|
+
out.updateUrl = options.updateUrl;
|
|
611
|
+
return out;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
static PromptOptions fromCall(PluginCall call, RuntimeOptions defaults) {
|
|
615
|
+
PromptOptions out = fromRuntime(defaults);
|
|
616
|
+
out.title = emptyToNull(call.getString("title", out.title));
|
|
617
|
+
out.message = emptyToNull(call.getString("message", out.message));
|
|
618
|
+
out.updateButtonText = emptyToNull(call.getString("updateButtonText", out.updateButtonText));
|
|
619
|
+
out.cancelButtonText = emptyToNull(call.getString("cancelButtonText", out.cancelButtonText));
|
|
620
|
+
out.updateUrl = emptyToNull(call.getString("updateUrl", out.updateUrl));
|
|
621
|
+
return out;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
@Nullable
|
|
625
|
+
private static String emptyToNull(@Nullable String value) {
|
|
626
|
+
if (value == null || value.trim().isEmpty()) {
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
return value;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
private static final class RuntimeOptions {
|
|
634
|
+
|
|
635
|
+
private boolean autoCheckOnLoad = true;
|
|
636
|
+
private boolean autoCheckOnResume = true;
|
|
637
|
+
private boolean autoPromptOnOutdated;
|
|
638
|
+
private boolean showPromptOnOutdated;
|
|
639
|
+
|
|
640
|
+
@Nullable
|
|
641
|
+
private String latestVersion;
|
|
642
|
+
|
|
643
|
+
@Nullable
|
|
644
|
+
private Integer minimumMajorVersion;
|
|
645
|
+
|
|
646
|
+
@Nullable
|
|
647
|
+
private String latestVersionApiUrl;
|
|
648
|
+
|
|
649
|
+
@Nullable
|
|
650
|
+
private String updateUrl;
|
|
651
|
+
|
|
652
|
+
@Nullable
|
|
653
|
+
private String promptTitle;
|
|
654
|
+
|
|
655
|
+
@Nullable
|
|
656
|
+
private String promptMessage;
|
|
657
|
+
|
|
658
|
+
@Nullable
|
|
659
|
+
private String promptUpdateButtonText;
|
|
660
|
+
|
|
661
|
+
@Nullable
|
|
662
|
+
private String promptCancelButtonText;
|
|
663
|
+
|
|
664
|
+
static RuntimeOptions fromConfig(PluginConfig config) {
|
|
665
|
+
RuntimeOptions out = new RuntimeOptions();
|
|
666
|
+
out.autoCheckOnLoad = config.getBoolean("autoCheckOnLoad", true);
|
|
667
|
+
out.autoCheckOnResume = config.getBoolean("autoCheckOnResume", true);
|
|
668
|
+
out.autoPromptOnOutdated = config.getBoolean("autoPromptOnOutdated", false);
|
|
669
|
+
out.latestVersion = emptyToNull(config.getString("latestVersion"));
|
|
670
|
+
Integer minimum = config.getConfigJSON().has("minimumMajorVersion")
|
|
671
|
+
? config.getInt("minimumMajorVersion", 0)
|
|
672
|
+
: null;
|
|
673
|
+
out.minimumMajorVersion = minimum;
|
|
674
|
+
out.latestVersionApiUrl = emptyToNull(config.getString("latestVersionApiUrl"));
|
|
675
|
+
out.updateUrl = emptyToNull(config.getString("updateUrl"));
|
|
676
|
+
out.promptTitle = emptyToNull(config.getString("promptTitle"));
|
|
677
|
+
out.promptMessage = emptyToNull(config.getString("promptMessage"));
|
|
678
|
+
out.promptUpdateButtonText = emptyToNull(config.getString("promptUpdateButtonText"));
|
|
679
|
+
out.promptCancelButtonText = emptyToNull(config.getString("promptCancelButtonText"));
|
|
680
|
+
return out;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
RuntimeOptions merge(PluginCall call) {
|
|
684
|
+
RuntimeOptions out = copy();
|
|
685
|
+
|
|
686
|
+
if (call.hasOption("autoCheckOnLoad")) {
|
|
687
|
+
out.autoCheckOnLoad = call.getBoolean("autoCheckOnLoad", out.autoCheckOnLoad);
|
|
688
|
+
}
|
|
689
|
+
if (call.hasOption("autoCheckOnResume")) {
|
|
690
|
+
out.autoCheckOnResume = call.getBoolean("autoCheckOnResume", out.autoCheckOnResume);
|
|
691
|
+
}
|
|
692
|
+
if (call.hasOption("autoPromptOnOutdated")) {
|
|
693
|
+
out.autoPromptOnOutdated = call.getBoolean("autoPromptOnOutdated", out.autoPromptOnOutdated);
|
|
694
|
+
}
|
|
695
|
+
if (call.hasOption("showPromptOnOutdated")) {
|
|
696
|
+
out.showPromptOnOutdated = call.getBoolean("showPromptOnOutdated", out.showPromptOnOutdated);
|
|
697
|
+
}
|
|
698
|
+
if (call.hasOption("latestVersion")) {
|
|
699
|
+
out.latestVersion = emptyToNull(call.getString("latestVersion"));
|
|
700
|
+
}
|
|
701
|
+
if (call.hasOption("minimumMajorVersion")) {
|
|
702
|
+
out.minimumMajorVersion = call.getInt("minimumMajorVersion");
|
|
703
|
+
}
|
|
704
|
+
if (call.hasOption("latestVersionApiUrl")) {
|
|
705
|
+
out.latestVersionApiUrl = emptyToNull(call.getString("latestVersionApiUrl"));
|
|
706
|
+
}
|
|
707
|
+
if (call.hasOption("updateUrl")) {
|
|
708
|
+
out.updateUrl = emptyToNull(call.getString("updateUrl"));
|
|
709
|
+
}
|
|
710
|
+
if (call.hasOption("promptTitle")) {
|
|
711
|
+
out.promptTitle = emptyToNull(call.getString("promptTitle"));
|
|
712
|
+
}
|
|
713
|
+
if (call.hasOption("promptMessage")) {
|
|
714
|
+
out.promptMessage = emptyToNull(call.getString("promptMessage"));
|
|
715
|
+
}
|
|
716
|
+
if (call.hasOption("promptUpdateButtonText")) {
|
|
717
|
+
out.promptUpdateButtonText = emptyToNull(call.getString("promptUpdateButtonText"));
|
|
718
|
+
}
|
|
719
|
+
if (call.hasOption("promptCancelButtonText")) {
|
|
720
|
+
out.promptCancelButtonText = emptyToNull(call.getString("promptCancelButtonText"));
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return out;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
private RuntimeOptions copy() {
|
|
727
|
+
RuntimeOptions out = new RuntimeOptions();
|
|
728
|
+
out.autoCheckOnLoad = autoCheckOnLoad;
|
|
729
|
+
out.autoCheckOnResume = autoCheckOnResume;
|
|
730
|
+
out.autoPromptOnOutdated = autoPromptOnOutdated;
|
|
731
|
+
out.showPromptOnOutdated = showPromptOnOutdated;
|
|
732
|
+
out.latestVersion = latestVersion;
|
|
733
|
+
out.minimumMajorVersion = minimumMajorVersion;
|
|
734
|
+
out.latestVersionApiUrl = latestVersionApiUrl;
|
|
735
|
+
out.updateUrl = updateUrl;
|
|
736
|
+
out.promptTitle = promptTitle;
|
|
737
|
+
out.promptMessage = promptMessage;
|
|
738
|
+
out.promptUpdateButtonText = promptUpdateButtonText;
|
|
739
|
+
out.promptCancelButtonText = promptCancelButtonText;
|
|
740
|
+
return out;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
@Nullable
|
|
744
|
+
private static String emptyToNull(@Nullable String value) {
|
|
745
|
+
if (value == null || value.trim().isEmpty()) {
|
|
746
|
+
return null;
|
|
747
|
+
}
|
|
748
|
+
return value;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|