@capgo/capacitor-updater 7.45.10 → 7.50.1
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/Package.swift +1 -1
- package/README.md +510 -92
- package/android/src/main/java/ee/forgr/capacitor_updater/AndroidAppExitReporter.java +92 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +3182 -779
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +798 -299
- package/android/src/main/java/ee/forgr/capacitor_updater/DeviceIdHelper.java +45 -30
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +46 -28
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +2 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeDetector.java +3 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +359 -25
- package/android/src/main/java/ee/forgr/capacitor_updater/ThreeFingerPinchDetector.java +323 -0
- package/dist/docs.json +1283 -169
- package/dist/esm/definitions.d.ts +621 -44
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +11 -1
- package/dist/esm/web.js +59 -1
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +59 -1
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +59 -1
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/AES.swift +0 -1
- package/ios/Sources/CapacitorUpdaterPlugin/AppHealthTracker.swift +82 -0
- package/ios/Sources/CapacitorUpdaterPlugin/BigInt.swift +0 -16
- package/ios/Sources/CapacitorUpdaterPlugin/BundleInfo.swift +2 -2
- package/ios/Sources/CapacitorUpdaterPlugin/BundleStatus.swift +78 -2
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +2116 -223
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +997 -332
- package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +0 -1
- package/ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift +78 -1
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +418 -36
- package/ios/Sources/CapacitorUpdaterPlugin/WebViewStatsReporter.swift +276 -0
- package/package.json +15 -3
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
package ee.forgr.capacitor_updater;
|
|
8
8
|
|
|
9
9
|
import android.app.Activity;
|
|
10
|
+
import android.app.AlertDialog;
|
|
10
11
|
import android.content.Context;
|
|
11
12
|
import android.content.Intent;
|
|
12
13
|
import android.content.SharedPreferences;
|
|
@@ -20,6 +21,7 @@ import android.os.Looper;
|
|
|
20
21
|
import android.view.Gravity;
|
|
21
22
|
import android.view.View;
|
|
22
23
|
import android.view.ViewGroup;
|
|
24
|
+
import android.webkit.RenderProcessGoneDetail;
|
|
23
25
|
import android.widget.FrameLayout;
|
|
24
26
|
import android.widget.ProgressBar;
|
|
25
27
|
import androidx.core.content.pm.PackageInfoCompat;
|
|
@@ -32,6 +34,7 @@ import com.getcapacitor.PluginCall;
|
|
|
32
34
|
import com.getcapacitor.PluginHandle;
|
|
33
35
|
import com.getcapacitor.PluginMethod;
|
|
34
36
|
import com.getcapacitor.PluginResult;
|
|
37
|
+
import com.getcapacitor.WebViewListener;
|
|
35
38
|
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
36
39
|
import com.getcapacitor.plugin.WebView;
|
|
37
40
|
import com.google.android.gms.tasks.Task;
|
|
@@ -46,16 +49,23 @@ import com.google.android.play.core.install.model.AppUpdateType;
|
|
|
46
49
|
import com.google.android.play.core.install.model.InstallStatus;
|
|
47
50
|
import com.google.android.play.core.install.model.UpdateAvailability;
|
|
48
51
|
import io.github.g00fy2.versioncompare.Version;
|
|
52
|
+
import java.io.ByteArrayOutputStream;
|
|
49
53
|
import java.io.IOException;
|
|
54
|
+
import java.io.InputStream;
|
|
55
|
+
import java.net.HttpURLConnection;
|
|
50
56
|
import java.net.MalformedURLException;
|
|
51
57
|
import java.net.URL;
|
|
58
|
+
import java.nio.charset.StandardCharsets;
|
|
59
|
+
import java.text.SimpleDateFormat;
|
|
52
60
|
import java.util.ArrayList;
|
|
53
61
|
import java.util.Date;
|
|
62
|
+
import java.util.HashMap;
|
|
54
63
|
import java.util.HashSet;
|
|
55
64
|
import java.util.List;
|
|
56
65
|
import java.util.Map;
|
|
57
66
|
import java.util.Objects;
|
|
58
67
|
import java.util.Set;
|
|
68
|
+
import java.util.TimeZone;
|
|
59
69
|
import java.util.Timer;
|
|
60
70
|
import java.util.TimerTask;
|
|
61
71
|
import java.util.concurrent.Phaser;
|
|
@@ -71,6 +81,16 @@ import org.json.JSONObject;
|
|
|
71
81
|
@CapacitorPlugin(name = "CapacitorUpdater")
|
|
72
82
|
public class CapacitorUpdaterPlugin extends Plugin {
|
|
73
83
|
|
|
84
|
+
static final String SHAKE_MENU_GESTURE_SHAKE = "shake";
|
|
85
|
+
static final String SHAKE_MENU_GESTURE_THREE_FINGER_PINCH = "threeFingerPinch";
|
|
86
|
+
|
|
87
|
+
private static final String AUTO_UPDATE_MODE_OFF = "off";
|
|
88
|
+
private static final String AUTO_UPDATE_MODE_BACKGROUND = "atBackground";
|
|
89
|
+
private static final String AUTO_UPDATE_MODE_INSTALL = "atInstall";
|
|
90
|
+
private static final String AUTO_UPDATE_MODE_LAUNCH = "onLaunch";
|
|
91
|
+
private static final String AUTO_UPDATE_MODE_ALWAYS = "always";
|
|
92
|
+
private static final String AUTO_UPDATE_MODE_ONLY_DOWNLOAD = "onlyDownload";
|
|
93
|
+
|
|
74
94
|
private Logger logger;
|
|
75
95
|
|
|
76
96
|
private static final String updateUrlDefault = "https://plugin.capgo.app/updates";
|
|
@@ -82,18 +102,52 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
82
102
|
private static final String STATS_URL_PREF_KEY = "CapacitorUpdater.statsUrl";
|
|
83
103
|
private static final String CHANNEL_URL_PREF_KEY = "CapacitorUpdater.channelUrl";
|
|
84
104
|
private static final String DEFAULT_CHANNEL_PREF_KEY = "CapacitorUpdater.defaultChannel";
|
|
105
|
+
private static final String PREVIEW_SESSION_PREF_KEY = "CapacitorUpdater.previewSession";
|
|
106
|
+
private static final String PREVIEW_PREVIOUS_SHAKE_MENU_PREF_KEY = "CapacitorUpdater.previewPreviousShakeMenu";
|
|
107
|
+
private static final String PREVIEW_PREVIOUS_SHAKE_CHANNEL_SELECTOR_PREF_KEY = "CapacitorUpdater.previewPreviousShakeChannelSelector";
|
|
108
|
+
private static final String PREVIEW_PREVIOUS_NEXT_BUNDLE_PREF_KEY = "CapacitorUpdater.previewPreviousNextBundle";
|
|
109
|
+
private static final String PREVIEW_PREVIOUS_APP_ID_PREF_KEY = "CapacitorUpdater.previewPreviousAppId";
|
|
110
|
+
private static final String PREVIEW_PREVIOUS_DEFAULT_CHANNEL_PREF_KEY = "CapacitorUpdater.previewPreviousDefaultChannel";
|
|
111
|
+
private static final String PREVIEW_PREVIOUS_DEFAULT_CHANNEL_WAS_SET_PREF_KEY = "CapacitorUpdater.previewPreviousDefaultChannelWasSet";
|
|
112
|
+
private static final String PREVIEW_APP_ID_PREF_KEY = "CapacitorUpdater.previewAppId";
|
|
113
|
+
private static final String PREVIEW_PAYLOAD_URL_PREF_KEY = "CapacitorUpdater.previewPayloadUrl";
|
|
114
|
+
private static final String PREVIEW_NAME_PREF_KEY = "CapacitorUpdater.previewName";
|
|
115
|
+
private static final String PREVIEW_SOURCE_PREF_KEY = "CapacitorUpdater.previewSource";
|
|
116
|
+
private static final String PREVIEW_SESSIONS_PREF_KEY = "CapacitorUpdater.previewSessions";
|
|
117
|
+
private static final String PREVIEW_SESSION_ALERT_PENDING_PREF_KEY = "CapacitorUpdater.previewSessionAlertPending";
|
|
85
118
|
private static final String[] BREAKING_EVENT_NAMES = { "breakingAvailable", "majorAvailable" };
|
|
86
119
|
private static final String LAST_FAILED_BUNDLE_PREF_KEY = "CapacitorUpdater.lastFailedBundle";
|
|
120
|
+
private static final String LAST_REPORTED_APP_EXIT_TIMESTAMP_PREF_KEY = "CapacitorUpdater.lastReportedAppExitTimestamp";
|
|
121
|
+
private static final String LAST_WEBVIEW_RENDER_PROCESS_GONE_PREF_KEY = "CapacitorUpdater.lastWebViewRenderProcessGone";
|
|
122
|
+
private static final String LAST_VERSION_OS_PREF_KEY = "CapacitorUpdater.lastVersionOs";
|
|
123
|
+
private static final String LAST_VERSION_BUILD_PREF_KEY = "CapacitorUpdater.lastVersionBuild";
|
|
124
|
+
private static final String LAST_VERSION_CODE_PREF_KEY = "CapacitorUpdater.lastVersionCode";
|
|
125
|
+
private static final String OS_VERSION_CHANGED_ACTION = "os_version_changed";
|
|
126
|
+
private static final String NATIVE_APP_VERSION_CHANGED_ACTION = "native_app_version_changed";
|
|
87
127
|
private static final String SPLASH_SCREEN_PLUGIN_ID = "SplashScreen";
|
|
88
128
|
private static final int SPLASH_SCREEN_RETRY_DELAY_MS = 100;
|
|
89
129
|
private static final int SPLASH_SCREEN_MAX_RETRIES = 20;
|
|
90
130
|
private static final long PENDING_BUNDLE_APP_READY_MIN_TIMEOUT_MS = 30000L;
|
|
91
|
-
|
|
92
|
-
|
|
131
|
+
private static final long PREVIEW_TRANSITION_LOADER_TIMEOUT_MS = 60000L;
|
|
132
|
+
static final int APPLICATION_EXIT_REASON_UNKNOWN = 0;
|
|
133
|
+
static final int APPLICATION_EXIT_REASON_EXIT_SELF = 1;
|
|
134
|
+
static final int APPLICATION_EXIT_REASON_SIGNALED = 2;
|
|
135
|
+
static final int APPLICATION_EXIT_REASON_LOW_MEMORY = 3;
|
|
136
|
+
static final int APPLICATION_EXIT_REASON_CRASH = 4;
|
|
137
|
+
static final int APPLICATION_EXIT_REASON_CRASH_NATIVE = 5;
|
|
138
|
+
static final int APPLICATION_EXIT_REASON_ANR = 6;
|
|
139
|
+
static final int APPLICATION_EXIT_REASON_INITIALIZATION_FAILURE = 7;
|
|
140
|
+
static final int APPLICATION_EXIT_REASON_PERMISSION_CHANGE = 8;
|
|
141
|
+
static final int APPLICATION_EXIT_REASON_EXCESSIVE_RESOURCE_USAGE = 9;
|
|
142
|
+
static final int APPLICATION_EXIT_REASON_USER_REQUESTED = 10;
|
|
143
|
+
static final int APPLICATION_EXIT_REASON_DEPENDENCY_DIED = 12;
|
|
144
|
+
|
|
145
|
+
private final String pluginVersion = "7.50.1";
|
|
93
146
|
private static final String DELAY_CONDITION_PREFERENCES = "";
|
|
94
147
|
|
|
95
148
|
private SharedPreferences.Editor editor;
|
|
96
149
|
private SharedPreferences prefs;
|
|
150
|
+
private final Object previewSessionsLock = new Object();
|
|
97
151
|
protected CapgoUpdater implementation;
|
|
98
152
|
private Boolean persistCustomId = false;
|
|
99
153
|
private Boolean persistModifyUrl = false;
|
|
@@ -103,6 +157,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
103
157
|
private Boolean autoDeleteFailed = true;
|
|
104
158
|
private Boolean autoDeletePrevious = true;
|
|
105
159
|
private Boolean autoUpdate = false;
|
|
160
|
+
private String autoUpdateMode = AUTO_UPDATE_MODE_OFF;
|
|
106
161
|
private String updateUrl = "";
|
|
107
162
|
private Version currentVersionNative;
|
|
108
163
|
private String currentBuildVersion;
|
|
@@ -119,20 +174,49 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
119
174
|
private volatile boolean onLaunchDirectUpdateUsed = false;
|
|
120
175
|
Boolean shakeMenuEnabled = false;
|
|
121
176
|
Boolean shakeChannelSelectorEnabled = false;
|
|
177
|
+
String shakeMenuGesture = SHAKE_MENU_GESTURE_SHAKE;
|
|
178
|
+
volatile Boolean previewSessionEnabled = false;
|
|
179
|
+
private Boolean previewSessionAlertPending = false;
|
|
180
|
+
private volatile Boolean isLeavingPreviewForIncomingLink = false;
|
|
122
181
|
private Boolean allowManualBundleError = false;
|
|
182
|
+
private Boolean allowPreview = false;
|
|
123
183
|
Boolean allowSetDefaultChannel = true;
|
|
124
184
|
|
|
125
185
|
String getUpdateUrl() {
|
|
126
186
|
return this.updateUrl;
|
|
127
187
|
}
|
|
128
188
|
|
|
189
|
+
private boolean isPreviewSessionStateActive() {
|
|
190
|
+
return (
|
|
191
|
+
Boolean.TRUE.equals(this.previewSessionEnabled) ||
|
|
192
|
+
Boolean.TRUE.equals(this.isLeavingPreviewForIncomingLink) ||
|
|
193
|
+
(this.implementation != null && this.implementation.previewSession)
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private boolean shouldBlockAutoUpdateForPreviewSession() {
|
|
198
|
+
if (!this.isPreviewSessionStateActive()) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
logger.info("Preview session is active. Skipping normal auto-update work.");
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private void clearIncomingPreviewTransition() {
|
|
207
|
+
this.isLeavingPreviewForIncomingLink = false;
|
|
208
|
+
if (!Boolean.TRUE.equals(this.previewSessionEnabled) && this.implementation != null) {
|
|
209
|
+
this.implementation.previewSession = false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
129
213
|
// Used for activity-based foreground/background detection on Android < 14
|
|
130
214
|
private Boolean isPreviousMainActivity = true;
|
|
131
215
|
|
|
132
216
|
private volatile Thread backgroundDownloadTask;
|
|
133
217
|
private volatile Thread appReadyCheck;
|
|
134
218
|
private volatile long downloadStartTimeMs = 0;
|
|
135
|
-
private static final long DOWNLOAD_TIMEOUT_MS =
|
|
219
|
+
private static final long DOWNLOAD_TIMEOUT_MS = 600000; // 10 minute timeout
|
|
136
220
|
|
|
137
221
|
private final Phaser semaphoreReady = new Phaser(0) {
|
|
138
222
|
@Override
|
|
@@ -154,6 +238,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
154
238
|
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
|
155
239
|
private FrameLayout splashscreenLoaderOverlay;
|
|
156
240
|
private Runnable splashscreenTimeoutRunnable;
|
|
241
|
+
private FrameLayout previewTransitionLoaderOverlay;
|
|
242
|
+
private Runnable previewTransitionLoaderTimeoutRunnable;
|
|
243
|
+
private boolean previewTransitionLoaderRequested = false;
|
|
244
|
+
private WebViewListener webViewStatsListener;
|
|
157
245
|
|
|
158
246
|
private static final class FireAndForgetPluginCall extends PluginCall {
|
|
159
247
|
|
|
@@ -210,6 +298,26 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
210
298
|
}
|
|
211
299
|
}
|
|
212
300
|
|
|
301
|
+
private boolean shouldNotifyBreakingEvents(final JSObject response) {
|
|
302
|
+
if (response == null) {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (response.optBoolean("breaking", false)) {
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
final String error = response.optString("error", "");
|
|
311
|
+
final String message = response.optString("message", "");
|
|
312
|
+
return "disable_auto_update_to_major".equals(error) || "store_update_required".equals(message);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private void notifyBreakingEventsIfNeeded(final JSObject response, final String version) {
|
|
316
|
+
if (shouldNotifyBreakingEvents(response)) {
|
|
317
|
+
notifyBreakingEvents(version);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
213
321
|
private void persistLastFailedBundle(BundleInfo bundle) {
|
|
214
322
|
if (this.prefs == null) {
|
|
215
323
|
return;
|
|
@@ -241,6 +349,296 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
241
349
|
}
|
|
242
350
|
}
|
|
243
351
|
|
|
352
|
+
private String nowIsoString() {
|
|
353
|
+
final SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", java.util.Locale.US);
|
|
354
|
+
formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
|
|
355
|
+
return formatter.format(new Date());
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private String normalizedPreviewMetadataValue(final String rawValue) {
|
|
359
|
+
if (rawValue == null) {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
final String value = rawValue.trim();
|
|
364
|
+
if (value.isEmpty()) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
final String lowercased = value.toLowerCase(java.util.Locale.ROOT);
|
|
369
|
+
if ("undefined".equals(lowercased) || "null".equals(lowercased)) {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return value;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private JSONObject previewSessionsJson() {
|
|
377
|
+
final String raw = this.prefs == null ? null : this.prefs.getString(PREVIEW_SESSIONS_PREF_KEY, null);
|
|
378
|
+
if (raw == null || raw.trim().isEmpty()) {
|
|
379
|
+
return new JSONObject();
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
382
|
+
return new JSONObject(raw);
|
|
383
|
+
} catch (final JSONException e) {
|
|
384
|
+
logger.warn("Could not parse preview sessions, clearing them: " + e.getMessage());
|
|
385
|
+
this.editor.remove(PREVIEW_SESSIONS_PREF_KEY).apply();
|
|
386
|
+
return new JSONObject();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private void savePreviewSessionsJson(final JSONObject sessions) {
|
|
391
|
+
this.editor.putString(PREVIEW_SESSIONS_PREF_KEY, sessions.toString()).apply();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private boolean hasSavedPreviewSessions() {
|
|
395
|
+
synchronized (this.previewSessionsLock) {
|
|
396
|
+
return this.previewSessionsJson().length() > 0;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private String metadataString(final JSONObject metadata, final String key) {
|
|
401
|
+
return this.normalizedPreviewMetadataValue(metadata.optString(key, null));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private String currentPreviewMetadataValue(final String key) {
|
|
405
|
+
return this.normalizedPreviewMetadataValue(this.prefs.getString(key, null));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private Set<String> availableBundleIds() {
|
|
409
|
+
final Set<String> ids = new HashSet<>();
|
|
410
|
+
for (final BundleInfo bundle : this.implementation.list(false)) {
|
|
411
|
+
ids.add(bundle.getId());
|
|
412
|
+
}
|
|
413
|
+
return ids;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private JSObject previewInfo(
|
|
417
|
+
final String id,
|
|
418
|
+
final JSONObject metadata,
|
|
419
|
+
final Set<String> availableBundleIds,
|
|
420
|
+
final String currentBundleId
|
|
421
|
+
) throws JSONException {
|
|
422
|
+
final BundleInfo bundle = this.implementation.getBundleInfo(id);
|
|
423
|
+
if (!bundle.isBuiltin() && !availableBundleIds.contains(id)) {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
if (bundle.isDeleted() || bundle.isErrorStatus()) {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
final String now = this.nowIsoString();
|
|
431
|
+
final JSObject info = new JSObject();
|
|
432
|
+
info.put("id", id);
|
|
433
|
+
info.put("bundle", InternalUtils.mapToJSObject(bundle.toJSONMap()));
|
|
434
|
+
info.put("createdAt", Objects.requireNonNullElse(this.metadataString(metadata, "createdAt"), now));
|
|
435
|
+
info.put("updatedAt", Objects.requireNonNullElse(this.metadataString(metadata, "updatedAt"), now));
|
|
436
|
+
info.put("lastUsedAt", Objects.requireNonNullElse(this.metadataString(metadata, "lastUsedAt"), now));
|
|
437
|
+
info.put("isActive", Boolean.TRUE.equals(this.previewSessionEnabled) && id.equals(currentBundleId));
|
|
438
|
+
|
|
439
|
+
for (final String key : new String[] { "name", "source", "appId", "payloadUrl" }) {
|
|
440
|
+
final String value = this.metadataString(metadata, key);
|
|
441
|
+
if (value != null) {
|
|
442
|
+
info.put(key, value);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return info;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
private JSArray listPreviewInfos(final boolean cleanup) {
|
|
450
|
+
synchronized (this.previewSessionsLock) {
|
|
451
|
+
final JSONObject sessions = this.previewSessionsJson();
|
|
452
|
+
final Set<String> availableBundleIds = this.availableBundleIds();
|
|
453
|
+
final String currentBundleId = this.implementation.getCurrentBundle().getId();
|
|
454
|
+
final JSArray previews = new JSArray();
|
|
455
|
+
final List<String> staleIds = new ArrayList<>();
|
|
456
|
+
|
|
457
|
+
final JSONArray names = sessions.names();
|
|
458
|
+
if (names == null) {
|
|
459
|
+
return previews;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
final List<JSObject> sortedPreviews = new ArrayList<>();
|
|
463
|
+
for (int i = 0; i < names.length(); i++) {
|
|
464
|
+
final String id = names.optString(i, "");
|
|
465
|
+
if (id.isEmpty()) {
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
final JSONObject metadata = sessions.optJSONObject(id);
|
|
469
|
+
if (metadata == null) {
|
|
470
|
+
staleIds.add(id);
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
try {
|
|
474
|
+
final JSObject info = this.previewInfo(id, metadata, availableBundleIds, currentBundleId);
|
|
475
|
+
if (info == null) {
|
|
476
|
+
staleIds.add(id);
|
|
477
|
+
} else {
|
|
478
|
+
sortedPreviews.add(info);
|
|
479
|
+
}
|
|
480
|
+
} catch (final JSONException e) {
|
|
481
|
+
logger.warn("Could not read preview metadata for " + id + ": " + e.getMessage());
|
|
482
|
+
staleIds.add(id);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
sortedPreviews.sort((first, second) -> second.optString("lastUsedAt", "").compareTo(first.optString("lastUsedAt", "")));
|
|
487
|
+
for (final JSObject preview : sortedPreviews) {
|
|
488
|
+
previews.put(preview);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (cleanup && !staleIds.isEmpty()) {
|
|
492
|
+
for (final String id : staleIds) {
|
|
493
|
+
sessions.remove(id);
|
|
494
|
+
}
|
|
495
|
+
this.savePreviewSessionsJson(sessions);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return previews;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
private JSObject storedPreviewInfo(final String id) {
|
|
503
|
+
synchronized (this.previewSessionsLock) {
|
|
504
|
+
final JSONObject metadata = this.previewSessionsJson().optJSONObject(id);
|
|
505
|
+
if (metadata == null) {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
try {
|
|
509
|
+
return this.previewInfo(id, metadata, this.availableBundleIds(), this.implementation.getCurrentBundle().getId());
|
|
510
|
+
} catch (final JSONException e) {
|
|
511
|
+
logger.warn("Could not read preview metadata for " + id + ": " + e.getMessage());
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
private JSObject recordPreviewBundle(final BundleInfo bundle) {
|
|
518
|
+
return this.recordPreviewBundle(bundle, null);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
private JSObject recordPreviewBundle(final BundleInfo bundle, final String oldId) {
|
|
522
|
+
final String now = this.nowIsoString();
|
|
523
|
+
final String id = bundle.getId();
|
|
524
|
+
synchronized (this.previewSessionsLock) {
|
|
525
|
+
final JSONObject sessions = this.previewSessionsJson();
|
|
526
|
+
JSONObject metadata = sessions.optJSONObject(id);
|
|
527
|
+
final boolean replacingPreview = oldId != null && !oldId.equals(id);
|
|
528
|
+
|
|
529
|
+
try {
|
|
530
|
+
if (metadata == null && replacingPreview) {
|
|
531
|
+
final JSONObject oldMetadata = sessions.optJSONObject(oldId);
|
|
532
|
+
if (oldMetadata != null) {
|
|
533
|
+
metadata = new JSONObject(oldMetadata.toString());
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
if (metadata == null) {
|
|
537
|
+
metadata = new JSONObject();
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (!metadata.has("createdAt")) {
|
|
541
|
+
metadata.put("createdAt", now);
|
|
542
|
+
}
|
|
543
|
+
metadata.put("updatedAt", now);
|
|
544
|
+
if (metadata.isNull("lastUsedAt") || this.implementation.getCurrentBundle().getId().equals(id)) {
|
|
545
|
+
metadata.put("lastUsedAt", now);
|
|
546
|
+
}
|
|
547
|
+
metadata.put("version", bundle.getVersionName());
|
|
548
|
+
|
|
549
|
+
if (!replacingPreview) {
|
|
550
|
+
final String appId = this.currentPreviewMetadataValue(PREVIEW_APP_ID_PREF_KEY);
|
|
551
|
+
if (appId == null) {
|
|
552
|
+
metadata.remove("appId");
|
|
553
|
+
} else {
|
|
554
|
+
metadata.put("appId", appId);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
final String payloadUrl = this.currentPreviewMetadataValue(PREVIEW_PAYLOAD_URL_PREF_KEY);
|
|
558
|
+
if (payloadUrl == null) {
|
|
559
|
+
metadata.remove("payloadUrl");
|
|
560
|
+
} else {
|
|
561
|
+
metadata.put("payloadUrl", payloadUrl);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (!replacingPreview) {
|
|
566
|
+
final String name = this.currentPreviewMetadataValue(PREVIEW_NAME_PREF_KEY);
|
|
567
|
+
if (name == null) {
|
|
568
|
+
metadata.remove("name");
|
|
569
|
+
} else {
|
|
570
|
+
metadata.put("name", name);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
final String source = this.currentPreviewMetadataValue(PREVIEW_SOURCE_PREF_KEY);
|
|
574
|
+
if (source == null) {
|
|
575
|
+
metadata.remove("source");
|
|
576
|
+
} else {
|
|
577
|
+
metadata.put("source", source);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (this.metadataString(metadata, "name") == null) {
|
|
581
|
+
metadata.put("name", bundle.getVersionName());
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (oldId != null && !oldId.equals(id)) {
|
|
585
|
+
sessions.remove(oldId);
|
|
586
|
+
}
|
|
587
|
+
sessions.put(id, metadata);
|
|
588
|
+
this.savePreviewSessionsJson(sessions);
|
|
589
|
+
|
|
590
|
+
return this.previewInfo(id, metadata, this.availableBundleIds(), this.implementation.getCurrentBundle().getId());
|
|
591
|
+
} catch (final JSONException e) {
|
|
592
|
+
logger.warn("Could not store preview metadata: " + e.getMessage());
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
final JSObject fallback = new JSObject();
|
|
597
|
+
fallback.put("id", id);
|
|
598
|
+
fallback.put("bundle", InternalUtils.mapToJSObject(bundle.toJSONMap()));
|
|
599
|
+
fallback.put("createdAt", now);
|
|
600
|
+
fallback.put("updatedAt", now);
|
|
601
|
+
fallback.put("lastUsedAt", now);
|
|
602
|
+
fallback.put(
|
|
603
|
+
"isActive",
|
|
604
|
+
Boolean.TRUE.equals(this.previewSessionEnabled) && this.implementation.getCurrentBundle().getId().equals(id)
|
|
605
|
+
);
|
|
606
|
+
return fallback;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
private void updateCurrentPreviewSessionMetadataFrom(final JSObject preview) {
|
|
610
|
+
final String appId = this.normalizedPreviewMetadataValue(preview.optString("appId", null));
|
|
611
|
+
if (appId == null) {
|
|
612
|
+
this.restorePreviewPreviousAppId();
|
|
613
|
+
this.editor.remove(PREVIEW_APP_ID_PREF_KEY);
|
|
614
|
+
} else {
|
|
615
|
+
this.setActiveAppId(appId);
|
|
616
|
+
this.editor.putString(PREVIEW_APP_ID_PREF_KEY, appId);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
final String payloadUrl = this.normalizedPreviewMetadataValue(preview.optString("payloadUrl", null));
|
|
620
|
+
if (payloadUrl == null) {
|
|
621
|
+
this.editor.remove(PREVIEW_PAYLOAD_URL_PREF_KEY);
|
|
622
|
+
} else {
|
|
623
|
+
this.editor.putString(PREVIEW_PAYLOAD_URL_PREF_KEY, payloadUrl);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
final String name = this.normalizedPreviewMetadataValue(preview.optString("name", null));
|
|
627
|
+
if (name == null) {
|
|
628
|
+
this.editor.remove(PREVIEW_NAME_PREF_KEY);
|
|
629
|
+
} else {
|
|
630
|
+
this.editor.putString(PREVIEW_NAME_PREF_KEY, name);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
final String source = this.normalizedPreviewMetadataValue(preview.optString("source", null));
|
|
634
|
+
if (source == null) {
|
|
635
|
+
this.editor.remove(PREVIEW_SOURCE_PREF_KEY);
|
|
636
|
+
} else {
|
|
637
|
+
this.editor.putString(PREVIEW_SOURCE_PREF_KEY, source);
|
|
638
|
+
}
|
|
639
|
+
this.editor.apply();
|
|
640
|
+
}
|
|
641
|
+
|
|
244
642
|
public Thread startNewThread(final Runnable function, Number waitTime) {
|
|
245
643
|
Thread bgTask = new Thread(() -> {
|
|
246
644
|
try {
|
|
@@ -309,45 +707,6 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
309
707
|
this.implementation.pluginVersion = this.pluginVersion;
|
|
310
708
|
this.implementation.versionCode = this.getVersionCode(pInfo);
|
|
311
709
|
// Removed unused OkHttpClient creation - using shared client in DownloadService instead
|
|
312
|
-
// Handle directUpdate configuration - support string values and backward compatibility
|
|
313
|
-
String directUpdateConfig = this.getConfig().getString("directUpdate", null);
|
|
314
|
-
if (directUpdateConfig != null) {
|
|
315
|
-
// Handle backward compatibility for boolean true
|
|
316
|
-
if (directUpdateConfig.equals("true")) {
|
|
317
|
-
this.directUpdateMode = "always";
|
|
318
|
-
this.implementation.directUpdate = true;
|
|
319
|
-
} else {
|
|
320
|
-
this.directUpdateMode = directUpdateConfig;
|
|
321
|
-
this.implementation.directUpdate =
|
|
322
|
-
directUpdateConfig.equals("always") ||
|
|
323
|
-
directUpdateConfig.equals("atInstall") ||
|
|
324
|
-
directUpdateConfig.equals("onLaunch");
|
|
325
|
-
// Validate directUpdate value
|
|
326
|
-
if (
|
|
327
|
-
!directUpdateConfig.equals("false") &&
|
|
328
|
-
!directUpdateConfig.equals("always") &&
|
|
329
|
-
!directUpdateConfig.equals("atInstall") &&
|
|
330
|
-
!directUpdateConfig.equals("onLaunch")
|
|
331
|
-
) {
|
|
332
|
-
logger.error(
|
|
333
|
-
"Invalid directUpdate value: \"" +
|
|
334
|
-
directUpdateConfig +
|
|
335
|
-
"\". Supported values are: \"false\", \"true\", \"always\", \"atInstall\", \"onLaunch\". Defaulting to \"false\"."
|
|
336
|
-
);
|
|
337
|
-
this.directUpdateMode = "false";
|
|
338
|
-
this.implementation.directUpdate = false;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
} else {
|
|
342
|
-
Boolean directUpdateBool = this.getConfig().getBoolean("directUpdate", false);
|
|
343
|
-
if (directUpdateBool) {
|
|
344
|
-
this.directUpdateMode = "always"; // backward compatibility: true = always
|
|
345
|
-
this.implementation.directUpdate = true;
|
|
346
|
-
} else {
|
|
347
|
-
this.directUpdateMode = "false";
|
|
348
|
-
this.implementation.directUpdate = false;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
710
|
this.currentVersionNative = new Version(this.getConfig().getString("version", pInfo.versionName));
|
|
352
711
|
this.currentBuildVersion = this.getVersionCode(pInfo);
|
|
353
712
|
this.delayUpdateUtils = new DelayUpdateUtils(this.prefs, this.editor, this.currentVersionNative, logger);
|
|
@@ -383,6 +742,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
383
742
|
"appId is missing in capacitor.config.json or plugin config, and cannot be retrieved from the native app, please add it globally or in the plugin config"
|
|
384
743
|
);
|
|
385
744
|
}
|
|
745
|
+
this.allowPreview = this.getConfig().getBoolean("allowPreview", false);
|
|
386
746
|
logger.info("appId: " + implementation.appId);
|
|
387
747
|
|
|
388
748
|
this.persistCustomId = this.getConfig().getBoolean("persistCustomId", false);
|
|
@@ -426,13 +786,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
426
786
|
this.implementation.defaultChannel = this.getConfig().getString("defaultChannel", "");
|
|
427
787
|
}
|
|
428
788
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
if (userValue >= 0 && userValue <= 600) {
|
|
432
|
-
this.periodCheckDelay = 600 * 1000;
|
|
433
|
-
} else if (userValue > 600) {
|
|
434
|
-
this.periodCheckDelay = userValue * 1000;
|
|
435
|
-
}
|
|
789
|
+
this.periodCheckDelay = normalizedPeriodCheckDelayMs(this.getConfig().getInt("periodCheckDelay", 0));
|
|
436
790
|
|
|
437
791
|
this.implementation.documentsDir = this.getContext().getFilesDir();
|
|
438
792
|
this.implementation.prefs = this.prefs;
|
|
@@ -465,7 +819,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
465
819
|
}
|
|
466
820
|
}
|
|
467
821
|
}
|
|
468
|
-
this.
|
|
822
|
+
this.configureAutoUpdateModeFromConfig();
|
|
469
823
|
this.appReadyTimeout = Math.max(1000, this.getConfig().getInt("appReadyTimeout", 10000)); // Minimum 1 second
|
|
470
824
|
this.keepUrlPathAfterReload = this.getConfig().getBoolean("keepUrlPathAfterReload", false);
|
|
471
825
|
this.syncKeepUrlPathFlag(this.keepUrlPathAfterReload);
|
|
@@ -477,14 +831,45 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
477
831
|
this.implementation.timeout = this.getConfig().getInt("responseTimeout", 20) * 1000;
|
|
478
832
|
this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
|
|
479
833
|
this.shakeChannelSelectorEnabled = this.getConfig().getBoolean("allowShakeChannelSelector", false);
|
|
834
|
+
this.shakeMenuGesture = normalizedShakeMenuGesture(this.getConfig().getString("shakeMenuGesture", SHAKE_MENU_GESTURE_SHAKE));
|
|
835
|
+
this.previewSessionEnabled = Boolean.TRUE.equals(this.allowPreview) && this.prefs.getBoolean(PREVIEW_SESSION_PREF_KEY, false);
|
|
836
|
+
if (!Boolean.TRUE.equals(this.allowPreview) && this.prefs.getBoolean(PREVIEW_SESSION_PREF_KEY, false)) {
|
|
837
|
+
this.clearPreviewSessionBecauseDisabled();
|
|
838
|
+
}
|
|
839
|
+
this.implementation.previewSession = Boolean.TRUE.equals(this.previewSessionEnabled);
|
|
840
|
+
if (Boolean.TRUE.equals(this.previewSessionEnabled)) {
|
|
841
|
+
this.previewSessionAlertPending = this.prefs.contains(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY)
|
|
842
|
+
? this.prefs.getBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, false)
|
|
843
|
+
: true;
|
|
844
|
+
final String previewAppId = this.prefs.getString(PREVIEW_APP_ID_PREF_KEY, "");
|
|
845
|
+
if (previewAppId != null && !previewAppId.isEmpty()) {
|
|
846
|
+
this.setActiveAppId(previewAppId);
|
|
847
|
+
logger.info("Using preview appId " + previewAppId);
|
|
848
|
+
}
|
|
849
|
+
this.shakeMenuEnabled = true;
|
|
850
|
+
this.shakeChannelSelectorEnabled = this.prefs.contains(PREVIEW_PREVIOUS_SHAKE_CHANNEL_SELECTOR_PREF_KEY)
|
|
851
|
+
? this.prefs.getBoolean(PREVIEW_PREVIOUS_SHAKE_CHANNEL_SELECTOR_PREF_KEY, false)
|
|
852
|
+
: this.shakeChannelSelectorEnabled;
|
|
853
|
+
}
|
|
480
854
|
boolean resetWhenUpdate = this.getConfig().getBoolean("resetWhenUpdate", true);
|
|
481
855
|
|
|
482
856
|
// Check if app was recently installed/updated BEFORE cleanupObsoleteVersions updates LatestVersionNative
|
|
483
857
|
this.wasRecentlyInstalledOrUpdated = this.checkIfRecentlyInstalledOrUpdated();
|
|
858
|
+
final boolean nativeBuildVersionChanged = this.hasNativeBuildVersionChanged();
|
|
484
859
|
|
|
485
|
-
this.implementation.autoReset();
|
|
860
|
+
this.implementation.autoReset(this.currentBuildVersion, resetWhenUpdate);
|
|
861
|
+
if (nativeBuildVersionChanged) {
|
|
862
|
+
this.clearPreviewSessionForNativeBuildChange();
|
|
863
|
+
}
|
|
864
|
+
this.leavePreviewSessionForLaunchIntentIfNeeded();
|
|
865
|
+
this.reportNativeVersionStatsIfChanged();
|
|
866
|
+
this.reportPreviousAppExitReasons();
|
|
867
|
+
this.reportPreviousWebViewRenderProcessGone();
|
|
868
|
+
this.installWebViewStatsReporter();
|
|
486
869
|
if (resetWhenUpdate) {
|
|
487
870
|
this.cleanupObsoleteVersions();
|
|
871
|
+
} else {
|
|
872
|
+
this.persistCurrentNativeBuildVersion();
|
|
488
873
|
}
|
|
489
874
|
|
|
490
875
|
// Check for 'kill' delay condition on app launch
|
|
@@ -492,6 +877,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
492
877
|
this.delayUpdateUtils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.KILLED);
|
|
493
878
|
|
|
494
879
|
this.checkForUpdateAfterDelay();
|
|
880
|
+
this.showPreviewSessionNoticeIfNeeded();
|
|
881
|
+
this.syncShakeMenuLifecycle();
|
|
495
882
|
|
|
496
883
|
// On Android 14+ (API 34+), topActivity in RecentTaskInfo returns null due to
|
|
497
884
|
// security restrictions (StrandHogg task hijacking mitigations). Use ProcessLifecycleOwner
|
|
@@ -609,6 +996,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
609
996
|
if (this.autoSplashscreen) {
|
|
610
997
|
this.hideSplashscreen();
|
|
611
998
|
}
|
|
999
|
+
this.hidePreviewTransitionLoader("app-ready");
|
|
612
1000
|
}
|
|
613
1001
|
|
|
614
1002
|
private void hideSplashscreen() {
|
|
@@ -720,6 +1108,38 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
720
1108
|
return requestToken == this.splashscreenInvocationToken;
|
|
721
1109
|
}
|
|
722
1110
|
|
|
1111
|
+
private FrameLayout createLoaderOverlay(final Activity activity, final boolean blocksTouches, final int backgroundColor) {
|
|
1112
|
+
final ProgressBar progressBar = new ProgressBar(activity);
|
|
1113
|
+
progressBar.setIndeterminate(true);
|
|
1114
|
+
|
|
1115
|
+
final FrameLayout overlay = new FrameLayout(activity);
|
|
1116
|
+
overlay.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
|
1117
|
+
overlay.setClickable(blocksTouches);
|
|
1118
|
+
overlay.setFocusable(blocksTouches);
|
|
1119
|
+
overlay.setBackgroundColor(backgroundColor);
|
|
1120
|
+
overlay.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
|
|
1121
|
+
|
|
1122
|
+
final FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
|
|
1123
|
+
ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
1124
|
+
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
1125
|
+
);
|
|
1126
|
+
params.gravity = Gravity.CENTER;
|
|
1127
|
+
overlay.addView(progressBar, params);
|
|
1128
|
+
return overlay;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
private void attachLoaderOverlay(final Activity activity, final FrameLayout overlay) {
|
|
1132
|
+
final ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
|
|
1133
|
+
decorView.addView(overlay);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
private void removeLoaderOverlay(final FrameLayout overlay) {
|
|
1137
|
+
final ViewGroup parent = (ViewGroup) overlay.getParent();
|
|
1138
|
+
if (parent != null) {
|
|
1139
|
+
parent.removeView(overlay);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
723
1143
|
private void addSplashscreenLoaderIfNeeded() {
|
|
724
1144
|
if (!Boolean.TRUE.equals(this.autoSplashscreenLoader)) {
|
|
725
1145
|
return;
|
|
@@ -736,26 +1156,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
736
1156
|
return;
|
|
737
1157
|
}
|
|
738
1158
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
FrameLayout overlay = new FrameLayout(activity);
|
|
743
|
-
overlay.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
|
744
|
-
overlay.setClickable(false);
|
|
745
|
-
overlay.setFocusable(false);
|
|
746
|
-
overlay.setBackgroundColor(Color.TRANSPARENT);
|
|
747
|
-
overlay.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
|
|
748
|
-
|
|
749
|
-
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
|
|
750
|
-
ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
751
|
-
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
752
|
-
);
|
|
753
|
-
params.gravity = Gravity.CENTER;
|
|
754
|
-
overlay.addView(progressBar, params);
|
|
755
|
-
|
|
756
|
-
ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
|
|
757
|
-
decorView.addView(overlay);
|
|
758
|
-
|
|
1159
|
+
FrameLayout overlay = createLoaderOverlay(activity, false, Color.TRANSPARENT);
|
|
1160
|
+
attachLoaderOverlay(activity, overlay);
|
|
759
1161
|
this.splashscreenLoaderOverlay = overlay;
|
|
760
1162
|
};
|
|
761
1163
|
|
|
@@ -769,10 +1171,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
769
1171
|
private void removeSplashscreenLoader() {
|
|
770
1172
|
Runnable removeLoader = () -> {
|
|
771
1173
|
if (this.splashscreenLoaderOverlay != null) {
|
|
772
|
-
|
|
773
|
-
if (parent != null) {
|
|
774
|
-
parent.removeView(this.splashscreenLoaderOverlay);
|
|
775
|
-
}
|
|
1174
|
+
removeLoaderOverlay(this.splashscreenLoaderOverlay);
|
|
776
1175
|
this.splashscreenLoaderOverlay = null;
|
|
777
1176
|
}
|
|
778
1177
|
};
|
|
@@ -784,6 +1183,84 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
784
1183
|
}
|
|
785
1184
|
}
|
|
786
1185
|
|
|
1186
|
+
private void showPreviewTransitionLoader(final String reason) {
|
|
1187
|
+
this.previewTransitionLoaderRequested = true;
|
|
1188
|
+
final Runnable showLoader = () -> {
|
|
1189
|
+
if (!this.previewTransitionLoaderRequested) {
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
if (this.previewTransitionLoaderOverlay != null) {
|
|
1194
|
+
cancelPreviewTransitionLoaderTimeout();
|
|
1195
|
+
schedulePreviewTransitionLoaderTimeout();
|
|
1196
|
+
this.previewTransitionLoaderOverlay.bringToFront();
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
final Activity activity = getActivity();
|
|
1201
|
+
if (activity == null) {
|
|
1202
|
+
logger.warn("Preview transition loader unavailable: activity missing for " + reason);
|
|
1203
|
+
this.previewTransitionLoaderRequested = false;
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
cancelPreviewTransitionLoaderTimeout();
|
|
1208
|
+
schedulePreviewTransitionLoaderTimeout();
|
|
1209
|
+
|
|
1210
|
+
final FrameLayout overlay = createLoaderOverlay(activity, true, Color.argb(46, 0, 0, 0));
|
|
1211
|
+
attachLoaderOverlay(activity, overlay);
|
|
1212
|
+
this.previewTransitionLoaderOverlay = overlay;
|
|
1213
|
+
logger.info("Preview transition loader shown: " + reason);
|
|
1214
|
+
};
|
|
1215
|
+
|
|
1216
|
+
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
1217
|
+
showLoader.run();
|
|
1218
|
+
} else {
|
|
1219
|
+
this.mainHandler.post(showLoader);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
private void hidePreviewTransitionLoader(final String reason) {
|
|
1224
|
+
if (
|
|
1225
|
+
!this.previewTransitionLoaderRequested &&
|
|
1226
|
+
this.previewTransitionLoaderOverlay == null &&
|
|
1227
|
+
this.previewTransitionLoaderTimeoutRunnable == null
|
|
1228
|
+
) {
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
final Runnable hideLoader = () -> {
|
|
1233
|
+
this.previewTransitionLoaderRequested = false;
|
|
1234
|
+
cancelPreviewTransitionLoaderTimeout();
|
|
1235
|
+
if (this.previewTransitionLoaderOverlay == null) {
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
removeLoaderOverlay(this.previewTransitionLoaderOverlay);
|
|
1240
|
+
this.previewTransitionLoaderOverlay = null;
|
|
1241
|
+
logger.info("Preview transition loader hidden: " + reason);
|
|
1242
|
+
};
|
|
1243
|
+
|
|
1244
|
+
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
1245
|
+
hideLoader.run();
|
|
1246
|
+
} else {
|
|
1247
|
+
this.mainHandler.post(hideLoader);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
private void schedulePreviewTransitionLoaderTimeout() {
|
|
1252
|
+
cancelPreviewTransitionLoaderTimeout();
|
|
1253
|
+
this.previewTransitionLoaderTimeoutRunnable = () -> hidePreviewTransitionLoader("preview-transition-timeout");
|
|
1254
|
+
this.mainHandler.postDelayed(this.previewTransitionLoaderTimeoutRunnable, PREVIEW_TRANSITION_LOADER_TIMEOUT_MS);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
private void cancelPreviewTransitionLoaderTimeout() {
|
|
1258
|
+
if (this.previewTransitionLoaderTimeoutRunnable != null) {
|
|
1259
|
+
this.mainHandler.removeCallbacks(this.previewTransitionLoaderTimeoutRunnable);
|
|
1260
|
+
this.previewTransitionLoaderTimeoutRunnable = null;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
787
1264
|
private void scheduleSplashscreenTimeout() {
|
|
788
1265
|
if (this.autoSplashscreenTimeout == null || this.autoSplashscreenTimeout <= 0) {
|
|
789
1266
|
return;
|
|
@@ -810,7 +1287,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
810
1287
|
|
|
811
1288
|
private boolean checkIfRecentlyInstalledOrUpdated() {
|
|
812
1289
|
String currentVersion = this.currentBuildVersion;
|
|
813
|
-
String lastKnownVersion = this.
|
|
1290
|
+
String lastKnownVersion = this.getStoredNativeBuildVersion();
|
|
814
1291
|
|
|
815
1292
|
if (lastKnownVersion.isEmpty()) {
|
|
816
1293
|
// First time running, consider it as recently installed
|
|
@@ -823,781 +1300,2452 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
823
1300
|
return false;
|
|
824
1301
|
}
|
|
825
1302
|
|
|
826
|
-
private boolean
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
}
|
|
830
|
-
switch (this.directUpdateMode) {
|
|
831
|
-
case "false":
|
|
832
|
-
return false;
|
|
833
|
-
case "always":
|
|
834
|
-
return true;
|
|
835
|
-
case "atInstall":
|
|
836
|
-
if (this.wasRecentlyInstalledOrUpdated) {
|
|
837
|
-
// Reset the flag after first use to prevent subsequent foreground events from using direct update
|
|
838
|
-
this.wasRecentlyInstalledOrUpdated = false;
|
|
839
|
-
return true;
|
|
840
|
-
}
|
|
841
|
-
return false;
|
|
842
|
-
case "onLaunch":
|
|
843
|
-
if (!this.onLaunchDirectUpdateUsed) {
|
|
844
|
-
return true;
|
|
845
|
-
}
|
|
846
|
-
return false;
|
|
847
|
-
default:
|
|
848
|
-
logger.error(
|
|
849
|
-
"Invalid directUpdateMode: \"" +
|
|
850
|
-
this.directUpdateMode +
|
|
851
|
-
"\". Supported values are: \"false\", \"always\", \"atInstall\", \"onLaunch\". Defaulting to \"false\" behavior."
|
|
852
|
-
);
|
|
853
|
-
return false;
|
|
854
|
-
}
|
|
1303
|
+
private boolean hasNativeBuildVersionChanged() {
|
|
1304
|
+
final String lastKnownVersion = this.getStoredNativeBuildVersion();
|
|
1305
|
+
return !lastKnownVersion.isEmpty() && !lastKnownVersion.equals(this.currentBuildVersion);
|
|
855
1306
|
}
|
|
856
1307
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1308
|
+
void reportNativeVersionStatsIfChanged() {
|
|
1309
|
+
if (this.implementation == null || this.prefs == null || this.editor == null) {
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
860
1312
|
|
|
861
|
-
|
|
862
|
-
|
|
1313
|
+
this.reportNativeVersionStatsIfChanged(
|
|
1314
|
+
this.implementation.versionBuild,
|
|
1315
|
+
this.implementation.versionCode,
|
|
1316
|
+
this.implementation.versionOs
|
|
1317
|
+
);
|
|
863
1318
|
}
|
|
864
1319
|
|
|
865
|
-
|
|
866
|
-
|
|
1320
|
+
void reportNativeVersionStatsIfChanged(
|
|
1321
|
+
final String currentVersionBuild,
|
|
1322
|
+
final String currentVersionCode,
|
|
1323
|
+
final String currentVersionOs
|
|
1324
|
+
) {
|
|
1325
|
+
if (this.implementation == null || this.prefs == null || this.editor == null) {
|
|
867
1326
|
return;
|
|
868
1327
|
}
|
|
869
1328
|
|
|
870
|
-
|
|
871
|
-
|
|
1329
|
+
final String normalizedVersionBuild = this.normalizedStatsValue(currentVersionBuild);
|
|
1330
|
+
final String normalizedVersionCode = this.normalizedStatsValue(currentVersionCode);
|
|
1331
|
+
final String normalizedVersionOs = this.normalizedStatsValue(currentVersionOs);
|
|
1332
|
+
final String previousVersionOs = this.prefs.getString(LAST_VERSION_OS_PREF_KEY, "");
|
|
1333
|
+
final String previousVersionBuild = this.prefs.getString(LAST_VERSION_BUILD_PREF_KEY, "");
|
|
1334
|
+
final String previousVersionCode = this.prefs.getString(LAST_VERSION_CODE_PREF_KEY, "");
|
|
1335
|
+
final boolean osVersionChanged =
|
|
1336
|
+
!normalizedVersionOs.isEmpty() &&
|
|
1337
|
+
previousVersionOs != null &&
|
|
1338
|
+
!previousVersionOs.isEmpty() &&
|
|
1339
|
+
!previousVersionOs.equals(normalizedVersionOs);
|
|
1340
|
+
|
|
1341
|
+
if (osVersionChanged) {
|
|
1342
|
+
final Map<String, String> metadata = new HashMap<>();
|
|
1343
|
+
metadata.put("previous_version_os", previousVersionOs);
|
|
1344
|
+
metadata.put("current_version_os", normalizedVersionOs);
|
|
1345
|
+
this.implementation.sendStats(
|
|
1346
|
+
OS_VERSION_CHANGED_ACTION,
|
|
1347
|
+
this.implementation.getCurrentBundle().getVersionName(),
|
|
1348
|
+
"",
|
|
1349
|
+
metadata,
|
|
1350
|
+
() -> this.persistLastVersionOs(normalizedVersionOs)
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
872
1353
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1354
|
+
final boolean hasPreviousNativeVersion =
|
|
1355
|
+
(previousVersionBuild != null && !previousVersionBuild.isEmpty()) ||
|
|
1356
|
+
(previousVersionCode != null && !previousVersionCode.isEmpty());
|
|
1357
|
+
final boolean nativeVersionChanged =
|
|
1358
|
+
hasPreviousNativeVersion &&
|
|
1359
|
+
(!Objects.equals(previousVersionBuild, normalizedVersionBuild) || !Objects.equals(previousVersionCode, normalizedVersionCode));
|
|
1360
|
+
|
|
1361
|
+
if (nativeVersionChanged) {
|
|
1362
|
+
final Map<String, String> metadata = new HashMap<>();
|
|
1363
|
+
metadata.put("previous_version_build", previousVersionBuild == null ? "" : previousVersionBuild);
|
|
1364
|
+
metadata.put("current_version_build", normalizedVersionBuild);
|
|
1365
|
+
metadata.put("previous_version_code", previousVersionCode == null ? "" : previousVersionCode);
|
|
1366
|
+
metadata.put("current_version_code", normalizedVersionCode);
|
|
1367
|
+
this.implementation.sendStats(
|
|
1368
|
+
NATIVE_APP_VERSION_CHANGED_ACTION,
|
|
1369
|
+
this.implementation.getCurrentBundle().getVersionName(),
|
|
1370
|
+
"",
|
|
1371
|
+
metadata,
|
|
1372
|
+
() -> this.persistLastNativeAppVersion(normalizedVersionBuild, normalizedVersionCode)
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
if (!osVersionChanged || !nativeVersionChanged) {
|
|
1377
|
+
if (!osVersionChanged) {
|
|
1378
|
+
this.editor.putString(LAST_VERSION_OS_PREF_KEY, normalizedVersionOs);
|
|
1379
|
+
}
|
|
1380
|
+
if (!nativeVersionChanged) {
|
|
1381
|
+
this.editor.putString(LAST_VERSION_BUILD_PREF_KEY, normalizedVersionBuild);
|
|
1382
|
+
this.editor.putString(LAST_VERSION_CODE_PREF_KEY, normalizedVersionCode);
|
|
1383
|
+
}
|
|
1384
|
+
this.editor.apply();
|
|
1385
|
+
}
|
|
876
1386
|
}
|
|
877
1387
|
|
|
878
|
-
|
|
879
|
-
|
|
1388
|
+
private void persistLastVersionOs(final String versionOs) {
|
|
1389
|
+
if (this.editor == null) {
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
this.editor.putString(LAST_VERSION_OS_PREF_KEY, versionOs);
|
|
1394
|
+
this.editor.apply();
|
|
880
1395
|
}
|
|
881
1396
|
|
|
882
|
-
|
|
883
|
-
|
|
1397
|
+
private void persistLastNativeAppVersion(final String versionBuild, final String versionCode) {
|
|
1398
|
+
if (this.editor == null) {
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
this.editor.putString(LAST_VERSION_BUILD_PREF_KEY, versionBuild);
|
|
1403
|
+
this.editor.putString(LAST_VERSION_CODE_PREF_KEY, versionCode);
|
|
1404
|
+
this.editor.apply();
|
|
884
1405
|
}
|
|
885
1406
|
|
|
886
|
-
|
|
887
|
-
return
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1407
|
+
private String normalizedStatsValue(final String value) {
|
|
1408
|
+
return value == null ? "" : value;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
private void reportPreviousAppExitReasons() {
|
|
1412
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R || this.implementation == null || this.implementation.statsUrl.isEmpty()) {
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
AndroidAppExitReporter.reportPreviousAppExitReasons(
|
|
1417
|
+
this.getContext(),
|
|
1418
|
+
this.prefs,
|
|
1419
|
+
this.implementation,
|
|
1420
|
+
this.logger,
|
|
1421
|
+
LAST_REPORTED_APP_EXIT_TIMESTAMP_PREF_KEY
|
|
893
1422
|
);
|
|
894
1423
|
}
|
|
895
1424
|
|
|
896
|
-
|
|
897
|
-
|
|
1425
|
+
static String statsActionForApplicationExitReason(final int reason) {
|
|
1426
|
+
switch (reason) {
|
|
1427
|
+
case APPLICATION_EXIT_REASON_CRASH:
|
|
1428
|
+
return "app_crash";
|
|
1429
|
+
case APPLICATION_EXIT_REASON_CRASH_NATIVE:
|
|
1430
|
+
return "app_crash_native";
|
|
1431
|
+
case APPLICATION_EXIT_REASON_ANR:
|
|
1432
|
+
return "app_anr";
|
|
1433
|
+
case APPLICATION_EXIT_REASON_LOW_MEMORY:
|
|
1434
|
+
return "app_killed_low_memory";
|
|
1435
|
+
case APPLICATION_EXIT_REASON_EXCESSIVE_RESOURCE_USAGE:
|
|
1436
|
+
return "app_killed_excessive_resource_usage";
|
|
1437
|
+
case APPLICATION_EXIT_REASON_INITIALIZATION_FAILURE:
|
|
1438
|
+
return "app_initialization_failure";
|
|
1439
|
+
default:
|
|
1440
|
+
return null;
|
|
1441
|
+
}
|
|
898
1442
|
}
|
|
899
1443
|
|
|
900
|
-
|
|
901
|
-
|
|
1444
|
+
static String applicationExitReasonName(final int reason) {
|
|
1445
|
+
switch (reason) {
|
|
1446
|
+
case APPLICATION_EXIT_REASON_EXIT_SELF:
|
|
1447
|
+
return "exit_self";
|
|
1448
|
+
case APPLICATION_EXIT_REASON_SIGNALED:
|
|
1449
|
+
return "signaled";
|
|
1450
|
+
case APPLICATION_EXIT_REASON_LOW_MEMORY:
|
|
1451
|
+
return "low_memory";
|
|
1452
|
+
case APPLICATION_EXIT_REASON_CRASH:
|
|
1453
|
+
return "crash";
|
|
1454
|
+
case APPLICATION_EXIT_REASON_CRASH_NATIVE:
|
|
1455
|
+
return "crash_native";
|
|
1456
|
+
case APPLICATION_EXIT_REASON_ANR:
|
|
1457
|
+
return "anr";
|
|
1458
|
+
case APPLICATION_EXIT_REASON_INITIALIZATION_FAILURE:
|
|
1459
|
+
return "initialization_failure";
|
|
1460
|
+
case APPLICATION_EXIT_REASON_PERMISSION_CHANGE:
|
|
1461
|
+
return "permission_change";
|
|
1462
|
+
case APPLICATION_EXIT_REASON_EXCESSIVE_RESOURCE_USAGE:
|
|
1463
|
+
return "excessive_resource_usage";
|
|
1464
|
+
case APPLICATION_EXIT_REASON_USER_REQUESTED:
|
|
1465
|
+
return "user_requested";
|
|
1466
|
+
case APPLICATION_EXIT_REASON_DEPENDENCY_DIED:
|
|
1467
|
+
return "dependency_died";
|
|
1468
|
+
default:
|
|
1469
|
+
return "unknown";
|
|
1470
|
+
}
|
|
902
1471
|
}
|
|
903
1472
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
try {
|
|
907
|
-
Activity currentActivity = this.getActivity();
|
|
908
|
-
if (currentActivity != null) {
|
|
909
|
-
this.implementation.activity = currentActivity;
|
|
910
|
-
} else {
|
|
911
|
-
logger.warn("directUpdateFinish: Activity is null, proceeding without refreshing the activity reference");
|
|
912
|
-
}
|
|
913
|
-
this.directUpdateFinish(latest);
|
|
914
|
-
} catch (final Exception e) {
|
|
915
|
-
logger.error("directUpdateFinish failed: " + e.getMessage());
|
|
916
|
-
}
|
|
917
|
-
});
|
|
1473
|
+
static String truncateStatsMetadataValue(final String value, final int maxLength) {
|
|
1474
|
+
return value.length() <= maxLength ? value : value.substring(0, maxLength);
|
|
918
1475
|
}
|
|
919
1476
|
|
|
920
|
-
private void
|
|
921
|
-
if (
|
|
922
|
-
|
|
923
|
-
this.implementation.directUpdate = false;
|
|
924
|
-
}
|
|
925
|
-
if (CapacitorUpdaterPlugin.this.implementation.set(latest) && CapacitorUpdaterPlugin.this._reload()) {
|
|
926
|
-
this.notifyBundleSet(latest);
|
|
927
|
-
sendReadyToJs(latest, "update installed", true);
|
|
1477
|
+
private void installWebViewStatsReporter() {
|
|
1478
|
+
if (this.bridge == null || this.bridge.getWebView() == null || this.webViewStatsListener != null) {
|
|
1479
|
+
return;
|
|
928
1480
|
}
|
|
929
|
-
}
|
|
930
1481
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
try {
|
|
935
|
-
final String previous = this.prefs.getString("LatestNativeBuildVersion", "");
|
|
936
|
-
if (!"".equals(previous) && !Objects.equals(this.currentBuildVersion, previous)) {
|
|
937
|
-
logger.info("New native build version detected: " + this.currentBuildVersion);
|
|
938
|
-
this.implementation.reset(true);
|
|
939
|
-
final List<BundleInfo> installed = this.implementation.list(false);
|
|
940
|
-
for (final BundleInfo bundle : installed) {
|
|
941
|
-
// Check if thread was interrupted (cancelled)
|
|
942
|
-
if (Thread.currentThread().isInterrupted()) {
|
|
943
|
-
logger.warn("Cleanup was cancelled, stopping");
|
|
944
|
-
return;
|
|
945
|
-
}
|
|
946
|
-
try {
|
|
947
|
-
logger.info("Deleting obsolete bundle: " + bundle.getId());
|
|
948
|
-
this.implementation.delete(bundle.getId());
|
|
949
|
-
} catch (final Exception e) {
|
|
950
|
-
logger.error("Failed to delete: " + bundle.getId() + " " + e.getMessage());
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
final List<BundleInfo> storedBundles = this.implementation.list(true);
|
|
954
|
-
final Set<String> allowedIds = new HashSet<>();
|
|
955
|
-
for (final BundleInfo info : storedBundles) {
|
|
956
|
-
if (info != null && info.getId() != null && !info.getId().isEmpty()) {
|
|
957
|
-
allowedIds.add(info.getId());
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
this.implementation.cleanupDownloadDirectories(allowedIds, Thread.currentThread());
|
|
961
|
-
this.implementation.cleanupOrphanedTempFolders(Thread.currentThread());
|
|
1482
|
+
final android.webkit.WebView webView = this.bridge.getWebView();
|
|
1483
|
+
final String script = buildWebViewStatsReporterScript();
|
|
1484
|
+
this.installDocumentStartWebViewStatsReporter(webView, script);
|
|
962
1485
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
}
|
|
968
|
-
this.implementation.cleanupDeltaCache(Thread.currentThread());
|
|
969
|
-
}
|
|
970
|
-
this.editor.putString("LatestNativeBuildVersion", this.currentBuildVersion);
|
|
971
|
-
this.editor.apply();
|
|
972
|
-
} catch (Exception e) {
|
|
973
|
-
logger.error("Error during cleanupObsoleteVersions: " + e.getMessage());
|
|
974
|
-
} finally {
|
|
975
|
-
cleanupComplete = true;
|
|
976
|
-
logger.info("Cleanup complete");
|
|
977
|
-
}
|
|
1486
|
+
this.webViewStatsListener = new WebViewListener() {
|
|
1487
|
+
@Override
|
|
1488
|
+
public void onPageStarted(final android.webkit.WebView view) {
|
|
1489
|
+
CapacitorUpdaterPlugin.this.evaluateWebViewStatsReporterScript(view, script);
|
|
978
1490
|
}
|
|
979
|
-
});
|
|
980
1491
|
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
try {
|
|
985
|
-
Thread.sleep(timeout);
|
|
986
|
-
if (cleanupThread != null && cleanupThread.isAlive() && !cleanupComplete) {
|
|
987
|
-
logger.warn("Cleanup timeout exceeded (" + timeout + "ms), interrupting cleanup thread");
|
|
988
|
-
cleanupThread.interrupt();
|
|
989
|
-
}
|
|
990
|
-
} catch (InterruptedException e) {
|
|
991
|
-
// Watchdog thread was interrupted, that's fine
|
|
1492
|
+
@Override
|
|
1493
|
+
public void onPageLoaded(final android.webkit.WebView view) {
|
|
1494
|
+
CapacitorUpdaterPlugin.this.evaluateWebViewStatsReporterScript(view, script);
|
|
992
1495
|
}
|
|
993
|
-
});
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
private void waitForCleanupIfNeeded() {
|
|
997
|
-
if (cleanupComplete) {
|
|
998
|
-
return; // Already done, no need to wait
|
|
999
|
-
}
|
|
1000
1496
|
|
|
1001
|
-
|
|
1497
|
+
@Override
|
|
1498
|
+
public boolean onRenderProcessGone(final android.webkit.WebView view, final RenderProcessGoneDetail detail) {
|
|
1499
|
+
final Map<String, String> metadata = CapacitorUpdaterPlugin.this.buildWebViewRenderProcessGoneMetadata(detail);
|
|
1500
|
+
CapacitorUpdaterPlugin.this.persistPendingWebViewRenderProcessGone(metadata);
|
|
1501
|
+
return false;
|
|
1502
|
+
}
|
|
1503
|
+
};
|
|
1002
1504
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
logger.info("Cleanup finished, proceeding with download");
|
|
1006
|
-
}
|
|
1505
|
+
this.bridge.addWebViewListener(this.webViewStatsListener);
|
|
1506
|
+
this.evaluateWebViewStatsReporterScript(webView, script);
|
|
1007
1507
|
}
|
|
1008
1508
|
|
|
1009
|
-
|
|
1509
|
+
private void installDocumentStartWebViewStatsReporter(final android.webkit.WebView webView, final String script) {
|
|
1010
1510
|
try {
|
|
1011
|
-
final
|
|
1012
|
-
|
|
1013
|
-
final
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
if (percent == 100) {
|
|
1018
|
-
final JSObject retDownloadComplete = new JSObject(ret, new String[] { "bundle" });
|
|
1019
|
-
this.notifyListeners("downloadComplete", retDownloadComplete);
|
|
1020
|
-
this.implementation.sendStats("download_complete", bundleInfo.getVersionName());
|
|
1021
|
-
lastNotifiedStatPercent = 100;
|
|
1022
|
-
} else {
|
|
1023
|
-
int currentStatPercent = (percent / 10) * 10; // Round down to nearest 10
|
|
1024
|
-
if (currentStatPercent > lastNotifiedStatPercent) {
|
|
1025
|
-
this.implementation.sendStats("download_" + currentStatPercent, bundleInfo.getVersionName());
|
|
1026
|
-
lastNotifiedStatPercent = currentStatPercent;
|
|
1027
|
-
}
|
|
1511
|
+
final Class<?> webViewFeature = Class.forName("androidx.webkit.WebViewFeature");
|
|
1512
|
+
final String feature = (String) webViewFeature.getField("DOCUMENT_START_SCRIPT").get(null);
|
|
1513
|
+
final Boolean supported = (Boolean) webViewFeature.getMethod("isFeatureSupported", String.class).invoke(null, feature);
|
|
1514
|
+
if (!Boolean.TRUE.equals(supported)) {
|
|
1515
|
+
return;
|
|
1028
1516
|
}
|
|
1517
|
+
|
|
1518
|
+
final String allowedOrigin = Uri.parse(this.bridge.getAppUrl())
|
|
1519
|
+
.buildUpon()
|
|
1520
|
+
.path(null)
|
|
1521
|
+
.fragment(null)
|
|
1522
|
+
.clearQuery()
|
|
1523
|
+
.build()
|
|
1524
|
+
.toString();
|
|
1525
|
+
final Class<?> webViewCompat = Class.forName("androidx.webkit.WebViewCompat");
|
|
1526
|
+
webViewCompat
|
|
1527
|
+
.getMethod("addDocumentStartJavaScript", android.webkit.WebView.class, String.class, Set.class)
|
|
1528
|
+
.invoke(null, webView, script, java.util.Collections.singleton(allowedOrigin));
|
|
1029
1529
|
} catch (final Exception e) {
|
|
1030
|
-
logger.
|
|
1530
|
+
logger.debug("Unable to install document-start WebView stats reporter: " + e.getMessage());
|
|
1031
1531
|
}
|
|
1032
1532
|
}
|
|
1033
1533
|
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
if (!this.getConfig().getBoolean("allowModifyUrl", false)) {
|
|
1037
|
-
logger.error("setUpdateUrl not allowed set allowModifyUrl in your config to true to allow it");
|
|
1038
|
-
call.reject("setUpdateUrl not allowed");
|
|
1534
|
+
private void evaluateWebViewStatsReporterScript(final android.webkit.WebView webView, final String script) {
|
|
1535
|
+
if (webView == null) {
|
|
1039
1536
|
return;
|
|
1040
1537
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1538
|
+
|
|
1539
|
+
this.mainHandler.post(() -> {
|
|
1540
|
+
try {
|
|
1541
|
+
webView.evaluateJavascript(script, null);
|
|
1542
|
+
} catch (final Exception e) {
|
|
1543
|
+
logger.debug("Unable to evaluate WebView stats reporter: " + e.getMessage());
|
|
1544
|
+
}
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
private Map<String, String> buildWebViewRenderProcessGoneMetadata(final RenderProcessGoneDetail detail) {
|
|
1549
|
+
final Map<String, String> metadata = new HashMap<>();
|
|
1550
|
+
metadata.put("error_type", "render_process_gone");
|
|
1551
|
+
metadata.put("source", "android_on_render_process_gone");
|
|
1552
|
+
metadata.put("timestamp", Long.toString(System.currentTimeMillis()));
|
|
1553
|
+
if (detail != null) {
|
|
1554
|
+
metadata.put("did_crash", Boolean.toString(detail.didCrash()));
|
|
1555
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
1556
|
+
metadata.put("renderer_priority_at_exit", Integer.toString(detail.rendererPriorityAtExit()));
|
|
1557
|
+
}
|
|
1046
1558
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1559
|
+
return metadata;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
private void persistPendingWebViewRenderProcessGone(final Map<String, String> metadata) {
|
|
1563
|
+
try {
|
|
1564
|
+
final JSONObject json = new JSONObject(metadata);
|
|
1565
|
+
this.prefs.edit().putString(LAST_WEBVIEW_RENDER_PROCESS_GONE_PREF_KEY, json.toString()).commit();
|
|
1566
|
+
} catch (final Exception e) {
|
|
1567
|
+
logger.debug("Unable to persist WebView render process crash metadata: " + e.getMessage());
|
|
1051
1568
|
}
|
|
1052
|
-
call.resolve();
|
|
1053
1569
|
}
|
|
1054
1570
|
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
if (!this.getConfig().getBoolean("allowModifyUrl", false)) {
|
|
1058
|
-
logger.error("setStatsUrl not allowed set allowModifyUrl in your config to true to allow it");
|
|
1059
|
-
call.reject("setStatsUrl not allowed");
|
|
1571
|
+
private void reportPreviousWebViewRenderProcessGone() {
|
|
1572
|
+
if (this.implementation == null || this.implementation.statsUrl.isEmpty()) {
|
|
1060
1573
|
return;
|
|
1061
1574
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
call.reject("setStatsUrl called without url");
|
|
1575
|
+
|
|
1576
|
+
final String rawMetadata = this.prefs.getString(LAST_WEBVIEW_RENDER_PROCESS_GONE_PREF_KEY, "");
|
|
1577
|
+
if (rawMetadata == null || rawMetadata.isEmpty()) {
|
|
1066
1578
|
return;
|
|
1067
1579
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1580
|
+
|
|
1581
|
+
try {
|
|
1582
|
+
final Map<String, String> metadata = jsonObjectToStringMap(new JSONObject(rawMetadata));
|
|
1583
|
+
metadata.put("reported_after_restart", "true");
|
|
1584
|
+
this.reportWebViewStats("webview_render_process_gone", metadata);
|
|
1585
|
+
this.prefs.edit().remove(LAST_WEBVIEW_RENDER_PROCESS_GONE_PREF_KEY).apply();
|
|
1586
|
+
} catch (final JSONException e) {
|
|
1587
|
+
this.prefs.edit().remove(LAST_WEBVIEW_RENDER_PROCESS_GONE_PREF_KEY).apply();
|
|
1072
1588
|
}
|
|
1073
|
-
call.resolve();
|
|
1074
1589
|
}
|
|
1075
1590
|
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
return;
|
|
1591
|
+
private static Map<String, String> jsonObjectToStringMap(final JSONObject json) throws JSONException {
|
|
1592
|
+
final Map<String, String> map = new HashMap<>();
|
|
1593
|
+
final JSONArray names = json.names();
|
|
1594
|
+
if (names == null) {
|
|
1595
|
+
return map;
|
|
1082
1596
|
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1597
|
+
|
|
1598
|
+
for (int i = 0; i < names.length(); i++) {
|
|
1599
|
+
final String key = names.getString(i);
|
|
1600
|
+
final String value = json.optString(key, "");
|
|
1601
|
+
if (!value.isEmpty()) {
|
|
1602
|
+
map.put(key, value);
|
|
1603
|
+
}
|
|
1088
1604
|
}
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1605
|
+
return map;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
private static JSObject jsonObjectToJSObject(final JSONObject json) throws JSONException {
|
|
1609
|
+
final JSObject ret = new JSObject();
|
|
1610
|
+
final JSONArray names = json.names();
|
|
1611
|
+
if (names == null) {
|
|
1612
|
+
return ret;
|
|
1093
1613
|
}
|
|
1094
|
-
|
|
1614
|
+
for (int i = 0; i < names.length(); i++) {
|
|
1615
|
+
final String key = names.getString(i);
|
|
1616
|
+
ret.put(key, json.get(key));
|
|
1617
|
+
}
|
|
1618
|
+
return ret;
|
|
1095
1619
|
}
|
|
1096
1620
|
|
|
1097
1621
|
@PluginMethod
|
|
1098
|
-
public void
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1622
|
+
public void reportWebViewError(final PluginCall call) {
|
|
1623
|
+
final JSObject data = call.getData();
|
|
1624
|
+
this.reportWebViewStats(
|
|
1625
|
+
statsActionForWebViewErrorType(data.optString("type", "javascript_error")),
|
|
1626
|
+
buildWebViewErrorMetadata(data)
|
|
1627
|
+
);
|
|
1628
|
+
call.resolve();
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
private void reportWebViewStats(final String action, final Map<String, String> metadata) {
|
|
1632
|
+
if (this.implementation == null) {
|
|
1633
|
+
return;
|
|
1106
1634
|
}
|
|
1635
|
+
|
|
1636
|
+
final BundleInfo current = this.implementation.getCurrentBundle();
|
|
1637
|
+
final String versionName = current == null ? "" : current.getVersionName();
|
|
1638
|
+
this.implementation.sendStats(action, versionName, "", metadata);
|
|
1107
1639
|
}
|
|
1108
1640
|
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1641
|
+
static String statsActionForWebViewErrorType(final String type) {
|
|
1642
|
+
switch (type) {
|
|
1643
|
+
case "unhandled_rejection":
|
|
1644
|
+
return "webview_unhandled_rejection";
|
|
1645
|
+
case "resource_error":
|
|
1646
|
+
return "webview_resource_error";
|
|
1647
|
+
case "security_policy_violation":
|
|
1648
|
+
return "webview_security_policy_violation";
|
|
1649
|
+
case "webview_unclean_restart":
|
|
1650
|
+
return "webview_unclean_restart";
|
|
1651
|
+
case "render_process_gone":
|
|
1652
|
+
return "webview_render_process_gone";
|
|
1653
|
+
case "web_content_process_terminated":
|
|
1654
|
+
return "webview_content_process_terminated";
|
|
1655
|
+
case "javascript_error":
|
|
1656
|
+
default:
|
|
1657
|
+
return "webview_javascript_error";
|
|
1118
1658
|
}
|
|
1119
1659
|
}
|
|
1120
1660
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1661
|
+
static Map<String, String> buildWebViewErrorMetadata(final JSObject data) {
|
|
1662
|
+
final Map<String, String> metadata = new HashMap<>();
|
|
1663
|
+
putStatsMetadataValue(metadata, "error_type", data.optString("type", "javascript_error"), 64);
|
|
1664
|
+
putStatsMetadataValue(metadata, "message", data.optString("message", ""), 1024);
|
|
1665
|
+
putStatsMetadataValue(metadata, "source", sanitizeStatsMetadataUrl(data.optString("source", "")), 512);
|
|
1666
|
+
putStatsMetadataValue(metadata, "line", data.optString("line", data.optString("lineno", "")), 32);
|
|
1667
|
+
putStatsMetadataValue(metadata, "column", data.optString("column", data.optString("colno", "")), 32);
|
|
1668
|
+
putStatsMetadataValue(metadata, "stack", data.optString("stack", ""), 2048);
|
|
1669
|
+
putStatsMetadataValue(metadata, "tag_name", data.optString("tag_name", ""), 64);
|
|
1670
|
+
putStatsMetadataValue(metadata, "href", sanitizeStatsMetadataUrl(data.optString("href", "")), 512);
|
|
1671
|
+
putStatsMetadataValue(metadata, "user_agent", data.optString("user_agent", ""), 256);
|
|
1672
|
+
putStatsMetadataValue(metadata, "session_id", data.optString("session_id", ""), 128);
|
|
1673
|
+
putStatsMetadataValue(metadata, "previous_session_id", data.optString("previous_session_id", ""), 128);
|
|
1674
|
+
putStatsMetadataValue(metadata, "previous_href", sanitizeStatsMetadataUrl(data.optString("previous_href", "")), 512);
|
|
1675
|
+
putStatsMetadataValue(metadata, "previous_started_at", data.optString("previous_started_at", ""), 64);
|
|
1676
|
+
putStatsMetadataValue(metadata, "previous_updated_at", data.optString("previous_updated_at", ""), 64);
|
|
1677
|
+
return metadata;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
private static void putStatsMetadataValue(
|
|
1681
|
+
final Map<String, String> metadata,
|
|
1682
|
+
final String key,
|
|
1683
|
+
final String value,
|
|
1684
|
+
final int maxLength
|
|
1685
|
+
) {
|
|
1686
|
+
if (value == null || value.isEmpty()) {
|
|
1127
1687
|
return;
|
|
1128
1688
|
}
|
|
1129
|
-
|
|
1130
|
-
|
|
1689
|
+
|
|
1690
|
+
metadata.put(key, truncateStatsMetadataValue(value, maxLength));
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
static String sanitizeStatsMetadataUrl(final String value) {
|
|
1694
|
+
if (value == null || value.isEmpty()) {
|
|
1695
|
+
return "";
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
try {
|
|
1699
|
+
final java.net.URI uri = new java.net.URI(value);
|
|
1700
|
+
if (uri.getScheme() != null && uri.getHost() != null) {
|
|
1701
|
+
final String path = sanitizeStatsMetadataUrlPath(uri.getPath());
|
|
1702
|
+
return new java.net.URI(
|
|
1703
|
+
uri.getScheme(),
|
|
1704
|
+
null,
|
|
1705
|
+
uri.getHost(),
|
|
1706
|
+
uri.getPort(),
|
|
1707
|
+
path.isEmpty() ? null : path,
|
|
1708
|
+
null,
|
|
1709
|
+
null
|
|
1710
|
+
).toString();
|
|
1711
|
+
}
|
|
1712
|
+
} catch (Exception ignored) {}
|
|
1713
|
+
|
|
1714
|
+
try {
|
|
1715
|
+
final Uri uri = Uri.parse(value);
|
|
1716
|
+
if (uri.getScheme() != null && uri.getHost() != null) {
|
|
1717
|
+
final String host = stripUrlUserInfo(uri.getHost());
|
|
1718
|
+
if (host.isEmpty()) {
|
|
1719
|
+
return stripUrlQueryAndFragment(value);
|
|
1720
|
+
}
|
|
1721
|
+
final StringBuilder authority = new StringBuilder(host);
|
|
1722
|
+
if (uri.getPort() != -1) {
|
|
1723
|
+
authority.append(':').append(uri.getPort());
|
|
1724
|
+
}
|
|
1725
|
+
final Uri.Builder builder = new Uri.Builder().scheme(uri.getScheme()).authority(authority.toString());
|
|
1726
|
+
final String path = sanitizeStatsMetadataUrlPath(uri.getPath());
|
|
1727
|
+
if (!path.isEmpty()) {
|
|
1728
|
+
builder.path(path);
|
|
1729
|
+
}
|
|
1730
|
+
return builder.build().toString();
|
|
1731
|
+
}
|
|
1732
|
+
} catch (Exception ignored) {}
|
|
1733
|
+
|
|
1734
|
+
return stripUrlQueryAndFragment(value);
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
private static String sanitizeStatsMetadataUrlPath(final String path) {
|
|
1738
|
+
if (path == null || path.isEmpty()) {
|
|
1739
|
+
return "";
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
final String[] segments = path.split("/", -1);
|
|
1743
|
+
for (int index = 0; index < segments.length; index++) {
|
|
1744
|
+
if (isSensitiveUrlPathSegment(segments[index])) {
|
|
1745
|
+
segments[index] = "redacted";
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
return String.join("/", segments);
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
private static String stripUrlUserInfo(final String host) {
|
|
1752
|
+
if (host == null || host.isEmpty()) {
|
|
1753
|
+
return "";
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
final int userInfoIndex = host.lastIndexOf('@');
|
|
1757
|
+
if (userInfoIndex < 0) {
|
|
1758
|
+
return host;
|
|
1759
|
+
}
|
|
1760
|
+
return host.substring(userInfoIndex + 1);
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
private static boolean isSensitiveUrlPathSegment(final String segment) {
|
|
1764
|
+
return (
|
|
1765
|
+
segment.matches("[0-9]{6,}") ||
|
|
1766
|
+
segment.matches("[0-9a-fA-F]{16,}") ||
|
|
1767
|
+
segment.matches("[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}")
|
|
1768
|
+
);
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
private static String stripUrlQueryAndFragment(final String value) {
|
|
1772
|
+
int end = value.length();
|
|
1773
|
+
final int queryIndex = value.indexOf('?');
|
|
1774
|
+
final int fragmentIndex = value.indexOf('#');
|
|
1775
|
+
if (queryIndex >= 0) {
|
|
1776
|
+
end = Math.min(end, queryIndex);
|
|
1777
|
+
}
|
|
1778
|
+
if (fragmentIndex >= 0) {
|
|
1779
|
+
end = Math.min(end, fragmentIndex);
|
|
1780
|
+
}
|
|
1781
|
+
return value.substring(0, end);
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
static String buildWebViewStatsReporterScript() {
|
|
1785
|
+
return (
|
|
1786
|
+
"(function(){" +
|
|
1787
|
+
"if(window.__capgoWebViewErrorReporterInstalled){return;}" +
|
|
1788
|
+
"window.__capgoWebViewErrorReporterInstalled=true;" +
|
|
1789
|
+
"var maxReports=20,sentReports=0,queue=[],seen={};" +
|
|
1790
|
+
"var sessionKey='CapacitorUpdater.webViewSession';" +
|
|
1791
|
+
"var sessionId=String(Date.now())+'-'+Math.random().toString(36).slice(2);" +
|
|
1792
|
+
"function s(value){try{if(value===undefined){return '';}if(value===null){return 'null';}if(typeof value==='string'){return value;}if(value&&typeof value.message==='string'){return value.message;}return String(value);}catch(_){return '';}}" +
|
|
1793
|
+
"function stack(value){try{return value&&value.stack?String(value.stack):'';}catch(_){return '';}}" +
|
|
1794
|
+
"function updater(){var cap=window.Capacitor;if(!cap||!cap.Plugins){return null;}return cap.Plugins.CapacitorUpdater||null;}" +
|
|
1795
|
+
"function flush(){var plugin=updater();if(!plugin||typeof plugin.reportWebViewError!=='function'){return false;}while(queue.length){var payload=queue.shift();try{var result=plugin.reportWebViewError(payload);if(result&&typeof result.catch==='function'){result.catch(function(){});}}catch(_){}}return true;}" +
|
|
1796
|
+
"var retries=0;function scheduleFlush(){if(flush()){return;}if(retries++<40){setTimeout(scheduleFlush,250);}}" +
|
|
1797
|
+
"function send(payload){try{if(sentReports>=maxReports){return;}payload.href=payload.href||location.href||'';payload.user_agent=navigator.userAgent||'';payload.session_id=sessionId;var key=[payload.type,payload.message,payload.source,payload.line,payload.column,payload.tag_name].join('|');if(seen[key]){return;}seen[key]=true;sentReports+=1;queue.push(payload);scheduleFlush();}catch(_){}}" +
|
|
1798
|
+
"function readSession(){try{return JSON.parse(localStorage.getItem(sessionKey)||'null')||null;}catch(_){return null;}}" +
|
|
1799
|
+
"function writeSession(active){try{localStorage.setItem(sessionKey,JSON.stringify({id:sessionId,active:active,href:location.href||'',started_at:window.__capgoWebViewSessionStartedAt,updated_at:String(Date.now())}));}catch(_){}}" +
|
|
1800
|
+
"window.__capgoWebViewSessionStartedAt=String(Date.now());" +
|
|
1801
|
+
"var previous=readSession();" +
|
|
1802
|
+
"if(previous&&previous.active){send({type:'webview_unclean_restart',message:'WebView restarted without a clean page unload',previous_session_id:s(previous.id),previous_href:s(previous.href),previous_started_at:s(previous.started_at),previous_updated_at:s(previous.updated_at)});}" +
|
|
1803
|
+
"writeSession(true);" +
|
|
1804
|
+
"setInterval(function(){writeSession(true);},15000);" +
|
|
1805
|
+
"function markClean(){writeSession(false);}" +
|
|
1806
|
+
"window.addEventListener('pagehide',markClean,true);" +
|
|
1807
|
+
"window.addEventListener('beforeunload',markClean,true);" +
|
|
1808
|
+
"window.addEventListener('error',function(event){var target=event&&event.target;if(target&&target!==window&&(target.src||target.href)){send({type:'resource_error',message:'Resource failed to load',source:s(target.src||target.href),tag_name:s(target.tagName)});return;}send({type:'javascript_error',message:s((event&&event.message)||(event&&event.error)),source:s(event&&event.filename),line:s(event&&event.lineno),column:s(event&&event.colno),stack:stack(event&&event.error)});},true);" +
|
|
1809
|
+
"window.addEventListener('unhandledrejection',function(event){var reason=event&&event.reason;send({type:'unhandled_rejection',message:s(reason),stack:stack(reason)});},true);" +
|
|
1810
|
+
"document.addEventListener('securitypolicyviolation',function(event){send({type:'security_policy_violation',message:s(event&&event.violatedDirective),source:s(event&&event.blockedURI)});},true);" +
|
|
1811
|
+
"document.addEventListener('deviceready',scheduleFlush,false);" +
|
|
1812
|
+
"setTimeout(scheduleFlush,0);" +
|
|
1813
|
+
"})();"
|
|
1814
|
+
);
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
private boolean shouldUseDirectUpdate() {
|
|
1818
|
+
if (!Boolean.TRUE.equals(this.autoUpdate) || AUTO_UPDATE_MODE_ONLY_DOWNLOAD.equals(this.autoUpdateMode)) {
|
|
1819
|
+
return false;
|
|
1820
|
+
}
|
|
1821
|
+
if (Boolean.TRUE.equals(this.autoSplashscreenTimedOut)) {
|
|
1822
|
+
return false;
|
|
1823
|
+
}
|
|
1824
|
+
switch (this.directUpdateMode) {
|
|
1825
|
+
case "false":
|
|
1826
|
+
return false;
|
|
1827
|
+
case "always":
|
|
1828
|
+
return true;
|
|
1829
|
+
case "atInstall":
|
|
1830
|
+
if (this.wasRecentlyInstalledOrUpdated) {
|
|
1831
|
+
// Reset the flag after first use to prevent subsequent foreground events from using direct update
|
|
1832
|
+
this.wasRecentlyInstalledOrUpdated = false;
|
|
1833
|
+
return true;
|
|
1834
|
+
}
|
|
1835
|
+
return false;
|
|
1836
|
+
case "onLaunch":
|
|
1837
|
+
if (!this.onLaunchDirectUpdateUsed) {
|
|
1838
|
+
return true;
|
|
1839
|
+
}
|
|
1840
|
+
return false;
|
|
1841
|
+
default:
|
|
1842
|
+
logger.error(
|
|
1843
|
+
"Invalid directUpdateMode: \"" +
|
|
1844
|
+
this.directUpdateMode +
|
|
1845
|
+
"\". Supported values are: \"false\", \"always\", \"atInstall\", \"onLaunch\". Defaulting to \"false\" behavior."
|
|
1846
|
+
);
|
|
1847
|
+
return false;
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
private void configureAutoUpdateModeFromConfig() {
|
|
1852
|
+
final String configuredMode = this.getConfig().getString("autoUpdate", null);
|
|
1853
|
+
if (configuredMode != null && !configuredMode.isEmpty() && !"true".equals(configuredMode) && !"false".equals(configuredMode)) {
|
|
1854
|
+
this.autoUpdateMode = normalizedAutoUpdateMode(configuredMode);
|
|
1855
|
+
if (!this.autoUpdateMode.equals(configuredMode)) {
|
|
1856
|
+
logger.error(
|
|
1857
|
+
"Invalid autoUpdate value: \"" +
|
|
1858
|
+
configuredMode +
|
|
1859
|
+
"\". Supported values are: true, false, \"off\", \"atBackground\", \"atInstall\", \"onLaunch\", \"always\", \"onlyDownload\". Defaulting to \"atBackground\"."
|
|
1860
|
+
);
|
|
1861
|
+
}
|
|
1862
|
+
} else {
|
|
1863
|
+
final boolean enabled =
|
|
1864
|
+
configuredMode != null
|
|
1865
|
+
? "true".equals(configuredMode)
|
|
1866
|
+
: Boolean.TRUE.equals(this.getConfig().getBoolean("autoUpdate", true));
|
|
1867
|
+
this.autoUpdateMode = enabled
|
|
1868
|
+
? autoUpdateModeForLegacyDirectUpdateMode(this.resolveLegacyDirectUpdateModeFromConfig())
|
|
1869
|
+
: AUTO_UPDATE_MODE_OFF;
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
this.autoUpdate = isAutoUpdateModeEnabled(this.autoUpdateMode);
|
|
1873
|
+
this.directUpdateMode = directUpdateModeForAutoUpdateMode(this.autoUpdateMode);
|
|
1874
|
+
this.implementation.directUpdate = isDirectUpdateMode(this.directUpdateMode);
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
private String resolveLegacyDirectUpdateModeFromConfig() {
|
|
1878
|
+
final String directUpdateConfig = this.getConfig().getString("directUpdate", null);
|
|
1879
|
+
if (directUpdateConfig != null) {
|
|
1880
|
+
if ("true".equals(directUpdateConfig)) {
|
|
1881
|
+
return AUTO_UPDATE_MODE_ALWAYS;
|
|
1882
|
+
}
|
|
1883
|
+
if ("false".equals(directUpdateConfig) || isDirectUpdateMode(directUpdateConfig)) {
|
|
1884
|
+
return directUpdateConfig;
|
|
1885
|
+
}
|
|
1886
|
+
logger.error(
|
|
1887
|
+
"Invalid directUpdate value: \"" +
|
|
1888
|
+
directUpdateConfig +
|
|
1889
|
+
"\". Supported values are: false, true, \"always\", \"atInstall\", \"onLaunch\". Defaulting to \"false\"."
|
|
1890
|
+
);
|
|
1891
|
+
return "false";
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
return Boolean.TRUE.equals(this.getConfig().getBoolean("directUpdate", false)) ? AUTO_UPDATE_MODE_ALWAYS : "false";
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
static String normalizedAutoUpdateMode(final String value) {
|
|
1898
|
+
if (value == null) {
|
|
1899
|
+
return AUTO_UPDATE_MODE_BACKGROUND;
|
|
1900
|
+
}
|
|
1901
|
+
switch (value) {
|
|
1902
|
+
case "false":
|
|
1903
|
+
case AUTO_UPDATE_MODE_OFF:
|
|
1904
|
+
return AUTO_UPDATE_MODE_OFF;
|
|
1905
|
+
case "true":
|
|
1906
|
+
case AUTO_UPDATE_MODE_BACKGROUND:
|
|
1907
|
+
return AUTO_UPDATE_MODE_BACKGROUND;
|
|
1908
|
+
case AUTO_UPDATE_MODE_INSTALL:
|
|
1909
|
+
case AUTO_UPDATE_MODE_LAUNCH:
|
|
1910
|
+
case AUTO_UPDATE_MODE_ALWAYS:
|
|
1911
|
+
case AUTO_UPDATE_MODE_ONLY_DOWNLOAD:
|
|
1912
|
+
return value;
|
|
1913
|
+
default:
|
|
1914
|
+
return AUTO_UPDATE_MODE_BACKGROUND;
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
static String normalizedShakeMenuGesture(final String value) {
|
|
1919
|
+
if (value == null || value.trim().isEmpty()) {
|
|
1920
|
+
return SHAKE_MENU_GESTURE_SHAKE;
|
|
1921
|
+
}
|
|
1922
|
+
final String normalized = value.trim();
|
|
1923
|
+
if (SHAKE_MENU_GESTURE_THREE_FINGER_PINCH.equals(normalized)) {
|
|
1924
|
+
return SHAKE_MENU_GESTURE_THREE_FINGER_PINCH;
|
|
1925
|
+
}
|
|
1926
|
+
return SHAKE_MENU_GESTURE_SHAKE;
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
static boolean isSupportedShakeMenuGesture(final String value) {
|
|
1930
|
+
if (value == null) {
|
|
1931
|
+
return true;
|
|
1932
|
+
}
|
|
1933
|
+
final String normalized = value.trim();
|
|
1934
|
+
if (normalized.isEmpty()) {
|
|
1935
|
+
return false;
|
|
1936
|
+
}
|
|
1937
|
+
return SHAKE_MENU_GESTURE_SHAKE.equals(normalized) || SHAKE_MENU_GESTURE_THREE_FINGER_PINCH.equals(normalized);
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
static String autoUpdateModeForLegacyDirectUpdateMode(final String directUpdateMode) {
|
|
1941
|
+
switch (directUpdateMode) {
|
|
1942
|
+
case AUTO_UPDATE_MODE_INSTALL:
|
|
1943
|
+
case AUTO_UPDATE_MODE_LAUNCH:
|
|
1944
|
+
case AUTO_UPDATE_MODE_ALWAYS:
|
|
1945
|
+
return directUpdateMode;
|
|
1946
|
+
case "false":
|
|
1947
|
+
default:
|
|
1948
|
+
return AUTO_UPDATE_MODE_BACKGROUND;
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
static String directUpdateModeForAutoUpdateMode(final String autoUpdateMode) {
|
|
1953
|
+
switch (autoUpdateMode) {
|
|
1954
|
+
case AUTO_UPDATE_MODE_INSTALL:
|
|
1955
|
+
case AUTO_UPDATE_MODE_LAUNCH:
|
|
1956
|
+
case AUTO_UPDATE_MODE_ALWAYS:
|
|
1957
|
+
return autoUpdateMode;
|
|
1958
|
+
default:
|
|
1959
|
+
return "false";
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
static boolean isAutoUpdateModeEnabled(final String autoUpdateMode) {
|
|
1964
|
+
return !AUTO_UPDATE_MODE_OFF.equals(autoUpdateMode);
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
static boolean shouldAutoUpdateModeSetNextBundle(final String autoUpdateMode) {
|
|
1968
|
+
return isAutoUpdateModeEnabled(autoUpdateMode) && !AUTO_UPDATE_MODE_ONLY_DOWNLOAD.equals(autoUpdateMode);
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
static boolean isDirectUpdateMode(final String directUpdateMode) {
|
|
1972
|
+
return (
|
|
1973
|
+
AUTO_UPDATE_MODE_INSTALL.equals(directUpdateMode) ||
|
|
1974
|
+
AUTO_UPDATE_MODE_LAUNCH.equals(directUpdateMode) ||
|
|
1975
|
+
AUTO_UPDATE_MODE_ALWAYS.equals(directUpdateMode)
|
|
1976
|
+
);
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
private boolean shouldAutoSetNextBundle() {
|
|
1980
|
+
return shouldAutoUpdateModeSetNextBundle(this.autoUpdateMode);
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
private boolean isDirectUpdateCurrentlyAllowed(final boolean plannedDirectUpdate) {
|
|
1984
|
+
return plannedDirectUpdate && !Boolean.TRUE.equals(this.autoSplashscreenTimedOut);
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
static boolean shouldConsumeOnLaunchDirectUpdate(final String directUpdateMode, final boolean plannedDirectUpdate) {
|
|
1988
|
+
return plannedDirectUpdate && "onLaunch".equals(directUpdateMode);
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
static int normalizedPeriodCheckDelayMs(final int valueSeconds) {
|
|
1992
|
+
final int normalizedSeconds = normalizedPeriodCheckDelaySeconds(valueSeconds);
|
|
1993
|
+
if (normalizedSeconds <= 0) {
|
|
1994
|
+
return 0;
|
|
1995
|
+
}
|
|
1996
|
+
final long delayMs = (long) normalizedSeconds * 1000L;
|
|
1997
|
+
return delayMs > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) delayMs;
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
static int normalizedPeriodCheckDelaySeconds(final int valueSeconds) {
|
|
2001
|
+
if (valueSeconds <= 0) {
|
|
2002
|
+
return 0;
|
|
2003
|
+
}
|
|
2004
|
+
return Math.max(600, valueSeconds);
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
private void consumeOnLaunchDirectUpdateAttempt(final boolean plannedDirectUpdate) {
|
|
2008
|
+
if (!shouldConsumeOnLaunchDirectUpdate(this.directUpdateMode, plannedDirectUpdate)) {
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
this.onLaunchDirectUpdateUsed = true;
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
void configureDirectUpdateModeForTesting(final String directUpdateMode, final boolean onLaunchDirectUpdateUsed) {
|
|
2016
|
+
this.directUpdateMode = directUpdateMode;
|
|
2017
|
+
this.autoUpdateMode = autoUpdateModeForLegacyDirectUpdateMode(directUpdateMode);
|
|
2018
|
+
this.autoUpdate = isAutoUpdateModeEnabled(this.autoUpdateMode);
|
|
2019
|
+
if (this.implementation != null) {
|
|
2020
|
+
this.implementation.directUpdate = isDirectUpdateMode(this.directUpdateMode);
|
|
2021
|
+
}
|
|
2022
|
+
this.onLaunchDirectUpdateUsed = onLaunchDirectUpdateUsed;
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
void setAutoUpdateModeForTesting(final String autoUpdateMode) {
|
|
2026
|
+
this.autoUpdateMode = normalizedAutoUpdateMode(autoUpdateMode);
|
|
2027
|
+
this.autoUpdate = isAutoUpdateModeEnabled(this.autoUpdateMode);
|
|
2028
|
+
this.directUpdateMode = directUpdateModeForAutoUpdateMode(this.autoUpdateMode);
|
|
2029
|
+
if (this.implementation != null) {
|
|
2030
|
+
this.implementation.directUpdate = isDirectUpdateMode(this.directUpdateMode);
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
boolean shouldUseDirectUpdateForTesting() {
|
|
2035
|
+
return this.shouldUseDirectUpdate();
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
boolean hasConsumedOnLaunchDirectUpdateForTesting() {
|
|
2039
|
+
return this.onLaunchDirectUpdateUsed;
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
boolean isVersionDownloadInProgress(final String version) {
|
|
2043
|
+
return (
|
|
2044
|
+
version != null &&
|
|
2045
|
+
!version.isEmpty() &&
|
|
2046
|
+
this.implementation != null &&
|
|
2047
|
+
this.implementation.activity != null &&
|
|
2048
|
+
DownloadWorkerManager.isVersionDownloading(this.implementation.activity, version)
|
|
2049
|
+
);
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
void setLoggerForTesting(final Logger logger) {
|
|
2053
|
+
this.logger = logger;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
void completeBackgroundTaskForTesting(final BundleInfo current, final boolean plannedDirectUpdate) {
|
|
2057
|
+
this.endBackGroundTaskWithNotif("test", current.getVersionName(), current, false, plannedDirectUpdate);
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
void scheduleDirectUpdateFinish(final BundleInfo latest) {
|
|
2061
|
+
startNewThread(() -> {
|
|
2062
|
+
try {
|
|
2063
|
+
if (this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
2064
|
+
logger.info("Skipping direct update install while preview session state is active");
|
|
2065
|
+
this.implementation.directUpdate = false;
|
|
2066
|
+
this.clearBackgroundDownloadState();
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
Activity currentActivity = this.getActivity();
|
|
2070
|
+
if (currentActivity != null) {
|
|
2071
|
+
this.implementation.activity = currentActivity;
|
|
2072
|
+
} else {
|
|
2073
|
+
logger.warn("directUpdateFinish: Activity is null, proceeding without refreshing the activity reference");
|
|
2074
|
+
}
|
|
2075
|
+
this.directUpdateFinish(latest);
|
|
2076
|
+
} catch (final Exception e) {
|
|
2077
|
+
logger.error("directUpdateFinish failed: " + e.getMessage());
|
|
2078
|
+
}
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
private void directUpdateFinish(final BundleInfo latest) {
|
|
2083
|
+
if (this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
2084
|
+
logger.info("Skipping direct update finish while preview session state is active");
|
|
2085
|
+
this.implementation.directUpdate = false;
|
|
2086
|
+
this.clearBackgroundDownloadState();
|
|
2087
|
+
return;
|
|
2088
|
+
}
|
|
2089
|
+
if ("onLaunch".equals(this.directUpdateMode)) {
|
|
2090
|
+
this.onLaunchDirectUpdateUsed = true;
|
|
2091
|
+
this.implementation.directUpdate = false;
|
|
2092
|
+
}
|
|
2093
|
+
if (this.applyDownloadedBundleForDirectUpdate(latest)) {
|
|
2094
|
+
this.implementation.setNextBundle(null);
|
|
2095
|
+
this.notifyBundleSet(latest);
|
|
2096
|
+
sendReadyToJs(latest, "update installed", true);
|
|
2097
|
+
} else {
|
|
2098
|
+
this.implementation.setNextBundle(latest.getId());
|
|
2099
|
+
final JSObject ret = new JSObject();
|
|
2100
|
+
ret.put("bundle", InternalUtils.mapToJSObject(latest.toJSONMap()));
|
|
2101
|
+
this.notifyListeners("updateAvailable", ret);
|
|
2102
|
+
sendReadyToJs(
|
|
2103
|
+
this.implementation.getCurrentBundle(),
|
|
2104
|
+
"Direct update reload failed, update will install next background",
|
|
2105
|
+
false
|
|
2106
|
+
);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
private boolean applyDownloadedBundleForDirectUpdate(final BundleInfo latest) {
|
|
2111
|
+
final CapgoUpdater.ResetState previousState = this.implementation.captureResetState();
|
|
2112
|
+
final String previousBundleName = this.implementation.getCurrentBundle().getVersionName();
|
|
2113
|
+
|
|
2114
|
+
if (!this.implementation.stagePendingReload(latest)) {
|
|
2115
|
+
this.implementation.restoreResetState(previousState);
|
|
2116
|
+
logger.error("Direct update failed to stage downloaded bundle: " + latest.toString());
|
|
2117
|
+
return false;
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
if (this._reload()) {
|
|
2121
|
+
this.implementation.finalizePendingReload(latest, previousBundleName);
|
|
2122
|
+
return true;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
this.implementation.restoreResetState(previousState);
|
|
2126
|
+
this.restoreLiveBundleStateAfterFailedReload();
|
|
2127
|
+
logger.error("Direct update reload failed after staging bundle: " + latest.toString());
|
|
2128
|
+
return false;
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
private void cleanupObsoleteVersions() {
|
|
2132
|
+
cleanupThread = startNewThread(() -> {
|
|
2133
|
+
synchronized (cleanupLock) {
|
|
2134
|
+
try {
|
|
2135
|
+
final String previous = this.getStoredNativeBuildVersion();
|
|
2136
|
+
if (!"".equals(previous) && !Objects.equals(this.currentBuildVersion, previous)) {
|
|
2137
|
+
logger.info("New native build version detected: " + this.currentBuildVersion);
|
|
2138
|
+
this.implementation.reset(true);
|
|
2139
|
+
final List<BundleInfo> installed = this.implementation.list(false);
|
|
2140
|
+
for (final BundleInfo bundle : installed) {
|
|
2141
|
+
// Check if thread was interrupted (cancelled)
|
|
2142
|
+
if (Thread.currentThread().isInterrupted()) {
|
|
2143
|
+
logger.warn("Cleanup was cancelled, stopping");
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
2146
|
+
try {
|
|
2147
|
+
logger.info("Deleting obsolete bundle: " + bundle.getId());
|
|
2148
|
+
this.implementation.delete(bundle.getId());
|
|
2149
|
+
} catch (final Exception e) {
|
|
2150
|
+
logger.error("Failed to delete: " + bundle.getId() + " " + e.getMessage());
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
final List<BundleInfo> storedBundles = this.implementation.list(true);
|
|
2154
|
+
final Set<String> allowedIds = new HashSet<>();
|
|
2155
|
+
for (final BundleInfo info : storedBundles) {
|
|
2156
|
+
if (info != null && info.getId() != null && !info.getId().isEmpty()) {
|
|
2157
|
+
allowedIds.add(info.getId());
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
this.implementation.cleanupDownloadDirectories(allowedIds, Thread.currentThread());
|
|
2161
|
+
this.implementation.cleanupOrphanedTempFolders(Thread.currentThread());
|
|
2162
|
+
|
|
2163
|
+
// Check again before the expensive delta cache cleanup
|
|
2164
|
+
if (Thread.currentThread().isInterrupted()) {
|
|
2165
|
+
logger.warn("Cleanup was cancelled before delta cache cleanup");
|
|
2166
|
+
return;
|
|
2167
|
+
}
|
|
2168
|
+
this.implementation.cleanupDeltaCache(Thread.currentThread());
|
|
2169
|
+
}
|
|
2170
|
+
this.editor.putString("LatestNativeBuildVersion", this.currentBuildVersion);
|
|
2171
|
+
this.editor.apply();
|
|
2172
|
+
} catch (Exception e) {
|
|
2173
|
+
logger.error("Error during cleanupObsoleteVersions: " + e.getMessage());
|
|
2174
|
+
} finally {
|
|
2175
|
+
cleanupComplete = true;
|
|
2176
|
+
logger.info("Cleanup complete");
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
});
|
|
2180
|
+
|
|
2181
|
+
// Start a timeout watchdog thread to cancel cleanup if it takes too long
|
|
2182
|
+
final long timeout = this.appReadyTimeout / 2;
|
|
2183
|
+
startNewThread(() -> {
|
|
2184
|
+
try {
|
|
2185
|
+
Thread.sleep(timeout);
|
|
2186
|
+
if (cleanupThread != null && cleanupThread.isAlive() && !cleanupComplete) {
|
|
2187
|
+
logger.warn("Cleanup timeout exceeded (" + timeout + "ms), interrupting cleanup thread");
|
|
2188
|
+
cleanupThread.interrupt();
|
|
2189
|
+
}
|
|
2190
|
+
} catch (InterruptedException e) {
|
|
2191
|
+
// Watchdog thread was interrupted, that's fine
|
|
2192
|
+
}
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
String getStoredNativeBuildVersion() {
|
|
2197
|
+
String previous = this.prefs.getString("LatestNativeBuildVersion", "");
|
|
2198
|
+
if (previous == null || previous.isEmpty()) {
|
|
2199
|
+
previous = this.prefs.getString("LatestVersionNative", "");
|
|
2200
|
+
}
|
|
2201
|
+
return previous == null ? "" : previous;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
void persistCurrentNativeBuildVersion() {
|
|
2205
|
+
this.editor.putString("LatestNativeBuildVersion", this.currentBuildVersion);
|
|
2206
|
+
this.editor.apply();
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
private void waitForCleanupIfNeeded() {
|
|
2210
|
+
if (cleanupComplete) {
|
|
2211
|
+
return; // Already done, no need to wait
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
logger.info("Waiting for cleanup to complete before starting download...");
|
|
2215
|
+
|
|
2216
|
+
// Wait for cleanup to complete - blocks until lock is released
|
|
2217
|
+
synchronized (cleanupLock) {
|
|
2218
|
+
logger.info("Cleanup finished, proceeding with download");
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
public void notifyDownload(final String id, final int percent) {
|
|
2223
|
+
try {
|
|
2224
|
+
final JSObject ret = new JSObject();
|
|
2225
|
+
ret.put("percent", percent);
|
|
2226
|
+
final BundleInfo bundleInfo = this.implementation.getBundleInfo(id);
|
|
2227
|
+
ret.put("bundle", InternalUtils.mapToJSObject(bundleInfo.toJSONMap()));
|
|
2228
|
+
this.notifyListeners("download", ret);
|
|
2229
|
+
|
|
2230
|
+
if (percent == 100) {
|
|
2231
|
+
final JSObject retDownloadComplete = new JSObject(ret, new String[] { "bundle" });
|
|
2232
|
+
this.notifyListeners("downloadComplete", retDownloadComplete);
|
|
2233
|
+
this.implementation.sendStats("download_complete", bundleInfo.getVersionName());
|
|
2234
|
+
lastNotifiedStatPercent = 100;
|
|
2235
|
+
} else {
|
|
2236
|
+
int currentStatPercent = (percent / 10) * 10; // Round down to nearest 10
|
|
2237
|
+
if (currentStatPercent > lastNotifiedStatPercent) {
|
|
2238
|
+
this.implementation.sendStats("download_" + currentStatPercent, bundleInfo.getVersionName());
|
|
2239
|
+
lastNotifiedStatPercent = currentStatPercent;
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
} catch (final Exception e) {
|
|
2243
|
+
logger.error("Could not notify listeners " + e.getMessage());
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
@PluginMethod
|
|
2248
|
+
public void setUpdateUrl(final PluginCall call) {
|
|
2249
|
+
if (!this.getConfig().getBoolean("allowModifyUrl", false)) {
|
|
2250
|
+
logger.error("setUpdateUrl not allowed set allowModifyUrl in your config to true to allow it");
|
|
2251
|
+
call.reject("setUpdateUrl not allowed");
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
2254
|
+
final String url = call.getString("url");
|
|
2255
|
+
if (url == null) {
|
|
2256
|
+
logger.error("setUpdateUrl called without url");
|
|
2257
|
+
call.reject("setUpdateUrl called without url");
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
2260
|
+
this.updateUrl = url;
|
|
2261
|
+
if (Boolean.TRUE.equals(this.persistModifyUrl)) {
|
|
2262
|
+
this.editor.putString(UPDATE_URL_PREF_KEY, url);
|
|
2263
|
+
this.editor.apply();
|
|
2264
|
+
}
|
|
2265
|
+
call.resolve();
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
@PluginMethod
|
|
2269
|
+
public void setStatsUrl(final PluginCall call) {
|
|
2270
|
+
if (!this.getConfig().getBoolean("allowModifyUrl", false)) {
|
|
2271
|
+
logger.error("setStatsUrl not allowed set allowModifyUrl in your config to true to allow it");
|
|
2272
|
+
call.reject("setStatsUrl not allowed");
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
final String url = call.getString("url");
|
|
2276
|
+
if (url == null) {
|
|
2277
|
+
logger.error("setStatsUrl called without url");
|
|
2278
|
+
call.reject("setStatsUrl called without url");
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
2281
|
+
this.implementation.statsUrl = url;
|
|
2282
|
+
if (Boolean.TRUE.equals(this.persistModifyUrl)) {
|
|
2283
|
+
this.editor.putString(STATS_URL_PREF_KEY, url);
|
|
2284
|
+
this.editor.apply();
|
|
2285
|
+
}
|
|
2286
|
+
call.resolve();
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
@PluginMethod
|
|
2290
|
+
public void setChannelUrl(final PluginCall call) {
|
|
2291
|
+
if (!this.getConfig().getBoolean("allowModifyUrl", false)) {
|
|
2292
|
+
logger.error("setChannelUrl not allowed set allowModifyUrl in your config to true to allow it");
|
|
2293
|
+
call.reject("setChannelUrl not allowed");
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
2296
|
+
final String url = call.getString("url");
|
|
2297
|
+
if (url == null) {
|
|
2298
|
+
logger.error("setChannelUrl called without url");
|
|
2299
|
+
call.reject("setChannelUrl called without url");
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
2302
|
+
this.implementation.channelUrl = url;
|
|
2303
|
+
if (Boolean.TRUE.equals(this.persistModifyUrl)) {
|
|
2304
|
+
this.editor.putString(CHANNEL_URL_PREF_KEY, url);
|
|
2305
|
+
this.editor.apply();
|
|
2306
|
+
}
|
|
2307
|
+
call.resolve();
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
@PluginMethod
|
|
2311
|
+
public void getBuiltinVersion(final PluginCall call) {
|
|
2312
|
+
try {
|
|
2313
|
+
final JSObject ret = new JSObject();
|
|
2314
|
+
ret.put("version", this.implementation.versionBuild);
|
|
2315
|
+
call.resolve(ret);
|
|
2316
|
+
} catch (final Exception e) {
|
|
2317
|
+
logger.error("Could not get version " + e.getMessage());
|
|
2318
|
+
call.reject("Could not get version", e);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
@PluginMethod
|
|
2323
|
+
public void getDeviceId(final PluginCall call) {
|
|
2324
|
+
try {
|
|
2325
|
+
final JSObject ret = new JSObject();
|
|
2326
|
+
ret.put("deviceId", this.implementation.deviceID);
|
|
2327
|
+
call.resolve(ret);
|
|
2328
|
+
} catch (final Exception e) {
|
|
2329
|
+
logger.error("Could not get device id " + e.getMessage());
|
|
2330
|
+
call.reject("Could not get device id", e);
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
@PluginMethod
|
|
2335
|
+
public void setCustomId(final PluginCall call) {
|
|
2336
|
+
final String customId = call.getString("customId");
|
|
2337
|
+
if (customId == null) {
|
|
2338
|
+
logger.error("setCustomId called without customId");
|
|
2339
|
+
call.reject("setCustomId called without customId");
|
|
2340
|
+
return;
|
|
2341
|
+
}
|
|
2342
|
+
this.implementation.customId = customId;
|
|
2343
|
+
if (Boolean.TRUE.equals(this.persistCustomId)) {
|
|
1131
2344
|
if (customId.isEmpty()) {
|
|
1132
2345
|
this.editor.remove(CUSTOM_ID_PREF_KEY);
|
|
1133
2346
|
} else {
|
|
1134
|
-
this.editor.putString(CUSTOM_ID_PREF_KEY, customId);
|
|
2347
|
+
this.editor.putString(CUSTOM_ID_PREF_KEY, customId);
|
|
2348
|
+
}
|
|
2349
|
+
this.editor.apply();
|
|
2350
|
+
}
|
|
2351
|
+
call.resolve();
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
@PluginMethod
|
|
2355
|
+
public void getPluginVersion(final PluginCall call) {
|
|
2356
|
+
try {
|
|
2357
|
+
final JSObject ret = new JSObject();
|
|
2358
|
+
ret.put("version", this.pluginVersion);
|
|
2359
|
+
call.resolve(ret);
|
|
2360
|
+
} catch (final Exception e) {
|
|
2361
|
+
logger.error("Could not get plugin version " + e.getMessage());
|
|
2362
|
+
call.reject("Could not get plugin version", e);
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
@PluginMethod
|
|
2367
|
+
public void unsetChannel(final PluginCall call) {
|
|
2368
|
+
final Boolean triggerAutoUpdate = call.getBoolean("triggerAutoUpdate", false);
|
|
2369
|
+
|
|
2370
|
+
try {
|
|
2371
|
+
logger.info("unsetChannel triggerAutoUpdate: " + triggerAutoUpdate);
|
|
2372
|
+
startNewThread(() -> {
|
|
2373
|
+
String configDefaultChannel = CapacitorUpdaterPlugin.this.getConfig().getString("defaultChannel", "");
|
|
2374
|
+
CapacitorUpdaterPlugin.this.implementation.unsetChannel(
|
|
2375
|
+
CapacitorUpdaterPlugin.this.editor,
|
|
2376
|
+
DEFAULT_CHANNEL_PREF_KEY,
|
|
2377
|
+
configDefaultChannel,
|
|
2378
|
+
(res) -> {
|
|
2379
|
+
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
2380
|
+
if (jsRes.has("error")) {
|
|
2381
|
+
String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
|
|
2382
|
+
String errorCode = jsRes.getString("error");
|
|
2383
|
+
|
|
2384
|
+
JSObject errorObj = new JSObject();
|
|
2385
|
+
errorObj.put("message", errorMessage);
|
|
2386
|
+
errorObj.put("error", errorCode);
|
|
2387
|
+
|
|
2388
|
+
call.reject(errorMessage, "UNSETCHANNEL_FAILED", null, errorObj);
|
|
2389
|
+
} else {
|
|
2390
|
+
if (CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() && Boolean.TRUE.equals(triggerAutoUpdate)) {
|
|
2391
|
+
logger.info("Calling autoupdater after channel change!");
|
|
2392
|
+
// Check if download is already in progress (with timeout protection)
|
|
2393
|
+
if (!this.isDownloadStuckOrTimedOut()) {
|
|
2394
|
+
backgroundDownload();
|
|
2395
|
+
} else {
|
|
2396
|
+
logger.info("Download already in progress, skipping duplicate download request");
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
call.resolve(jsRes);
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
);
|
|
2403
|
+
});
|
|
2404
|
+
} catch (final Exception e) {
|
|
2405
|
+
logger.error("Failed to unsetChannel: " + e.getMessage());
|
|
2406
|
+
call.reject("Failed to unsetChannel: ", e);
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
@PluginMethod
|
|
2411
|
+
public void setChannel(final PluginCall call) {
|
|
2412
|
+
final String channel = call.getString("channel");
|
|
2413
|
+
final Boolean triggerAutoUpdate = call.getBoolean("triggerAutoUpdate", false);
|
|
2414
|
+
|
|
2415
|
+
if (channel == null) {
|
|
2416
|
+
logger.error("setChannel called without channel");
|
|
2417
|
+
JSObject errorObj = new JSObject();
|
|
2418
|
+
errorObj.put("message", "setChannel called without channel");
|
|
2419
|
+
errorObj.put("error", "missing_parameter");
|
|
2420
|
+
call.reject("setChannel called without channel", "SETCHANNEL_INVALID_PARAMS", null, errorObj);
|
|
2421
|
+
return;
|
|
2422
|
+
}
|
|
2423
|
+
try {
|
|
2424
|
+
logger.info("setChannel " + channel + " triggerAutoUpdate: " + triggerAutoUpdate);
|
|
2425
|
+
startNewThread(() ->
|
|
2426
|
+
CapacitorUpdaterPlugin.this.implementation.setChannel(
|
|
2427
|
+
channel,
|
|
2428
|
+
CapacitorUpdaterPlugin.this.editor,
|
|
2429
|
+
DEFAULT_CHANNEL_PREF_KEY,
|
|
2430
|
+
CapacitorUpdaterPlugin.this.allowSetDefaultChannel,
|
|
2431
|
+
CapacitorUpdaterPlugin.this.getConfig().getString("defaultChannel", ""),
|
|
2432
|
+
(res) -> {
|
|
2433
|
+
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
2434
|
+
if (jsRes.has("error")) {
|
|
2435
|
+
String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
|
|
2436
|
+
String errorCode = jsRes.getString("error");
|
|
2437
|
+
|
|
2438
|
+
// Fire channelPrivate event if channel doesn't allow self-assignment
|
|
2439
|
+
if (
|
|
2440
|
+
errorCode.contains("cannot_update_via_private_channel") ||
|
|
2441
|
+
errorCode.contains("channel_self_set_not_allowed")
|
|
2442
|
+
) {
|
|
2443
|
+
JSObject eventData = new JSObject();
|
|
2444
|
+
eventData.put("channel", channel);
|
|
2445
|
+
eventData.put("message", errorMessage);
|
|
2446
|
+
notifyListeners("channelPrivate", eventData);
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
JSObject errorObj = new JSObject();
|
|
2450
|
+
errorObj.put("message", errorMessage);
|
|
2451
|
+
errorObj.put("error", errorCode);
|
|
2452
|
+
|
|
2453
|
+
call.reject(errorMessage, "SETCHANNEL_FAILED", null, errorObj);
|
|
2454
|
+
} else {
|
|
2455
|
+
if (CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() && Boolean.TRUE.equals(triggerAutoUpdate)) {
|
|
2456
|
+
logger.info("Calling autoupdater after channel change!");
|
|
2457
|
+
// Check if download is already in progress (with timeout protection)
|
|
2458
|
+
if (!this.isDownloadStuckOrTimedOut()) {
|
|
2459
|
+
backgroundDownload();
|
|
2460
|
+
} else {
|
|
2461
|
+
logger.info("Download already in progress, skipping duplicate download request");
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
call.resolve(jsRes);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
)
|
|
2468
|
+
);
|
|
2469
|
+
} catch (final Exception e) {
|
|
2470
|
+
logger.error("Failed to setChannel: " + channel + " " + e.getMessage());
|
|
2471
|
+
call.reject("Failed to setChannel: " + channel, e);
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
@PluginMethod
|
|
2476
|
+
public void getChannel(final PluginCall call) {
|
|
2477
|
+
try {
|
|
2478
|
+
logger.info("getChannel");
|
|
2479
|
+
startNewThread(() ->
|
|
2480
|
+
CapacitorUpdaterPlugin.this.implementation.getChannel(
|
|
2481
|
+
(res) -> {
|
|
2482
|
+
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
2483
|
+
if (jsRes.has("error")) {
|
|
2484
|
+
String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
|
|
2485
|
+
String errorCode = jsRes.getString("error");
|
|
2486
|
+
|
|
2487
|
+
JSObject errorObj = new JSObject();
|
|
2488
|
+
errorObj.put("message", errorMessage);
|
|
2489
|
+
errorObj.put("error", errorCode);
|
|
2490
|
+
|
|
2491
|
+
call.reject(errorMessage, "GETCHANNEL_FAILED", null, errorObj);
|
|
2492
|
+
} else {
|
|
2493
|
+
call.resolve(jsRes);
|
|
2494
|
+
}
|
|
2495
|
+
},
|
|
2496
|
+
CapacitorUpdaterPlugin.this.editor,
|
|
2497
|
+
DEFAULT_CHANNEL_PREF_KEY
|
|
2498
|
+
)
|
|
2499
|
+
);
|
|
2500
|
+
} catch (final Exception e) {
|
|
2501
|
+
logger.error("Failed to getChannel " + e.getMessage());
|
|
2502
|
+
call.reject("Failed to getChannel", e);
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
@PluginMethod
|
|
2507
|
+
public void listChannels(final PluginCall call) {
|
|
2508
|
+
try {
|
|
2509
|
+
logger.info("listChannels");
|
|
2510
|
+
startNewThread(() ->
|
|
2511
|
+
CapacitorUpdaterPlugin.this.implementation.listChannels((res) -> {
|
|
2512
|
+
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
2513
|
+
Object channels = res.get("channels");
|
|
2514
|
+
if (channels instanceof List<?> channelsList) {
|
|
2515
|
+
JSArray channelsArray = new JSArray();
|
|
2516
|
+
for (Object channel : channelsList) {
|
|
2517
|
+
if (channel instanceof Map<?, ?> channelMap) {
|
|
2518
|
+
JSObject channelObject = new JSObject();
|
|
2519
|
+
for (Map.Entry<?, ?> entry : channelMap.entrySet()) {
|
|
2520
|
+
Object key = entry.getKey();
|
|
2521
|
+
if (key != null) {
|
|
2522
|
+
channelObject.put(key.toString(), entry.getValue());
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
channelsArray.put(channelObject);
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
jsRes.put("channels", channelsArray);
|
|
2529
|
+
}
|
|
2530
|
+
if (jsRes.has("error")) {
|
|
2531
|
+
String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
|
|
2532
|
+
String errorCode = jsRes.getString("error");
|
|
2533
|
+
|
|
2534
|
+
JSObject errorObj = new JSObject();
|
|
2535
|
+
errorObj.put("message", errorMessage);
|
|
2536
|
+
errorObj.put("error", errorCode);
|
|
2537
|
+
|
|
2538
|
+
call.reject(errorMessage, "LISTCHANNELS_FAILED", null, errorObj);
|
|
2539
|
+
} else {
|
|
2540
|
+
call.resolve(jsRes);
|
|
2541
|
+
}
|
|
2542
|
+
})
|
|
2543
|
+
);
|
|
2544
|
+
} catch (final Exception e) {
|
|
2545
|
+
logger.error("Failed to listChannels: " + e.getMessage());
|
|
2546
|
+
call.reject("Failed to listChannels", e);
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
private BundleInfo downloadBundle(
|
|
2551
|
+
final String url,
|
|
2552
|
+
final String version,
|
|
2553
|
+
final String sessionKey,
|
|
2554
|
+
final String checksum,
|
|
2555
|
+
final JSONArray manifest
|
|
2556
|
+
) throws IOException {
|
|
2557
|
+
if (manifest != null) {
|
|
2558
|
+
return this.implementation.downloadManifest(url, version, sessionKey, checksum, manifest);
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
return this.implementation.download(url, version, sessionKey, checksum);
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
@PluginMethod
|
|
2565
|
+
public void download(final PluginCall call) {
|
|
2566
|
+
final String url = call.getString("url");
|
|
2567
|
+
final String version = call.getString("version");
|
|
2568
|
+
final String sessionKey = call.getString("sessionKey", "");
|
|
2569
|
+
final String checksum = call.getString("checksum", "");
|
|
2570
|
+
final JSONArray manifest = call.getData().optJSONArray("manifest");
|
|
2571
|
+
if (url == null) {
|
|
2572
|
+
logger.error("Download called without url");
|
|
2573
|
+
call.reject("Download called without url");
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
if (version == null) {
|
|
2577
|
+
logger.error("Download called without version");
|
|
2578
|
+
call.reject("Download called without version");
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2581
|
+
try {
|
|
2582
|
+
logger.info("Downloading " + url);
|
|
2583
|
+
startNewThread(() -> {
|
|
2584
|
+
try {
|
|
2585
|
+
final BundleInfo downloaded = this.downloadBundle(url, version, sessionKey, checksum, manifest);
|
|
2586
|
+
if (downloaded.isErrorStatus()) {
|
|
2587
|
+
throw new RuntimeException("Download failed: " + downloaded.getStatus());
|
|
2588
|
+
} else {
|
|
2589
|
+
call.resolve(InternalUtils.mapToJSObject(downloaded.toJSONMap()));
|
|
2590
|
+
}
|
|
2591
|
+
} catch (final Exception e) {
|
|
2592
|
+
logger.error("Failed to download from: " + url + " " + e.getMessage());
|
|
2593
|
+
call.reject("Failed to download from: " + url, e);
|
|
2594
|
+
final JSObject ret = new JSObject();
|
|
2595
|
+
ret.put("version", version);
|
|
2596
|
+
CapacitorUpdaterPlugin.this.notifyListeners("downloadFailed", ret);
|
|
2597
|
+
final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
|
|
2598
|
+
CapacitorUpdaterPlugin.this.implementation.sendStats("download_fail", current.getVersionName());
|
|
2599
|
+
}
|
|
2600
|
+
});
|
|
2601
|
+
} catch (final Exception e) {
|
|
2602
|
+
logger.error("Failed to download from: " + url + " " + e.getMessage());
|
|
2603
|
+
call.reject("Failed to download from: " + url, e);
|
|
2604
|
+
final JSObject ret = new JSObject();
|
|
2605
|
+
ret.put("version", version);
|
|
2606
|
+
CapacitorUpdaterPlugin.this.notifyListeners("downloadFailed", ret);
|
|
2607
|
+
final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
|
|
2608
|
+
CapacitorUpdaterPlugin.this.implementation.sendStats("download_fail", current.getVersionName());
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
private void syncKeepUrlPathFlag(final boolean enabled) {
|
|
2613
|
+
if (this.bridge == null || this.bridge.getWebView() == null) {
|
|
2614
|
+
return;
|
|
2615
|
+
}
|
|
2616
|
+
final String script = enabled
|
|
2617
|
+
? "(function(){try{localStorage.setItem('" +
|
|
2618
|
+
KEEP_URL_FLAG_KEY +
|
|
2619
|
+
"','1');}catch(e){}window.__capgoKeepUrlPathAfterReload=true;var evt;try{evt=new CustomEvent('CapacitorUpdaterKeepUrlPathAfterReload',{detail:{enabled:true}});}catch(err){evt=document.createEvent('CustomEvent');evt.initCustomEvent('CapacitorUpdaterKeepUrlPathAfterReload',false,false,{enabled:true});}window.dispatchEvent(evt);})();"
|
|
2620
|
+
: "(function(){try{localStorage.removeItem('" +
|
|
2621
|
+
KEEP_URL_FLAG_KEY +
|
|
2622
|
+
"');}catch(e){}delete window.__capgoKeepUrlPathAfterReload;var evt;try{evt=new CustomEvent('CapacitorUpdaterKeepUrlPathAfterReload',{detail:{enabled:false}});}catch(err){evt=document.createEvent('CustomEvent');evt.initCustomEvent('CapacitorUpdaterKeepUrlPathAfterReload',false,false,{enabled:false});}window.dispatchEvent(evt);})();";
|
|
2623
|
+
this.bridge.getWebView().post(() -> this.bridge.getWebView().evaluateJavascript(script, null));
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
private void applyCurrentBundleToBridge() {
|
|
2627
|
+
final String path = this.implementation.getCurrentBundlePath();
|
|
2628
|
+
final boolean usingBuiltin = this.implementation.isUsingBuiltin();
|
|
2629
|
+
if (this.keepUrlPathAfterReload) {
|
|
2630
|
+
this.syncKeepUrlPathFlag(true);
|
|
2631
|
+
}
|
|
2632
|
+
logger.info("Reloading: " + path);
|
|
2633
|
+
|
|
2634
|
+
AtomicReference<URL> url = new AtomicReference<>();
|
|
2635
|
+
if (this.keepUrlPathAfterReload) {
|
|
2636
|
+
try {
|
|
2637
|
+
if (Looper.myLooper() != Looper.getMainLooper()) {
|
|
2638
|
+
Semaphore mainThreadSemaphore = new Semaphore(0);
|
|
2639
|
+
this.bridge.executeOnMainThread(() -> {
|
|
2640
|
+
try {
|
|
2641
|
+
if (this.bridge != null && this.bridge.getWebView() != null) {
|
|
2642
|
+
String currentUrl = this.bridge.getWebView().getUrl();
|
|
2643
|
+
if (currentUrl != null) {
|
|
2644
|
+
url.set(new URL(currentUrl));
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
} catch (Exception e) {
|
|
2648
|
+
logger.error("Error executing on main thread " + e.getMessage());
|
|
2649
|
+
}
|
|
2650
|
+
mainThreadSemaphore.release();
|
|
2651
|
+
});
|
|
2652
|
+
|
|
2653
|
+
// Add timeout to prevent indefinite blocking
|
|
2654
|
+
if (!mainThreadSemaphore.tryAcquire(10, TimeUnit.SECONDS)) {
|
|
2655
|
+
logger.error("Timeout waiting for main thread operation");
|
|
2656
|
+
}
|
|
2657
|
+
} else {
|
|
2658
|
+
try {
|
|
2659
|
+
if (this.bridge != null && this.bridge.getWebView() != null) {
|
|
2660
|
+
String currentUrl = this.bridge.getWebView().getUrl();
|
|
2661
|
+
if (currentUrl != null) {
|
|
2662
|
+
url.set(new URL(currentUrl));
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
} catch (Exception e) {
|
|
2666
|
+
logger.error("Error executing on main thread " + e.getMessage());
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
} catch (InterruptedException e) {
|
|
2670
|
+
logger.error("Error waiting for main thread or getting the current URL from webview " + e.getMessage());
|
|
2671
|
+
Thread.currentThread().interrupt(); // Restore interrupted status
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
if (url.get() != null) {
|
|
2676
|
+
if (usingBuiltin) {
|
|
2677
|
+
this.bridge.getLocalServer().hostAssets(path);
|
|
2678
|
+
} else {
|
|
2679
|
+
this.bridge.getLocalServer().hostFiles(path);
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
try {
|
|
2683
|
+
URL finalUrl = null;
|
|
2684
|
+
finalUrl = new URL(this.bridge.getAppUrl());
|
|
2685
|
+
finalUrl = new URL(finalUrl.getProtocol(), finalUrl.getHost(), finalUrl.getPort(), url.get().getPath());
|
|
2686
|
+
URL finalUrl1 = finalUrl;
|
|
2687
|
+
this.bridge.getWebView().post(() -> {
|
|
2688
|
+
this.bridge.getWebView().loadUrl(finalUrl1.toString());
|
|
2689
|
+
if (!this.keepUrlPathAfterReload) {
|
|
2690
|
+
this.bridge.getWebView().clearHistory();
|
|
2691
|
+
}
|
|
2692
|
+
});
|
|
2693
|
+
} catch (MalformedURLException e) {
|
|
2694
|
+
logger.error("Cannot get finalUrl from capacitor bridge " + e.getMessage());
|
|
2695
|
+
|
|
2696
|
+
if (usingBuiltin) {
|
|
2697
|
+
this.bridge.setServerAssetPath(path);
|
|
2698
|
+
} else {
|
|
2699
|
+
this.bridge.setServerBasePath(path);
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
} else {
|
|
2703
|
+
if (usingBuiltin) {
|
|
2704
|
+
this.bridge.setServerAssetPath(path);
|
|
2705
|
+
} else {
|
|
2706
|
+
this.bridge.setServerBasePath(path);
|
|
2707
|
+
}
|
|
2708
|
+
if (this.bridge != null && this.bridge.getWebView() != null) {
|
|
2709
|
+
this.bridge.getWebView().post(() -> {
|
|
2710
|
+
if (this.bridge.getWebView() != null) {
|
|
2711
|
+
this.bridge.getWebView().loadUrl(this.bridge.getAppUrl());
|
|
2712
|
+
if (!this.keepUrlPathAfterReload) {
|
|
2713
|
+
this.bridge.getWebView().clearHistory();
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
});
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
protected void restoreLiveBundleStateAfterFailedReload() {
|
|
2722
|
+
try {
|
|
2723
|
+
this.applyCurrentBundleToBridge();
|
|
2724
|
+
} catch (final Exception e) {
|
|
2725
|
+
logger.warn("Failed to restore live bundle after rejected reload: " + e.getMessage());
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
protected boolean _reload() {
|
|
2730
|
+
final int phase = this.semaphoreUp();
|
|
2731
|
+
this.applyCurrentBundleToBridge();
|
|
2732
|
+
|
|
2733
|
+
final long waitTimeMs = this.resolveAppReadyCheckTimeoutMs();
|
|
2734
|
+
this.checkAppReady(waitTimeMs);
|
|
2735
|
+
this.notifyListeners("appReloaded", new JSObject());
|
|
2736
|
+
|
|
2737
|
+
// Wait for the reload to complete (until notifyAppReady is called)
|
|
2738
|
+
return this.semaphoreWait(phase, waitTimeMs);
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
protected boolean reloadWithoutWaitingForAppReady() {
|
|
2742
|
+
this.applyCurrentBundleToBridge();
|
|
2743
|
+
|
|
2744
|
+
final long waitTimeMs = this.resolveAppReadyCheckTimeoutMs();
|
|
2745
|
+
this.checkAppReady(waitTimeMs);
|
|
2746
|
+
this.notifyListeners("appReloaded", new JSObject());
|
|
2747
|
+
return true;
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
@PluginMethod
|
|
2751
|
+
public void reload(final PluginCall call) {
|
|
2752
|
+
startNewThread(() -> {
|
|
2753
|
+
try {
|
|
2754
|
+
final BundleInfo current = this.implementation.getCurrentBundle();
|
|
2755
|
+
final BundleInfo next = this.implementation.getNextBundle();
|
|
2756
|
+
|
|
2757
|
+
if (!this.isPreviewSessionStateActive() && next != null && !next.isErrorStatus() && !next.getId().equals(current.getId())) {
|
|
2758
|
+
final CapgoUpdater.ResetState previousState = this.implementation.captureResetState();
|
|
2759
|
+
final String previousBundleName = this.implementation.getCurrentBundle().getVersionName();
|
|
2760
|
+
logger.info("Applying pending bundle before reload: " + next.getVersionName());
|
|
2761
|
+
final boolean didApplyPendingBundle;
|
|
2762
|
+
if (next.isBuiltin()) {
|
|
2763
|
+
this.implementation.prepareResetStateForTransition();
|
|
2764
|
+
didApplyPendingBundle = true;
|
|
2765
|
+
} else {
|
|
2766
|
+
didApplyPendingBundle = this.implementation.stagePendingReload(next);
|
|
2767
|
+
}
|
|
2768
|
+
if (didApplyPendingBundle && this._reload()) {
|
|
2769
|
+
if (next.isBuiltin()) {
|
|
2770
|
+
this.implementation.finalizeResetTransition(previousBundleName, false);
|
|
2771
|
+
} else {
|
|
2772
|
+
this.implementation.finalizePendingReload(next, previousBundleName);
|
|
2773
|
+
}
|
|
2774
|
+
this.notifyBundleSet(next);
|
|
2775
|
+
this.implementation.setNextBundle(null);
|
|
2776
|
+
this.showPreviewSessionNoticeIfNeeded();
|
|
2777
|
+
call.resolve();
|
|
2778
|
+
return;
|
|
2779
|
+
}
|
|
2780
|
+
this.implementation.restoreResetState(previousState);
|
|
2781
|
+
this.restoreLiveBundleStateAfterFailedReload();
|
|
2782
|
+
logger.error("Reload failed after applying pending bundle: " + next.getVersionName());
|
|
2783
|
+
call.reject("Reload failed after applying pending bundle: " + next.getVersionName());
|
|
2784
|
+
return;
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
if (this._reload()) {
|
|
2788
|
+
this.showPreviewSessionNoticeIfNeeded();
|
|
2789
|
+
call.resolve();
|
|
2790
|
+
} else {
|
|
2791
|
+
logger.error("Reload failed");
|
|
2792
|
+
call.reject("Reload failed");
|
|
2793
|
+
}
|
|
2794
|
+
} catch (final Exception e) {
|
|
2795
|
+
logger.error("Could not reload " + e.getMessage());
|
|
2796
|
+
call.reject("Could not reload", e);
|
|
2797
|
+
}
|
|
2798
|
+
});
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
@PluginMethod
|
|
2802
|
+
public void next(final PluginCall call) {
|
|
2803
|
+
final String id = call.getString("id");
|
|
2804
|
+
if (id == null) {
|
|
2805
|
+
logger.error("Next called without id");
|
|
2806
|
+
call.reject("Next called without id");
|
|
2807
|
+
return;
|
|
2808
|
+
}
|
|
2809
|
+
try {
|
|
2810
|
+
logger.info("Setting next active id " + id);
|
|
2811
|
+
if (!this.implementation.setNextBundle(id)) {
|
|
2812
|
+
logger.error("Set next id failed. Bundle " + id + " does not exist.");
|
|
2813
|
+
call.reject("Set next id failed. Bundle " + id + " does not exist.");
|
|
2814
|
+
} else {
|
|
2815
|
+
call.resolve(InternalUtils.mapToJSObject(this.implementation.getBundleInfo(id).toJSONMap()));
|
|
2816
|
+
}
|
|
2817
|
+
} catch (final Exception e) {
|
|
2818
|
+
logger.error("Could not set next id " + id + " " + e.getMessage());
|
|
2819
|
+
call.reject("Could not set next id: " + id, e);
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
@PluginMethod
|
|
2824
|
+
public void set(final PluginCall call) {
|
|
2825
|
+
final String id = call.getString("id");
|
|
2826
|
+
if (id == null) {
|
|
2827
|
+
logger.error("Set called without id");
|
|
2828
|
+
call.reject("Set called without id");
|
|
2829
|
+
return;
|
|
2830
|
+
}
|
|
2831
|
+
startNewThread(() -> {
|
|
2832
|
+
try {
|
|
2833
|
+
logger.info("Setting active bundle " + id);
|
|
2834
|
+
if (!this.implementation.set(id)) {
|
|
2835
|
+
logger.info("No such bundle " + id);
|
|
2836
|
+
call.reject("Update failed, id " + id + " does not exist.");
|
|
2837
|
+
} else if (Boolean.TRUE.equals(this.previewSessionEnabled)) {
|
|
2838
|
+
logger.info("Preview session set active bundle " + id + " without waiting for preview app readiness");
|
|
2839
|
+
final BundleInfo bundle = this.implementation.getBundleInfo(id);
|
|
2840
|
+
this.recordPreviewBundle(bundle);
|
|
2841
|
+
this.reloadWithoutWaitingForAppReady();
|
|
2842
|
+
this.notifyBundleSet(bundle);
|
|
2843
|
+
this.showPreviewSessionNoticeIfNeeded();
|
|
2844
|
+
call.resolve();
|
|
2845
|
+
} else if (!this._reload()) {
|
|
2846
|
+
logger.error("Reload failed after setting bundle " + id);
|
|
2847
|
+
call.reject("Reload failed after setting bundle " + id);
|
|
2848
|
+
} else {
|
|
2849
|
+
logger.info("Bundle successfully set to " + id);
|
|
2850
|
+
this.notifyBundleSet(this.implementation.getBundleInfo(id));
|
|
2851
|
+
this.showPreviewSessionNoticeIfNeeded();
|
|
2852
|
+
call.resolve();
|
|
2853
|
+
}
|
|
2854
|
+
} catch (final Exception e) {
|
|
2855
|
+
logger.error("Could not set id " + id + " " + e.getMessage());
|
|
2856
|
+
call.reject("Could not set id " + id, e);
|
|
2857
|
+
}
|
|
2858
|
+
});
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
private boolean preparePreviewFallbackIfNeeded() {
|
|
2862
|
+
if (Boolean.TRUE.equals(this.previewSessionEnabled)) {
|
|
2863
|
+
return true;
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
final BundleInfo current = this.implementation.getCurrentBundle();
|
|
2867
|
+
if (!this.implementation.setPreviewFallbackBundle(current.getId())) {
|
|
2868
|
+
logger.error("Could not save current bundle as preview fallback");
|
|
2869
|
+
return false;
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
final BundleInfo previousNext = this.implementation.getNextBundle();
|
|
2873
|
+
if (previousNext == null || previousNext.isDeleted() || previousNext.isErrorStatus()) {
|
|
2874
|
+
this.editor.remove(PREVIEW_PREVIOUS_NEXT_BUNDLE_PREF_KEY);
|
|
2875
|
+
} else {
|
|
2876
|
+
this.editor.putString(PREVIEW_PREVIOUS_NEXT_BUNDLE_PREF_KEY, previousNext.getId());
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
this.editor.putString(PREVIEW_PREVIOUS_APP_ID_PREF_KEY, this.implementation.appId);
|
|
2880
|
+
if (this.prefs.contains(DEFAULT_CHANNEL_PREF_KEY)) {
|
|
2881
|
+
this.editor.putString(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_PREF_KEY, this.prefs.getString(DEFAULT_CHANNEL_PREF_KEY, ""));
|
|
2882
|
+
this.editor.putBoolean(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_WAS_SET_PREF_KEY, true);
|
|
2883
|
+
} else {
|
|
2884
|
+
this.editor.remove(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_PREF_KEY);
|
|
2885
|
+
this.editor.putBoolean(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_WAS_SET_PREF_KEY, false);
|
|
2886
|
+
}
|
|
2887
|
+
this.editor.putBoolean(PREVIEW_PREVIOUS_SHAKE_MENU_PREF_KEY, Boolean.TRUE.equals(this.shakeMenuEnabled));
|
|
2888
|
+
this.editor.putBoolean(PREVIEW_PREVIOUS_SHAKE_CHANNEL_SELECTOR_PREF_KEY, Boolean.TRUE.equals(this.shakeChannelSelectorEnabled));
|
|
2889
|
+
logger.info("Preview session started with fallback bundle: " + current);
|
|
2890
|
+
return true;
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
private void activatePreviewSessionState() {
|
|
2894
|
+
this.clearIncomingPreviewTransition();
|
|
2895
|
+
this.hidePreviewTransitionLoader("preview-session-started");
|
|
2896
|
+
this.previewSessionEnabled = true;
|
|
2897
|
+
this.previewSessionAlertPending = true;
|
|
2898
|
+
this.implementation.previewSession = true;
|
|
2899
|
+
this.shakeMenuEnabled = true;
|
|
2900
|
+
this.editor.putBoolean(PREVIEW_SESSION_PREF_KEY, true);
|
|
2901
|
+
this.editor.putBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, true);
|
|
2902
|
+
this.editor.apply();
|
|
2903
|
+
this.syncShakeMenuLifecycle();
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
@PluginMethod
|
|
2907
|
+
public void startPreviewSession(final PluginCall call) {
|
|
2908
|
+
if (!Boolean.TRUE.equals(this.allowPreview)) {
|
|
2909
|
+
this.hidePreviewTransitionLoader("preview-session-not-allowed");
|
|
2910
|
+
logger.error("startPreviewSession not allowed set allowPreview in your config to true to enable it");
|
|
2911
|
+
call.reject("startPreviewSession not allowed");
|
|
2912
|
+
return;
|
|
2913
|
+
}
|
|
2914
|
+
final String previewAppId = this.normalizePreviewAppId(call.getString("appId"));
|
|
2915
|
+
final String rawPayloadUrl = call.getString("payloadUrl");
|
|
2916
|
+
final String previewPayloadUrl = this.normalizePreviewPayloadUrl(rawPayloadUrl);
|
|
2917
|
+
if (this.hasPreviewPayloadUrl(rawPayloadUrl) && previewPayloadUrl == null) {
|
|
2918
|
+
this.hidePreviewTransitionLoader("preview-session-invalid-payload");
|
|
2919
|
+
logger.error("startPreviewSession called with invalid payloadUrl");
|
|
2920
|
+
call.reject("Invalid preview payloadUrl");
|
|
2921
|
+
return;
|
|
2922
|
+
}
|
|
2923
|
+
startNewThread(() -> {
|
|
2924
|
+
try {
|
|
2925
|
+
if (!this.preparePreviewFallbackIfNeeded()) {
|
|
2926
|
+
this.hidePreviewTransitionLoader("preview-session-fallback-failed");
|
|
2927
|
+
call.reject("Could not save current bundle as preview fallback");
|
|
2928
|
+
return;
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
if (previewAppId != null) {
|
|
2932
|
+
this.setActiveAppId(previewAppId);
|
|
2933
|
+
this.editor.putString(PREVIEW_APP_ID_PREF_KEY, previewAppId);
|
|
2934
|
+
logger.info("Preview session using appId: " + previewAppId);
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
if (previewPayloadUrl != null) {
|
|
2938
|
+
this.editor.putString(PREVIEW_PAYLOAD_URL_PREF_KEY, previewPayloadUrl);
|
|
2939
|
+
logger.info("Preview session using payload URL");
|
|
2940
|
+
} else {
|
|
2941
|
+
this.editor.remove(PREVIEW_PAYLOAD_URL_PREF_KEY);
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
final String previewName = this.normalizedPreviewMetadataValue(call.getString("name"));
|
|
2945
|
+
if (previewName == null) {
|
|
2946
|
+
this.editor.remove(PREVIEW_NAME_PREF_KEY);
|
|
2947
|
+
} else {
|
|
2948
|
+
this.editor.putString(PREVIEW_NAME_PREF_KEY, previewName);
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
final String previewSource = this.normalizedPreviewMetadataValue(call.getString("source"));
|
|
2952
|
+
if (previewSource == null) {
|
|
2953
|
+
this.editor.remove(PREVIEW_SOURCE_PREF_KEY);
|
|
2954
|
+
} else {
|
|
2955
|
+
this.editor.putString(PREVIEW_SOURCE_PREF_KEY, previewSource);
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
this.activatePreviewSessionState();
|
|
2959
|
+
call.resolve();
|
|
2960
|
+
} catch (final Exception e) {
|
|
2961
|
+
this.hidePreviewTransitionLoader("preview-session-failed");
|
|
2962
|
+
logger.error("Could not start preview session " + e.getMessage());
|
|
2963
|
+
call.reject("Could not start preview session", e);
|
|
2964
|
+
}
|
|
2965
|
+
});
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
@PluginMethod
|
|
2969
|
+
public void listPreviews(final PluginCall call) {
|
|
2970
|
+
if (!Boolean.TRUE.equals(this.allowPreview)) {
|
|
2971
|
+
call.reject("listPreviews not allowed");
|
|
2972
|
+
return;
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
final JSArray previews = this.listPreviewInfos(true);
|
|
2976
|
+
final JSObject ret = new JSObject();
|
|
2977
|
+
ret.put("previews", previews);
|
|
2978
|
+
ret.put("currentBundle", InternalUtils.mapToJSObject(this.implementation.getCurrentBundle().toJSONMap()));
|
|
2979
|
+
|
|
2980
|
+
for (int i = 0; i < previews.length(); i++) {
|
|
2981
|
+
final JSONObject preview = previews.optJSONObject(i);
|
|
2982
|
+
if (preview != null && preview.optBoolean("isActive", false)) {
|
|
2983
|
+
ret.put("current", preview);
|
|
2984
|
+
break;
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
final BundleInfo liveBundle = this.implementation.getPreviewFallbackBundle();
|
|
2989
|
+
if (liveBundle != null) {
|
|
2990
|
+
ret.put("liveBundle", InternalUtils.mapToJSObject(liveBundle.toJSONMap()));
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
call.resolve(ret);
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
@PluginMethod
|
|
2997
|
+
public void setPreview(final PluginCall call) {
|
|
2998
|
+
if (!Boolean.TRUE.equals(this.allowPreview)) {
|
|
2999
|
+
call.reject("setPreview not allowed");
|
|
3000
|
+
return;
|
|
3001
|
+
}
|
|
3002
|
+
final String id = call.getString("id");
|
|
3003
|
+
if (id == null || id.isEmpty()) {
|
|
3004
|
+
call.reject("setPreview called without id");
|
|
3005
|
+
return;
|
|
3006
|
+
}
|
|
3007
|
+
final JSObject preview = this.storedPreviewInfo(id);
|
|
3008
|
+
if (preview == null) {
|
|
3009
|
+
call.reject("Preview " + id + " is not available locally");
|
|
3010
|
+
return;
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
this.showPreviewTransitionLoader("set-preview");
|
|
3014
|
+
startNewThread(() -> {
|
|
3015
|
+
if (!this.preparePreviewFallbackIfNeeded()) {
|
|
3016
|
+
this.hidePreviewTransitionLoader("set-preview-fallback-failed");
|
|
3017
|
+
call.reject("Could not save current bundle as preview fallback");
|
|
3018
|
+
return;
|
|
1135
3019
|
}
|
|
1136
|
-
|
|
3020
|
+
|
|
3021
|
+
if (!this.implementation.set(id)) {
|
|
3022
|
+
this.hidePreviewTransitionLoader("set-preview-failed");
|
|
3023
|
+
call.reject("Preview " + id + " cannot be applied");
|
|
3024
|
+
return;
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
final BundleInfo bundle = this.implementation.getBundleInfo(id);
|
|
3028
|
+
this.updateCurrentPreviewSessionMetadataFrom(preview);
|
|
3029
|
+
this.activatePreviewSessionState();
|
|
3030
|
+
this.recordPreviewBundle(bundle);
|
|
3031
|
+
if (!this.reloadWithoutWaitingForAppReady()) {
|
|
3032
|
+
this.hidePreviewTransitionLoader("set-preview-reload-failed");
|
|
3033
|
+
call.reject("Reload failed after setting preview " + id);
|
|
3034
|
+
return;
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
this.notifyBundleSet(bundle);
|
|
3038
|
+
this.showPreviewSessionNoticeIfNeeded();
|
|
3039
|
+
call.resolve();
|
|
3040
|
+
});
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
public JSArray previewMenuPreviews() {
|
|
3044
|
+
return this.listPreviewInfos(true);
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
public boolean setPreviewFromShakeMenu(final String id) {
|
|
3048
|
+
final JSObject preview = this.storedPreviewInfo(id);
|
|
3049
|
+
if (!Boolean.TRUE.equals(this.allowPreview) || preview == null) {
|
|
3050
|
+
return false;
|
|
1137
3051
|
}
|
|
1138
|
-
|
|
3052
|
+
|
|
3053
|
+
this.showPreviewTransitionLoader("set-preview-menu");
|
|
3054
|
+
if (!this.preparePreviewFallbackIfNeeded()) {
|
|
3055
|
+
this.hidePreviewTransitionLoader("set-preview-menu-fallback-failed");
|
|
3056
|
+
return false;
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
if (!this.implementation.set(id)) {
|
|
3060
|
+
this.hidePreviewTransitionLoader("set-preview-menu-failed");
|
|
3061
|
+
return false;
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
final BundleInfo bundle = this.implementation.getBundleInfo(id);
|
|
3065
|
+
this.updateCurrentPreviewSessionMetadataFrom(preview);
|
|
3066
|
+
this.activatePreviewSessionState();
|
|
3067
|
+
this.recordPreviewBundle(bundle);
|
|
3068
|
+
if (!this.reloadWithoutWaitingForAppReady()) {
|
|
3069
|
+
this.hidePreviewTransitionLoader("set-preview-menu-reload-failed");
|
|
3070
|
+
return false;
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
this.notifyBundleSet(bundle);
|
|
3074
|
+
this.showPreviewSessionNoticeIfNeeded();
|
|
3075
|
+
return true;
|
|
1139
3076
|
}
|
|
1140
3077
|
|
|
1141
3078
|
@PluginMethod
|
|
1142
|
-
public void
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
call.resolve(ret);
|
|
1147
|
-
} catch (final Exception e) {
|
|
1148
|
-
logger.error("Could not get plugin version " + e.getMessage());
|
|
1149
|
-
call.reject("Could not get plugin version", e);
|
|
3079
|
+
public void resetPreview(final PluginCall call) {
|
|
3080
|
+
if (!Boolean.TRUE.equals(this.previewSessionEnabled)) {
|
|
3081
|
+
call.resolve();
|
|
3082
|
+
return;
|
|
1150
3083
|
}
|
|
3084
|
+
startNewThread(() -> {
|
|
3085
|
+
if (this.leavePreviewSessionFromShakeMenu()) {
|
|
3086
|
+
call.resolve();
|
|
3087
|
+
} else {
|
|
3088
|
+
call.reject("Could not leave preview session");
|
|
3089
|
+
}
|
|
3090
|
+
});
|
|
1151
3091
|
}
|
|
1152
3092
|
|
|
1153
3093
|
@PluginMethod
|
|
1154
|
-
public void
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
|
|
1169
|
-
String errorCode = jsRes.getString("error");
|
|
3094
|
+
public void deletePreview(final PluginCall call) {
|
|
3095
|
+
if (!Boolean.TRUE.equals(this.allowPreview)) {
|
|
3096
|
+
call.reject("deletePreview not allowed");
|
|
3097
|
+
return;
|
|
3098
|
+
}
|
|
3099
|
+
final String id = call.getString("id");
|
|
3100
|
+
if (id == null || id.isEmpty()) {
|
|
3101
|
+
call.reject("deletePreview called without id");
|
|
3102
|
+
return;
|
|
3103
|
+
}
|
|
3104
|
+
if (Boolean.TRUE.equals(this.previewSessionEnabled) && this.implementation.getCurrentBundle().getId().equals(id)) {
|
|
3105
|
+
call.reject("Cannot delete the active preview");
|
|
3106
|
+
return;
|
|
3107
|
+
}
|
|
1170
3108
|
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
3109
|
+
final boolean removed;
|
|
3110
|
+
synchronized (this.previewSessionsLock) {
|
|
3111
|
+
final JSONObject sessions = this.previewSessionsJson();
|
|
3112
|
+
removed = sessions.has(id);
|
|
3113
|
+
sessions.remove(id);
|
|
3114
|
+
this.savePreviewSessionsJson(sessions);
|
|
3115
|
+
}
|
|
1174
3116
|
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
);
|
|
1190
|
-
});
|
|
1191
|
-
} catch (final Exception e) {
|
|
1192
|
-
logger.error("Failed to unsetChannel: " + e.getMessage());
|
|
1193
|
-
call.reject("Failed to unsetChannel: ", e);
|
|
3117
|
+
boolean deleted = false;
|
|
3118
|
+
final BundleInfo fallback = this.implementation.getPreviewFallbackBundle();
|
|
3119
|
+
final BundleInfo next = this.implementation.getNextBundle();
|
|
3120
|
+
if (
|
|
3121
|
+
removed &&
|
|
3122
|
+
!BundleInfo.ID_BUILTIN.equals(id) &&
|
|
3123
|
+
(fallback == null || !id.equals(fallback.getId())) &&
|
|
3124
|
+
(next == null || !id.equals(next.getId()))
|
|
3125
|
+
) {
|
|
3126
|
+
try {
|
|
3127
|
+
deleted = this.implementation.delete(id, false);
|
|
3128
|
+
} catch (final Exception err) {
|
|
3129
|
+
logger.warn("Could not delete preview bundle " + id + ": " + err.getMessage());
|
|
3130
|
+
}
|
|
1194
3131
|
}
|
|
3132
|
+
|
|
3133
|
+
final JSObject ret = new JSObject();
|
|
3134
|
+
ret.put("removed", removed);
|
|
3135
|
+
ret.put("deleted", deleted);
|
|
3136
|
+
call.resolve(ret);
|
|
1195
3137
|
}
|
|
1196
3138
|
|
|
1197
3139
|
@PluginMethod
|
|
1198
|
-
public void
|
|
1199
|
-
|
|
1200
|
-
|
|
3140
|
+
public void checkPreviewUpdate(final PluginCall call) {
|
|
3141
|
+
this.handlePreviewUpdate(call, false);
|
|
3142
|
+
}
|
|
1201
3143
|
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
3144
|
+
@PluginMethod
|
|
3145
|
+
public void updatePreview(final PluginCall call) {
|
|
3146
|
+
this.handlePreviewUpdate(call, true);
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
private void handlePreviewUpdate(final PluginCall call, final boolean shouldDownload) {
|
|
3150
|
+
if (!Boolean.TRUE.equals(this.allowPreview)) {
|
|
3151
|
+
call.reject("Preview updates not allowed");
|
|
3152
|
+
return;
|
|
3153
|
+
}
|
|
3154
|
+
final String id = call.getString("id");
|
|
3155
|
+
if (id == null || id.isEmpty()) {
|
|
3156
|
+
call.reject("Preview update called without id");
|
|
3157
|
+
return;
|
|
3158
|
+
}
|
|
3159
|
+
final JSObject preview = this.storedPreviewInfo(id);
|
|
3160
|
+
final String payloadUrl = preview == null ? null : this.normalizePreviewPayloadUrl(preview.optString("payloadUrl", null));
|
|
3161
|
+
if (payloadUrl == null) {
|
|
3162
|
+
call.reject("Preview " + id + " has no payloadUrl to update from");
|
|
1208
3163
|
return;
|
|
1209
3164
|
}
|
|
1210
|
-
try {
|
|
1211
|
-
logger.info("setChannel " + channel + " triggerAutoUpdate: " + triggerAutoUpdate);
|
|
1212
|
-
startNewThread(() ->
|
|
1213
|
-
CapacitorUpdaterPlugin.this.implementation.setChannel(
|
|
1214
|
-
channel,
|
|
1215
|
-
CapacitorUpdaterPlugin.this.editor,
|
|
1216
|
-
DEFAULT_CHANNEL_PREF_KEY,
|
|
1217
|
-
CapacitorUpdaterPlugin.this.allowSetDefaultChannel,
|
|
1218
|
-
(res) -> {
|
|
1219
|
-
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
1220
|
-
if (jsRes.has("error")) {
|
|
1221
|
-
String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
|
|
1222
|
-
String errorCode = jsRes.getString("error");
|
|
1223
3165
|
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
eventData.put("message", errorMessage);
|
|
1232
|
-
notifyListeners("channelPrivate", eventData);
|
|
1233
|
-
}
|
|
3166
|
+
startNewThread(() -> {
|
|
3167
|
+
try {
|
|
3168
|
+
final JSONObject payload = this.fetchPreviewPayload(payloadUrl);
|
|
3169
|
+
final String version = payload.optString("version", "").trim();
|
|
3170
|
+
if (version.isEmpty()) {
|
|
3171
|
+
throw new IOException("Preview payload is missing a version");
|
|
3172
|
+
}
|
|
1234
3173
|
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
3174
|
+
final BundleInfo currentPreviewBundle = this.implementation.getBundleInfo(id);
|
|
3175
|
+
final boolean upToDate = version.equals(currentPreviewBundle.getVersionName());
|
|
3176
|
+
if (upToDate || !shouldDownload) {
|
|
3177
|
+
final JSObject ret = new JSObject();
|
|
3178
|
+
ret.put("preview", preview);
|
|
3179
|
+
ret.put("latestVersion", version);
|
|
3180
|
+
ret.put("upToDate", upToDate);
|
|
3181
|
+
ret.put("updated", false);
|
|
3182
|
+
ret.put("bundle", InternalUtils.mapToJSObject(currentPreviewBundle.toJSONMap()));
|
|
3183
|
+
call.resolve(ret);
|
|
3184
|
+
return;
|
|
3185
|
+
}
|
|
1238
3186
|
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
3187
|
+
final BundleInfo next = this.downloadPreviewPayloadBundle(payload);
|
|
3188
|
+
if (next.isErrorStatus()) {
|
|
3189
|
+
throw new IOException("Download failed: " + next.getStatus());
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
final boolean wasActive =
|
|
3193
|
+
Boolean.TRUE.equals(this.previewSessionEnabled) && this.implementation.getCurrentBundle().getId().equals(id);
|
|
3194
|
+
if (wasActive && !this.implementation.set(next.getId())) {
|
|
3195
|
+
throw new IOException("Downloaded preview bundle cannot be applied");
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
final JSObject savedPreview = this.recordPreviewBundle(next, id);
|
|
3199
|
+
if (wasActive) {
|
|
3200
|
+
if (!this.reloadWithoutWaitingForAppReady()) {
|
|
3201
|
+
throw new IOException("Reload failed after updating preview");
|
|
1252
3202
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
3203
|
+
this.notifyBundleSet(next);
|
|
3204
|
+
this.showPreviewSessionNoticeIfNeeded();
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
final JSObject ret = new JSObject();
|
|
3208
|
+
ret.put("preview", savedPreview);
|
|
3209
|
+
ret.put("latestVersion", version);
|
|
3210
|
+
ret.put("upToDate", false);
|
|
3211
|
+
ret.put("updated", true);
|
|
3212
|
+
ret.put("bundle", InternalUtils.mapToJSObject(next.toJSONMap()));
|
|
3213
|
+
call.resolve(ret);
|
|
3214
|
+
} catch (final Exception err) {
|
|
3215
|
+
logger.error("Could not update preview: " + err.getMessage());
|
|
3216
|
+
call.reject("Could not update preview: " + err.getMessage());
|
|
3217
|
+
}
|
|
3218
|
+
});
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
public boolean leavePreviewSessionFromShakeMenu() {
|
|
3222
|
+
this.showPreviewTransitionLoader("leave-preview-session");
|
|
3223
|
+
final boolean didReset = this.resetToPreviewFallbackBundle();
|
|
3224
|
+
if (!didReset) {
|
|
3225
|
+
this.hidePreviewTransitionLoader("leave-preview-session-failed");
|
|
3226
|
+
return false;
|
|
1258
3227
|
}
|
|
3228
|
+
|
|
3229
|
+
this.endPreviewSession(true);
|
|
3230
|
+
return true;
|
|
1259
3231
|
}
|
|
1260
3232
|
|
|
1261
|
-
|
|
1262
|
-
|
|
3233
|
+
private boolean leavePreviewSessionForIncomingPreviewLink() {
|
|
3234
|
+
this.showPreviewTransitionLoader("incoming-preview-deeplink");
|
|
3235
|
+
final BundleInfo previewFallbackBundle = this.resolvePreviewFallbackBundle("incoming preview deeplink");
|
|
3236
|
+
boolean didReload = false;
|
|
3237
|
+
|
|
1263
3238
|
try {
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
1268
|
-
if (jsRes.has("error")) {
|
|
1269
|
-
String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
|
|
1270
|
-
String errorCode = jsRes.getString("error");
|
|
3239
|
+
if (previewFallbackBundle == null) {
|
|
3240
|
+
return false;
|
|
3241
|
+
}
|
|
1271
3242
|
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
3243
|
+
final CapgoUpdater.ResetState previousState = this.implementation.captureResetState();
|
|
3244
|
+
if (!this.implementation.stagePreviewFallbackReload(previewFallbackBundle)) {
|
|
3245
|
+
logger.error("Could not stage preview fallback bundle");
|
|
3246
|
+
return false;
|
|
3247
|
+
}
|
|
1275
3248
|
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
3249
|
+
if (!this.reloadWithoutWaitingForAppReady()) {
|
|
3250
|
+
this.implementation.restoreResetState(previousState);
|
|
3251
|
+
this.restoreLiveBundleStateAfterFailedReload();
|
|
3252
|
+
return false;
|
|
3253
|
+
}
|
|
3254
|
+
didReload = true;
|
|
3255
|
+
|
|
3256
|
+
this.endPreviewSession(true);
|
|
3257
|
+
return true;
|
|
3258
|
+
} finally {
|
|
3259
|
+
this.clearIncomingPreviewTransition();
|
|
3260
|
+
if (!didReload) {
|
|
3261
|
+
this.hidePreviewTransitionLoader("incoming-preview-deeplink-failed");
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3266
|
+
private void leavePreviewSessionForLaunchIntentIfNeeded() {
|
|
3267
|
+
final Intent intent = getActivity() == null ? null : getActivity().getIntent();
|
|
3268
|
+
if (
|
|
3269
|
+
intent == null ||
|
|
3270
|
+
!Intent.ACTION_VIEW.equals(intent.getAction()) ||
|
|
3271
|
+
intent.getData() == null ||
|
|
3272
|
+
!Boolean.TRUE.equals(this.previewSessionEnabled) ||
|
|
3273
|
+
!isPreviewDeepLink(intent.getData()) ||
|
|
3274
|
+
Boolean.TRUE.equals(this.isLeavingPreviewForIncomingLink)
|
|
3275
|
+
) {
|
|
3276
|
+
return;
|
|
3277
|
+
}
|
|
3278
|
+
|
|
3279
|
+
this.isLeavingPreviewForIncomingLink = true;
|
|
3280
|
+
this.showPreviewTransitionLoader("preview-launch-deeplink");
|
|
3281
|
+
logger.info("Preview deeplink launch detected while preview session is active; restoring fallback before initial load");
|
|
3282
|
+
if (!this.leavePreviewSessionWithoutReload()) {
|
|
3283
|
+
logger.error("Could not leave preview session before initial preview deeplink routing");
|
|
3284
|
+
this.isLeavingPreviewForIncomingLink = false;
|
|
3285
|
+
this.hidePreviewTransitionLoader("preview-launch-deeplink-failed");
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
|
|
3289
|
+
private boolean leavePreviewSessionWithoutReload() {
|
|
3290
|
+
return this.leavePreviewSessionWithoutReload(false);
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3293
|
+
private boolean leavePreviewSessionWithoutReload(final boolean keepPreviewGuard) {
|
|
3294
|
+
final BundleInfo previewFallbackBundle = this.resolvePreviewFallbackBundle("preview deeplink launch");
|
|
3295
|
+
if (previewFallbackBundle == null) {
|
|
3296
|
+
return false;
|
|
3297
|
+
}
|
|
3298
|
+
if (!this.implementation.stagePreviewFallbackReload(previewFallbackBundle)) {
|
|
3299
|
+
logger.error("Could not stage preview fallback bundle");
|
|
3300
|
+
return false;
|
|
3301
|
+
}
|
|
3302
|
+
|
|
3303
|
+
this.endPreviewSession(keepPreviewGuard);
|
|
3304
|
+
return true;
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3307
|
+
public boolean reloadPreviewSessionFromShakeMenu() {
|
|
3308
|
+
this.showPreviewTransitionLoader("reload-preview-session");
|
|
3309
|
+
final boolean didReload;
|
|
3310
|
+
final String payloadUrl = this.storedPreviewPayloadUrl();
|
|
3311
|
+
if (payloadUrl != null) {
|
|
3312
|
+
didReload = this.refreshPreviewSessionFromPayloadUrl(payloadUrl);
|
|
3313
|
+
} else {
|
|
3314
|
+
didReload = this.reloadWithoutWaitingForAppReady();
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
if (!didReload) {
|
|
3318
|
+
this.hidePreviewTransitionLoader("reload-preview-session-failed");
|
|
3319
|
+
}
|
|
3320
|
+
return didReload;
|
|
3321
|
+
}
|
|
3322
|
+
|
|
3323
|
+
public boolean hasActivePreviewSession() {
|
|
3324
|
+
return Boolean.TRUE.equals(this.previewSessionEnabled);
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
private boolean resetToPreviewFallbackBundle() {
|
|
3328
|
+
final BundleInfo fallback = this.resolvePreviewFallbackBundle("leave preview");
|
|
3329
|
+
if (fallback == null) {
|
|
3330
|
+
return false;
|
|
3331
|
+
}
|
|
3332
|
+
|
|
3333
|
+
final CapgoUpdater.ResetState previousState = this.implementation.captureResetState();
|
|
3334
|
+
final String previousBundleName = this.implementation.getCurrentBundle().getVersionName();
|
|
3335
|
+
logger.info("Resetting to preview fallback bundle: " + fallback.getVersionName());
|
|
3336
|
+
if (this.implementation.stagePreviewFallbackReload(fallback) && this.reloadWithoutWaitingForAppReady()) {
|
|
3337
|
+
this.implementation.finalizeResetTransition(previousBundleName, false);
|
|
3338
|
+
this.notifyBundleSet(fallback);
|
|
3339
|
+
return true;
|
|
1285
3340
|
}
|
|
3341
|
+
this.implementation.restoreResetState(previousState);
|
|
3342
|
+
this.restoreLiveBundleStateAfterFailedReload();
|
|
3343
|
+
return false;
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
private BundleInfo resolvePreviewFallbackBundle(final String reason) {
|
|
3347
|
+
final BundleInfo fallback = this.implementation.getPreviewFallbackBundle();
|
|
3348
|
+
if (fallback != null && !fallback.isErrorStatus() && this.implementation.canSet(fallback)) {
|
|
3349
|
+
return fallback;
|
|
3350
|
+
}
|
|
3351
|
+
|
|
3352
|
+
if (fallback == null) {
|
|
3353
|
+
logger.warn("No preview fallback bundle available for " + reason + ". Falling back to builtin bundle.");
|
|
3354
|
+
} else if (fallback.isErrorStatus()) {
|
|
3355
|
+
logger.warn("Preview fallback bundle is in error state for " + reason + ". Falling back to builtin bundle.");
|
|
3356
|
+
} else {
|
|
3357
|
+
logger.warn("Preview fallback bundle is not installable for " + reason + ". Falling back to builtin bundle.");
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3360
|
+
final BundleInfo builtin = this.implementation.getBundleInfo(BundleInfo.ID_BUILTIN);
|
|
3361
|
+
if (builtin != null && !builtin.isErrorStatus() && this.implementation.canSet(builtin)) {
|
|
3362
|
+
return builtin;
|
|
3363
|
+
}
|
|
3364
|
+
|
|
3365
|
+
logger.error("Builtin bundle is not available to leave preview for " + reason);
|
|
3366
|
+
return null;
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
private void endPreviewSession() {
|
|
3370
|
+
this.endPreviewSession(false);
|
|
3371
|
+
}
|
|
3372
|
+
|
|
3373
|
+
private void endPreviewSession(final boolean keepPreviewGuard) {
|
|
3374
|
+
final boolean previousShakeMenuEnabled = this.prefs.getBoolean(
|
|
3375
|
+
PREVIEW_PREVIOUS_SHAKE_MENU_PREF_KEY,
|
|
3376
|
+
this.getConfig().getBoolean("shakeMenu", false)
|
|
3377
|
+
);
|
|
3378
|
+
final boolean previousShakeChannelSelectorEnabled = this.prefs.getBoolean(
|
|
3379
|
+
PREVIEW_PREVIOUS_SHAKE_CHANNEL_SELECTOR_PREF_KEY,
|
|
3380
|
+
this.getConfig().getBoolean("allowShakeChannelSelector", false)
|
|
3381
|
+
);
|
|
3382
|
+
this.restorePreviewPreviousNextBundle();
|
|
3383
|
+
this.restorePreviewPreviousAppId();
|
|
3384
|
+
this.restorePreviewPreviousDefaultChannel();
|
|
3385
|
+
|
|
3386
|
+
this.previewSessionEnabled = false;
|
|
3387
|
+
this.previewSessionAlertPending = false;
|
|
3388
|
+
if (keepPreviewGuard) {
|
|
3389
|
+
this.implementation.previewSession = true;
|
|
3390
|
+
} else {
|
|
3391
|
+
this.clearIncomingPreviewTransition();
|
|
3392
|
+
}
|
|
3393
|
+
this.shakeMenuEnabled = previousShakeMenuEnabled;
|
|
3394
|
+
this.shakeChannelSelectorEnabled = previousShakeChannelSelectorEnabled;
|
|
3395
|
+
this.syncShakeMenuLifecycle();
|
|
3396
|
+
this.implementation.setPreviewFallbackBundle(null);
|
|
3397
|
+
this.clearPreviewSessionPreferences();
|
|
3398
|
+
logger.info("Preview session ended");
|
|
1286
3399
|
}
|
|
1287
3400
|
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
|
|
1297
|
-
String errorCode = jsRes.getString("error");
|
|
3401
|
+
private void clearPreviewSessionBecauseDisabled() {
|
|
3402
|
+
logger.info("Preview session disabled by config; restoring preview fallback");
|
|
3403
|
+
final BundleInfo bundleToRestore = this.resolvePreviewFallbackBundle("preview disabled");
|
|
3404
|
+
if (bundleToRestore != null) {
|
|
3405
|
+
this.implementation.stagePreviewFallbackReload(bundleToRestore);
|
|
3406
|
+
} else {
|
|
3407
|
+
logger.warn("Could not restore preview fallback while disabling preview");
|
|
3408
|
+
}
|
|
1298
3409
|
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
3410
|
+
this.restorePreviewPreviousNextBundle();
|
|
3411
|
+
this.restorePreviewPreviousAppId();
|
|
3412
|
+
this.restorePreviewPreviousDefaultChannel();
|
|
3413
|
+
this.previewSessionEnabled = false;
|
|
3414
|
+
this.previewSessionAlertPending = false;
|
|
3415
|
+
this.isLeavingPreviewForIncomingLink = false;
|
|
3416
|
+
this.implementation.previewSession = false;
|
|
3417
|
+
this.hidePreviewTransitionLoader("preview-session-disabled");
|
|
3418
|
+
this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
|
|
3419
|
+
this.shakeChannelSelectorEnabled = this.getConfig().getBoolean("allowShakeChannelSelector", false);
|
|
3420
|
+
this.shakeMenuGesture = normalizedShakeMenuGesture(this.getConfig().getString("shakeMenuGesture", SHAKE_MENU_GESTURE_SHAKE));
|
|
3421
|
+
this.syncShakeMenuLifecycle();
|
|
3422
|
+
this.clearPreviewSessionPreferences();
|
|
3423
|
+
}
|
|
1302
3424
|
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
3425
|
+
private void clearPreviewSessionPreferences() {
|
|
3426
|
+
if (this.implementation != null) {
|
|
3427
|
+
this.implementation.setPreviewFallbackBundle(null);
|
|
3428
|
+
}
|
|
3429
|
+
this.editor.remove(PREVIEW_SESSION_PREF_KEY);
|
|
3430
|
+
this.editor.remove(PREVIEW_PREVIOUS_SHAKE_MENU_PREF_KEY);
|
|
3431
|
+
this.editor.remove(PREVIEW_PREVIOUS_SHAKE_CHANNEL_SELECTOR_PREF_KEY);
|
|
3432
|
+
this.editor.remove(PREVIEW_PREVIOUS_NEXT_BUNDLE_PREF_KEY);
|
|
3433
|
+
this.editor.remove(PREVIEW_PREVIOUS_APP_ID_PREF_KEY);
|
|
3434
|
+
this.editor.remove(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_PREF_KEY);
|
|
3435
|
+
this.editor.remove(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_WAS_SET_PREF_KEY);
|
|
3436
|
+
this.editor.remove(PREVIEW_APP_ID_PREF_KEY);
|
|
3437
|
+
this.editor.remove(PREVIEW_PAYLOAD_URL_PREF_KEY);
|
|
3438
|
+
this.editor.remove(PREVIEW_NAME_PREF_KEY);
|
|
3439
|
+
this.editor.remove(PREVIEW_SOURCE_PREF_KEY);
|
|
3440
|
+
this.editor.remove(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY);
|
|
3441
|
+
this.editor.apply();
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3444
|
+
private void setActiveAppId(final String appId) {
|
|
3445
|
+
this.implementation.appId = appId;
|
|
3446
|
+
if (this.implementation.versionOs != null) {
|
|
3447
|
+
DownloadService.updateUserAgent(this.implementation.appId, this.pluginVersion, this.implementation.versionOs);
|
|
1312
3448
|
}
|
|
1313
3449
|
}
|
|
1314
3450
|
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
final String version = call.getString("version");
|
|
1319
|
-
final String sessionKey = call.getString("sessionKey", "");
|
|
1320
|
-
final String checksum = call.getString("checksum", "");
|
|
1321
|
-
final JSONArray manifest = call.getData().optJSONArray("manifest");
|
|
1322
|
-
if (url == null) {
|
|
1323
|
-
logger.error("Download called without url");
|
|
1324
|
-
call.reject("Download called without url");
|
|
3451
|
+
private void restorePreviewPreviousAppId() {
|
|
3452
|
+
final String previousAppId = this.prefs.getString(PREVIEW_PREVIOUS_APP_ID_PREF_KEY, "");
|
|
3453
|
+
if (previousAppId == null || previousAppId.isEmpty()) {
|
|
1325
3454
|
return;
|
|
1326
3455
|
}
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
3456
|
+
this.setActiveAppId(previousAppId);
|
|
3457
|
+
logger.info("Restored appId after preview: " + previousAppId);
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3460
|
+
private void restorePreviewPreviousDefaultChannel() {
|
|
3461
|
+
final String configDefaultChannel = this.getConfig().getString("defaultChannel", "");
|
|
3462
|
+
if (this.prefs.getBoolean(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_WAS_SET_PREF_KEY, false)) {
|
|
3463
|
+
final String previousDefaultChannel = this.prefs.getString(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_PREF_KEY, "");
|
|
3464
|
+
this.editor.putString(DEFAULT_CHANNEL_PREF_KEY, previousDefaultChannel);
|
|
3465
|
+
this.implementation.defaultChannel = previousDefaultChannel;
|
|
3466
|
+
this.editor.apply();
|
|
3467
|
+
logger.info("Restored defaultChannel after preview");
|
|
1330
3468
|
return;
|
|
1331
3469
|
}
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
if (manifest != null) {
|
|
1338
|
-
// For manifest downloads, we need to handle this asynchronously
|
|
1339
|
-
// to avoid automatically scheduling/applying the downloaded bundle.
|
|
1340
|
-
// Manual download must not schedule/apply the bundle automatically.
|
|
1341
|
-
CapacitorUpdaterPlugin.this.implementation.downloadBackground(url, version, sessionKey, checksum, manifest, false);
|
|
1342
|
-
// Return immediately with a pending status - the actual result will come via listeners
|
|
1343
|
-
final String id = CapacitorUpdaterPlugin.this.implementation.randomString();
|
|
1344
|
-
downloaded = new BundleInfo(id, version, BundleStatus.DOWNLOADING, new Date(System.currentTimeMillis()), "");
|
|
1345
|
-
call.resolve(InternalUtils.mapToJSObject(downloaded.toJSONMap()));
|
|
1346
|
-
return;
|
|
1347
|
-
} else {
|
|
1348
|
-
downloaded = CapacitorUpdaterPlugin.this.implementation.download(url, version, sessionKey, checksum);
|
|
1349
|
-
}
|
|
1350
|
-
if (downloaded.isErrorStatus()) {
|
|
1351
|
-
throw new RuntimeException("Download failed: " + downloaded.getStatus());
|
|
1352
|
-
} else {
|
|
1353
|
-
call.resolve(InternalUtils.mapToJSObject(downloaded.toJSONMap()));
|
|
1354
|
-
}
|
|
1355
|
-
} catch (final Exception e) {
|
|
1356
|
-
logger.error("Failed to download from: " + url + " " + e.getMessage());
|
|
1357
|
-
call.reject("Failed to download from: " + url, e);
|
|
1358
|
-
final JSObject ret = new JSObject();
|
|
1359
|
-
ret.put("version", version);
|
|
1360
|
-
CapacitorUpdaterPlugin.this.notifyListeners("downloadFailed", ret);
|
|
1361
|
-
final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
|
|
1362
|
-
CapacitorUpdaterPlugin.this.implementation.sendStats("download_fail", current.getVersionName());
|
|
1363
|
-
}
|
|
1364
|
-
});
|
|
1365
|
-
} catch (final Exception e) {
|
|
1366
|
-
logger.error("Failed to download from: " + url + " " + e.getMessage());
|
|
1367
|
-
call.reject("Failed to download from: " + url, e);
|
|
1368
|
-
final JSObject ret = new JSObject();
|
|
1369
|
-
ret.put("version", version);
|
|
1370
|
-
CapacitorUpdaterPlugin.this.notifyListeners("downloadFailed", ret);
|
|
1371
|
-
final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
|
|
1372
|
-
CapacitorUpdaterPlugin.this.implementation.sendStats("download_fail", current.getVersionName());
|
|
1373
|
-
}
|
|
3470
|
+
|
|
3471
|
+
this.editor.remove(DEFAULT_CHANNEL_PREF_KEY);
|
|
3472
|
+
this.implementation.defaultChannel = configDefaultChannel;
|
|
3473
|
+
this.editor.apply();
|
|
3474
|
+
logger.info("Restored defaultChannel after preview to config value");
|
|
1374
3475
|
}
|
|
1375
3476
|
|
|
1376
|
-
private
|
|
1377
|
-
if (
|
|
1378
|
-
return;
|
|
3477
|
+
private String normalizePreviewAppId(final String rawAppId) {
|
|
3478
|
+
if (rawAppId == null) {
|
|
3479
|
+
return null;
|
|
1379
3480
|
}
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
3481
|
+
|
|
3482
|
+
final String appId = rawAppId.trim();
|
|
3483
|
+
if (appId.isEmpty()) {
|
|
3484
|
+
return null;
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
final String lowercasedAppId = appId.toLowerCase(java.util.Locale.ROOT);
|
|
3488
|
+
if ("undefined".equals(lowercasedAppId) || "null".equals(lowercasedAppId)) {
|
|
3489
|
+
return null;
|
|
3490
|
+
}
|
|
3491
|
+
|
|
3492
|
+
return appId;
|
|
1388
3493
|
}
|
|
1389
3494
|
|
|
1390
|
-
private
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
if (this.keepUrlPathAfterReload) {
|
|
1394
|
-
this.syncKeepUrlPathFlag(true);
|
|
3495
|
+
private boolean hasPreviewPayloadUrl(final String rawPayloadUrl) {
|
|
3496
|
+
if (rawPayloadUrl == null) {
|
|
3497
|
+
return false;
|
|
1395
3498
|
}
|
|
1396
|
-
logger.info("Reloading: " + path);
|
|
1397
3499
|
|
|
1398
|
-
|
|
1399
|
-
if (
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
Semaphore mainThreadSemaphore = new Semaphore(0);
|
|
1403
|
-
this.bridge.executeOnMainThread(() -> {
|
|
1404
|
-
try {
|
|
1405
|
-
if (this.bridge != null && this.bridge.getWebView() != null) {
|
|
1406
|
-
String currentUrl = this.bridge.getWebView().getUrl();
|
|
1407
|
-
if (currentUrl != null) {
|
|
1408
|
-
url.set(new URL(currentUrl));
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
} catch (Exception e) {
|
|
1412
|
-
logger.error("Error executing on main thread " + e.getMessage());
|
|
1413
|
-
}
|
|
1414
|
-
mainThreadSemaphore.release();
|
|
1415
|
-
});
|
|
3500
|
+
final String payloadUrl = rawPayloadUrl.trim();
|
|
3501
|
+
if (payloadUrl.isEmpty()) {
|
|
3502
|
+
return false;
|
|
3503
|
+
}
|
|
1416
3504
|
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
String currentUrl = this.bridge.getWebView().getUrl();
|
|
1425
|
-
if (currentUrl != null) {
|
|
1426
|
-
url.set(new URL(currentUrl));
|
|
1427
|
-
}
|
|
1428
|
-
}
|
|
1429
|
-
} catch (Exception e) {
|
|
1430
|
-
logger.error("Error executing on main thread " + e.getMessage());
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
} catch (InterruptedException e) {
|
|
1434
|
-
logger.error("Error waiting for main thread or getting the current URL from webview " + e.getMessage());
|
|
1435
|
-
Thread.currentThread().interrupt(); // Restore interrupted status
|
|
1436
|
-
}
|
|
3505
|
+
final String lowercasedPayloadUrl = payloadUrl.toLowerCase(java.util.Locale.ROOT);
|
|
3506
|
+
return !"undefined".equals(lowercasedPayloadUrl) && !"null".equals(lowercasedPayloadUrl);
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
private String normalizePreviewPayloadUrl(final String rawPayloadUrl) {
|
|
3510
|
+
if (!this.hasPreviewPayloadUrl(rawPayloadUrl)) {
|
|
3511
|
+
return null;
|
|
1437
3512
|
}
|
|
1438
3513
|
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
3514
|
+
final String payloadUrl = rawPayloadUrl.trim();
|
|
3515
|
+
try {
|
|
3516
|
+
final URL parsedUrl = new URL(payloadUrl);
|
|
3517
|
+
final String protocol = parsedUrl.getProtocol();
|
|
3518
|
+
if (!"https".equals(protocol) && !"http".equals(protocol)) {
|
|
3519
|
+
return null;
|
|
1444
3520
|
}
|
|
3521
|
+
return parsedUrl.toString();
|
|
3522
|
+
} catch (final MalformedURLException ignored) {
|
|
3523
|
+
return null;
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
1445
3526
|
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
finalUrl = new URL(finalUrl.getProtocol(), finalUrl.getHost(), finalUrl.getPort(), url.get().getPath());
|
|
1450
|
-
URL finalUrl1 = finalUrl;
|
|
1451
|
-
this.bridge.getWebView().post(() -> {
|
|
1452
|
-
this.bridge.getWebView().loadUrl(finalUrl1.toString());
|
|
1453
|
-
if (!this.keepUrlPathAfterReload) {
|
|
1454
|
-
this.bridge.getWebView().clearHistory();
|
|
1455
|
-
}
|
|
1456
|
-
});
|
|
1457
|
-
} catch (MalformedURLException e) {
|
|
1458
|
-
logger.error("Cannot get finalUrl from capacitor bridge " + e.getMessage());
|
|
3527
|
+
private String storedPreviewPayloadUrl() {
|
|
3528
|
+
return this.normalizePreviewPayloadUrl(this.prefs.getString(PREVIEW_PAYLOAD_URL_PREF_KEY, null));
|
|
3529
|
+
}
|
|
1459
3530
|
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
3531
|
+
private String previewPathFromUri(final Uri uri) {
|
|
3532
|
+
if ("capgo".equals(uri.getScheme())) {
|
|
3533
|
+
final String host = uri.getHost();
|
|
3534
|
+
final String path = uri.getPath();
|
|
3535
|
+
return ("/" + (host == null ? "" : host) + (path == null ? "" : path)).replaceAll("/+", "/");
|
|
3536
|
+
}
|
|
3537
|
+
|
|
3538
|
+
return uri.getPath();
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3541
|
+
private boolean isPreviewDeepLink(final Uri uri) {
|
|
3542
|
+
final String path = this.previewPathFromUri(uri);
|
|
3543
|
+
return "/preview/channel".equals(path) || "/preview/bundle".equals(path);
|
|
3544
|
+
}
|
|
3545
|
+
|
|
3546
|
+
private String readResponseBody(final InputStream stream) throws IOException {
|
|
3547
|
+
if (stream == null) {
|
|
3548
|
+
return "";
|
|
3549
|
+
}
|
|
3550
|
+
|
|
3551
|
+
try (InputStream input = stream; ByteArrayOutputStream output = new ByteArrayOutputStream()) {
|
|
3552
|
+
final byte[] buffer = new byte[8192];
|
|
3553
|
+
int read;
|
|
3554
|
+
while ((read = input.read(buffer)) != -1) {
|
|
3555
|
+
output.write(buffer, 0, read);
|
|
1481
3556
|
}
|
|
3557
|
+
return output.toString(StandardCharsets.UTF_8.name());
|
|
1482
3558
|
}
|
|
1483
3559
|
}
|
|
1484
3560
|
|
|
1485
|
-
|
|
3561
|
+
private JSONObject fetchPreviewPayload(final String payloadUrl) throws IOException, JSONException {
|
|
3562
|
+
final HttpURLConnection connection = (HttpURLConnection) new URL(payloadUrl).openConnection();
|
|
3563
|
+
connection.setRequestMethod("GET");
|
|
3564
|
+
connection.setRequestProperty("Accept", "application/json");
|
|
3565
|
+
connection.setConnectTimeout(30000);
|
|
3566
|
+
connection.setReadTimeout(60000);
|
|
3567
|
+
|
|
1486
3568
|
try {
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
3569
|
+
final int statusCode = connection.getResponseCode();
|
|
3570
|
+
final String body = this.readResponseBody(
|
|
3571
|
+
statusCode >= 200 && statusCode < 300 ? connection.getInputStream() : connection.getErrorStream()
|
|
3572
|
+
);
|
|
3573
|
+
final JSONObject payload = new JSONObject(body);
|
|
3574
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
3575
|
+
throw new IOException(
|
|
3576
|
+
payload.optString("message", payload.optString("error", "Preview payload request failed with HTTP " + statusCode))
|
|
3577
|
+
);
|
|
3578
|
+
}
|
|
3579
|
+
return payload;
|
|
3580
|
+
} finally {
|
|
3581
|
+
connection.disconnect();
|
|
1490
3582
|
}
|
|
1491
3583
|
}
|
|
1492
3584
|
|
|
1493
|
-
|
|
1494
|
-
final
|
|
1495
|
-
|
|
3585
|
+
private BundleInfo downloadPreviewPayloadBundle(final JSONObject payload) throws IOException, JSONException {
|
|
3586
|
+
final String version = payload.optString("version", "").trim();
|
|
3587
|
+
if (version.isEmpty()) {
|
|
3588
|
+
throw new IOException("Preview payload is missing a version");
|
|
3589
|
+
}
|
|
1496
3590
|
|
|
1497
|
-
final
|
|
1498
|
-
|
|
1499
|
-
|
|
3591
|
+
final JSONArray manifest = payload.optJSONArray("manifest");
|
|
3592
|
+
final String url = payload.optString("url", "");
|
|
3593
|
+
if ((url == null || url.isEmpty()) && (manifest == null || manifest.length() == 0)) {
|
|
3594
|
+
throw new IOException("Preview payload is missing download information");
|
|
3595
|
+
}
|
|
1500
3596
|
|
|
1501
|
-
|
|
1502
|
-
|
|
3597
|
+
return this.downloadBundle(
|
|
3598
|
+
url == null || url.isEmpty() ? "https://404.capgo.app/no.zip" : url,
|
|
3599
|
+
version,
|
|
3600
|
+
payload.optString("sessionKey", ""),
|
|
3601
|
+
payload.optString("checksum", ""),
|
|
3602
|
+
manifest
|
|
3603
|
+
);
|
|
1503
3604
|
}
|
|
1504
3605
|
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
3606
|
+
private boolean refreshPreviewSessionFromPayloadUrl(final String payloadUrl) {
|
|
3607
|
+
try {
|
|
3608
|
+
final JSONObject payload = this.fetchPreviewPayload(payloadUrl);
|
|
3609
|
+
final String version = payload.optString("version", "").trim();
|
|
3610
|
+
if (version.isEmpty()) {
|
|
3611
|
+
throw new IOException("Preview payload is missing a version");
|
|
3612
|
+
}
|
|
1511
3613
|
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
if (next.isBuiltin()) {
|
|
1518
|
-
this.implementation.prepareResetStateForTransition();
|
|
1519
|
-
didApplyPendingBundle = true;
|
|
1520
|
-
} else {
|
|
1521
|
-
didApplyPendingBundle = this.implementation.stagePendingReload(next);
|
|
1522
|
-
}
|
|
1523
|
-
if (didApplyPendingBundle && this._reload()) {
|
|
1524
|
-
if (next.isBuiltin()) {
|
|
1525
|
-
this.implementation.finalizeResetTransition(previousBundleName, false);
|
|
1526
|
-
} else {
|
|
1527
|
-
this.implementation.finalizePendingReload(next, previousBundleName);
|
|
1528
|
-
}
|
|
1529
|
-
this.notifyBundleSet(next);
|
|
1530
|
-
this.implementation.setNextBundle(null);
|
|
1531
|
-
call.resolve();
|
|
1532
|
-
return;
|
|
1533
|
-
}
|
|
1534
|
-
this.implementation.restoreResetState(previousState);
|
|
1535
|
-
this.restoreLiveBundleStateAfterFailedReload();
|
|
1536
|
-
logger.error("Reload failed after applying pending bundle: " + next.getVersionName());
|
|
1537
|
-
call.reject("Reload failed after applying pending bundle: " + next.getVersionName());
|
|
1538
|
-
return;
|
|
1539
|
-
}
|
|
3614
|
+
final BundleInfo current = this.implementation.getCurrentBundle();
|
|
3615
|
+
if (version.equals(current.getVersionName())) {
|
|
3616
|
+
logger.info("Preview payload unchanged, reloading current bundle");
|
|
3617
|
+
return this.reloadWithoutWaitingForAppReady();
|
|
3618
|
+
}
|
|
1540
3619
|
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
logger.error("Reload failed");
|
|
1545
|
-
call.reject("Reload failed");
|
|
1546
|
-
}
|
|
1547
|
-
} catch (final Exception e) {
|
|
1548
|
-
logger.error("Could not reload " + e.getMessage());
|
|
1549
|
-
call.reject("Could not reload", e);
|
|
3620
|
+
final BundleInfo next = this.downloadPreviewPayloadBundle(payload);
|
|
3621
|
+
if (next.isErrorStatus()) {
|
|
3622
|
+
throw new IOException("Download failed: " + next.getStatus());
|
|
1550
3623
|
}
|
|
1551
|
-
|
|
3624
|
+
if (!this.implementation.set(next.getId())) {
|
|
3625
|
+
throw new IOException("Downloaded preview bundle cannot be applied");
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3628
|
+
this.recordPreviewBundle(next, current.getId());
|
|
3629
|
+
this.notifyBundleSet(next);
|
|
3630
|
+
return this.reloadWithoutWaitingForAppReady();
|
|
3631
|
+
} catch (final Exception err) {
|
|
3632
|
+
logger.error("Could not refresh preview session: " + err.getMessage());
|
|
3633
|
+
return false;
|
|
3634
|
+
}
|
|
1552
3635
|
}
|
|
1553
3636
|
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
3637
|
+
private void clearPreviewSessionForNativeBuildChange() {
|
|
3638
|
+
if (
|
|
3639
|
+
!Boolean.TRUE.equals(this.previewSessionEnabled) &&
|
|
3640
|
+
this.implementation.getPreviewFallbackBundle() == null &&
|
|
3641
|
+
!this.hasSavedPreviewSessions()
|
|
3642
|
+
) {
|
|
1560
3643
|
return;
|
|
1561
3644
|
}
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
3645
|
+
logger.info("Native build changed; clearing preview session state");
|
|
3646
|
+
this.previewSessionEnabled = false;
|
|
3647
|
+
this.previewSessionAlertPending = false;
|
|
3648
|
+
this.isLeavingPreviewForIncomingLink = false;
|
|
3649
|
+
this.implementation.previewSession = false;
|
|
3650
|
+
this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
|
|
3651
|
+
this.shakeChannelSelectorEnabled = this.getConfig().getBoolean("allowShakeChannelSelector", false);
|
|
3652
|
+
this.shakeMenuGesture = normalizedShakeMenuGesture(this.getConfig().getString("shakeMenuGesture", SHAKE_MENU_GESTURE_SHAKE));
|
|
3653
|
+
this.syncShakeMenuLifecycle();
|
|
3654
|
+
this.restorePreviewPreviousAppId();
|
|
3655
|
+
this.restorePreviewPreviousDefaultChannel();
|
|
3656
|
+
this.implementation.setPreviewFallbackBundle(null);
|
|
3657
|
+
this.implementation.setNextBundle(null);
|
|
3658
|
+
this.clearPreviewSessionPreferences();
|
|
3659
|
+
synchronized (this.previewSessionsLock) {
|
|
3660
|
+
this.editor.remove(PREVIEW_SESSIONS_PREF_KEY);
|
|
3661
|
+
this.editor.apply();
|
|
1573
3662
|
}
|
|
1574
3663
|
}
|
|
1575
3664
|
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
logger.error("Set called without id");
|
|
1581
|
-
call.reject("Set called without id");
|
|
3665
|
+
private void restorePreviewPreviousNextBundle() {
|
|
3666
|
+
final String previousNextBundleId = this.prefs.getString(PREVIEW_PREVIOUS_NEXT_BUNDLE_PREF_KEY, null);
|
|
3667
|
+
if (previousNextBundleId == null || previousNextBundleId.isEmpty()) {
|
|
3668
|
+
this.implementation.setNextBundle(null);
|
|
1582
3669
|
return;
|
|
1583
3670
|
}
|
|
1584
|
-
|
|
1585
|
-
logger.
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
3671
|
+
if (!this.implementation.setNextBundle(previousNextBundleId)) {
|
|
3672
|
+
logger.warn("Could not restore pre-preview next bundle: " + previousNextBundleId);
|
|
3673
|
+
this.implementation.setNextBundle(null);
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
private void ensureShakeMenuStarted() {
|
|
3678
|
+
if (shakeMenu != null && !shakeMenu.usesGesture(this.shakeMenuGesture)) {
|
|
3679
|
+
try {
|
|
3680
|
+
shakeMenu.stop();
|
|
3681
|
+
shakeMenu = null;
|
|
3682
|
+
logger.info("Shake menu restarted for " + this.shakeMenuGesture + " gesture");
|
|
3683
|
+
} catch (Exception e) {
|
|
3684
|
+
logger.error("Failed to restart shake menu: " + e.getMessage());
|
|
3685
|
+
return;
|
|
1596
3686
|
}
|
|
1597
|
-
} catch (final Exception e) {
|
|
1598
|
-
logger.error("Could not set id " + id + " " + e.getMessage());
|
|
1599
|
-
call.reject("Could not set id " + id, e);
|
|
1600
3687
|
}
|
|
3688
|
+
|
|
3689
|
+
if (getActivity() instanceof com.getcapacitor.BridgeActivity && shakeMenu == null) {
|
|
3690
|
+
try {
|
|
3691
|
+
shakeMenu = new ShakeMenu(this, (com.getcapacitor.BridgeActivity) getActivity(), logger, this.shakeMenuGesture);
|
|
3692
|
+
logger.info("Shake menu initialized with " + this.shakeMenuGesture + " gesture");
|
|
3693
|
+
} catch (Exception e) {
|
|
3694
|
+
logger.error("Failed to initialize shake menu: " + e.getMessage());
|
|
3695
|
+
}
|
|
3696
|
+
}
|
|
3697
|
+
}
|
|
3698
|
+
|
|
3699
|
+
private void syncShakeMenuLifecycle() {
|
|
3700
|
+
if (this.shouldListenForShake()) {
|
|
3701
|
+
this.ensureShakeMenuStarted();
|
|
3702
|
+
} else if (shakeMenu != null) {
|
|
3703
|
+
try {
|
|
3704
|
+
shakeMenu.stop();
|
|
3705
|
+
shakeMenu = null;
|
|
3706
|
+
logger.info("Shake menu stopped");
|
|
3707
|
+
} catch (Exception e) {
|
|
3708
|
+
logger.error("Failed to stop shake menu: " + e.getMessage());
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
|
|
3713
|
+
private boolean shouldListenForShake() {
|
|
3714
|
+
return Boolean.TRUE.equals(this.shakeMenuEnabled) || Boolean.TRUE.equals(this.shakeChannelSelectorEnabled);
|
|
3715
|
+
}
|
|
3716
|
+
|
|
3717
|
+
private void showPreviewSessionNoticeIfNeeded() {
|
|
3718
|
+
if (!Boolean.TRUE.equals(this.previewSessionEnabled) || !Boolean.TRUE.equals(this.previewSessionAlertPending)) {
|
|
3719
|
+
return;
|
|
3720
|
+
}
|
|
3721
|
+
this.previewSessionAlertPending = false;
|
|
3722
|
+
this.editor.putBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, false);
|
|
3723
|
+
this.editor.apply();
|
|
3724
|
+
|
|
3725
|
+
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
|
3726
|
+
try {
|
|
3727
|
+
if (!Boolean.TRUE.equals(this.previewSessionEnabled)) {
|
|
3728
|
+
return;
|
|
3729
|
+
}
|
|
3730
|
+
if (getActivity() == null || getActivity().isFinishing()) {
|
|
3731
|
+
this.previewSessionAlertPending = true;
|
|
3732
|
+
this.editor.putBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, true);
|
|
3733
|
+
this.editor.apply();
|
|
3734
|
+
return;
|
|
3735
|
+
}
|
|
3736
|
+
|
|
3737
|
+
new AlertDialog.Builder(getActivity())
|
|
3738
|
+
.setTitle("Preview started")
|
|
3739
|
+
.setMessage("Shake your device anytime to reload or leave the test app.")
|
|
3740
|
+
.setPositiveButton("Got it", (dialog, which) -> dialog.dismiss())
|
|
3741
|
+
.show();
|
|
3742
|
+
} catch (final Exception e) {
|
|
3743
|
+
this.previewSessionAlertPending = true;
|
|
3744
|
+
this.editor.putBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, true);
|
|
3745
|
+
this.editor.apply();
|
|
3746
|
+
logger.warn("Could not show preview session notice: " + e.getMessage());
|
|
3747
|
+
}
|
|
3748
|
+
}, 600);
|
|
1601
3749
|
}
|
|
1602
3750
|
|
|
1603
3751
|
@PluginMethod
|
|
@@ -1681,33 +3829,146 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1681
3829
|
@PluginMethod
|
|
1682
3830
|
public void getLatest(final PluginCall call) {
|
|
1683
3831
|
final String channel = call.getString("channel");
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
call.reject(
|
|
1705
|
-
return;
|
|
3832
|
+
final boolean includeBundleSize = call.getBoolean("includeBundleSize", false);
|
|
3833
|
+
final String previewAppId = this.normalizePreviewAppId(call.getString("appId"));
|
|
3834
|
+
final boolean hasPreviewAppId = previewAppId != null;
|
|
3835
|
+
if (hasPreviewAppId && !Boolean.TRUE.equals(this.allowPreview)) {
|
|
3836
|
+
logger.error("getLatest preview override not allowed set allowPreview in your config to true to enable it");
|
|
3837
|
+
call.reject("getLatest preview override not allowed");
|
|
3838
|
+
return;
|
|
3839
|
+
}
|
|
3840
|
+
|
|
3841
|
+
final Callback latestCallback = (res) -> {
|
|
3842
|
+
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
3843
|
+
if (jsRes.has("error") || jsRes.has("kind")) {
|
|
3844
|
+
String error = jsRes.has("error") ? jsRes.getString("error") : "";
|
|
3845
|
+
String errorMessage = jsRes.has("message") ? jsRes.getString("message") : "server did not provide a message";
|
|
3846
|
+
String kind = CapacitorUpdaterPlugin.this.getUpdateResponseKind(jsRes.has("kind") ? jsRes.getString("kind") : null);
|
|
3847
|
+
String latestVersion = jsRes.has("version") ? jsRes.getString("version") : "";
|
|
3848
|
+
jsRes.put("kind", kind);
|
|
3849
|
+
CapacitorUpdaterPlugin.this.notifyBreakingEventsIfNeeded(jsRes, latestVersion);
|
|
3850
|
+
if ("failed".equals(kind)) {
|
|
3851
|
+
logger.error("getLatest failed with error: " + error + ", message: " + errorMessage);
|
|
3852
|
+
call.reject(error.isEmpty() ? errorMessage : error);
|
|
1706
3853
|
} else {
|
|
3854
|
+
if (!jsRes.has("version") || jsRes.getString("version").isEmpty()) {
|
|
3855
|
+
jsRes.put("version", CapacitorUpdaterPlugin.this.implementation.getCurrentBundle().getVersionName());
|
|
3856
|
+
}
|
|
3857
|
+
logger.info("getLatest returned " + kind + ": " + errorMessage);
|
|
1707
3858
|
call.resolve(jsRes);
|
|
1708
3859
|
}
|
|
1709
|
-
|
|
1710
|
-
|
|
3860
|
+
return;
|
|
3861
|
+
} else if (jsRes.has("message")) {
|
|
3862
|
+
String latestVersion = jsRes.has("version") ? jsRes.getString("version") : "";
|
|
3863
|
+
CapacitorUpdaterPlugin.this.notifyBreakingEventsIfNeeded(jsRes, latestVersion);
|
|
3864
|
+
call.reject(jsRes.getString("message"));
|
|
3865
|
+
return;
|
|
3866
|
+
} else {
|
|
3867
|
+
if (includeBundleSize) {
|
|
3868
|
+
CapacitorUpdaterPlugin.this.attachBundleSize(jsRes);
|
|
3869
|
+
}
|
|
3870
|
+
call.resolve(jsRes);
|
|
3871
|
+
}
|
|
3872
|
+
};
|
|
3873
|
+
|
|
3874
|
+
startNewThread(() -> {
|
|
3875
|
+
if (hasPreviewAppId) {
|
|
3876
|
+
CapacitorUpdaterPlugin.this.implementation.getLatest(
|
|
3877
|
+
CapacitorUpdaterPlugin.this.updateUrl,
|
|
3878
|
+
channel,
|
|
3879
|
+
previewAppId,
|
|
3880
|
+
latestCallback
|
|
3881
|
+
);
|
|
3882
|
+
return;
|
|
3883
|
+
}
|
|
3884
|
+
CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, channel, latestCallback);
|
|
3885
|
+
});
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3888
|
+
private void attachBundleSize(final JSObject latest) {
|
|
3889
|
+
try {
|
|
3890
|
+
final JSONArray manifest = latest.optJSONArray("manifest");
|
|
3891
|
+
if (manifest == null || manifest.length() == 0) {
|
|
3892
|
+
return;
|
|
3893
|
+
}
|
|
3894
|
+
final String sessionKey = latest.optString("sessionKey", "");
|
|
3895
|
+
final JSONObject missing = this.implementation.missingBundleFilesResult(manifest, sessionKey);
|
|
3896
|
+
final JSONArray missingManifest = missing.getJSONArray("missing");
|
|
3897
|
+
final JSONObject size = this.implementation.getBundleDownloadSize(
|
|
3898
|
+
this.updateUrl,
|
|
3899
|
+
latest.optString("version", ""),
|
|
3900
|
+
missingManifest
|
|
3901
|
+
);
|
|
3902
|
+
latest.put("missing", missing);
|
|
3903
|
+
latest.put("downloadSize", size);
|
|
3904
|
+
} catch (Exception e) {
|
|
3905
|
+
logger.error("Failed to attach bundle size to getLatest result");
|
|
3906
|
+
logger.debug("Error: " + e.getMessage());
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
3909
|
+
|
|
3910
|
+
@PluginMethod
|
|
3911
|
+
public void getMissingBundleFiles(final PluginCall call) {
|
|
3912
|
+
final JSONArray manifest = call.getData().optJSONArray("manifest");
|
|
3913
|
+
if (manifest == null) {
|
|
3914
|
+
call.reject("getMissingBundleFiles called without manifest");
|
|
3915
|
+
return;
|
|
3916
|
+
}
|
|
3917
|
+
String sessionKey = call.getString("sessionKey");
|
|
3918
|
+
if (sessionKey == null) {
|
|
3919
|
+
sessionKey = "";
|
|
3920
|
+
}
|
|
3921
|
+
final String finalSessionKey = sessionKey;
|
|
3922
|
+
startNewThread(() -> {
|
|
3923
|
+
try {
|
|
3924
|
+
call.resolve(jsonObjectToJSObject(this.implementation.missingBundleFilesResult(manifest, finalSessionKey)));
|
|
3925
|
+
} catch (Exception e) {
|
|
3926
|
+
call.reject("Could not get missing bundle files", e);
|
|
3927
|
+
}
|
|
3928
|
+
});
|
|
3929
|
+
}
|
|
3930
|
+
|
|
3931
|
+
@PluginMethod
|
|
3932
|
+
public void getBundleDownloadSize(final PluginCall call) {
|
|
3933
|
+
final JSONArray manifest = call.getData().optJSONArray("manifest");
|
|
3934
|
+
if (manifest == null) {
|
|
3935
|
+
call.reject("getBundleDownloadSize called without manifest");
|
|
3936
|
+
return;
|
|
3937
|
+
}
|
|
3938
|
+
final String version = call.getData().optString("version", "");
|
|
3939
|
+
startNewThread(() -> {
|
|
3940
|
+
try {
|
|
3941
|
+
final JSONObject size = this.implementation.getBundleDownloadSize(this.updateUrl, version, manifest);
|
|
3942
|
+
call.resolve(jsonObjectToJSObject(size));
|
|
3943
|
+
} catch (Exception e) {
|
|
3944
|
+
call.reject("Could not get bundle download size", e);
|
|
3945
|
+
}
|
|
3946
|
+
});
|
|
3947
|
+
}
|
|
3948
|
+
|
|
3949
|
+
public String triggerBackgroundUpdateCheck() {
|
|
3950
|
+
if (this.updateUrl == null || this.updateUrl.isEmpty() || !this.isValidURL(this.updateUrl)) {
|
|
3951
|
+
logger.error("Error no url or wrong format");
|
|
3952
|
+
return "unavailable";
|
|
3953
|
+
}
|
|
3954
|
+
if (this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
3955
|
+
return "preview_session";
|
|
3956
|
+
}
|
|
3957
|
+
if (this.isDownloadStuckOrTimedOut()) {
|
|
3958
|
+
logger.info("Download already in progress, skipping duplicate download request");
|
|
3959
|
+
return "already_running";
|
|
3960
|
+
}
|
|
3961
|
+
this.backgroundDownload();
|
|
3962
|
+
return "queued";
|
|
3963
|
+
}
|
|
3964
|
+
|
|
3965
|
+
@PluginMethod
|
|
3966
|
+
public void triggerUpdateCheck(final PluginCall call) {
|
|
3967
|
+
final String status = this.triggerBackgroundUpdateCheck();
|
|
3968
|
+
final JSObject ret = new JSObject();
|
|
3969
|
+
ret.put("status", status);
|
|
3970
|
+
ret.put("queued", "queued".equals(status));
|
|
3971
|
+
call.resolve(ret);
|
|
1711
3972
|
}
|
|
1712
3973
|
|
|
1713
3974
|
private boolean _reset(final Boolean toLastSuccessful, final Boolean usePendingBundle) {
|
|
@@ -1783,19 +4044,21 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1783
4044
|
|
|
1784
4045
|
@PluginMethod
|
|
1785
4046
|
public void reset(final PluginCall call) {
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
4047
|
+
startNewThread(() -> {
|
|
4048
|
+
try {
|
|
4049
|
+
final Boolean toLastSuccessful = call.getBoolean("toLastSuccessful", false);
|
|
4050
|
+
final Boolean usePendingBundle = call.getBoolean("usePendingBundle", false);
|
|
4051
|
+
if (this._reset(toLastSuccessful, usePendingBundle)) {
|
|
4052
|
+
call.resolve();
|
|
4053
|
+
return;
|
|
4054
|
+
}
|
|
4055
|
+
logger.error("Reset failed");
|
|
4056
|
+
call.reject("Reset failed");
|
|
4057
|
+
} catch (final Exception e) {
|
|
4058
|
+
logger.error("Reset failed " + e.getMessage());
|
|
4059
|
+
call.reject("Reset failed", e);
|
|
1792
4060
|
}
|
|
1793
|
-
|
|
1794
|
-
call.reject("Reset failed");
|
|
1795
|
-
} catch (final Exception e) {
|
|
1796
|
-
logger.error("Reset failed " + e.getMessage());
|
|
1797
|
-
call.reject("Reset failed", e);
|
|
1798
|
-
}
|
|
4061
|
+
});
|
|
1799
4062
|
}
|
|
1800
4063
|
|
|
1801
4064
|
@PluginMethod
|
|
@@ -1859,7 +4122,13 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1859
4122
|
@Override
|
|
1860
4123
|
public void run() {
|
|
1861
4124
|
try {
|
|
4125
|
+
if (CapacitorUpdaterPlugin.this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
4126
|
+
return;
|
|
4127
|
+
}
|
|
1862
4128
|
CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, null, (res) -> {
|
|
4129
|
+
if (CapacitorUpdaterPlugin.this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
4130
|
+
return;
|
|
4131
|
+
}
|
|
1863
4132
|
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
1864
4133
|
if (jsRes.has("error") || jsRes.has("kind")) {
|
|
1865
4134
|
final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
|
|
@@ -1922,6 +4191,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1922
4191
|
logger.info("semaphoreReady countDown");
|
|
1923
4192
|
this.semaphoreDown();
|
|
1924
4193
|
logger.info("semaphoreReady countDown done");
|
|
4194
|
+
this.clearIncomingPreviewTransition();
|
|
4195
|
+
this.hidePreviewTransitionLoader("notify-app-ready");
|
|
1925
4196
|
final JSObject ret = new JSObject();
|
|
1926
4197
|
ret.put("bundle", InternalUtils.mapToJSObject(bundle.toJSONMap()));
|
|
1927
4198
|
call.resolve(ret);
|
|
@@ -1969,6 +4240,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1969
4240
|
}
|
|
1970
4241
|
|
|
1971
4242
|
private Boolean _isAutoUpdateEnabled() {
|
|
4243
|
+
if (this.isPreviewSessionStateActive()) {
|
|
4244
|
+
return false;
|
|
4245
|
+
}
|
|
1972
4246
|
final CapConfig config = CapConfig.loadDefault(this.getActivity());
|
|
1973
4247
|
String serverUrl = config.getServerUrl();
|
|
1974
4248
|
if (serverUrl != null && !serverUrl.isEmpty()) {
|
|
@@ -2012,10 +4286,22 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2012
4286
|
this.checkAppReady(this.resolveAppReadyCheckTimeoutMs());
|
|
2013
4287
|
}
|
|
2014
4288
|
|
|
4289
|
+
synchronized boolean shouldInterruptAppReadyCheck(final Thread existingCheck, final Thread currentThread) {
|
|
4290
|
+
return existingCheck != null && existingCheck != currentThread;
|
|
4291
|
+
}
|
|
4292
|
+
|
|
4293
|
+
synchronized void clearAppReadyCheckIfCurrent(final Thread expectedThread) {
|
|
4294
|
+
if (this.appReadyCheck == expectedThread) {
|
|
4295
|
+
this.appReadyCheck = null;
|
|
4296
|
+
}
|
|
4297
|
+
}
|
|
4298
|
+
|
|
2015
4299
|
private void checkAppReady(final long waitTimeMs) {
|
|
2016
4300
|
try {
|
|
2017
|
-
|
|
2018
|
-
|
|
4301
|
+
final Thread currentThread = Thread.currentThread();
|
|
4302
|
+
final Thread existingCheck = this.appReadyCheck;
|
|
4303
|
+
if (this.shouldInterruptAppReadyCheck(existingCheck, currentThread)) {
|
|
4304
|
+
existingCheck.interrupt();
|
|
2019
4305
|
}
|
|
2020
4306
|
this.appReadyCheck = startNewThread(new DeferredNotifyAppReadyCheck(waitTimeMs));
|
|
2021
4307
|
} catch (final Exception e) {
|
|
@@ -2032,13 +4318,17 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2032
4318
|
}
|
|
2033
4319
|
}
|
|
2034
4320
|
|
|
2035
|
-
|
|
4321
|
+
static String normalizedUpdateResponseKind(final String kind) {
|
|
2036
4322
|
if ("up_to_date".equals(kind) || "blocked".equals(kind) || "failed".equals(kind)) {
|
|
2037
4323
|
return kind;
|
|
2038
4324
|
}
|
|
2039
4325
|
return "failed";
|
|
2040
4326
|
}
|
|
2041
4327
|
|
|
4328
|
+
private String getUpdateResponseKind(final String kind) {
|
|
4329
|
+
return normalizedUpdateResponseKind(kind);
|
|
4330
|
+
}
|
|
4331
|
+
|
|
2042
4332
|
private void notifyUpdateCheckResult(
|
|
2043
4333
|
final String kind,
|
|
2044
4334
|
final String error,
|
|
@@ -2098,6 +4388,30 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2098
4388
|
String failureAction,
|
|
2099
4389
|
String failureEvent,
|
|
2100
4390
|
boolean shouldSendStats
|
|
4391
|
+
) {
|
|
4392
|
+
endBackGroundTaskWithNotif(
|
|
4393
|
+
msg,
|
|
4394
|
+
latestVersionName,
|
|
4395
|
+
current,
|
|
4396
|
+
error,
|
|
4397
|
+
plannedDirectUpdate,
|
|
4398
|
+
failureAction,
|
|
4399
|
+
failureEvent,
|
|
4400
|
+
shouldSendStats,
|
|
4401
|
+
true
|
|
4402
|
+
);
|
|
4403
|
+
}
|
|
4404
|
+
|
|
4405
|
+
private void endBackGroundTaskWithNotif(
|
|
4406
|
+
String msg,
|
|
4407
|
+
String latestVersionName,
|
|
4408
|
+
BundleInfo current,
|
|
4409
|
+
Boolean error,
|
|
4410
|
+
Boolean plannedDirectUpdate,
|
|
4411
|
+
String failureAction,
|
|
4412
|
+
String failureEvent,
|
|
4413
|
+
boolean shouldSendStats,
|
|
4414
|
+
boolean shouldNotifyNoNeedUpdate
|
|
2101
4415
|
) {
|
|
2102
4416
|
this.consumeOnLaunchDirectUpdateAttempt(Boolean.TRUE.equals(plannedDirectUpdate));
|
|
2103
4417
|
if (error) {
|
|
@@ -2116,15 +4430,22 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2116
4430
|
ret.put("version", latestVersionName);
|
|
2117
4431
|
this.notifyListeners(failureEvent, ret);
|
|
2118
4432
|
}
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
4433
|
+
if (shouldNotifyNoNeedUpdate) {
|
|
4434
|
+
final JSObject ret = new JSObject();
|
|
4435
|
+
ret.put("bundle", InternalUtils.mapToJSObject(current.toJSONMap()));
|
|
4436
|
+
this.notifyListeners("noNeedUpdate", ret);
|
|
4437
|
+
}
|
|
2122
4438
|
this.sendReadyToJs(current, msg, plannedDirectUpdate);
|
|
2123
4439
|
this.backgroundDownloadTask = null;
|
|
2124
4440
|
this.downloadStartTimeMs = 0;
|
|
2125
4441
|
logger.info("endBackGroundTaskWithNotif " + msg);
|
|
2126
4442
|
}
|
|
2127
4443
|
|
|
4444
|
+
private void clearBackgroundDownloadState() {
|
|
4445
|
+
this.backgroundDownloadTask = null;
|
|
4446
|
+
this.downloadStartTimeMs = 0;
|
|
4447
|
+
}
|
|
4448
|
+
|
|
2128
4449
|
private boolean isDownloadStuckOrTimedOut() {
|
|
2129
4450
|
if (this.backgroundDownloadTask == null || !this.backgroundDownloadTask.isAlive()) {
|
|
2130
4451
|
return false;
|
|
@@ -2151,17 +4472,30 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2151
4472
|
}
|
|
2152
4473
|
|
|
2153
4474
|
private Thread backgroundDownload() {
|
|
4475
|
+
if (this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
4476
|
+
return null;
|
|
4477
|
+
}
|
|
2154
4478
|
final boolean plannedDirectUpdate = this.shouldUseDirectUpdate();
|
|
2155
4479
|
final boolean initialDirectUpdateAllowed = this.isDirectUpdateCurrentlyAllowed(plannedDirectUpdate);
|
|
2156
4480
|
final String messageUpdate = initialDirectUpdateAllowed
|
|
2157
4481
|
? "Update will occur now."
|
|
2158
|
-
:
|
|
4482
|
+
: this.shouldAutoSetNextBundle()
|
|
4483
|
+
? "Update will occur next time app moves to background."
|
|
4484
|
+
: "Update will be downloaded and made available.";
|
|
2159
4485
|
Thread newTask = startNewThread(() -> {
|
|
2160
4486
|
// Wait for cleanup to complete before starting download
|
|
2161
4487
|
waitForCleanupIfNeeded();
|
|
4488
|
+
if (CapacitorUpdaterPlugin.this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
4489
|
+
CapacitorUpdaterPlugin.this.clearBackgroundDownloadState();
|
|
4490
|
+
return;
|
|
4491
|
+
}
|
|
2162
4492
|
logger.info("Check for update via: " + CapacitorUpdaterPlugin.this.updateUrl);
|
|
2163
4493
|
try {
|
|
2164
4494
|
CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, null, (res) -> {
|
|
4495
|
+
if (CapacitorUpdaterPlugin.this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
4496
|
+
CapacitorUpdaterPlugin.this.clearBackgroundDownloadState();
|
|
4497
|
+
return;
|
|
4498
|
+
}
|
|
2165
4499
|
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
2166
4500
|
final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
|
|
2167
4501
|
|
|
@@ -2173,6 +4507,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2173
4507
|
String kind = CapacitorUpdaterPlugin.this.getUpdateResponseKind(jsRes.has("kind") ? jsRes.getString("kind") : null);
|
|
2174
4508
|
String latestVersion = jsRes.has("version") ? jsRes.getString("version") : current.getVersionName();
|
|
2175
4509
|
CapacitorUpdaterPlugin.this.notifyUpdateCheckResult(kind, error, errorMessage, statusCode, latestVersion, current);
|
|
4510
|
+
CapacitorUpdaterPlugin.this.notifyBreakingEventsIfNeeded(
|
|
4511
|
+
jsRes,
|
|
4512
|
+
jsRes.has("version") ? jsRes.getString("version") : ""
|
|
4513
|
+
);
|
|
2176
4514
|
|
|
2177
4515
|
if ("up_to_date".equals(kind)) {
|
|
2178
4516
|
logger.info("No new version available");
|
|
@@ -2215,7 +4553,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2215
4553
|
false,
|
|
2216
4554
|
true
|
|
2217
4555
|
);
|
|
2218
|
-
} else {
|
|
4556
|
+
} else if (CapacitorUpdaterPlugin.this.shouldAutoSetNextBundle()) {
|
|
2219
4557
|
if (plannedDirectUpdate && !directUpdateAllowedNow) {
|
|
2220
4558
|
logger.info(
|
|
2221
4559
|
"Direct update skipped because splashscreen timeout occurred. Update will be applied later."
|
|
@@ -2230,11 +4568,34 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2230
4568
|
false,
|
|
2231
4569
|
plannedDirectUpdate
|
|
2232
4570
|
);
|
|
4571
|
+
} else {
|
|
4572
|
+
logger.info("autoUpdate is set to onlyDownload, builtin version will not be set as next bundle");
|
|
4573
|
+
final boolean builtinUpdateAvailable = !current.isBuiltin();
|
|
4574
|
+
if (builtinUpdateAvailable) {
|
|
4575
|
+
final JSObject ret = new JSObject();
|
|
4576
|
+
final BundleInfo builtinBundle = CapacitorUpdaterPlugin.this.implementation.getBundleInfo(
|
|
4577
|
+
BundleInfo.ID_BUILTIN
|
|
4578
|
+
);
|
|
4579
|
+
ret.put("bundle", InternalUtils.mapToJSObject(builtinBundle.toJSONMap()));
|
|
4580
|
+
CapacitorUpdaterPlugin.this.notifyListeners("updateAvailable", ret);
|
|
4581
|
+
}
|
|
4582
|
+
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
4583
|
+
"Latest version is builtin, autoUpdate onlyDownload",
|
|
4584
|
+
latestVersionName,
|
|
4585
|
+
current,
|
|
4586
|
+
false,
|
|
4587
|
+
plannedDirectUpdate,
|
|
4588
|
+
"download_fail",
|
|
4589
|
+
"downloadFailed",
|
|
4590
|
+
true,
|
|
4591
|
+
!builtinUpdateAvailable
|
|
4592
|
+
);
|
|
2233
4593
|
}
|
|
2234
4594
|
return;
|
|
2235
4595
|
}
|
|
2236
4596
|
|
|
2237
4597
|
if (!jsRes.has("url") || !CapacitorUpdaterPlugin.this.isValidURL(jsRes.getString("url"))) {
|
|
4598
|
+
CapacitorUpdaterPlugin.this.notifyBreakingEventsIfNeeded(jsRes, latestVersionName);
|
|
2238
4599
|
logger.error("Error no url or wrong format");
|
|
2239
4600
|
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
2240
4601
|
"Error no url or wrong format",
|
|
@@ -2305,7 +4666,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2305
4666
|
true
|
|
2306
4667
|
);
|
|
2307
4668
|
}
|
|
2308
|
-
} else {
|
|
4669
|
+
} else if (CapacitorUpdaterPlugin.this.shouldAutoSetNextBundle()) {
|
|
2309
4670
|
if (plannedDirectUpdate && !directUpdateAllowedNow) {
|
|
2310
4671
|
logger.info(
|
|
2311
4672
|
"Direct update skipped because splashscreen timeout occurred. Update will install on next background."
|
|
@@ -2320,6 +4681,20 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2320
4681
|
false,
|
|
2321
4682
|
plannedDirectUpdate
|
|
2322
4683
|
);
|
|
4684
|
+
} else {
|
|
4685
|
+
logger.info("autoUpdate is set to onlyDownload, downloaded update will not be set as next bundle");
|
|
4686
|
+
CapacitorUpdaterPlugin.this.notifyListeners("updateAvailable", ret);
|
|
4687
|
+
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
4688
|
+
"update downloaded, autoUpdate onlyDownload",
|
|
4689
|
+
latestVersionName,
|
|
4690
|
+
current,
|
|
4691
|
+
false,
|
|
4692
|
+
plannedDirectUpdate,
|
|
4693
|
+
"download_fail",
|
|
4694
|
+
"downloadFailed",
|
|
4695
|
+
true,
|
|
4696
|
+
false
|
|
4697
|
+
);
|
|
2323
4698
|
}
|
|
2324
4699
|
return;
|
|
2325
4700
|
}
|
|
@@ -2345,6 +4720,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2345
4720
|
: initialDirectUpdateAllowed;
|
|
2346
4721
|
startNewThread(() -> {
|
|
2347
4722
|
try {
|
|
4723
|
+
if (CapacitorUpdaterPlugin.this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
4724
|
+
CapacitorUpdaterPlugin.this.clearBackgroundDownloadState();
|
|
4725
|
+
return;
|
|
4726
|
+
}
|
|
2348
4727
|
logger.info(
|
|
2349
4728
|
"New bundle: " +
|
|
2350
4729
|
latestVersionName +
|
|
@@ -2366,7 +4745,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2366
4745
|
latestVersionName,
|
|
2367
4746
|
sessionKey,
|
|
2368
4747
|
checksum,
|
|
2369
|
-
manifest
|
|
4748
|
+
manifest,
|
|
4749
|
+
CapacitorUpdaterPlugin.this.shouldAutoSetNextBundle()
|
|
2370
4750
|
);
|
|
2371
4751
|
} else {
|
|
2372
4752
|
// Handle single file download (existing code)
|
|
@@ -2375,7 +4755,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2375
4755
|
latestVersionName,
|
|
2376
4756
|
sessionKey,
|
|
2377
4757
|
checksum,
|
|
2378
|
-
null
|
|
4758
|
+
null,
|
|
4759
|
+
CapacitorUpdaterPlugin.this.shouldAutoSetNextBundle()
|
|
2379
4760
|
);
|
|
2380
4761
|
}
|
|
2381
4762
|
} catch (final Exception e) {
|
|
@@ -2429,6 +4810,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2429
4810
|
|
|
2430
4811
|
private void installNext() {
|
|
2431
4812
|
try {
|
|
4813
|
+
if (this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
4814
|
+
return;
|
|
4815
|
+
}
|
|
2432
4816
|
String delayUpdatePreferences = prefs.getString(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES, "[]");
|
|
2433
4817
|
ArrayList<DelayCondition> delayConditionList = delayUpdateUtils.parseDelayConditions(delayUpdatePreferences);
|
|
2434
4818
|
if (!delayConditionList.isEmpty()) {
|
|
@@ -2464,6 +4848,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2464
4848
|
logger.info("Built-in bundle is active. We skip the check for notifyAppReady.");
|
|
2465
4849
|
return;
|
|
2466
4850
|
}
|
|
4851
|
+
if (this.isPreviewSessionStateActive()) {
|
|
4852
|
+
logger.info("Preview session is active. We skip the check for notifyAppReady.");
|
|
4853
|
+
return;
|
|
4854
|
+
}
|
|
2467
4855
|
logger.debug("Current bundle is: " + current);
|
|
2468
4856
|
|
|
2469
4857
|
if (BundleStatus.SUCCESS != current.getStatus()) {
|
|
@@ -2502,12 +4890,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2502
4890
|
|
|
2503
4891
|
@Override
|
|
2504
4892
|
public void run() {
|
|
4893
|
+
final Thread currentThread = Thread.currentThread();
|
|
2505
4894
|
try {
|
|
2506
4895
|
logger.info("Wait for " + this.waitTimeMs + "ms, then check for notifyAppReady");
|
|
2507
4896
|
Thread.sleep(this.waitTimeMs);
|
|
2508
4897
|
CapacitorUpdaterPlugin.this.checkRevert();
|
|
2509
|
-
CapacitorUpdaterPlugin.this.
|
|
4898
|
+
CapacitorUpdaterPlugin.this.clearAppReadyCheckIfCurrent(currentThread);
|
|
2510
4899
|
} catch (final InterruptedException e) {
|
|
4900
|
+
CapacitorUpdaterPlugin.this.clearAppReadyCheckIfCurrent(currentThread);
|
|
2511
4901
|
logger.info(DeferredNotifyAppReadyCheck.class.getName() + " was interrupted.");
|
|
2512
4902
|
}
|
|
2513
4903
|
}
|
|
@@ -2636,6 +5026,36 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2636
5026
|
}
|
|
2637
5027
|
}
|
|
2638
5028
|
|
|
5029
|
+
@Override
|
|
5030
|
+
protected void handleOnNewIntent(Intent intent) {
|
|
5031
|
+
super.handleOnNewIntent(intent);
|
|
5032
|
+
if (
|
|
5033
|
+
intent == null ||
|
|
5034
|
+
!Intent.ACTION_VIEW.equals(intent.getAction()) ||
|
|
5035
|
+
intent.getData() == null ||
|
|
5036
|
+
!Boolean.TRUE.equals(this.previewSessionEnabled) ||
|
|
5037
|
+
!isPreviewDeepLink(intent.getData()) ||
|
|
5038
|
+
Boolean.TRUE.equals(this.isLeavingPreviewForIncomingLink)
|
|
5039
|
+
) {
|
|
5040
|
+
return;
|
|
5041
|
+
}
|
|
5042
|
+
|
|
5043
|
+
this.isLeavingPreviewForIncomingLink = true;
|
|
5044
|
+
this.showPreviewTransitionLoader("incoming-preview-deeplink");
|
|
5045
|
+
if (getActivity() != null) {
|
|
5046
|
+
getActivity().setIntent(intent);
|
|
5047
|
+
}
|
|
5048
|
+
logger.info("Preview deeplink received while preview session is active; restoring fallback before routing");
|
|
5049
|
+
startNewThread(() -> {
|
|
5050
|
+
final boolean didLeave = this.leavePreviewSessionForIncomingPreviewLink();
|
|
5051
|
+
if (!didLeave) {
|
|
5052
|
+
logger.error("Could not leave preview session before routing incoming preview deeplink");
|
|
5053
|
+
this.isLeavingPreviewForIncomingLink = false;
|
|
5054
|
+
this.hidePreviewTransitionLoader("incoming-preview-deeplink-failed");
|
|
5055
|
+
}
|
|
5056
|
+
});
|
|
5057
|
+
}
|
|
5058
|
+
|
|
2639
5059
|
@Override
|
|
2640
5060
|
public void handleOnStart() {
|
|
2641
5061
|
try {
|
|
@@ -2651,15 +5071,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2651
5071
|
isPreviousMainActivity = true;
|
|
2652
5072
|
}
|
|
2653
5073
|
|
|
2654
|
-
|
|
2655
|
-
if (shakeMenuEnabled && getActivity() instanceof com.getcapacitor.BridgeActivity && shakeMenu == null) {
|
|
2656
|
-
try {
|
|
2657
|
-
shakeMenu = new ShakeMenu(this, (com.getcapacitor.BridgeActivity) getActivity(), logger);
|
|
2658
|
-
logger.info("Shake menu initialized");
|
|
2659
|
-
} catch (Exception e) {
|
|
2660
|
-
logger.error("Failed to initialize shake menu: " + e.getMessage());
|
|
2661
|
-
}
|
|
2662
|
-
}
|
|
5074
|
+
this.syncShakeMenuLifecycle();
|
|
2663
5075
|
} catch (Exception e) {
|
|
2664
5076
|
logger.error("Failed to run handleOnStart: " + e.getMessage());
|
|
2665
5077
|
}
|
|
@@ -2691,6 +5103,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2691
5103
|
backgroundTask.interrupt();
|
|
2692
5104
|
}
|
|
2693
5105
|
this.implementation.activity = getActivity();
|
|
5106
|
+
this.syncShakeMenuLifecycle();
|
|
2694
5107
|
} catch (Exception e) {
|
|
2695
5108
|
logger.error("Failed to run handleOnResume: " + e.getMessage());
|
|
2696
5109
|
}
|
|
@@ -2715,25 +5128,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2715
5128
|
}
|
|
2716
5129
|
|
|
2717
5130
|
this.shakeMenuEnabled = enabled;
|
|
2718
|
-
logger.info("Shake menu " + (enabled ? "enabled" : "disabled"));
|
|
2719
|
-
|
|
2720
|
-
// Manage shake menu instance based on enabled state
|
|
2721
|
-
if (enabled && getActivity() instanceof com.getcapacitor.BridgeActivity && shakeMenu == null) {
|
|
2722
|
-
try {
|
|
2723
|
-
shakeMenu = new ShakeMenu(this, (com.getcapacitor.BridgeActivity) getActivity(), logger);
|
|
2724
|
-
logger.info("Shake menu initialized");
|
|
2725
|
-
} catch (Exception e) {
|
|
2726
|
-
logger.error("Failed to initialize shake menu: " + e.getMessage());
|
|
2727
|
-
}
|
|
2728
|
-
} else if (!enabled && shakeMenu != null) {
|
|
2729
|
-
try {
|
|
2730
|
-
shakeMenu.stop();
|
|
2731
|
-
shakeMenu = null;
|
|
2732
|
-
logger.info("Shake menu stopped");
|
|
2733
|
-
} catch (Exception e) {
|
|
2734
|
-
logger.error("Failed to stop shake menu: " + e.getMessage());
|
|
2735
|
-
}
|
|
2736
|
-
}
|
|
5131
|
+
logger.info("Shake menu " + (enabled ? "enabled" : "disabled") + " with " + this.shakeMenuGesture + " gesture");
|
|
5132
|
+
this.syncShakeMenuLifecycle();
|
|
2737
5133
|
|
|
2738
5134
|
call.resolve();
|
|
2739
5135
|
}
|
|
@@ -2743,6 +5139,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2743
5139
|
try {
|
|
2744
5140
|
final JSObject ret = new JSObject();
|
|
2745
5141
|
ret.put("enabled", this.shakeMenuEnabled);
|
|
5142
|
+
ret.put("gesture", this.shakeMenuGesture);
|
|
2746
5143
|
call.resolve(ret);
|
|
2747
5144
|
} catch (final Exception e) {
|
|
2748
5145
|
logger.error("Could not get shake menu status " + e.getMessage());
|
|
@@ -2761,6 +5158,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2761
5158
|
|
|
2762
5159
|
this.shakeChannelSelectorEnabled = enabled;
|
|
2763
5160
|
logger.info("Shake channel selector " + (enabled ? "enabled" : "disabled"));
|
|
5161
|
+
this.syncShakeMenuLifecycle();
|
|
2764
5162
|
call.resolve();
|
|
2765
5163
|
}
|
|
2766
5164
|
|
|
@@ -2801,7 +5199,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2801
5199
|
call.reject("setAppId called without appId");
|
|
2802
5200
|
return;
|
|
2803
5201
|
}
|
|
2804
|
-
this.
|
|
5202
|
+
this.setActiveAppId(appId);
|
|
2805
5203
|
call.resolve();
|
|
2806
5204
|
}
|
|
2807
5205
|
|
|
@@ -3139,6 +5537,11 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
3139
5537
|
logger.error("Failed to clean up AppLifecycleObserver: " + e.getMessage());
|
|
3140
5538
|
}
|
|
3141
5539
|
}
|
|
5540
|
+
|
|
5541
|
+
if (webViewStatsListener != null && bridge != null) {
|
|
5542
|
+
bridge.removeWebViewListener(webViewStatsListener);
|
|
5543
|
+
webViewStatsListener = null;
|
|
5544
|
+
}
|
|
3142
5545
|
} catch (Exception e) {
|
|
3143
5546
|
logger.error("Failed to run handleOnDestroy: " + e.getMessage());
|
|
3144
5547
|
}
|