@capgo/capacitor-updater 7.45.10 → 7.50.2
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 +3192 -777
- 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,2464 @@ 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;
|
|
1477
|
+
private void installWebViewStatsReporter() {
|
|
1478
|
+
if (this.bridge == null || this.bridge.getWebView() == null || this.webViewStatsListener != null) {
|
|
1479
|
+
return;
|
|
924
1480
|
}
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1481
|
+
|
|
1482
|
+
final android.webkit.WebView webView = this.bridge.getWebView();
|
|
1483
|
+
final String script = buildWebViewStatsReporterScript();
|
|
1484
|
+
this.installDocumentStartWebViewStatsReporter(webView, script);
|
|
1485
|
+
|
|
1486
|
+
this.webViewStatsListener = new WebViewListener() {
|
|
1487
|
+
@Override
|
|
1488
|
+
public void onPageStarted(final android.webkit.WebView view) {
|
|
1489
|
+
CapacitorUpdaterPlugin.this.evaluateWebViewStatsReporterScript(view, script);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
@Override
|
|
1493
|
+
public void onPageLoaded(final android.webkit.WebView view) {
|
|
1494
|
+
CapacitorUpdaterPlugin.this.evaluateWebViewStatsReporterScript(view, script);
|
|
1495
|
+
}
|
|
1496
|
+
};
|
|
1497
|
+
|
|
1498
|
+
this.bridge.addWebViewListener(this.webViewStatsListener);
|
|
1499
|
+
// Keep RenderProcessGoneDetail off the Plugin class method table and off this
|
|
1500
|
+
// listener so Android < 8 (API 26) does not crash during plugin reflection.
|
|
1501
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
1502
|
+
this.installWebViewRenderProcessGoneReporter();
|
|
928
1503
|
}
|
|
1504
|
+
this.evaluateWebViewStatsReporterScript(webView, script);
|
|
929
1505
|
}
|
|
930
1506
|
|
|
931
|
-
private void
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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());
|
|
962
|
-
|
|
963
|
-
// Check again before the expensive delta cache cleanup
|
|
964
|
-
if (Thread.currentThread().isInterrupted()) {
|
|
965
|
-
logger.warn("Cleanup was cancelled before delta cache cleanup");
|
|
966
|
-
return;
|
|
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");
|
|
1507
|
+
private void installWebViewRenderProcessGoneReporter() {
|
|
1508
|
+
this.bridge.addWebViewListener(
|
|
1509
|
+
new WebViewListener() {
|
|
1510
|
+
@Override
|
|
1511
|
+
public boolean onRenderProcessGone(final android.webkit.WebView view, final RenderProcessGoneDetail detail) {
|
|
1512
|
+
final Map<String, String> metadata = CapacitorUpdaterPlugin.this.buildWebViewRenderProcessGoneMetadata(detail);
|
|
1513
|
+
CapacitorUpdaterPlugin.this.persistPendingWebViewRenderProcessGone(metadata);
|
|
1514
|
+
return false;
|
|
977
1515
|
}
|
|
978
1516
|
}
|
|
979
|
-
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
980
1519
|
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
cleanupThread.interrupt();
|
|
989
|
-
}
|
|
990
|
-
} catch (InterruptedException e) {
|
|
991
|
-
// Watchdog thread was interrupted, that's fine
|
|
1520
|
+
private void installDocumentStartWebViewStatsReporter(final android.webkit.WebView webView, final String script) {
|
|
1521
|
+
try {
|
|
1522
|
+
final Class<?> webViewFeature = Class.forName("androidx.webkit.WebViewFeature");
|
|
1523
|
+
final String feature = (String) webViewFeature.getField("DOCUMENT_START_SCRIPT").get(null);
|
|
1524
|
+
final Boolean supported = (Boolean) webViewFeature.getMethod("isFeatureSupported", String.class).invoke(null, feature);
|
|
1525
|
+
if (!Boolean.TRUE.equals(supported)) {
|
|
1526
|
+
return;
|
|
992
1527
|
}
|
|
993
|
-
|
|
1528
|
+
|
|
1529
|
+
final String allowedOrigin = Uri.parse(this.bridge.getAppUrl())
|
|
1530
|
+
.buildUpon()
|
|
1531
|
+
.path(null)
|
|
1532
|
+
.fragment(null)
|
|
1533
|
+
.clearQuery()
|
|
1534
|
+
.build()
|
|
1535
|
+
.toString();
|
|
1536
|
+
final Class<?> webViewCompat = Class.forName("androidx.webkit.WebViewCompat");
|
|
1537
|
+
webViewCompat
|
|
1538
|
+
.getMethod("addDocumentStartJavaScript", android.webkit.WebView.class, String.class, Set.class)
|
|
1539
|
+
.invoke(null, webView, script, java.util.Collections.singleton(allowedOrigin));
|
|
1540
|
+
} catch (final Exception e) {
|
|
1541
|
+
logger.debug("Unable to install document-start WebView stats reporter: " + e.getMessage());
|
|
1542
|
+
}
|
|
994
1543
|
}
|
|
995
1544
|
|
|
996
|
-
private void
|
|
997
|
-
if (
|
|
998
|
-
return;
|
|
1545
|
+
private void evaluateWebViewStatsReporterScript(final android.webkit.WebView webView, final String script) {
|
|
1546
|
+
if (webView == null) {
|
|
1547
|
+
return;
|
|
999
1548
|
}
|
|
1000
1549
|
|
|
1001
|
-
|
|
1550
|
+
this.mainHandler.post(() -> {
|
|
1551
|
+
try {
|
|
1552
|
+
webView.evaluateJavascript(script, null);
|
|
1553
|
+
} catch (final Exception e) {
|
|
1554
|
+
logger.debug("Unable to evaluate WebView stats reporter: " + e.getMessage());
|
|
1555
|
+
}
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1002
1558
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1559
|
+
private Map<String, String> buildWebViewRenderProcessGoneMetadata(final Object detailObj) {
|
|
1560
|
+
final Map<String, String> metadata = new HashMap<>();
|
|
1561
|
+
metadata.put("error_type", "render_process_gone");
|
|
1562
|
+
metadata.put("source", "android_on_render_process_gone");
|
|
1563
|
+
metadata.put("timestamp", Long.toString(System.currentTimeMillis()));
|
|
1564
|
+
// Parameter typed as Object so Android < 8 ART does not resolve
|
|
1565
|
+
// RenderProcessGoneDetail while reflecting CapacitorUpdaterPlugin methods.
|
|
1566
|
+
if (detailObj != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
1567
|
+
final RenderProcessGoneDetail detail = (RenderProcessGoneDetail) detailObj;
|
|
1568
|
+
metadata.put("did_crash", Boolean.toString(detail.didCrash()));
|
|
1569
|
+
metadata.put("renderer_priority_at_exit", Integer.toString(detail.rendererPriorityAtExit()));
|
|
1570
|
+
}
|
|
1571
|
+
return metadata;
|
|
1007
1572
|
}
|
|
1008
1573
|
|
|
1009
|
-
|
|
1574
|
+
private void persistPendingWebViewRenderProcessGone(final Map<String, String> metadata) {
|
|
1010
1575
|
try {
|
|
1011
|
-
final
|
|
1012
|
-
|
|
1013
|
-
final BundleInfo bundleInfo = this.implementation.getBundleInfo(id);
|
|
1014
|
-
ret.put("bundle", InternalUtils.mapToJSObject(bundleInfo.toJSONMap()));
|
|
1015
|
-
this.notifyListeners("download", ret);
|
|
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
|
-
}
|
|
1028
|
-
}
|
|
1576
|
+
final JSONObject json = new JSONObject(metadata);
|
|
1577
|
+
this.prefs.edit().putString(LAST_WEBVIEW_RENDER_PROCESS_GONE_PREF_KEY, json.toString()).commit();
|
|
1029
1578
|
} catch (final Exception e) {
|
|
1030
|
-
logger.
|
|
1579
|
+
logger.debug("Unable to persist WebView render process crash metadata: " + e.getMessage());
|
|
1031
1580
|
}
|
|
1032
1581
|
}
|
|
1033
1582
|
|
|
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");
|
|
1583
|
+
private void reportPreviousWebViewRenderProcessGone() {
|
|
1584
|
+
if (this.implementation == null || this.implementation.statsUrl.isEmpty()) {
|
|
1039
1585
|
return;
|
|
1040
1586
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
call.reject("setUpdateUrl called without url");
|
|
1587
|
+
|
|
1588
|
+
final String rawMetadata = this.prefs.getString(LAST_WEBVIEW_RENDER_PROCESS_GONE_PREF_KEY, "");
|
|
1589
|
+
if (rawMetadata == null || rawMetadata.isEmpty()) {
|
|
1045
1590
|
return;
|
|
1046
1591
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1592
|
+
|
|
1593
|
+
try {
|
|
1594
|
+
final Map<String, String> metadata = jsonObjectToStringMap(new JSONObject(rawMetadata));
|
|
1595
|
+
metadata.put("reported_after_restart", "true");
|
|
1596
|
+
this.reportWebViewStats("webview_render_process_gone", metadata);
|
|
1597
|
+
this.prefs.edit().remove(LAST_WEBVIEW_RENDER_PROCESS_GONE_PREF_KEY).apply();
|
|
1598
|
+
} catch (final JSONException e) {
|
|
1599
|
+
this.prefs.edit().remove(LAST_WEBVIEW_RENDER_PROCESS_GONE_PREF_KEY).apply();
|
|
1051
1600
|
}
|
|
1052
|
-
call.resolve();
|
|
1053
1601
|
}
|
|
1054
1602
|
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
return;
|
|
1061
|
-
}
|
|
1062
|
-
final String url = call.getString("url");
|
|
1063
|
-
if (url == null) {
|
|
1064
|
-
logger.error("setStatsUrl called without url");
|
|
1065
|
-
call.reject("setStatsUrl called without url");
|
|
1066
|
-
return;
|
|
1603
|
+
private static Map<String, String> jsonObjectToStringMap(final JSONObject json) throws JSONException {
|
|
1604
|
+
final Map<String, String> map = new HashMap<>();
|
|
1605
|
+
final JSONArray names = json.names();
|
|
1606
|
+
if (names == null) {
|
|
1607
|
+
return map;
|
|
1067
1608
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1609
|
+
|
|
1610
|
+
for (int i = 0; i < names.length(); i++) {
|
|
1611
|
+
final String key = names.getString(i);
|
|
1612
|
+
final String value = json.optString(key, "");
|
|
1613
|
+
if (!value.isEmpty()) {
|
|
1614
|
+
map.put(key, value);
|
|
1615
|
+
}
|
|
1072
1616
|
}
|
|
1073
|
-
|
|
1617
|
+
return map;
|
|
1074
1618
|
}
|
|
1075
1619
|
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
return;
|
|
1082
|
-
}
|
|
1083
|
-
final String url = call.getString("url");
|
|
1084
|
-
if (url == null) {
|
|
1085
|
-
logger.error("setChannelUrl called without url");
|
|
1086
|
-
call.reject("setChannelUrl called without url");
|
|
1087
|
-
return;
|
|
1620
|
+
private static JSObject jsonObjectToJSObject(final JSONObject json) throws JSONException {
|
|
1621
|
+
final JSObject ret = new JSObject();
|
|
1622
|
+
final JSONArray names = json.names();
|
|
1623
|
+
if (names == null) {
|
|
1624
|
+
return ret;
|
|
1088
1625
|
}
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
this.editor.apply();
|
|
1626
|
+
for (int i = 0; i < names.length(); i++) {
|
|
1627
|
+
final String key = names.getString(i);
|
|
1628
|
+
ret.put(key, json.get(key));
|
|
1093
1629
|
}
|
|
1094
|
-
|
|
1630
|
+
return ret;
|
|
1095
1631
|
}
|
|
1096
1632
|
|
|
1097
1633
|
@PluginMethod
|
|
1098
|
-
public void
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1634
|
+
public void reportWebViewError(final PluginCall call) {
|
|
1635
|
+
final JSObject data = call.getData();
|
|
1636
|
+
this.reportWebViewStats(
|
|
1637
|
+
statsActionForWebViewErrorType(data.optString("type", "javascript_error")),
|
|
1638
|
+
buildWebViewErrorMetadata(data)
|
|
1639
|
+
);
|
|
1640
|
+
call.resolve();
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
private void reportWebViewStats(final String action, final Map<String, String> metadata) {
|
|
1644
|
+
if (this.implementation == null) {
|
|
1645
|
+
return;
|
|
1106
1646
|
}
|
|
1647
|
+
|
|
1648
|
+
final BundleInfo current = this.implementation.getCurrentBundle();
|
|
1649
|
+
final String versionName = current == null ? "" : current.getVersionName();
|
|
1650
|
+
this.implementation.sendStats(action, versionName, "", metadata);
|
|
1107
1651
|
}
|
|
1108
1652
|
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1653
|
+
static String statsActionForWebViewErrorType(final String type) {
|
|
1654
|
+
switch (type) {
|
|
1655
|
+
case "unhandled_rejection":
|
|
1656
|
+
return "webview_unhandled_rejection";
|
|
1657
|
+
case "resource_error":
|
|
1658
|
+
return "webview_resource_error";
|
|
1659
|
+
case "security_policy_violation":
|
|
1660
|
+
return "webview_security_policy_violation";
|
|
1661
|
+
case "webview_unclean_restart":
|
|
1662
|
+
return "webview_unclean_restart";
|
|
1663
|
+
case "render_process_gone":
|
|
1664
|
+
return "webview_render_process_gone";
|
|
1665
|
+
case "web_content_process_terminated":
|
|
1666
|
+
return "webview_content_process_terminated";
|
|
1667
|
+
case "javascript_error":
|
|
1668
|
+
default:
|
|
1669
|
+
return "webview_javascript_error";
|
|
1118
1670
|
}
|
|
1119
1671
|
}
|
|
1120
1672
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1673
|
+
static Map<String, String> buildWebViewErrorMetadata(final JSObject data) {
|
|
1674
|
+
final Map<String, String> metadata = new HashMap<>();
|
|
1675
|
+
putStatsMetadataValue(metadata, "error_type", data.optString("type", "javascript_error"), 64);
|
|
1676
|
+
putStatsMetadataValue(metadata, "message", data.optString("message", ""), 1024);
|
|
1677
|
+
putStatsMetadataValue(metadata, "source", sanitizeStatsMetadataUrl(data.optString("source", "")), 512);
|
|
1678
|
+
putStatsMetadataValue(metadata, "line", data.optString("line", data.optString("lineno", "")), 32);
|
|
1679
|
+
putStatsMetadataValue(metadata, "column", data.optString("column", data.optString("colno", "")), 32);
|
|
1680
|
+
putStatsMetadataValue(metadata, "stack", data.optString("stack", ""), 2048);
|
|
1681
|
+
putStatsMetadataValue(metadata, "tag_name", data.optString("tag_name", ""), 64);
|
|
1682
|
+
putStatsMetadataValue(metadata, "href", sanitizeStatsMetadataUrl(data.optString("href", "")), 512);
|
|
1683
|
+
putStatsMetadataValue(metadata, "user_agent", data.optString("user_agent", ""), 256);
|
|
1684
|
+
putStatsMetadataValue(metadata, "session_id", data.optString("session_id", ""), 128);
|
|
1685
|
+
putStatsMetadataValue(metadata, "previous_session_id", data.optString("previous_session_id", ""), 128);
|
|
1686
|
+
putStatsMetadataValue(metadata, "previous_href", sanitizeStatsMetadataUrl(data.optString("previous_href", "")), 512);
|
|
1687
|
+
putStatsMetadataValue(metadata, "previous_started_at", data.optString("previous_started_at", ""), 64);
|
|
1688
|
+
putStatsMetadataValue(metadata, "previous_updated_at", data.optString("previous_updated_at", ""), 64);
|
|
1689
|
+
return metadata;
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
private static void putStatsMetadataValue(
|
|
1693
|
+
final Map<String, String> metadata,
|
|
1694
|
+
final String key,
|
|
1695
|
+
final String value,
|
|
1696
|
+
final int maxLength
|
|
1697
|
+
) {
|
|
1698
|
+
if (value == null || value.isEmpty()) {
|
|
1127
1699
|
return;
|
|
1128
1700
|
}
|
|
1129
|
-
|
|
1701
|
+
|
|
1702
|
+
metadata.put(key, truncateStatsMetadataValue(value, maxLength));
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
static String sanitizeStatsMetadataUrl(final String value) {
|
|
1706
|
+
if (value == null || value.isEmpty()) {
|
|
1707
|
+
return "";
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
try {
|
|
1711
|
+
final java.net.URI uri = new java.net.URI(value);
|
|
1712
|
+
if (uri.getScheme() != null && uri.getHost() != null) {
|
|
1713
|
+
final String path = sanitizeStatsMetadataUrlPath(uri.getPath());
|
|
1714
|
+
return new java.net.URI(
|
|
1715
|
+
uri.getScheme(),
|
|
1716
|
+
null,
|
|
1717
|
+
uri.getHost(),
|
|
1718
|
+
uri.getPort(),
|
|
1719
|
+
path.isEmpty() ? null : path,
|
|
1720
|
+
null,
|
|
1721
|
+
null
|
|
1722
|
+
).toString();
|
|
1723
|
+
}
|
|
1724
|
+
} catch (Exception ignored) {}
|
|
1725
|
+
|
|
1726
|
+
try {
|
|
1727
|
+
final Uri uri = Uri.parse(value);
|
|
1728
|
+
if (uri.getScheme() != null && uri.getHost() != null) {
|
|
1729
|
+
final String host = stripUrlUserInfo(uri.getHost());
|
|
1730
|
+
if (host.isEmpty()) {
|
|
1731
|
+
return stripUrlQueryAndFragment(value);
|
|
1732
|
+
}
|
|
1733
|
+
final StringBuilder authority = new StringBuilder(host);
|
|
1734
|
+
if (uri.getPort() != -1) {
|
|
1735
|
+
authority.append(':').append(uri.getPort());
|
|
1736
|
+
}
|
|
1737
|
+
final Uri.Builder builder = new Uri.Builder().scheme(uri.getScheme()).authority(authority.toString());
|
|
1738
|
+
final String path = sanitizeStatsMetadataUrlPath(uri.getPath());
|
|
1739
|
+
if (!path.isEmpty()) {
|
|
1740
|
+
builder.path(path);
|
|
1741
|
+
}
|
|
1742
|
+
return builder.build().toString();
|
|
1743
|
+
}
|
|
1744
|
+
} catch (Exception ignored) {}
|
|
1745
|
+
|
|
1746
|
+
return stripUrlQueryAndFragment(value);
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
private static String sanitizeStatsMetadataUrlPath(final String path) {
|
|
1750
|
+
if (path == null || path.isEmpty()) {
|
|
1751
|
+
return "";
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
final String[] segments = path.split("/", -1);
|
|
1755
|
+
for (int index = 0; index < segments.length; index++) {
|
|
1756
|
+
if (isSensitiveUrlPathSegment(segments[index])) {
|
|
1757
|
+
segments[index] = "redacted";
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
return String.join("/", segments);
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
private static String stripUrlUserInfo(final String host) {
|
|
1764
|
+
if (host == null || host.isEmpty()) {
|
|
1765
|
+
return "";
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
final int userInfoIndex = host.lastIndexOf('@');
|
|
1769
|
+
if (userInfoIndex < 0) {
|
|
1770
|
+
return host;
|
|
1771
|
+
}
|
|
1772
|
+
return host.substring(userInfoIndex + 1);
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
private static boolean isSensitiveUrlPathSegment(final String segment) {
|
|
1776
|
+
return (
|
|
1777
|
+
segment.matches("[0-9]{6,}") ||
|
|
1778
|
+
segment.matches("[0-9a-fA-F]{16,}") ||
|
|
1779
|
+
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}")
|
|
1780
|
+
);
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
private static String stripUrlQueryAndFragment(final String value) {
|
|
1784
|
+
int end = value.length();
|
|
1785
|
+
final int queryIndex = value.indexOf('?');
|
|
1786
|
+
final int fragmentIndex = value.indexOf('#');
|
|
1787
|
+
if (queryIndex >= 0) {
|
|
1788
|
+
end = Math.min(end, queryIndex);
|
|
1789
|
+
}
|
|
1790
|
+
if (fragmentIndex >= 0) {
|
|
1791
|
+
end = Math.min(end, fragmentIndex);
|
|
1792
|
+
}
|
|
1793
|
+
return value.substring(0, end);
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
static String buildWebViewStatsReporterScript() {
|
|
1797
|
+
return (
|
|
1798
|
+
"(function(){" +
|
|
1799
|
+
"if(window.__capgoWebViewErrorReporterInstalled){return;}" +
|
|
1800
|
+
"window.__capgoWebViewErrorReporterInstalled=true;" +
|
|
1801
|
+
"var maxReports=20,sentReports=0,queue=[],seen={};" +
|
|
1802
|
+
"var sessionKey='CapacitorUpdater.webViewSession';" +
|
|
1803
|
+
"var sessionId=String(Date.now())+'-'+Math.random().toString(36).slice(2);" +
|
|
1804
|
+
"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 '';}}" +
|
|
1805
|
+
"function stack(value){try{return value&&value.stack?String(value.stack):'';}catch(_){return '';}}" +
|
|
1806
|
+
"function updater(){var cap=window.Capacitor;if(!cap||!cap.Plugins){return null;}return cap.Plugins.CapacitorUpdater||null;}" +
|
|
1807
|
+
"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;}" +
|
|
1808
|
+
"var retries=0;function scheduleFlush(){if(flush()){return;}if(retries++<40){setTimeout(scheduleFlush,250);}}" +
|
|
1809
|
+
"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(_){}}" +
|
|
1810
|
+
"function readSession(){try{return JSON.parse(localStorage.getItem(sessionKey)||'null')||null;}catch(_){return null;}}" +
|
|
1811
|
+
"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(_){}}" +
|
|
1812
|
+
"window.__capgoWebViewSessionStartedAt=String(Date.now());" +
|
|
1813
|
+
"var previous=readSession();" +
|
|
1814
|
+
"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)});}" +
|
|
1815
|
+
"writeSession(true);" +
|
|
1816
|
+
"setInterval(function(){writeSession(true);},15000);" +
|
|
1817
|
+
"function markClean(){writeSession(false);}" +
|
|
1818
|
+
"window.addEventListener('pagehide',markClean,true);" +
|
|
1819
|
+
"window.addEventListener('beforeunload',markClean,true);" +
|
|
1820
|
+
"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);" +
|
|
1821
|
+
"window.addEventListener('unhandledrejection',function(event){var reason=event&&event.reason;send({type:'unhandled_rejection',message:s(reason),stack:stack(reason)});},true);" +
|
|
1822
|
+
"document.addEventListener('securitypolicyviolation',function(event){send({type:'security_policy_violation',message:s(event&&event.violatedDirective),source:s(event&&event.blockedURI)});},true);" +
|
|
1823
|
+
"document.addEventListener('deviceready',scheduleFlush,false);" +
|
|
1824
|
+
"setTimeout(scheduleFlush,0);" +
|
|
1825
|
+
"})();"
|
|
1826
|
+
);
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
private boolean shouldUseDirectUpdate() {
|
|
1830
|
+
if (!Boolean.TRUE.equals(this.autoUpdate) || AUTO_UPDATE_MODE_ONLY_DOWNLOAD.equals(this.autoUpdateMode)) {
|
|
1831
|
+
return false;
|
|
1832
|
+
}
|
|
1833
|
+
if (Boolean.TRUE.equals(this.autoSplashscreenTimedOut)) {
|
|
1834
|
+
return false;
|
|
1835
|
+
}
|
|
1836
|
+
switch (this.directUpdateMode) {
|
|
1837
|
+
case "false":
|
|
1838
|
+
return false;
|
|
1839
|
+
case "always":
|
|
1840
|
+
return true;
|
|
1841
|
+
case "atInstall":
|
|
1842
|
+
if (this.wasRecentlyInstalledOrUpdated) {
|
|
1843
|
+
// Reset the flag after first use to prevent subsequent foreground events from using direct update
|
|
1844
|
+
this.wasRecentlyInstalledOrUpdated = false;
|
|
1845
|
+
return true;
|
|
1846
|
+
}
|
|
1847
|
+
return false;
|
|
1848
|
+
case "onLaunch":
|
|
1849
|
+
if (!this.onLaunchDirectUpdateUsed) {
|
|
1850
|
+
return true;
|
|
1851
|
+
}
|
|
1852
|
+
return false;
|
|
1853
|
+
default:
|
|
1854
|
+
logger.error(
|
|
1855
|
+
"Invalid directUpdateMode: \"" +
|
|
1856
|
+
this.directUpdateMode +
|
|
1857
|
+
"\". Supported values are: \"false\", \"always\", \"atInstall\", \"onLaunch\". Defaulting to \"false\" behavior."
|
|
1858
|
+
);
|
|
1859
|
+
return false;
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
private void configureAutoUpdateModeFromConfig() {
|
|
1864
|
+
final String configuredMode = this.getConfig().getString("autoUpdate", null);
|
|
1865
|
+
if (configuredMode != null && !configuredMode.isEmpty() && !"true".equals(configuredMode) && !"false".equals(configuredMode)) {
|
|
1866
|
+
this.autoUpdateMode = normalizedAutoUpdateMode(configuredMode);
|
|
1867
|
+
if (!this.autoUpdateMode.equals(configuredMode)) {
|
|
1868
|
+
logger.error(
|
|
1869
|
+
"Invalid autoUpdate value: \"" +
|
|
1870
|
+
configuredMode +
|
|
1871
|
+
"\". Supported values are: true, false, \"off\", \"atBackground\", \"atInstall\", \"onLaunch\", \"always\", \"onlyDownload\". Defaulting to \"atBackground\"."
|
|
1872
|
+
);
|
|
1873
|
+
}
|
|
1874
|
+
} else {
|
|
1875
|
+
final boolean enabled =
|
|
1876
|
+
configuredMode != null
|
|
1877
|
+
? "true".equals(configuredMode)
|
|
1878
|
+
: Boolean.TRUE.equals(this.getConfig().getBoolean("autoUpdate", true));
|
|
1879
|
+
this.autoUpdateMode = enabled
|
|
1880
|
+
? autoUpdateModeForLegacyDirectUpdateMode(this.resolveLegacyDirectUpdateModeFromConfig())
|
|
1881
|
+
: AUTO_UPDATE_MODE_OFF;
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
this.autoUpdate = isAutoUpdateModeEnabled(this.autoUpdateMode);
|
|
1885
|
+
this.directUpdateMode = directUpdateModeForAutoUpdateMode(this.autoUpdateMode);
|
|
1886
|
+
this.implementation.directUpdate = isDirectUpdateMode(this.directUpdateMode);
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
private String resolveLegacyDirectUpdateModeFromConfig() {
|
|
1890
|
+
final String directUpdateConfig = this.getConfig().getString("directUpdate", null);
|
|
1891
|
+
if (directUpdateConfig != null) {
|
|
1892
|
+
if ("true".equals(directUpdateConfig)) {
|
|
1893
|
+
return AUTO_UPDATE_MODE_ALWAYS;
|
|
1894
|
+
}
|
|
1895
|
+
if ("false".equals(directUpdateConfig) || isDirectUpdateMode(directUpdateConfig)) {
|
|
1896
|
+
return directUpdateConfig;
|
|
1897
|
+
}
|
|
1898
|
+
logger.error(
|
|
1899
|
+
"Invalid directUpdate value: \"" +
|
|
1900
|
+
directUpdateConfig +
|
|
1901
|
+
"\". Supported values are: false, true, \"always\", \"atInstall\", \"onLaunch\". Defaulting to \"false\"."
|
|
1902
|
+
);
|
|
1903
|
+
return "false";
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
return Boolean.TRUE.equals(this.getConfig().getBoolean("directUpdate", false)) ? AUTO_UPDATE_MODE_ALWAYS : "false";
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
static String normalizedAutoUpdateMode(final String value) {
|
|
1910
|
+
if (value == null) {
|
|
1911
|
+
return AUTO_UPDATE_MODE_BACKGROUND;
|
|
1912
|
+
}
|
|
1913
|
+
switch (value) {
|
|
1914
|
+
case "false":
|
|
1915
|
+
case AUTO_UPDATE_MODE_OFF:
|
|
1916
|
+
return AUTO_UPDATE_MODE_OFF;
|
|
1917
|
+
case "true":
|
|
1918
|
+
case AUTO_UPDATE_MODE_BACKGROUND:
|
|
1919
|
+
return AUTO_UPDATE_MODE_BACKGROUND;
|
|
1920
|
+
case AUTO_UPDATE_MODE_INSTALL:
|
|
1921
|
+
case AUTO_UPDATE_MODE_LAUNCH:
|
|
1922
|
+
case AUTO_UPDATE_MODE_ALWAYS:
|
|
1923
|
+
case AUTO_UPDATE_MODE_ONLY_DOWNLOAD:
|
|
1924
|
+
return value;
|
|
1925
|
+
default:
|
|
1926
|
+
return AUTO_UPDATE_MODE_BACKGROUND;
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
static String normalizedShakeMenuGesture(final String value) {
|
|
1931
|
+
if (value == null || value.trim().isEmpty()) {
|
|
1932
|
+
return SHAKE_MENU_GESTURE_SHAKE;
|
|
1933
|
+
}
|
|
1934
|
+
final String normalized = value.trim();
|
|
1935
|
+
if (SHAKE_MENU_GESTURE_THREE_FINGER_PINCH.equals(normalized)) {
|
|
1936
|
+
return SHAKE_MENU_GESTURE_THREE_FINGER_PINCH;
|
|
1937
|
+
}
|
|
1938
|
+
return SHAKE_MENU_GESTURE_SHAKE;
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
static boolean isSupportedShakeMenuGesture(final String value) {
|
|
1942
|
+
if (value == null) {
|
|
1943
|
+
return true;
|
|
1944
|
+
}
|
|
1945
|
+
final String normalized = value.trim();
|
|
1946
|
+
if (normalized.isEmpty()) {
|
|
1947
|
+
return false;
|
|
1948
|
+
}
|
|
1949
|
+
return SHAKE_MENU_GESTURE_SHAKE.equals(normalized) || SHAKE_MENU_GESTURE_THREE_FINGER_PINCH.equals(normalized);
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
static String autoUpdateModeForLegacyDirectUpdateMode(final String directUpdateMode) {
|
|
1953
|
+
switch (directUpdateMode) {
|
|
1954
|
+
case AUTO_UPDATE_MODE_INSTALL:
|
|
1955
|
+
case AUTO_UPDATE_MODE_LAUNCH:
|
|
1956
|
+
case AUTO_UPDATE_MODE_ALWAYS:
|
|
1957
|
+
return directUpdateMode;
|
|
1958
|
+
case "false":
|
|
1959
|
+
default:
|
|
1960
|
+
return AUTO_UPDATE_MODE_BACKGROUND;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
static String directUpdateModeForAutoUpdateMode(final String autoUpdateMode) {
|
|
1965
|
+
switch (autoUpdateMode) {
|
|
1966
|
+
case AUTO_UPDATE_MODE_INSTALL:
|
|
1967
|
+
case AUTO_UPDATE_MODE_LAUNCH:
|
|
1968
|
+
case AUTO_UPDATE_MODE_ALWAYS:
|
|
1969
|
+
return autoUpdateMode;
|
|
1970
|
+
default:
|
|
1971
|
+
return "false";
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
static boolean isAutoUpdateModeEnabled(final String autoUpdateMode) {
|
|
1976
|
+
return !AUTO_UPDATE_MODE_OFF.equals(autoUpdateMode);
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
static boolean shouldAutoUpdateModeSetNextBundle(final String autoUpdateMode) {
|
|
1980
|
+
return isAutoUpdateModeEnabled(autoUpdateMode) && !AUTO_UPDATE_MODE_ONLY_DOWNLOAD.equals(autoUpdateMode);
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
static boolean isDirectUpdateMode(final String directUpdateMode) {
|
|
1984
|
+
return (
|
|
1985
|
+
AUTO_UPDATE_MODE_INSTALL.equals(directUpdateMode) ||
|
|
1986
|
+
AUTO_UPDATE_MODE_LAUNCH.equals(directUpdateMode) ||
|
|
1987
|
+
AUTO_UPDATE_MODE_ALWAYS.equals(directUpdateMode)
|
|
1988
|
+
);
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
private boolean shouldAutoSetNextBundle() {
|
|
1992
|
+
return shouldAutoUpdateModeSetNextBundle(this.autoUpdateMode);
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
private boolean isDirectUpdateCurrentlyAllowed(final boolean plannedDirectUpdate) {
|
|
1996
|
+
return plannedDirectUpdate && !Boolean.TRUE.equals(this.autoSplashscreenTimedOut);
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
static boolean shouldConsumeOnLaunchDirectUpdate(final String directUpdateMode, final boolean plannedDirectUpdate) {
|
|
2000
|
+
return plannedDirectUpdate && "onLaunch".equals(directUpdateMode);
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
static int normalizedPeriodCheckDelayMs(final int valueSeconds) {
|
|
2004
|
+
final int normalizedSeconds = normalizedPeriodCheckDelaySeconds(valueSeconds);
|
|
2005
|
+
if (normalizedSeconds <= 0) {
|
|
2006
|
+
return 0;
|
|
2007
|
+
}
|
|
2008
|
+
final long delayMs = (long) normalizedSeconds * 1000L;
|
|
2009
|
+
return delayMs > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) delayMs;
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
static int normalizedPeriodCheckDelaySeconds(final int valueSeconds) {
|
|
2013
|
+
if (valueSeconds <= 0) {
|
|
2014
|
+
return 0;
|
|
2015
|
+
}
|
|
2016
|
+
return Math.max(600, valueSeconds);
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
private void consumeOnLaunchDirectUpdateAttempt(final boolean plannedDirectUpdate) {
|
|
2020
|
+
if (!shouldConsumeOnLaunchDirectUpdate(this.directUpdateMode, plannedDirectUpdate)) {
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
this.onLaunchDirectUpdateUsed = true;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
void configureDirectUpdateModeForTesting(final String directUpdateMode, final boolean onLaunchDirectUpdateUsed) {
|
|
2028
|
+
this.directUpdateMode = directUpdateMode;
|
|
2029
|
+
this.autoUpdateMode = autoUpdateModeForLegacyDirectUpdateMode(directUpdateMode);
|
|
2030
|
+
this.autoUpdate = isAutoUpdateModeEnabled(this.autoUpdateMode);
|
|
2031
|
+
if (this.implementation != null) {
|
|
2032
|
+
this.implementation.directUpdate = isDirectUpdateMode(this.directUpdateMode);
|
|
2033
|
+
}
|
|
2034
|
+
this.onLaunchDirectUpdateUsed = onLaunchDirectUpdateUsed;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
void setAutoUpdateModeForTesting(final String autoUpdateMode) {
|
|
2038
|
+
this.autoUpdateMode = normalizedAutoUpdateMode(autoUpdateMode);
|
|
2039
|
+
this.autoUpdate = isAutoUpdateModeEnabled(this.autoUpdateMode);
|
|
2040
|
+
this.directUpdateMode = directUpdateModeForAutoUpdateMode(this.autoUpdateMode);
|
|
2041
|
+
if (this.implementation != null) {
|
|
2042
|
+
this.implementation.directUpdate = isDirectUpdateMode(this.directUpdateMode);
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
boolean shouldUseDirectUpdateForTesting() {
|
|
2047
|
+
return this.shouldUseDirectUpdate();
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
boolean hasConsumedOnLaunchDirectUpdateForTesting() {
|
|
2051
|
+
return this.onLaunchDirectUpdateUsed;
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
boolean isVersionDownloadInProgress(final String version) {
|
|
2055
|
+
return (
|
|
2056
|
+
version != null &&
|
|
2057
|
+
!version.isEmpty() &&
|
|
2058
|
+
this.implementation != null &&
|
|
2059
|
+
this.implementation.activity != null &&
|
|
2060
|
+
DownloadWorkerManager.isVersionDownloading(this.implementation.activity, version)
|
|
2061
|
+
);
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
void setLoggerForTesting(final Logger logger) {
|
|
2065
|
+
this.logger = logger;
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
void completeBackgroundTaskForTesting(final BundleInfo current, final boolean plannedDirectUpdate) {
|
|
2069
|
+
this.endBackGroundTaskWithNotif("test", current.getVersionName(), current, false, plannedDirectUpdate);
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
void scheduleDirectUpdateFinish(final BundleInfo latest) {
|
|
2073
|
+
startNewThread(() -> {
|
|
2074
|
+
try {
|
|
2075
|
+
if (this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
2076
|
+
logger.info("Skipping direct update install while preview session state is active");
|
|
2077
|
+
this.implementation.directUpdate = false;
|
|
2078
|
+
this.clearBackgroundDownloadState();
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
Activity currentActivity = this.getActivity();
|
|
2082
|
+
if (currentActivity != null) {
|
|
2083
|
+
this.implementation.activity = currentActivity;
|
|
2084
|
+
} else {
|
|
2085
|
+
logger.warn("directUpdateFinish: Activity is null, proceeding without refreshing the activity reference");
|
|
2086
|
+
}
|
|
2087
|
+
this.directUpdateFinish(latest);
|
|
2088
|
+
} catch (final Exception e) {
|
|
2089
|
+
logger.error("directUpdateFinish failed: " + e.getMessage());
|
|
2090
|
+
}
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
private void directUpdateFinish(final BundleInfo latest) {
|
|
2095
|
+
if (this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
2096
|
+
logger.info("Skipping direct update finish while preview session state is active");
|
|
2097
|
+
this.implementation.directUpdate = false;
|
|
2098
|
+
this.clearBackgroundDownloadState();
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
if ("onLaunch".equals(this.directUpdateMode)) {
|
|
2102
|
+
this.onLaunchDirectUpdateUsed = true;
|
|
2103
|
+
this.implementation.directUpdate = false;
|
|
2104
|
+
}
|
|
2105
|
+
if (this.applyDownloadedBundleForDirectUpdate(latest)) {
|
|
2106
|
+
this.implementation.setNextBundle(null);
|
|
2107
|
+
this.notifyBundleSet(latest);
|
|
2108
|
+
sendReadyToJs(latest, "update installed", true);
|
|
2109
|
+
} else {
|
|
2110
|
+
this.implementation.setNextBundle(latest.getId());
|
|
2111
|
+
final JSObject ret = new JSObject();
|
|
2112
|
+
ret.put("bundle", InternalUtils.mapToJSObject(latest.toJSONMap()));
|
|
2113
|
+
this.notifyListeners("updateAvailable", ret);
|
|
2114
|
+
sendReadyToJs(
|
|
2115
|
+
this.implementation.getCurrentBundle(),
|
|
2116
|
+
"Direct update reload failed, update will install next background",
|
|
2117
|
+
false
|
|
2118
|
+
);
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
private boolean applyDownloadedBundleForDirectUpdate(final BundleInfo latest) {
|
|
2123
|
+
final CapgoUpdater.ResetState previousState = this.implementation.captureResetState();
|
|
2124
|
+
final String previousBundleName = this.implementation.getCurrentBundle().getVersionName();
|
|
2125
|
+
|
|
2126
|
+
if (!this.implementation.stagePendingReload(latest)) {
|
|
2127
|
+
this.implementation.restoreResetState(previousState);
|
|
2128
|
+
logger.error("Direct update failed to stage downloaded bundle: " + latest.toString());
|
|
2129
|
+
return false;
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
if (this._reload()) {
|
|
2133
|
+
this.implementation.finalizePendingReload(latest, previousBundleName);
|
|
2134
|
+
return true;
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
this.implementation.restoreResetState(previousState);
|
|
2138
|
+
this.restoreLiveBundleStateAfterFailedReload();
|
|
2139
|
+
logger.error("Direct update reload failed after staging bundle: " + latest.toString());
|
|
2140
|
+
return false;
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
private void cleanupObsoleteVersions() {
|
|
2144
|
+
cleanupThread = startNewThread(() -> {
|
|
2145
|
+
synchronized (cleanupLock) {
|
|
2146
|
+
try {
|
|
2147
|
+
final String previous = this.getStoredNativeBuildVersion();
|
|
2148
|
+
if (!"".equals(previous) && !Objects.equals(this.currentBuildVersion, previous)) {
|
|
2149
|
+
logger.info("New native build version detected: " + this.currentBuildVersion);
|
|
2150
|
+
this.implementation.reset(true);
|
|
2151
|
+
final List<BundleInfo> installed = this.implementation.list(false);
|
|
2152
|
+
for (final BundleInfo bundle : installed) {
|
|
2153
|
+
// Check if thread was interrupted (cancelled)
|
|
2154
|
+
if (Thread.currentThread().isInterrupted()) {
|
|
2155
|
+
logger.warn("Cleanup was cancelled, stopping");
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2158
|
+
try {
|
|
2159
|
+
logger.info("Deleting obsolete bundle: " + bundle.getId());
|
|
2160
|
+
this.implementation.delete(bundle.getId());
|
|
2161
|
+
} catch (final Exception e) {
|
|
2162
|
+
logger.error("Failed to delete: " + bundle.getId() + " " + e.getMessage());
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
final List<BundleInfo> storedBundles = this.implementation.list(true);
|
|
2166
|
+
final Set<String> allowedIds = new HashSet<>();
|
|
2167
|
+
for (final BundleInfo info : storedBundles) {
|
|
2168
|
+
if (info != null && info.getId() != null && !info.getId().isEmpty()) {
|
|
2169
|
+
allowedIds.add(info.getId());
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
this.implementation.cleanupDownloadDirectories(allowedIds, Thread.currentThread());
|
|
2173
|
+
this.implementation.cleanupOrphanedTempFolders(Thread.currentThread());
|
|
2174
|
+
|
|
2175
|
+
// Check again before the expensive delta cache cleanup
|
|
2176
|
+
if (Thread.currentThread().isInterrupted()) {
|
|
2177
|
+
logger.warn("Cleanup was cancelled before delta cache cleanup");
|
|
2178
|
+
return;
|
|
2179
|
+
}
|
|
2180
|
+
this.implementation.cleanupDeltaCache(Thread.currentThread());
|
|
2181
|
+
}
|
|
2182
|
+
this.editor.putString("LatestNativeBuildVersion", this.currentBuildVersion);
|
|
2183
|
+
this.editor.apply();
|
|
2184
|
+
} catch (Exception e) {
|
|
2185
|
+
logger.error("Error during cleanupObsoleteVersions: " + e.getMessage());
|
|
2186
|
+
} finally {
|
|
2187
|
+
cleanupComplete = true;
|
|
2188
|
+
logger.info("Cleanup complete");
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
});
|
|
2192
|
+
|
|
2193
|
+
// Start a timeout watchdog thread to cancel cleanup if it takes too long
|
|
2194
|
+
final long timeout = this.appReadyTimeout / 2;
|
|
2195
|
+
startNewThread(() -> {
|
|
2196
|
+
try {
|
|
2197
|
+
Thread.sleep(timeout);
|
|
2198
|
+
if (cleanupThread != null && cleanupThread.isAlive() && !cleanupComplete) {
|
|
2199
|
+
logger.warn("Cleanup timeout exceeded (" + timeout + "ms), interrupting cleanup thread");
|
|
2200
|
+
cleanupThread.interrupt();
|
|
2201
|
+
}
|
|
2202
|
+
} catch (InterruptedException e) {
|
|
2203
|
+
// Watchdog thread was interrupted, that's fine
|
|
2204
|
+
}
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
String getStoredNativeBuildVersion() {
|
|
2209
|
+
String previous = this.prefs.getString("LatestNativeBuildVersion", "");
|
|
2210
|
+
if (previous == null || previous.isEmpty()) {
|
|
2211
|
+
previous = this.prefs.getString("LatestVersionNative", "");
|
|
2212
|
+
}
|
|
2213
|
+
return previous == null ? "" : previous;
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
void persistCurrentNativeBuildVersion() {
|
|
2217
|
+
this.editor.putString("LatestNativeBuildVersion", this.currentBuildVersion);
|
|
2218
|
+
this.editor.apply();
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
private void waitForCleanupIfNeeded() {
|
|
2222
|
+
if (cleanupComplete) {
|
|
2223
|
+
return; // Already done, no need to wait
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
logger.info("Waiting for cleanup to complete before starting download...");
|
|
2227
|
+
|
|
2228
|
+
// Wait for cleanup to complete - blocks until lock is released
|
|
2229
|
+
synchronized (cleanupLock) {
|
|
2230
|
+
logger.info("Cleanup finished, proceeding with download");
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
public void notifyDownload(final String id, final int percent) {
|
|
2235
|
+
try {
|
|
2236
|
+
final JSObject ret = new JSObject();
|
|
2237
|
+
ret.put("percent", percent);
|
|
2238
|
+
final BundleInfo bundleInfo = this.implementation.getBundleInfo(id);
|
|
2239
|
+
ret.put("bundle", InternalUtils.mapToJSObject(bundleInfo.toJSONMap()));
|
|
2240
|
+
this.notifyListeners("download", ret);
|
|
2241
|
+
|
|
2242
|
+
if (percent == 100) {
|
|
2243
|
+
final JSObject retDownloadComplete = new JSObject(ret, new String[] { "bundle" });
|
|
2244
|
+
this.notifyListeners("downloadComplete", retDownloadComplete);
|
|
2245
|
+
this.implementation.sendStats("download_complete", bundleInfo.getVersionName());
|
|
2246
|
+
lastNotifiedStatPercent = 100;
|
|
2247
|
+
} else {
|
|
2248
|
+
int currentStatPercent = (percent / 10) * 10; // Round down to nearest 10
|
|
2249
|
+
if (currentStatPercent > lastNotifiedStatPercent) {
|
|
2250
|
+
this.implementation.sendStats("download_" + currentStatPercent, bundleInfo.getVersionName());
|
|
2251
|
+
lastNotifiedStatPercent = currentStatPercent;
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
} catch (final Exception e) {
|
|
2255
|
+
logger.error("Could not notify listeners " + e.getMessage());
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
@PluginMethod
|
|
2260
|
+
public void setUpdateUrl(final PluginCall call) {
|
|
2261
|
+
if (!this.getConfig().getBoolean("allowModifyUrl", false)) {
|
|
2262
|
+
logger.error("setUpdateUrl not allowed set allowModifyUrl in your config to true to allow it");
|
|
2263
|
+
call.reject("setUpdateUrl not allowed");
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
2266
|
+
final String url = call.getString("url");
|
|
2267
|
+
if (url == null) {
|
|
2268
|
+
logger.error("setUpdateUrl called without url");
|
|
2269
|
+
call.reject("setUpdateUrl called without url");
|
|
2270
|
+
return;
|
|
2271
|
+
}
|
|
2272
|
+
this.updateUrl = url;
|
|
2273
|
+
if (Boolean.TRUE.equals(this.persistModifyUrl)) {
|
|
2274
|
+
this.editor.putString(UPDATE_URL_PREF_KEY, url);
|
|
2275
|
+
this.editor.apply();
|
|
2276
|
+
}
|
|
2277
|
+
call.resolve();
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
@PluginMethod
|
|
2281
|
+
public void setStatsUrl(final PluginCall call) {
|
|
2282
|
+
if (!this.getConfig().getBoolean("allowModifyUrl", false)) {
|
|
2283
|
+
logger.error("setStatsUrl not allowed set allowModifyUrl in your config to true to allow it");
|
|
2284
|
+
call.reject("setStatsUrl not allowed");
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
final String url = call.getString("url");
|
|
2288
|
+
if (url == null) {
|
|
2289
|
+
logger.error("setStatsUrl called without url");
|
|
2290
|
+
call.reject("setStatsUrl called without url");
|
|
2291
|
+
return;
|
|
2292
|
+
}
|
|
2293
|
+
this.implementation.statsUrl = url;
|
|
2294
|
+
if (Boolean.TRUE.equals(this.persistModifyUrl)) {
|
|
2295
|
+
this.editor.putString(STATS_URL_PREF_KEY, url);
|
|
2296
|
+
this.editor.apply();
|
|
2297
|
+
}
|
|
2298
|
+
call.resolve();
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
@PluginMethod
|
|
2302
|
+
public void setChannelUrl(final PluginCall call) {
|
|
2303
|
+
if (!this.getConfig().getBoolean("allowModifyUrl", false)) {
|
|
2304
|
+
logger.error("setChannelUrl not allowed set allowModifyUrl in your config to true to allow it");
|
|
2305
|
+
call.reject("setChannelUrl not allowed");
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
final String url = call.getString("url");
|
|
2309
|
+
if (url == null) {
|
|
2310
|
+
logger.error("setChannelUrl called without url");
|
|
2311
|
+
call.reject("setChannelUrl called without url");
|
|
2312
|
+
return;
|
|
2313
|
+
}
|
|
2314
|
+
this.implementation.channelUrl = url;
|
|
2315
|
+
if (Boolean.TRUE.equals(this.persistModifyUrl)) {
|
|
2316
|
+
this.editor.putString(CHANNEL_URL_PREF_KEY, url);
|
|
2317
|
+
this.editor.apply();
|
|
2318
|
+
}
|
|
2319
|
+
call.resolve();
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
@PluginMethod
|
|
2323
|
+
public void getBuiltinVersion(final PluginCall call) {
|
|
2324
|
+
try {
|
|
2325
|
+
final JSObject ret = new JSObject();
|
|
2326
|
+
ret.put("version", this.implementation.versionBuild);
|
|
2327
|
+
call.resolve(ret);
|
|
2328
|
+
} catch (final Exception e) {
|
|
2329
|
+
logger.error("Could not get version " + e.getMessage());
|
|
2330
|
+
call.reject("Could not get version", e);
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
@PluginMethod
|
|
2335
|
+
public void getDeviceId(final PluginCall call) {
|
|
2336
|
+
try {
|
|
2337
|
+
final JSObject ret = new JSObject();
|
|
2338
|
+
ret.put("deviceId", this.implementation.deviceID);
|
|
2339
|
+
call.resolve(ret);
|
|
2340
|
+
} catch (final Exception e) {
|
|
2341
|
+
logger.error("Could not get device id " + e.getMessage());
|
|
2342
|
+
call.reject("Could not get device id", e);
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
@PluginMethod
|
|
2347
|
+
public void setCustomId(final PluginCall call) {
|
|
2348
|
+
final String customId = call.getString("customId");
|
|
2349
|
+
if (customId == null) {
|
|
2350
|
+
logger.error("setCustomId called without customId");
|
|
2351
|
+
call.reject("setCustomId called without customId");
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2354
|
+
this.implementation.customId = customId;
|
|
1130
2355
|
if (Boolean.TRUE.equals(this.persistCustomId)) {
|
|
1131
2356
|
if (customId.isEmpty()) {
|
|
1132
2357
|
this.editor.remove(CUSTOM_ID_PREF_KEY);
|
|
1133
2358
|
} else {
|
|
1134
|
-
this.editor.putString(CUSTOM_ID_PREF_KEY, customId);
|
|
2359
|
+
this.editor.putString(CUSTOM_ID_PREF_KEY, customId);
|
|
2360
|
+
}
|
|
2361
|
+
this.editor.apply();
|
|
2362
|
+
}
|
|
2363
|
+
call.resolve();
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
@PluginMethod
|
|
2367
|
+
public void getPluginVersion(final PluginCall call) {
|
|
2368
|
+
try {
|
|
2369
|
+
final JSObject ret = new JSObject();
|
|
2370
|
+
ret.put("version", this.pluginVersion);
|
|
2371
|
+
call.resolve(ret);
|
|
2372
|
+
} catch (final Exception e) {
|
|
2373
|
+
logger.error("Could not get plugin version " + e.getMessage());
|
|
2374
|
+
call.reject("Could not get plugin version", e);
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
@PluginMethod
|
|
2379
|
+
public void unsetChannel(final PluginCall call) {
|
|
2380
|
+
final Boolean triggerAutoUpdate = call.getBoolean("triggerAutoUpdate", false);
|
|
2381
|
+
|
|
2382
|
+
try {
|
|
2383
|
+
logger.info("unsetChannel triggerAutoUpdate: " + triggerAutoUpdate);
|
|
2384
|
+
startNewThread(() -> {
|
|
2385
|
+
String configDefaultChannel = CapacitorUpdaterPlugin.this.getConfig().getString("defaultChannel", "");
|
|
2386
|
+
CapacitorUpdaterPlugin.this.implementation.unsetChannel(
|
|
2387
|
+
CapacitorUpdaterPlugin.this.editor,
|
|
2388
|
+
DEFAULT_CHANNEL_PREF_KEY,
|
|
2389
|
+
configDefaultChannel,
|
|
2390
|
+
(res) -> {
|
|
2391
|
+
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
2392
|
+
if (jsRes.has("error")) {
|
|
2393
|
+
String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
|
|
2394
|
+
String errorCode = jsRes.getString("error");
|
|
2395
|
+
|
|
2396
|
+
JSObject errorObj = new JSObject();
|
|
2397
|
+
errorObj.put("message", errorMessage);
|
|
2398
|
+
errorObj.put("error", errorCode);
|
|
2399
|
+
|
|
2400
|
+
call.reject(errorMessage, "UNSETCHANNEL_FAILED", null, errorObj);
|
|
2401
|
+
} else {
|
|
2402
|
+
if (CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() && Boolean.TRUE.equals(triggerAutoUpdate)) {
|
|
2403
|
+
logger.info("Calling autoupdater after channel change!");
|
|
2404
|
+
// Check if download is already in progress (with timeout protection)
|
|
2405
|
+
if (!this.isDownloadStuckOrTimedOut()) {
|
|
2406
|
+
backgroundDownload();
|
|
2407
|
+
} else {
|
|
2408
|
+
logger.info("Download already in progress, skipping duplicate download request");
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
call.resolve(jsRes);
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
);
|
|
2415
|
+
});
|
|
2416
|
+
} catch (final Exception e) {
|
|
2417
|
+
logger.error("Failed to unsetChannel: " + e.getMessage());
|
|
2418
|
+
call.reject("Failed to unsetChannel: ", e);
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
@PluginMethod
|
|
2423
|
+
public void setChannel(final PluginCall call) {
|
|
2424
|
+
final String channel = call.getString("channel");
|
|
2425
|
+
final Boolean triggerAutoUpdate = call.getBoolean("triggerAutoUpdate", false);
|
|
2426
|
+
|
|
2427
|
+
if (channel == null) {
|
|
2428
|
+
logger.error("setChannel called without channel");
|
|
2429
|
+
JSObject errorObj = new JSObject();
|
|
2430
|
+
errorObj.put("message", "setChannel called without channel");
|
|
2431
|
+
errorObj.put("error", "missing_parameter");
|
|
2432
|
+
call.reject("setChannel called without channel", "SETCHANNEL_INVALID_PARAMS", null, errorObj);
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
2435
|
+
try {
|
|
2436
|
+
logger.info("setChannel " + channel + " triggerAutoUpdate: " + triggerAutoUpdate);
|
|
2437
|
+
startNewThread(() ->
|
|
2438
|
+
CapacitorUpdaterPlugin.this.implementation.setChannel(
|
|
2439
|
+
channel,
|
|
2440
|
+
CapacitorUpdaterPlugin.this.editor,
|
|
2441
|
+
DEFAULT_CHANNEL_PREF_KEY,
|
|
2442
|
+
CapacitorUpdaterPlugin.this.allowSetDefaultChannel,
|
|
2443
|
+
CapacitorUpdaterPlugin.this.getConfig().getString("defaultChannel", ""),
|
|
2444
|
+
(res) -> {
|
|
2445
|
+
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
2446
|
+
if (jsRes.has("error")) {
|
|
2447
|
+
String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
|
|
2448
|
+
String errorCode = jsRes.getString("error");
|
|
2449
|
+
|
|
2450
|
+
// Fire channelPrivate event if channel doesn't allow self-assignment
|
|
2451
|
+
if (
|
|
2452
|
+
errorCode.contains("cannot_update_via_private_channel") ||
|
|
2453
|
+
errorCode.contains("channel_self_set_not_allowed")
|
|
2454
|
+
) {
|
|
2455
|
+
JSObject eventData = new JSObject();
|
|
2456
|
+
eventData.put("channel", channel);
|
|
2457
|
+
eventData.put("message", errorMessage);
|
|
2458
|
+
notifyListeners("channelPrivate", eventData);
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
JSObject errorObj = new JSObject();
|
|
2462
|
+
errorObj.put("message", errorMessage);
|
|
2463
|
+
errorObj.put("error", errorCode);
|
|
2464
|
+
|
|
2465
|
+
call.reject(errorMessage, "SETCHANNEL_FAILED", null, errorObj);
|
|
2466
|
+
} else {
|
|
2467
|
+
if (CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() && Boolean.TRUE.equals(triggerAutoUpdate)) {
|
|
2468
|
+
logger.info("Calling autoupdater after channel change!");
|
|
2469
|
+
// Check if download is already in progress (with timeout protection)
|
|
2470
|
+
if (!this.isDownloadStuckOrTimedOut()) {
|
|
2471
|
+
backgroundDownload();
|
|
2472
|
+
} else {
|
|
2473
|
+
logger.info("Download already in progress, skipping duplicate download request");
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
call.resolve(jsRes);
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
)
|
|
2480
|
+
);
|
|
2481
|
+
} catch (final Exception e) {
|
|
2482
|
+
logger.error("Failed to setChannel: " + channel + " " + e.getMessage());
|
|
2483
|
+
call.reject("Failed to setChannel: " + channel, e);
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
@PluginMethod
|
|
2488
|
+
public void getChannel(final PluginCall call) {
|
|
2489
|
+
try {
|
|
2490
|
+
logger.info("getChannel");
|
|
2491
|
+
startNewThread(() ->
|
|
2492
|
+
CapacitorUpdaterPlugin.this.implementation.getChannel(
|
|
2493
|
+
(res) -> {
|
|
2494
|
+
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
2495
|
+
if (jsRes.has("error")) {
|
|
2496
|
+
String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
|
|
2497
|
+
String errorCode = jsRes.getString("error");
|
|
2498
|
+
|
|
2499
|
+
JSObject errorObj = new JSObject();
|
|
2500
|
+
errorObj.put("message", errorMessage);
|
|
2501
|
+
errorObj.put("error", errorCode);
|
|
2502
|
+
|
|
2503
|
+
call.reject(errorMessage, "GETCHANNEL_FAILED", null, errorObj);
|
|
2504
|
+
} else {
|
|
2505
|
+
call.resolve(jsRes);
|
|
2506
|
+
}
|
|
2507
|
+
},
|
|
2508
|
+
CapacitorUpdaterPlugin.this.editor,
|
|
2509
|
+
DEFAULT_CHANNEL_PREF_KEY
|
|
2510
|
+
)
|
|
2511
|
+
);
|
|
2512
|
+
} catch (final Exception e) {
|
|
2513
|
+
logger.error("Failed to getChannel " + e.getMessage());
|
|
2514
|
+
call.reject("Failed to getChannel", e);
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
@PluginMethod
|
|
2519
|
+
public void listChannels(final PluginCall call) {
|
|
2520
|
+
try {
|
|
2521
|
+
logger.info("listChannels");
|
|
2522
|
+
startNewThread(() ->
|
|
2523
|
+
CapacitorUpdaterPlugin.this.implementation.listChannels((res) -> {
|
|
2524
|
+
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
2525
|
+
Object channels = res.get("channels");
|
|
2526
|
+
if (channels instanceof List<?> channelsList) {
|
|
2527
|
+
JSArray channelsArray = new JSArray();
|
|
2528
|
+
for (Object channel : channelsList) {
|
|
2529
|
+
if (channel instanceof Map<?, ?> channelMap) {
|
|
2530
|
+
JSObject channelObject = new JSObject();
|
|
2531
|
+
for (Map.Entry<?, ?> entry : channelMap.entrySet()) {
|
|
2532
|
+
Object key = entry.getKey();
|
|
2533
|
+
if (key != null) {
|
|
2534
|
+
channelObject.put(key.toString(), entry.getValue());
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
channelsArray.put(channelObject);
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
jsRes.put("channels", channelsArray);
|
|
2541
|
+
}
|
|
2542
|
+
if (jsRes.has("error")) {
|
|
2543
|
+
String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
|
|
2544
|
+
String errorCode = jsRes.getString("error");
|
|
2545
|
+
|
|
2546
|
+
JSObject errorObj = new JSObject();
|
|
2547
|
+
errorObj.put("message", errorMessage);
|
|
2548
|
+
errorObj.put("error", errorCode);
|
|
2549
|
+
|
|
2550
|
+
call.reject(errorMessage, "LISTCHANNELS_FAILED", null, errorObj);
|
|
2551
|
+
} else {
|
|
2552
|
+
call.resolve(jsRes);
|
|
2553
|
+
}
|
|
2554
|
+
})
|
|
2555
|
+
);
|
|
2556
|
+
} catch (final Exception e) {
|
|
2557
|
+
logger.error("Failed to listChannels: " + e.getMessage());
|
|
2558
|
+
call.reject("Failed to listChannels", e);
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
private BundleInfo downloadBundle(
|
|
2563
|
+
final String url,
|
|
2564
|
+
final String version,
|
|
2565
|
+
final String sessionKey,
|
|
2566
|
+
final String checksum,
|
|
2567
|
+
final JSONArray manifest
|
|
2568
|
+
) throws IOException {
|
|
2569
|
+
if (manifest != null) {
|
|
2570
|
+
return this.implementation.downloadManifest(url, version, sessionKey, checksum, manifest);
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
return this.implementation.download(url, version, sessionKey, checksum);
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
@PluginMethod
|
|
2577
|
+
public void download(final PluginCall call) {
|
|
2578
|
+
final String url = call.getString("url");
|
|
2579
|
+
final String version = call.getString("version");
|
|
2580
|
+
final String sessionKey = call.getString("sessionKey", "");
|
|
2581
|
+
final String checksum = call.getString("checksum", "");
|
|
2582
|
+
final JSONArray manifest = call.getData().optJSONArray("manifest");
|
|
2583
|
+
if (url == null) {
|
|
2584
|
+
logger.error("Download called without url");
|
|
2585
|
+
call.reject("Download called without url");
|
|
2586
|
+
return;
|
|
2587
|
+
}
|
|
2588
|
+
if (version == null) {
|
|
2589
|
+
logger.error("Download called without version");
|
|
2590
|
+
call.reject("Download called without version");
|
|
2591
|
+
return;
|
|
2592
|
+
}
|
|
2593
|
+
try {
|
|
2594
|
+
logger.info("Downloading " + url);
|
|
2595
|
+
startNewThread(() -> {
|
|
2596
|
+
try {
|
|
2597
|
+
final BundleInfo downloaded = this.downloadBundle(url, version, sessionKey, checksum, manifest);
|
|
2598
|
+
if (downloaded.isErrorStatus()) {
|
|
2599
|
+
throw new RuntimeException("Download failed: " + downloaded.getStatus());
|
|
2600
|
+
} else {
|
|
2601
|
+
call.resolve(InternalUtils.mapToJSObject(downloaded.toJSONMap()));
|
|
2602
|
+
}
|
|
2603
|
+
} catch (final Exception e) {
|
|
2604
|
+
logger.error("Failed to download from: " + url + " " + e.getMessage());
|
|
2605
|
+
call.reject("Failed to download from: " + url, e);
|
|
2606
|
+
final JSObject ret = new JSObject();
|
|
2607
|
+
ret.put("version", version);
|
|
2608
|
+
CapacitorUpdaterPlugin.this.notifyListeners("downloadFailed", ret);
|
|
2609
|
+
final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
|
|
2610
|
+
CapacitorUpdaterPlugin.this.implementation.sendStats("download_fail", current.getVersionName());
|
|
2611
|
+
}
|
|
2612
|
+
});
|
|
2613
|
+
} catch (final Exception e) {
|
|
2614
|
+
logger.error("Failed to download from: " + url + " " + e.getMessage());
|
|
2615
|
+
call.reject("Failed to download from: " + url, e);
|
|
2616
|
+
final JSObject ret = new JSObject();
|
|
2617
|
+
ret.put("version", version);
|
|
2618
|
+
CapacitorUpdaterPlugin.this.notifyListeners("downloadFailed", ret);
|
|
2619
|
+
final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
|
|
2620
|
+
CapacitorUpdaterPlugin.this.implementation.sendStats("download_fail", current.getVersionName());
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
private void syncKeepUrlPathFlag(final boolean enabled) {
|
|
2625
|
+
if (this.bridge == null || this.bridge.getWebView() == null) {
|
|
2626
|
+
return;
|
|
2627
|
+
}
|
|
2628
|
+
final String script = enabled
|
|
2629
|
+
? "(function(){try{localStorage.setItem('" +
|
|
2630
|
+
KEEP_URL_FLAG_KEY +
|
|
2631
|
+
"','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);})();"
|
|
2632
|
+
: "(function(){try{localStorage.removeItem('" +
|
|
2633
|
+
KEEP_URL_FLAG_KEY +
|
|
2634
|
+
"');}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);})();";
|
|
2635
|
+
this.bridge.getWebView().post(() -> this.bridge.getWebView().evaluateJavascript(script, null));
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
private void applyCurrentBundleToBridge() {
|
|
2639
|
+
final String path = this.implementation.getCurrentBundlePath();
|
|
2640
|
+
final boolean usingBuiltin = this.implementation.isUsingBuiltin();
|
|
2641
|
+
if (this.keepUrlPathAfterReload) {
|
|
2642
|
+
this.syncKeepUrlPathFlag(true);
|
|
2643
|
+
}
|
|
2644
|
+
logger.info("Reloading: " + path);
|
|
2645
|
+
|
|
2646
|
+
AtomicReference<URL> url = new AtomicReference<>();
|
|
2647
|
+
if (this.keepUrlPathAfterReload) {
|
|
2648
|
+
try {
|
|
2649
|
+
if (Looper.myLooper() != Looper.getMainLooper()) {
|
|
2650
|
+
Semaphore mainThreadSemaphore = new Semaphore(0);
|
|
2651
|
+
this.bridge.executeOnMainThread(() -> {
|
|
2652
|
+
try {
|
|
2653
|
+
if (this.bridge != null && this.bridge.getWebView() != null) {
|
|
2654
|
+
String currentUrl = this.bridge.getWebView().getUrl();
|
|
2655
|
+
if (currentUrl != null) {
|
|
2656
|
+
url.set(new URL(currentUrl));
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
} catch (Exception e) {
|
|
2660
|
+
logger.error("Error executing on main thread " + e.getMessage());
|
|
2661
|
+
}
|
|
2662
|
+
mainThreadSemaphore.release();
|
|
2663
|
+
});
|
|
2664
|
+
|
|
2665
|
+
// Add timeout to prevent indefinite blocking
|
|
2666
|
+
if (!mainThreadSemaphore.tryAcquire(10, TimeUnit.SECONDS)) {
|
|
2667
|
+
logger.error("Timeout waiting for main thread operation");
|
|
2668
|
+
}
|
|
2669
|
+
} else {
|
|
2670
|
+
try {
|
|
2671
|
+
if (this.bridge != null && this.bridge.getWebView() != null) {
|
|
2672
|
+
String currentUrl = this.bridge.getWebView().getUrl();
|
|
2673
|
+
if (currentUrl != null) {
|
|
2674
|
+
url.set(new URL(currentUrl));
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
} catch (Exception e) {
|
|
2678
|
+
logger.error("Error executing on main thread " + e.getMessage());
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
} catch (InterruptedException e) {
|
|
2682
|
+
logger.error("Error waiting for main thread or getting the current URL from webview " + e.getMessage());
|
|
2683
|
+
Thread.currentThread().interrupt(); // Restore interrupted status
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
if (url.get() != null) {
|
|
2688
|
+
if (usingBuiltin) {
|
|
2689
|
+
this.bridge.getLocalServer().hostAssets(path);
|
|
2690
|
+
} else {
|
|
2691
|
+
this.bridge.getLocalServer().hostFiles(path);
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
try {
|
|
2695
|
+
URL finalUrl = null;
|
|
2696
|
+
finalUrl = new URL(this.bridge.getAppUrl());
|
|
2697
|
+
finalUrl = new URL(finalUrl.getProtocol(), finalUrl.getHost(), finalUrl.getPort(), url.get().getPath());
|
|
2698
|
+
URL finalUrl1 = finalUrl;
|
|
2699
|
+
this.bridge.getWebView().post(() -> {
|
|
2700
|
+
this.bridge.getWebView().loadUrl(finalUrl1.toString());
|
|
2701
|
+
if (!this.keepUrlPathAfterReload) {
|
|
2702
|
+
this.bridge.getWebView().clearHistory();
|
|
2703
|
+
}
|
|
2704
|
+
});
|
|
2705
|
+
} catch (MalformedURLException e) {
|
|
2706
|
+
logger.error("Cannot get finalUrl from capacitor bridge " + e.getMessage());
|
|
2707
|
+
|
|
2708
|
+
if (usingBuiltin) {
|
|
2709
|
+
this.bridge.setServerAssetPath(path);
|
|
2710
|
+
} else {
|
|
2711
|
+
this.bridge.setServerBasePath(path);
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
} else {
|
|
2715
|
+
if (usingBuiltin) {
|
|
2716
|
+
this.bridge.setServerAssetPath(path);
|
|
2717
|
+
} else {
|
|
2718
|
+
this.bridge.setServerBasePath(path);
|
|
2719
|
+
}
|
|
2720
|
+
if (this.bridge != null && this.bridge.getWebView() != null) {
|
|
2721
|
+
this.bridge.getWebView().post(() -> {
|
|
2722
|
+
if (this.bridge.getWebView() != null) {
|
|
2723
|
+
this.bridge.getWebView().loadUrl(this.bridge.getAppUrl());
|
|
2724
|
+
if (!this.keepUrlPathAfterReload) {
|
|
2725
|
+
this.bridge.getWebView().clearHistory();
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
});
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
protected void restoreLiveBundleStateAfterFailedReload() {
|
|
2734
|
+
try {
|
|
2735
|
+
this.applyCurrentBundleToBridge();
|
|
2736
|
+
} catch (final Exception e) {
|
|
2737
|
+
logger.warn("Failed to restore live bundle after rejected reload: " + e.getMessage());
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
protected boolean _reload() {
|
|
2742
|
+
final int phase = this.semaphoreUp();
|
|
2743
|
+
this.applyCurrentBundleToBridge();
|
|
2744
|
+
|
|
2745
|
+
final long waitTimeMs = this.resolveAppReadyCheckTimeoutMs();
|
|
2746
|
+
this.checkAppReady(waitTimeMs);
|
|
2747
|
+
this.notifyListeners("appReloaded", new JSObject());
|
|
2748
|
+
|
|
2749
|
+
// Wait for the reload to complete (until notifyAppReady is called)
|
|
2750
|
+
return this.semaphoreWait(phase, waitTimeMs);
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
protected boolean reloadWithoutWaitingForAppReady() {
|
|
2754
|
+
this.applyCurrentBundleToBridge();
|
|
2755
|
+
|
|
2756
|
+
final long waitTimeMs = this.resolveAppReadyCheckTimeoutMs();
|
|
2757
|
+
this.checkAppReady(waitTimeMs);
|
|
2758
|
+
this.notifyListeners("appReloaded", new JSObject());
|
|
2759
|
+
return true;
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
@PluginMethod
|
|
2763
|
+
public void reload(final PluginCall call) {
|
|
2764
|
+
startNewThread(() -> {
|
|
2765
|
+
try {
|
|
2766
|
+
final BundleInfo current = this.implementation.getCurrentBundle();
|
|
2767
|
+
final BundleInfo next = this.implementation.getNextBundle();
|
|
2768
|
+
|
|
2769
|
+
if (!this.isPreviewSessionStateActive() && next != null && !next.isErrorStatus() && !next.getId().equals(current.getId())) {
|
|
2770
|
+
final CapgoUpdater.ResetState previousState = this.implementation.captureResetState();
|
|
2771
|
+
final String previousBundleName = this.implementation.getCurrentBundle().getVersionName();
|
|
2772
|
+
logger.info("Applying pending bundle before reload: " + next.getVersionName());
|
|
2773
|
+
final boolean didApplyPendingBundle;
|
|
2774
|
+
if (next.isBuiltin()) {
|
|
2775
|
+
this.implementation.prepareResetStateForTransition();
|
|
2776
|
+
didApplyPendingBundle = true;
|
|
2777
|
+
} else {
|
|
2778
|
+
didApplyPendingBundle = this.implementation.stagePendingReload(next);
|
|
2779
|
+
}
|
|
2780
|
+
if (didApplyPendingBundle && this._reload()) {
|
|
2781
|
+
if (next.isBuiltin()) {
|
|
2782
|
+
this.implementation.finalizeResetTransition(previousBundleName, false);
|
|
2783
|
+
} else {
|
|
2784
|
+
this.implementation.finalizePendingReload(next, previousBundleName);
|
|
2785
|
+
}
|
|
2786
|
+
this.notifyBundleSet(next);
|
|
2787
|
+
this.implementation.setNextBundle(null);
|
|
2788
|
+
this.showPreviewSessionNoticeIfNeeded();
|
|
2789
|
+
call.resolve();
|
|
2790
|
+
return;
|
|
2791
|
+
}
|
|
2792
|
+
this.implementation.restoreResetState(previousState);
|
|
2793
|
+
this.restoreLiveBundleStateAfterFailedReload();
|
|
2794
|
+
logger.error("Reload failed after applying pending bundle: " + next.getVersionName());
|
|
2795
|
+
call.reject("Reload failed after applying pending bundle: " + next.getVersionName());
|
|
2796
|
+
return;
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
if (this._reload()) {
|
|
2800
|
+
this.showPreviewSessionNoticeIfNeeded();
|
|
2801
|
+
call.resolve();
|
|
2802
|
+
} else {
|
|
2803
|
+
logger.error("Reload failed");
|
|
2804
|
+
call.reject("Reload failed");
|
|
2805
|
+
}
|
|
2806
|
+
} catch (final Exception e) {
|
|
2807
|
+
logger.error("Could not reload " + e.getMessage());
|
|
2808
|
+
call.reject("Could not reload", e);
|
|
2809
|
+
}
|
|
2810
|
+
});
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
@PluginMethod
|
|
2814
|
+
public void next(final PluginCall call) {
|
|
2815
|
+
final String id = call.getString("id");
|
|
2816
|
+
if (id == null) {
|
|
2817
|
+
logger.error("Next called without id");
|
|
2818
|
+
call.reject("Next called without id");
|
|
2819
|
+
return;
|
|
2820
|
+
}
|
|
2821
|
+
try {
|
|
2822
|
+
logger.info("Setting next active id " + id);
|
|
2823
|
+
if (!this.implementation.setNextBundle(id)) {
|
|
2824
|
+
logger.error("Set next id failed. Bundle " + id + " does not exist.");
|
|
2825
|
+
call.reject("Set next id failed. Bundle " + id + " does not exist.");
|
|
2826
|
+
} else {
|
|
2827
|
+
call.resolve(InternalUtils.mapToJSObject(this.implementation.getBundleInfo(id).toJSONMap()));
|
|
2828
|
+
}
|
|
2829
|
+
} catch (final Exception e) {
|
|
2830
|
+
logger.error("Could not set next id " + id + " " + e.getMessage());
|
|
2831
|
+
call.reject("Could not set next id: " + id, e);
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
@PluginMethod
|
|
2836
|
+
public void set(final PluginCall call) {
|
|
2837
|
+
final String id = call.getString("id");
|
|
2838
|
+
if (id == null) {
|
|
2839
|
+
logger.error("Set called without id");
|
|
2840
|
+
call.reject("Set called without id");
|
|
2841
|
+
return;
|
|
2842
|
+
}
|
|
2843
|
+
startNewThread(() -> {
|
|
2844
|
+
try {
|
|
2845
|
+
logger.info("Setting active bundle " + id);
|
|
2846
|
+
if (!this.implementation.set(id)) {
|
|
2847
|
+
logger.info("No such bundle " + id);
|
|
2848
|
+
call.reject("Update failed, id " + id + " does not exist.");
|
|
2849
|
+
} else if (Boolean.TRUE.equals(this.previewSessionEnabled)) {
|
|
2850
|
+
logger.info("Preview session set active bundle " + id + " without waiting for preview app readiness");
|
|
2851
|
+
final BundleInfo bundle = this.implementation.getBundleInfo(id);
|
|
2852
|
+
this.recordPreviewBundle(bundle);
|
|
2853
|
+
this.reloadWithoutWaitingForAppReady();
|
|
2854
|
+
this.notifyBundleSet(bundle);
|
|
2855
|
+
this.showPreviewSessionNoticeIfNeeded();
|
|
2856
|
+
call.resolve();
|
|
2857
|
+
} else if (!this._reload()) {
|
|
2858
|
+
logger.error("Reload failed after setting bundle " + id);
|
|
2859
|
+
call.reject("Reload failed after setting bundle " + id);
|
|
2860
|
+
} else {
|
|
2861
|
+
logger.info("Bundle successfully set to " + id);
|
|
2862
|
+
this.notifyBundleSet(this.implementation.getBundleInfo(id));
|
|
2863
|
+
this.showPreviewSessionNoticeIfNeeded();
|
|
2864
|
+
call.resolve();
|
|
2865
|
+
}
|
|
2866
|
+
} catch (final Exception e) {
|
|
2867
|
+
logger.error("Could not set id " + id + " " + e.getMessage());
|
|
2868
|
+
call.reject("Could not set id " + id, e);
|
|
2869
|
+
}
|
|
2870
|
+
});
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
private boolean preparePreviewFallbackIfNeeded() {
|
|
2874
|
+
if (Boolean.TRUE.equals(this.previewSessionEnabled)) {
|
|
2875
|
+
return true;
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
final BundleInfo current = this.implementation.getCurrentBundle();
|
|
2879
|
+
if (!this.implementation.setPreviewFallbackBundle(current.getId())) {
|
|
2880
|
+
logger.error("Could not save current bundle as preview fallback");
|
|
2881
|
+
return false;
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
final BundleInfo previousNext = this.implementation.getNextBundle();
|
|
2885
|
+
if (previousNext == null || previousNext.isDeleted() || previousNext.isErrorStatus()) {
|
|
2886
|
+
this.editor.remove(PREVIEW_PREVIOUS_NEXT_BUNDLE_PREF_KEY);
|
|
2887
|
+
} else {
|
|
2888
|
+
this.editor.putString(PREVIEW_PREVIOUS_NEXT_BUNDLE_PREF_KEY, previousNext.getId());
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2891
|
+
this.editor.putString(PREVIEW_PREVIOUS_APP_ID_PREF_KEY, this.implementation.appId);
|
|
2892
|
+
if (this.prefs.contains(DEFAULT_CHANNEL_PREF_KEY)) {
|
|
2893
|
+
this.editor.putString(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_PREF_KEY, this.prefs.getString(DEFAULT_CHANNEL_PREF_KEY, ""));
|
|
2894
|
+
this.editor.putBoolean(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_WAS_SET_PREF_KEY, true);
|
|
2895
|
+
} else {
|
|
2896
|
+
this.editor.remove(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_PREF_KEY);
|
|
2897
|
+
this.editor.putBoolean(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_WAS_SET_PREF_KEY, false);
|
|
2898
|
+
}
|
|
2899
|
+
this.editor.putBoolean(PREVIEW_PREVIOUS_SHAKE_MENU_PREF_KEY, Boolean.TRUE.equals(this.shakeMenuEnabled));
|
|
2900
|
+
this.editor.putBoolean(PREVIEW_PREVIOUS_SHAKE_CHANNEL_SELECTOR_PREF_KEY, Boolean.TRUE.equals(this.shakeChannelSelectorEnabled));
|
|
2901
|
+
logger.info("Preview session started with fallback bundle: " + current);
|
|
2902
|
+
return true;
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
private void activatePreviewSessionState() {
|
|
2906
|
+
this.clearIncomingPreviewTransition();
|
|
2907
|
+
this.hidePreviewTransitionLoader("preview-session-started");
|
|
2908
|
+
this.previewSessionEnabled = true;
|
|
2909
|
+
this.previewSessionAlertPending = true;
|
|
2910
|
+
this.implementation.previewSession = true;
|
|
2911
|
+
this.shakeMenuEnabled = true;
|
|
2912
|
+
this.editor.putBoolean(PREVIEW_SESSION_PREF_KEY, true);
|
|
2913
|
+
this.editor.putBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, true);
|
|
2914
|
+
this.editor.apply();
|
|
2915
|
+
this.syncShakeMenuLifecycle();
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
@PluginMethod
|
|
2919
|
+
public void startPreviewSession(final PluginCall call) {
|
|
2920
|
+
if (!Boolean.TRUE.equals(this.allowPreview)) {
|
|
2921
|
+
this.hidePreviewTransitionLoader("preview-session-not-allowed");
|
|
2922
|
+
logger.error("startPreviewSession not allowed set allowPreview in your config to true to enable it");
|
|
2923
|
+
call.reject("startPreviewSession not allowed");
|
|
2924
|
+
return;
|
|
2925
|
+
}
|
|
2926
|
+
final String previewAppId = this.normalizePreviewAppId(call.getString("appId"));
|
|
2927
|
+
final String rawPayloadUrl = call.getString("payloadUrl");
|
|
2928
|
+
final String previewPayloadUrl = this.normalizePreviewPayloadUrl(rawPayloadUrl);
|
|
2929
|
+
if (this.hasPreviewPayloadUrl(rawPayloadUrl) && previewPayloadUrl == null) {
|
|
2930
|
+
this.hidePreviewTransitionLoader("preview-session-invalid-payload");
|
|
2931
|
+
logger.error("startPreviewSession called with invalid payloadUrl");
|
|
2932
|
+
call.reject("Invalid preview payloadUrl");
|
|
2933
|
+
return;
|
|
2934
|
+
}
|
|
2935
|
+
startNewThread(() -> {
|
|
2936
|
+
try {
|
|
2937
|
+
if (!this.preparePreviewFallbackIfNeeded()) {
|
|
2938
|
+
this.hidePreviewTransitionLoader("preview-session-fallback-failed");
|
|
2939
|
+
call.reject("Could not save current bundle as preview fallback");
|
|
2940
|
+
return;
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
if (previewAppId != null) {
|
|
2944
|
+
this.setActiveAppId(previewAppId);
|
|
2945
|
+
this.editor.putString(PREVIEW_APP_ID_PREF_KEY, previewAppId);
|
|
2946
|
+
logger.info("Preview session using appId: " + previewAppId);
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
if (previewPayloadUrl != null) {
|
|
2950
|
+
this.editor.putString(PREVIEW_PAYLOAD_URL_PREF_KEY, previewPayloadUrl);
|
|
2951
|
+
logger.info("Preview session using payload URL");
|
|
2952
|
+
} else {
|
|
2953
|
+
this.editor.remove(PREVIEW_PAYLOAD_URL_PREF_KEY);
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
final String previewName = this.normalizedPreviewMetadataValue(call.getString("name"));
|
|
2957
|
+
if (previewName == null) {
|
|
2958
|
+
this.editor.remove(PREVIEW_NAME_PREF_KEY);
|
|
2959
|
+
} else {
|
|
2960
|
+
this.editor.putString(PREVIEW_NAME_PREF_KEY, previewName);
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
final String previewSource = this.normalizedPreviewMetadataValue(call.getString("source"));
|
|
2964
|
+
if (previewSource == null) {
|
|
2965
|
+
this.editor.remove(PREVIEW_SOURCE_PREF_KEY);
|
|
2966
|
+
} else {
|
|
2967
|
+
this.editor.putString(PREVIEW_SOURCE_PREF_KEY, previewSource);
|
|
2968
|
+
}
|
|
2969
|
+
|
|
2970
|
+
this.activatePreviewSessionState();
|
|
2971
|
+
call.resolve();
|
|
2972
|
+
} catch (final Exception e) {
|
|
2973
|
+
this.hidePreviewTransitionLoader("preview-session-failed");
|
|
2974
|
+
logger.error("Could not start preview session " + e.getMessage());
|
|
2975
|
+
call.reject("Could not start preview session", e);
|
|
2976
|
+
}
|
|
2977
|
+
});
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
@PluginMethod
|
|
2981
|
+
public void listPreviews(final PluginCall call) {
|
|
2982
|
+
if (!Boolean.TRUE.equals(this.allowPreview)) {
|
|
2983
|
+
call.reject("listPreviews not allowed");
|
|
2984
|
+
return;
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
final JSArray previews = this.listPreviewInfos(true);
|
|
2988
|
+
final JSObject ret = new JSObject();
|
|
2989
|
+
ret.put("previews", previews);
|
|
2990
|
+
ret.put("currentBundle", InternalUtils.mapToJSObject(this.implementation.getCurrentBundle().toJSONMap()));
|
|
2991
|
+
|
|
2992
|
+
for (int i = 0; i < previews.length(); i++) {
|
|
2993
|
+
final JSONObject preview = previews.optJSONObject(i);
|
|
2994
|
+
if (preview != null && preview.optBoolean("isActive", false)) {
|
|
2995
|
+
ret.put("current", preview);
|
|
2996
|
+
break;
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
final BundleInfo liveBundle = this.implementation.getPreviewFallbackBundle();
|
|
3001
|
+
if (liveBundle != null) {
|
|
3002
|
+
ret.put("liveBundle", InternalUtils.mapToJSObject(liveBundle.toJSONMap()));
|
|
3003
|
+
}
|
|
3004
|
+
|
|
3005
|
+
call.resolve(ret);
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
@PluginMethod
|
|
3009
|
+
public void setPreview(final PluginCall call) {
|
|
3010
|
+
if (!Boolean.TRUE.equals(this.allowPreview)) {
|
|
3011
|
+
call.reject("setPreview not allowed");
|
|
3012
|
+
return;
|
|
3013
|
+
}
|
|
3014
|
+
final String id = call.getString("id");
|
|
3015
|
+
if (id == null || id.isEmpty()) {
|
|
3016
|
+
call.reject("setPreview called without id");
|
|
3017
|
+
return;
|
|
3018
|
+
}
|
|
3019
|
+
final JSObject preview = this.storedPreviewInfo(id);
|
|
3020
|
+
if (preview == null) {
|
|
3021
|
+
call.reject("Preview " + id + " is not available locally");
|
|
3022
|
+
return;
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
this.showPreviewTransitionLoader("set-preview");
|
|
3026
|
+
startNewThread(() -> {
|
|
3027
|
+
if (!this.preparePreviewFallbackIfNeeded()) {
|
|
3028
|
+
this.hidePreviewTransitionLoader("set-preview-fallback-failed");
|
|
3029
|
+
call.reject("Could not save current bundle as preview fallback");
|
|
3030
|
+
return;
|
|
1135
3031
|
}
|
|
1136
|
-
|
|
3032
|
+
|
|
3033
|
+
if (!this.implementation.set(id)) {
|
|
3034
|
+
this.hidePreviewTransitionLoader("set-preview-failed");
|
|
3035
|
+
call.reject("Preview " + id + " cannot be applied");
|
|
3036
|
+
return;
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
final BundleInfo bundle = this.implementation.getBundleInfo(id);
|
|
3040
|
+
this.updateCurrentPreviewSessionMetadataFrom(preview);
|
|
3041
|
+
this.activatePreviewSessionState();
|
|
3042
|
+
this.recordPreviewBundle(bundle);
|
|
3043
|
+
if (!this.reloadWithoutWaitingForAppReady()) {
|
|
3044
|
+
this.hidePreviewTransitionLoader("set-preview-reload-failed");
|
|
3045
|
+
call.reject("Reload failed after setting preview " + id);
|
|
3046
|
+
return;
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
this.notifyBundleSet(bundle);
|
|
3050
|
+
this.showPreviewSessionNoticeIfNeeded();
|
|
3051
|
+
call.resolve();
|
|
3052
|
+
});
|
|
3053
|
+
}
|
|
3054
|
+
|
|
3055
|
+
public JSArray previewMenuPreviews() {
|
|
3056
|
+
return this.listPreviewInfos(true);
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
public boolean setPreviewFromShakeMenu(final String id) {
|
|
3060
|
+
final JSObject preview = this.storedPreviewInfo(id);
|
|
3061
|
+
if (!Boolean.TRUE.equals(this.allowPreview) || preview == null) {
|
|
3062
|
+
return false;
|
|
1137
3063
|
}
|
|
1138
|
-
|
|
3064
|
+
|
|
3065
|
+
this.showPreviewTransitionLoader("set-preview-menu");
|
|
3066
|
+
if (!this.preparePreviewFallbackIfNeeded()) {
|
|
3067
|
+
this.hidePreviewTransitionLoader("set-preview-menu-fallback-failed");
|
|
3068
|
+
return false;
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
if (!this.implementation.set(id)) {
|
|
3072
|
+
this.hidePreviewTransitionLoader("set-preview-menu-failed");
|
|
3073
|
+
return false;
|
|
3074
|
+
}
|
|
3075
|
+
|
|
3076
|
+
final BundleInfo bundle = this.implementation.getBundleInfo(id);
|
|
3077
|
+
this.updateCurrentPreviewSessionMetadataFrom(preview);
|
|
3078
|
+
this.activatePreviewSessionState();
|
|
3079
|
+
this.recordPreviewBundle(bundle);
|
|
3080
|
+
if (!this.reloadWithoutWaitingForAppReady()) {
|
|
3081
|
+
this.hidePreviewTransitionLoader("set-preview-menu-reload-failed");
|
|
3082
|
+
return false;
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
this.notifyBundleSet(bundle);
|
|
3086
|
+
this.showPreviewSessionNoticeIfNeeded();
|
|
3087
|
+
return true;
|
|
1139
3088
|
}
|
|
1140
3089
|
|
|
1141
3090
|
@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);
|
|
3091
|
+
public void resetPreview(final PluginCall call) {
|
|
3092
|
+
if (!Boolean.TRUE.equals(this.previewSessionEnabled)) {
|
|
3093
|
+
call.resolve();
|
|
3094
|
+
return;
|
|
1150
3095
|
}
|
|
3096
|
+
startNewThread(() -> {
|
|
3097
|
+
if (this.leavePreviewSessionFromShakeMenu()) {
|
|
3098
|
+
call.resolve();
|
|
3099
|
+
} else {
|
|
3100
|
+
call.reject("Could not leave preview session");
|
|
3101
|
+
}
|
|
3102
|
+
});
|
|
1151
3103
|
}
|
|
1152
3104
|
|
|
1153
3105
|
@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");
|
|
3106
|
+
public void deletePreview(final PluginCall call) {
|
|
3107
|
+
if (!Boolean.TRUE.equals(this.allowPreview)) {
|
|
3108
|
+
call.reject("deletePreview not allowed");
|
|
3109
|
+
return;
|
|
3110
|
+
}
|
|
3111
|
+
final String id = call.getString("id");
|
|
3112
|
+
if (id == null || id.isEmpty()) {
|
|
3113
|
+
call.reject("deletePreview called without id");
|
|
3114
|
+
return;
|
|
3115
|
+
}
|
|
3116
|
+
if (Boolean.TRUE.equals(this.previewSessionEnabled) && this.implementation.getCurrentBundle().getId().equals(id)) {
|
|
3117
|
+
call.reject("Cannot delete the active preview");
|
|
3118
|
+
return;
|
|
3119
|
+
}
|
|
1170
3120
|
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
3121
|
+
final boolean removed;
|
|
3122
|
+
synchronized (this.previewSessionsLock) {
|
|
3123
|
+
final JSONObject sessions = this.previewSessionsJson();
|
|
3124
|
+
removed = sessions.has(id);
|
|
3125
|
+
sessions.remove(id);
|
|
3126
|
+
this.savePreviewSessionsJson(sessions);
|
|
3127
|
+
}
|
|
1174
3128
|
|
|
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);
|
|
3129
|
+
boolean deleted = false;
|
|
3130
|
+
final BundleInfo fallback = this.implementation.getPreviewFallbackBundle();
|
|
3131
|
+
final BundleInfo next = this.implementation.getNextBundle();
|
|
3132
|
+
if (
|
|
3133
|
+
removed &&
|
|
3134
|
+
!BundleInfo.ID_BUILTIN.equals(id) &&
|
|
3135
|
+
(fallback == null || !id.equals(fallback.getId())) &&
|
|
3136
|
+
(next == null || !id.equals(next.getId()))
|
|
3137
|
+
) {
|
|
3138
|
+
try {
|
|
3139
|
+
deleted = this.implementation.delete(id, false);
|
|
3140
|
+
} catch (final Exception err) {
|
|
3141
|
+
logger.warn("Could not delete preview bundle " + id + ": " + err.getMessage());
|
|
3142
|
+
}
|
|
1194
3143
|
}
|
|
3144
|
+
|
|
3145
|
+
final JSObject ret = new JSObject();
|
|
3146
|
+
ret.put("removed", removed);
|
|
3147
|
+
ret.put("deleted", deleted);
|
|
3148
|
+
call.resolve(ret);
|
|
1195
3149
|
}
|
|
1196
3150
|
|
|
1197
3151
|
@PluginMethod
|
|
1198
|
-
public void
|
|
1199
|
-
|
|
1200
|
-
|
|
3152
|
+
public void checkPreviewUpdate(final PluginCall call) {
|
|
3153
|
+
this.handlePreviewUpdate(call, false);
|
|
3154
|
+
}
|
|
1201
3155
|
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
3156
|
+
@PluginMethod
|
|
3157
|
+
public void updatePreview(final PluginCall call) {
|
|
3158
|
+
this.handlePreviewUpdate(call, true);
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
private void handlePreviewUpdate(final PluginCall call, final boolean shouldDownload) {
|
|
3162
|
+
if (!Boolean.TRUE.equals(this.allowPreview)) {
|
|
3163
|
+
call.reject("Preview updates not allowed");
|
|
3164
|
+
return;
|
|
3165
|
+
}
|
|
3166
|
+
final String id = call.getString("id");
|
|
3167
|
+
if (id == null || id.isEmpty()) {
|
|
3168
|
+
call.reject("Preview update called without id");
|
|
3169
|
+
return;
|
|
3170
|
+
}
|
|
3171
|
+
final JSObject preview = this.storedPreviewInfo(id);
|
|
3172
|
+
final String payloadUrl = preview == null ? null : this.normalizePreviewPayloadUrl(preview.optString("payloadUrl", null));
|
|
3173
|
+
if (payloadUrl == null) {
|
|
3174
|
+
call.reject("Preview " + id + " has no payloadUrl to update from");
|
|
1208
3175
|
return;
|
|
1209
3176
|
}
|
|
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
3177
|
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
eventData.put("message", errorMessage);
|
|
1232
|
-
notifyListeners("channelPrivate", eventData);
|
|
1233
|
-
}
|
|
3178
|
+
startNewThread(() -> {
|
|
3179
|
+
try {
|
|
3180
|
+
final JSONObject payload = this.fetchPreviewPayload(payloadUrl);
|
|
3181
|
+
final String version = payload.optString("version", "").trim();
|
|
3182
|
+
if (version.isEmpty()) {
|
|
3183
|
+
throw new IOException("Preview payload is missing a version");
|
|
3184
|
+
}
|
|
1234
3185
|
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
3186
|
+
final BundleInfo currentPreviewBundle = this.implementation.getBundleInfo(id);
|
|
3187
|
+
final boolean upToDate = version.equals(currentPreviewBundle.getVersionName());
|
|
3188
|
+
if (upToDate || !shouldDownload) {
|
|
3189
|
+
final JSObject ret = new JSObject();
|
|
3190
|
+
ret.put("preview", preview);
|
|
3191
|
+
ret.put("latestVersion", version);
|
|
3192
|
+
ret.put("upToDate", upToDate);
|
|
3193
|
+
ret.put("updated", false);
|
|
3194
|
+
ret.put("bundle", InternalUtils.mapToJSObject(currentPreviewBundle.toJSONMap()));
|
|
3195
|
+
call.resolve(ret);
|
|
3196
|
+
return;
|
|
3197
|
+
}
|
|
1238
3198
|
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
3199
|
+
final BundleInfo next = this.downloadPreviewPayloadBundle(payload);
|
|
3200
|
+
if (next.isErrorStatus()) {
|
|
3201
|
+
throw new IOException("Download failed: " + next.getStatus());
|
|
3202
|
+
}
|
|
3203
|
+
|
|
3204
|
+
final boolean wasActive =
|
|
3205
|
+
Boolean.TRUE.equals(this.previewSessionEnabled) && this.implementation.getCurrentBundle().getId().equals(id);
|
|
3206
|
+
if (wasActive && !this.implementation.set(next.getId())) {
|
|
3207
|
+
throw new IOException("Downloaded preview bundle cannot be applied");
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
final JSObject savedPreview = this.recordPreviewBundle(next, id);
|
|
3211
|
+
if (wasActive) {
|
|
3212
|
+
if (!this.reloadWithoutWaitingForAppReady()) {
|
|
3213
|
+
throw new IOException("Reload failed after updating preview");
|
|
1252
3214
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
3215
|
+
this.notifyBundleSet(next);
|
|
3216
|
+
this.showPreviewSessionNoticeIfNeeded();
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
final JSObject ret = new JSObject();
|
|
3220
|
+
ret.put("preview", savedPreview);
|
|
3221
|
+
ret.put("latestVersion", version);
|
|
3222
|
+
ret.put("upToDate", false);
|
|
3223
|
+
ret.put("updated", true);
|
|
3224
|
+
ret.put("bundle", InternalUtils.mapToJSObject(next.toJSONMap()));
|
|
3225
|
+
call.resolve(ret);
|
|
3226
|
+
} catch (final Exception err) {
|
|
3227
|
+
logger.error("Could not update preview: " + err.getMessage());
|
|
3228
|
+
call.reject("Could not update preview: " + err.getMessage());
|
|
3229
|
+
}
|
|
3230
|
+
});
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
public boolean leavePreviewSessionFromShakeMenu() {
|
|
3234
|
+
this.showPreviewTransitionLoader("leave-preview-session");
|
|
3235
|
+
final boolean didReset = this.resetToPreviewFallbackBundle();
|
|
3236
|
+
if (!didReset) {
|
|
3237
|
+
this.hidePreviewTransitionLoader("leave-preview-session-failed");
|
|
3238
|
+
return false;
|
|
1258
3239
|
}
|
|
3240
|
+
|
|
3241
|
+
this.endPreviewSession(true);
|
|
3242
|
+
return true;
|
|
1259
3243
|
}
|
|
1260
3244
|
|
|
1261
|
-
|
|
1262
|
-
|
|
3245
|
+
private boolean leavePreviewSessionForIncomingPreviewLink() {
|
|
3246
|
+
this.showPreviewTransitionLoader("incoming-preview-deeplink");
|
|
3247
|
+
final BundleInfo previewFallbackBundle = this.resolvePreviewFallbackBundle("incoming preview deeplink");
|
|
3248
|
+
boolean didReload = false;
|
|
3249
|
+
|
|
1263
3250
|
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");
|
|
3251
|
+
if (previewFallbackBundle == null) {
|
|
3252
|
+
return false;
|
|
3253
|
+
}
|
|
1271
3254
|
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
3255
|
+
final CapgoUpdater.ResetState previousState = this.implementation.captureResetState();
|
|
3256
|
+
if (!this.implementation.stagePreviewFallbackReload(previewFallbackBundle)) {
|
|
3257
|
+
logger.error("Could not stage preview fallback bundle");
|
|
3258
|
+
return false;
|
|
3259
|
+
}
|
|
1275
3260
|
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
3261
|
+
if (!this.reloadWithoutWaitingForAppReady()) {
|
|
3262
|
+
this.implementation.restoreResetState(previousState);
|
|
3263
|
+
this.restoreLiveBundleStateAfterFailedReload();
|
|
3264
|
+
return false;
|
|
3265
|
+
}
|
|
3266
|
+
didReload = true;
|
|
3267
|
+
|
|
3268
|
+
this.endPreviewSession(true);
|
|
3269
|
+
return true;
|
|
3270
|
+
} finally {
|
|
3271
|
+
this.clearIncomingPreviewTransition();
|
|
3272
|
+
if (!didReload) {
|
|
3273
|
+
this.hidePreviewTransitionLoader("incoming-preview-deeplink-failed");
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
private void leavePreviewSessionForLaunchIntentIfNeeded() {
|
|
3279
|
+
final Intent intent = getActivity() == null ? null : getActivity().getIntent();
|
|
3280
|
+
if (
|
|
3281
|
+
intent == null ||
|
|
3282
|
+
!Intent.ACTION_VIEW.equals(intent.getAction()) ||
|
|
3283
|
+
intent.getData() == null ||
|
|
3284
|
+
!Boolean.TRUE.equals(this.previewSessionEnabled) ||
|
|
3285
|
+
!isPreviewDeepLink(intent.getData()) ||
|
|
3286
|
+
Boolean.TRUE.equals(this.isLeavingPreviewForIncomingLink)
|
|
3287
|
+
) {
|
|
3288
|
+
return;
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3291
|
+
this.isLeavingPreviewForIncomingLink = true;
|
|
3292
|
+
this.showPreviewTransitionLoader("preview-launch-deeplink");
|
|
3293
|
+
logger.info("Preview deeplink launch detected while preview session is active; restoring fallback before initial load");
|
|
3294
|
+
if (!this.leavePreviewSessionWithoutReload()) {
|
|
3295
|
+
logger.error("Could not leave preview session before initial preview deeplink routing");
|
|
3296
|
+
this.isLeavingPreviewForIncomingLink = false;
|
|
3297
|
+
this.hidePreviewTransitionLoader("preview-launch-deeplink-failed");
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
private boolean leavePreviewSessionWithoutReload() {
|
|
3302
|
+
return this.leavePreviewSessionWithoutReload(false);
|
|
3303
|
+
}
|
|
3304
|
+
|
|
3305
|
+
private boolean leavePreviewSessionWithoutReload(final boolean keepPreviewGuard) {
|
|
3306
|
+
final BundleInfo previewFallbackBundle = this.resolvePreviewFallbackBundle("preview deeplink launch");
|
|
3307
|
+
if (previewFallbackBundle == null) {
|
|
3308
|
+
return false;
|
|
3309
|
+
}
|
|
3310
|
+
if (!this.implementation.stagePreviewFallbackReload(previewFallbackBundle)) {
|
|
3311
|
+
logger.error("Could not stage preview fallback bundle");
|
|
3312
|
+
return false;
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3315
|
+
this.endPreviewSession(keepPreviewGuard);
|
|
3316
|
+
return true;
|
|
3317
|
+
}
|
|
3318
|
+
|
|
3319
|
+
public boolean reloadPreviewSessionFromShakeMenu() {
|
|
3320
|
+
this.showPreviewTransitionLoader("reload-preview-session");
|
|
3321
|
+
final boolean didReload;
|
|
3322
|
+
final String payloadUrl = this.storedPreviewPayloadUrl();
|
|
3323
|
+
if (payloadUrl != null) {
|
|
3324
|
+
didReload = this.refreshPreviewSessionFromPayloadUrl(payloadUrl);
|
|
3325
|
+
} else {
|
|
3326
|
+
didReload = this.reloadWithoutWaitingForAppReady();
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
if (!didReload) {
|
|
3330
|
+
this.hidePreviewTransitionLoader("reload-preview-session-failed");
|
|
3331
|
+
}
|
|
3332
|
+
return didReload;
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
public boolean hasActivePreviewSession() {
|
|
3336
|
+
return Boolean.TRUE.equals(this.previewSessionEnabled);
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3339
|
+
private boolean resetToPreviewFallbackBundle() {
|
|
3340
|
+
final BundleInfo fallback = this.resolvePreviewFallbackBundle("leave preview");
|
|
3341
|
+
if (fallback == null) {
|
|
3342
|
+
return false;
|
|
3343
|
+
}
|
|
3344
|
+
|
|
3345
|
+
final CapgoUpdater.ResetState previousState = this.implementation.captureResetState();
|
|
3346
|
+
final String previousBundleName = this.implementation.getCurrentBundle().getVersionName();
|
|
3347
|
+
logger.info("Resetting to preview fallback bundle: " + fallback.getVersionName());
|
|
3348
|
+
if (this.implementation.stagePreviewFallbackReload(fallback) && this.reloadWithoutWaitingForAppReady()) {
|
|
3349
|
+
this.implementation.finalizeResetTransition(previousBundleName, false);
|
|
3350
|
+
this.notifyBundleSet(fallback);
|
|
3351
|
+
return true;
|
|
1285
3352
|
}
|
|
3353
|
+
this.implementation.restoreResetState(previousState);
|
|
3354
|
+
this.restoreLiveBundleStateAfterFailedReload();
|
|
3355
|
+
return false;
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3358
|
+
private BundleInfo resolvePreviewFallbackBundle(final String reason) {
|
|
3359
|
+
final BundleInfo fallback = this.implementation.getPreviewFallbackBundle();
|
|
3360
|
+
if (fallback != null && !fallback.isErrorStatus() && this.implementation.canSet(fallback)) {
|
|
3361
|
+
return fallback;
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
if (fallback == null) {
|
|
3365
|
+
logger.warn("No preview fallback bundle available for " + reason + ". Falling back to builtin bundle.");
|
|
3366
|
+
} else if (fallback.isErrorStatus()) {
|
|
3367
|
+
logger.warn("Preview fallback bundle is in error state for " + reason + ". Falling back to builtin bundle.");
|
|
3368
|
+
} else {
|
|
3369
|
+
logger.warn("Preview fallback bundle is not installable for " + reason + ". Falling back to builtin bundle.");
|
|
3370
|
+
}
|
|
3371
|
+
|
|
3372
|
+
final BundleInfo builtin = this.implementation.getBundleInfo(BundleInfo.ID_BUILTIN);
|
|
3373
|
+
if (builtin != null && !builtin.isErrorStatus() && this.implementation.canSet(builtin)) {
|
|
3374
|
+
return builtin;
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3377
|
+
logger.error("Builtin bundle is not available to leave preview for " + reason);
|
|
3378
|
+
return null;
|
|
3379
|
+
}
|
|
3380
|
+
|
|
3381
|
+
private void endPreviewSession() {
|
|
3382
|
+
this.endPreviewSession(false);
|
|
3383
|
+
}
|
|
3384
|
+
|
|
3385
|
+
private void endPreviewSession(final boolean keepPreviewGuard) {
|
|
3386
|
+
final boolean previousShakeMenuEnabled = this.prefs.getBoolean(
|
|
3387
|
+
PREVIEW_PREVIOUS_SHAKE_MENU_PREF_KEY,
|
|
3388
|
+
this.getConfig().getBoolean("shakeMenu", false)
|
|
3389
|
+
);
|
|
3390
|
+
final boolean previousShakeChannelSelectorEnabled = this.prefs.getBoolean(
|
|
3391
|
+
PREVIEW_PREVIOUS_SHAKE_CHANNEL_SELECTOR_PREF_KEY,
|
|
3392
|
+
this.getConfig().getBoolean("allowShakeChannelSelector", false)
|
|
3393
|
+
);
|
|
3394
|
+
this.restorePreviewPreviousNextBundle();
|
|
3395
|
+
this.restorePreviewPreviousAppId();
|
|
3396
|
+
this.restorePreviewPreviousDefaultChannel();
|
|
3397
|
+
|
|
3398
|
+
this.previewSessionEnabled = false;
|
|
3399
|
+
this.previewSessionAlertPending = false;
|
|
3400
|
+
if (keepPreviewGuard) {
|
|
3401
|
+
this.implementation.previewSession = true;
|
|
3402
|
+
} else {
|
|
3403
|
+
this.clearIncomingPreviewTransition();
|
|
3404
|
+
}
|
|
3405
|
+
this.shakeMenuEnabled = previousShakeMenuEnabled;
|
|
3406
|
+
this.shakeChannelSelectorEnabled = previousShakeChannelSelectorEnabled;
|
|
3407
|
+
this.syncShakeMenuLifecycle();
|
|
3408
|
+
this.implementation.setPreviewFallbackBundle(null);
|
|
3409
|
+
this.clearPreviewSessionPreferences();
|
|
3410
|
+
logger.info("Preview session ended");
|
|
1286
3411
|
}
|
|
1287
3412
|
|
|
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");
|
|
3413
|
+
private void clearPreviewSessionBecauseDisabled() {
|
|
3414
|
+
logger.info("Preview session disabled by config; restoring preview fallback");
|
|
3415
|
+
final BundleInfo bundleToRestore = this.resolvePreviewFallbackBundle("preview disabled");
|
|
3416
|
+
if (bundleToRestore != null) {
|
|
3417
|
+
this.implementation.stagePreviewFallbackReload(bundleToRestore);
|
|
3418
|
+
} else {
|
|
3419
|
+
logger.warn("Could not restore preview fallback while disabling preview");
|
|
3420
|
+
}
|
|
1298
3421
|
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
3422
|
+
this.restorePreviewPreviousNextBundle();
|
|
3423
|
+
this.restorePreviewPreviousAppId();
|
|
3424
|
+
this.restorePreviewPreviousDefaultChannel();
|
|
3425
|
+
this.previewSessionEnabled = false;
|
|
3426
|
+
this.previewSessionAlertPending = false;
|
|
3427
|
+
this.isLeavingPreviewForIncomingLink = false;
|
|
3428
|
+
this.implementation.previewSession = false;
|
|
3429
|
+
this.hidePreviewTransitionLoader("preview-session-disabled");
|
|
3430
|
+
this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
|
|
3431
|
+
this.shakeChannelSelectorEnabled = this.getConfig().getBoolean("allowShakeChannelSelector", false);
|
|
3432
|
+
this.shakeMenuGesture = normalizedShakeMenuGesture(this.getConfig().getString("shakeMenuGesture", SHAKE_MENU_GESTURE_SHAKE));
|
|
3433
|
+
this.syncShakeMenuLifecycle();
|
|
3434
|
+
this.clearPreviewSessionPreferences();
|
|
3435
|
+
}
|
|
1302
3436
|
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
3437
|
+
private void clearPreviewSessionPreferences() {
|
|
3438
|
+
if (this.implementation != null) {
|
|
3439
|
+
this.implementation.setPreviewFallbackBundle(null);
|
|
3440
|
+
}
|
|
3441
|
+
this.editor.remove(PREVIEW_SESSION_PREF_KEY);
|
|
3442
|
+
this.editor.remove(PREVIEW_PREVIOUS_SHAKE_MENU_PREF_KEY);
|
|
3443
|
+
this.editor.remove(PREVIEW_PREVIOUS_SHAKE_CHANNEL_SELECTOR_PREF_KEY);
|
|
3444
|
+
this.editor.remove(PREVIEW_PREVIOUS_NEXT_BUNDLE_PREF_KEY);
|
|
3445
|
+
this.editor.remove(PREVIEW_PREVIOUS_APP_ID_PREF_KEY);
|
|
3446
|
+
this.editor.remove(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_PREF_KEY);
|
|
3447
|
+
this.editor.remove(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_WAS_SET_PREF_KEY);
|
|
3448
|
+
this.editor.remove(PREVIEW_APP_ID_PREF_KEY);
|
|
3449
|
+
this.editor.remove(PREVIEW_PAYLOAD_URL_PREF_KEY);
|
|
3450
|
+
this.editor.remove(PREVIEW_NAME_PREF_KEY);
|
|
3451
|
+
this.editor.remove(PREVIEW_SOURCE_PREF_KEY);
|
|
3452
|
+
this.editor.remove(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY);
|
|
3453
|
+
this.editor.apply();
|
|
3454
|
+
}
|
|
3455
|
+
|
|
3456
|
+
private void setActiveAppId(final String appId) {
|
|
3457
|
+
this.implementation.appId = appId;
|
|
3458
|
+
if (this.implementation.versionOs != null) {
|
|
3459
|
+
DownloadService.updateUserAgent(this.implementation.appId, this.pluginVersion, this.implementation.versionOs);
|
|
1312
3460
|
}
|
|
1313
3461
|
}
|
|
1314
3462
|
|
|
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");
|
|
3463
|
+
private void restorePreviewPreviousAppId() {
|
|
3464
|
+
final String previousAppId = this.prefs.getString(PREVIEW_PREVIOUS_APP_ID_PREF_KEY, "");
|
|
3465
|
+
if (previousAppId == null || previousAppId.isEmpty()) {
|
|
1325
3466
|
return;
|
|
1326
3467
|
}
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
3468
|
+
this.setActiveAppId(previousAppId);
|
|
3469
|
+
logger.info("Restored appId after preview: " + previousAppId);
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
private void restorePreviewPreviousDefaultChannel() {
|
|
3473
|
+
final String configDefaultChannel = this.getConfig().getString("defaultChannel", "");
|
|
3474
|
+
if (this.prefs.getBoolean(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_WAS_SET_PREF_KEY, false)) {
|
|
3475
|
+
final String previousDefaultChannel = this.prefs.getString(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_PREF_KEY, "");
|
|
3476
|
+
this.editor.putString(DEFAULT_CHANNEL_PREF_KEY, previousDefaultChannel);
|
|
3477
|
+
this.implementation.defaultChannel = previousDefaultChannel;
|
|
3478
|
+
this.editor.apply();
|
|
3479
|
+
logger.info("Restored defaultChannel after preview");
|
|
1330
3480
|
return;
|
|
1331
3481
|
}
|
|
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
|
-
}
|
|
3482
|
+
|
|
3483
|
+
this.editor.remove(DEFAULT_CHANNEL_PREF_KEY);
|
|
3484
|
+
this.implementation.defaultChannel = configDefaultChannel;
|
|
3485
|
+
this.editor.apply();
|
|
3486
|
+
logger.info("Restored defaultChannel after preview to config value");
|
|
1374
3487
|
}
|
|
1375
3488
|
|
|
1376
|
-
private
|
|
1377
|
-
if (
|
|
1378
|
-
return;
|
|
3489
|
+
private String normalizePreviewAppId(final String rawAppId) {
|
|
3490
|
+
if (rawAppId == null) {
|
|
3491
|
+
return null;
|
|
1379
3492
|
}
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
3493
|
+
|
|
3494
|
+
final String appId = rawAppId.trim();
|
|
3495
|
+
if (appId.isEmpty()) {
|
|
3496
|
+
return null;
|
|
3497
|
+
}
|
|
3498
|
+
|
|
3499
|
+
final String lowercasedAppId = appId.toLowerCase(java.util.Locale.ROOT);
|
|
3500
|
+
if ("undefined".equals(lowercasedAppId) || "null".equals(lowercasedAppId)) {
|
|
3501
|
+
return null;
|
|
3502
|
+
}
|
|
3503
|
+
|
|
3504
|
+
return appId;
|
|
1388
3505
|
}
|
|
1389
3506
|
|
|
1390
|
-
private
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
if (this.keepUrlPathAfterReload) {
|
|
1394
|
-
this.syncKeepUrlPathFlag(true);
|
|
3507
|
+
private boolean hasPreviewPayloadUrl(final String rawPayloadUrl) {
|
|
3508
|
+
if (rawPayloadUrl == null) {
|
|
3509
|
+
return false;
|
|
1395
3510
|
}
|
|
1396
|
-
logger.info("Reloading: " + path);
|
|
1397
3511
|
|
|
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
|
-
});
|
|
3512
|
+
final String payloadUrl = rawPayloadUrl.trim();
|
|
3513
|
+
if (payloadUrl.isEmpty()) {
|
|
3514
|
+
return false;
|
|
3515
|
+
}
|
|
1416
3516
|
|
|
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
|
-
}
|
|
3517
|
+
final String lowercasedPayloadUrl = payloadUrl.toLowerCase(java.util.Locale.ROOT);
|
|
3518
|
+
return !"undefined".equals(lowercasedPayloadUrl) && !"null".equals(lowercasedPayloadUrl);
|
|
3519
|
+
}
|
|
3520
|
+
|
|
3521
|
+
private String normalizePreviewPayloadUrl(final String rawPayloadUrl) {
|
|
3522
|
+
if (!this.hasPreviewPayloadUrl(rawPayloadUrl)) {
|
|
3523
|
+
return null;
|
|
1437
3524
|
}
|
|
1438
3525
|
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
3526
|
+
final String payloadUrl = rawPayloadUrl.trim();
|
|
3527
|
+
try {
|
|
3528
|
+
final URL parsedUrl = new URL(payloadUrl);
|
|
3529
|
+
final String protocol = parsedUrl.getProtocol();
|
|
3530
|
+
if (!"https".equals(protocol) && !"http".equals(protocol)) {
|
|
3531
|
+
return null;
|
|
1444
3532
|
}
|
|
3533
|
+
return parsedUrl.toString();
|
|
3534
|
+
} catch (final MalformedURLException ignored) {
|
|
3535
|
+
return null;
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
1445
3538
|
|
|
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());
|
|
3539
|
+
private String storedPreviewPayloadUrl() {
|
|
3540
|
+
return this.normalizePreviewPayloadUrl(this.prefs.getString(PREVIEW_PAYLOAD_URL_PREF_KEY, null));
|
|
3541
|
+
}
|
|
1459
3542
|
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
3543
|
+
private String previewPathFromUri(final Uri uri) {
|
|
3544
|
+
if ("capgo".equals(uri.getScheme())) {
|
|
3545
|
+
final String host = uri.getHost();
|
|
3546
|
+
final String path = uri.getPath();
|
|
3547
|
+
return ("/" + (host == null ? "" : host) + (path == null ? "" : path)).replaceAll("/+", "/");
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
return uri.getPath();
|
|
3551
|
+
}
|
|
3552
|
+
|
|
3553
|
+
private boolean isPreviewDeepLink(final Uri uri) {
|
|
3554
|
+
final String path = this.previewPathFromUri(uri);
|
|
3555
|
+
return "/preview/channel".equals(path) || "/preview/bundle".equals(path);
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3558
|
+
private String readResponseBody(final InputStream stream) throws IOException {
|
|
3559
|
+
if (stream == null) {
|
|
3560
|
+
return "";
|
|
3561
|
+
}
|
|
3562
|
+
|
|
3563
|
+
try (InputStream input = stream; ByteArrayOutputStream output = new ByteArrayOutputStream()) {
|
|
3564
|
+
final byte[] buffer = new byte[8192];
|
|
3565
|
+
int read;
|
|
3566
|
+
while ((read = input.read(buffer)) != -1) {
|
|
3567
|
+
output.write(buffer, 0, read);
|
|
1481
3568
|
}
|
|
3569
|
+
return output.toString(StandardCharsets.UTF_8.name());
|
|
1482
3570
|
}
|
|
1483
3571
|
}
|
|
1484
3572
|
|
|
1485
|
-
|
|
3573
|
+
private JSONObject fetchPreviewPayload(final String payloadUrl) throws IOException, JSONException {
|
|
3574
|
+
final HttpURLConnection connection = (HttpURLConnection) new URL(payloadUrl).openConnection();
|
|
3575
|
+
connection.setRequestMethod("GET");
|
|
3576
|
+
connection.setRequestProperty("Accept", "application/json");
|
|
3577
|
+
connection.setConnectTimeout(30000);
|
|
3578
|
+
connection.setReadTimeout(60000);
|
|
3579
|
+
|
|
1486
3580
|
try {
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
3581
|
+
final int statusCode = connection.getResponseCode();
|
|
3582
|
+
final String body = this.readResponseBody(
|
|
3583
|
+
statusCode >= 200 && statusCode < 300 ? connection.getInputStream() : connection.getErrorStream()
|
|
3584
|
+
);
|
|
3585
|
+
final JSONObject payload = new JSONObject(body);
|
|
3586
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
3587
|
+
throw new IOException(
|
|
3588
|
+
payload.optString("message", payload.optString("error", "Preview payload request failed with HTTP " + statusCode))
|
|
3589
|
+
);
|
|
3590
|
+
}
|
|
3591
|
+
return payload;
|
|
3592
|
+
} finally {
|
|
3593
|
+
connection.disconnect();
|
|
1490
3594
|
}
|
|
1491
3595
|
}
|
|
1492
3596
|
|
|
1493
|
-
|
|
1494
|
-
final
|
|
1495
|
-
|
|
3597
|
+
private BundleInfo downloadPreviewPayloadBundle(final JSONObject payload) throws IOException, JSONException {
|
|
3598
|
+
final String version = payload.optString("version", "").trim();
|
|
3599
|
+
if (version.isEmpty()) {
|
|
3600
|
+
throw new IOException("Preview payload is missing a version");
|
|
3601
|
+
}
|
|
1496
3602
|
|
|
1497
|
-
final
|
|
1498
|
-
|
|
1499
|
-
|
|
3603
|
+
final JSONArray manifest = payload.optJSONArray("manifest");
|
|
3604
|
+
final String url = payload.optString("url", "");
|
|
3605
|
+
if ((url == null || url.isEmpty()) && (manifest == null || manifest.length() == 0)) {
|
|
3606
|
+
throw new IOException("Preview payload is missing download information");
|
|
3607
|
+
}
|
|
1500
3608
|
|
|
1501
|
-
|
|
1502
|
-
|
|
3609
|
+
return this.downloadBundle(
|
|
3610
|
+
url == null || url.isEmpty() ? "https://404.capgo.app/no.zip" : url,
|
|
3611
|
+
version,
|
|
3612
|
+
payload.optString("sessionKey", ""),
|
|
3613
|
+
payload.optString("checksum", ""),
|
|
3614
|
+
manifest
|
|
3615
|
+
);
|
|
1503
3616
|
}
|
|
1504
3617
|
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
3618
|
+
private boolean refreshPreviewSessionFromPayloadUrl(final String payloadUrl) {
|
|
3619
|
+
try {
|
|
3620
|
+
final JSONObject payload = this.fetchPreviewPayload(payloadUrl);
|
|
3621
|
+
final String version = payload.optString("version", "").trim();
|
|
3622
|
+
if (version.isEmpty()) {
|
|
3623
|
+
throw new IOException("Preview payload is missing a version");
|
|
3624
|
+
}
|
|
1511
3625
|
|
|
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
|
-
}
|
|
3626
|
+
final BundleInfo current = this.implementation.getCurrentBundle();
|
|
3627
|
+
if (version.equals(current.getVersionName())) {
|
|
3628
|
+
logger.info("Preview payload unchanged, reloading current bundle");
|
|
3629
|
+
return this.reloadWithoutWaitingForAppReady();
|
|
3630
|
+
}
|
|
1540
3631
|
|
|
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);
|
|
3632
|
+
final BundleInfo next = this.downloadPreviewPayloadBundle(payload);
|
|
3633
|
+
if (next.isErrorStatus()) {
|
|
3634
|
+
throw new IOException("Download failed: " + next.getStatus());
|
|
1550
3635
|
}
|
|
1551
|
-
|
|
3636
|
+
if (!this.implementation.set(next.getId())) {
|
|
3637
|
+
throw new IOException("Downloaded preview bundle cannot be applied");
|
|
3638
|
+
}
|
|
3639
|
+
|
|
3640
|
+
this.recordPreviewBundle(next, current.getId());
|
|
3641
|
+
this.notifyBundleSet(next);
|
|
3642
|
+
return this.reloadWithoutWaitingForAppReady();
|
|
3643
|
+
} catch (final Exception err) {
|
|
3644
|
+
logger.error("Could not refresh preview session: " + err.getMessage());
|
|
3645
|
+
return false;
|
|
3646
|
+
}
|
|
1552
3647
|
}
|
|
1553
3648
|
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
3649
|
+
private void clearPreviewSessionForNativeBuildChange() {
|
|
3650
|
+
if (
|
|
3651
|
+
!Boolean.TRUE.equals(this.previewSessionEnabled) &&
|
|
3652
|
+
this.implementation.getPreviewFallbackBundle() == null &&
|
|
3653
|
+
!this.hasSavedPreviewSessions()
|
|
3654
|
+
) {
|
|
1560
3655
|
return;
|
|
1561
3656
|
}
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
3657
|
+
logger.info("Native build changed; clearing preview session state");
|
|
3658
|
+
this.previewSessionEnabled = false;
|
|
3659
|
+
this.previewSessionAlertPending = false;
|
|
3660
|
+
this.isLeavingPreviewForIncomingLink = false;
|
|
3661
|
+
this.implementation.previewSession = false;
|
|
3662
|
+
this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
|
|
3663
|
+
this.shakeChannelSelectorEnabled = this.getConfig().getBoolean("allowShakeChannelSelector", false);
|
|
3664
|
+
this.shakeMenuGesture = normalizedShakeMenuGesture(this.getConfig().getString("shakeMenuGesture", SHAKE_MENU_GESTURE_SHAKE));
|
|
3665
|
+
this.syncShakeMenuLifecycle();
|
|
3666
|
+
this.restorePreviewPreviousAppId();
|
|
3667
|
+
this.restorePreviewPreviousDefaultChannel();
|
|
3668
|
+
this.implementation.setPreviewFallbackBundle(null);
|
|
3669
|
+
this.implementation.setNextBundle(null);
|
|
3670
|
+
this.clearPreviewSessionPreferences();
|
|
3671
|
+
synchronized (this.previewSessionsLock) {
|
|
3672
|
+
this.editor.remove(PREVIEW_SESSIONS_PREF_KEY);
|
|
3673
|
+
this.editor.apply();
|
|
1573
3674
|
}
|
|
1574
3675
|
}
|
|
1575
3676
|
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
logger.error("Set called without id");
|
|
1581
|
-
call.reject("Set called without id");
|
|
3677
|
+
private void restorePreviewPreviousNextBundle() {
|
|
3678
|
+
final String previousNextBundleId = this.prefs.getString(PREVIEW_PREVIOUS_NEXT_BUNDLE_PREF_KEY, null);
|
|
3679
|
+
if (previousNextBundleId == null || previousNextBundleId.isEmpty()) {
|
|
3680
|
+
this.implementation.setNextBundle(null);
|
|
1582
3681
|
return;
|
|
1583
3682
|
}
|
|
1584
|
-
|
|
1585
|
-
logger.
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
3683
|
+
if (!this.implementation.setNextBundle(previousNextBundleId)) {
|
|
3684
|
+
logger.warn("Could not restore pre-preview next bundle: " + previousNextBundleId);
|
|
3685
|
+
this.implementation.setNextBundle(null);
|
|
3686
|
+
}
|
|
3687
|
+
}
|
|
3688
|
+
|
|
3689
|
+
private void ensureShakeMenuStarted() {
|
|
3690
|
+
if (shakeMenu != null && !shakeMenu.usesGesture(this.shakeMenuGesture)) {
|
|
3691
|
+
try {
|
|
3692
|
+
shakeMenu.stop();
|
|
3693
|
+
shakeMenu = null;
|
|
3694
|
+
logger.info("Shake menu restarted for " + this.shakeMenuGesture + " gesture");
|
|
3695
|
+
} catch (Exception e) {
|
|
3696
|
+
logger.error("Failed to restart shake menu: " + e.getMessage());
|
|
3697
|
+
return;
|
|
1596
3698
|
}
|
|
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
3699
|
}
|
|
3700
|
+
|
|
3701
|
+
if (getActivity() instanceof com.getcapacitor.BridgeActivity && shakeMenu == null) {
|
|
3702
|
+
try {
|
|
3703
|
+
shakeMenu = new ShakeMenu(this, (com.getcapacitor.BridgeActivity) getActivity(), logger, this.shakeMenuGesture);
|
|
3704
|
+
logger.info("Shake menu initialized with " + this.shakeMenuGesture + " gesture");
|
|
3705
|
+
} catch (Exception e) {
|
|
3706
|
+
logger.error("Failed to initialize shake menu: " + e.getMessage());
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
|
|
3711
|
+
private void syncShakeMenuLifecycle() {
|
|
3712
|
+
if (this.shouldListenForShake()) {
|
|
3713
|
+
this.ensureShakeMenuStarted();
|
|
3714
|
+
} else if (shakeMenu != null) {
|
|
3715
|
+
try {
|
|
3716
|
+
shakeMenu.stop();
|
|
3717
|
+
shakeMenu = null;
|
|
3718
|
+
logger.info("Shake menu stopped");
|
|
3719
|
+
} catch (Exception e) {
|
|
3720
|
+
logger.error("Failed to stop shake menu: " + e.getMessage());
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
|
|
3725
|
+
private boolean shouldListenForShake() {
|
|
3726
|
+
return Boolean.TRUE.equals(this.shakeMenuEnabled) || Boolean.TRUE.equals(this.shakeChannelSelectorEnabled);
|
|
3727
|
+
}
|
|
3728
|
+
|
|
3729
|
+
private void showPreviewSessionNoticeIfNeeded() {
|
|
3730
|
+
if (!Boolean.TRUE.equals(this.previewSessionEnabled) || !Boolean.TRUE.equals(this.previewSessionAlertPending)) {
|
|
3731
|
+
return;
|
|
3732
|
+
}
|
|
3733
|
+
this.previewSessionAlertPending = false;
|
|
3734
|
+
this.editor.putBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, false);
|
|
3735
|
+
this.editor.apply();
|
|
3736
|
+
|
|
3737
|
+
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
|
3738
|
+
try {
|
|
3739
|
+
if (!Boolean.TRUE.equals(this.previewSessionEnabled)) {
|
|
3740
|
+
return;
|
|
3741
|
+
}
|
|
3742
|
+
if (getActivity() == null || getActivity().isFinishing()) {
|
|
3743
|
+
this.previewSessionAlertPending = true;
|
|
3744
|
+
this.editor.putBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, true);
|
|
3745
|
+
this.editor.apply();
|
|
3746
|
+
return;
|
|
3747
|
+
}
|
|
3748
|
+
|
|
3749
|
+
new AlertDialog.Builder(getActivity())
|
|
3750
|
+
.setTitle("Preview started")
|
|
3751
|
+
.setMessage("Shake your device anytime to reload or leave the test app.")
|
|
3752
|
+
.setPositiveButton("Got it", (dialog, which) -> dialog.dismiss())
|
|
3753
|
+
.show();
|
|
3754
|
+
} catch (final Exception e) {
|
|
3755
|
+
this.previewSessionAlertPending = true;
|
|
3756
|
+
this.editor.putBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, true);
|
|
3757
|
+
this.editor.apply();
|
|
3758
|
+
logger.warn("Could not show preview session notice: " + e.getMessage());
|
|
3759
|
+
}
|
|
3760
|
+
}, 600);
|
|
1601
3761
|
}
|
|
1602
3762
|
|
|
1603
3763
|
@PluginMethod
|
|
@@ -1681,33 +3841,146 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1681
3841
|
@PluginMethod
|
|
1682
3842
|
public void getLatest(final PluginCall call) {
|
|
1683
3843
|
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;
|
|
3844
|
+
final boolean includeBundleSize = call.getBoolean("includeBundleSize", false);
|
|
3845
|
+
final String previewAppId = this.normalizePreviewAppId(call.getString("appId"));
|
|
3846
|
+
final boolean hasPreviewAppId = previewAppId != null;
|
|
3847
|
+
if (hasPreviewAppId && !Boolean.TRUE.equals(this.allowPreview)) {
|
|
3848
|
+
logger.error("getLatest preview override not allowed set allowPreview in your config to true to enable it");
|
|
3849
|
+
call.reject("getLatest preview override not allowed");
|
|
3850
|
+
return;
|
|
3851
|
+
}
|
|
3852
|
+
|
|
3853
|
+
final Callback latestCallback = (res) -> {
|
|
3854
|
+
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
3855
|
+
if (jsRes.has("error") || jsRes.has("kind")) {
|
|
3856
|
+
String error = jsRes.has("error") ? jsRes.getString("error") : "";
|
|
3857
|
+
String errorMessage = jsRes.has("message") ? jsRes.getString("message") : "server did not provide a message";
|
|
3858
|
+
String kind = CapacitorUpdaterPlugin.this.getUpdateResponseKind(jsRes.has("kind") ? jsRes.getString("kind") : null);
|
|
3859
|
+
String latestVersion = jsRes.has("version") ? jsRes.getString("version") : "";
|
|
3860
|
+
jsRes.put("kind", kind);
|
|
3861
|
+
CapacitorUpdaterPlugin.this.notifyBreakingEventsIfNeeded(jsRes, latestVersion);
|
|
3862
|
+
if ("failed".equals(kind)) {
|
|
3863
|
+
logger.error("getLatest failed with error: " + error + ", message: " + errorMessage);
|
|
3864
|
+
call.reject(error.isEmpty() ? errorMessage : error);
|
|
1706
3865
|
} else {
|
|
3866
|
+
if (!jsRes.has("version") || jsRes.getString("version").isEmpty()) {
|
|
3867
|
+
jsRes.put("version", CapacitorUpdaterPlugin.this.implementation.getCurrentBundle().getVersionName());
|
|
3868
|
+
}
|
|
3869
|
+
logger.info("getLatest returned " + kind + ": " + errorMessage);
|
|
1707
3870
|
call.resolve(jsRes);
|
|
1708
3871
|
}
|
|
1709
|
-
|
|
1710
|
-
|
|
3872
|
+
return;
|
|
3873
|
+
} else if (jsRes.has("message")) {
|
|
3874
|
+
String latestVersion = jsRes.has("version") ? jsRes.getString("version") : "";
|
|
3875
|
+
CapacitorUpdaterPlugin.this.notifyBreakingEventsIfNeeded(jsRes, latestVersion);
|
|
3876
|
+
call.reject(jsRes.getString("message"));
|
|
3877
|
+
return;
|
|
3878
|
+
} else {
|
|
3879
|
+
if (includeBundleSize) {
|
|
3880
|
+
CapacitorUpdaterPlugin.this.attachBundleSize(jsRes);
|
|
3881
|
+
}
|
|
3882
|
+
call.resolve(jsRes);
|
|
3883
|
+
}
|
|
3884
|
+
};
|
|
3885
|
+
|
|
3886
|
+
startNewThread(() -> {
|
|
3887
|
+
if (hasPreviewAppId) {
|
|
3888
|
+
CapacitorUpdaterPlugin.this.implementation.getLatest(
|
|
3889
|
+
CapacitorUpdaterPlugin.this.updateUrl,
|
|
3890
|
+
channel,
|
|
3891
|
+
previewAppId,
|
|
3892
|
+
latestCallback
|
|
3893
|
+
);
|
|
3894
|
+
return;
|
|
3895
|
+
}
|
|
3896
|
+
CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, channel, latestCallback);
|
|
3897
|
+
});
|
|
3898
|
+
}
|
|
3899
|
+
|
|
3900
|
+
private void attachBundleSize(final JSObject latest) {
|
|
3901
|
+
try {
|
|
3902
|
+
final JSONArray manifest = latest.optJSONArray("manifest");
|
|
3903
|
+
if (manifest == null || manifest.length() == 0) {
|
|
3904
|
+
return;
|
|
3905
|
+
}
|
|
3906
|
+
final String sessionKey = latest.optString("sessionKey", "");
|
|
3907
|
+
final JSONObject missing = this.implementation.missingBundleFilesResult(manifest, sessionKey);
|
|
3908
|
+
final JSONArray missingManifest = missing.getJSONArray("missing");
|
|
3909
|
+
final JSONObject size = this.implementation.getBundleDownloadSize(
|
|
3910
|
+
this.updateUrl,
|
|
3911
|
+
latest.optString("version", ""),
|
|
3912
|
+
missingManifest
|
|
3913
|
+
);
|
|
3914
|
+
latest.put("missing", missing);
|
|
3915
|
+
latest.put("downloadSize", size);
|
|
3916
|
+
} catch (Exception e) {
|
|
3917
|
+
logger.error("Failed to attach bundle size to getLatest result");
|
|
3918
|
+
logger.debug("Error: " + e.getMessage());
|
|
3919
|
+
}
|
|
3920
|
+
}
|
|
3921
|
+
|
|
3922
|
+
@PluginMethod
|
|
3923
|
+
public void getMissingBundleFiles(final PluginCall call) {
|
|
3924
|
+
final JSONArray manifest = call.getData().optJSONArray("manifest");
|
|
3925
|
+
if (manifest == null) {
|
|
3926
|
+
call.reject("getMissingBundleFiles called without manifest");
|
|
3927
|
+
return;
|
|
3928
|
+
}
|
|
3929
|
+
String sessionKey = call.getString("sessionKey");
|
|
3930
|
+
if (sessionKey == null) {
|
|
3931
|
+
sessionKey = "";
|
|
3932
|
+
}
|
|
3933
|
+
final String finalSessionKey = sessionKey;
|
|
3934
|
+
startNewThread(() -> {
|
|
3935
|
+
try {
|
|
3936
|
+
call.resolve(jsonObjectToJSObject(this.implementation.missingBundleFilesResult(manifest, finalSessionKey)));
|
|
3937
|
+
} catch (Exception e) {
|
|
3938
|
+
call.reject("Could not get missing bundle files", e);
|
|
3939
|
+
}
|
|
3940
|
+
});
|
|
3941
|
+
}
|
|
3942
|
+
|
|
3943
|
+
@PluginMethod
|
|
3944
|
+
public void getBundleDownloadSize(final PluginCall call) {
|
|
3945
|
+
final JSONArray manifest = call.getData().optJSONArray("manifest");
|
|
3946
|
+
if (manifest == null) {
|
|
3947
|
+
call.reject("getBundleDownloadSize called without manifest");
|
|
3948
|
+
return;
|
|
3949
|
+
}
|
|
3950
|
+
final String version = call.getData().optString("version", "");
|
|
3951
|
+
startNewThread(() -> {
|
|
3952
|
+
try {
|
|
3953
|
+
final JSONObject size = this.implementation.getBundleDownloadSize(this.updateUrl, version, manifest);
|
|
3954
|
+
call.resolve(jsonObjectToJSObject(size));
|
|
3955
|
+
} catch (Exception e) {
|
|
3956
|
+
call.reject("Could not get bundle download size", e);
|
|
3957
|
+
}
|
|
3958
|
+
});
|
|
3959
|
+
}
|
|
3960
|
+
|
|
3961
|
+
public String triggerBackgroundUpdateCheck() {
|
|
3962
|
+
if (this.updateUrl == null || this.updateUrl.isEmpty() || !this.isValidURL(this.updateUrl)) {
|
|
3963
|
+
logger.error("Error no url or wrong format");
|
|
3964
|
+
return "unavailable";
|
|
3965
|
+
}
|
|
3966
|
+
if (this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
3967
|
+
return "preview_session";
|
|
3968
|
+
}
|
|
3969
|
+
if (this.isDownloadStuckOrTimedOut()) {
|
|
3970
|
+
logger.info("Download already in progress, skipping duplicate download request");
|
|
3971
|
+
return "already_running";
|
|
3972
|
+
}
|
|
3973
|
+
this.backgroundDownload();
|
|
3974
|
+
return "queued";
|
|
3975
|
+
}
|
|
3976
|
+
|
|
3977
|
+
@PluginMethod
|
|
3978
|
+
public void triggerUpdateCheck(final PluginCall call) {
|
|
3979
|
+
final String status = this.triggerBackgroundUpdateCheck();
|
|
3980
|
+
final JSObject ret = new JSObject();
|
|
3981
|
+
ret.put("status", status);
|
|
3982
|
+
ret.put("queued", "queued".equals(status));
|
|
3983
|
+
call.resolve(ret);
|
|
1711
3984
|
}
|
|
1712
3985
|
|
|
1713
3986
|
private boolean _reset(final Boolean toLastSuccessful, final Boolean usePendingBundle) {
|
|
@@ -1783,19 +4056,21 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1783
4056
|
|
|
1784
4057
|
@PluginMethod
|
|
1785
4058
|
public void reset(final PluginCall call) {
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
4059
|
+
startNewThread(() -> {
|
|
4060
|
+
try {
|
|
4061
|
+
final Boolean toLastSuccessful = call.getBoolean("toLastSuccessful", false);
|
|
4062
|
+
final Boolean usePendingBundle = call.getBoolean("usePendingBundle", false);
|
|
4063
|
+
if (this._reset(toLastSuccessful, usePendingBundle)) {
|
|
4064
|
+
call.resolve();
|
|
4065
|
+
return;
|
|
4066
|
+
}
|
|
4067
|
+
logger.error("Reset failed");
|
|
4068
|
+
call.reject("Reset failed");
|
|
4069
|
+
} catch (final Exception e) {
|
|
4070
|
+
logger.error("Reset failed " + e.getMessage());
|
|
4071
|
+
call.reject("Reset failed", e);
|
|
1792
4072
|
}
|
|
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
|
-
}
|
|
4073
|
+
});
|
|
1799
4074
|
}
|
|
1800
4075
|
|
|
1801
4076
|
@PluginMethod
|
|
@@ -1859,7 +4134,13 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1859
4134
|
@Override
|
|
1860
4135
|
public void run() {
|
|
1861
4136
|
try {
|
|
4137
|
+
if (CapacitorUpdaterPlugin.this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
4138
|
+
return;
|
|
4139
|
+
}
|
|
1862
4140
|
CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, null, (res) -> {
|
|
4141
|
+
if (CapacitorUpdaterPlugin.this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
4142
|
+
return;
|
|
4143
|
+
}
|
|
1863
4144
|
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
1864
4145
|
if (jsRes.has("error") || jsRes.has("kind")) {
|
|
1865
4146
|
final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
|
|
@@ -1922,6 +4203,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1922
4203
|
logger.info("semaphoreReady countDown");
|
|
1923
4204
|
this.semaphoreDown();
|
|
1924
4205
|
logger.info("semaphoreReady countDown done");
|
|
4206
|
+
this.clearIncomingPreviewTransition();
|
|
4207
|
+
this.hidePreviewTransitionLoader("notify-app-ready");
|
|
1925
4208
|
final JSObject ret = new JSObject();
|
|
1926
4209
|
ret.put("bundle", InternalUtils.mapToJSObject(bundle.toJSONMap()));
|
|
1927
4210
|
call.resolve(ret);
|
|
@@ -1969,6 +4252,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1969
4252
|
}
|
|
1970
4253
|
|
|
1971
4254
|
private Boolean _isAutoUpdateEnabled() {
|
|
4255
|
+
if (this.isPreviewSessionStateActive()) {
|
|
4256
|
+
return false;
|
|
4257
|
+
}
|
|
1972
4258
|
final CapConfig config = CapConfig.loadDefault(this.getActivity());
|
|
1973
4259
|
String serverUrl = config.getServerUrl();
|
|
1974
4260
|
if (serverUrl != null && !serverUrl.isEmpty()) {
|
|
@@ -2012,10 +4298,22 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2012
4298
|
this.checkAppReady(this.resolveAppReadyCheckTimeoutMs());
|
|
2013
4299
|
}
|
|
2014
4300
|
|
|
4301
|
+
synchronized boolean shouldInterruptAppReadyCheck(final Thread existingCheck, final Thread currentThread) {
|
|
4302
|
+
return existingCheck != null && existingCheck != currentThread;
|
|
4303
|
+
}
|
|
4304
|
+
|
|
4305
|
+
synchronized void clearAppReadyCheckIfCurrent(final Thread expectedThread) {
|
|
4306
|
+
if (this.appReadyCheck == expectedThread) {
|
|
4307
|
+
this.appReadyCheck = null;
|
|
4308
|
+
}
|
|
4309
|
+
}
|
|
4310
|
+
|
|
2015
4311
|
private void checkAppReady(final long waitTimeMs) {
|
|
2016
4312
|
try {
|
|
2017
|
-
|
|
2018
|
-
|
|
4313
|
+
final Thread currentThread = Thread.currentThread();
|
|
4314
|
+
final Thread existingCheck = this.appReadyCheck;
|
|
4315
|
+
if (this.shouldInterruptAppReadyCheck(existingCheck, currentThread)) {
|
|
4316
|
+
existingCheck.interrupt();
|
|
2019
4317
|
}
|
|
2020
4318
|
this.appReadyCheck = startNewThread(new DeferredNotifyAppReadyCheck(waitTimeMs));
|
|
2021
4319
|
} catch (final Exception e) {
|
|
@@ -2032,13 +4330,17 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2032
4330
|
}
|
|
2033
4331
|
}
|
|
2034
4332
|
|
|
2035
|
-
|
|
4333
|
+
static String normalizedUpdateResponseKind(final String kind) {
|
|
2036
4334
|
if ("up_to_date".equals(kind) || "blocked".equals(kind) || "failed".equals(kind)) {
|
|
2037
4335
|
return kind;
|
|
2038
4336
|
}
|
|
2039
4337
|
return "failed";
|
|
2040
4338
|
}
|
|
2041
4339
|
|
|
4340
|
+
private String getUpdateResponseKind(final String kind) {
|
|
4341
|
+
return normalizedUpdateResponseKind(kind);
|
|
4342
|
+
}
|
|
4343
|
+
|
|
2042
4344
|
private void notifyUpdateCheckResult(
|
|
2043
4345
|
final String kind,
|
|
2044
4346
|
final String error,
|
|
@@ -2098,6 +4400,30 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2098
4400
|
String failureAction,
|
|
2099
4401
|
String failureEvent,
|
|
2100
4402
|
boolean shouldSendStats
|
|
4403
|
+
) {
|
|
4404
|
+
endBackGroundTaskWithNotif(
|
|
4405
|
+
msg,
|
|
4406
|
+
latestVersionName,
|
|
4407
|
+
current,
|
|
4408
|
+
error,
|
|
4409
|
+
plannedDirectUpdate,
|
|
4410
|
+
failureAction,
|
|
4411
|
+
failureEvent,
|
|
4412
|
+
shouldSendStats,
|
|
4413
|
+
true
|
|
4414
|
+
);
|
|
4415
|
+
}
|
|
4416
|
+
|
|
4417
|
+
private void endBackGroundTaskWithNotif(
|
|
4418
|
+
String msg,
|
|
4419
|
+
String latestVersionName,
|
|
4420
|
+
BundleInfo current,
|
|
4421
|
+
Boolean error,
|
|
4422
|
+
Boolean plannedDirectUpdate,
|
|
4423
|
+
String failureAction,
|
|
4424
|
+
String failureEvent,
|
|
4425
|
+
boolean shouldSendStats,
|
|
4426
|
+
boolean shouldNotifyNoNeedUpdate
|
|
2101
4427
|
) {
|
|
2102
4428
|
this.consumeOnLaunchDirectUpdateAttempt(Boolean.TRUE.equals(plannedDirectUpdate));
|
|
2103
4429
|
if (error) {
|
|
@@ -2116,15 +4442,22 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2116
4442
|
ret.put("version", latestVersionName);
|
|
2117
4443
|
this.notifyListeners(failureEvent, ret);
|
|
2118
4444
|
}
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
4445
|
+
if (shouldNotifyNoNeedUpdate) {
|
|
4446
|
+
final JSObject ret = new JSObject();
|
|
4447
|
+
ret.put("bundle", InternalUtils.mapToJSObject(current.toJSONMap()));
|
|
4448
|
+
this.notifyListeners("noNeedUpdate", ret);
|
|
4449
|
+
}
|
|
2122
4450
|
this.sendReadyToJs(current, msg, plannedDirectUpdate);
|
|
2123
4451
|
this.backgroundDownloadTask = null;
|
|
2124
4452
|
this.downloadStartTimeMs = 0;
|
|
2125
4453
|
logger.info("endBackGroundTaskWithNotif " + msg);
|
|
2126
4454
|
}
|
|
2127
4455
|
|
|
4456
|
+
private void clearBackgroundDownloadState() {
|
|
4457
|
+
this.backgroundDownloadTask = null;
|
|
4458
|
+
this.downloadStartTimeMs = 0;
|
|
4459
|
+
}
|
|
4460
|
+
|
|
2128
4461
|
private boolean isDownloadStuckOrTimedOut() {
|
|
2129
4462
|
if (this.backgroundDownloadTask == null || !this.backgroundDownloadTask.isAlive()) {
|
|
2130
4463
|
return false;
|
|
@@ -2151,17 +4484,30 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2151
4484
|
}
|
|
2152
4485
|
|
|
2153
4486
|
private Thread backgroundDownload() {
|
|
4487
|
+
if (this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
4488
|
+
return null;
|
|
4489
|
+
}
|
|
2154
4490
|
final boolean plannedDirectUpdate = this.shouldUseDirectUpdate();
|
|
2155
4491
|
final boolean initialDirectUpdateAllowed = this.isDirectUpdateCurrentlyAllowed(plannedDirectUpdate);
|
|
2156
4492
|
final String messageUpdate = initialDirectUpdateAllowed
|
|
2157
4493
|
? "Update will occur now."
|
|
2158
|
-
:
|
|
4494
|
+
: this.shouldAutoSetNextBundle()
|
|
4495
|
+
? "Update will occur next time app moves to background."
|
|
4496
|
+
: "Update will be downloaded and made available.";
|
|
2159
4497
|
Thread newTask = startNewThread(() -> {
|
|
2160
4498
|
// Wait for cleanup to complete before starting download
|
|
2161
4499
|
waitForCleanupIfNeeded();
|
|
4500
|
+
if (CapacitorUpdaterPlugin.this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
4501
|
+
CapacitorUpdaterPlugin.this.clearBackgroundDownloadState();
|
|
4502
|
+
return;
|
|
4503
|
+
}
|
|
2162
4504
|
logger.info("Check for update via: " + CapacitorUpdaterPlugin.this.updateUrl);
|
|
2163
4505
|
try {
|
|
2164
4506
|
CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, null, (res) -> {
|
|
4507
|
+
if (CapacitorUpdaterPlugin.this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
4508
|
+
CapacitorUpdaterPlugin.this.clearBackgroundDownloadState();
|
|
4509
|
+
return;
|
|
4510
|
+
}
|
|
2165
4511
|
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
2166
4512
|
final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
|
|
2167
4513
|
|
|
@@ -2173,6 +4519,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2173
4519
|
String kind = CapacitorUpdaterPlugin.this.getUpdateResponseKind(jsRes.has("kind") ? jsRes.getString("kind") : null);
|
|
2174
4520
|
String latestVersion = jsRes.has("version") ? jsRes.getString("version") : current.getVersionName();
|
|
2175
4521
|
CapacitorUpdaterPlugin.this.notifyUpdateCheckResult(kind, error, errorMessage, statusCode, latestVersion, current);
|
|
4522
|
+
CapacitorUpdaterPlugin.this.notifyBreakingEventsIfNeeded(
|
|
4523
|
+
jsRes,
|
|
4524
|
+
jsRes.has("version") ? jsRes.getString("version") : ""
|
|
4525
|
+
);
|
|
2176
4526
|
|
|
2177
4527
|
if ("up_to_date".equals(kind)) {
|
|
2178
4528
|
logger.info("No new version available");
|
|
@@ -2215,7 +4565,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2215
4565
|
false,
|
|
2216
4566
|
true
|
|
2217
4567
|
);
|
|
2218
|
-
} else {
|
|
4568
|
+
} else if (CapacitorUpdaterPlugin.this.shouldAutoSetNextBundle()) {
|
|
2219
4569
|
if (plannedDirectUpdate && !directUpdateAllowedNow) {
|
|
2220
4570
|
logger.info(
|
|
2221
4571
|
"Direct update skipped because splashscreen timeout occurred. Update will be applied later."
|
|
@@ -2230,11 +4580,34 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2230
4580
|
false,
|
|
2231
4581
|
plannedDirectUpdate
|
|
2232
4582
|
);
|
|
4583
|
+
} else {
|
|
4584
|
+
logger.info("autoUpdate is set to onlyDownload, builtin version will not be set as next bundle");
|
|
4585
|
+
final boolean builtinUpdateAvailable = !current.isBuiltin();
|
|
4586
|
+
if (builtinUpdateAvailable) {
|
|
4587
|
+
final JSObject ret = new JSObject();
|
|
4588
|
+
final BundleInfo builtinBundle = CapacitorUpdaterPlugin.this.implementation.getBundleInfo(
|
|
4589
|
+
BundleInfo.ID_BUILTIN
|
|
4590
|
+
);
|
|
4591
|
+
ret.put("bundle", InternalUtils.mapToJSObject(builtinBundle.toJSONMap()));
|
|
4592
|
+
CapacitorUpdaterPlugin.this.notifyListeners("updateAvailable", ret);
|
|
4593
|
+
}
|
|
4594
|
+
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
4595
|
+
"Latest version is builtin, autoUpdate onlyDownload",
|
|
4596
|
+
latestVersionName,
|
|
4597
|
+
current,
|
|
4598
|
+
false,
|
|
4599
|
+
plannedDirectUpdate,
|
|
4600
|
+
"download_fail",
|
|
4601
|
+
"downloadFailed",
|
|
4602
|
+
true,
|
|
4603
|
+
!builtinUpdateAvailable
|
|
4604
|
+
);
|
|
2233
4605
|
}
|
|
2234
4606
|
return;
|
|
2235
4607
|
}
|
|
2236
4608
|
|
|
2237
4609
|
if (!jsRes.has("url") || !CapacitorUpdaterPlugin.this.isValidURL(jsRes.getString("url"))) {
|
|
4610
|
+
CapacitorUpdaterPlugin.this.notifyBreakingEventsIfNeeded(jsRes, latestVersionName);
|
|
2238
4611
|
logger.error("Error no url or wrong format");
|
|
2239
4612
|
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
2240
4613
|
"Error no url or wrong format",
|
|
@@ -2305,7 +4678,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2305
4678
|
true
|
|
2306
4679
|
);
|
|
2307
4680
|
}
|
|
2308
|
-
} else {
|
|
4681
|
+
} else if (CapacitorUpdaterPlugin.this.shouldAutoSetNextBundle()) {
|
|
2309
4682
|
if (plannedDirectUpdate && !directUpdateAllowedNow) {
|
|
2310
4683
|
logger.info(
|
|
2311
4684
|
"Direct update skipped because splashscreen timeout occurred. Update will install on next background."
|
|
@@ -2320,6 +4693,20 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2320
4693
|
false,
|
|
2321
4694
|
plannedDirectUpdate
|
|
2322
4695
|
);
|
|
4696
|
+
} else {
|
|
4697
|
+
logger.info("autoUpdate is set to onlyDownload, downloaded update will not be set as next bundle");
|
|
4698
|
+
CapacitorUpdaterPlugin.this.notifyListeners("updateAvailable", ret);
|
|
4699
|
+
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
4700
|
+
"update downloaded, autoUpdate onlyDownload",
|
|
4701
|
+
latestVersionName,
|
|
4702
|
+
current,
|
|
4703
|
+
false,
|
|
4704
|
+
plannedDirectUpdate,
|
|
4705
|
+
"download_fail",
|
|
4706
|
+
"downloadFailed",
|
|
4707
|
+
true,
|
|
4708
|
+
false
|
|
4709
|
+
);
|
|
2323
4710
|
}
|
|
2324
4711
|
return;
|
|
2325
4712
|
}
|
|
@@ -2345,6 +4732,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2345
4732
|
: initialDirectUpdateAllowed;
|
|
2346
4733
|
startNewThread(() -> {
|
|
2347
4734
|
try {
|
|
4735
|
+
if (CapacitorUpdaterPlugin.this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
4736
|
+
CapacitorUpdaterPlugin.this.clearBackgroundDownloadState();
|
|
4737
|
+
return;
|
|
4738
|
+
}
|
|
2348
4739
|
logger.info(
|
|
2349
4740
|
"New bundle: " +
|
|
2350
4741
|
latestVersionName +
|
|
@@ -2366,7 +4757,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2366
4757
|
latestVersionName,
|
|
2367
4758
|
sessionKey,
|
|
2368
4759
|
checksum,
|
|
2369
|
-
manifest
|
|
4760
|
+
manifest,
|
|
4761
|
+
CapacitorUpdaterPlugin.this.shouldAutoSetNextBundle()
|
|
2370
4762
|
);
|
|
2371
4763
|
} else {
|
|
2372
4764
|
// Handle single file download (existing code)
|
|
@@ -2375,7 +4767,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2375
4767
|
latestVersionName,
|
|
2376
4768
|
sessionKey,
|
|
2377
4769
|
checksum,
|
|
2378
|
-
null
|
|
4770
|
+
null,
|
|
4771
|
+
CapacitorUpdaterPlugin.this.shouldAutoSetNextBundle()
|
|
2379
4772
|
);
|
|
2380
4773
|
}
|
|
2381
4774
|
} catch (final Exception e) {
|
|
@@ -2429,6 +4822,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2429
4822
|
|
|
2430
4823
|
private void installNext() {
|
|
2431
4824
|
try {
|
|
4825
|
+
if (this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
4826
|
+
return;
|
|
4827
|
+
}
|
|
2432
4828
|
String delayUpdatePreferences = prefs.getString(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES, "[]");
|
|
2433
4829
|
ArrayList<DelayCondition> delayConditionList = delayUpdateUtils.parseDelayConditions(delayUpdatePreferences);
|
|
2434
4830
|
if (!delayConditionList.isEmpty()) {
|
|
@@ -2464,6 +4860,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2464
4860
|
logger.info("Built-in bundle is active. We skip the check for notifyAppReady.");
|
|
2465
4861
|
return;
|
|
2466
4862
|
}
|
|
4863
|
+
if (this.isPreviewSessionStateActive()) {
|
|
4864
|
+
logger.info("Preview session is active. We skip the check for notifyAppReady.");
|
|
4865
|
+
return;
|
|
4866
|
+
}
|
|
2467
4867
|
logger.debug("Current bundle is: " + current);
|
|
2468
4868
|
|
|
2469
4869
|
if (BundleStatus.SUCCESS != current.getStatus()) {
|
|
@@ -2502,12 +4902,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2502
4902
|
|
|
2503
4903
|
@Override
|
|
2504
4904
|
public void run() {
|
|
4905
|
+
final Thread currentThread = Thread.currentThread();
|
|
2505
4906
|
try {
|
|
2506
4907
|
logger.info("Wait for " + this.waitTimeMs + "ms, then check for notifyAppReady");
|
|
2507
4908
|
Thread.sleep(this.waitTimeMs);
|
|
2508
4909
|
CapacitorUpdaterPlugin.this.checkRevert();
|
|
2509
|
-
CapacitorUpdaterPlugin.this.
|
|
4910
|
+
CapacitorUpdaterPlugin.this.clearAppReadyCheckIfCurrent(currentThread);
|
|
2510
4911
|
} catch (final InterruptedException e) {
|
|
4912
|
+
CapacitorUpdaterPlugin.this.clearAppReadyCheckIfCurrent(currentThread);
|
|
2511
4913
|
logger.info(DeferredNotifyAppReadyCheck.class.getName() + " was interrupted.");
|
|
2512
4914
|
}
|
|
2513
4915
|
}
|
|
@@ -2636,6 +5038,36 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2636
5038
|
}
|
|
2637
5039
|
}
|
|
2638
5040
|
|
|
5041
|
+
@Override
|
|
5042
|
+
protected void handleOnNewIntent(Intent intent) {
|
|
5043
|
+
super.handleOnNewIntent(intent);
|
|
5044
|
+
if (
|
|
5045
|
+
intent == null ||
|
|
5046
|
+
!Intent.ACTION_VIEW.equals(intent.getAction()) ||
|
|
5047
|
+
intent.getData() == null ||
|
|
5048
|
+
!Boolean.TRUE.equals(this.previewSessionEnabled) ||
|
|
5049
|
+
!isPreviewDeepLink(intent.getData()) ||
|
|
5050
|
+
Boolean.TRUE.equals(this.isLeavingPreviewForIncomingLink)
|
|
5051
|
+
) {
|
|
5052
|
+
return;
|
|
5053
|
+
}
|
|
5054
|
+
|
|
5055
|
+
this.isLeavingPreviewForIncomingLink = true;
|
|
5056
|
+
this.showPreviewTransitionLoader("incoming-preview-deeplink");
|
|
5057
|
+
if (getActivity() != null) {
|
|
5058
|
+
getActivity().setIntent(intent);
|
|
5059
|
+
}
|
|
5060
|
+
logger.info("Preview deeplink received while preview session is active; restoring fallback before routing");
|
|
5061
|
+
startNewThread(() -> {
|
|
5062
|
+
final boolean didLeave = this.leavePreviewSessionForIncomingPreviewLink();
|
|
5063
|
+
if (!didLeave) {
|
|
5064
|
+
logger.error("Could not leave preview session before routing incoming preview deeplink");
|
|
5065
|
+
this.isLeavingPreviewForIncomingLink = false;
|
|
5066
|
+
this.hidePreviewTransitionLoader("incoming-preview-deeplink-failed");
|
|
5067
|
+
}
|
|
5068
|
+
});
|
|
5069
|
+
}
|
|
5070
|
+
|
|
2639
5071
|
@Override
|
|
2640
5072
|
public void handleOnStart() {
|
|
2641
5073
|
try {
|
|
@@ -2651,15 +5083,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2651
5083
|
isPreviousMainActivity = true;
|
|
2652
5084
|
}
|
|
2653
5085
|
|
|
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
|
-
}
|
|
5086
|
+
this.syncShakeMenuLifecycle();
|
|
2663
5087
|
} catch (Exception e) {
|
|
2664
5088
|
logger.error("Failed to run handleOnStart: " + e.getMessage());
|
|
2665
5089
|
}
|
|
@@ -2691,6 +5115,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2691
5115
|
backgroundTask.interrupt();
|
|
2692
5116
|
}
|
|
2693
5117
|
this.implementation.activity = getActivity();
|
|
5118
|
+
this.syncShakeMenuLifecycle();
|
|
2694
5119
|
} catch (Exception e) {
|
|
2695
5120
|
logger.error("Failed to run handleOnResume: " + e.getMessage());
|
|
2696
5121
|
}
|
|
@@ -2715,25 +5140,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2715
5140
|
}
|
|
2716
5141
|
|
|
2717
5142
|
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
|
-
}
|
|
5143
|
+
logger.info("Shake menu " + (enabled ? "enabled" : "disabled") + " with " + this.shakeMenuGesture + " gesture");
|
|
5144
|
+
this.syncShakeMenuLifecycle();
|
|
2737
5145
|
|
|
2738
5146
|
call.resolve();
|
|
2739
5147
|
}
|
|
@@ -2743,6 +5151,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2743
5151
|
try {
|
|
2744
5152
|
final JSObject ret = new JSObject();
|
|
2745
5153
|
ret.put("enabled", this.shakeMenuEnabled);
|
|
5154
|
+
ret.put("gesture", this.shakeMenuGesture);
|
|
2746
5155
|
call.resolve(ret);
|
|
2747
5156
|
} catch (final Exception e) {
|
|
2748
5157
|
logger.error("Could not get shake menu status " + e.getMessage());
|
|
@@ -2761,6 +5170,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2761
5170
|
|
|
2762
5171
|
this.shakeChannelSelectorEnabled = enabled;
|
|
2763
5172
|
logger.info("Shake channel selector " + (enabled ? "enabled" : "disabled"));
|
|
5173
|
+
this.syncShakeMenuLifecycle();
|
|
2764
5174
|
call.resolve();
|
|
2765
5175
|
}
|
|
2766
5176
|
|
|
@@ -2801,7 +5211,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2801
5211
|
call.reject("setAppId called without appId");
|
|
2802
5212
|
return;
|
|
2803
5213
|
}
|
|
2804
|
-
this.
|
|
5214
|
+
this.setActiveAppId(appId);
|
|
2805
5215
|
call.resolve();
|
|
2806
5216
|
}
|
|
2807
5217
|
|
|
@@ -3139,6 +5549,11 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
3139
5549
|
logger.error("Failed to clean up AppLifecycleObserver: " + e.getMessage());
|
|
3140
5550
|
}
|
|
3141
5551
|
}
|
|
5552
|
+
|
|
5553
|
+
if (webViewStatsListener != null && bridge != null) {
|
|
5554
|
+
bridge.removeWebViewListener(webViewStatsListener);
|
|
5555
|
+
webViewStatsListener = null;
|
|
5556
|
+
}
|
|
3142
5557
|
} catch (Exception e) {
|
|
3143
5558
|
logger.error("Failed to run handleOnDestroy: " + e.getMessage());
|
|
3144
5559
|
}
|