@capgo/capacitor-webview-crash 8.0.3 → 8.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -30
- package/android/src/main/java/app/capgo/webviewcrash/WebViewCrash.java +231 -1
- package/android/src/main/java/app/capgo/webviewcrash/WebViewCrashPlugin.java +99 -18
- package/dist/docs.json +37 -17
- package/dist/esm/definitions.d.ts +71 -16
- package/dist/esm/definitions.js +1 -0
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +5 -2
- package/dist/esm/web.js +28 -13
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +28 -13
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +28 -13
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/WebViewCrashPlugin/WebViewCrash.swift +256 -2
- package/ios/Sources/WebViewCrashPlugin/WebViewCrashPlugin.swift +101 -11
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -18,12 +18,15 @@
|
|
|
18
18
|
</h2>
|
|
19
19
|
</div>
|
|
20
20
|
|
|
21
|
-
Detect
|
|
21
|
+
Detect recovered Capacitor WebView crashes, restart dead WebViews natively, and optionally recycle long-running WebViews on a fixed interval before memory pressure turns into an OOM.
|
|
22
22
|
|
|
23
23
|
## What It Does
|
|
24
24
|
|
|
25
25
|
- Stores a native crash marker when Android reports `onRenderProcessGone`.
|
|
26
26
|
- Hooks the iOS WebView termination callback and persists equivalent crash metadata.
|
|
27
|
+
- Restarts the WebView natively after crashes so recovery does not depend on a still-running JavaScript runtime.
|
|
28
|
+
- Can restart the WebView on a fixed native interval for kiosk, POS, signage, telemetry, and other always-on apps.
|
|
29
|
+
- Lets JavaScript request a native WebView restart with `restartWebView()` when the app wants to proactively recycle memory.
|
|
27
30
|
- Exposes the marker through an event, a polling method, and a simulation helper for testing recovery flows.
|
|
28
31
|
- Ships a web implementation that simulates the same recovery flow with local storage.
|
|
29
32
|
|
|
@@ -31,6 +34,7 @@ Detect a recovered Capacitor WebView crash and give the next JavaScript runtime
|
|
|
31
34
|
|
|
32
35
|
- Prevent the underlying WebView crash from happening.
|
|
33
36
|
- Restore lost in-memory JavaScript state automatically.
|
|
37
|
+
- Replace application-level state persistence. Persist critical state before enabling scheduled restarts.
|
|
34
38
|
|
|
35
39
|
## Compatibility
|
|
36
40
|
|
|
@@ -62,18 +66,75 @@ await WebViewCrash.addListener('webViewRestoredAfterCrash', async (info) => {
|
|
|
62
66
|
await WebViewCrash.clearPendingCrashInfo();
|
|
63
67
|
});
|
|
64
68
|
|
|
69
|
+
await WebViewCrash.addListener('webViewRestoredAfterRestart', async (info) => {
|
|
70
|
+
console.log('Recovered after a native WebView restart', info);
|
|
71
|
+
await WebViewCrash.clearPendingCrashInfo();
|
|
72
|
+
});
|
|
73
|
+
|
|
65
74
|
const pending = await WebViewCrash.getPendingCrashInfo();
|
|
66
75
|
if (pending.value) {
|
|
67
|
-
console.log('Pending crash marker', pending.value);
|
|
76
|
+
console.log('Pending crash or restart marker', pending.value);
|
|
68
77
|
}
|
|
69
78
|
```
|
|
70
79
|
|
|
71
80
|
Use `simulateCrashRecovery()` in development or automated tests to exercise your recovery UI without forcing a real native WebView crash.
|
|
72
81
|
|
|
82
|
+
Call `restartWebView()` when the current JavaScript runtime decides the native WebView should be replaced:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
await WebViewCrash.restartWebView();
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The call writes a pending marker with `reason: 'manualRestart'`, then native code restarts the WebView. Android recreates the host Activity. iOS rebuilds the Capacitor bridge view so a new `WKWebView` is created instead of reloading the current page.
|
|
89
|
+
|
|
90
|
+
## Native Auto Restart
|
|
91
|
+
|
|
92
|
+
Configure the plugin in `capacitor.config.ts` so restart decisions happen in native code, even when the JavaScript runtime is unavailable:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import type { CapacitorConfig } from '@capacitor/cli';
|
|
96
|
+
import type { WebViewCrashPluginConfig } from '@capgo/capacitor-webview-crash';
|
|
97
|
+
|
|
98
|
+
const webViewCrash: WebViewCrashPluginConfig = {
|
|
99
|
+
// Keep native crash recovery enabled. This is the default.
|
|
100
|
+
restartOnCrash: true,
|
|
101
|
+
|
|
102
|
+
// Use a 5-field cron schedule in the device local timezone.
|
|
103
|
+
// Do not combine restartCron with an active restartIntervalMs.
|
|
104
|
+
restartCron: '0 3 * * *',
|
|
105
|
+
|
|
106
|
+
// Optional delay before restarting after a crash.
|
|
107
|
+
restartAfterCrashDelayMs: 0,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const config: CapacitorConfig = {
|
|
111
|
+
plugins: {
|
|
112
|
+
WebViewCrash: webViewCrash,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export default config;
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Use scheduled restarts for apps that stay open for days: kiosk screens, control-room dashboards, point-of-sale terminals, warehouse scanners, vehicle tablets, or any Capacitor app that cannot rely on users force-closing it. The restart is native, writes a pending marker with `reason: 'periodicRestart'`, and then creates a fresh WebView.
|
|
120
|
+
|
|
121
|
+
Set `restartIntervalMs` to a maintenance window that your product can tolerate, or set `restartCron` for a wall-clock schedule such as `0 3 * * *` for a daily 03:00 restart. `restartCron` uses 5-field cron syntax in the device local timezone and supports `*`, lists, ranges, and steps. Do not configure both schedules at once: native initialization throws a fatal config error when `restartCron` is set and `restartIntervalMs` is greater than `0`. The user will get a fresh JavaScript runtime, so persist unsaved form state, queued events, and in-progress work before enabling a short interval or cron schedule.
|
|
122
|
+
|
|
123
|
+
## Config Type
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
export interface WebViewCrashPluginConfig {
|
|
127
|
+
restartOnCrash?: boolean;
|
|
128
|
+
restartIntervalMs?: number;
|
|
129
|
+
restartCron?: string;
|
|
130
|
+
restartAfterCrashDelayMs?: number;
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
73
134
|
## Platform Notes
|
|
74
135
|
|
|
75
|
-
- **iOS:** Uses method swizzling on Capacitor's `WebViewDelegationHandler` to persist crash metadata before Capacitor reloads the WebView. No extra permissions are required.
|
|
76
|
-
- **Android:** Registers a Capacitor `WebViewListener` and persists crash metadata from `onRenderProcessGone`. No extra permissions are required.
|
|
136
|
+
- **iOS:** Uses method swizzling on Capacitor's `WebViewDelegationHandler` to persist crash metadata before Capacitor reloads the WebView. Manual and scheduled restarts rebuild the Capacitor bridge view so a new `WKWebView` instance is created. No extra permissions are required.
|
|
137
|
+
- **Android:** Registers a Capacitor `WebViewListener` and persists crash metadata from `onRenderProcessGone`. Crash and scheduled restarts reset the bridge and recreate the host activity, giving the app a fresh WebView. No extra permissions are required.
|
|
77
138
|
- **Web:** There is no real browser crash detection. The web implementation only simulates the recovery flow with local storage.
|
|
78
139
|
|
|
79
140
|
## Documentation
|
|
@@ -89,7 +150,8 @@ Use `simulateCrashRecovery()` in development or automated tests to exercise your
|
|
|
89
150
|
- [`getPendingCrashInfo()`](#getpendingcrashinfo)
|
|
90
151
|
- [`clearPendingCrashInfo()`](#clearpendingcrashinfo)
|
|
91
152
|
- [`simulateCrashRecovery()`](#simulatecrashrecovery)
|
|
92
|
-
- [`
|
|
153
|
+
- [`restartWebView()`](#restartwebview)
|
|
154
|
+
- [`addListener('webViewRestoredAfterCrash' | 'webViewRestoredAfterRestart', ...)`](#addlistenerwebviewrestoredaftercrash--webviewrestoredafterrestart-)
|
|
93
155
|
- [`removeAllListeners()`](#removealllisteners)
|
|
94
156
|
- [Interfaces](#interfaces)
|
|
95
157
|
- [Type Aliases](#type-aliases)
|
|
@@ -99,7 +161,7 @@ Use `simulateCrashRecovery()` in development or automated tests to exercise your
|
|
|
99
161
|
<docgen-api>
|
|
100
162
|
<!--Update the source file JSDoc comments and rerun docgen to update the docs below-->
|
|
101
163
|
|
|
102
|
-
Capacitor API for recovered WebView crash detection.
|
|
164
|
+
Capacitor API for recovered WebView crash and restart detection.
|
|
103
165
|
|
|
104
166
|
### getPendingCrashInfo()
|
|
105
167
|
|
|
@@ -107,7 +169,7 @@ Capacitor API for recovered WebView crash detection.
|
|
|
107
169
|
getPendingCrashInfo() => Promise<PendingCrashInfoResult>
|
|
108
170
|
```
|
|
109
171
|
|
|
110
|
-
Returns the pending native crash marker, if one exists.
|
|
172
|
+
Returns the pending native crash or restart marker, if one exists.
|
|
111
173
|
|
|
112
174
|
**Returns:** <code>Promise<<a href="#pendingcrashinforesult">PendingCrashInfoResult</a>></code>
|
|
113
175
|
|
|
@@ -119,7 +181,7 @@ Returns the pending native crash marker, if one exists.
|
|
|
119
181
|
clearPendingCrashInfo() => Promise<void>
|
|
120
182
|
```
|
|
121
183
|
|
|
122
|
-
Clears the stored
|
|
184
|
+
Clears the stored marker after the app has handled recovery.
|
|
123
185
|
|
|
124
186
|
---
|
|
125
187
|
|
|
@@ -135,17 +197,32 @@ Creates a fake crash marker so recovery flows can be tested locally.
|
|
|
135
197
|
|
|
136
198
|
---
|
|
137
199
|
|
|
138
|
-
###
|
|
200
|
+
### restartWebView()
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
restartWebView() => Promise<PendingCrashInfoResult>
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Stores a manual restart marker and asks native code to create a fresh WebView.
|
|
207
|
+
|
|
208
|
+
On Android this recreates the host Activity. On iOS this rebuilds the Capacitor bridge view so a new `WKWebView`
|
|
209
|
+
instance is created instead of reloading the current page.
|
|
210
|
+
|
|
211
|
+
**Returns:** <code>Promise<<a href="#pendingcrashinforesult">PendingCrashInfoResult</a>></code>
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
### addListener('webViewRestoredAfterCrash' | 'webViewRestoredAfterRestart', ...)
|
|
139
216
|
|
|
140
217
|
```typescript
|
|
141
|
-
addListener(eventName: 'webViewRestoredAfterCrash', listenerFunc: (info: WebViewCrashInfo) => void) => Promise<PluginListenerHandle>
|
|
218
|
+
addListener(eventName: 'webViewRestoredAfterCrash' | 'webViewRestoredAfterRestart', listenerFunc: (info: WebViewCrashInfo) => void) => Promise<PluginListenerHandle>
|
|
142
219
|
```
|
|
143
220
|
|
|
144
|
-
Fires after a new JavaScript runtime attaches a listener and a
|
|
221
|
+
Fires after a new JavaScript runtime attaches a listener and a matching marker is still pending.
|
|
145
222
|
|
|
146
223
|
| Param | Type |
|
|
147
224
|
| ------------------ | -------------------------------------------------------------------------------- |
|
|
148
|
-
| **`eventName`** | <code>'webViewRestoredAfterCrash'</code>
|
|
225
|
+
| **`eventName`** | <code>'webViewRestoredAfterCrash' \| 'webViewRestoredAfterRestart'</code> |
|
|
149
226
|
| **`listenerFunc`** | <code>(info: <a href="#webviewcrashinfo">WebViewCrashInfo</a>) => void</code> |
|
|
150
227
|
|
|
151
228
|
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
@@ -166,26 +243,26 @@ Removes all plugin listeners.
|
|
|
166
243
|
|
|
167
244
|
#### PendingCrashInfoResult
|
|
168
245
|
|
|
169
|
-
Pending crash marker returned to JavaScript.
|
|
246
|
+
Pending crash or restart marker returned to JavaScript.
|
|
170
247
|
|
|
171
|
-
| Prop | Type | Description
|
|
172
|
-
| ----------- | --------------------------------------------------------------------- |
|
|
173
|
-
| **`value`** | <code><a href="#webviewcrashinfo">WebViewCrashInfo</a> \| null</code> | Stored crash metadata, or `null` when no marker is pending. |
|
|
248
|
+
| Prop | Type | Description |
|
|
249
|
+
| ----------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
|
250
|
+
| **`value`** | <code><a href="#webviewcrashinfo">WebViewCrashInfo</a> \| null</code> | Stored crash or restart metadata, or `null` when no marker is pending. |
|
|
174
251
|
|
|
175
252
|
#### WebViewCrashInfo
|
|
176
253
|
|
|
177
|
-
Metadata captured natively after the previous WebView process died.
|
|
254
|
+
Metadata captured natively after the previous WebView process died or was restarted.
|
|
178
255
|
|
|
179
|
-
| Prop | Type | Description
|
|
180
|
-
| ---------------------------- | --------------------------------------------------------------------- |
|
|
181
|
-
| **`platform`** | <code><a href="#webviewcrashplatform">WebViewCrashPlatform</a></code> | Platform that detected and stored the
|
|
182
|
-
| **`timestamp`** | <code>number</code> | Unix timestamp in milliseconds for when the
|
|
183
|
-
| **`timestampISO`** | <code>string</code> | ISO-8601 version of `timestamp`.
|
|
184
|
-
| **`reason`** | <code><a href="#webviewcrashreason">WebViewCrashReason</a></code> | Platform-specific reason for the crash marker.
|
|
185
|
-
| **`url`** | <code>string</code> | Last known WebView URL when the
|
|
186
|
-
| **`didCrash`** | <code>boolean</code> | Android-only hint from `RenderProcessGoneDetail.didCrash()`.
|
|
187
|
-
| **`rendererPriorityAtExit`** | <code>number</code> | Android-only renderer priority reported at exit.
|
|
188
|
-
| **`appState`** | <code><a href="#webviewcrashappstate">WebViewCrashAppState</a></code> | iOS-only application state captured when the
|
|
256
|
+
| Prop | Type | Description |
|
|
257
|
+
| ---------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------- |
|
|
258
|
+
| **`platform`** | <code><a href="#webviewcrashplatform">WebViewCrashPlatform</a></code> | Platform that detected and stored the marker. |
|
|
259
|
+
| **`timestamp`** | <code>number</code> | Unix timestamp in milliseconds for when the marker was written. |
|
|
260
|
+
| **`timestampISO`** | <code>string</code> | ISO-8601 version of `timestamp`. |
|
|
261
|
+
| **`reason`** | <code><a href="#webviewcrashreason">WebViewCrashReason</a></code> | Platform-specific reason for the crash or restart marker. |
|
|
262
|
+
| **`url`** | <code>string</code> | Last known WebView URL when the marker was written. |
|
|
263
|
+
| **`didCrash`** | <code>boolean</code> | Android-only hint from `RenderProcessGoneDetail.didCrash()`. |
|
|
264
|
+
| **`rendererPriorityAtExit`** | <code>number</code> | Android-only renderer priority reported at exit. |
|
|
265
|
+
| **`appState`** | <code><a href="#webviewcrashappstate">WebViewCrashAppState</a></code> | iOS-only application state captured when the marker was written. |
|
|
189
266
|
|
|
190
267
|
#### PluginListenerHandle
|
|
191
268
|
|
|
@@ -197,15 +274,15 @@ Metadata captured natively after the previous WebView process died.
|
|
|
197
274
|
|
|
198
275
|
#### WebViewCrashPlatform
|
|
199
276
|
|
|
200
|
-
Platform that produced the stored
|
|
277
|
+
Platform that produced the stored marker.
|
|
201
278
|
|
|
202
279
|
<code>'android' | 'ios' | 'web'</code>
|
|
203
280
|
|
|
204
281
|
#### WebViewCrashReason
|
|
205
282
|
|
|
206
|
-
Native reason reported for the previous WebView failure.
|
|
283
|
+
Native reason reported for the previous WebView failure or restart.
|
|
207
284
|
|
|
208
|
-
<code>'renderProcessGone' | 'webContentProcessDidTerminate' | 'simulated'</code>
|
|
285
|
+
<code>'renderProcessGone' | 'webContentProcessDidTerminate' | 'periodicRestart' | 'manualRestart' | 'simulated'</code>
|
|
209
286
|
|
|
210
287
|
#### WebViewCrashAppState
|
|
211
288
|
|
|
@@ -2,12 +2,20 @@ package app.capgo.webviewcrash;
|
|
|
2
2
|
|
|
3
3
|
import android.content.Context;
|
|
4
4
|
import com.getcapacitor.JSObject;
|
|
5
|
+
import com.getcapacitor.PluginConfig;
|
|
6
|
+
import java.time.Duration;
|
|
5
7
|
import java.time.Instant;
|
|
8
|
+
import java.time.ZoneId;
|
|
9
|
+
import java.time.ZonedDateTime;
|
|
10
|
+
import java.time.temporal.ChronoUnit;
|
|
6
11
|
import org.json.JSONException;
|
|
7
12
|
|
|
8
13
|
final class WebViewCrash {
|
|
9
14
|
|
|
10
|
-
static final String
|
|
15
|
+
static final String CRASH_EVENT_NAME = "webViewRestoredAfterCrash";
|
|
16
|
+
static final String RESTART_EVENT_NAME = "webViewRestoredAfterRestart";
|
|
17
|
+
static final String PERIODIC_RESTART_REASON = "periodicRestart";
|
|
18
|
+
static final String MANUAL_RESTART_REASON = "manualRestart";
|
|
11
19
|
|
|
12
20
|
private static final String PREFERENCES_NAME = "CapgoWebViewCrash";
|
|
13
21
|
private static final String PENDING_CRASH_KEY = "pendingCrashInfo";
|
|
@@ -55,4 +63,226 @@ final class WebViewCrash {
|
|
|
55
63
|
void clearPendingCrashInfo(Context context) {
|
|
56
64
|
context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE).edit().remove(PENDING_CRASH_KEY).apply();
|
|
57
65
|
}
|
|
66
|
+
|
|
67
|
+
boolean shouldDispatchEvent(String eventName, JSObject crashInfo) {
|
|
68
|
+
if (RESTART_EVENT_NAME.equals(eventName)) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!CRASH_EVENT_NAME.equals(eventName)) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
String reason = crashInfo.optString("reason", "");
|
|
77
|
+
return !PERIODIC_RESTART_REASON.equals(reason) && !MANUAL_RESTART_REASON.equals(reason);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
RestartOptions readRestartOptions(PluginConfig config) {
|
|
81
|
+
int restartIntervalMs = Math.max(0, config.getInt("restartIntervalMs", 0));
|
|
82
|
+
String restartCronExpression = config.getString("restartCron", null);
|
|
83
|
+
|
|
84
|
+
validateRestartScheduleConfig(restartIntervalMs, restartCronExpression);
|
|
85
|
+
|
|
86
|
+
return new RestartOptions(
|
|
87
|
+
config.getBoolean("restartOnCrash", true),
|
|
88
|
+
restartIntervalMs,
|
|
89
|
+
CronSchedule.parse(restartCronExpression),
|
|
90
|
+
Math.max(0, config.getInt("restartAfterCrashDelayMs", 0))
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static void validateRestartScheduleConfig(int restartIntervalMs, String restartCronExpression) {
|
|
95
|
+
if (restartIntervalMs > 0 && restartCronExpression != null && !restartCronExpression.isBlank()) {
|
|
96
|
+
throw new IllegalStateException("Invalid WebViewCrash config: set either restartIntervalMs or restartCron, not both.");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
static final class RestartOptions {
|
|
101
|
+
|
|
102
|
+
final boolean restartOnCrash;
|
|
103
|
+
final int restartIntervalMs;
|
|
104
|
+
final CronSchedule restartCron;
|
|
105
|
+
final int restartAfterCrashDelayMs;
|
|
106
|
+
|
|
107
|
+
RestartOptions(boolean restartOnCrash, int restartIntervalMs, CronSchedule restartCron, int restartAfterCrashDelayMs) {
|
|
108
|
+
this.restartOnCrash = restartOnCrash;
|
|
109
|
+
this.restartIntervalMs = restartIntervalMs;
|
|
110
|
+
this.restartCron = restartCron;
|
|
111
|
+
this.restartAfterCrashDelayMs = restartAfterCrashDelayMs;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
Long nextRestartDelayMs() {
|
|
115
|
+
if (restartCron != null) {
|
|
116
|
+
return restartCron.nextDelayMs();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return restartIntervalMs > 0 ? (long) restartIntervalMs : null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
static final class CronSchedule {
|
|
124
|
+
|
|
125
|
+
private static final int SEARCH_LIMIT_MINUTES = 366 * 24 * 60 * 5;
|
|
126
|
+
|
|
127
|
+
private final CronField minutes;
|
|
128
|
+
private final CronField hours;
|
|
129
|
+
private final CronField daysOfMonth;
|
|
130
|
+
private final CronField months;
|
|
131
|
+
private final CronField daysOfWeek;
|
|
132
|
+
|
|
133
|
+
private CronSchedule(CronField minutes, CronField hours, CronField daysOfMonth, CronField months, CronField daysOfWeek) {
|
|
134
|
+
this.minutes = minutes;
|
|
135
|
+
this.hours = hours;
|
|
136
|
+
this.daysOfMonth = daysOfMonth;
|
|
137
|
+
this.months = months;
|
|
138
|
+
this.daysOfWeek = daysOfWeek;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
static CronSchedule parse(String expression) {
|
|
142
|
+
if (expression == null || expression.isBlank()) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
String[] parts = expression.trim().split("\\s+");
|
|
147
|
+
if (parts.length != 5) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
CronField minutes = CronField.parse(parts[0], 0, 59, false);
|
|
152
|
+
CronField hours = CronField.parse(parts[1], 0, 23, false);
|
|
153
|
+
CronField daysOfMonth = CronField.parse(parts[2], 1, 31, false);
|
|
154
|
+
CronField months = CronField.parse(parts[3], 1, 12, false);
|
|
155
|
+
CronField daysOfWeek = CronField.parse(parts[4], 0, 7, true);
|
|
156
|
+
|
|
157
|
+
if (minutes == null || hours == null || daysOfMonth == null || months == null || daysOfWeek == null) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return new CronSchedule(minutes, hours, daysOfMonth, months, daysOfWeek);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
Long nextDelayMs() {
|
|
165
|
+
ZonedDateTime now = ZonedDateTime.now(ZoneId.systemDefault());
|
|
166
|
+
ZonedDateTime candidate = now.truncatedTo(ChronoUnit.MINUTES).plusMinutes(1);
|
|
167
|
+
|
|
168
|
+
for (int index = 0; index < SEARCH_LIMIT_MINUTES; index++) {
|
|
169
|
+
if (matches(candidate)) {
|
|
170
|
+
return Math.max(0, Duration.between(now, candidate).toMillis());
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
candidate = candidate.plusMinutes(1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private boolean matches(ZonedDateTime value) {
|
|
180
|
+
if (!minutes.matches(value.getMinute()) || !hours.matches(value.getHour()) || !months.matches(value.getMonthValue())) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
boolean dayOfMonthMatches = daysOfMonth.matches(value.getDayOfMonth());
|
|
185
|
+
boolean dayOfWeekMatches = daysOfWeek.matches(value.getDayOfWeek().getValue() % 7);
|
|
186
|
+
|
|
187
|
+
if (daysOfMonth.restricted && daysOfWeek.restricted) {
|
|
188
|
+
return dayOfMonthMatches || dayOfWeekMatches;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return dayOfMonthMatches && dayOfWeekMatches;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private static final class CronField {
|
|
196
|
+
|
|
197
|
+
private final boolean[] values;
|
|
198
|
+
private final boolean restricted;
|
|
199
|
+
|
|
200
|
+
private CronField(boolean[] values, boolean restricted) {
|
|
201
|
+
this.values = values;
|
|
202
|
+
this.restricted = restricted;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
static CronField parse(String expression, int min, int max, boolean normalizeSunday) {
|
|
206
|
+
boolean[] values = new boolean[normalizeSunday ? 7 : max + 1];
|
|
207
|
+
|
|
208
|
+
for (String part : expression.split(",", -1)) {
|
|
209
|
+
if (part.isBlank() || !applyPart(values, part, min, max, normalizeSunday)) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
int selectedCount = 0;
|
|
215
|
+
for (boolean selected : values) {
|
|
216
|
+
if (selected) {
|
|
217
|
+
selectedCount++;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (selectedCount == 0) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
int allValueCount = normalizeSunday ? 7 : max - min + 1;
|
|
226
|
+
return new CronField(values, selectedCount != allValueCount);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
boolean matches(int value) {
|
|
230
|
+
return value >= 0 && value < values.length && values[value];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private static boolean applyPart(boolean[] values, String part, int min, int max, boolean normalizeSunday) {
|
|
234
|
+
String[] stepParts = part.split("/", -1);
|
|
235
|
+
if (stepParts.length > 2) {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
int step = 1;
|
|
240
|
+
if (stepParts.length == 2) {
|
|
241
|
+
step = parseNumber(stepParts[1]);
|
|
242
|
+
if (step <= 0) {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
String rangePart = stepParts[0];
|
|
248
|
+
int start;
|
|
249
|
+
int end;
|
|
250
|
+
if ("*".equals(rangePart)) {
|
|
251
|
+
start = min;
|
|
252
|
+
end = max;
|
|
253
|
+
} else if (rangePart.contains("-")) {
|
|
254
|
+
String[] range = rangePart.split("-", -1);
|
|
255
|
+
if (range.length != 2) {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
start = parseNumber(range[0]);
|
|
259
|
+
end = parseNumber(range[1]);
|
|
260
|
+
} else {
|
|
261
|
+
start = parseNumber(rangePart);
|
|
262
|
+
end = start;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (start < min || end > max || start > end) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
for (int value = start; value <= end; value += step) {
|
|
270
|
+
values[normalizeValue(value, normalizeSunday)] = true;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private static int parseNumber(String value) {
|
|
277
|
+
try {
|
|
278
|
+
return Integer.parseInt(value);
|
|
279
|
+
} catch (NumberFormatException ignored) {
|
|
280
|
+
return -1;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private static int normalizeValue(int value, boolean normalizeSunday) {
|
|
285
|
+
return normalizeSunday && value == 7 ? 0 : value;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
58
288
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
package app.capgo.webviewcrash;
|
|
2
2
|
|
|
3
|
+
import android.os.Handler;
|
|
4
|
+
import android.os.Looper;
|
|
3
5
|
import android.webkit.RenderProcessGoneDetail;
|
|
4
6
|
import android.webkit.WebView;
|
|
5
7
|
import androidx.appcompat.app.AppCompatActivity;
|
|
@@ -9,12 +11,26 @@ import com.getcapacitor.PluginCall;
|
|
|
9
11
|
import com.getcapacitor.PluginMethod;
|
|
10
12
|
import com.getcapacitor.WebViewListener;
|
|
11
13
|
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
14
|
+
import java.util.HashSet;
|
|
15
|
+
import java.util.Set;
|
|
12
16
|
|
|
13
17
|
@CapacitorPlugin(name = "WebViewCrash")
|
|
14
18
|
public class WebViewCrashPlugin extends Plugin {
|
|
15
19
|
|
|
16
20
|
private final WebViewCrash implementation = new WebViewCrash();
|
|
17
|
-
private
|
|
21
|
+
private final Set<String> dispatchedPendingEvents = new HashSet<>();
|
|
22
|
+
private WebViewCrash.RestartOptions restartOptions = new WebViewCrash.RestartOptions(true, 0, null, 0);
|
|
23
|
+
private Handler mainHandler;
|
|
24
|
+
|
|
25
|
+
private final Runnable periodicRestartRunnable = () -> {
|
|
26
|
+
WebView webView = bridge != null ? bridge.getWebView() : null;
|
|
27
|
+
String url = webView != null ? webView.getUrl() : null;
|
|
28
|
+
JSObject restartInfo = implementation.buildCrashInfo(WebViewCrash.PERIODIC_RESTART_REASON, url, null, null);
|
|
29
|
+
|
|
30
|
+
implementation.writePendingCrashInfo(getContext(), restartInfo);
|
|
31
|
+
dispatchedPendingEvents.clear();
|
|
32
|
+
restartWebView(0);
|
|
33
|
+
};
|
|
18
34
|
|
|
19
35
|
private final WebViewListener webViewListener = new WebViewListener() {
|
|
20
36
|
@Override
|
|
@@ -27,36 +43,37 @@ public class WebViewCrashPlugin extends Plugin {
|
|
|
27
43
|
);
|
|
28
44
|
|
|
29
45
|
implementation.writePendingCrashInfo(getContext(), crashInfo);
|
|
30
|
-
|
|
46
|
+
dispatchedPendingEvents.clear();
|
|
31
47
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
currentActivity.runOnUiThread(() -> {
|
|
35
|
-
bridge.reset();
|
|
36
|
-
currentActivity.recreate();
|
|
37
|
-
});
|
|
48
|
+
if (!restartOptions.restartOnCrash) {
|
|
49
|
+
return false;
|
|
38
50
|
}
|
|
39
51
|
|
|
52
|
+
restartWebView(restartOptions.restartAfterCrashDelayMs);
|
|
40
53
|
return true;
|
|
41
54
|
}
|
|
42
55
|
};
|
|
43
56
|
|
|
44
57
|
@Override
|
|
45
58
|
public void load() {
|
|
59
|
+
restartOptions = implementation.readRestartOptions(getConfig());
|
|
46
60
|
bridge.addWebViewListener(webViewListener);
|
|
61
|
+
schedulePeriodicRestart();
|
|
47
62
|
}
|
|
48
63
|
|
|
49
64
|
@Override
|
|
50
65
|
protected void handleOnDestroy() {
|
|
51
66
|
bridge.removeWebViewListener(webViewListener);
|
|
67
|
+
cancelPeriodicRestart();
|
|
52
68
|
}
|
|
53
69
|
|
|
54
70
|
@Override
|
|
55
71
|
public void addListener(PluginCall call) {
|
|
56
72
|
super.addListener(call);
|
|
57
73
|
|
|
58
|
-
|
|
59
|
-
|
|
74
|
+
String eventName = call.getString("eventName");
|
|
75
|
+
if (WebViewCrash.CRASH_EVENT_NAME.equals(eventName) || WebViewCrash.RESTART_EVENT_NAME.equals(eventName)) {
|
|
76
|
+
dispatchPendingCrashIfNeeded(eventName);
|
|
60
77
|
}
|
|
61
78
|
}
|
|
62
79
|
|
|
@@ -70,7 +87,7 @@ public class WebViewCrashPlugin extends Plugin {
|
|
|
70
87
|
@PluginMethod
|
|
71
88
|
public void clearPendingCrashInfo(PluginCall call) {
|
|
72
89
|
implementation.clearPendingCrashInfo(getContext());
|
|
73
|
-
|
|
90
|
+
dispatchedPendingEvents.clear();
|
|
74
91
|
call.resolve();
|
|
75
92
|
}
|
|
76
93
|
|
|
@@ -80,25 +97,89 @@ public class WebViewCrashPlugin extends Plugin {
|
|
|
80
97
|
JSObject crashInfo = implementation.buildCrashInfo("simulated", url, null, null);
|
|
81
98
|
|
|
82
99
|
implementation.writePendingCrashInfo(getContext(), crashInfo);
|
|
83
|
-
|
|
84
|
-
dispatchPendingCrashIfNeeded();
|
|
100
|
+
dispatchedPendingEvents.clear();
|
|
101
|
+
dispatchPendingCrashIfNeeded(WebViewCrash.CRASH_EVENT_NAME);
|
|
102
|
+
dispatchPendingCrashIfNeeded(WebViewCrash.RESTART_EVENT_NAME);
|
|
85
103
|
|
|
86
104
|
JSObject result = new JSObject();
|
|
87
105
|
result.put("value", crashInfo);
|
|
88
106
|
call.resolve(result);
|
|
89
107
|
}
|
|
90
108
|
|
|
91
|
-
|
|
92
|
-
|
|
109
|
+
@PluginMethod
|
|
110
|
+
public void restartWebView(PluginCall call) {
|
|
111
|
+
WebView webView = bridge != null ? bridge.getWebView() : null;
|
|
112
|
+
String url = webView != null ? webView.getUrl() : null;
|
|
113
|
+
JSObject restartInfo = implementation.buildCrashInfo(WebViewCrash.MANUAL_RESTART_REASON, url, null, null);
|
|
114
|
+
|
|
115
|
+
implementation.writePendingCrashInfo(getContext(), restartInfo);
|
|
116
|
+
dispatchedPendingEvents.clear();
|
|
117
|
+
|
|
118
|
+
JSObject result = new JSObject();
|
|
119
|
+
result.put("value", restartInfo);
|
|
120
|
+
call.resolve(result);
|
|
121
|
+
|
|
122
|
+
restartWebView(0);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private void dispatchPendingCrashIfNeeded(String eventName) {
|
|
126
|
+
if (dispatchedPendingEvents.contains(eventName)) {
|
|
93
127
|
return;
|
|
94
128
|
}
|
|
95
129
|
|
|
96
130
|
JSObject crashInfo = implementation.readPendingCrashInfo(getContext());
|
|
97
|
-
if (crashInfo == null) {
|
|
131
|
+
if (crashInfo == null || !implementation.shouldDispatchEvent(eventName, crashInfo)) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
dispatchedPendingEvents.add(eventName);
|
|
136
|
+
notifyListeners(eventName, crashInfo);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private void schedulePeriodicRestart() {
|
|
140
|
+
cancelPeriodicRestart();
|
|
141
|
+
Long delayMs = restartOptions.nextRestartDelayMs();
|
|
142
|
+
if (delayMs == null || delayMs <= 0) {
|
|
98
143
|
return;
|
|
99
144
|
}
|
|
100
145
|
|
|
101
|
-
|
|
102
|
-
|
|
146
|
+
getMainHandler().postDelayed(periodicRestartRunnable, delayMs);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private void cancelPeriodicRestart() {
|
|
150
|
+
if (mainHandler != null) {
|
|
151
|
+
mainHandler.removeCallbacks(periodicRestartRunnable);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private void restartWebView(int delayMs) {
|
|
156
|
+
Runnable restart = () -> {
|
|
157
|
+
AppCompatActivity currentActivity = getActivity();
|
|
158
|
+
if (currentActivity == null) {
|
|
159
|
+
schedulePeriodicRestart();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
currentActivity.runOnUiThread(() -> {
|
|
164
|
+
if (bridge != null) {
|
|
165
|
+
bridge.reset();
|
|
166
|
+
}
|
|
167
|
+
currentActivity.recreate();
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
if (delayMs > 0) {
|
|
172
|
+
getMainHandler().postDelayed(restart, delayMs);
|
|
173
|
+
} else {
|
|
174
|
+
getMainHandler().post(restart);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private Handler getMainHandler() {
|
|
179
|
+
if (mainHandler == null) {
|
|
180
|
+
mainHandler = new Handler(Looper.getMainLooper());
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return mainHandler;
|
|
103
184
|
}
|
|
104
185
|
}
|