@capgo/capacitor-webview-version-checker 8.0.0 → 8.2.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.
@@ -25,6 +25,7 @@ import java.net.URL;
25
25
  import java.nio.charset.StandardCharsets;
26
26
  import java.text.SimpleDateFormat;
27
27
  import java.util.Date;
28
+ import java.util.Iterator;
28
29
  import java.util.Locale;
29
30
  import java.util.TimeZone;
30
31
  import org.json.JSONArray;
@@ -66,6 +67,19 @@ public class WebviewVersionCheckerPlugin extends Plugin {
66
67
 
67
68
  private long cachedLatestVersionTimestamp;
68
69
 
70
+ @Nullable
71
+ private JSONObject cachedVersionShareByMajor;
72
+
73
+ @Nullable
74
+ private String cachedVersionShareApiUrl;
75
+
76
+ private long cachedVersionShareTimestamp;
77
+
78
+ private boolean isPromptShowing;
79
+
80
+ @Nullable
81
+ private AlertDialog activePromptDialog;
82
+
69
83
  @Override
70
84
  public void load() {
71
85
  super.load();
@@ -123,7 +137,7 @@ public class WebviewVersionCheckerPlugin extends Plugin {
123
137
  @PluginMethod
124
138
  public void showUpdatePrompt(PluginCall call) {
125
139
  PromptOptions promptOptions = PromptOptions.fromCall(call, runtimeOptions);
126
- showUpdatePrompt(promptOptions, true, (shown, opened) -> {
140
+ showUpdatePrompt(promptOptions, true, runtimeOptions.autoPromptDismissible, (shown, opened) -> {
127
141
  JSObject payload = new JSObject();
128
142
  payload.put("shown", shown);
129
143
  payload.put("openedUpdatePage", opened);
@@ -157,6 +171,7 @@ public class WebviewVersionCheckerPlugin extends Plugin {
157
171
  lastStatus = status;
158
172
 
159
173
  notifyStatus(status);
174
+ dismissAutoPromptIfUpToDate(status);
160
175
  maybeAutoPrompt(status, options, forcePrompt);
161
176
 
162
177
  if (call != null) {
@@ -167,7 +182,6 @@ public class WebviewVersionCheckerPlugin extends Plugin {
167
182
 
168
183
  private JSObject evaluateStatus(String source, RuntimeOptions options) {
169
184
  CurrentWebViewInfo current = resolveCurrentWebViewInfo();
170
- LatestVersionResolution latestResolution = resolveLatestVersion(options);
171
185
 
172
186
  JSObject status = new JSObject();
173
187
  status.put("platform", "android");
@@ -181,15 +195,6 @@ public class WebviewVersionCheckerPlugin extends Plugin {
181
195
  status.put("currentMajorVersion", currentMajorVersion);
182
196
  }
183
197
 
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
198
  String updateUrl = resolveUpdateUrl(options.updateUrl, status);
194
199
  if (!isBlank(updateUrl)) {
195
200
  status.put("updateUrl", updateUrl);
@@ -202,6 +207,55 @@ public class WebviewVersionCheckerPlugin extends Plugin {
202
207
  return status;
203
208
  }
204
209
 
210
+ String deviceShareErrorMessage = null;
211
+ if (options.minimumDeviceSharePercent != null) {
212
+ status.put("minimumDeviceSharePercent", options.minimumDeviceSharePercent);
213
+ if (currentMajorVersion == null) {
214
+ deviceShareErrorMessage =
215
+ "Could not evaluate device-share threshold because the installed WebView major version could not be parsed.";
216
+ } else {
217
+ VersionShareResolution versionShareResolution = resolveVersionShare(options);
218
+ if (!isBlank(versionShareResolution.source)) {
219
+ status.put("versionShareSource", versionShareResolution.source);
220
+ }
221
+
222
+ Double currentVersionSharePercent = readShareForMajor(versionShareResolution.shareByMajor, currentMajorVersion);
223
+ if (currentVersionSharePercent != null) {
224
+ status.put("currentVersionSharePercent", currentVersionSharePercent);
225
+ boolean isLatest = currentVersionSharePercent >= options.minimumDeviceSharePercent;
226
+ status.put("state", isLatest ? "latest" : "outdated");
227
+ status.put("isLatest", isLatest);
228
+ status.put(
229
+ "reason",
230
+ isLatest
231
+ ? "Installed WebView major version satisfies the configured minimum device-share threshold."
232
+ : "Installed WebView major version is below the configured minimum device-share threshold."
233
+ );
234
+ return status;
235
+ }
236
+
237
+ if (!isBlank(versionShareResolution.errorMessage)) {
238
+ deviceShareErrorMessage = versionShareResolution.errorMessage;
239
+ } else {
240
+ deviceShareErrorMessage =
241
+ "No version-share data was found for installed WebView major version " + currentMajorVersion + ".";
242
+ }
243
+ }
244
+ if (!isBlank(deviceShareErrorMessage)) {
245
+ status.put("deviceShareError", deviceShareErrorMessage);
246
+ }
247
+ }
248
+
249
+ LatestVersionResolution latestResolution = resolveLatestVersion(options);
250
+ String latestVersion = latestResolution.version;
251
+ if (!isBlank(latestVersion)) {
252
+ status.put("latestVersion", latestVersion);
253
+ Integer latestMajorVersion = versionChecker.parseMajorVersion(latestVersion);
254
+ if (latestMajorVersion != null) {
255
+ status.put("latestMajorVersion", latestMajorVersion);
256
+ }
257
+ }
258
+
205
259
  if (!isBlank(latestVersion)) {
206
260
  int cmp = versionChecker.compareVersions(current.versionName, latestVersion);
207
261
  boolean isLatest = cmp >= 0;
@@ -234,8 +288,13 @@ public class WebviewVersionCheckerPlugin extends Plugin {
234
288
  status.put("isLatest", false);
235
289
  if (!isBlank(latestResolution.errorMessage)) {
236
290
  status.put("reason", latestResolution.errorMessage);
291
+ } else if (!isBlank(deviceShareErrorMessage)) {
292
+ status.put("reason", deviceShareErrorMessage);
237
293
  } else {
238
- status.put("reason", "Unable to resolve latest WebView version. Configure latestVersion or minimumMajorVersion.");
294
+ status.put(
295
+ "reason",
296
+ "Unable to resolve compatibility status. Configure latestVersion, minimumMajorVersion, or minimumDeviceSharePercent."
297
+ );
239
298
  }
240
299
 
241
300
  return status;
@@ -261,10 +320,15 @@ public class WebviewVersionCheckerPlugin extends Plugin {
261
320
  }
262
321
 
263
322
  PromptOptions promptOptions = PromptOptions.fromRuntime(options);
264
- showUpdatePrompt(promptOptions, forcePrompt, null);
323
+ showUpdatePrompt(promptOptions, forcePrompt, options.autoPromptDismissible, null);
265
324
  }
266
325
 
267
- private void showUpdatePrompt(PromptOptions promptOptions, boolean force, @Nullable PromptResultCallback callback) {
326
+ private void showUpdatePrompt(
327
+ PromptOptions promptOptions,
328
+ boolean force,
329
+ boolean dismissible,
330
+ @Nullable PromptResultCallback callback
331
+ ) {
268
332
  Activity activity = getActivity();
269
333
  if (activity == null || activity.isFinishing()) {
270
334
  if (callback != null) {
@@ -282,15 +346,24 @@ public class WebviewVersionCheckerPlugin extends Plugin {
282
346
  }
283
347
 
284
348
  String fingerprint = buildPromptFingerprint(lastStatus, updateUrl);
285
- if (!force && !isBlank(fingerprint) && fingerprint.equals(lastPromptFingerprint)) {
349
+ if (dismissible && !force && !isBlank(fingerprint) && fingerprint.equals(lastPromptFingerprint)) {
350
+ if (callback != null) {
351
+ callback.onResult(false, false);
352
+ }
353
+ return;
354
+ }
355
+
356
+ if (isPromptShowing) {
286
357
  if (callback != null) {
287
358
  callback.onResult(false, false);
288
359
  }
289
360
  return;
290
361
  }
291
362
 
292
- if (!isBlank(fingerprint)) {
363
+ if (dismissible && !isBlank(fingerprint)) {
293
364
  lastPromptFingerprint = fingerprint;
365
+ } else {
366
+ lastPromptFingerprint = null;
294
367
  }
295
368
 
296
369
  String title = !isBlank(promptOptions.title) ? promptOptions.title : DEFAULT_PROMPT_TITLE;
@@ -300,7 +373,7 @@ public class WebviewVersionCheckerPlugin extends Plugin {
300
373
 
301
374
  bridge.executeOnMainThread(() -> {
302
375
  final boolean[] resolved = { false };
303
- AlertDialog dialog = new AlertDialog.Builder(activity)
376
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity)
304
377
  .setTitle(title)
305
378
  .setMessage(message)
306
379
  .setPositiveButton(updateButtonText, (dialogInterface, which) -> {
@@ -309,21 +382,64 @@ public class WebviewVersionCheckerPlugin extends Plugin {
309
382
  resolved[0] = true;
310
383
  callback.onResult(true, opened);
311
384
  }
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();
385
+ });
386
+
387
+ if (dismissible) {
388
+ builder
389
+ .setNegativeButton(cancelButtonText, (dialogInterface, which) -> {
390
+ if (callback != null && !resolved[0]) {
391
+ resolved[0] = true;
392
+ callback.onResult(true, false);
393
+ }
394
+ })
395
+ .setOnCancelListener((dialogInterface) -> {
396
+ if (callback != null && !resolved[0]) {
397
+ resolved[0] = true;
398
+ callback.onResult(true, false);
399
+ }
400
+ });
401
+ }
402
+
403
+ AlertDialog dialog = builder.setCancelable(dismissible).create();
404
+ isPromptShowing = true;
405
+ activePromptDialog = dialog;
406
+ dialog.setOnDismissListener((dialogInterface) -> {
407
+ isPromptShowing = false;
408
+ if (activePromptDialog == dialog) {
409
+ activePromptDialog = null;
410
+ }
411
+ });
326
412
  dialog.show();
413
+ if (!dismissible) {
414
+ dialog
415
+ .getButton(AlertDialog.BUTTON_POSITIVE)
416
+ .setOnClickListener((view) -> {
417
+ boolean opened = openUpdateUrl(updateUrl);
418
+ if (callback != null && !resolved[0]) {
419
+ resolved[0] = true;
420
+ callback.onResult(true, opened);
421
+ }
422
+ });
423
+ }
424
+ });
425
+ }
426
+
427
+ private void dismissAutoPromptIfUpToDate(JSObject status) {
428
+ if (!"latest".equals(status.optString("state", "unknown"))) {
429
+ return;
430
+ }
431
+ AlertDialog dialog = activePromptDialog;
432
+ if (dialog == null) {
433
+ return;
434
+ }
435
+
436
+ bridge.executeOnMainThread(() -> {
437
+ if (dialog.isShowing()) {
438
+ dialog.dismiss();
439
+ }
440
+ if (activePromptDialog == dialog) {
441
+ activePromptDialog = null;
442
+ }
327
443
  });
328
444
  }
329
445
 
@@ -392,6 +508,82 @@ public class WebviewVersionCheckerPlugin extends Plugin {
392
508
  }
393
509
  }
394
510
 
511
+ private VersionShareResolution resolveVersionShare(RuntimeOptions options) {
512
+ if (options.versionShareByMajor != null && options.versionShareByMajor.length() > 0) {
513
+ return new VersionShareResolution(copyJsonObject(options.versionShareByMajor), "versionShareByMajor", null);
514
+ }
515
+
516
+ if (!isBlank(options.versionShareApiUrl)) {
517
+ String apiUrl = options.versionShareApiUrl;
518
+
519
+ if (
520
+ cachedVersionShareByMajor != null &&
521
+ !isBlank(cachedVersionShareApiUrl) &&
522
+ apiUrl.equals(cachedVersionShareApiUrl) &&
523
+ (System.currentTimeMillis() - cachedVersionShareTimestamp) < LATEST_VERSION_CACHE_TTL_MS
524
+ ) {
525
+ return new VersionShareResolution(copyJsonObject(cachedVersionShareByMajor), "versionShareApiUrl", null);
526
+ }
527
+
528
+ try {
529
+ String payload = readUrl(apiUrl);
530
+ JSONObject shareByMajor = extractVersionShareByMajor(payload);
531
+ if (shareByMajor == null || shareByMajor.length() == 0) {
532
+ return new VersionShareResolution(null, "versionShareApiUrl", "Version-share API returned no compatible dataset.");
533
+ }
534
+
535
+ cachedVersionShareByMajor = copyJsonObject(shareByMajor);
536
+ cachedVersionShareApiUrl = apiUrl;
537
+ cachedVersionShareTimestamp = System.currentTimeMillis();
538
+
539
+ return new VersionShareResolution(shareByMajor, "versionShareApiUrl", null);
540
+ } catch (Exception error) {
541
+ return new VersionShareResolution(
542
+ null,
543
+ "versionShareApiUrl",
544
+ "Could not resolve version-share dataset: " + error.getMessage()
545
+ );
546
+ }
547
+ }
548
+
549
+ JSONObject defaultDataset = GeneratedVersionShareData.getDefaultVersionShareByMajor();
550
+ if (defaultDataset != null && defaultDataset.length() > 0) {
551
+ return new VersionShareResolution(defaultDataset, GeneratedVersionShareData.getDataSource(), null);
552
+ }
553
+
554
+ return new VersionShareResolution(
555
+ null,
556
+ GeneratedVersionShareData.getDataSource(),
557
+ "Built-in version-share dataset is unavailable."
558
+ );
559
+ }
560
+
561
+ @Nullable
562
+ private Double readShareForMajor(@Nullable JSONObject shareByMajor, int majorVersion) {
563
+ if (shareByMajor == null) {
564
+ return null;
565
+ }
566
+
567
+ String directKey = String.valueOf(majorVersion);
568
+ if (shareByMajor.has(directKey) && !shareByMajor.isNull(directKey)) {
569
+ return parsePercentFromAny(shareByMajor.opt(directKey));
570
+ }
571
+
572
+ Iterator<String> keys = shareByMajor.keys();
573
+ while (keys.hasNext()) {
574
+ String key = keys.next();
575
+ Integer mappedMajor = parseMajorFromShareKey(key);
576
+ if (mappedMajor != null && mappedMajor == majorVersion) {
577
+ Double share = parsePercentFromAny(shareByMajor.opt(key));
578
+ if (share != null) {
579
+ return share;
580
+ }
581
+ }
582
+ }
583
+
584
+ return null;
585
+ }
586
+
395
587
  private String readUrl(String urlValue) throws IOException {
396
588
  HttpURLConnection connection = (HttpURLConnection) new URL(urlValue).openConnection();
397
589
  connection.setRequestMethod("GET");
@@ -417,6 +609,223 @@ public class WebviewVersionCheckerPlugin extends Plugin {
417
609
  }
418
610
  }
419
611
 
612
+ @Nullable
613
+ private JSONObject extractVersionShareByMajor(String payload) throws Exception {
614
+ JSONObject json = new JSONObject(payload);
615
+ return extractVersionShareByMajor(json);
616
+ }
617
+
618
+ @Nullable
619
+ private JSONObject extractVersionShareByMajor(JSONObject json) {
620
+ JSONObject out = new JSONObject();
621
+
622
+ JSONObject explicitMap = json.optJSONObject("versionShareByMajor");
623
+ if (explicitMap != null) {
624
+ mergeVersionShareMap(out, explicitMap);
625
+ }
626
+
627
+ if (out.length() == 0) {
628
+ JSONObject secondaryMap = json.optJSONObject("shareByMajor");
629
+ if (secondaryMap != null) {
630
+ mergeVersionShareMap(out, secondaryMap);
631
+ }
632
+ }
633
+
634
+ if (out.length() == 0) {
635
+ JSONArray versionsArray = json.optJSONArray("versions");
636
+ if (versionsArray != null) {
637
+ mergeVersionShareArray(out, versionsArray);
638
+ }
639
+ }
640
+
641
+ if (out.length() == 0) {
642
+ JSONArray dataArray = json.optJSONArray("data");
643
+ if (dataArray != null) {
644
+ mergeVersionShareArray(out, dataArray);
645
+ }
646
+ }
647
+
648
+ if (out.length() == 0) {
649
+ JSONObject caniuseShareByMajor = extractCaniuseVersionShareByMajor(json);
650
+ if (caniuseShareByMajor != null) {
651
+ mergeVersionShareMap(out, caniuseShareByMajor);
652
+ }
653
+ }
654
+
655
+ if (out.length() == 0) {
656
+ mergeVersionShareMap(out, json);
657
+ }
658
+
659
+ if (out.length() == 0) {
660
+ return null;
661
+ }
662
+ return out;
663
+ }
664
+
665
+ @Nullable
666
+ private JSONObject extractCaniuseVersionShareByMajor(JSONObject json) {
667
+ JSONObject agents = json.optJSONObject("agents");
668
+ if (agents == null) {
669
+ return null;
670
+ }
671
+
672
+ JSONObject out = new JSONObject();
673
+
674
+ // Prefer Android-specific buckets first.
675
+ mergeVersionShareMap(out, getUsageGlobalMap(agents, "and_chr"));
676
+ mergeVersionShareMap(out, getUsageGlobalMap(agents, "android"));
677
+
678
+ // If Android-specific buckets are sparse, complete with Chrome global data.
679
+ if (out.length() < 5) {
680
+ mergeVersionShareMap(out, getUsageGlobalMap(agents, "chrome"));
681
+ }
682
+
683
+ return out.length() > 0 ? out : null;
684
+ }
685
+
686
+ @Nullable
687
+ private JSONObject getUsageGlobalMap(JSONObject agents, String agentKey) {
688
+ JSONObject agent = agents.optJSONObject(agentKey);
689
+ if (agent == null) {
690
+ return null;
691
+ }
692
+ return agent.optJSONObject("usage_global");
693
+ }
694
+
695
+ private void mergeVersionShareMap(JSONObject target, JSONObject source) {
696
+ if (source == null) {
697
+ return;
698
+ }
699
+ Iterator<String> keys = source.keys();
700
+ while (keys.hasNext()) {
701
+ String key = keys.next();
702
+ Integer majorVersion = parseMajorFromShareKey(key);
703
+ Double sharePercent = parsePercentFromAny(source.opt(key));
704
+ putShareEntry(target, majorVersion, sharePercent);
705
+ }
706
+ }
707
+
708
+ private void mergeVersionShareArray(JSONObject target, JSONArray source) {
709
+ for (int index = 0; index < source.length(); index++) {
710
+ JSONObject item = source.optJSONObject(index);
711
+ if (item == null) {
712
+ continue;
713
+ }
714
+
715
+ Integer majorVersion = parseMajorFromAny(item.opt("major"));
716
+ if (majorVersion == null) {
717
+ majorVersion = parseMajorFromAny(item.opt("version"));
718
+ }
719
+ if (majorVersion == null) {
720
+ majorVersion = parseMajorFromAny(item.opt("name"));
721
+ }
722
+
723
+ Double sharePercent = parsePercentFromAny(item.opt("share"));
724
+ if (sharePercent == null) {
725
+ sharePercent = parsePercentFromAny(item.opt("sharePercent"));
726
+ }
727
+ if (sharePercent == null) {
728
+ sharePercent = parsePercentFromAny(item.opt("percent"));
729
+ }
730
+ if (sharePercent == null) {
731
+ sharePercent = parsePercentFromAny(item.opt("usage"));
732
+ }
733
+ if (sharePercent == null) {
734
+ sharePercent = parsePercentFromAny(item.opt("value"));
735
+ }
736
+
737
+ putShareEntry(target, majorVersion, sharePercent);
738
+ }
739
+ }
740
+
741
+ private void putShareEntry(JSONObject target, @Nullable Integer majorVersion, @Nullable Double sharePercent) {
742
+ if (majorVersion == null || sharePercent == null) {
743
+ return;
744
+ }
745
+ try {
746
+ target.put(String.valueOf(majorVersion), sharePercent);
747
+ } catch (Exception ignored) {
748
+ // Ignore invalid JSON insertions for malformed input.
749
+ }
750
+ }
751
+
752
+ @Nullable
753
+ private Integer parseMajorFromShareKey(@Nullable String key) {
754
+ if (isBlank(key)) {
755
+ return null;
756
+ }
757
+
758
+ String normalized = key.trim();
759
+ char first = normalized.charAt(0);
760
+ if (Character.isDigit(first)) {
761
+ return versionChecker.parseMajorVersion(normalized);
762
+ }
763
+
764
+ if ((first == 'v' || first == 'V') && normalized.length() > 1 && Character.isDigit(normalized.charAt(1))) {
765
+ return versionChecker.parseMajorVersion(normalized.substring(1));
766
+ }
767
+
768
+ return null;
769
+ }
770
+
771
+ @Nullable
772
+ private Integer parseMajorFromAny(@Nullable Object value) {
773
+ if (value == null) {
774
+ return null;
775
+ }
776
+
777
+ if (value instanceof Number) {
778
+ int major = ((Number) value).intValue();
779
+ return major >= 0 ? major : null;
780
+ }
781
+
782
+ if (value instanceof String) {
783
+ return versionChecker.parseMajorVersion((String) value);
784
+ }
785
+
786
+ return null;
787
+ }
788
+
789
+ @Nullable
790
+ private Double parsePercentFromAny(@Nullable Object value) {
791
+ if (value == null) {
792
+ return null;
793
+ }
794
+
795
+ Double percent = null;
796
+ if (value instanceof Number) {
797
+ percent = ((Number) value).doubleValue();
798
+ } else if (value instanceof String) {
799
+ try {
800
+ percent = Double.parseDouble(((String) value).trim());
801
+ } catch (NumberFormatException ignored) {
802
+ return null;
803
+ }
804
+ }
805
+
806
+ return normalizePercentValue(percent);
807
+ }
808
+
809
+ @Nullable
810
+ private Double normalizePercentValue(@Nullable Double value) {
811
+ if (value == null || value.isNaN() || value.isInfinite()) {
812
+ return null;
813
+ }
814
+ return Math.max(0.0, Math.min(100.0, value));
815
+ }
816
+
817
+ @Nullable
818
+ private JSONObject copyJsonObject(@Nullable JSONObject value) {
819
+ if (value == null) {
820
+ return null;
821
+ }
822
+ try {
823
+ return new JSONObject(value.toString());
824
+ } catch (Exception error) {
825
+ return null;
826
+ }
827
+ }
828
+
420
829
  @Nullable
421
830
  private String extractLatestVersion(String payload) throws Exception {
422
831
  JSONObject json = new JSONObject(payload);
@@ -584,6 +993,24 @@ public class WebviewVersionCheckerPlugin extends Plugin {
584
993
  }
585
994
  }
586
995
 
996
+ private static final class VersionShareResolution {
997
+
998
+ @Nullable
999
+ private final JSONObject shareByMajor;
1000
+
1001
+ @Nullable
1002
+ private final String source;
1003
+
1004
+ @Nullable
1005
+ private final String errorMessage;
1006
+
1007
+ private VersionShareResolution(@Nullable JSONObject shareByMajor, @Nullable String source, @Nullable String errorMessage) {
1008
+ this.shareByMajor = shareByMajor;
1009
+ this.source = source;
1010
+ this.errorMessage = errorMessage;
1011
+ }
1012
+ }
1013
+
587
1014
  private static final class PromptOptions {
588
1015
 
589
1016
  @Nullable
@@ -636,6 +1063,7 @@ public class WebviewVersionCheckerPlugin extends Plugin {
636
1063
  private boolean autoCheckOnResume = true;
637
1064
  private boolean autoPromptOnOutdated;
638
1065
  private boolean showPromptOnOutdated;
1066
+ private boolean autoPromptDismissible = true;
639
1067
 
640
1068
  @Nullable
641
1069
  private String latestVersion;
@@ -643,6 +1071,15 @@ public class WebviewVersionCheckerPlugin extends Plugin {
643
1071
  @Nullable
644
1072
  private Integer minimumMajorVersion;
645
1073
 
1074
+ @Nullable
1075
+ private Double minimumDeviceSharePercent = 3.0;
1076
+
1077
+ @Nullable
1078
+ private JSONObject versionShareByMajor;
1079
+
1080
+ @Nullable
1081
+ private String versionShareApiUrl;
1082
+
646
1083
  @Nullable
647
1084
  private String latestVersionApiUrl;
648
1085
 
@@ -666,11 +1103,15 @@ public class WebviewVersionCheckerPlugin extends Plugin {
666
1103
  out.autoCheckOnLoad = config.getBoolean("autoCheckOnLoad", true);
667
1104
  out.autoCheckOnResume = config.getBoolean("autoCheckOnResume", true);
668
1105
  out.autoPromptOnOutdated = config.getBoolean("autoPromptOnOutdated", false);
1106
+ out.autoPromptDismissible = config.getBoolean("autoPromptDismissible", true);
669
1107
  out.latestVersion = emptyToNull(config.getString("latestVersion"));
670
- Integer minimum = config.getConfigJSON().has("minimumMajorVersion")
671
- ? config.getInt("minimumMajorVersion", 0)
672
- : null;
1108
+ Integer minimum = config.getConfigJSON().has("minimumMajorVersion") ? config.getInt("minimumMajorVersion", 0) : null;
673
1109
  out.minimumMajorVersion = minimum;
1110
+ if (config.getConfigJSON().has("minimumDeviceSharePercent")) {
1111
+ out.minimumDeviceSharePercent = normalizePercent(readOptionalDouble(config.getConfigJSON(), "minimumDeviceSharePercent"));
1112
+ }
1113
+ out.versionShareByMajor = copyJsonObject(config.getObject("versionShareByMajor"));
1114
+ out.versionShareApiUrl = emptyToNull(config.getString("versionShareApiUrl"));
674
1115
  out.latestVersionApiUrl = emptyToNull(config.getString("latestVersionApiUrl"));
675
1116
  out.updateUrl = emptyToNull(config.getString("updateUrl"));
676
1117
  out.promptTitle = emptyToNull(config.getString("promptTitle"));
@@ -692,6 +1133,9 @@ public class WebviewVersionCheckerPlugin extends Plugin {
692
1133
  if (call.hasOption("autoPromptOnOutdated")) {
693
1134
  out.autoPromptOnOutdated = call.getBoolean("autoPromptOnOutdated", out.autoPromptOnOutdated);
694
1135
  }
1136
+ if (call.hasOption("autoPromptDismissible")) {
1137
+ out.autoPromptDismissible = call.getBoolean("autoPromptDismissible", out.autoPromptDismissible);
1138
+ }
695
1139
  if (call.hasOption("showPromptOnOutdated")) {
696
1140
  out.showPromptOnOutdated = call.getBoolean("showPromptOnOutdated", out.showPromptOnOutdated);
697
1141
  }
@@ -701,6 +1145,15 @@ public class WebviewVersionCheckerPlugin extends Plugin {
701
1145
  if (call.hasOption("minimumMajorVersion")) {
702
1146
  out.minimumMajorVersion = call.getInt("minimumMajorVersion");
703
1147
  }
1148
+ if (call.hasOption("minimumDeviceSharePercent")) {
1149
+ out.minimumDeviceSharePercent = normalizePercent(call.getDouble("minimumDeviceSharePercent"));
1150
+ }
1151
+ if (call.hasOption("versionShareByMajor")) {
1152
+ out.versionShareByMajor = copyJsonObject(call.getObject("versionShareByMajor"));
1153
+ }
1154
+ if (call.hasOption("versionShareApiUrl")) {
1155
+ out.versionShareApiUrl = emptyToNull(call.getString("versionShareApiUrl"));
1156
+ }
704
1157
  if (call.hasOption("latestVersionApiUrl")) {
705
1158
  out.latestVersionApiUrl = emptyToNull(call.getString("latestVersionApiUrl"));
706
1159
  }
@@ -728,9 +1181,13 @@ public class WebviewVersionCheckerPlugin extends Plugin {
728
1181
  out.autoCheckOnLoad = autoCheckOnLoad;
729
1182
  out.autoCheckOnResume = autoCheckOnResume;
730
1183
  out.autoPromptOnOutdated = autoPromptOnOutdated;
1184
+ out.autoPromptDismissible = autoPromptDismissible;
731
1185
  out.showPromptOnOutdated = showPromptOnOutdated;
732
1186
  out.latestVersion = latestVersion;
733
1187
  out.minimumMajorVersion = minimumMajorVersion;
1188
+ out.minimumDeviceSharePercent = minimumDeviceSharePercent;
1189
+ out.versionShareByMajor = copyJsonObject(versionShareByMajor);
1190
+ out.versionShareApiUrl = versionShareApiUrl;
734
1191
  out.latestVersionApiUrl = latestVersionApiUrl;
735
1192
  out.updateUrl = updateUrl;
736
1193
  out.promptTitle = promptTitle;
@@ -747,5 +1204,47 @@ public class WebviewVersionCheckerPlugin extends Plugin {
747
1204
  }
748
1205
  return value;
749
1206
  }
1207
+
1208
+ @Nullable
1209
+ private static Double readOptionalDouble(JSONObject source, String key) {
1210
+ if (!source.has(key) || source.isNull(key)) {
1211
+ return null;
1212
+ }
1213
+
1214
+ Object value = source.opt(key);
1215
+ if (value instanceof Number) {
1216
+ return ((Number) value).doubleValue();
1217
+ }
1218
+
1219
+ if (value instanceof String) {
1220
+ try {
1221
+ return Double.parseDouble(((String) value).trim());
1222
+ } catch (NumberFormatException ignored) {
1223
+ return null;
1224
+ }
1225
+ }
1226
+
1227
+ return null;
1228
+ }
1229
+
1230
+ @Nullable
1231
+ private static Double normalizePercent(@Nullable Double value) {
1232
+ if (value == null || value.isNaN() || value.isInfinite()) {
1233
+ return null;
1234
+ }
1235
+ return Math.max(0.0, Math.min(100.0, value));
1236
+ }
1237
+
1238
+ @Nullable
1239
+ private static JSONObject copyJsonObject(@Nullable JSONObject value) {
1240
+ if (value == null || value.length() == 0) {
1241
+ return null;
1242
+ }
1243
+ try {
1244
+ return new JSONObject(value.toString());
1245
+ } catch (Exception error) {
1246
+ return null;
1247
+ }
1248
+ }
750
1249
  }
751
1250
  }