@capgo/capacitor-updater 7.43.3 → 7.50.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Package.swift +5 -2
- package/README.md +643 -115
- package/android/build.gradle +3 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/AndroidAppExitReporter.java +92 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +3121 -353
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +924 -290
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +49 -13
- package/android/src/main/java/ee/forgr/capacitor_updater/DeviceIdHelper.java +45 -30
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +75 -32
- 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 +401 -27
- package/android/src/main/java/ee/forgr/capacitor_updater/ThreeFingerPinchDetector.java +323 -0
- package/dist/docs.json +1590 -196
- package/dist/esm/definitions.d.ts +755 -66
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +11 -2
- package/dist/esm/web.js +59 -5
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +59 -5
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +59 -5
- 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 +2732 -417
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +1191 -363
- package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +0 -1
- package/ios/Sources/CapacitorUpdaterPlugin/DelayUpdateUtils.swift +37 -16
- package/ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift +80 -1
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +438 -39
- package/ios/Sources/CapacitorUpdaterPlugin/WebViewStatsReporter.swift +276 -0
- package/package.json +24 -9
|
@@ -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,8 +21,11 @@ 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;
|
|
27
|
+
import androidx.core.content.pm.PackageInfoCompat;
|
|
28
|
+
import com.getcapacitor.Bridge;
|
|
25
29
|
import com.getcapacitor.CapConfig;
|
|
26
30
|
import com.getcapacitor.JSArray;
|
|
27
31
|
import com.getcapacitor.JSObject;
|
|
@@ -29,6 +33,8 @@ import com.getcapacitor.Plugin;
|
|
|
29
33
|
import com.getcapacitor.PluginCall;
|
|
30
34
|
import com.getcapacitor.PluginHandle;
|
|
31
35
|
import com.getcapacitor.PluginMethod;
|
|
36
|
+
import com.getcapacitor.PluginResult;
|
|
37
|
+
import com.getcapacitor.WebViewListener;
|
|
32
38
|
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
33
39
|
import com.getcapacitor.plugin.WebView;
|
|
34
40
|
import com.google.android.gms.tasks.Task;
|
|
@@ -43,20 +49,25 @@ import com.google.android.play.core.install.model.AppUpdateType;
|
|
|
43
49
|
import com.google.android.play.core.install.model.InstallStatus;
|
|
44
50
|
import com.google.android.play.core.install.model.UpdateAvailability;
|
|
45
51
|
import io.github.g00fy2.versioncompare.Version;
|
|
52
|
+
import java.io.ByteArrayOutputStream;
|
|
46
53
|
import java.io.IOException;
|
|
54
|
+
import java.io.InputStream;
|
|
55
|
+
import java.net.HttpURLConnection;
|
|
47
56
|
import java.net.MalformedURLException;
|
|
48
57
|
import java.net.URL;
|
|
58
|
+
import java.nio.charset.StandardCharsets;
|
|
59
|
+
import java.text.SimpleDateFormat;
|
|
49
60
|
import java.util.ArrayList;
|
|
50
|
-
import java.util.Arrays;
|
|
51
61
|
import java.util.Date;
|
|
62
|
+
import java.util.HashMap;
|
|
52
63
|
import java.util.HashSet;
|
|
53
64
|
import java.util.List;
|
|
54
65
|
import java.util.Map;
|
|
55
66
|
import java.util.Objects;
|
|
56
67
|
import java.util.Set;
|
|
68
|
+
import java.util.TimeZone;
|
|
57
69
|
import java.util.Timer;
|
|
58
70
|
import java.util.TimerTask;
|
|
59
|
-
import java.util.UUID;
|
|
60
71
|
import java.util.concurrent.Phaser;
|
|
61
72
|
import java.util.concurrent.Semaphore;
|
|
62
73
|
import java.util.concurrent.TimeUnit;
|
|
@@ -70,6 +81,16 @@ import org.json.JSONObject;
|
|
|
70
81
|
@CapacitorPlugin(name = "CapacitorUpdater")
|
|
71
82
|
public class CapacitorUpdaterPlugin extends Plugin {
|
|
72
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
|
+
|
|
73
94
|
private Logger logger;
|
|
74
95
|
|
|
75
96
|
private static final String updateUrlDefault = "https://plugin.capgo.app/updates";
|
|
@@ -81,14 +102,52 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
81
102
|
private static final String STATS_URL_PREF_KEY = "CapacitorUpdater.statsUrl";
|
|
82
103
|
private static final String CHANNEL_URL_PREF_KEY = "CapacitorUpdater.channelUrl";
|
|
83
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";
|
|
84
118
|
private static final String[] BREAKING_EVENT_NAMES = { "breakingAvailable", "majorAvailable" };
|
|
85
119
|
private static final String LAST_FAILED_BUNDLE_PREF_KEY = "CapacitorUpdater.lastFailedBundle";
|
|
86
|
-
|
|
87
|
-
private final String
|
|
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";
|
|
127
|
+
private static final String SPLASH_SCREEN_PLUGIN_ID = "SplashScreen";
|
|
128
|
+
private static final int SPLASH_SCREEN_RETRY_DELAY_MS = 100;
|
|
129
|
+
private static final int SPLASH_SCREEN_MAX_RETRIES = 20;
|
|
130
|
+
private static final long PENDING_BUNDLE_APP_READY_MIN_TIMEOUT_MS = 30000L;
|
|
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";
|
|
88
146
|
private static final String DELAY_CONDITION_PREFERENCES = "";
|
|
89
147
|
|
|
90
148
|
private SharedPreferences.Editor editor;
|
|
91
149
|
private SharedPreferences prefs;
|
|
150
|
+
private final Object previewSessionsLock = new Object();
|
|
92
151
|
protected CapgoUpdater implementation;
|
|
93
152
|
private Boolean persistCustomId = false;
|
|
94
153
|
private Boolean persistModifyUrl = false;
|
|
@@ -98,6 +157,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
98
157
|
private Boolean autoDeleteFailed = true;
|
|
99
158
|
private Boolean autoDeletePrevious = true;
|
|
100
159
|
private Boolean autoUpdate = false;
|
|
160
|
+
private String autoUpdateMode = AUTO_UPDATE_MODE_OFF;
|
|
101
161
|
private String updateUrl = "";
|
|
102
162
|
private Version currentVersionNative;
|
|
103
163
|
private String currentBuildVersion;
|
|
@@ -108,28 +168,62 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
108
168
|
private Boolean autoSplashscreenLoader = false;
|
|
109
169
|
private Integer autoSplashscreenTimeout = 10000;
|
|
110
170
|
private Boolean autoSplashscreenTimedOut = false;
|
|
171
|
+
private int splashscreenInvocationToken = 0;
|
|
111
172
|
private String directUpdateMode = "false";
|
|
112
173
|
private Boolean wasRecentlyInstalledOrUpdated = false;
|
|
113
|
-
private
|
|
174
|
+
private volatile boolean onLaunchDirectUpdateUsed = false;
|
|
114
175
|
Boolean shakeMenuEnabled = false;
|
|
115
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;
|
|
116
181
|
private Boolean allowManualBundleError = false;
|
|
182
|
+
private Boolean allowPreview = false;
|
|
117
183
|
Boolean allowSetDefaultChannel = true;
|
|
118
184
|
|
|
119
185
|
String getUpdateUrl() {
|
|
120
186
|
return this.updateUrl;
|
|
121
187
|
}
|
|
122
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
|
+
|
|
123
213
|
// Used for activity-based foreground/background detection on Android < 14
|
|
124
214
|
private Boolean isPreviousMainActivity = true;
|
|
125
215
|
|
|
126
216
|
private volatile Thread backgroundDownloadTask;
|
|
127
217
|
private volatile Thread appReadyCheck;
|
|
128
218
|
private volatile long downloadStartTimeMs = 0;
|
|
129
|
-
private static final long DOWNLOAD_TIMEOUT_MS =
|
|
219
|
+
private static final long DOWNLOAD_TIMEOUT_MS = 600000; // 10 minute timeout
|
|
130
220
|
|
|
131
|
-
|
|
132
|
-
|
|
221
|
+
private final Phaser semaphoreReady = new Phaser(0) {
|
|
222
|
+
@Override
|
|
223
|
+
protected boolean onAdvance(final int phase, final int registeredParties) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
};
|
|
133
227
|
|
|
134
228
|
// Lock to ensure cleanup completes before downloads start
|
|
135
229
|
private final Object cleanupLock = new Object();
|
|
@@ -144,6 +238,32 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
144
238
|
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
|
145
239
|
private FrameLayout splashscreenLoaderOverlay;
|
|
146
240
|
private Runnable splashscreenTimeoutRunnable;
|
|
241
|
+
private FrameLayout previewTransitionLoaderOverlay;
|
|
242
|
+
private Runnable previewTransitionLoaderTimeoutRunnable;
|
|
243
|
+
private boolean previewTransitionLoaderRequested = false;
|
|
244
|
+
private WebViewListener webViewStatsListener;
|
|
245
|
+
|
|
246
|
+
private static final class FireAndForgetPluginCall extends PluginCall {
|
|
247
|
+
|
|
248
|
+
FireAndForgetPluginCall(final String methodName, final JSObject data) {
|
|
249
|
+
super(null, SPLASH_SCREEN_PLUGIN_ID, PluginCall.CALLBACK_ID_DANGLING, methodName, data);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
@Override
|
|
253
|
+
public void successCallback(final PluginResult successResult) {}
|
|
254
|
+
|
|
255
|
+
@Override
|
|
256
|
+
public void resolve(final JSObject data) {}
|
|
257
|
+
|
|
258
|
+
@Override
|
|
259
|
+
public void resolve() {}
|
|
260
|
+
|
|
261
|
+
@Override
|
|
262
|
+
public void errorCallback(final String msg) {}
|
|
263
|
+
|
|
264
|
+
@Override
|
|
265
|
+
public void reject(final String msg, final String code, final Exception ex, final JSObject data) {}
|
|
266
|
+
}
|
|
147
267
|
|
|
148
268
|
// App lifecycle observer using ProcessLifecycleOwner for reliable foreground/background detection
|
|
149
269
|
private AppLifecycleObserver appLifecycleObserver;
|
|
@@ -154,6 +274,19 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
154
274
|
private static final int APP_UPDATE_REQUEST_CODE = 9001;
|
|
155
275
|
private InstallStateUpdatedListener installStateUpdatedListener;
|
|
156
276
|
|
|
277
|
+
private PackageInfo getCurrentPackageInfo() throws PackageManager.NameNotFoundException {
|
|
278
|
+
final PackageManager packageManager = this.getContext().getPackageManager();
|
|
279
|
+
final String packageName = this.getContext().getPackageName();
|
|
280
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
281
|
+
return packageManager.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0));
|
|
282
|
+
}
|
|
283
|
+
return packageManager.getPackageInfo(packageName, 0);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private String getVersionCode(final PackageInfo packageInfo) {
|
|
287
|
+
return Long.toString(PackageInfoCompat.getLongVersionCode(packageInfo));
|
|
288
|
+
}
|
|
289
|
+
|
|
157
290
|
private void notifyBreakingEvents(final String version) {
|
|
158
291
|
if (version == null || version.isEmpty()) {
|
|
159
292
|
return;
|
|
@@ -165,6 +298,26 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
165
298
|
}
|
|
166
299
|
}
|
|
167
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
|
+
|
|
168
321
|
private void persistLastFailedBundle(BundleInfo bundle) {
|
|
169
322
|
if (this.prefs == null) {
|
|
170
323
|
return;
|
|
@@ -196,6 +349,296 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
196
349
|
}
|
|
197
350
|
}
|
|
198
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
|
+
|
|
199
642
|
public Thread startNewThread(final Runnable function, Number waitTime) {
|
|
200
643
|
Thread bgTask = new Thread(() -> {
|
|
201
644
|
try {
|
|
@@ -243,13 +686,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
243
686
|
|
|
244
687
|
@Override
|
|
245
688
|
public void directUpdateFinish(final BundleInfo latest) {
|
|
246
|
-
|
|
247
|
-
activity.runOnUiThread(() -> {
|
|
248
|
-
CapacitorUpdaterPlugin.this.directUpdateFinish(latest);
|
|
249
|
-
});
|
|
250
|
-
} else {
|
|
251
|
-
logger.warn("directUpdateFinish: Activity is null, skipping notification");
|
|
252
|
-
}
|
|
689
|
+
CapacitorUpdaterPlugin.this.scheduleDirectUpdateFinish(latest);
|
|
253
690
|
}
|
|
254
691
|
|
|
255
692
|
@Override
|
|
@@ -263,54 +700,15 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
263
700
|
}
|
|
264
701
|
}
|
|
265
702
|
};
|
|
266
|
-
final PackageInfo pInfo = this.
|
|
703
|
+
final PackageInfo pInfo = this.getCurrentPackageInfo();
|
|
267
704
|
this.implementation.activity = this.getActivity();
|
|
268
705
|
this.implementation.versionBuild = this.getConfig().getString("version", pInfo.versionName);
|
|
269
706
|
this.implementation.CAP_SERVER_PATH = WebView.CAP_SERVER_PATH;
|
|
270
707
|
this.implementation.pluginVersion = this.pluginVersion;
|
|
271
|
-
this.implementation.versionCode =
|
|
708
|
+
this.implementation.versionCode = this.getVersionCode(pInfo);
|
|
272
709
|
// Removed unused OkHttpClient creation - using shared client in DownloadService instead
|
|
273
|
-
// Handle directUpdate configuration - support string values and backward compatibility
|
|
274
|
-
String directUpdateConfig = this.getConfig().getString("directUpdate", null);
|
|
275
|
-
if (directUpdateConfig != null) {
|
|
276
|
-
// Handle backward compatibility for boolean true
|
|
277
|
-
if (directUpdateConfig.equals("true")) {
|
|
278
|
-
this.directUpdateMode = "always";
|
|
279
|
-
this.implementation.directUpdate = true;
|
|
280
|
-
} else {
|
|
281
|
-
this.directUpdateMode = directUpdateConfig;
|
|
282
|
-
this.implementation.directUpdate =
|
|
283
|
-
directUpdateConfig.equals("always") ||
|
|
284
|
-
directUpdateConfig.equals("atInstall") ||
|
|
285
|
-
directUpdateConfig.equals("onLaunch");
|
|
286
|
-
// Validate directUpdate value
|
|
287
|
-
if (
|
|
288
|
-
!directUpdateConfig.equals("false") &&
|
|
289
|
-
!directUpdateConfig.equals("always") &&
|
|
290
|
-
!directUpdateConfig.equals("atInstall") &&
|
|
291
|
-
!directUpdateConfig.equals("onLaunch")
|
|
292
|
-
) {
|
|
293
|
-
logger.error(
|
|
294
|
-
"Invalid directUpdate value: \"" +
|
|
295
|
-
directUpdateConfig +
|
|
296
|
-
"\". Supported values are: \"false\", \"true\", \"always\", \"atInstall\", \"onLaunch\". Defaulting to \"false\"."
|
|
297
|
-
);
|
|
298
|
-
this.directUpdateMode = "false";
|
|
299
|
-
this.implementation.directUpdate = false;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
} else {
|
|
303
|
-
Boolean directUpdateBool = this.getConfig().getBoolean("directUpdate", false);
|
|
304
|
-
if (directUpdateBool) {
|
|
305
|
-
this.directUpdateMode = "always"; // backward compatibility: true = always
|
|
306
|
-
this.implementation.directUpdate = true;
|
|
307
|
-
} else {
|
|
308
|
-
this.directUpdateMode = "false";
|
|
309
|
-
this.implementation.directUpdate = false;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
710
|
this.currentVersionNative = new Version(this.getConfig().getString("version", pInfo.versionName));
|
|
313
|
-
this.currentBuildVersion =
|
|
711
|
+
this.currentBuildVersion = this.getVersionCode(pInfo);
|
|
314
712
|
this.delayUpdateUtils = new DelayUpdateUtils(this.prefs, this.editor, this.currentVersionNative, logger);
|
|
315
713
|
} catch (final PackageManager.NameNotFoundException e) {
|
|
316
714
|
logger.error("Error instantiating implementation " + e.getMessage());
|
|
@@ -344,6 +742,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
344
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"
|
|
345
743
|
);
|
|
346
744
|
}
|
|
745
|
+
this.allowPreview = this.getConfig().getBoolean("allowPreview", false);
|
|
347
746
|
logger.info("appId: " + implementation.appId);
|
|
348
747
|
|
|
349
748
|
this.persistCustomId = this.getConfig().getBoolean("persistCustomId", false);
|
|
@@ -387,13 +786,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
387
786
|
this.implementation.defaultChannel = this.getConfig().getString("defaultChannel", "");
|
|
388
787
|
}
|
|
389
788
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
if (userValue >= 0 && userValue <= 600) {
|
|
393
|
-
this.periodCheckDelay = 600 * 1000;
|
|
394
|
-
} else if (userValue > 600) {
|
|
395
|
-
this.periodCheckDelay = userValue * 1000;
|
|
396
|
-
}
|
|
789
|
+
this.periodCheckDelay = normalizedPeriodCheckDelayMs(this.getConfig().getInt("periodCheckDelay", 0));
|
|
397
790
|
|
|
398
791
|
this.implementation.documentsDir = this.getContext().getFilesDir();
|
|
399
792
|
this.implementation.prefs = this.prefs;
|
|
@@ -426,7 +819,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
426
819
|
}
|
|
427
820
|
}
|
|
428
821
|
}
|
|
429
|
-
this.
|
|
822
|
+
this.configureAutoUpdateModeFromConfig();
|
|
430
823
|
this.appReadyTimeout = Math.max(1000, this.getConfig().getInt("appReadyTimeout", 10000)); // Minimum 1 second
|
|
431
824
|
this.keepUrlPathAfterReload = this.getConfig().getBoolean("keepUrlPathAfterReload", false);
|
|
432
825
|
this.syncKeepUrlPathFlag(this.keepUrlPathAfterReload);
|
|
@@ -438,14 +831,45 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
438
831
|
this.implementation.timeout = this.getConfig().getInt("responseTimeout", 20) * 1000;
|
|
439
832
|
this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
|
|
440
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
|
+
}
|
|
441
854
|
boolean resetWhenUpdate = this.getConfig().getBoolean("resetWhenUpdate", true);
|
|
442
855
|
|
|
443
856
|
// Check if app was recently installed/updated BEFORE cleanupObsoleteVersions updates LatestVersionNative
|
|
444
857
|
this.wasRecentlyInstalledOrUpdated = this.checkIfRecentlyInstalledOrUpdated();
|
|
858
|
+
final boolean nativeBuildVersionChanged = this.hasNativeBuildVersionChanged();
|
|
445
859
|
|
|
446
|
-
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();
|
|
447
869
|
if (resetWhenUpdate) {
|
|
448
870
|
this.cleanupObsoleteVersions();
|
|
871
|
+
} else {
|
|
872
|
+
this.persistCurrentNativeBuildVersion();
|
|
449
873
|
}
|
|
450
874
|
|
|
451
875
|
// Check for 'kill' delay condition on app launch
|
|
@@ -453,6 +877,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
453
877
|
this.delayUpdateUtils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.KILLED);
|
|
454
878
|
|
|
455
879
|
this.checkForUpdateAfterDelay();
|
|
880
|
+
this.showPreviewSessionNoticeIfNeeded();
|
|
881
|
+
this.syncShakeMenuLifecycle();
|
|
456
882
|
|
|
457
883
|
// On Android 14+ (API 34+), topActivity in RecentTaskInfo returns null due to
|
|
458
884
|
// security restrictions (StrandHogg task hijacking mitigations). Use ProcessLifecycleOwner
|
|
@@ -480,34 +906,82 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
480
906
|
}
|
|
481
907
|
}
|
|
482
908
|
|
|
483
|
-
private
|
|
909
|
+
private boolean semaphoreWait(final int phase, Number waitTime) {
|
|
484
910
|
try {
|
|
485
|
-
semaphoreReady.awaitAdvanceInterruptibly(
|
|
911
|
+
semaphoreReady.awaitAdvanceInterruptibly(phase, waitTime.longValue(), TimeUnit.MILLISECONDS);
|
|
486
912
|
logger.info("semaphoreReady count " + semaphoreReady.getPhase());
|
|
913
|
+
return true;
|
|
487
914
|
} catch (InterruptedException e) {
|
|
488
915
|
logger.info("semaphoreWait InterruptedException");
|
|
916
|
+
cleanupTimedOutSemaphoreWait(phase);
|
|
489
917
|
Thread.currentThread().interrupt(); // Restore interrupted status
|
|
918
|
+
return false;
|
|
490
919
|
} catch (TimeoutException e) {
|
|
491
920
|
logger.error("Semaphore timeout: " + e.getMessage());
|
|
492
|
-
|
|
921
|
+
cleanupTimedOutSemaphoreWait(phase);
|
|
922
|
+
return false;
|
|
493
923
|
}
|
|
494
924
|
}
|
|
495
925
|
|
|
496
|
-
private
|
|
926
|
+
private int semaphoreUp() {
|
|
497
927
|
logger.info("semaphoreUp");
|
|
498
|
-
semaphoreReady.register();
|
|
928
|
+
return semaphoreReady.register();
|
|
499
929
|
}
|
|
500
930
|
|
|
501
931
|
private void semaphoreDown() {
|
|
932
|
+
if (semaphoreReady.getRegisteredParties() == 0) {
|
|
933
|
+
logger.info("semaphoreDown skipped, no pending app ready wait");
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
502
936
|
logger.info("semaphoreDown");
|
|
503
937
|
logger.info("semaphoreDown count " + semaphoreReady.getPhase());
|
|
504
938
|
semaphoreReady.arriveAndDeregister();
|
|
505
939
|
}
|
|
506
940
|
|
|
941
|
+
private void cleanupTimedOutSemaphoreWait(final int phase) {
|
|
942
|
+
if (semaphoreReady.getPhase() != phase || semaphoreReady.getRegisteredParties() == 0) {
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
logger.info("Cleaning up stale app ready wait for phase " + phase);
|
|
946
|
+
semaphoreReady.arriveAndDeregister();
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
protected long getMinimumPendingBundleAppReadyTimeoutMs() {
|
|
950
|
+
return PENDING_BUNDLE_APP_READY_MIN_TIMEOUT_MS;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
private long resolveAppReadyCheckTimeoutMs() {
|
|
954
|
+
long configuredTimeoutMs = this.appReadyTimeout.longValue();
|
|
955
|
+
try {
|
|
956
|
+
if (this.implementation == null) {
|
|
957
|
+
return configuredTimeoutMs;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
final BundleInfo current = this.implementation.getCurrentBundle();
|
|
961
|
+
if (current == null || BundleStatus.SUCCESS == current.getStatus()) {
|
|
962
|
+
return configuredTimeoutMs;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
return Math.max(configuredTimeoutMs, this.getMinimumPendingBundleAppReadyTimeoutMs());
|
|
966
|
+
} catch (final Exception e) {
|
|
967
|
+
logger.warn("Falling back to configured appReadyTimeout: " + e.getMessage());
|
|
968
|
+
return configuredTimeoutMs;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
507
972
|
private void sendReadyToJs(final BundleInfo current, final String msg) {
|
|
508
973
|
sendReadyToJs(current, msg, false);
|
|
509
974
|
}
|
|
510
975
|
|
|
976
|
+
private void notifyBundleSet(final BundleInfo bundle) {
|
|
977
|
+
if (bundle == null) {
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
final JSObject ret = new JSObject();
|
|
981
|
+
ret.put("bundle", InternalUtils.mapToJSObject(bundle.toJSONMap()));
|
|
982
|
+
this.notifyListeners("set", ret, true);
|
|
983
|
+
}
|
|
984
|
+
|
|
511
985
|
private void sendReadyToJs(final BundleInfo current, final String msg, final boolean isDirectUpdate) {
|
|
512
986
|
logger.info("sendReadyToJs: " + msg);
|
|
513
987
|
final JSObject ret = new JSObject();
|
|
@@ -522,6 +996,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
522
996
|
if (this.autoSplashscreen) {
|
|
523
997
|
this.hideSplashscreen();
|
|
524
998
|
}
|
|
999
|
+
this.hidePreviewTransitionLoader("app-ready");
|
|
525
1000
|
}
|
|
526
1001
|
|
|
527
1002
|
private void hideSplashscreen() {
|
|
@@ -535,47 +1010,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
535
1010
|
private void hideSplashscreenInternal() {
|
|
536
1011
|
cancelSplashscreenTimeout();
|
|
537
1012
|
removeSplashscreenLoader();
|
|
538
|
-
|
|
539
|
-
try {
|
|
540
|
-
if (getBridge() == null) {
|
|
541
|
-
logger.warn("Bridge not ready for hiding splashscreen with autoSplashscreen");
|
|
542
|
-
return;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// Try to call the SplashScreen plugin directly through the bridge
|
|
546
|
-
PluginHandle splashScreenPlugin = getBridge().getPlugin("SplashScreen");
|
|
547
|
-
if (splashScreenPlugin != null) {
|
|
548
|
-
try {
|
|
549
|
-
// Create a plugin call for the hide method using reflection to access private msgHandler
|
|
550
|
-
JSObject options = new JSObject();
|
|
551
|
-
java.lang.reflect.Field msgHandlerField = getBridge().getClass().getDeclaredField("msgHandler");
|
|
552
|
-
msgHandlerField.setAccessible(true);
|
|
553
|
-
Object msgHandler = msgHandlerField.get(getBridge());
|
|
554
|
-
|
|
555
|
-
PluginCall call = new PluginCall(
|
|
556
|
-
(com.getcapacitor.MessageHandler) msgHandler,
|
|
557
|
-
"SplashScreen",
|
|
558
|
-
"FAKE_CALLBACK_ID_HIDE",
|
|
559
|
-
"hide",
|
|
560
|
-
options
|
|
561
|
-
);
|
|
562
|
-
|
|
563
|
-
// Call the hide method directly
|
|
564
|
-
splashScreenPlugin.invoke("hide", call);
|
|
565
|
-
logger.info("Splashscreen hidden automatically via direct plugin call");
|
|
566
|
-
} catch (Exception e) {
|
|
567
|
-
logger.error("Failed to call SplashScreen hide method: " + e.getMessage());
|
|
568
|
-
}
|
|
569
|
-
} else {
|
|
570
|
-
logger.warn("autoSplashscreen: SplashScreen plugin not found. Install @capacitor/splash-screen plugin.");
|
|
571
|
-
}
|
|
572
|
-
} catch (Exception e) {
|
|
573
|
-
logger.error(
|
|
574
|
-
"Error hiding splashscreen with autoSplashscreen: " +
|
|
575
|
-
e.getMessage() +
|
|
576
|
-
". Make sure @capacitor/splash-screen plugin is installed and configured."
|
|
577
|
-
);
|
|
578
|
-
}
|
|
1013
|
+
invokeSplashScreenPluginMethod("hide", new JSObject(), SPLASH_SCREEN_MAX_RETRIES, ++this.splashscreenInvocationToken);
|
|
579
1014
|
}
|
|
580
1015
|
|
|
581
1016
|
private void showSplashscreen() {
|
|
@@ -590,75 +1025,139 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
590
1025
|
cancelSplashscreenTimeout();
|
|
591
1026
|
this.autoSplashscreenTimedOut = false;
|
|
592
1027
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
} else {
|
|
597
|
-
PluginHandle splashScreenPlugin = getBridge().getPlugin("SplashScreen");
|
|
598
|
-
if (splashScreenPlugin != null) {
|
|
599
|
-
JSObject options = new JSObject();
|
|
600
|
-
java.lang.reflect.Field msgHandlerField = getBridge().getClass().getDeclaredField("msgHandler");
|
|
601
|
-
msgHandlerField.setAccessible(true);
|
|
602
|
-
Object msgHandler = msgHandlerField.get(getBridge());
|
|
603
|
-
|
|
604
|
-
PluginCall call = new PluginCall(
|
|
605
|
-
(com.getcapacitor.MessageHandler) msgHandler,
|
|
606
|
-
"SplashScreen",
|
|
607
|
-
"FAKE_CALLBACK_ID_SHOW",
|
|
608
|
-
"show",
|
|
609
|
-
options
|
|
610
|
-
);
|
|
611
|
-
|
|
612
|
-
splashScreenPlugin.invoke("show", call);
|
|
613
|
-
logger.info("Splashscreen shown synchronously to prevent flash");
|
|
614
|
-
} else {
|
|
615
|
-
logger.warn("autoSplashscreen: SplashScreen plugin not found");
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
} catch (Exception e) {
|
|
619
|
-
logger.error("Failed to show splashscreen synchronously: " + e.getMessage());
|
|
620
|
-
}
|
|
1028
|
+
final JSObject options = new JSObject();
|
|
1029
|
+
options.put("autoHide", false);
|
|
1030
|
+
invokeSplashScreenPluginMethod("show", options, SPLASH_SCREEN_MAX_RETRIES, ++this.splashscreenInvocationToken);
|
|
621
1031
|
|
|
622
1032
|
addSplashscreenLoaderIfNeeded();
|
|
623
1033
|
scheduleSplashscreenTimeout();
|
|
624
1034
|
}
|
|
625
1035
|
|
|
626
|
-
private void
|
|
627
|
-
|
|
1036
|
+
private void invokeSplashScreenPluginMethod(
|
|
1037
|
+
final String methodName,
|
|
1038
|
+
final JSObject options,
|
|
1039
|
+
final int retriesRemaining,
|
|
1040
|
+
final int requestToken
|
|
1041
|
+
) {
|
|
1042
|
+
if (requestToken != this.splashscreenInvocationToken) {
|
|
628
1043
|
return;
|
|
629
1044
|
}
|
|
630
1045
|
|
|
631
|
-
|
|
632
|
-
|
|
1046
|
+
try {
|
|
1047
|
+
final Bridge bridge = getBridge();
|
|
1048
|
+
if (bridge == null) {
|
|
1049
|
+
retrySplashScreenInvocation(
|
|
1050
|
+
methodName,
|
|
1051
|
+
options,
|
|
1052
|
+
retriesRemaining,
|
|
1053
|
+
requestToken,
|
|
1054
|
+
"Bridge not ready for " + ("show".equals(methodName) ? "showing" : "hiding") + " splashscreen"
|
|
1055
|
+
);
|
|
633
1056
|
return;
|
|
634
1057
|
}
|
|
635
1058
|
|
|
636
|
-
|
|
637
|
-
if (
|
|
638
|
-
|
|
1059
|
+
final PluginHandle splashScreenPlugin = bridge.getPlugin(SPLASH_SCREEN_PLUGIN_ID);
|
|
1060
|
+
if (splashScreenPlugin == null) {
|
|
1061
|
+
retrySplashScreenInvocation(
|
|
1062
|
+
methodName,
|
|
1063
|
+
options,
|
|
1064
|
+
retriesRemaining,
|
|
1065
|
+
requestToken,
|
|
1066
|
+
"autoSplashscreen: SplashScreen plugin not found. Install @capacitor/splash-screen plugin."
|
|
1067
|
+
);
|
|
639
1068
|
return;
|
|
640
1069
|
}
|
|
641
1070
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
1071
|
+
splashScreenPlugin.invoke(methodName, new FireAndForgetPluginCall(methodName, options));
|
|
1072
|
+
logger.info("Splashscreen " + methodName + " invoked automatically");
|
|
1073
|
+
} catch (final Exception e) {
|
|
1074
|
+
retrySplashScreenInvocation(
|
|
1075
|
+
methodName,
|
|
1076
|
+
options,
|
|
1077
|
+
retriesRemaining,
|
|
1078
|
+
requestToken,
|
|
1079
|
+
"Failed to call SplashScreen " + methodName + " method: " + e.getMessage()
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
651
1083
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
1084
|
+
private void retrySplashScreenInvocation(
|
|
1085
|
+
final String methodName,
|
|
1086
|
+
final JSObject options,
|
|
1087
|
+
final int retriesRemaining,
|
|
1088
|
+
final int requestToken,
|
|
1089
|
+
final String message
|
|
1090
|
+
) {
|
|
1091
|
+
if (retriesRemaining > 0) {
|
|
1092
|
+
logger.info(message + ". Retrying.");
|
|
1093
|
+
this.mainHandler.postDelayed(
|
|
1094
|
+
() -> invokeSplashScreenPluginMethod(methodName, options, retriesRemaining - 1, requestToken),
|
|
1095
|
+
SPLASH_SCREEN_RETRY_DELAY_MS
|
|
655
1096
|
);
|
|
656
|
-
|
|
657
|
-
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
658
1099
|
|
|
659
|
-
|
|
660
|
-
|
|
1100
|
+
if ("show".equals(methodName)) {
|
|
1101
|
+
logger.warn(message);
|
|
1102
|
+
} else {
|
|
1103
|
+
logger.error(message);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
boolean isCurrentSplashscreenInvocationTokenForTesting(final int requestToken) {
|
|
1108
|
+
return requestToken == this.splashscreenInvocationToken;
|
|
1109
|
+
}
|
|
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
|
+
|
|
1143
|
+
private void addSplashscreenLoaderIfNeeded() {
|
|
1144
|
+
if (!Boolean.TRUE.equals(this.autoSplashscreenLoader)) {
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
Runnable addLoader = () -> {
|
|
1149
|
+
if (this.splashscreenLoaderOverlay != null) {
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
Activity activity = getActivity();
|
|
1154
|
+
if (activity == null) {
|
|
1155
|
+
logger.warn("autoSplashscreen: Activity not available for loader overlay");
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
661
1158
|
|
|
1159
|
+
FrameLayout overlay = createLoaderOverlay(activity, false, Color.TRANSPARENT);
|
|
1160
|
+
attachLoaderOverlay(activity, overlay);
|
|
662
1161
|
this.splashscreenLoaderOverlay = overlay;
|
|
663
1162
|
};
|
|
664
1163
|
|
|
@@ -672,10 +1171,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
672
1171
|
private void removeSplashscreenLoader() {
|
|
673
1172
|
Runnable removeLoader = () -> {
|
|
674
1173
|
if (this.splashscreenLoaderOverlay != null) {
|
|
675
|
-
|
|
676
|
-
if (parent != null) {
|
|
677
|
-
parent.removeView(this.splashscreenLoaderOverlay);
|
|
678
|
-
}
|
|
1174
|
+
removeLoaderOverlay(this.splashscreenLoaderOverlay);
|
|
679
1175
|
this.splashscreenLoaderOverlay = null;
|
|
680
1176
|
}
|
|
681
1177
|
};
|
|
@@ -687,6 +1183,84 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
687
1183
|
}
|
|
688
1184
|
}
|
|
689
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
|
+
|
|
690
1264
|
private void scheduleSplashscreenTimeout() {
|
|
691
1265
|
if (this.autoSplashscreenTimeout == null || this.autoSplashscreenTimeout <= 0) {
|
|
692
1266
|
return;
|
|
@@ -713,7 +1287,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
713
1287
|
|
|
714
1288
|
private boolean checkIfRecentlyInstalledOrUpdated() {
|
|
715
1289
|
String currentVersion = this.currentBuildVersion;
|
|
716
|
-
String lastKnownVersion = this.
|
|
1290
|
+
String lastKnownVersion = this.getStoredNativeBuildVersion();
|
|
717
1291
|
|
|
718
1292
|
if (lastKnownVersion.isEmpty()) {
|
|
719
1293
|
// First time running, consider it as recently installed
|
|
@@ -726,7 +1300,524 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
726
1300
|
return false;
|
|
727
1301
|
}
|
|
728
1302
|
|
|
1303
|
+
private boolean hasNativeBuildVersionChanged() {
|
|
1304
|
+
final String lastKnownVersion = this.getStoredNativeBuildVersion();
|
|
1305
|
+
return !lastKnownVersion.isEmpty() && !lastKnownVersion.equals(this.currentBuildVersion);
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
void reportNativeVersionStatsIfChanged() {
|
|
1309
|
+
if (this.implementation == null || this.prefs == null || this.editor == null) {
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
this.reportNativeVersionStatsIfChanged(
|
|
1314
|
+
this.implementation.versionBuild,
|
|
1315
|
+
this.implementation.versionCode,
|
|
1316
|
+
this.implementation.versionOs
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
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) {
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
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
|
+
}
|
|
1353
|
+
|
|
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
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
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();
|
|
1395
|
+
}
|
|
1396
|
+
|
|
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();
|
|
1405
|
+
}
|
|
1406
|
+
|
|
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
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
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
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
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
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
static String truncateStatsMetadataValue(final String value, final int maxLength) {
|
|
1474
|
+
return value.length() <= maxLength ? value : value.substring(0, maxLength);
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
private void installWebViewStatsReporter() {
|
|
1478
|
+
if (this.bridge == null || this.bridge.getWebView() == null || this.webViewStatsListener != null) {
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
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
|
+
@Override
|
|
1498
|
+
public boolean onRenderProcessGone(final android.webkit.WebView view, final RenderProcessGoneDetail detail) {
|
|
1499
|
+
final Map<String, String> metadata = CapacitorUpdaterPlugin.this.buildWebViewRenderProcessGoneMetadata(detail);
|
|
1500
|
+
CapacitorUpdaterPlugin.this.persistPendingWebViewRenderProcessGone(metadata);
|
|
1501
|
+
return false;
|
|
1502
|
+
}
|
|
1503
|
+
};
|
|
1504
|
+
|
|
1505
|
+
this.bridge.addWebViewListener(this.webViewStatsListener);
|
|
1506
|
+
this.evaluateWebViewStatsReporterScript(webView, script);
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
private void installDocumentStartWebViewStatsReporter(final android.webkit.WebView webView, final String script) {
|
|
1510
|
+
try {
|
|
1511
|
+
final Class<?> webViewFeature = Class.forName("androidx.webkit.WebViewFeature");
|
|
1512
|
+
final String feature = (String) webViewFeature.getField("DOCUMENT_START_SCRIPT").get(null);
|
|
1513
|
+
final Boolean supported = (Boolean) webViewFeature.getMethod("isFeatureSupported", String.class).invoke(null, feature);
|
|
1514
|
+
if (!Boolean.TRUE.equals(supported)) {
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
final String allowedOrigin = Uri.parse(this.bridge.getAppUrl())
|
|
1519
|
+
.buildUpon()
|
|
1520
|
+
.path(null)
|
|
1521
|
+
.fragment(null)
|
|
1522
|
+
.clearQuery()
|
|
1523
|
+
.build()
|
|
1524
|
+
.toString();
|
|
1525
|
+
final Class<?> webViewCompat = Class.forName("androidx.webkit.WebViewCompat");
|
|
1526
|
+
webViewCompat
|
|
1527
|
+
.getMethod("addDocumentStartJavaScript", android.webkit.WebView.class, String.class, Set.class)
|
|
1528
|
+
.invoke(null, webView, script, java.util.Collections.singleton(allowedOrigin));
|
|
1529
|
+
} catch (final Exception e) {
|
|
1530
|
+
logger.debug("Unable to install document-start WebView stats reporter: " + e.getMessage());
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
private void evaluateWebViewStatsReporterScript(final android.webkit.WebView webView, final String script) {
|
|
1535
|
+
if (webView == null) {
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
this.mainHandler.post(() -> {
|
|
1540
|
+
try {
|
|
1541
|
+
webView.evaluateJavascript(script, null);
|
|
1542
|
+
} catch (final Exception e) {
|
|
1543
|
+
logger.debug("Unable to evaluate WebView stats reporter: " + e.getMessage());
|
|
1544
|
+
}
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
private Map<String, String> buildWebViewRenderProcessGoneMetadata(final RenderProcessGoneDetail detail) {
|
|
1549
|
+
final Map<String, String> metadata = new HashMap<>();
|
|
1550
|
+
metadata.put("error_type", "render_process_gone");
|
|
1551
|
+
metadata.put("source", "android_on_render_process_gone");
|
|
1552
|
+
metadata.put("timestamp", Long.toString(System.currentTimeMillis()));
|
|
1553
|
+
if (detail != null) {
|
|
1554
|
+
metadata.put("did_crash", Boolean.toString(detail.didCrash()));
|
|
1555
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
1556
|
+
metadata.put("renderer_priority_at_exit", Integer.toString(detail.rendererPriorityAtExit()));
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
return metadata;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
private void persistPendingWebViewRenderProcessGone(final Map<String, String> metadata) {
|
|
1563
|
+
try {
|
|
1564
|
+
final JSONObject json = new JSONObject(metadata);
|
|
1565
|
+
this.prefs.edit().putString(LAST_WEBVIEW_RENDER_PROCESS_GONE_PREF_KEY, json.toString()).commit();
|
|
1566
|
+
} catch (final Exception e) {
|
|
1567
|
+
logger.debug("Unable to persist WebView render process crash metadata: " + e.getMessage());
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
private void reportPreviousWebViewRenderProcessGone() {
|
|
1572
|
+
if (this.implementation == null || this.implementation.statsUrl.isEmpty()) {
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
final String rawMetadata = this.prefs.getString(LAST_WEBVIEW_RENDER_PROCESS_GONE_PREF_KEY, "");
|
|
1577
|
+
if (rawMetadata == null || rawMetadata.isEmpty()) {
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
try {
|
|
1582
|
+
final Map<String, String> metadata = jsonObjectToStringMap(new JSONObject(rawMetadata));
|
|
1583
|
+
metadata.put("reported_after_restart", "true");
|
|
1584
|
+
this.reportWebViewStats("webview_render_process_gone", metadata);
|
|
1585
|
+
this.prefs.edit().remove(LAST_WEBVIEW_RENDER_PROCESS_GONE_PREF_KEY).apply();
|
|
1586
|
+
} catch (final JSONException e) {
|
|
1587
|
+
this.prefs.edit().remove(LAST_WEBVIEW_RENDER_PROCESS_GONE_PREF_KEY).apply();
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
private static Map<String, String> jsonObjectToStringMap(final JSONObject json) throws JSONException {
|
|
1592
|
+
final Map<String, String> map = new HashMap<>();
|
|
1593
|
+
final JSONArray names = json.names();
|
|
1594
|
+
if (names == null) {
|
|
1595
|
+
return map;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
for (int i = 0; i < names.length(); i++) {
|
|
1599
|
+
final String key = names.getString(i);
|
|
1600
|
+
final String value = json.optString(key, "");
|
|
1601
|
+
if (!value.isEmpty()) {
|
|
1602
|
+
map.put(key, value);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
return map;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
private static JSObject jsonObjectToJSObject(final JSONObject json) throws JSONException {
|
|
1609
|
+
final JSObject ret = new JSObject();
|
|
1610
|
+
final JSONArray names = json.names();
|
|
1611
|
+
if (names == null) {
|
|
1612
|
+
return ret;
|
|
1613
|
+
}
|
|
1614
|
+
for (int i = 0; i < names.length(); i++) {
|
|
1615
|
+
final String key = names.getString(i);
|
|
1616
|
+
ret.put(key, json.get(key));
|
|
1617
|
+
}
|
|
1618
|
+
return ret;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
@PluginMethod
|
|
1622
|
+
public void reportWebViewError(final PluginCall call) {
|
|
1623
|
+
final JSObject data = call.getData();
|
|
1624
|
+
this.reportWebViewStats(
|
|
1625
|
+
statsActionForWebViewErrorType(data.optString("type", "javascript_error")),
|
|
1626
|
+
buildWebViewErrorMetadata(data)
|
|
1627
|
+
);
|
|
1628
|
+
call.resolve();
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
private void reportWebViewStats(final String action, final Map<String, String> metadata) {
|
|
1632
|
+
if (this.implementation == null) {
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
final BundleInfo current = this.implementation.getCurrentBundle();
|
|
1637
|
+
final String versionName = current == null ? "" : current.getVersionName();
|
|
1638
|
+
this.implementation.sendStats(action, versionName, "", metadata);
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
static String statsActionForWebViewErrorType(final String type) {
|
|
1642
|
+
switch (type) {
|
|
1643
|
+
case "unhandled_rejection":
|
|
1644
|
+
return "webview_unhandled_rejection";
|
|
1645
|
+
case "resource_error":
|
|
1646
|
+
return "webview_resource_error";
|
|
1647
|
+
case "security_policy_violation":
|
|
1648
|
+
return "webview_security_policy_violation";
|
|
1649
|
+
case "webview_unclean_restart":
|
|
1650
|
+
return "webview_unclean_restart";
|
|
1651
|
+
case "render_process_gone":
|
|
1652
|
+
return "webview_render_process_gone";
|
|
1653
|
+
case "web_content_process_terminated":
|
|
1654
|
+
return "webview_content_process_terminated";
|
|
1655
|
+
case "javascript_error":
|
|
1656
|
+
default:
|
|
1657
|
+
return "webview_javascript_error";
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
static Map<String, String> buildWebViewErrorMetadata(final JSObject data) {
|
|
1662
|
+
final Map<String, String> metadata = new HashMap<>();
|
|
1663
|
+
putStatsMetadataValue(metadata, "error_type", data.optString("type", "javascript_error"), 64);
|
|
1664
|
+
putStatsMetadataValue(metadata, "message", data.optString("message", ""), 1024);
|
|
1665
|
+
putStatsMetadataValue(metadata, "source", sanitizeStatsMetadataUrl(data.optString("source", "")), 512);
|
|
1666
|
+
putStatsMetadataValue(metadata, "line", data.optString("line", data.optString("lineno", "")), 32);
|
|
1667
|
+
putStatsMetadataValue(metadata, "column", data.optString("column", data.optString("colno", "")), 32);
|
|
1668
|
+
putStatsMetadataValue(metadata, "stack", data.optString("stack", ""), 2048);
|
|
1669
|
+
putStatsMetadataValue(metadata, "tag_name", data.optString("tag_name", ""), 64);
|
|
1670
|
+
putStatsMetadataValue(metadata, "href", sanitizeStatsMetadataUrl(data.optString("href", "")), 512);
|
|
1671
|
+
putStatsMetadataValue(metadata, "user_agent", data.optString("user_agent", ""), 256);
|
|
1672
|
+
putStatsMetadataValue(metadata, "session_id", data.optString("session_id", ""), 128);
|
|
1673
|
+
putStatsMetadataValue(metadata, "previous_session_id", data.optString("previous_session_id", ""), 128);
|
|
1674
|
+
putStatsMetadataValue(metadata, "previous_href", sanitizeStatsMetadataUrl(data.optString("previous_href", "")), 512);
|
|
1675
|
+
putStatsMetadataValue(metadata, "previous_started_at", data.optString("previous_started_at", ""), 64);
|
|
1676
|
+
putStatsMetadataValue(metadata, "previous_updated_at", data.optString("previous_updated_at", ""), 64);
|
|
1677
|
+
return metadata;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
private static void putStatsMetadataValue(
|
|
1681
|
+
final Map<String, String> metadata,
|
|
1682
|
+
final String key,
|
|
1683
|
+
final String value,
|
|
1684
|
+
final int maxLength
|
|
1685
|
+
) {
|
|
1686
|
+
if (value == null || value.isEmpty()) {
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
metadata.put(key, truncateStatsMetadataValue(value, maxLength));
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
static String sanitizeStatsMetadataUrl(final String value) {
|
|
1694
|
+
if (value == null || value.isEmpty()) {
|
|
1695
|
+
return "";
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
try {
|
|
1699
|
+
final java.net.URI uri = new java.net.URI(value);
|
|
1700
|
+
if (uri.getScheme() != null && uri.getHost() != null) {
|
|
1701
|
+
final String path = sanitizeStatsMetadataUrlPath(uri.getPath());
|
|
1702
|
+
return new java.net.URI(
|
|
1703
|
+
uri.getScheme(),
|
|
1704
|
+
null,
|
|
1705
|
+
uri.getHost(),
|
|
1706
|
+
uri.getPort(),
|
|
1707
|
+
path.isEmpty() ? null : path,
|
|
1708
|
+
null,
|
|
1709
|
+
null
|
|
1710
|
+
).toString();
|
|
1711
|
+
}
|
|
1712
|
+
} catch (Exception ignored) {}
|
|
1713
|
+
|
|
1714
|
+
try {
|
|
1715
|
+
final Uri uri = Uri.parse(value);
|
|
1716
|
+
if (uri.getScheme() != null && uri.getHost() != null) {
|
|
1717
|
+
final String host = stripUrlUserInfo(uri.getHost());
|
|
1718
|
+
if (host.isEmpty()) {
|
|
1719
|
+
return stripUrlQueryAndFragment(value);
|
|
1720
|
+
}
|
|
1721
|
+
final StringBuilder authority = new StringBuilder(host);
|
|
1722
|
+
if (uri.getPort() != -1) {
|
|
1723
|
+
authority.append(':').append(uri.getPort());
|
|
1724
|
+
}
|
|
1725
|
+
final Uri.Builder builder = new Uri.Builder().scheme(uri.getScheme()).authority(authority.toString());
|
|
1726
|
+
final String path = sanitizeStatsMetadataUrlPath(uri.getPath());
|
|
1727
|
+
if (!path.isEmpty()) {
|
|
1728
|
+
builder.path(path);
|
|
1729
|
+
}
|
|
1730
|
+
return builder.build().toString();
|
|
1731
|
+
}
|
|
1732
|
+
} catch (Exception ignored) {}
|
|
1733
|
+
|
|
1734
|
+
return stripUrlQueryAndFragment(value);
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
private static String sanitizeStatsMetadataUrlPath(final String path) {
|
|
1738
|
+
if (path == null || path.isEmpty()) {
|
|
1739
|
+
return "";
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
final String[] segments = path.split("/", -1);
|
|
1743
|
+
for (int index = 0; index < segments.length; index++) {
|
|
1744
|
+
if (isSensitiveUrlPathSegment(segments[index])) {
|
|
1745
|
+
segments[index] = "redacted";
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
return String.join("/", segments);
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
private static String stripUrlUserInfo(final String host) {
|
|
1752
|
+
if (host == null || host.isEmpty()) {
|
|
1753
|
+
return "";
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
final int userInfoIndex = host.lastIndexOf('@');
|
|
1757
|
+
if (userInfoIndex < 0) {
|
|
1758
|
+
return host;
|
|
1759
|
+
}
|
|
1760
|
+
return host.substring(userInfoIndex + 1);
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
private static boolean isSensitiveUrlPathSegment(final String segment) {
|
|
1764
|
+
return (
|
|
1765
|
+
segment.matches("[0-9]{6,}") ||
|
|
1766
|
+
segment.matches("[0-9a-fA-F]{16,}") ||
|
|
1767
|
+
segment.matches("[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}")
|
|
1768
|
+
);
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
private static String stripUrlQueryAndFragment(final String value) {
|
|
1772
|
+
int end = value.length();
|
|
1773
|
+
final int queryIndex = value.indexOf('?');
|
|
1774
|
+
final int fragmentIndex = value.indexOf('#');
|
|
1775
|
+
if (queryIndex >= 0) {
|
|
1776
|
+
end = Math.min(end, queryIndex);
|
|
1777
|
+
}
|
|
1778
|
+
if (fragmentIndex >= 0) {
|
|
1779
|
+
end = Math.min(end, fragmentIndex);
|
|
1780
|
+
}
|
|
1781
|
+
return value.substring(0, end);
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
static String buildWebViewStatsReporterScript() {
|
|
1785
|
+
return (
|
|
1786
|
+
"(function(){" +
|
|
1787
|
+
"if(window.__capgoWebViewErrorReporterInstalled){return;}" +
|
|
1788
|
+
"window.__capgoWebViewErrorReporterInstalled=true;" +
|
|
1789
|
+
"var maxReports=20,sentReports=0,queue=[],seen={};" +
|
|
1790
|
+
"var sessionKey='CapacitorUpdater.webViewSession';" +
|
|
1791
|
+
"var sessionId=String(Date.now())+'-'+Math.random().toString(36).slice(2);" +
|
|
1792
|
+
"function s(value){try{if(value===undefined){return '';}if(value===null){return 'null';}if(typeof value==='string'){return value;}if(value&&typeof value.message==='string'){return value.message;}return String(value);}catch(_){return '';}}" +
|
|
1793
|
+
"function stack(value){try{return value&&value.stack?String(value.stack):'';}catch(_){return '';}}" +
|
|
1794
|
+
"function updater(){var cap=window.Capacitor;if(!cap||!cap.Plugins){return null;}return cap.Plugins.CapacitorUpdater||null;}" +
|
|
1795
|
+
"function flush(){var plugin=updater();if(!plugin||typeof plugin.reportWebViewError!=='function'){return false;}while(queue.length){var payload=queue.shift();try{var result=plugin.reportWebViewError(payload);if(result&&typeof result.catch==='function'){result.catch(function(){});}}catch(_){}}return true;}" +
|
|
1796
|
+
"var retries=0;function scheduleFlush(){if(flush()){return;}if(retries++<40){setTimeout(scheduleFlush,250);}}" +
|
|
1797
|
+
"function send(payload){try{if(sentReports>=maxReports){return;}payload.href=payload.href||location.href||'';payload.user_agent=navigator.userAgent||'';payload.session_id=sessionId;var key=[payload.type,payload.message,payload.source,payload.line,payload.column,payload.tag_name].join('|');if(seen[key]){return;}seen[key]=true;sentReports+=1;queue.push(payload);scheduleFlush();}catch(_){}}" +
|
|
1798
|
+
"function readSession(){try{return JSON.parse(localStorage.getItem(sessionKey)||'null')||null;}catch(_){return null;}}" +
|
|
1799
|
+
"function writeSession(active){try{localStorage.setItem(sessionKey,JSON.stringify({id:sessionId,active:active,href:location.href||'',started_at:window.__capgoWebViewSessionStartedAt,updated_at:String(Date.now())}));}catch(_){}}" +
|
|
1800
|
+
"window.__capgoWebViewSessionStartedAt=String(Date.now());" +
|
|
1801
|
+
"var previous=readSession();" +
|
|
1802
|
+
"if(previous&&previous.active){send({type:'webview_unclean_restart',message:'WebView restarted without a clean page unload',previous_session_id:s(previous.id),previous_href:s(previous.href),previous_started_at:s(previous.started_at),previous_updated_at:s(previous.updated_at)});}" +
|
|
1803
|
+
"writeSession(true);" +
|
|
1804
|
+
"setInterval(function(){writeSession(true);},15000);" +
|
|
1805
|
+
"function markClean(){writeSession(false);}" +
|
|
1806
|
+
"window.addEventListener('pagehide',markClean,true);" +
|
|
1807
|
+
"window.addEventListener('beforeunload',markClean,true);" +
|
|
1808
|
+
"window.addEventListener('error',function(event){var target=event&&event.target;if(target&&target!==window&&(target.src||target.href)){send({type:'resource_error',message:'Resource failed to load',source:s(target.src||target.href),tag_name:s(target.tagName)});return;}send({type:'javascript_error',message:s((event&&event.message)||(event&&event.error)),source:s(event&&event.filename),line:s(event&&event.lineno),column:s(event&&event.colno),stack:stack(event&&event.error)});},true);" +
|
|
1809
|
+
"window.addEventListener('unhandledrejection',function(event){var reason=event&&event.reason;send({type:'unhandled_rejection',message:s(reason),stack:stack(reason)});},true);" +
|
|
1810
|
+
"document.addEventListener('securitypolicyviolation',function(event){send({type:'security_policy_violation',message:s(event&&event.violatedDirective),source:s(event&&event.blockedURI)});},true);" +
|
|
1811
|
+
"document.addEventListener('deviceready',scheduleFlush,false);" +
|
|
1812
|
+
"setTimeout(scheduleFlush,0);" +
|
|
1813
|
+
"})();"
|
|
1814
|
+
);
|
|
1815
|
+
}
|
|
1816
|
+
|
|
729
1817
|
private boolean shouldUseDirectUpdate() {
|
|
1818
|
+
if (!Boolean.TRUE.equals(this.autoUpdate) || AUTO_UPDATE_MODE_ONLY_DOWNLOAD.equals(this.autoUpdateMode)) {
|
|
1819
|
+
return false;
|
|
1820
|
+
}
|
|
730
1821
|
if (Boolean.TRUE.equals(this.autoSplashscreenTimedOut)) {
|
|
731
1822
|
return false;
|
|
732
1823
|
}
|
|
@@ -757,25 +1848,291 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
757
1848
|
}
|
|
758
1849
|
}
|
|
759
1850
|
|
|
760
|
-
private
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
1851
|
+
private void configureAutoUpdateModeFromConfig() {
|
|
1852
|
+
final String configuredMode = this.getConfig().getString("autoUpdate", null);
|
|
1853
|
+
if (configuredMode != null && !configuredMode.isEmpty() && !"true".equals(configuredMode) && !"false".equals(configuredMode)) {
|
|
1854
|
+
this.autoUpdateMode = normalizedAutoUpdateMode(configuredMode);
|
|
1855
|
+
if (!this.autoUpdateMode.equals(configuredMode)) {
|
|
1856
|
+
logger.error(
|
|
1857
|
+
"Invalid autoUpdate value: \"" +
|
|
1858
|
+
configuredMode +
|
|
1859
|
+
"\". Supported values are: true, false, \"off\", \"atBackground\", \"atInstall\", \"onLaunch\", \"always\", \"onlyDownload\". Defaulting to \"atBackground\"."
|
|
1860
|
+
);
|
|
1861
|
+
}
|
|
1862
|
+
} else {
|
|
1863
|
+
final boolean enabled =
|
|
1864
|
+
configuredMode != null
|
|
1865
|
+
? "true".equals(configuredMode)
|
|
1866
|
+
: Boolean.TRUE.equals(this.getConfig().getBoolean("autoUpdate", true));
|
|
1867
|
+
this.autoUpdateMode = enabled
|
|
1868
|
+
? autoUpdateModeForLegacyDirectUpdateMode(this.resolveLegacyDirectUpdateModeFromConfig())
|
|
1869
|
+
: AUTO_UPDATE_MODE_OFF;
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
this.autoUpdate = isAutoUpdateModeEnabled(this.autoUpdateMode);
|
|
1873
|
+
this.directUpdateMode = directUpdateModeForAutoUpdateMode(this.autoUpdateMode);
|
|
1874
|
+
this.implementation.directUpdate = isDirectUpdateMode(this.directUpdateMode);
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
private String resolveLegacyDirectUpdateModeFromConfig() {
|
|
1878
|
+
final String directUpdateConfig = this.getConfig().getString("directUpdate", null);
|
|
1879
|
+
if (directUpdateConfig != null) {
|
|
1880
|
+
if ("true".equals(directUpdateConfig)) {
|
|
1881
|
+
return AUTO_UPDATE_MODE_ALWAYS;
|
|
1882
|
+
}
|
|
1883
|
+
if ("false".equals(directUpdateConfig) || isDirectUpdateMode(directUpdateConfig)) {
|
|
1884
|
+
return directUpdateConfig;
|
|
1885
|
+
}
|
|
1886
|
+
logger.error(
|
|
1887
|
+
"Invalid directUpdate value: \"" +
|
|
1888
|
+
directUpdateConfig +
|
|
1889
|
+
"\". Supported values are: false, true, \"always\", \"atInstall\", \"onLaunch\". Defaulting to \"false\"."
|
|
1890
|
+
);
|
|
1891
|
+
return "false";
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
return Boolean.TRUE.equals(this.getConfig().getBoolean("directUpdate", false)) ? AUTO_UPDATE_MODE_ALWAYS : "false";
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
static String normalizedAutoUpdateMode(final String value) {
|
|
1898
|
+
if (value == null) {
|
|
1899
|
+
return AUTO_UPDATE_MODE_BACKGROUND;
|
|
1900
|
+
}
|
|
1901
|
+
switch (value) {
|
|
1902
|
+
case "false":
|
|
1903
|
+
case AUTO_UPDATE_MODE_OFF:
|
|
1904
|
+
return AUTO_UPDATE_MODE_OFF;
|
|
1905
|
+
case "true":
|
|
1906
|
+
case AUTO_UPDATE_MODE_BACKGROUND:
|
|
1907
|
+
return AUTO_UPDATE_MODE_BACKGROUND;
|
|
1908
|
+
case AUTO_UPDATE_MODE_INSTALL:
|
|
1909
|
+
case AUTO_UPDATE_MODE_LAUNCH:
|
|
1910
|
+
case AUTO_UPDATE_MODE_ALWAYS:
|
|
1911
|
+
case AUTO_UPDATE_MODE_ONLY_DOWNLOAD:
|
|
1912
|
+
return value;
|
|
1913
|
+
default:
|
|
1914
|
+
return AUTO_UPDATE_MODE_BACKGROUND;
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
static String normalizedShakeMenuGesture(final String value) {
|
|
1919
|
+
if (value == null || value.trim().isEmpty()) {
|
|
1920
|
+
return SHAKE_MENU_GESTURE_SHAKE;
|
|
1921
|
+
}
|
|
1922
|
+
final String normalized = value.trim();
|
|
1923
|
+
if (SHAKE_MENU_GESTURE_THREE_FINGER_PINCH.equals(normalized)) {
|
|
1924
|
+
return SHAKE_MENU_GESTURE_THREE_FINGER_PINCH;
|
|
1925
|
+
}
|
|
1926
|
+
return SHAKE_MENU_GESTURE_SHAKE;
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
static boolean isSupportedShakeMenuGesture(final String value) {
|
|
1930
|
+
if (value == null) {
|
|
1931
|
+
return true;
|
|
1932
|
+
}
|
|
1933
|
+
final String normalized = value.trim();
|
|
1934
|
+
if (normalized.isEmpty()) {
|
|
1935
|
+
return false;
|
|
1936
|
+
}
|
|
1937
|
+
return SHAKE_MENU_GESTURE_SHAKE.equals(normalized) || SHAKE_MENU_GESTURE_THREE_FINGER_PINCH.equals(normalized);
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
static String autoUpdateModeForLegacyDirectUpdateMode(final String directUpdateMode) {
|
|
1941
|
+
switch (directUpdateMode) {
|
|
1942
|
+
case AUTO_UPDATE_MODE_INSTALL:
|
|
1943
|
+
case AUTO_UPDATE_MODE_LAUNCH:
|
|
1944
|
+
case AUTO_UPDATE_MODE_ALWAYS:
|
|
1945
|
+
return directUpdateMode;
|
|
1946
|
+
case "false":
|
|
1947
|
+
default:
|
|
1948
|
+
return AUTO_UPDATE_MODE_BACKGROUND;
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
static String directUpdateModeForAutoUpdateMode(final String autoUpdateMode) {
|
|
1953
|
+
switch (autoUpdateMode) {
|
|
1954
|
+
case AUTO_UPDATE_MODE_INSTALL:
|
|
1955
|
+
case AUTO_UPDATE_MODE_LAUNCH:
|
|
1956
|
+
case AUTO_UPDATE_MODE_ALWAYS:
|
|
1957
|
+
return autoUpdateMode;
|
|
1958
|
+
default:
|
|
1959
|
+
return "false";
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
static boolean isAutoUpdateModeEnabled(final String autoUpdateMode) {
|
|
1964
|
+
return !AUTO_UPDATE_MODE_OFF.equals(autoUpdateMode);
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
static boolean shouldAutoUpdateModeSetNextBundle(final String autoUpdateMode) {
|
|
1968
|
+
return isAutoUpdateModeEnabled(autoUpdateMode) && !AUTO_UPDATE_MODE_ONLY_DOWNLOAD.equals(autoUpdateMode);
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
static boolean isDirectUpdateMode(final String directUpdateMode) {
|
|
1972
|
+
return (
|
|
1973
|
+
AUTO_UPDATE_MODE_INSTALL.equals(directUpdateMode) ||
|
|
1974
|
+
AUTO_UPDATE_MODE_LAUNCH.equals(directUpdateMode) ||
|
|
1975
|
+
AUTO_UPDATE_MODE_ALWAYS.equals(directUpdateMode)
|
|
1976
|
+
);
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
private boolean shouldAutoSetNextBundle() {
|
|
1980
|
+
return shouldAutoUpdateModeSetNextBundle(this.autoUpdateMode);
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
private boolean isDirectUpdateCurrentlyAllowed(final boolean plannedDirectUpdate) {
|
|
1984
|
+
return plannedDirectUpdate && !Boolean.TRUE.equals(this.autoSplashscreenTimedOut);
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
static boolean shouldConsumeOnLaunchDirectUpdate(final String directUpdateMode, final boolean plannedDirectUpdate) {
|
|
1988
|
+
return plannedDirectUpdate && "onLaunch".equals(directUpdateMode);
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
static int normalizedPeriodCheckDelayMs(final int valueSeconds) {
|
|
1992
|
+
final int normalizedSeconds = normalizedPeriodCheckDelaySeconds(valueSeconds);
|
|
1993
|
+
if (normalizedSeconds <= 0) {
|
|
1994
|
+
return 0;
|
|
1995
|
+
}
|
|
1996
|
+
final long delayMs = (long) normalizedSeconds * 1000L;
|
|
1997
|
+
return delayMs > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) delayMs;
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
static int normalizedPeriodCheckDelaySeconds(final int valueSeconds) {
|
|
2001
|
+
if (valueSeconds <= 0) {
|
|
2002
|
+
return 0;
|
|
2003
|
+
}
|
|
2004
|
+
return Math.max(600, valueSeconds);
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
private void consumeOnLaunchDirectUpdateAttempt(final boolean plannedDirectUpdate) {
|
|
2008
|
+
if (!shouldConsumeOnLaunchDirectUpdate(this.directUpdateMode, plannedDirectUpdate)) {
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
this.onLaunchDirectUpdateUsed = true;
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
void configureDirectUpdateModeForTesting(final String directUpdateMode, final boolean onLaunchDirectUpdateUsed) {
|
|
2016
|
+
this.directUpdateMode = directUpdateMode;
|
|
2017
|
+
this.autoUpdateMode = autoUpdateModeForLegacyDirectUpdateMode(directUpdateMode);
|
|
2018
|
+
this.autoUpdate = isAutoUpdateModeEnabled(this.autoUpdateMode);
|
|
2019
|
+
if (this.implementation != null) {
|
|
2020
|
+
this.implementation.directUpdate = isDirectUpdateMode(this.directUpdateMode);
|
|
2021
|
+
}
|
|
2022
|
+
this.onLaunchDirectUpdateUsed = onLaunchDirectUpdateUsed;
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
void setAutoUpdateModeForTesting(final String autoUpdateMode) {
|
|
2026
|
+
this.autoUpdateMode = normalizedAutoUpdateMode(autoUpdateMode);
|
|
2027
|
+
this.autoUpdate = isAutoUpdateModeEnabled(this.autoUpdateMode);
|
|
2028
|
+
this.directUpdateMode = directUpdateModeForAutoUpdateMode(this.autoUpdateMode);
|
|
2029
|
+
if (this.implementation != null) {
|
|
2030
|
+
this.implementation.directUpdate = isDirectUpdateMode(this.directUpdateMode);
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
boolean shouldUseDirectUpdateForTesting() {
|
|
2035
|
+
return this.shouldUseDirectUpdate();
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
boolean hasConsumedOnLaunchDirectUpdateForTesting() {
|
|
2039
|
+
return this.onLaunchDirectUpdateUsed;
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
boolean isVersionDownloadInProgress(final String version) {
|
|
2043
|
+
return (
|
|
2044
|
+
version != null &&
|
|
2045
|
+
!version.isEmpty() &&
|
|
2046
|
+
this.implementation != null &&
|
|
2047
|
+
this.implementation.activity != null &&
|
|
2048
|
+
DownloadWorkerManager.isVersionDownloading(this.implementation.activity, version)
|
|
2049
|
+
);
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
void setLoggerForTesting(final Logger logger) {
|
|
2053
|
+
this.logger = logger;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
void completeBackgroundTaskForTesting(final BundleInfo current, final boolean plannedDirectUpdate) {
|
|
2057
|
+
this.endBackGroundTaskWithNotif("test", current.getVersionName(), current, false, plannedDirectUpdate);
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
void scheduleDirectUpdateFinish(final BundleInfo latest) {
|
|
2061
|
+
startNewThread(() -> {
|
|
2062
|
+
try {
|
|
2063
|
+
if (this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
2064
|
+
logger.info("Skipping direct update install while preview session state is active");
|
|
2065
|
+
this.implementation.directUpdate = false;
|
|
2066
|
+
this.clearBackgroundDownloadState();
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
Activity currentActivity = this.getActivity();
|
|
2070
|
+
if (currentActivity != null) {
|
|
2071
|
+
this.implementation.activity = currentActivity;
|
|
2072
|
+
} else {
|
|
2073
|
+
logger.warn("directUpdateFinish: Activity is null, proceeding without refreshing the activity reference");
|
|
2074
|
+
}
|
|
2075
|
+
this.directUpdateFinish(latest);
|
|
2076
|
+
} catch (final Exception e) {
|
|
2077
|
+
logger.error("directUpdateFinish failed: " + e.getMessage());
|
|
2078
|
+
}
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
private void directUpdateFinish(final BundleInfo latest) {
|
|
2083
|
+
if (this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
2084
|
+
logger.info("Skipping direct update finish while preview session state is active");
|
|
2085
|
+
this.implementation.directUpdate = false;
|
|
2086
|
+
this.clearBackgroundDownloadState();
|
|
2087
|
+
return;
|
|
2088
|
+
}
|
|
2089
|
+
if ("onLaunch".equals(this.directUpdateMode)) {
|
|
2090
|
+
this.onLaunchDirectUpdateUsed = true;
|
|
2091
|
+
this.implementation.directUpdate = false;
|
|
2092
|
+
}
|
|
2093
|
+
if (this.applyDownloadedBundleForDirectUpdate(latest)) {
|
|
2094
|
+
this.implementation.setNextBundle(null);
|
|
2095
|
+
this.notifyBundleSet(latest);
|
|
2096
|
+
sendReadyToJs(latest, "update installed", true);
|
|
2097
|
+
} else {
|
|
2098
|
+
this.implementation.setNextBundle(latest.getId());
|
|
2099
|
+
final JSObject ret = new JSObject();
|
|
2100
|
+
ret.put("bundle", InternalUtils.mapToJSObject(latest.toJSONMap()));
|
|
2101
|
+
this.notifyListeners("updateAvailable", ret);
|
|
2102
|
+
sendReadyToJs(
|
|
2103
|
+
this.implementation.getCurrentBundle(),
|
|
2104
|
+
"Direct update reload failed, update will install next background",
|
|
2105
|
+
false
|
|
2106
|
+
);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
private boolean applyDownloadedBundleForDirectUpdate(final BundleInfo latest) {
|
|
2111
|
+
final CapgoUpdater.ResetState previousState = this.implementation.captureResetState();
|
|
2112
|
+
final String previousBundleName = this.implementation.getCurrentBundle().getVersionName();
|
|
2113
|
+
|
|
2114
|
+
if (!this.implementation.stagePendingReload(latest)) {
|
|
2115
|
+
this.implementation.restoreResetState(previousState);
|
|
2116
|
+
logger.error("Direct update failed to stage downloaded bundle: " + latest.toString());
|
|
2117
|
+
return false;
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
if (this._reload()) {
|
|
2121
|
+
this.implementation.finalizePendingReload(latest, previousBundleName);
|
|
2122
|
+
return true;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
this.implementation.restoreResetState(previousState);
|
|
2126
|
+
this.restoreLiveBundleStateAfterFailedReload();
|
|
2127
|
+
logger.error("Direct update reload failed after staging bundle: " + latest.toString());
|
|
2128
|
+
return false;
|
|
772
2129
|
}
|
|
773
2130
|
|
|
774
2131
|
private void cleanupObsoleteVersions() {
|
|
775
2132
|
cleanupThread = startNewThread(() -> {
|
|
776
2133
|
synchronized (cleanupLock) {
|
|
777
2134
|
try {
|
|
778
|
-
final String previous = this.
|
|
2135
|
+
final String previous = this.getStoredNativeBuildVersion();
|
|
779
2136
|
if (!"".equals(previous) && !Objects.equals(this.currentBuildVersion, previous)) {
|
|
780
2137
|
logger.info("New native build version detected: " + this.currentBuildVersion);
|
|
781
2138
|
this.implementation.reset(true);
|
|
@@ -836,6 +2193,19 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
836
2193
|
});
|
|
837
2194
|
}
|
|
838
2195
|
|
|
2196
|
+
String getStoredNativeBuildVersion() {
|
|
2197
|
+
String previous = this.prefs.getString("LatestNativeBuildVersion", "");
|
|
2198
|
+
if (previous == null || previous.isEmpty()) {
|
|
2199
|
+
previous = this.prefs.getString("LatestVersionNative", "");
|
|
2200
|
+
}
|
|
2201
|
+
return previous == null ? "" : previous;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
void persistCurrentNativeBuildVersion() {
|
|
2205
|
+
this.editor.putString("LatestNativeBuildVersion", this.currentBuildVersion);
|
|
2206
|
+
this.editor.apply();
|
|
2207
|
+
}
|
|
2208
|
+
|
|
839
2209
|
private void waitForCleanupIfNeeded() {
|
|
840
2210
|
if (cleanupComplete) {
|
|
841
2211
|
return; // Already done, no need to wait
|
|
@@ -1058,6 +2428,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1058
2428
|
CapacitorUpdaterPlugin.this.editor,
|
|
1059
2429
|
DEFAULT_CHANNEL_PREF_KEY,
|
|
1060
2430
|
CapacitorUpdaterPlugin.this.allowSetDefaultChannel,
|
|
2431
|
+
CapacitorUpdaterPlugin.this.getConfig().getString("defaultChannel", ""),
|
|
1061
2432
|
(res) -> {
|
|
1062
2433
|
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
1063
2434
|
if (jsRes.has("error")) {
|
|
@@ -1106,21 +2477,25 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1106
2477
|
try {
|
|
1107
2478
|
logger.info("getChannel");
|
|
1108
2479
|
startNewThread(() ->
|
|
1109
|
-
CapacitorUpdaterPlugin.this.implementation.getChannel(
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
2480
|
+
CapacitorUpdaterPlugin.this.implementation.getChannel(
|
|
2481
|
+
(res) -> {
|
|
2482
|
+
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
2483
|
+
if (jsRes.has("error")) {
|
|
2484
|
+
String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
|
|
2485
|
+
String errorCode = jsRes.getString("error");
|
|
1114
2486
|
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
2487
|
+
JSObject errorObj = new JSObject();
|
|
2488
|
+
errorObj.put("message", errorMessage);
|
|
2489
|
+
errorObj.put("error", errorCode);
|
|
1118
2490
|
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
2491
|
+
call.reject(errorMessage, "GETCHANNEL_FAILED", null, errorObj);
|
|
2492
|
+
} else {
|
|
2493
|
+
call.resolve(jsRes);
|
|
2494
|
+
}
|
|
2495
|
+
},
|
|
2496
|
+
CapacitorUpdaterPlugin.this.editor,
|
|
2497
|
+
DEFAULT_CHANNEL_PREF_KEY
|
|
2498
|
+
)
|
|
1124
2499
|
);
|
|
1125
2500
|
} catch (final Exception e) {
|
|
1126
2501
|
logger.error("Failed to getChannel " + e.getMessage());
|
|
@@ -1135,6 +2510,23 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1135
2510
|
startNewThread(() ->
|
|
1136
2511
|
CapacitorUpdaterPlugin.this.implementation.listChannels((res) -> {
|
|
1137
2512
|
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
2513
|
+
Object channels = res.get("channels");
|
|
2514
|
+
if (channels instanceof List<?> channelsList) {
|
|
2515
|
+
JSArray channelsArray = new JSArray();
|
|
2516
|
+
for (Object channel : channelsList) {
|
|
2517
|
+
if (channel instanceof Map<?, ?> channelMap) {
|
|
2518
|
+
JSObject channelObject = new JSObject();
|
|
2519
|
+
for (Map.Entry<?, ?> entry : channelMap.entrySet()) {
|
|
2520
|
+
Object key = entry.getKey();
|
|
2521
|
+
if (key != null) {
|
|
2522
|
+
channelObject.put(key.toString(), entry.getValue());
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
channelsArray.put(channelObject);
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
jsRes.put("channels", channelsArray);
|
|
2529
|
+
}
|
|
1138
2530
|
if (jsRes.has("error")) {
|
|
1139
2531
|
String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
|
|
1140
2532
|
String errorCode = jsRes.getString("error");
|
|
@@ -1155,6 +2547,20 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1155
2547
|
}
|
|
1156
2548
|
}
|
|
1157
2549
|
|
|
2550
|
+
private BundleInfo downloadBundle(
|
|
2551
|
+
final String url,
|
|
2552
|
+
final String version,
|
|
2553
|
+
final String sessionKey,
|
|
2554
|
+
final String checksum,
|
|
2555
|
+
final JSONArray manifest
|
|
2556
|
+
) throws IOException {
|
|
2557
|
+
if (manifest != null) {
|
|
2558
|
+
return this.implementation.downloadManifest(url, version, sessionKey, checksum, manifest);
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
return this.implementation.download(url, version, sessionKey, checksum);
|
|
2562
|
+
}
|
|
2563
|
+
|
|
1158
2564
|
@PluginMethod
|
|
1159
2565
|
public void download(final PluginCall call) {
|
|
1160
2566
|
final String url = call.getString("url");
|
|
@@ -1176,19 +2582,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1176
2582
|
logger.info("Downloading " + url);
|
|
1177
2583
|
startNewThread(() -> {
|
|
1178
2584
|
try {
|
|
1179
|
-
final BundleInfo downloaded;
|
|
1180
|
-
if (manifest != null) {
|
|
1181
|
-
// For manifest downloads, we need to handle this asynchronously
|
|
1182
|
-
// since there's no synchronous downloadManifest method in Java
|
|
1183
|
-
CapacitorUpdaterPlugin.this.implementation.downloadBackground(url, version, sessionKey, checksum, manifest);
|
|
1184
|
-
// Return immediately with a pending status - the actual result will come via listeners
|
|
1185
|
-
final String id = CapacitorUpdaterPlugin.this.implementation.randomString();
|
|
1186
|
-
downloaded = new BundleInfo(id, version, BundleStatus.DOWNLOADING, new Date(System.currentTimeMillis()), "");
|
|
1187
|
-
call.resolve(InternalUtils.mapToJSObject(downloaded.toJSONMap()));
|
|
1188
|
-
return;
|
|
1189
|
-
} else {
|
|
1190
|
-
downloaded = CapacitorUpdaterPlugin.this.implementation.download(url, version, sessionKey, checksum);
|
|
1191
|
-
}
|
|
2585
|
+
final BundleInfo downloaded = this.downloadBundle(url, version, sessionKey, checksum, manifest);
|
|
1192
2586
|
if (downloaded.isErrorStatus()) {
|
|
1193
2587
|
throw new RuntimeException("Download failed: " + downloaded.getStatus());
|
|
1194
2588
|
} else {
|
|
@@ -1229,12 +2623,12 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1229
2623
|
this.bridge.getWebView().post(() -> this.bridge.getWebView().evaluateJavascript(script, null));
|
|
1230
2624
|
}
|
|
1231
2625
|
|
|
1232
|
-
|
|
2626
|
+
private void applyCurrentBundleToBridge() {
|
|
1233
2627
|
final String path = this.implementation.getCurrentBundlePath();
|
|
2628
|
+
final boolean usingBuiltin = this.implementation.isUsingBuiltin();
|
|
1234
2629
|
if (this.keepUrlPathAfterReload) {
|
|
1235
2630
|
this.syncKeepUrlPathFlag(true);
|
|
1236
2631
|
}
|
|
1237
|
-
this.semaphoreUp();
|
|
1238
2632
|
logger.info("Reloading: " + path);
|
|
1239
2633
|
|
|
1240
2634
|
AtomicReference<URL> url = new AtomicReference<>();
|
|
@@ -1279,7 +2673,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1279
2673
|
}
|
|
1280
2674
|
|
|
1281
2675
|
if (url.get() != null) {
|
|
1282
|
-
if (
|
|
2676
|
+
if (usingBuiltin) {
|
|
1283
2677
|
this.bridge.getLocalServer().hostAssets(path);
|
|
1284
2678
|
} else {
|
|
1285
2679
|
this.bridge.getLocalServer().hostFiles(path);
|
|
@@ -1299,14 +2693,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1299
2693
|
} catch (MalformedURLException e) {
|
|
1300
2694
|
logger.error("Cannot get finalUrl from capacitor bridge " + e.getMessage());
|
|
1301
2695
|
|
|
1302
|
-
if (
|
|
2696
|
+
if (usingBuiltin) {
|
|
1303
2697
|
this.bridge.setServerAssetPath(path);
|
|
1304
2698
|
} else {
|
|
1305
2699
|
this.bridge.setServerBasePath(path);
|
|
1306
2700
|
}
|
|
1307
2701
|
}
|
|
1308
2702
|
} else {
|
|
1309
|
-
if (
|
|
2703
|
+
if (usingBuiltin) {
|
|
1310
2704
|
this.bridge.setServerAssetPath(path);
|
|
1311
2705
|
} else {
|
|
1312
2706
|
this.bridge.setServerBasePath(path);
|
|
@@ -1322,34 +2716,86 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1322
2716
|
});
|
|
1323
2717
|
}
|
|
1324
2718
|
}
|
|
2719
|
+
}
|
|
1325
2720
|
|
|
1326
|
-
|
|
2721
|
+
protected void restoreLiveBundleStateAfterFailedReload() {
|
|
2722
|
+
try {
|
|
2723
|
+
this.applyCurrentBundleToBridge();
|
|
2724
|
+
} catch (final Exception e) {
|
|
2725
|
+
logger.warn("Failed to restore live bundle after rejected reload: " + e.getMessage());
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
protected boolean _reload() {
|
|
2730
|
+
final int phase = this.semaphoreUp();
|
|
2731
|
+
this.applyCurrentBundleToBridge();
|
|
2732
|
+
|
|
2733
|
+
final long waitTimeMs = this.resolveAppReadyCheckTimeoutMs();
|
|
2734
|
+
this.checkAppReady(waitTimeMs);
|
|
1327
2735
|
this.notifyListeners("appReloaded", new JSObject());
|
|
1328
2736
|
|
|
1329
2737
|
// Wait for the reload to complete (until notifyAppReady is called)
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
}
|
|
2738
|
+
return this.semaphoreWait(phase, waitTimeMs);
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
protected boolean reloadWithoutWaitingForAppReady() {
|
|
2742
|
+
this.applyCurrentBundleToBridge();
|
|
1336
2743
|
|
|
2744
|
+
final long waitTimeMs = this.resolveAppReadyCheckTimeoutMs();
|
|
2745
|
+
this.checkAppReady(waitTimeMs);
|
|
2746
|
+
this.notifyListeners("appReloaded", new JSObject());
|
|
1337
2747
|
return true;
|
|
1338
2748
|
}
|
|
1339
2749
|
|
|
1340
2750
|
@PluginMethod
|
|
1341
2751
|
public void reload(final PluginCall call) {
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
2752
|
+
startNewThread(() -> {
|
|
2753
|
+
try {
|
|
2754
|
+
final BundleInfo current = this.implementation.getCurrentBundle();
|
|
2755
|
+
final BundleInfo next = this.implementation.getNextBundle();
|
|
2756
|
+
|
|
2757
|
+
if (!this.isPreviewSessionStateActive() && next != null && !next.isErrorStatus() && !next.getId().equals(current.getId())) {
|
|
2758
|
+
final CapgoUpdater.ResetState previousState = this.implementation.captureResetState();
|
|
2759
|
+
final String previousBundleName = this.implementation.getCurrentBundle().getVersionName();
|
|
2760
|
+
logger.info("Applying pending bundle before reload: " + next.getVersionName());
|
|
2761
|
+
final boolean didApplyPendingBundle;
|
|
2762
|
+
if (next.isBuiltin()) {
|
|
2763
|
+
this.implementation.prepareResetStateForTransition();
|
|
2764
|
+
didApplyPendingBundle = true;
|
|
2765
|
+
} else {
|
|
2766
|
+
didApplyPendingBundle = this.implementation.stagePendingReload(next);
|
|
2767
|
+
}
|
|
2768
|
+
if (didApplyPendingBundle && this._reload()) {
|
|
2769
|
+
if (next.isBuiltin()) {
|
|
2770
|
+
this.implementation.finalizeResetTransition(previousBundleName, false);
|
|
2771
|
+
} else {
|
|
2772
|
+
this.implementation.finalizePendingReload(next, previousBundleName);
|
|
2773
|
+
}
|
|
2774
|
+
this.notifyBundleSet(next);
|
|
2775
|
+
this.implementation.setNextBundle(null);
|
|
2776
|
+
this.showPreviewSessionNoticeIfNeeded();
|
|
2777
|
+
call.resolve();
|
|
2778
|
+
return;
|
|
2779
|
+
}
|
|
2780
|
+
this.implementation.restoreResetState(previousState);
|
|
2781
|
+
this.restoreLiveBundleStateAfterFailedReload();
|
|
2782
|
+
logger.error("Reload failed after applying pending bundle: " + next.getVersionName());
|
|
2783
|
+
call.reject("Reload failed after applying pending bundle: " + next.getVersionName());
|
|
2784
|
+
return;
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
if (this._reload()) {
|
|
2788
|
+
this.showPreviewSessionNoticeIfNeeded();
|
|
2789
|
+
call.resolve();
|
|
2790
|
+
} else {
|
|
2791
|
+
logger.error("Reload failed");
|
|
2792
|
+
call.reject("Reload failed");
|
|
2793
|
+
}
|
|
2794
|
+
} catch (final Exception e) {
|
|
2795
|
+
logger.error("Could not reload " + e.getMessage());
|
|
2796
|
+
call.reject("Could not reload", e);
|
|
1348
2797
|
}
|
|
1349
|
-
}
|
|
1350
|
-
logger.error("Could not reload " + e.getMessage());
|
|
1351
|
-
call.reject("Could not reload", e);
|
|
1352
|
-
}
|
|
2798
|
+
});
|
|
1353
2799
|
}
|
|
1354
2800
|
|
|
1355
2801
|
@PluginMethod
|
|
@@ -1382,19 +2828,924 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1382
2828
|
call.reject("Set called without id");
|
|
1383
2829
|
return;
|
|
1384
2830
|
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
2831
|
+
startNewThread(() -> {
|
|
2832
|
+
try {
|
|
2833
|
+
logger.info("Setting active bundle " + id);
|
|
2834
|
+
if (!this.implementation.set(id)) {
|
|
2835
|
+
logger.info("No such bundle " + id);
|
|
2836
|
+
call.reject("Update failed, id " + id + " does not exist.");
|
|
2837
|
+
} else if (Boolean.TRUE.equals(this.previewSessionEnabled)) {
|
|
2838
|
+
logger.info("Preview session set active bundle " + id + " without waiting for preview app readiness");
|
|
2839
|
+
final BundleInfo bundle = this.implementation.getBundleInfo(id);
|
|
2840
|
+
this.recordPreviewBundle(bundle);
|
|
2841
|
+
this.reloadWithoutWaitingForAppReady();
|
|
2842
|
+
this.notifyBundleSet(bundle);
|
|
2843
|
+
this.showPreviewSessionNoticeIfNeeded();
|
|
2844
|
+
call.resolve();
|
|
2845
|
+
} else if (!this._reload()) {
|
|
2846
|
+
logger.error("Reload failed after setting bundle " + id);
|
|
2847
|
+
call.reject("Reload failed after setting bundle " + id);
|
|
2848
|
+
} else {
|
|
2849
|
+
logger.info("Bundle successfully set to " + id);
|
|
2850
|
+
this.notifyBundleSet(this.implementation.getBundleInfo(id));
|
|
2851
|
+
this.showPreviewSessionNoticeIfNeeded();
|
|
2852
|
+
call.resolve();
|
|
2853
|
+
}
|
|
2854
|
+
} catch (final Exception e) {
|
|
2855
|
+
logger.error("Could not set id " + id + " " + e.getMessage());
|
|
2856
|
+
call.reject("Could not set id " + id, e);
|
|
2857
|
+
}
|
|
2858
|
+
});
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
private boolean preparePreviewFallbackIfNeeded() {
|
|
2862
|
+
if (Boolean.TRUE.equals(this.previewSessionEnabled)) {
|
|
2863
|
+
return true;
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
final BundleInfo current = this.implementation.getCurrentBundle();
|
|
2867
|
+
if (!this.implementation.setPreviewFallbackBundle(current.getId())) {
|
|
2868
|
+
logger.error("Could not save current bundle as preview fallback");
|
|
2869
|
+
return false;
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
final BundleInfo previousNext = this.implementation.getNextBundle();
|
|
2873
|
+
if (previousNext == null || previousNext.isDeleted() || previousNext.isErrorStatus()) {
|
|
2874
|
+
this.editor.remove(PREVIEW_PREVIOUS_NEXT_BUNDLE_PREF_KEY);
|
|
2875
|
+
} else {
|
|
2876
|
+
this.editor.putString(PREVIEW_PREVIOUS_NEXT_BUNDLE_PREF_KEY, previousNext.getId());
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
this.editor.putString(PREVIEW_PREVIOUS_APP_ID_PREF_KEY, this.implementation.appId);
|
|
2880
|
+
if (this.prefs.contains(DEFAULT_CHANNEL_PREF_KEY)) {
|
|
2881
|
+
this.editor.putString(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_PREF_KEY, this.prefs.getString(DEFAULT_CHANNEL_PREF_KEY, ""));
|
|
2882
|
+
this.editor.putBoolean(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_WAS_SET_PREF_KEY, true);
|
|
2883
|
+
} else {
|
|
2884
|
+
this.editor.remove(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_PREF_KEY);
|
|
2885
|
+
this.editor.putBoolean(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_WAS_SET_PREF_KEY, false);
|
|
2886
|
+
}
|
|
2887
|
+
this.editor.putBoolean(PREVIEW_PREVIOUS_SHAKE_MENU_PREF_KEY, Boolean.TRUE.equals(this.shakeMenuEnabled));
|
|
2888
|
+
this.editor.putBoolean(PREVIEW_PREVIOUS_SHAKE_CHANNEL_SELECTOR_PREF_KEY, Boolean.TRUE.equals(this.shakeChannelSelectorEnabled));
|
|
2889
|
+
logger.info("Preview session started with fallback bundle: " + current);
|
|
2890
|
+
return true;
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
private void activatePreviewSessionState() {
|
|
2894
|
+
this.clearIncomingPreviewTransition();
|
|
2895
|
+
this.hidePreviewTransitionLoader("preview-session-started");
|
|
2896
|
+
this.previewSessionEnabled = true;
|
|
2897
|
+
this.previewSessionAlertPending = true;
|
|
2898
|
+
this.implementation.previewSession = true;
|
|
2899
|
+
this.shakeMenuEnabled = true;
|
|
2900
|
+
this.editor.putBoolean(PREVIEW_SESSION_PREF_KEY, true);
|
|
2901
|
+
this.editor.putBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, true);
|
|
2902
|
+
this.editor.apply();
|
|
2903
|
+
this.syncShakeMenuLifecycle();
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
@PluginMethod
|
|
2907
|
+
public void startPreviewSession(final PluginCall call) {
|
|
2908
|
+
if (!Boolean.TRUE.equals(this.allowPreview)) {
|
|
2909
|
+
this.hidePreviewTransitionLoader("preview-session-not-allowed");
|
|
2910
|
+
logger.error("startPreviewSession not allowed set allowPreview in your config to true to enable it");
|
|
2911
|
+
call.reject("startPreviewSession not allowed");
|
|
2912
|
+
return;
|
|
2913
|
+
}
|
|
2914
|
+
final String previewAppId = this.normalizePreviewAppId(call.getString("appId"));
|
|
2915
|
+
final String rawPayloadUrl = call.getString("payloadUrl");
|
|
2916
|
+
final String previewPayloadUrl = this.normalizePreviewPayloadUrl(rawPayloadUrl);
|
|
2917
|
+
if (this.hasPreviewPayloadUrl(rawPayloadUrl) && previewPayloadUrl == null) {
|
|
2918
|
+
this.hidePreviewTransitionLoader("preview-session-invalid-payload");
|
|
2919
|
+
logger.error("startPreviewSession called with invalid payloadUrl");
|
|
2920
|
+
call.reject("Invalid preview payloadUrl");
|
|
2921
|
+
return;
|
|
2922
|
+
}
|
|
2923
|
+
startNewThread(() -> {
|
|
2924
|
+
try {
|
|
2925
|
+
if (!this.preparePreviewFallbackIfNeeded()) {
|
|
2926
|
+
this.hidePreviewTransitionLoader("preview-session-fallback-failed");
|
|
2927
|
+
call.reject("Could not save current bundle as preview fallback");
|
|
2928
|
+
return;
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
if (previewAppId != null) {
|
|
2932
|
+
this.setActiveAppId(previewAppId);
|
|
2933
|
+
this.editor.putString(PREVIEW_APP_ID_PREF_KEY, previewAppId);
|
|
2934
|
+
logger.info("Preview session using appId: " + previewAppId);
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
if (previewPayloadUrl != null) {
|
|
2938
|
+
this.editor.putString(PREVIEW_PAYLOAD_URL_PREF_KEY, previewPayloadUrl);
|
|
2939
|
+
logger.info("Preview session using payload URL");
|
|
2940
|
+
} else {
|
|
2941
|
+
this.editor.remove(PREVIEW_PAYLOAD_URL_PREF_KEY);
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
final String previewName = this.normalizedPreviewMetadataValue(call.getString("name"));
|
|
2945
|
+
if (previewName == null) {
|
|
2946
|
+
this.editor.remove(PREVIEW_NAME_PREF_KEY);
|
|
2947
|
+
} else {
|
|
2948
|
+
this.editor.putString(PREVIEW_NAME_PREF_KEY, previewName);
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
final String previewSource = this.normalizedPreviewMetadataValue(call.getString("source"));
|
|
2952
|
+
if (previewSource == null) {
|
|
2953
|
+
this.editor.remove(PREVIEW_SOURCE_PREF_KEY);
|
|
2954
|
+
} else {
|
|
2955
|
+
this.editor.putString(PREVIEW_SOURCE_PREF_KEY, previewSource);
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
this.activatePreviewSessionState();
|
|
2959
|
+
call.resolve();
|
|
2960
|
+
} catch (final Exception e) {
|
|
2961
|
+
this.hidePreviewTransitionLoader("preview-session-failed");
|
|
2962
|
+
logger.error("Could not start preview session " + e.getMessage());
|
|
2963
|
+
call.reject("Could not start preview session", e);
|
|
2964
|
+
}
|
|
2965
|
+
});
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
@PluginMethod
|
|
2969
|
+
public void listPreviews(final PluginCall call) {
|
|
2970
|
+
if (!Boolean.TRUE.equals(this.allowPreview)) {
|
|
2971
|
+
call.reject("listPreviews not allowed");
|
|
2972
|
+
return;
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
final JSArray previews = this.listPreviewInfos(true);
|
|
2976
|
+
final JSObject ret = new JSObject();
|
|
2977
|
+
ret.put("previews", previews);
|
|
2978
|
+
ret.put("currentBundle", InternalUtils.mapToJSObject(this.implementation.getCurrentBundle().toJSONMap()));
|
|
2979
|
+
|
|
2980
|
+
for (int i = 0; i < previews.length(); i++) {
|
|
2981
|
+
final JSONObject preview = previews.optJSONObject(i);
|
|
2982
|
+
if (preview != null && preview.optBoolean("isActive", false)) {
|
|
2983
|
+
ret.put("current", preview);
|
|
2984
|
+
break;
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
final BundleInfo liveBundle = this.implementation.getPreviewFallbackBundle();
|
|
2989
|
+
if (liveBundle != null) {
|
|
2990
|
+
ret.put("liveBundle", InternalUtils.mapToJSObject(liveBundle.toJSONMap()));
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
call.resolve(ret);
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
@PluginMethod
|
|
2997
|
+
public void setPreview(final PluginCall call) {
|
|
2998
|
+
if (!Boolean.TRUE.equals(this.allowPreview)) {
|
|
2999
|
+
call.reject("setPreview not allowed");
|
|
3000
|
+
return;
|
|
3001
|
+
}
|
|
3002
|
+
final String id = call.getString("id");
|
|
3003
|
+
if (id == null || id.isEmpty()) {
|
|
3004
|
+
call.reject("setPreview called without id");
|
|
3005
|
+
return;
|
|
3006
|
+
}
|
|
3007
|
+
final JSObject preview = this.storedPreviewInfo(id);
|
|
3008
|
+
if (preview == null) {
|
|
3009
|
+
call.reject("Preview " + id + " is not available locally");
|
|
3010
|
+
return;
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
this.showPreviewTransitionLoader("set-preview");
|
|
3014
|
+
startNewThread(() -> {
|
|
3015
|
+
if (!this.preparePreviewFallbackIfNeeded()) {
|
|
3016
|
+
this.hidePreviewTransitionLoader("set-preview-fallback-failed");
|
|
3017
|
+
call.reject("Could not save current bundle as preview fallback");
|
|
3018
|
+
return;
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
if (!this.implementation.set(id)) {
|
|
3022
|
+
this.hidePreviewTransitionLoader("set-preview-failed");
|
|
3023
|
+
call.reject("Preview " + id + " cannot be applied");
|
|
3024
|
+
return;
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
final BundleInfo bundle = this.implementation.getBundleInfo(id);
|
|
3028
|
+
this.updateCurrentPreviewSessionMetadataFrom(preview);
|
|
3029
|
+
this.activatePreviewSessionState();
|
|
3030
|
+
this.recordPreviewBundle(bundle);
|
|
3031
|
+
if (!this.reloadWithoutWaitingForAppReady()) {
|
|
3032
|
+
this.hidePreviewTransitionLoader("set-preview-reload-failed");
|
|
3033
|
+
call.reject("Reload failed after setting preview " + id);
|
|
3034
|
+
return;
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
this.notifyBundleSet(bundle);
|
|
3038
|
+
this.showPreviewSessionNoticeIfNeeded();
|
|
3039
|
+
call.resolve();
|
|
3040
|
+
});
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
public JSArray previewMenuPreviews() {
|
|
3044
|
+
return this.listPreviewInfos(true);
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
public boolean setPreviewFromShakeMenu(final String id) {
|
|
3048
|
+
final JSObject preview = this.storedPreviewInfo(id);
|
|
3049
|
+
if (!Boolean.TRUE.equals(this.allowPreview) || preview == null) {
|
|
3050
|
+
return false;
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
this.showPreviewTransitionLoader("set-preview-menu");
|
|
3054
|
+
if (!this.preparePreviewFallbackIfNeeded()) {
|
|
3055
|
+
this.hidePreviewTransitionLoader("set-preview-menu-fallback-failed");
|
|
3056
|
+
return false;
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
if (!this.implementation.set(id)) {
|
|
3060
|
+
this.hidePreviewTransitionLoader("set-preview-menu-failed");
|
|
3061
|
+
return false;
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
final BundleInfo bundle = this.implementation.getBundleInfo(id);
|
|
3065
|
+
this.updateCurrentPreviewSessionMetadataFrom(preview);
|
|
3066
|
+
this.activatePreviewSessionState();
|
|
3067
|
+
this.recordPreviewBundle(bundle);
|
|
3068
|
+
if (!this.reloadWithoutWaitingForAppReady()) {
|
|
3069
|
+
this.hidePreviewTransitionLoader("set-preview-menu-reload-failed");
|
|
3070
|
+
return false;
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
this.notifyBundleSet(bundle);
|
|
3074
|
+
this.showPreviewSessionNoticeIfNeeded();
|
|
3075
|
+
return true;
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
@PluginMethod
|
|
3079
|
+
public void resetPreview(final PluginCall call) {
|
|
3080
|
+
if (!Boolean.TRUE.equals(this.previewSessionEnabled)) {
|
|
3081
|
+
call.resolve();
|
|
3082
|
+
return;
|
|
3083
|
+
}
|
|
3084
|
+
startNewThread(() -> {
|
|
3085
|
+
if (this.leavePreviewSessionFromShakeMenu()) {
|
|
3086
|
+
call.resolve();
|
|
3087
|
+
} else {
|
|
3088
|
+
call.reject("Could not leave preview session");
|
|
3089
|
+
}
|
|
3090
|
+
});
|
|
3091
|
+
}
|
|
3092
|
+
|
|
3093
|
+
@PluginMethod
|
|
3094
|
+
public void deletePreview(final PluginCall call) {
|
|
3095
|
+
if (!Boolean.TRUE.equals(this.allowPreview)) {
|
|
3096
|
+
call.reject("deletePreview not allowed");
|
|
3097
|
+
return;
|
|
3098
|
+
}
|
|
3099
|
+
final String id = call.getString("id");
|
|
3100
|
+
if (id == null || id.isEmpty()) {
|
|
3101
|
+
call.reject("deletePreview called without id");
|
|
3102
|
+
return;
|
|
3103
|
+
}
|
|
3104
|
+
if (Boolean.TRUE.equals(this.previewSessionEnabled) && this.implementation.getCurrentBundle().getId().equals(id)) {
|
|
3105
|
+
call.reject("Cannot delete the active preview");
|
|
3106
|
+
return;
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
final boolean removed;
|
|
3110
|
+
synchronized (this.previewSessionsLock) {
|
|
3111
|
+
final JSONObject sessions = this.previewSessionsJson();
|
|
3112
|
+
removed = sessions.has(id);
|
|
3113
|
+
sessions.remove(id);
|
|
3114
|
+
this.savePreviewSessionsJson(sessions);
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
boolean deleted = false;
|
|
3118
|
+
final BundleInfo fallback = this.implementation.getPreviewFallbackBundle();
|
|
3119
|
+
final BundleInfo next = this.implementation.getNextBundle();
|
|
3120
|
+
if (
|
|
3121
|
+
removed &&
|
|
3122
|
+
!BundleInfo.ID_BUILTIN.equals(id) &&
|
|
3123
|
+
(fallback == null || !id.equals(fallback.getId())) &&
|
|
3124
|
+
(next == null || !id.equals(next.getId()))
|
|
3125
|
+
) {
|
|
3126
|
+
try {
|
|
3127
|
+
deleted = this.implementation.delete(id, false);
|
|
3128
|
+
} catch (final Exception err) {
|
|
3129
|
+
logger.warn("Could not delete preview bundle " + id + ": " + err.getMessage());
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
final JSObject ret = new JSObject();
|
|
3134
|
+
ret.put("removed", removed);
|
|
3135
|
+
ret.put("deleted", deleted);
|
|
3136
|
+
call.resolve(ret);
|
|
3137
|
+
}
|
|
3138
|
+
|
|
3139
|
+
@PluginMethod
|
|
3140
|
+
public void checkPreviewUpdate(final PluginCall call) {
|
|
3141
|
+
this.handlePreviewUpdate(call, false);
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
@PluginMethod
|
|
3145
|
+
public void updatePreview(final PluginCall call) {
|
|
3146
|
+
this.handlePreviewUpdate(call, true);
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
private void handlePreviewUpdate(final PluginCall call, final boolean shouldDownload) {
|
|
3150
|
+
if (!Boolean.TRUE.equals(this.allowPreview)) {
|
|
3151
|
+
call.reject("Preview updates not allowed");
|
|
3152
|
+
return;
|
|
3153
|
+
}
|
|
3154
|
+
final String id = call.getString("id");
|
|
3155
|
+
if (id == null || id.isEmpty()) {
|
|
3156
|
+
call.reject("Preview update called without id");
|
|
3157
|
+
return;
|
|
3158
|
+
}
|
|
3159
|
+
final JSObject preview = this.storedPreviewInfo(id);
|
|
3160
|
+
final String payloadUrl = preview == null ? null : this.normalizePreviewPayloadUrl(preview.optString("payloadUrl", null));
|
|
3161
|
+
if (payloadUrl == null) {
|
|
3162
|
+
call.reject("Preview " + id + " has no payloadUrl to update from");
|
|
3163
|
+
return;
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
startNewThread(() -> {
|
|
3167
|
+
try {
|
|
3168
|
+
final JSONObject payload = this.fetchPreviewPayload(payloadUrl);
|
|
3169
|
+
final String version = payload.optString("version", "").trim();
|
|
3170
|
+
if (version.isEmpty()) {
|
|
3171
|
+
throw new IOException("Preview payload is missing a version");
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
final BundleInfo currentPreviewBundle = this.implementation.getBundleInfo(id);
|
|
3175
|
+
final boolean upToDate = version.equals(currentPreviewBundle.getVersionName());
|
|
3176
|
+
if (upToDate || !shouldDownload) {
|
|
3177
|
+
final JSObject ret = new JSObject();
|
|
3178
|
+
ret.put("preview", preview);
|
|
3179
|
+
ret.put("latestVersion", version);
|
|
3180
|
+
ret.put("upToDate", upToDate);
|
|
3181
|
+
ret.put("updated", false);
|
|
3182
|
+
ret.put("bundle", InternalUtils.mapToJSObject(currentPreviewBundle.toJSONMap()));
|
|
3183
|
+
call.resolve(ret);
|
|
3184
|
+
return;
|
|
3185
|
+
}
|
|
3186
|
+
|
|
3187
|
+
final BundleInfo next = this.downloadPreviewPayloadBundle(payload);
|
|
3188
|
+
if (next.isErrorStatus()) {
|
|
3189
|
+
throw new IOException("Download failed: " + next.getStatus());
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
final boolean wasActive =
|
|
3193
|
+
Boolean.TRUE.equals(this.previewSessionEnabled) && this.implementation.getCurrentBundle().getId().equals(id);
|
|
3194
|
+
if (wasActive && !this.implementation.set(next.getId())) {
|
|
3195
|
+
throw new IOException("Downloaded preview bundle cannot be applied");
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
final JSObject savedPreview = this.recordPreviewBundle(next, id);
|
|
3199
|
+
if (wasActive) {
|
|
3200
|
+
if (!this.reloadWithoutWaitingForAppReady()) {
|
|
3201
|
+
throw new IOException("Reload failed after updating preview");
|
|
3202
|
+
}
|
|
3203
|
+
this.notifyBundleSet(next);
|
|
3204
|
+
this.showPreviewSessionNoticeIfNeeded();
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
final JSObject ret = new JSObject();
|
|
3208
|
+
ret.put("preview", savedPreview);
|
|
3209
|
+
ret.put("latestVersion", version);
|
|
3210
|
+
ret.put("upToDate", false);
|
|
3211
|
+
ret.put("updated", true);
|
|
3212
|
+
ret.put("bundle", InternalUtils.mapToJSObject(next.toJSONMap()));
|
|
3213
|
+
call.resolve(ret);
|
|
3214
|
+
} catch (final Exception err) {
|
|
3215
|
+
logger.error("Could not update preview: " + err.getMessage());
|
|
3216
|
+
call.reject("Could not update preview: " + err.getMessage());
|
|
3217
|
+
}
|
|
3218
|
+
});
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
public boolean leavePreviewSessionFromShakeMenu() {
|
|
3222
|
+
this.showPreviewTransitionLoader("leave-preview-session");
|
|
3223
|
+
final boolean didReset = this.resetToPreviewFallbackBundle();
|
|
3224
|
+
if (!didReset) {
|
|
3225
|
+
this.hidePreviewTransitionLoader("leave-preview-session-failed");
|
|
3226
|
+
return false;
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3229
|
+
this.endPreviewSession(true);
|
|
3230
|
+
return true;
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
private boolean leavePreviewSessionForIncomingPreviewLink() {
|
|
3234
|
+
this.showPreviewTransitionLoader("incoming-preview-deeplink");
|
|
3235
|
+
final BundleInfo previewFallbackBundle = this.resolvePreviewFallbackBundle("incoming preview deeplink");
|
|
3236
|
+
boolean didReload = false;
|
|
3237
|
+
|
|
3238
|
+
try {
|
|
3239
|
+
if (previewFallbackBundle == null) {
|
|
3240
|
+
return false;
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
final CapgoUpdater.ResetState previousState = this.implementation.captureResetState();
|
|
3244
|
+
if (!this.implementation.stagePreviewFallbackReload(previewFallbackBundle)) {
|
|
3245
|
+
logger.error("Could not stage preview fallback bundle");
|
|
3246
|
+
return false;
|
|
3247
|
+
}
|
|
3248
|
+
|
|
3249
|
+
if (!this.reloadWithoutWaitingForAppReady()) {
|
|
3250
|
+
this.implementation.restoreResetState(previousState);
|
|
3251
|
+
this.restoreLiveBundleStateAfterFailedReload();
|
|
3252
|
+
return false;
|
|
3253
|
+
}
|
|
3254
|
+
didReload = true;
|
|
3255
|
+
|
|
3256
|
+
this.endPreviewSession(true);
|
|
3257
|
+
return true;
|
|
3258
|
+
} finally {
|
|
3259
|
+
this.clearIncomingPreviewTransition();
|
|
3260
|
+
if (!didReload) {
|
|
3261
|
+
this.hidePreviewTransitionLoader("incoming-preview-deeplink-failed");
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3266
|
+
private void leavePreviewSessionForLaunchIntentIfNeeded() {
|
|
3267
|
+
final Intent intent = getActivity() == null ? null : getActivity().getIntent();
|
|
3268
|
+
if (
|
|
3269
|
+
intent == null ||
|
|
3270
|
+
!Intent.ACTION_VIEW.equals(intent.getAction()) ||
|
|
3271
|
+
intent.getData() == null ||
|
|
3272
|
+
!Boolean.TRUE.equals(this.previewSessionEnabled) ||
|
|
3273
|
+
!isPreviewDeepLink(intent.getData()) ||
|
|
3274
|
+
Boolean.TRUE.equals(this.isLeavingPreviewForIncomingLink)
|
|
3275
|
+
) {
|
|
3276
|
+
return;
|
|
3277
|
+
}
|
|
3278
|
+
|
|
3279
|
+
this.isLeavingPreviewForIncomingLink = true;
|
|
3280
|
+
this.showPreviewTransitionLoader("preview-launch-deeplink");
|
|
3281
|
+
logger.info("Preview deeplink launch detected while preview session is active; restoring fallback before initial load");
|
|
3282
|
+
if (!this.leavePreviewSessionWithoutReload()) {
|
|
3283
|
+
logger.error("Could not leave preview session before initial preview deeplink routing");
|
|
3284
|
+
this.isLeavingPreviewForIncomingLink = false;
|
|
3285
|
+
this.hidePreviewTransitionLoader("preview-launch-deeplink-failed");
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
|
|
3289
|
+
private boolean leavePreviewSessionWithoutReload() {
|
|
3290
|
+
return this.leavePreviewSessionWithoutReload(false);
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3293
|
+
private boolean leavePreviewSessionWithoutReload(final boolean keepPreviewGuard) {
|
|
3294
|
+
final BundleInfo previewFallbackBundle = this.resolvePreviewFallbackBundle("preview deeplink launch");
|
|
3295
|
+
if (previewFallbackBundle == null) {
|
|
3296
|
+
return false;
|
|
3297
|
+
}
|
|
3298
|
+
if (!this.implementation.stagePreviewFallbackReload(previewFallbackBundle)) {
|
|
3299
|
+
logger.error("Could not stage preview fallback bundle");
|
|
3300
|
+
return false;
|
|
3301
|
+
}
|
|
3302
|
+
|
|
3303
|
+
this.endPreviewSession(keepPreviewGuard);
|
|
3304
|
+
return true;
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3307
|
+
public boolean reloadPreviewSessionFromShakeMenu() {
|
|
3308
|
+
this.showPreviewTransitionLoader("reload-preview-session");
|
|
3309
|
+
final boolean didReload;
|
|
3310
|
+
final String payloadUrl = this.storedPreviewPayloadUrl();
|
|
3311
|
+
if (payloadUrl != null) {
|
|
3312
|
+
didReload = this.refreshPreviewSessionFromPayloadUrl(payloadUrl);
|
|
3313
|
+
} else {
|
|
3314
|
+
didReload = this.reloadWithoutWaitingForAppReady();
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
if (!didReload) {
|
|
3318
|
+
this.hidePreviewTransitionLoader("reload-preview-session-failed");
|
|
3319
|
+
}
|
|
3320
|
+
return didReload;
|
|
3321
|
+
}
|
|
3322
|
+
|
|
3323
|
+
public boolean hasActivePreviewSession() {
|
|
3324
|
+
return Boolean.TRUE.equals(this.previewSessionEnabled);
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
private boolean resetToPreviewFallbackBundle() {
|
|
3328
|
+
final BundleInfo fallback = this.resolvePreviewFallbackBundle("leave preview");
|
|
3329
|
+
if (fallback == null) {
|
|
3330
|
+
return false;
|
|
3331
|
+
}
|
|
3332
|
+
|
|
3333
|
+
final CapgoUpdater.ResetState previousState = this.implementation.captureResetState();
|
|
3334
|
+
final String previousBundleName = this.implementation.getCurrentBundle().getVersionName();
|
|
3335
|
+
logger.info("Resetting to preview fallback bundle: " + fallback.getVersionName());
|
|
3336
|
+
if (this.implementation.stagePreviewFallbackReload(fallback) && this.reloadWithoutWaitingForAppReady()) {
|
|
3337
|
+
this.implementation.finalizeResetTransition(previousBundleName, false);
|
|
3338
|
+
this.notifyBundleSet(fallback);
|
|
3339
|
+
return true;
|
|
3340
|
+
}
|
|
3341
|
+
this.implementation.restoreResetState(previousState);
|
|
3342
|
+
this.restoreLiveBundleStateAfterFailedReload();
|
|
3343
|
+
return false;
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
private BundleInfo resolvePreviewFallbackBundle(final String reason) {
|
|
3347
|
+
final BundleInfo fallback = this.implementation.getPreviewFallbackBundle();
|
|
3348
|
+
if (fallback != null && !fallback.isErrorStatus() && this.implementation.canSet(fallback)) {
|
|
3349
|
+
return fallback;
|
|
3350
|
+
}
|
|
3351
|
+
|
|
3352
|
+
if (fallback == null) {
|
|
3353
|
+
logger.warn("No preview fallback bundle available for " + reason + ". Falling back to builtin bundle.");
|
|
3354
|
+
} else if (fallback.isErrorStatus()) {
|
|
3355
|
+
logger.warn("Preview fallback bundle is in error state for " + reason + ". Falling back to builtin bundle.");
|
|
3356
|
+
} else {
|
|
3357
|
+
logger.warn("Preview fallback bundle is not installable for " + reason + ". Falling back to builtin bundle.");
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3360
|
+
final BundleInfo builtin = this.implementation.getBundleInfo(BundleInfo.ID_BUILTIN);
|
|
3361
|
+
if (builtin != null && !builtin.isErrorStatus() && this.implementation.canSet(builtin)) {
|
|
3362
|
+
return builtin;
|
|
3363
|
+
}
|
|
3364
|
+
|
|
3365
|
+
logger.error("Builtin bundle is not available to leave preview for " + reason);
|
|
3366
|
+
return null;
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
private void endPreviewSession() {
|
|
3370
|
+
this.endPreviewSession(false);
|
|
3371
|
+
}
|
|
3372
|
+
|
|
3373
|
+
private void endPreviewSession(final boolean keepPreviewGuard) {
|
|
3374
|
+
final boolean previousShakeMenuEnabled = this.prefs.getBoolean(
|
|
3375
|
+
PREVIEW_PREVIOUS_SHAKE_MENU_PREF_KEY,
|
|
3376
|
+
this.getConfig().getBoolean("shakeMenu", false)
|
|
3377
|
+
);
|
|
3378
|
+
final boolean previousShakeChannelSelectorEnabled = this.prefs.getBoolean(
|
|
3379
|
+
PREVIEW_PREVIOUS_SHAKE_CHANNEL_SELECTOR_PREF_KEY,
|
|
3380
|
+
this.getConfig().getBoolean("allowShakeChannelSelector", false)
|
|
3381
|
+
);
|
|
3382
|
+
this.restorePreviewPreviousNextBundle();
|
|
3383
|
+
this.restorePreviewPreviousAppId();
|
|
3384
|
+
this.restorePreviewPreviousDefaultChannel();
|
|
3385
|
+
|
|
3386
|
+
this.previewSessionEnabled = false;
|
|
3387
|
+
this.previewSessionAlertPending = false;
|
|
3388
|
+
if (keepPreviewGuard) {
|
|
3389
|
+
this.implementation.previewSession = true;
|
|
3390
|
+
} else {
|
|
3391
|
+
this.clearIncomingPreviewTransition();
|
|
3392
|
+
}
|
|
3393
|
+
this.shakeMenuEnabled = previousShakeMenuEnabled;
|
|
3394
|
+
this.shakeChannelSelectorEnabled = previousShakeChannelSelectorEnabled;
|
|
3395
|
+
this.syncShakeMenuLifecycle();
|
|
3396
|
+
this.implementation.setPreviewFallbackBundle(null);
|
|
3397
|
+
this.clearPreviewSessionPreferences();
|
|
3398
|
+
logger.info("Preview session ended");
|
|
3399
|
+
}
|
|
3400
|
+
|
|
3401
|
+
private void clearPreviewSessionBecauseDisabled() {
|
|
3402
|
+
logger.info("Preview session disabled by config; restoring preview fallback");
|
|
3403
|
+
final BundleInfo bundleToRestore = this.resolvePreviewFallbackBundle("preview disabled");
|
|
3404
|
+
if (bundleToRestore != null) {
|
|
3405
|
+
this.implementation.stagePreviewFallbackReload(bundleToRestore);
|
|
3406
|
+
} else {
|
|
3407
|
+
logger.warn("Could not restore preview fallback while disabling preview");
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3410
|
+
this.restorePreviewPreviousNextBundle();
|
|
3411
|
+
this.restorePreviewPreviousAppId();
|
|
3412
|
+
this.restorePreviewPreviousDefaultChannel();
|
|
3413
|
+
this.previewSessionEnabled = false;
|
|
3414
|
+
this.previewSessionAlertPending = false;
|
|
3415
|
+
this.isLeavingPreviewForIncomingLink = false;
|
|
3416
|
+
this.implementation.previewSession = false;
|
|
3417
|
+
this.hidePreviewTransitionLoader("preview-session-disabled");
|
|
3418
|
+
this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
|
|
3419
|
+
this.shakeChannelSelectorEnabled = this.getConfig().getBoolean("allowShakeChannelSelector", false);
|
|
3420
|
+
this.shakeMenuGesture = normalizedShakeMenuGesture(this.getConfig().getString("shakeMenuGesture", SHAKE_MENU_GESTURE_SHAKE));
|
|
3421
|
+
this.syncShakeMenuLifecycle();
|
|
3422
|
+
this.clearPreviewSessionPreferences();
|
|
3423
|
+
}
|
|
3424
|
+
|
|
3425
|
+
private void clearPreviewSessionPreferences() {
|
|
3426
|
+
if (this.implementation != null) {
|
|
3427
|
+
this.implementation.setPreviewFallbackBundle(null);
|
|
3428
|
+
}
|
|
3429
|
+
this.editor.remove(PREVIEW_SESSION_PREF_KEY);
|
|
3430
|
+
this.editor.remove(PREVIEW_PREVIOUS_SHAKE_MENU_PREF_KEY);
|
|
3431
|
+
this.editor.remove(PREVIEW_PREVIOUS_SHAKE_CHANNEL_SELECTOR_PREF_KEY);
|
|
3432
|
+
this.editor.remove(PREVIEW_PREVIOUS_NEXT_BUNDLE_PREF_KEY);
|
|
3433
|
+
this.editor.remove(PREVIEW_PREVIOUS_APP_ID_PREF_KEY);
|
|
3434
|
+
this.editor.remove(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_PREF_KEY);
|
|
3435
|
+
this.editor.remove(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_WAS_SET_PREF_KEY);
|
|
3436
|
+
this.editor.remove(PREVIEW_APP_ID_PREF_KEY);
|
|
3437
|
+
this.editor.remove(PREVIEW_PAYLOAD_URL_PREF_KEY);
|
|
3438
|
+
this.editor.remove(PREVIEW_NAME_PREF_KEY);
|
|
3439
|
+
this.editor.remove(PREVIEW_SOURCE_PREF_KEY);
|
|
3440
|
+
this.editor.remove(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY);
|
|
3441
|
+
this.editor.apply();
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3444
|
+
private void setActiveAppId(final String appId) {
|
|
3445
|
+
this.implementation.appId = appId;
|
|
3446
|
+
if (this.implementation.versionOs != null) {
|
|
3447
|
+
DownloadService.updateUserAgent(this.implementation.appId, this.pluginVersion, this.implementation.versionOs);
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3451
|
+
private void restorePreviewPreviousAppId() {
|
|
3452
|
+
final String previousAppId = this.prefs.getString(PREVIEW_PREVIOUS_APP_ID_PREF_KEY, "");
|
|
3453
|
+
if (previousAppId == null || previousAppId.isEmpty()) {
|
|
3454
|
+
return;
|
|
3455
|
+
}
|
|
3456
|
+
this.setActiveAppId(previousAppId);
|
|
3457
|
+
logger.info("Restored appId after preview: " + previousAppId);
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3460
|
+
private void restorePreviewPreviousDefaultChannel() {
|
|
3461
|
+
final String configDefaultChannel = this.getConfig().getString("defaultChannel", "");
|
|
3462
|
+
if (this.prefs.getBoolean(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_WAS_SET_PREF_KEY, false)) {
|
|
3463
|
+
final String previousDefaultChannel = this.prefs.getString(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_PREF_KEY, "");
|
|
3464
|
+
this.editor.putString(DEFAULT_CHANNEL_PREF_KEY, previousDefaultChannel);
|
|
3465
|
+
this.implementation.defaultChannel = previousDefaultChannel;
|
|
3466
|
+
this.editor.apply();
|
|
3467
|
+
logger.info("Restored defaultChannel after preview");
|
|
3468
|
+
return;
|
|
3469
|
+
}
|
|
3470
|
+
|
|
3471
|
+
this.editor.remove(DEFAULT_CHANNEL_PREF_KEY);
|
|
3472
|
+
this.implementation.defaultChannel = configDefaultChannel;
|
|
3473
|
+
this.editor.apply();
|
|
3474
|
+
logger.info("Restored defaultChannel after preview to config value");
|
|
3475
|
+
}
|
|
3476
|
+
|
|
3477
|
+
private String normalizePreviewAppId(final String rawAppId) {
|
|
3478
|
+
if (rawAppId == null) {
|
|
3479
|
+
return null;
|
|
3480
|
+
}
|
|
3481
|
+
|
|
3482
|
+
final String appId = rawAppId.trim();
|
|
3483
|
+
if (appId.isEmpty()) {
|
|
3484
|
+
return null;
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
final String lowercasedAppId = appId.toLowerCase(java.util.Locale.ROOT);
|
|
3488
|
+
if ("undefined".equals(lowercasedAppId) || "null".equals(lowercasedAppId)) {
|
|
3489
|
+
return null;
|
|
3490
|
+
}
|
|
3491
|
+
|
|
3492
|
+
return appId;
|
|
3493
|
+
}
|
|
3494
|
+
|
|
3495
|
+
private boolean hasPreviewPayloadUrl(final String rawPayloadUrl) {
|
|
3496
|
+
if (rawPayloadUrl == null) {
|
|
3497
|
+
return false;
|
|
3498
|
+
}
|
|
3499
|
+
|
|
3500
|
+
final String payloadUrl = rawPayloadUrl.trim();
|
|
3501
|
+
if (payloadUrl.isEmpty()) {
|
|
3502
|
+
return false;
|
|
3503
|
+
}
|
|
3504
|
+
|
|
3505
|
+
final String lowercasedPayloadUrl = payloadUrl.toLowerCase(java.util.Locale.ROOT);
|
|
3506
|
+
return !"undefined".equals(lowercasedPayloadUrl) && !"null".equals(lowercasedPayloadUrl);
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
private String normalizePreviewPayloadUrl(final String rawPayloadUrl) {
|
|
3510
|
+
if (!this.hasPreviewPayloadUrl(rawPayloadUrl)) {
|
|
3511
|
+
return null;
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3514
|
+
final String payloadUrl = rawPayloadUrl.trim();
|
|
3515
|
+
try {
|
|
3516
|
+
final URL parsedUrl = new URL(payloadUrl);
|
|
3517
|
+
final String protocol = parsedUrl.getProtocol();
|
|
3518
|
+
if (!"https".equals(protocol) && !"http".equals(protocol)) {
|
|
3519
|
+
return null;
|
|
3520
|
+
}
|
|
3521
|
+
return parsedUrl.toString();
|
|
3522
|
+
} catch (final MalformedURLException ignored) {
|
|
3523
|
+
return null;
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
private String storedPreviewPayloadUrl() {
|
|
3528
|
+
return this.normalizePreviewPayloadUrl(this.prefs.getString(PREVIEW_PAYLOAD_URL_PREF_KEY, null));
|
|
3529
|
+
}
|
|
3530
|
+
|
|
3531
|
+
private String previewPathFromUri(final Uri uri) {
|
|
3532
|
+
if ("capgo".equals(uri.getScheme())) {
|
|
3533
|
+
final String host = uri.getHost();
|
|
3534
|
+
final String path = uri.getPath();
|
|
3535
|
+
return ("/" + (host == null ? "" : host) + (path == null ? "" : path)).replaceAll("/+", "/");
|
|
3536
|
+
}
|
|
3537
|
+
|
|
3538
|
+
return uri.getPath();
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3541
|
+
private boolean isPreviewDeepLink(final Uri uri) {
|
|
3542
|
+
final String path = this.previewPathFromUri(uri);
|
|
3543
|
+
return "/preview/channel".equals(path) || "/preview/bundle".equals(path);
|
|
3544
|
+
}
|
|
3545
|
+
|
|
3546
|
+
private String readResponseBody(final InputStream stream) throws IOException {
|
|
3547
|
+
if (stream == null) {
|
|
3548
|
+
return "";
|
|
3549
|
+
}
|
|
3550
|
+
|
|
3551
|
+
try (InputStream input = stream; ByteArrayOutputStream output = new ByteArrayOutputStream()) {
|
|
3552
|
+
final byte[] buffer = new byte[8192];
|
|
3553
|
+
int read;
|
|
3554
|
+
while ((read = input.read(buffer)) != -1) {
|
|
3555
|
+
output.write(buffer, 0, read);
|
|
3556
|
+
}
|
|
3557
|
+
return output.toString(StandardCharsets.UTF_8.name());
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
|
|
3561
|
+
private JSONObject fetchPreviewPayload(final String payloadUrl) throws IOException, JSONException {
|
|
3562
|
+
final HttpURLConnection connection = (HttpURLConnection) new URL(payloadUrl).openConnection();
|
|
3563
|
+
connection.setRequestMethod("GET");
|
|
3564
|
+
connection.setRequestProperty("Accept", "application/json");
|
|
3565
|
+
connection.setConnectTimeout(30000);
|
|
3566
|
+
connection.setReadTimeout(60000);
|
|
3567
|
+
|
|
3568
|
+
try {
|
|
3569
|
+
final int statusCode = connection.getResponseCode();
|
|
3570
|
+
final String body = this.readResponseBody(
|
|
3571
|
+
statusCode >= 200 && statusCode < 300 ? connection.getInputStream() : connection.getErrorStream()
|
|
3572
|
+
);
|
|
3573
|
+
final JSONObject payload = new JSONObject(body);
|
|
3574
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
3575
|
+
throw new IOException(
|
|
3576
|
+
payload.optString("message", payload.optString("error", "Preview payload request failed with HTTP " + statusCode))
|
|
3577
|
+
);
|
|
3578
|
+
}
|
|
3579
|
+
return payload;
|
|
3580
|
+
} finally {
|
|
3581
|
+
connection.disconnect();
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3584
|
+
|
|
3585
|
+
private BundleInfo downloadPreviewPayloadBundle(final JSONObject payload) throws IOException, JSONException {
|
|
3586
|
+
final String version = payload.optString("version", "").trim();
|
|
3587
|
+
if (version.isEmpty()) {
|
|
3588
|
+
throw new IOException("Preview payload is missing a version");
|
|
3589
|
+
}
|
|
3590
|
+
|
|
3591
|
+
final JSONArray manifest = payload.optJSONArray("manifest");
|
|
3592
|
+
final String url = payload.optString("url", "");
|
|
3593
|
+
if ((url == null || url.isEmpty()) && (manifest == null || manifest.length() == 0)) {
|
|
3594
|
+
throw new IOException("Preview payload is missing download information");
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3597
|
+
return this.downloadBundle(
|
|
3598
|
+
url == null || url.isEmpty() ? "https://404.capgo.app/no.zip" : url,
|
|
3599
|
+
version,
|
|
3600
|
+
payload.optString("sessionKey", ""),
|
|
3601
|
+
payload.optString("checksum", ""),
|
|
3602
|
+
manifest
|
|
3603
|
+
);
|
|
3604
|
+
}
|
|
3605
|
+
|
|
3606
|
+
private boolean refreshPreviewSessionFromPayloadUrl(final String payloadUrl) {
|
|
3607
|
+
try {
|
|
3608
|
+
final JSONObject payload = this.fetchPreviewPayload(payloadUrl);
|
|
3609
|
+
final String version = payload.optString("version", "").trim();
|
|
3610
|
+
if (version.isEmpty()) {
|
|
3611
|
+
throw new IOException("Preview payload is missing a version");
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3614
|
+
final BundleInfo current = this.implementation.getCurrentBundle();
|
|
3615
|
+
if (version.equals(current.getVersionName())) {
|
|
3616
|
+
logger.info("Preview payload unchanged, reloading current bundle");
|
|
3617
|
+
return this.reloadWithoutWaitingForAppReady();
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3620
|
+
final BundleInfo next = this.downloadPreviewPayloadBundle(payload);
|
|
3621
|
+
if (next.isErrorStatus()) {
|
|
3622
|
+
throw new IOException("Download failed: " + next.getStatus());
|
|
3623
|
+
}
|
|
3624
|
+
if (!this.implementation.set(next.getId())) {
|
|
3625
|
+
throw new IOException("Downloaded preview bundle cannot be applied");
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3628
|
+
this.recordPreviewBundle(next, current.getId());
|
|
3629
|
+
this.notifyBundleSet(next);
|
|
3630
|
+
return this.reloadWithoutWaitingForAppReady();
|
|
3631
|
+
} catch (final Exception err) {
|
|
3632
|
+
logger.error("Could not refresh preview session: " + err.getMessage());
|
|
3633
|
+
return false;
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3637
|
+
private void clearPreviewSessionForNativeBuildChange() {
|
|
3638
|
+
if (
|
|
3639
|
+
!Boolean.TRUE.equals(this.previewSessionEnabled) &&
|
|
3640
|
+
this.implementation.getPreviewFallbackBundle() == null &&
|
|
3641
|
+
!this.hasSavedPreviewSessions()
|
|
3642
|
+
) {
|
|
3643
|
+
return;
|
|
3644
|
+
}
|
|
3645
|
+
logger.info("Native build changed; clearing preview session state");
|
|
3646
|
+
this.previewSessionEnabled = false;
|
|
3647
|
+
this.previewSessionAlertPending = false;
|
|
3648
|
+
this.isLeavingPreviewForIncomingLink = false;
|
|
3649
|
+
this.implementation.previewSession = false;
|
|
3650
|
+
this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
|
|
3651
|
+
this.shakeChannelSelectorEnabled = this.getConfig().getBoolean("allowShakeChannelSelector", false);
|
|
3652
|
+
this.shakeMenuGesture = normalizedShakeMenuGesture(this.getConfig().getString("shakeMenuGesture", SHAKE_MENU_GESTURE_SHAKE));
|
|
3653
|
+
this.syncShakeMenuLifecycle();
|
|
3654
|
+
this.restorePreviewPreviousAppId();
|
|
3655
|
+
this.restorePreviewPreviousDefaultChannel();
|
|
3656
|
+
this.implementation.setPreviewFallbackBundle(null);
|
|
3657
|
+
this.implementation.setNextBundle(null);
|
|
3658
|
+
this.clearPreviewSessionPreferences();
|
|
3659
|
+
synchronized (this.previewSessionsLock) {
|
|
3660
|
+
this.editor.remove(PREVIEW_SESSIONS_PREF_KEY);
|
|
3661
|
+
this.editor.apply();
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
|
|
3665
|
+
private void restorePreviewPreviousNextBundle() {
|
|
3666
|
+
final String previousNextBundleId = this.prefs.getString(PREVIEW_PREVIOUS_NEXT_BUNDLE_PREF_KEY, null);
|
|
3667
|
+
if (previousNextBundleId == null || previousNextBundleId.isEmpty()) {
|
|
3668
|
+
this.implementation.setNextBundle(null);
|
|
3669
|
+
return;
|
|
3670
|
+
}
|
|
3671
|
+
if (!this.implementation.setNextBundle(previousNextBundleId)) {
|
|
3672
|
+
logger.warn("Could not restore pre-preview next bundle: " + previousNextBundleId);
|
|
3673
|
+
this.implementation.setNextBundle(null);
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
private void ensureShakeMenuStarted() {
|
|
3678
|
+
if (shakeMenu != null && !shakeMenu.usesGesture(this.shakeMenuGesture)) {
|
|
3679
|
+
try {
|
|
3680
|
+
shakeMenu.stop();
|
|
3681
|
+
shakeMenu = null;
|
|
3682
|
+
logger.info("Shake menu restarted for " + this.shakeMenuGesture + " gesture");
|
|
3683
|
+
} catch (Exception e) {
|
|
3684
|
+
logger.error("Failed to restart shake menu: " + e.getMessage());
|
|
3685
|
+
return;
|
|
3686
|
+
}
|
|
3687
|
+
}
|
|
3688
|
+
|
|
3689
|
+
if (getActivity() instanceof com.getcapacitor.BridgeActivity && shakeMenu == null) {
|
|
3690
|
+
try {
|
|
3691
|
+
shakeMenu = new ShakeMenu(this, (com.getcapacitor.BridgeActivity) getActivity(), logger, this.shakeMenuGesture);
|
|
3692
|
+
logger.info("Shake menu initialized with " + this.shakeMenuGesture + " gesture");
|
|
3693
|
+
} catch (Exception e) {
|
|
3694
|
+
logger.error("Failed to initialize shake menu: " + e.getMessage());
|
|
3695
|
+
}
|
|
3696
|
+
}
|
|
3697
|
+
}
|
|
3698
|
+
|
|
3699
|
+
private void syncShakeMenuLifecycle() {
|
|
3700
|
+
if (this.shouldListenForShake()) {
|
|
3701
|
+
this.ensureShakeMenuStarted();
|
|
3702
|
+
} else if (shakeMenu != null) {
|
|
3703
|
+
try {
|
|
3704
|
+
shakeMenu.stop();
|
|
3705
|
+
shakeMenu = null;
|
|
3706
|
+
logger.info("Shake menu stopped");
|
|
3707
|
+
} catch (Exception e) {
|
|
3708
|
+
logger.error("Failed to stop shake menu: " + e.getMessage());
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
|
|
3713
|
+
private boolean shouldListenForShake() {
|
|
3714
|
+
return Boolean.TRUE.equals(this.shakeMenuEnabled) || Boolean.TRUE.equals(this.shakeChannelSelectorEnabled);
|
|
3715
|
+
}
|
|
3716
|
+
|
|
3717
|
+
private void showPreviewSessionNoticeIfNeeded() {
|
|
3718
|
+
if (!Boolean.TRUE.equals(this.previewSessionEnabled) || !Boolean.TRUE.equals(this.previewSessionAlertPending)) {
|
|
3719
|
+
return;
|
|
3720
|
+
}
|
|
3721
|
+
this.previewSessionAlertPending = false;
|
|
3722
|
+
this.editor.putBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, false);
|
|
3723
|
+
this.editor.apply();
|
|
3724
|
+
|
|
3725
|
+
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
|
3726
|
+
try {
|
|
3727
|
+
if (!Boolean.TRUE.equals(this.previewSessionEnabled)) {
|
|
3728
|
+
return;
|
|
3729
|
+
}
|
|
3730
|
+
if (getActivity() == null || getActivity().isFinishing()) {
|
|
3731
|
+
this.previewSessionAlertPending = true;
|
|
3732
|
+
this.editor.putBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, true);
|
|
3733
|
+
this.editor.apply();
|
|
3734
|
+
return;
|
|
3735
|
+
}
|
|
3736
|
+
|
|
3737
|
+
new AlertDialog.Builder(getActivity())
|
|
3738
|
+
.setTitle("Preview started")
|
|
3739
|
+
.setMessage("Shake your device anytime to reload or leave the test app.")
|
|
3740
|
+
.setPositiveButton("Got it", (dialog, which) -> dialog.dismiss())
|
|
3741
|
+
.show();
|
|
3742
|
+
} catch (final Exception e) {
|
|
3743
|
+
this.previewSessionAlertPending = true;
|
|
3744
|
+
this.editor.putBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, true);
|
|
3745
|
+
this.editor.apply();
|
|
3746
|
+
logger.warn("Could not show preview session notice: " + e.getMessage());
|
|
1393
3747
|
}
|
|
1394
|
-
}
|
|
1395
|
-
logger.error("Could not set id " + id + " " + e.getMessage());
|
|
1396
|
-
call.reject("Could not set id " + id, e);
|
|
1397
|
-
}
|
|
3748
|
+
}, 600);
|
|
1398
3749
|
}
|
|
1399
3750
|
|
|
1400
3751
|
@PluginMethod
|
|
@@ -1478,52 +3829,236 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1478
3829
|
@PluginMethod
|
|
1479
3830
|
public void getLatest(final PluginCall call) {
|
|
1480
3831
|
final String channel = call.getString("channel");
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
3832
|
+
final boolean includeBundleSize = call.getBoolean("includeBundleSize", false);
|
|
3833
|
+
final String previewAppId = this.normalizePreviewAppId(call.getString("appId"));
|
|
3834
|
+
final boolean hasPreviewAppId = previewAppId != null;
|
|
3835
|
+
if (hasPreviewAppId && !Boolean.TRUE.equals(this.allowPreview)) {
|
|
3836
|
+
logger.error("getLatest preview override not allowed set allowPreview in your config to true to enable it");
|
|
3837
|
+
call.reject("getLatest preview override not allowed");
|
|
3838
|
+
return;
|
|
3839
|
+
}
|
|
3840
|
+
|
|
3841
|
+
final Callback latestCallback = (res) -> {
|
|
3842
|
+
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
3843
|
+
if (jsRes.has("error") || jsRes.has("kind")) {
|
|
3844
|
+
String error = jsRes.has("error") ? jsRes.getString("error") : "";
|
|
3845
|
+
String errorMessage = jsRes.has("message") ? jsRes.getString("message") : "server did not provide a message";
|
|
3846
|
+
String kind = CapacitorUpdaterPlugin.this.getUpdateResponseKind(jsRes.has("kind") ? jsRes.getString("kind") : null);
|
|
3847
|
+
String latestVersion = jsRes.has("version") ? jsRes.getString("version") : "";
|
|
3848
|
+
jsRes.put("kind", kind);
|
|
3849
|
+
CapacitorUpdaterPlugin.this.notifyBreakingEventsIfNeeded(jsRes, latestVersion);
|
|
3850
|
+
if ("failed".equals(kind)) {
|
|
1487
3851
|
logger.error("getLatest failed with error: " + error + ", message: " + errorMessage);
|
|
1488
|
-
call.reject(
|
|
1489
|
-
return;
|
|
1490
|
-
} else if (jsRes.has("message")) {
|
|
1491
|
-
call.reject(jsRes.getString("message"));
|
|
1492
|
-
return;
|
|
3852
|
+
call.reject(error.isEmpty() ? errorMessage : error);
|
|
1493
3853
|
} else {
|
|
3854
|
+
if (!jsRes.has("version") || jsRes.getString("version").isEmpty()) {
|
|
3855
|
+
jsRes.put("version", CapacitorUpdaterPlugin.this.implementation.getCurrentBundle().getVersionName());
|
|
3856
|
+
}
|
|
3857
|
+
logger.info("getLatest returned " + kind + ": " + errorMessage);
|
|
1494
3858
|
call.resolve(jsRes);
|
|
1495
3859
|
}
|
|
1496
|
-
|
|
1497
|
-
|
|
3860
|
+
return;
|
|
3861
|
+
} else if (jsRes.has("message")) {
|
|
3862
|
+
String latestVersion = jsRes.has("version") ? jsRes.getString("version") : "";
|
|
3863
|
+
CapacitorUpdaterPlugin.this.notifyBreakingEventsIfNeeded(jsRes, latestVersion);
|
|
3864
|
+
call.reject(jsRes.getString("message"));
|
|
3865
|
+
return;
|
|
3866
|
+
} else {
|
|
3867
|
+
if (includeBundleSize) {
|
|
3868
|
+
CapacitorUpdaterPlugin.this.attachBundleSize(jsRes);
|
|
3869
|
+
}
|
|
3870
|
+
call.resolve(jsRes);
|
|
3871
|
+
}
|
|
3872
|
+
};
|
|
3873
|
+
|
|
3874
|
+
startNewThread(() -> {
|
|
3875
|
+
if (hasPreviewAppId) {
|
|
3876
|
+
CapacitorUpdaterPlugin.this.implementation.getLatest(
|
|
3877
|
+
CapacitorUpdaterPlugin.this.updateUrl,
|
|
3878
|
+
channel,
|
|
3879
|
+
previewAppId,
|
|
3880
|
+
latestCallback
|
|
3881
|
+
);
|
|
3882
|
+
return;
|
|
3883
|
+
}
|
|
3884
|
+
CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, channel, latestCallback);
|
|
3885
|
+
});
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3888
|
+
private void attachBundleSize(final JSObject latest) {
|
|
3889
|
+
try {
|
|
3890
|
+
final JSONArray manifest = latest.optJSONArray("manifest");
|
|
3891
|
+
if (manifest == null || manifest.length() == 0) {
|
|
3892
|
+
return;
|
|
3893
|
+
}
|
|
3894
|
+
final String sessionKey = latest.optString("sessionKey", "");
|
|
3895
|
+
final JSONObject missing = this.implementation.missingBundleFilesResult(manifest, sessionKey);
|
|
3896
|
+
final JSONArray missingManifest = missing.getJSONArray("missing");
|
|
3897
|
+
final JSONObject size = this.implementation.getBundleDownloadSize(
|
|
3898
|
+
this.updateUrl,
|
|
3899
|
+
latest.optString("version", ""),
|
|
3900
|
+
missingManifest
|
|
3901
|
+
);
|
|
3902
|
+
latest.put("missing", missing);
|
|
3903
|
+
latest.put("downloadSize", size);
|
|
3904
|
+
} catch (Exception e) {
|
|
3905
|
+
logger.error("Failed to attach bundle size to getLatest result");
|
|
3906
|
+
logger.debug("Error: " + e.getMessage());
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
3909
|
+
|
|
3910
|
+
@PluginMethod
|
|
3911
|
+
public void getMissingBundleFiles(final PluginCall call) {
|
|
3912
|
+
final JSONArray manifest = call.getData().optJSONArray("manifest");
|
|
3913
|
+
if (manifest == null) {
|
|
3914
|
+
call.reject("getMissingBundleFiles called without manifest");
|
|
3915
|
+
return;
|
|
3916
|
+
}
|
|
3917
|
+
String sessionKey = call.getString("sessionKey");
|
|
3918
|
+
if (sessionKey == null) {
|
|
3919
|
+
sessionKey = "";
|
|
3920
|
+
}
|
|
3921
|
+
final String finalSessionKey = sessionKey;
|
|
3922
|
+
startNewThread(() -> {
|
|
3923
|
+
try {
|
|
3924
|
+
call.resolve(jsonObjectToJSObject(this.implementation.missingBundleFilesResult(manifest, finalSessionKey)));
|
|
3925
|
+
} catch (Exception e) {
|
|
3926
|
+
call.reject("Could not get missing bundle files", e);
|
|
3927
|
+
}
|
|
3928
|
+
});
|
|
3929
|
+
}
|
|
3930
|
+
|
|
3931
|
+
@PluginMethod
|
|
3932
|
+
public void getBundleDownloadSize(final PluginCall call) {
|
|
3933
|
+
final JSONArray manifest = call.getData().optJSONArray("manifest");
|
|
3934
|
+
if (manifest == null) {
|
|
3935
|
+
call.reject("getBundleDownloadSize called without manifest");
|
|
3936
|
+
return;
|
|
3937
|
+
}
|
|
3938
|
+
final String version = call.getData().optString("version", "");
|
|
3939
|
+
startNewThread(() -> {
|
|
3940
|
+
try {
|
|
3941
|
+
final JSONObject size = this.implementation.getBundleDownloadSize(this.updateUrl, version, manifest);
|
|
3942
|
+
call.resolve(jsonObjectToJSObject(size));
|
|
3943
|
+
} catch (Exception e) {
|
|
3944
|
+
call.reject("Could not get bundle download size", e);
|
|
3945
|
+
}
|
|
3946
|
+
});
|
|
3947
|
+
}
|
|
3948
|
+
|
|
3949
|
+
public String triggerBackgroundUpdateCheck() {
|
|
3950
|
+
if (this.updateUrl == null || this.updateUrl.isEmpty() || !this.isValidURL(this.updateUrl)) {
|
|
3951
|
+
logger.error("Error no url or wrong format");
|
|
3952
|
+
return "unavailable";
|
|
3953
|
+
}
|
|
3954
|
+
if (this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
3955
|
+
return "preview_session";
|
|
3956
|
+
}
|
|
3957
|
+
if (this.isDownloadStuckOrTimedOut()) {
|
|
3958
|
+
logger.info("Download already in progress, skipping duplicate download request");
|
|
3959
|
+
return "already_running";
|
|
3960
|
+
}
|
|
3961
|
+
this.backgroundDownload();
|
|
3962
|
+
return "queued";
|
|
3963
|
+
}
|
|
3964
|
+
|
|
3965
|
+
@PluginMethod
|
|
3966
|
+
public void triggerUpdateCheck(final PluginCall call) {
|
|
3967
|
+
final String status = this.triggerBackgroundUpdateCheck();
|
|
3968
|
+
final JSObject ret = new JSObject();
|
|
3969
|
+
ret.put("status", status);
|
|
3970
|
+
ret.put("queued", "queued".equals(status));
|
|
3971
|
+
call.resolve(ret);
|
|
3972
|
+
}
|
|
3973
|
+
|
|
3974
|
+
private boolean _reset(final Boolean toLastSuccessful, final Boolean usePendingBundle) {
|
|
3975
|
+
return this.performReset(toLastSuccessful, usePendingBundle, false);
|
|
1498
3976
|
}
|
|
1499
3977
|
|
|
1500
|
-
private boolean
|
|
3978
|
+
private boolean performReset(final Boolean toLastSuccessful, final Boolean usePendingBundle, final boolean internal) {
|
|
1501
3979
|
final BundleInfo fallback = this.implementation.getFallbackBundle();
|
|
1502
|
-
this.implementation.
|
|
3980
|
+
final BundleInfo pending = this.implementation.getNextBundle();
|
|
3981
|
+
final CapgoUpdater.ResetState previousState = this.implementation.captureResetState();
|
|
3982
|
+
final String previousBundleName = this.implementation.getCurrentBundle().getVersionName();
|
|
3983
|
+
|
|
3984
|
+
if (Boolean.TRUE.equals(usePendingBundle)) {
|
|
3985
|
+
if (pending == null || pending.isErrorStatus()) {
|
|
3986
|
+
logger.error("No pending bundle available to reset to");
|
|
3987
|
+
return false;
|
|
3988
|
+
}
|
|
3989
|
+
if (!this.implementation.canSet(pending)) {
|
|
3990
|
+
logger.error("Pending bundle is not installable");
|
|
3991
|
+
return false;
|
|
3992
|
+
}
|
|
3993
|
+
this.implementation.prepareResetStateForTransition();
|
|
3994
|
+
logger.info("Resetting to pending bundle: " + pending.getVersionName());
|
|
3995
|
+
final boolean didApplyPendingBundle;
|
|
3996
|
+
if (pending.isBuiltin()) {
|
|
3997
|
+
didApplyPendingBundle = true;
|
|
3998
|
+
} else {
|
|
3999
|
+
didApplyPendingBundle = this.implementation.set(pending);
|
|
4000
|
+
}
|
|
4001
|
+
if (didApplyPendingBundle && this._reload()) {
|
|
4002
|
+
this.implementation.finalizeResetTransition(previousBundleName, internal);
|
|
4003
|
+
this.notifyBundleSet(pending);
|
|
4004
|
+
this.implementation.setNextBundle(null);
|
|
4005
|
+
return true;
|
|
4006
|
+
}
|
|
4007
|
+
this.implementation.restoreResetState(previousState);
|
|
4008
|
+
this.restoreLiveBundleStateAfterFailedReload();
|
|
4009
|
+
return false;
|
|
4010
|
+
}
|
|
1503
4011
|
|
|
1504
|
-
if (toLastSuccessful && !fallback.isBuiltin()) {
|
|
1505
|
-
|
|
1506
|
-
|
|
4012
|
+
if (Boolean.TRUE.equals(toLastSuccessful) && !fallback.isBuiltin()) {
|
|
4013
|
+
if (this.implementation.canSet(fallback)) {
|
|
4014
|
+
this.implementation.prepareResetStateForTransition();
|
|
4015
|
+
logger.info("Resetting to: " + fallback);
|
|
4016
|
+
if (this.implementation.set(fallback) && this._reload()) {
|
|
4017
|
+
this.implementation.finalizeResetTransition(previousBundleName, internal);
|
|
4018
|
+
this.notifyBundleSet(fallback);
|
|
4019
|
+
return true;
|
|
4020
|
+
}
|
|
4021
|
+
if (!internal) {
|
|
4022
|
+
this.implementation.restoreResetState(previousState);
|
|
4023
|
+
this.restoreLiveBundleStateAfterFailedReload();
|
|
4024
|
+
return false;
|
|
4025
|
+
}
|
|
4026
|
+
logger.warn("Fallback reload failed during internal reset, resetting to native instead");
|
|
4027
|
+
} else {
|
|
4028
|
+
logger.warn("Fallback bundle is not installable, resetting to native instead");
|
|
4029
|
+
}
|
|
1507
4030
|
}
|
|
1508
4031
|
|
|
4032
|
+
this.implementation.prepareResetStateForTransition();
|
|
1509
4033
|
logger.info("Resetting to native.");
|
|
1510
|
-
|
|
4034
|
+
if (this._reload()) {
|
|
4035
|
+
this.implementation.finalizeResetTransition(previousBundleName, internal);
|
|
4036
|
+
return true;
|
|
4037
|
+
}
|
|
4038
|
+
if (!internal) {
|
|
4039
|
+
this.implementation.restoreResetState(previousState);
|
|
4040
|
+
this.restoreLiveBundleStateAfterFailedReload();
|
|
4041
|
+
}
|
|
4042
|
+
return false;
|
|
1511
4043
|
}
|
|
1512
4044
|
|
|
1513
4045
|
@PluginMethod
|
|
1514
4046
|
public void reset(final PluginCall call) {
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
call.
|
|
1519
|
-
|
|
4047
|
+
startNewThread(() -> {
|
|
4048
|
+
try {
|
|
4049
|
+
final Boolean toLastSuccessful = call.getBoolean("toLastSuccessful", false);
|
|
4050
|
+
final Boolean usePendingBundle = call.getBoolean("usePendingBundle", false);
|
|
4051
|
+
if (this._reset(toLastSuccessful, usePendingBundle)) {
|
|
4052
|
+
call.resolve();
|
|
4053
|
+
return;
|
|
4054
|
+
}
|
|
4055
|
+
logger.error("Reset failed");
|
|
4056
|
+
call.reject("Reset failed");
|
|
4057
|
+
} catch (final Exception e) {
|
|
4058
|
+
logger.error("Reset failed " + e.getMessage());
|
|
4059
|
+
call.reject("Reset failed", e);
|
|
1520
4060
|
}
|
|
1521
|
-
|
|
1522
|
-
call.reject("Reset failed");
|
|
1523
|
-
} catch (final Exception e) {
|
|
1524
|
-
logger.error("Reset failed " + e.getMessage());
|
|
1525
|
-
call.reject("Reset failed", e);
|
|
1526
|
-
}
|
|
4061
|
+
});
|
|
1527
4062
|
}
|
|
1528
4063
|
|
|
1529
4064
|
@PluginMethod
|
|
@@ -1587,14 +4122,41 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1587
4122
|
@Override
|
|
1588
4123
|
public void run() {
|
|
1589
4124
|
try {
|
|
4125
|
+
if (CapacitorUpdaterPlugin.this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
4126
|
+
return;
|
|
4127
|
+
}
|
|
1590
4128
|
CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, null, (res) -> {
|
|
4129
|
+
if (CapacitorUpdaterPlugin.this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
4130
|
+
return;
|
|
4131
|
+
}
|
|
1591
4132
|
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
1592
|
-
if (jsRes.has("error")) {
|
|
1593
|
-
|
|
4133
|
+
if (jsRes.has("error") || jsRes.has("kind")) {
|
|
4134
|
+
final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
|
|
4135
|
+
String error = jsRes.has("error") ? jsRes.getString("error") : "";
|
|
1594
4136
|
String errorMessage = jsRes.has("message")
|
|
1595
4137
|
? jsRes.getString("message")
|
|
1596
4138
|
: "server did not provide a message";
|
|
1597
|
-
|
|
4139
|
+
int statusCode = jsRes.has("statusCode") ? jsRes.optInt("statusCode", 0) : 0;
|
|
4140
|
+
String kind = CapacitorUpdaterPlugin.this.getUpdateResponseKind(
|
|
4141
|
+
jsRes.has("kind") ? jsRes.getString("kind") : null
|
|
4142
|
+
);
|
|
4143
|
+
String latestVersion = jsRes.has("version") ? jsRes.getString("version") : current.getVersionName();
|
|
4144
|
+
CapacitorUpdaterPlugin.this.notifyUpdateCheckResult(
|
|
4145
|
+
kind,
|
|
4146
|
+
error,
|
|
4147
|
+
errorMessage,
|
|
4148
|
+
statusCode,
|
|
4149
|
+
latestVersion,
|
|
4150
|
+
current
|
|
4151
|
+
);
|
|
4152
|
+
|
|
4153
|
+
if ("failed".equals(kind)) {
|
|
4154
|
+
logger.error("getLatest failed with error: " + error + ", message: " + errorMessage);
|
|
4155
|
+
} else if ("blocked".equals(kind)) {
|
|
4156
|
+
logger.info("Update check blocked with error: " + error);
|
|
4157
|
+
} else {
|
|
4158
|
+
logger.info("No new version available");
|
|
4159
|
+
}
|
|
1598
4160
|
} else if (jsRes.has("version")) {
|
|
1599
4161
|
String newVersion = jsRes.getString("version");
|
|
1600
4162
|
String currentVersion = String.valueOf(CapacitorUpdaterPlugin.this.implementation.getCurrentBundle());
|
|
@@ -1629,6 +4191,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1629
4191
|
logger.info("semaphoreReady countDown");
|
|
1630
4192
|
this.semaphoreDown();
|
|
1631
4193
|
logger.info("semaphoreReady countDown done");
|
|
4194
|
+
this.clearIncomingPreviewTransition();
|
|
4195
|
+
this.hidePreviewTransitionLoader("notify-app-ready");
|
|
1632
4196
|
final JSObject ret = new JSObject();
|
|
1633
4197
|
ret.put("bundle", InternalUtils.mapToJSObject(bundle.toJSONMap()));
|
|
1634
4198
|
call.resolve(ret);
|
|
@@ -1676,6 +4240,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1676
4240
|
}
|
|
1677
4241
|
|
|
1678
4242
|
private Boolean _isAutoUpdateEnabled() {
|
|
4243
|
+
if (this.isPreviewSessionStateActive()) {
|
|
4244
|
+
return false;
|
|
4245
|
+
}
|
|
1679
4246
|
final CapConfig config = CapConfig.loadDefault(this.getActivity());
|
|
1680
4247
|
String serverUrl = config.getServerUrl();
|
|
1681
4248
|
if (serverUrl != null && !serverUrl.isEmpty()) {
|
|
@@ -1716,11 +4283,27 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1716
4283
|
}
|
|
1717
4284
|
|
|
1718
4285
|
private void checkAppReady() {
|
|
4286
|
+
this.checkAppReady(this.resolveAppReadyCheckTimeoutMs());
|
|
4287
|
+
}
|
|
4288
|
+
|
|
4289
|
+
synchronized boolean shouldInterruptAppReadyCheck(final Thread existingCheck, final Thread currentThread) {
|
|
4290
|
+
return existingCheck != null && existingCheck != currentThread;
|
|
4291
|
+
}
|
|
4292
|
+
|
|
4293
|
+
synchronized void clearAppReadyCheckIfCurrent(final Thread expectedThread) {
|
|
4294
|
+
if (this.appReadyCheck == expectedThread) {
|
|
4295
|
+
this.appReadyCheck = null;
|
|
4296
|
+
}
|
|
4297
|
+
}
|
|
4298
|
+
|
|
4299
|
+
private void checkAppReady(final long waitTimeMs) {
|
|
1719
4300
|
try {
|
|
1720
|
-
|
|
1721
|
-
|
|
4301
|
+
final Thread currentThread = Thread.currentThread();
|
|
4302
|
+
final Thread existingCheck = this.appReadyCheck;
|
|
4303
|
+
if (this.shouldInterruptAppReadyCheck(existingCheck, currentThread)) {
|
|
4304
|
+
existingCheck.interrupt();
|
|
1722
4305
|
}
|
|
1723
|
-
this.appReadyCheck = startNewThread(new DeferredNotifyAppReadyCheck());
|
|
4306
|
+
this.appReadyCheck = startNewThread(new DeferredNotifyAppReadyCheck(waitTimeMs));
|
|
1724
4307
|
} catch (final Exception e) {
|
|
1725
4308
|
logger.error("Failed to start " + DeferredNotifyAppReadyCheck.class.getName() + " " + e.getMessage());
|
|
1726
4309
|
}
|
|
@@ -1735,6 +4318,35 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1735
4318
|
}
|
|
1736
4319
|
}
|
|
1737
4320
|
|
|
4321
|
+
static String normalizedUpdateResponseKind(final String kind) {
|
|
4322
|
+
if ("up_to_date".equals(kind) || "blocked".equals(kind) || "failed".equals(kind)) {
|
|
4323
|
+
return kind;
|
|
4324
|
+
}
|
|
4325
|
+
return "failed";
|
|
4326
|
+
}
|
|
4327
|
+
|
|
4328
|
+
private String getUpdateResponseKind(final String kind) {
|
|
4329
|
+
return normalizedUpdateResponseKind(kind);
|
|
4330
|
+
}
|
|
4331
|
+
|
|
4332
|
+
private void notifyUpdateCheckResult(
|
|
4333
|
+
final String kind,
|
|
4334
|
+
final String error,
|
|
4335
|
+
final String message,
|
|
4336
|
+
final int statusCode,
|
|
4337
|
+
final String version,
|
|
4338
|
+
final BundleInfo current
|
|
4339
|
+
) {
|
|
4340
|
+
JSObject ret = new JSObject();
|
|
4341
|
+
ret.put("kind", kind);
|
|
4342
|
+
ret.put("error", error);
|
|
4343
|
+
ret.put("message", message);
|
|
4344
|
+
ret.put("statusCode", statusCode);
|
|
4345
|
+
ret.put("version", version);
|
|
4346
|
+
ret.put("bundle", InternalUtils.mapToJSObject(current.toJSONMap()));
|
|
4347
|
+
this.notifyListeners("updateCheckResult", ret);
|
|
4348
|
+
}
|
|
4349
|
+
|
|
1738
4350
|
private void ensureBridgeSet() {
|
|
1739
4351
|
if (this.bridge != null && this.bridge.getWebView() != null) {
|
|
1740
4352
|
logger.setBridge(this.bridge);
|
|
@@ -1772,11 +4384,36 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1772
4384
|
String latestVersionName,
|
|
1773
4385
|
BundleInfo current,
|
|
1774
4386
|
Boolean error,
|
|
1775
|
-
Boolean
|
|
4387
|
+
Boolean plannedDirectUpdate,
|
|
1776
4388
|
String failureAction,
|
|
1777
4389
|
String failureEvent,
|
|
1778
4390
|
boolean shouldSendStats
|
|
1779
4391
|
) {
|
|
4392
|
+
endBackGroundTaskWithNotif(
|
|
4393
|
+
msg,
|
|
4394
|
+
latestVersionName,
|
|
4395
|
+
current,
|
|
4396
|
+
error,
|
|
4397
|
+
plannedDirectUpdate,
|
|
4398
|
+
failureAction,
|
|
4399
|
+
failureEvent,
|
|
4400
|
+
shouldSendStats,
|
|
4401
|
+
true
|
|
4402
|
+
);
|
|
4403
|
+
}
|
|
4404
|
+
|
|
4405
|
+
private void endBackGroundTaskWithNotif(
|
|
4406
|
+
String msg,
|
|
4407
|
+
String latestVersionName,
|
|
4408
|
+
BundleInfo current,
|
|
4409
|
+
Boolean error,
|
|
4410
|
+
Boolean plannedDirectUpdate,
|
|
4411
|
+
String failureAction,
|
|
4412
|
+
String failureEvent,
|
|
4413
|
+
boolean shouldSendStats,
|
|
4414
|
+
boolean shouldNotifyNoNeedUpdate
|
|
4415
|
+
) {
|
|
4416
|
+
this.consumeOnLaunchDirectUpdateAttempt(Boolean.TRUE.equals(plannedDirectUpdate));
|
|
1780
4417
|
if (error) {
|
|
1781
4418
|
logger.info(
|
|
1782
4419
|
"endBackGroundTaskWithNotif error: " +
|
|
@@ -1793,15 +4430,22 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1793
4430
|
ret.put("version", latestVersionName);
|
|
1794
4431
|
this.notifyListeners(failureEvent, ret);
|
|
1795
4432
|
}
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
4433
|
+
if (shouldNotifyNoNeedUpdate) {
|
|
4434
|
+
final JSObject ret = new JSObject();
|
|
4435
|
+
ret.put("bundle", InternalUtils.mapToJSObject(current.toJSONMap()));
|
|
4436
|
+
this.notifyListeners("noNeedUpdate", ret);
|
|
4437
|
+
}
|
|
4438
|
+
this.sendReadyToJs(current, msg, plannedDirectUpdate);
|
|
1800
4439
|
this.backgroundDownloadTask = null;
|
|
1801
4440
|
this.downloadStartTimeMs = 0;
|
|
1802
4441
|
logger.info("endBackGroundTaskWithNotif " + msg);
|
|
1803
4442
|
}
|
|
1804
4443
|
|
|
4444
|
+
private void clearBackgroundDownloadState() {
|
|
4445
|
+
this.backgroundDownloadTask = null;
|
|
4446
|
+
this.downloadStartTimeMs = 0;
|
|
4447
|
+
}
|
|
4448
|
+
|
|
1805
4449
|
private boolean isDownloadStuckOrTimedOut() {
|
|
1806
4450
|
if (this.backgroundDownloadTask == null || !this.backgroundDownloadTask.isAlive()) {
|
|
1807
4451
|
return false;
|
|
@@ -1828,46 +4472,69 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1828
4472
|
}
|
|
1829
4473
|
|
|
1830
4474
|
private Thread backgroundDownload() {
|
|
4475
|
+
if (this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
4476
|
+
return null;
|
|
4477
|
+
}
|
|
1831
4478
|
final boolean plannedDirectUpdate = this.shouldUseDirectUpdate();
|
|
1832
4479
|
final boolean initialDirectUpdateAllowed = this.isDirectUpdateCurrentlyAllowed(plannedDirectUpdate);
|
|
1833
|
-
this.implementation.directUpdate = initialDirectUpdateAllowed;
|
|
1834
4480
|
final String messageUpdate = initialDirectUpdateAllowed
|
|
1835
4481
|
? "Update will occur now."
|
|
1836
|
-
:
|
|
4482
|
+
: this.shouldAutoSetNextBundle()
|
|
4483
|
+
? "Update will occur next time app moves to background."
|
|
4484
|
+
: "Update will be downloaded and made available.";
|
|
1837
4485
|
Thread newTask = startNewThread(() -> {
|
|
1838
4486
|
// Wait for cleanup to complete before starting download
|
|
1839
4487
|
waitForCleanupIfNeeded();
|
|
4488
|
+
if (CapacitorUpdaterPlugin.this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
4489
|
+
CapacitorUpdaterPlugin.this.clearBackgroundDownloadState();
|
|
4490
|
+
return;
|
|
4491
|
+
}
|
|
1840
4492
|
logger.info("Check for update via: " + CapacitorUpdaterPlugin.this.updateUrl);
|
|
1841
4493
|
try {
|
|
1842
4494
|
CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, null, (res) -> {
|
|
4495
|
+
if (CapacitorUpdaterPlugin.this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
4496
|
+
CapacitorUpdaterPlugin.this.clearBackgroundDownloadState();
|
|
4497
|
+
return;
|
|
4498
|
+
}
|
|
1843
4499
|
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
1844
4500
|
final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
|
|
1845
4501
|
|
|
1846
4502
|
// Handle network errors and other failures first
|
|
1847
|
-
if (jsRes.has("error")) {
|
|
1848
|
-
String error = jsRes.getString("error");
|
|
4503
|
+
if (jsRes.has("error") || jsRes.has("kind")) {
|
|
4504
|
+
String error = jsRes.has("error") ? jsRes.getString("error") : "";
|
|
1849
4505
|
String errorMessage = jsRes.has("message") ? jsRes.getString("message") : "server did not provide a message";
|
|
1850
4506
|
int statusCode = jsRes.has("statusCode") ? jsRes.optInt("statusCode", 0) : 0;
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
logger.error(
|
|
1854
|
-
"getLatest failed with error: " + error + ", message: " + errorMessage + ", statusCode: " + statusCode
|
|
1855
|
-
);
|
|
4507
|
+
String kind = CapacitorUpdaterPlugin.this.getUpdateResponseKind(jsRes.has("kind") ? jsRes.getString("kind") : null);
|
|
1856
4508
|
String latestVersion = jsRes.has("version") ? jsRes.getString("version") : current.getVersionName();
|
|
4509
|
+
CapacitorUpdaterPlugin.this.notifyUpdateCheckResult(kind, error, errorMessage, statusCode, latestVersion, current);
|
|
4510
|
+
CapacitorUpdaterPlugin.this.notifyBreakingEventsIfNeeded(
|
|
4511
|
+
jsRes,
|
|
4512
|
+
jsRes.has("version") ? jsRes.getString("version") : ""
|
|
4513
|
+
);
|
|
4514
|
+
|
|
4515
|
+
if ("up_to_date".equals(kind)) {
|
|
4516
|
+
logger.info("No new version available");
|
|
4517
|
+
} else if ("blocked".equals(kind)) {
|
|
4518
|
+
logger.info("Update check blocked with error: " + error);
|
|
4519
|
+
} else {
|
|
4520
|
+
logger.error(
|
|
4521
|
+
"getLatest failed with error: " + error + ", message: " + errorMessage + ", statusCode: " + statusCode
|
|
4522
|
+
);
|
|
4523
|
+
}
|
|
1857
4524
|
|
|
4525
|
+
boolean isFailure = "failed".equals(kind);
|
|
1858
4526
|
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
1859
4527
|
errorMessage,
|
|
1860
4528
|
latestVersion,
|
|
1861
4529
|
current,
|
|
1862
|
-
|
|
4530
|
+
isFailure,
|
|
1863
4531
|
plannedDirectUpdate,
|
|
1864
4532
|
"download_fail",
|
|
1865
4533
|
"downloadFailed",
|
|
1866
|
-
|
|
4534
|
+
isFailure
|
|
1867
4535
|
);
|
|
1868
4536
|
return;
|
|
1869
4537
|
}
|
|
1870
|
-
|
|
1871
4538
|
try {
|
|
1872
4539
|
final String latestVersionName = jsRes.getString("version");
|
|
1873
4540
|
|
|
@@ -1878,7 +4545,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1878
4545
|
);
|
|
1879
4546
|
if (directUpdateAllowedNow) {
|
|
1880
4547
|
logger.info("Direct update to builtin version");
|
|
1881
|
-
this._reset(false);
|
|
4548
|
+
this._reset(false, false);
|
|
1882
4549
|
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
1883
4550
|
"Updated to builtin version",
|
|
1884
4551
|
latestVersionName,
|
|
@@ -1886,7 +4553,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1886
4553
|
false,
|
|
1887
4554
|
true
|
|
1888
4555
|
);
|
|
1889
|
-
} else {
|
|
4556
|
+
} else if (CapacitorUpdaterPlugin.this.shouldAutoSetNextBundle()) {
|
|
1890
4557
|
if (plannedDirectUpdate && !directUpdateAllowedNow) {
|
|
1891
4558
|
logger.info(
|
|
1892
4559
|
"Direct update skipped because splashscreen timeout occurred. Update will be applied later."
|
|
@@ -1898,13 +4565,37 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1898
4565
|
"Next update will be to builtin version",
|
|
1899
4566
|
latestVersionName,
|
|
1900
4567
|
current,
|
|
1901
|
-
false
|
|
4568
|
+
false,
|
|
4569
|
+
plannedDirectUpdate
|
|
4570
|
+
);
|
|
4571
|
+
} else {
|
|
4572
|
+
logger.info("autoUpdate is set to onlyDownload, builtin version will not be set as next bundle");
|
|
4573
|
+
final boolean builtinUpdateAvailable = !current.isBuiltin();
|
|
4574
|
+
if (builtinUpdateAvailable) {
|
|
4575
|
+
final JSObject ret = new JSObject();
|
|
4576
|
+
final BundleInfo builtinBundle = CapacitorUpdaterPlugin.this.implementation.getBundleInfo(
|
|
4577
|
+
BundleInfo.ID_BUILTIN
|
|
4578
|
+
);
|
|
4579
|
+
ret.put("bundle", InternalUtils.mapToJSObject(builtinBundle.toJSONMap()));
|
|
4580
|
+
CapacitorUpdaterPlugin.this.notifyListeners("updateAvailable", ret);
|
|
4581
|
+
}
|
|
4582
|
+
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
4583
|
+
"Latest version is builtin, autoUpdate onlyDownload",
|
|
4584
|
+
latestVersionName,
|
|
4585
|
+
current,
|
|
4586
|
+
false,
|
|
4587
|
+
plannedDirectUpdate,
|
|
4588
|
+
"download_fail",
|
|
4589
|
+
"downloadFailed",
|
|
4590
|
+
true,
|
|
4591
|
+
!builtinUpdateAvailable
|
|
1902
4592
|
);
|
|
1903
4593
|
}
|
|
1904
4594
|
return;
|
|
1905
4595
|
}
|
|
1906
4596
|
|
|
1907
4597
|
if (!jsRes.has("url") || !CapacitorUpdaterPlugin.this.isValidURL(jsRes.getString("url"))) {
|
|
4598
|
+
CapacitorUpdaterPlugin.this.notifyBreakingEventsIfNeeded(jsRes, latestVersionName);
|
|
1908
4599
|
logger.error("Error no url or wrong format");
|
|
1909
4600
|
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
1910
4601
|
"Error no url or wrong format",
|
|
@@ -1934,7 +4625,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1934
4625
|
);
|
|
1935
4626
|
return;
|
|
1936
4627
|
}
|
|
1937
|
-
if (latest.isDownloaded()) {
|
|
4628
|
+
if (latest.isDownloaded() && BundleStatus.DOWNLOADING != latest.getStatus()) {
|
|
1938
4629
|
logger.info("Latest bundle already exists and download is NOT required. " + messageUpdate);
|
|
1939
4630
|
final boolean directUpdateAllowedNow = CapacitorUpdaterPlugin.this.isDirectUpdateCurrentlyAllowed(
|
|
1940
4631
|
plannedDirectUpdate
|
|
@@ -1955,16 +4646,27 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1955
4646
|
);
|
|
1956
4647
|
return;
|
|
1957
4648
|
}
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
4649
|
+
if (
|
|
4650
|
+
CapacitorUpdaterPlugin.this.implementation.set(latest) && CapacitorUpdaterPlugin.this._reload()
|
|
4651
|
+
) {
|
|
4652
|
+
CapacitorUpdaterPlugin.this.notifyBundleSet(latest);
|
|
4653
|
+
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
4654
|
+
"Update installed",
|
|
4655
|
+
latestVersionName,
|
|
4656
|
+
latest,
|
|
4657
|
+
false,
|
|
4658
|
+
true
|
|
4659
|
+
);
|
|
4660
|
+
} else {
|
|
4661
|
+
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
4662
|
+
"Update install failed",
|
|
4663
|
+
latestVersionName,
|
|
4664
|
+
latest,
|
|
4665
|
+
true,
|
|
4666
|
+
true
|
|
4667
|
+
);
|
|
4668
|
+
}
|
|
4669
|
+
} else if (CapacitorUpdaterPlugin.this.shouldAutoSetNextBundle()) {
|
|
1968
4670
|
if (plannedDirectUpdate && !directUpdateAllowedNow) {
|
|
1969
4671
|
logger.info(
|
|
1970
4672
|
"Direct update skipped because splashscreen timeout occurred. Update will install on next background."
|
|
@@ -1976,6 +4678,21 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1976
4678
|
"update downloaded, will install next background",
|
|
1977
4679
|
latestVersionName,
|
|
1978
4680
|
latest,
|
|
4681
|
+
false,
|
|
4682
|
+
plannedDirectUpdate
|
|
4683
|
+
);
|
|
4684
|
+
} else {
|
|
4685
|
+
logger.info("autoUpdate is set to onlyDownload, downloaded update will not be set as next bundle");
|
|
4686
|
+
CapacitorUpdaterPlugin.this.notifyListeners("updateAvailable", ret);
|
|
4687
|
+
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
4688
|
+
"update downloaded, autoUpdate onlyDownload",
|
|
4689
|
+
latestVersionName,
|
|
4690
|
+
current,
|
|
4691
|
+
false,
|
|
4692
|
+
plannedDirectUpdate,
|
|
4693
|
+
"download_fail",
|
|
4694
|
+
"downloadFailed",
|
|
4695
|
+
true,
|
|
1979
4696
|
false
|
|
1980
4697
|
);
|
|
1981
4698
|
}
|
|
@@ -1993,8 +4710,20 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1993
4710
|
}
|
|
1994
4711
|
}
|
|
1995
4712
|
}
|
|
4713
|
+
final boolean retryingInFlightDownload =
|
|
4714
|
+
latest != null &&
|
|
4715
|
+
BundleStatus.DOWNLOADING == latest.getStatus() &&
|
|
4716
|
+
CapacitorUpdaterPlugin.this.isVersionDownloadInProgress(latest.getVersionName());
|
|
4717
|
+
CapacitorUpdaterPlugin.this.consumeOnLaunchDirectUpdateAttempt(plannedDirectUpdate);
|
|
4718
|
+
CapacitorUpdaterPlugin.this.implementation.directUpdate = retryingInFlightDownload
|
|
4719
|
+
? Boolean.TRUE.equals(CapacitorUpdaterPlugin.this.implementation.directUpdate) || initialDirectUpdateAllowed
|
|
4720
|
+
: initialDirectUpdateAllowed;
|
|
1996
4721
|
startNewThread(() -> {
|
|
1997
4722
|
try {
|
|
4723
|
+
if (CapacitorUpdaterPlugin.this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
4724
|
+
CapacitorUpdaterPlugin.this.clearBackgroundDownloadState();
|
|
4725
|
+
return;
|
|
4726
|
+
}
|
|
1998
4727
|
logger.info(
|
|
1999
4728
|
"New bundle: " +
|
|
2000
4729
|
latestVersionName +
|
|
@@ -2016,7 +4745,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2016
4745
|
latestVersionName,
|
|
2017
4746
|
sessionKey,
|
|
2018
4747
|
checksum,
|
|
2019
|
-
manifest
|
|
4748
|
+
manifest,
|
|
4749
|
+
CapacitorUpdaterPlugin.this.shouldAutoSetNextBundle()
|
|
2020
4750
|
);
|
|
2021
4751
|
} else {
|
|
2022
4752
|
// Handle single file download (existing code)
|
|
@@ -2025,7 +4755,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2025
4755
|
latestVersionName,
|
|
2026
4756
|
sessionKey,
|
|
2027
4757
|
checksum,
|
|
2028
|
-
null
|
|
4758
|
+
null,
|
|
4759
|
+
CapacitorUpdaterPlugin.this.shouldAutoSetNextBundle()
|
|
2029
4760
|
);
|
|
2030
4761
|
}
|
|
2031
4762
|
} catch (final Exception e) {
|
|
@@ -2041,7 +4772,13 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2041
4772
|
});
|
|
2042
4773
|
} else {
|
|
2043
4774
|
logger.info("No need to update, " + current.getId() + " is the latest bundle.");
|
|
2044
|
-
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
4775
|
+
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
4776
|
+
"No need to update",
|
|
4777
|
+
latestVersionName,
|
|
4778
|
+
current,
|
|
4779
|
+
false,
|
|
4780
|
+
plannedDirectUpdate
|
|
4781
|
+
);
|
|
2045
4782
|
}
|
|
2046
4783
|
} catch (final Exception e) {
|
|
2047
4784
|
logger.error("error in update check " + e.getMessage());
|
|
@@ -2073,6 +4810,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2073
4810
|
|
|
2074
4811
|
private void installNext() {
|
|
2075
4812
|
try {
|
|
4813
|
+
if (this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
4814
|
+
return;
|
|
4815
|
+
}
|
|
2076
4816
|
String delayUpdatePreferences = prefs.getString(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES, "[]");
|
|
2077
4817
|
ArrayList<DelayCondition> delayConditionList = delayUpdateUtils.parseDelayConditions(delayUpdatePreferences);
|
|
2078
4818
|
if (!delayConditionList.isEmpty()) {
|
|
@@ -2085,15 +4825,18 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2085
4825
|
if (next != null && !next.isErrorStatus() && !next.getId().equals(current.getId())) {
|
|
2086
4826
|
// There is a next bundle waiting for activation
|
|
2087
4827
|
logger.debug("Next bundle is: " + next.getVersionName());
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
4828
|
+
startNewThread(() -> {
|
|
4829
|
+
if (this.implementation.set(next) && this._reload()) {
|
|
4830
|
+
logger.info("Updated to bundle: " + next.getVersionName());
|
|
4831
|
+
this.notifyBundleSet(next);
|
|
4832
|
+
this.implementation.setNextBundle(null);
|
|
4833
|
+
} else {
|
|
4834
|
+
logger.error("Update to bundle: " + next.getVersionName() + " Failed!");
|
|
4835
|
+
}
|
|
4836
|
+
});
|
|
2094
4837
|
}
|
|
2095
4838
|
} catch (final Exception e) {
|
|
2096
|
-
logger.error("Error during
|
|
4839
|
+
logger.error("Error during installNext " + e);
|
|
2097
4840
|
}
|
|
2098
4841
|
}
|
|
2099
4842
|
|
|
@@ -2105,6 +4848,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2105
4848
|
logger.info("Built-in bundle is active. We skip the check for notifyAppReady.");
|
|
2106
4849
|
return;
|
|
2107
4850
|
}
|
|
4851
|
+
if (this.isPreviewSessionStateActive()) {
|
|
4852
|
+
logger.info("Preview session is active. We skip the check for notifyAppReady.");
|
|
4853
|
+
return;
|
|
4854
|
+
}
|
|
2108
4855
|
logger.debug("Current bundle is: " + current);
|
|
2109
4856
|
|
|
2110
4857
|
if (BundleStatus.SUCCESS != current.getStatus()) {
|
|
@@ -2116,7 +4863,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2116
4863
|
this.notifyListeners("updateFailed", ret);
|
|
2117
4864
|
this.implementation.sendStats("update_fail", current.getVersionName());
|
|
2118
4865
|
this.implementation.setError(current);
|
|
2119
|
-
this.
|
|
4866
|
+
this.performReset(true, false, true);
|
|
2120
4867
|
if (CapacitorUpdaterPlugin.this.autoDeleteFailed && !current.isBuiltin()) {
|
|
2121
4868
|
logger.info("Deleting failing bundle: " + current.getVersionName());
|
|
2122
4869
|
try {
|
|
@@ -2135,14 +4882,22 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2135
4882
|
|
|
2136
4883
|
private class DeferredNotifyAppReadyCheck implements Runnable {
|
|
2137
4884
|
|
|
4885
|
+
private final long waitTimeMs;
|
|
4886
|
+
|
|
4887
|
+
DeferredNotifyAppReadyCheck(final long waitTimeMs) {
|
|
4888
|
+
this.waitTimeMs = waitTimeMs;
|
|
4889
|
+
}
|
|
4890
|
+
|
|
2138
4891
|
@Override
|
|
2139
4892
|
public void run() {
|
|
4893
|
+
final Thread currentThread = Thread.currentThread();
|
|
2140
4894
|
try {
|
|
2141
|
-
logger.info("Wait for " +
|
|
2142
|
-
Thread.sleep(
|
|
4895
|
+
logger.info("Wait for " + this.waitTimeMs + "ms, then check for notifyAppReady");
|
|
4896
|
+
Thread.sleep(this.waitTimeMs);
|
|
2143
4897
|
CapacitorUpdaterPlugin.this.checkRevert();
|
|
2144
|
-
CapacitorUpdaterPlugin.this.
|
|
4898
|
+
CapacitorUpdaterPlugin.this.clearAppReadyCheckIfCurrent(currentThread);
|
|
2145
4899
|
} catch (final InterruptedException e) {
|
|
4900
|
+
CapacitorUpdaterPlugin.this.clearAppReadyCheckIfCurrent(currentThread);
|
|
2146
4901
|
logger.info(DeferredNotifyAppReadyCheck.class.getName() + " was interrupted.");
|
|
2147
4902
|
}
|
|
2148
4903
|
}
|
|
@@ -2271,6 +5026,36 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2271
5026
|
}
|
|
2272
5027
|
}
|
|
2273
5028
|
|
|
5029
|
+
@Override
|
|
5030
|
+
protected void handleOnNewIntent(Intent intent) {
|
|
5031
|
+
super.handleOnNewIntent(intent);
|
|
5032
|
+
if (
|
|
5033
|
+
intent == null ||
|
|
5034
|
+
!Intent.ACTION_VIEW.equals(intent.getAction()) ||
|
|
5035
|
+
intent.getData() == null ||
|
|
5036
|
+
!Boolean.TRUE.equals(this.previewSessionEnabled) ||
|
|
5037
|
+
!isPreviewDeepLink(intent.getData()) ||
|
|
5038
|
+
Boolean.TRUE.equals(this.isLeavingPreviewForIncomingLink)
|
|
5039
|
+
) {
|
|
5040
|
+
return;
|
|
5041
|
+
}
|
|
5042
|
+
|
|
5043
|
+
this.isLeavingPreviewForIncomingLink = true;
|
|
5044
|
+
this.showPreviewTransitionLoader("incoming-preview-deeplink");
|
|
5045
|
+
if (getActivity() != null) {
|
|
5046
|
+
getActivity().setIntent(intent);
|
|
5047
|
+
}
|
|
5048
|
+
logger.info("Preview deeplink received while preview session is active; restoring fallback before routing");
|
|
5049
|
+
startNewThread(() -> {
|
|
5050
|
+
final boolean didLeave = this.leavePreviewSessionForIncomingPreviewLink();
|
|
5051
|
+
if (!didLeave) {
|
|
5052
|
+
logger.error("Could not leave preview session before routing incoming preview deeplink");
|
|
5053
|
+
this.isLeavingPreviewForIncomingLink = false;
|
|
5054
|
+
this.hidePreviewTransitionLoader("incoming-preview-deeplink-failed");
|
|
5055
|
+
}
|
|
5056
|
+
});
|
|
5057
|
+
}
|
|
5058
|
+
|
|
2274
5059
|
@Override
|
|
2275
5060
|
public void handleOnStart() {
|
|
2276
5061
|
try {
|
|
@@ -2286,15 +5071,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2286
5071
|
isPreviousMainActivity = true;
|
|
2287
5072
|
}
|
|
2288
5073
|
|
|
2289
|
-
|
|
2290
|
-
if (shakeMenuEnabled && getActivity() instanceof com.getcapacitor.BridgeActivity && shakeMenu == null) {
|
|
2291
|
-
try {
|
|
2292
|
-
shakeMenu = new ShakeMenu(this, (com.getcapacitor.BridgeActivity) getActivity(), logger);
|
|
2293
|
-
logger.info("Shake menu initialized");
|
|
2294
|
-
} catch (Exception e) {
|
|
2295
|
-
logger.error("Failed to initialize shake menu: " + e.getMessage());
|
|
2296
|
-
}
|
|
2297
|
-
}
|
|
5074
|
+
this.syncShakeMenuLifecycle();
|
|
2298
5075
|
} catch (Exception e) {
|
|
2299
5076
|
logger.error("Failed to run handleOnStart: " + e.getMessage());
|
|
2300
5077
|
}
|
|
@@ -2326,6 +5103,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2326
5103
|
backgroundTask.interrupt();
|
|
2327
5104
|
}
|
|
2328
5105
|
this.implementation.activity = getActivity();
|
|
5106
|
+
this.syncShakeMenuLifecycle();
|
|
2329
5107
|
} catch (Exception e) {
|
|
2330
5108
|
logger.error("Failed to run handleOnResume: " + e.getMessage());
|
|
2331
5109
|
}
|
|
@@ -2350,25 +5128,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2350
5128
|
}
|
|
2351
5129
|
|
|
2352
5130
|
this.shakeMenuEnabled = enabled;
|
|
2353
|
-
logger.info("Shake menu " + (enabled ? "enabled" : "disabled"));
|
|
2354
|
-
|
|
2355
|
-
// Manage shake menu instance based on enabled state
|
|
2356
|
-
if (enabled && getActivity() instanceof com.getcapacitor.BridgeActivity && shakeMenu == null) {
|
|
2357
|
-
try {
|
|
2358
|
-
shakeMenu = new ShakeMenu(this, (com.getcapacitor.BridgeActivity) getActivity(), logger);
|
|
2359
|
-
logger.info("Shake menu initialized");
|
|
2360
|
-
} catch (Exception e) {
|
|
2361
|
-
logger.error("Failed to initialize shake menu: " + e.getMessage());
|
|
2362
|
-
}
|
|
2363
|
-
} else if (!enabled && shakeMenu != null) {
|
|
2364
|
-
try {
|
|
2365
|
-
shakeMenu.stop();
|
|
2366
|
-
shakeMenu = null;
|
|
2367
|
-
logger.info("Shake menu stopped");
|
|
2368
|
-
} catch (Exception e) {
|
|
2369
|
-
logger.error("Failed to stop shake menu: " + e.getMessage());
|
|
2370
|
-
}
|
|
2371
|
-
}
|
|
5131
|
+
logger.info("Shake menu " + (enabled ? "enabled" : "disabled") + " with " + this.shakeMenuGesture + " gesture");
|
|
5132
|
+
this.syncShakeMenuLifecycle();
|
|
2372
5133
|
|
|
2373
5134
|
call.resolve();
|
|
2374
5135
|
}
|
|
@@ -2378,6 +5139,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2378
5139
|
try {
|
|
2379
5140
|
final JSObject ret = new JSObject();
|
|
2380
5141
|
ret.put("enabled", this.shakeMenuEnabled);
|
|
5142
|
+
ret.put("gesture", this.shakeMenuGesture);
|
|
2381
5143
|
call.resolve(ret);
|
|
2382
5144
|
} catch (final Exception e) {
|
|
2383
5145
|
logger.error("Could not get shake menu status " + e.getMessage());
|
|
@@ -2396,6 +5158,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2396
5158
|
|
|
2397
5159
|
this.shakeChannelSelectorEnabled = enabled;
|
|
2398
5160
|
logger.info("Shake channel selector " + (enabled ? "enabled" : "disabled"));
|
|
5161
|
+
this.syncShakeMenuLifecycle();
|
|
2399
5162
|
call.resolve();
|
|
2400
5163
|
}
|
|
2401
5164
|
|
|
@@ -2436,7 +5199,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2436
5199
|
call.reject("setAppId called without appId");
|
|
2437
5200
|
return;
|
|
2438
5201
|
}
|
|
2439
|
-
this.
|
|
5202
|
+
this.setActiveAppId(appId);
|
|
2440
5203
|
call.resolve();
|
|
2441
5204
|
}
|
|
2442
5205
|
|
|
@@ -2492,9 +5255,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2492
5255
|
|
|
2493
5256
|
JSObject result = new JSObject();
|
|
2494
5257
|
try {
|
|
2495
|
-
PackageInfo pInfo =
|
|
5258
|
+
PackageInfo pInfo = getCurrentPackageInfo();
|
|
2496
5259
|
result.put("currentVersionName", pInfo.versionName);
|
|
2497
|
-
result.put("currentVersionCode",
|
|
5260
|
+
result.put("currentVersionCode", getVersionCode(pInfo));
|
|
2498
5261
|
} catch (PackageManager.NameNotFoundException e) {
|
|
2499
5262
|
result.put("currentVersionName", "0.0.0");
|
|
2500
5263
|
result.put("currentVersionCode", "0");
|
|
@@ -2774,6 +5537,11 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2774
5537
|
logger.error("Failed to clean up AppLifecycleObserver: " + e.getMessage());
|
|
2775
5538
|
}
|
|
2776
5539
|
}
|
|
5540
|
+
|
|
5541
|
+
if (webViewStatsListener != null && bridge != null) {
|
|
5542
|
+
bridge.removeWebViewListener(webViewStatsListener);
|
|
5543
|
+
webViewStatsListener = null;
|
|
5544
|
+
}
|
|
2777
5545
|
} catch (Exception e) {
|
|
2778
5546
|
logger.error("Failed to run handleOnDestroy: " + e.getMessage());
|
|
2779
5547
|
}
|