@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 CHANGED
@@ -18,12 +18,15 @@
18
18
  </h2>
19
19
  </div>
20
20
 
21
- Detect a recovered Capacitor WebView crash and give the next JavaScript runtime the native crash marker it needs to recover application state.
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
- - [`addListener('webViewRestoredAfterCrash', ...)`](#addlistenerwebviewrestoredaftercrash-)
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&lt;<a href="#pendingcrashinforesult">PendingCrashInfoResult</a>&gt;</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 crash marker after the app has handled recovery.
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
- ### addListener('webViewRestoredAfterCrash', ...)
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&lt;<a href="#pendingcrashinforesult">PendingCrashInfoResult</a>&gt;</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 crash marker is still pending.
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>) =&gt; void</code> |
150
227
 
151
228
  **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</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 crash marker. |
182
- | **`timestamp`** | <code>number</code> | Unix timestamp in milliseconds for when the crash marker was written. |
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 crash marker was written. |
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 crash marker was written. |
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 crash marker.
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 EVENT_NAME = "webViewRestoredAfterCrash";
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 boolean didDispatchPendingEvent = false;
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
- didDispatchPendingEvent = false;
46
+ dispatchedPendingEvents.clear();
31
47
 
32
- AppCompatActivity currentActivity = getActivity();
33
- if (currentActivity != null) {
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
- if (WebViewCrash.EVENT_NAME.equals(call.getString("eventName"))) {
59
- dispatchPendingCrashIfNeeded();
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
- didDispatchPendingEvent = false;
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
- didDispatchPendingEvent = false;
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
- private void dispatchPendingCrashIfNeeded() {
92
- if (didDispatchPendingEvent) {
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
- didDispatchPendingEvent = true;
102
- notifyListeners(WebViewCrash.EVENT_NAME, crashInfo);
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
  }