@capgo/capacitor-webview-crash 7.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/CapgoCapacitorWebViewCrash.podspec +17 -0
- package/LICENSE +373 -0
- package/Package.swift +28 -0
- package/README.md +293 -0
- package/android/build.gradle +59 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/app/capgo/webviewcrash/WebViewCrash.java +288 -0
- package/android/src/main/java/app/capgo/webviewcrash/WebViewCrashPlugin.java +185 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/dist/docs.json +271 -0
- package/dist/esm/definitions.d.ts +139 -0
- package/dist/esm/definitions.js +3 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +20 -0
- package/dist/esm/web.js +90 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +104 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +107 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/WebViewCrashPlugin/WebViewCrash.swift +359 -0
- package/ios/Sources/WebViewCrashPlugin/WebViewCrashPlugin.swift +152 -0
- package/ios/Tests/WebViewCrashPluginTests/WebViewCrashPluginTests.swift +12 -0
- package/package.json +94 -0
package/README.md
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# @capgo/capacitor-webview-crash
|
|
2
|
+
|
|
3
|
+
<a href="https://capgo.app/">
|
|
4
|
+
<img
|
|
5
|
+
src="https://raw.githubusercontent.com/Cap-go/capgo/main/assets/capgo_banner.png"
|
|
6
|
+
alt="Capgo - Instant updates for Capacitor"
|
|
7
|
+
/>
|
|
8
|
+
</a>
|
|
9
|
+
|
|
10
|
+
<div align="center">
|
|
11
|
+
<h2>
|
|
12
|
+
<a href="https://capgo.app/?ref=plugin_webview_crash"> ➡️ Get Instant updates for your App with Capgo</a>
|
|
13
|
+
</h2>
|
|
14
|
+
<h2>
|
|
15
|
+
<a href="https://capgo.app/consulting/?ref=plugin_webview_crash">
|
|
16
|
+
Missing a feature? We’ll build the plugin for you 💪
|
|
17
|
+
</a>
|
|
18
|
+
</h2>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
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
|
+
|
|
23
|
+
## What It Does
|
|
24
|
+
|
|
25
|
+
- Stores a native crash marker when Android reports `onRenderProcessGone`.
|
|
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.
|
|
30
|
+
- Exposes the marker through an event, a polling method, and a simulation helper for testing recovery flows.
|
|
31
|
+
- Ships a web implementation that simulates the same recovery flow with local storage.
|
|
32
|
+
|
|
33
|
+
## What It Does Not Do
|
|
34
|
+
|
|
35
|
+
- Prevent the underlying WebView crash from happening.
|
|
36
|
+
- Restore lost in-memory JavaScript state automatically.
|
|
37
|
+
- Replace application-level state persistence. Persist critical state before enabling scheduled restarts.
|
|
38
|
+
|
|
39
|
+
## Compatibility
|
|
40
|
+
|
|
41
|
+
| Plugin version | Capacitor compatibility | Maintained |
|
|
42
|
+
| -------------- | ----------------------- | ---------- |
|
|
43
|
+
| v8.\*.\* | v8.\*.\* | ✅ |
|
|
44
|
+
| v7.\*.\* | v7.\*.\* | On demand |
|
|
45
|
+
| v6.\*.\* | v6.\*.\* | On demand |
|
|
46
|
+
|
|
47
|
+
Policy:
|
|
48
|
+
|
|
49
|
+
- New plugins start at version `8.0.0` (Capacitor 8 baseline).
|
|
50
|
+
- Backward compatibility for older Capacitor majors is supported on demand.
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm install @capgo/capacitor-webview-crash
|
|
56
|
+
npx cap sync
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import { WebViewCrash } from '@capgo/capacitor-webview-crash';
|
|
63
|
+
|
|
64
|
+
await WebViewCrash.addListener('webViewRestoredAfterCrash', async (info) => {
|
|
65
|
+
console.log('Recovered after a WebView crash', info);
|
|
66
|
+
await WebViewCrash.clearPendingCrashInfo();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await WebViewCrash.addListener('webViewRestoredAfterRestart', async (info) => {
|
|
70
|
+
console.log('Recovered after a native WebView restart', info);
|
|
71
|
+
await WebViewCrash.clearPendingCrashInfo();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const pending = await WebViewCrash.getPendingCrashInfo();
|
|
75
|
+
if (pending.value) {
|
|
76
|
+
console.log('Pending crash or restart marker', pending.value);
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Use `simulateCrashRecovery()` in development or automated tests to exercise your recovery UI without forcing a real native WebView crash.
|
|
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
|
+
|
|
134
|
+
## Platform Notes
|
|
135
|
+
|
|
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.
|
|
138
|
+
- **Web:** There is no real browser crash detection. The web implementation only simulates the recovery flow with local storage.
|
|
139
|
+
|
|
140
|
+
## Documentation
|
|
141
|
+
|
|
142
|
+
- [Plugin docs](https://capgo.app/docs/plugins/webview-crash/)
|
|
143
|
+
- [Getting started guide](https://capgo.app/docs/plugins/webview-crash/getting-started/)
|
|
144
|
+
- [Example app](./example-app)
|
|
145
|
+
|
|
146
|
+
## API
|
|
147
|
+
|
|
148
|
+
<docgen-index>
|
|
149
|
+
|
|
150
|
+
- [`getPendingCrashInfo()`](#getpendingcrashinfo)
|
|
151
|
+
- [`clearPendingCrashInfo()`](#clearpendingcrashinfo)
|
|
152
|
+
- [`simulateCrashRecovery()`](#simulatecrashrecovery)
|
|
153
|
+
- [`restartWebView()`](#restartwebview)
|
|
154
|
+
- [`addListener('webViewRestoredAfterCrash' | 'webViewRestoredAfterRestart', ...)`](#addlistenerwebviewrestoredaftercrash--webviewrestoredafterrestart-)
|
|
155
|
+
- [`removeAllListeners()`](#removealllisteners)
|
|
156
|
+
- [Interfaces](#interfaces)
|
|
157
|
+
- [Type Aliases](#type-aliases)
|
|
158
|
+
|
|
159
|
+
</docgen-index>
|
|
160
|
+
|
|
161
|
+
<docgen-api>
|
|
162
|
+
<!--Update the source file JSDoc comments and rerun docgen to update the docs below-->
|
|
163
|
+
|
|
164
|
+
Capacitor API for recovered WebView crash and restart detection.
|
|
165
|
+
|
|
166
|
+
### getPendingCrashInfo()
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
getPendingCrashInfo() => Promise<PendingCrashInfoResult>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Returns the pending native crash or restart marker, if one exists.
|
|
173
|
+
|
|
174
|
+
**Returns:** <code>Promise<<a href="#pendingcrashinforesult">PendingCrashInfoResult</a>></code>
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
### clearPendingCrashInfo()
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
clearPendingCrashInfo() => Promise<void>
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Clears the stored marker after the app has handled recovery.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
### simulateCrashRecovery()
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
simulateCrashRecovery() => Promise<PendingCrashInfoResult>
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Creates a fake crash marker so recovery flows can be tested locally.
|
|
195
|
+
|
|
196
|
+
**Returns:** <code>Promise<<a href="#pendingcrashinforesult">PendingCrashInfoResult</a>></code>
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
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', ...)
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
addListener(eventName: 'webViewRestoredAfterCrash' | 'webViewRestoredAfterRestart', listenerFunc: (info: WebViewCrashInfo) => void) => Promise<PluginListenerHandle>
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Fires after a new JavaScript runtime attaches a listener and a matching marker is still pending.
|
|
222
|
+
|
|
223
|
+
| Param | Type |
|
|
224
|
+
| ------------------ | -------------------------------------------------------------------------------- |
|
|
225
|
+
| **`eventName`** | <code>'webViewRestoredAfterCrash' \| 'webViewRestoredAfterRestart'</code> |
|
|
226
|
+
| **`listenerFunc`** | <code>(info: <a href="#webviewcrashinfo">WebViewCrashInfo</a>) => void</code> |
|
|
227
|
+
|
|
228
|
+
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
### removeAllListeners()
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
removeAllListeners() => Promise<void>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Removes all plugin listeners.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
### Interfaces
|
|
243
|
+
|
|
244
|
+
#### PendingCrashInfoResult
|
|
245
|
+
|
|
246
|
+
Pending crash or restart marker returned to JavaScript.
|
|
247
|
+
|
|
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. |
|
|
251
|
+
|
|
252
|
+
#### WebViewCrashInfo
|
|
253
|
+
|
|
254
|
+
Metadata captured natively after the previous WebView process died or was restarted.
|
|
255
|
+
|
|
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. |
|
|
266
|
+
|
|
267
|
+
#### PluginListenerHandle
|
|
268
|
+
|
|
269
|
+
| Prop | Type |
|
|
270
|
+
| ------------ | ----------------------------------------- |
|
|
271
|
+
| **`remove`** | <code>() => Promise<void></code> |
|
|
272
|
+
|
|
273
|
+
### Type Aliases
|
|
274
|
+
|
|
275
|
+
#### WebViewCrashPlatform
|
|
276
|
+
|
|
277
|
+
Platform that produced the stored marker.
|
|
278
|
+
|
|
279
|
+
<code>'android' | 'ios' | 'web'</code>
|
|
280
|
+
|
|
281
|
+
#### WebViewCrashReason
|
|
282
|
+
|
|
283
|
+
Native reason reported for the previous WebView failure or restart.
|
|
284
|
+
|
|
285
|
+
<code>'renderProcessGone' | 'webContentProcessDidTerminate' | 'periodicRestart' | 'manualRestart' | 'simulated'</code>
|
|
286
|
+
|
|
287
|
+
#### WebViewCrashAppState
|
|
288
|
+
|
|
289
|
+
Best-effort application state captured on iOS when the WebView process died.
|
|
290
|
+
|
|
291
|
+
<code>'active' | 'inactive' | 'background' | 'unknown'</code>
|
|
292
|
+
|
|
293
|
+
</docgen-api>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
ext {
|
|
2
|
+
junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
|
|
3
|
+
androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.0'
|
|
4
|
+
androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.2.1'
|
|
5
|
+
androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.6.1'
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
buildscript {
|
|
9
|
+
repositories {
|
|
10
|
+
google()
|
|
11
|
+
mavenCentral()
|
|
12
|
+
}
|
|
13
|
+
dependencies {
|
|
14
|
+
classpath 'com.android.tools.build:gradle:8.7.2'
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
apply plugin: 'com.android.library'
|
|
19
|
+
|
|
20
|
+
android {
|
|
21
|
+
namespace = "app.capgo.webviewcrash"
|
|
22
|
+
compileSdk = project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 35
|
|
23
|
+
defaultConfig {
|
|
24
|
+
minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 23
|
|
25
|
+
targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 35
|
|
26
|
+
versionCode 1
|
|
27
|
+
versionName "1.0"
|
|
28
|
+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
29
|
+
}
|
|
30
|
+
buildTypes {
|
|
31
|
+
release {
|
|
32
|
+
minifyEnabled false
|
|
33
|
+
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
lintOptions {
|
|
37
|
+
abortOnError = false
|
|
38
|
+
}
|
|
39
|
+
compileOptions {
|
|
40
|
+
sourceCompatibility JavaVersion.VERSION_21
|
|
41
|
+
targetCompatibility JavaVersion.VERSION_21
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
repositories {
|
|
46
|
+
google()
|
|
47
|
+
mavenCentral()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
dependencies {
|
|
52
|
+
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
|
53
|
+
implementation project(':capacitor-android')
|
|
54
|
+
annotationProcessor project(':capacitor-android')
|
|
55
|
+
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
|
56
|
+
testImplementation "junit:junit:$junitVersion"
|
|
57
|
+
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
|
58
|
+
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
|
59
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
package app.capgo.webviewcrash;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import com.getcapacitor.JSObject;
|
|
5
|
+
import com.getcapacitor.PluginConfig;
|
|
6
|
+
import java.time.Duration;
|
|
7
|
+
import java.time.Instant;
|
|
8
|
+
import java.time.ZoneId;
|
|
9
|
+
import java.time.ZonedDateTime;
|
|
10
|
+
import java.time.temporal.ChronoUnit;
|
|
11
|
+
import org.json.JSONException;
|
|
12
|
+
|
|
13
|
+
final class WebViewCrash {
|
|
14
|
+
|
|
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";
|
|
19
|
+
|
|
20
|
+
private static final String PREFERENCES_NAME = "CapgoWebViewCrash";
|
|
21
|
+
private static final String PENDING_CRASH_KEY = "pendingCrashInfo";
|
|
22
|
+
|
|
23
|
+
JSObject buildCrashInfo(String reason, String url, Boolean didCrash, Integer rendererPriorityAtExit) {
|
|
24
|
+
long timestamp = System.currentTimeMillis();
|
|
25
|
+
JSObject crashInfo = new JSObject();
|
|
26
|
+
crashInfo.put("platform", "android");
|
|
27
|
+
crashInfo.put("timestamp", timestamp);
|
|
28
|
+
crashInfo.put("timestampISO", Instant.ofEpochMilli(timestamp).toString());
|
|
29
|
+
crashInfo.put("reason", reason);
|
|
30
|
+
|
|
31
|
+
if (url != null && !url.isBlank()) {
|
|
32
|
+
crashInfo.put("url", url);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (didCrash != null) {
|
|
36
|
+
crashInfo.put("didCrash", didCrash);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (rendererPriorityAtExit != null) {
|
|
40
|
+
crashInfo.put("rendererPriorityAtExit", rendererPriorityAtExit);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return crashInfo;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
JSObject readPendingCrashInfo(Context context) {
|
|
47
|
+
String raw = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE).getString(PENDING_CRASH_KEY, null);
|
|
48
|
+
if (raw == null) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
return new JSObject(raw);
|
|
54
|
+
} catch (JSONException ignored) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
void writePendingCrashInfo(Context context, JSObject value) {
|
|
60
|
+
context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE).edit().putString(PENDING_CRASH_KEY, value.toString()).apply();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
void clearPendingCrashInfo(Context context) {
|
|
64
|
+
context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE).edit().remove(PENDING_CRASH_KEY).apply();
|
|
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
|
+
}
|
|
288
|
+
}
|