@onekeyfe/react-native-chart-webview 3.0.55 → 3.0.57
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/ChartWebview.podspec +3 -0
- package/android/src/main/java/com/margelo/nitro/chartwebview/ChartWebview.kt +85 -18
- package/android/src/main/java/com/margelo/nitro/chartwebview/PooledChartWebView.kt +185 -23
- package/ios/ChartWebview.swift +190 -31
- package/lib/nitrogen/generated/android/c++/JHybridChartWebviewSpec.cpp +18 -0
- package/lib/nitrogen/generated/android/c++/JHybridChartWebviewSpec.hpp +4 -0
- package/lib/nitrogen/generated/android/c++/views/JHybridChartWebviewStateUpdater.cpp +8 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/chartwebview/HybridChartWebviewSpec.kt +12 -0
- package/lib/nitrogen/generated/ios/c++/HybridChartWebviewSpecSwift.hpp +14 -0
- package/lib/nitrogen/generated/ios/c++/views/HybridChartWebviewComponent.mm +10 -0
- package/lib/nitrogen/generated/ios/swift/HybridChartWebviewSpec.swift +2 -0
- package/lib/nitrogen/generated/ios/swift/HybridChartWebviewSpec_cxx.swift +48 -0
- package/lib/nitrogen/generated/shared/c++/HybridChartWebviewSpec.cpp +4 -0
- package/lib/nitrogen/generated/shared/c++/HybridChartWebviewSpec.hpp +4 -0
- package/lib/nitrogen/generated/shared/c++/views/HybridChartWebviewComponent.cpp +24 -0
- package/lib/nitrogen/generated/shared/c++/views/HybridChartWebviewComponent.hpp +2 -0
- package/lib/nitrogen/generated/shared/json/ChartWebviewConfig.json +2 -0
- package/lib/nitrogen/nitrogen/generated/android/c++/JHybridChartWebviewSpec.cpp +18 -0
- package/lib/nitrogen/nitrogen/generated/android/c++/JHybridChartWebviewSpec.hpp +4 -0
- package/lib/nitrogen/nitrogen/generated/android/c++/views/JHybridChartWebviewStateUpdater.cpp +8 -0
- package/lib/nitrogen/nitrogen/generated/android/kotlin/com/margelo/nitro/chartwebview/HybridChartWebviewSpec.kt +12 -0
- package/lib/nitrogen/nitrogen/generated/ios/c++/HybridChartWebviewSpecSwift.hpp +14 -0
- package/lib/nitrogen/nitrogen/generated/ios/c++/views/HybridChartWebviewComponent.mm +10 -0
- package/lib/nitrogen/nitrogen/generated/ios/swift/HybridChartWebviewSpec.swift +2 -0
- package/lib/nitrogen/nitrogen/generated/ios/swift/HybridChartWebviewSpec_cxx.swift +48 -0
- package/lib/nitrogen/nitrogen/generated/shared/c++/HybridChartWebviewSpec.cpp +4 -0
- package/lib/nitrogen/nitrogen/generated/shared/c++/HybridChartWebviewSpec.hpp +4 -0
- package/lib/nitrogen/nitrogen/generated/shared/c++/views/HybridChartWebviewComponent.cpp +24 -0
- package/lib/nitrogen/nitrogen/generated/shared/c++/views/HybridChartWebviewComponent.hpp +2 -0
- package/lib/nitrogen/nitrogen/generated/shared/json/ChartWebviewConfig.json +2 -0
- package/lib/typescript/src/ChartWebview.nitro.d.ts +2 -0
- package/lib/typescript/src/ChartWebview.nitro.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JHybridChartWebviewSpec.cpp +18 -0
- package/nitrogen/generated/android/c++/JHybridChartWebviewSpec.hpp +4 -0
- package/nitrogen/generated/android/c++/views/JHybridChartWebviewStateUpdater.cpp +8 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/chartwebview/HybridChartWebviewSpec.kt +12 -0
- package/nitrogen/generated/ios/c++/HybridChartWebviewSpecSwift.hpp +14 -0
- package/nitrogen/generated/ios/c++/views/HybridChartWebviewComponent.mm +10 -0
- package/nitrogen/generated/ios/swift/HybridChartWebviewSpec.swift +2 -0
- package/nitrogen/generated/ios/swift/HybridChartWebviewSpec_cxx.swift +48 -0
- package/nitrogen/generated/shared/c++/HybridChartWebviewSpec.cpp +4 -0
- package/nitrogen/generated/shared/c++/HybridChartWebviewSpec.hpp +4 -0
- package/nitrogen/generated/shared/c++/views/HybridChartWebviewComponent.cpp +24 -0
- package/nitrogen/generated/shared/c++/views/HybridChartWebviewComponent.hpp +2 -0
- package/nitrogen/generated/shared/json/ChartWebviewConfig.json +2 -0
- package/package.json +1 -1
- package/src/ChartWebview.nitro.ts +18 -0
package/ChartWebview.podspec
CHANGED
|
@@ -21,6 +21,9 @@ Pod::Spec.new do |s|
|
|
|
21
21
|
|
|
22
22
|
s.dependency 'React-jsi'
|
|
23
23
|
s.dependency 'React-callinvoker'
|
|
24
|
+
# DEBUG instrumentation: route native chart lifecycle logs into the shared
|
|
25
|
+
# OneKeyLog file (app-latest.log) via `import ReactNativeNativeLogger`.
|
|
26
|
+
s.dependency 'ReactNativeNativeLogger'
|
|
24
27
|
|
|
25
28
|
load 'nitrogen/generated/ios/ChartWebview+autolinking.rb'
|
|
26
29
|
add_nitrogen_files(s)
|
|
@@ -98,34 +98,46 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp
|
|
|
98
98
|
|
|
99
99
|
// --- Source props ---
|
|
100
100
|
|
|
101
|
+
// Source/bridge setters call applySource (synchronous) — NOT scheduleReconcile,
|
|
102
|
+
// which caused an infinite reconcile loop (reconcile -> setSource -> prop
|
|
103
|
+
// re-apply -> reconcile ...). applySource sets warmDriver + warm-boots the page
|
|
104
|
+
// even when this host is not the visible owner, so post-attach bridge arrival
|
|
105
|
+
// still makes this host the warm driver (page->native callbacks fall back to it).
|
|
101
106
|
private var _uri: String? = null
|
|
102
107
|
override var uri: String?
|
|
103
108
|
get() = _uri
|
|
104
|
-
set(value) { _uri = value;
|
|
109
|
+
set(value) { _uri = value; applySource() }
|
|
105
110
|
|
|
106
111
|
private var _localBundle: String? = null
|
|
107
112
|
override var localBundle: String?
|
|
108
113
|
get() = _localBundle
|
|
109
|
-
set(value) { _localBundle = value;
|
|
114
|
+
set(value) { _localBundle = value; applySource() }
|
|
110
115
|
|
|
111
116
|
private var _entry: String? = null
|
|
112
117
|
override var entry: String?
|
|
113
118
|
get() = _entry
|
|
114
|
-
set(value) { _entry = value;
|
|
119
|
+
set(value) { _entry = value; applySource() }
|
|
115
120
|
|
|
116
121
|
private var _paramsJson: String? = null
|
|
117
122
|
override var paramsJson: String?
|
|
118
123
|
get() = _paramsJson
|
|
119
|
-
set(value) { _paramsJson = value;
|
|
124
|
+
set(value) { _paramsJson = value; applySource() }
|
|
125
|
+
|
|
126
|
+
// ANDROID: the host the offline `localBundle` is served under. When set, the
|
|
127
|
+
// offline bundle is served from https://<assetHost>/ so the WebView reuses the
|
|
128
|
+
// real chart origin's same-origin storage (zero migration). Falls back to the
|
|
129
|
+
// built-in appassets host in the pool when null. (iOS ignores this prop.)
|
|
130
|
+
private var _assetHost: String? = null
|
|
131
|
+
override var assetHost: String?
|
|
132
|
+
get() = _assetHost
|
|
133
|
+
set(value) { _assetHost = value; applySource() }
|
|
120
134
|
|
|
121
135
|
// Document-start bridge JS (single source of truth in the TS layer). Stored and
|
|
122
136
|
// handed to the pooled WebView when we claim it, before its first load.
|
|
123
137
|
private var _bridgeScript: String? = null
|
|
124
138
|
override var bridgeScript: String?
|
|
125
139
|
get() = _bridgeScript
|
|
126
|
-
|
|
127
|
-
// run before the bridge is registered, so setSource deferred), this triggers it.
|
|
128
|
-
set(value) { _bridgeScript = value; applySourceIfOwner() }
|
|
140
|
+
set(value) { _bridgeScript = value; applySource() }
|
|
129
141
|
|
|
130
142
|
// --- Singleton props ---
|
|
131
143
|
// `pooled` + non-empty `reuseKey` => the backing WebView is shared (keyed by
|
|
@@ -162,6 +174,22 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp
|
|
|
162
174
|
scheduleReconcile()
|
|
163
175
|
}
|
|
164
176
|
|
|
177
|
+
// --- Debugging ---
|
|
178
|
+
// Make the backing WebView inspectable via chrome://inspect. Driven by the
|
|
179
|
+
// app's "Enable Native Webview Debugging" dev-mode toggle, mirroring the main
|
|
180
|
+
// react-native-webview (which calls WebView.setWebContentsDebuggingEnabled).
|
|
181
|
+
// Stored and applied to the backing WebView both here (on prop change) and at
|
|
182
|
+
// host claim, since `backing` is null until the first reconcile assigns it.
|
|
183
|
+
// CAVEAT: setWebContentsDebuggingEnabled is PROCESS-GLOBAL — see
|
|
184
|
+
// PooledChartWebView.setInspectable.
|
|
185
|
+
private var _webviewDebuggingEnabled: Boolean? = null
|
|
186
|
+
override var webviewDebuggingEnabled: Boolean?
|
|
187
|
+
get() = _webviewDebuggingEnabled
|
|
188
|
+
set(value) {
|
|
189
|
+
_webviewDebuggingEnabled = value
|
|
190
|
+
backing?.setInspectable(value)
|
|
191
|
+
}
|
|
192
|
+
|
|
165
193
|
// --- Event callbacks ---
|
|
166
194
|
override var onMessage: ((message: String) -> Unit)? = null
|
|
167
195
|
override var onLoadEnd: (() -> Unit)? = null
|
|
@@ -219,6 +247,9 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp
|
|
|
219
247
|
val key = effectiveKey()
|
|
220
248
|
val entry = ChartWebviewPool.acquireShared(key, context)
|
|
221
249
|
backing = entry
|
|
250
|
+
// Apply this host's debug preference to the (possibly freshly created) backing
|
|
251
|
+
// WebView. Mirrors the main react-native-webview's setWebContentsDebuggingEnabled.
|
|
252
|
+
entry.setInspectable(_webviewDebuggingEnabled)
|
|
222
253
|
// Refcount the pool entry once per host (reconcile runs many times). Balanced
|
|
223
254
|
// by releaseShared in dispose(). If the reuseKey changed, release the old one.
|
|
224
255
|
if (adoptedPoolKey != key) {
|
|
@@ -226,13 +257,24 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp
|
|
|
226
257
|
ChartWebviewPool.adopt(key)
|
|
227
258
|
adoptedPoolKey = key
|
|
228
259
|
}
|
|
260
|
+
// WARM-BOOT (mirror of iOS): boot the shared offline page + register as the
|
|
261
|
+
// warm DRIVER as soon as ANY referencing host has the bridge, even when not
|
|
262
|
+
// the visible owner (offscreen prewarm runs with attached=false/active=false).
|
|
263
|
+
// page->native callbacks fall back to warmDriver when there's no owner, so the
|
|
264
|
+
// bars-state / load-end signals aren't dropped during warm — otherwise the
|
|
265
|
+
// chart stays on the loading mask. Idempotent: setSource dedupes on same URL,
|
|
266
|
+
// setBridgeScript no-ops once registered.
|
|
267
|
+
val bs = _bridgeScript
|
|
268
|
+
if (!bs.isNullOrEmpty()) {
|
|
269
|
+
entry.setBridgeScript(bs)
|
|
270
|
+
entry.warmDriver = this
|
|
271
|
+
entry.setSource(_uri, _localBundle, _entry, _paramsJson, _assetHost)
|
|
272
|
+
}
|
|
229
273
|
if (wantsOwnership()) {
|
|
230
274
|
entry.owner = this
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
entry.setBridgeScript(_bridgeScript ?: "")
|
|
275
|
+
// PERF: a host is now showing the chart — make sure the renderer is running.
|
|
276
|
+
entry.resume()
|
|
234
277
|
entry.attachTo(container)
|
|
235
|
-
entry.setSource(_uri, _localBundle, _entry, _paramsJson)
|
|
236
278
|
// Keep a fresh frame of OUR content while we own the WebView, so that when
|
|
237
279
|
// we later go inactive (and the shared WebView reloads the other slot's
|
|
238
280
|
// chart) our slot freezes to its own last frame, not the other content.
|
|
@@ -248,6 +290,11 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp
|
|
|
248
290
|
if (wasOwner) entry.owner = null
|
|
249
291
|
stopOwnCapture()
|
|
250
292
|
showPlaceholder(ownSnapshot)
|
|
293
|
+
// PERF: if nobody owns the shared WebView now, pause its renderer so it
|
|
294
|
+
// doesn't keep burning a CPU core + GPU + RAM in the background (Android
|
|
295
|
+
// doesn't auto-throttle offscreen WebViews like iOS does). Resumed on the
|
|
296
|
+
// next CLAIM. No-op until the page's first load completed.
|
|
297
|
+
entry.pauseIfIdle()
|
|
251
298
|
}
|
|
252
299
|
}
|
|
253
300
|
|
|
@@ -257,19 +304,30 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp
|
|
|
257
304
|
if (!wantsOwnership()) return
|
|
258
305
|
val pooled = backing ?: PooledChartWebView.create(context, effectiveKey())
|
|
259
306
|
backing = pooled
|
|
307
|
+
pooled.setInspectable(_webviewDebuggingEnabled)
|
|
260
308
|
pooled.owner = this
|
|
261
309
|
pooled.setBridgeScript(_bridgeScript ?: "")
|
|
262
310
|
pooled.attachTo(container)
|
|
263
|
-
pooled.setSource(_uri, _localBundle, _entry, _paramsJson)
|
|
311
|
+
pooled.setSource(_uri, _localBundle, _entry, _paramsJson, _assetHost)
|
|
264
312
|
}
|
|
265
313
|
|
|
266
|
-
|
|
314
|
+
// Apply the source synchronously when a source/bridge prop changes. For a
|
|
315
|
+
// pooled host this ALSO registers it as the warmDriver and warm-boots the page
|
|
316
|
+
// even when it is not the visible owner, so page->native callbacks (bars-state /
|
|
317
|
+
// load-end) fall back to it when no host owns the pool. No scheduleReconcile —
|
|
318
|
+
// that looped. backing is null until the first reconcile assigns it (the
|
|
319
|
+
// reuseKey/pooled/active setters still scheduleReconcile), so warmDriver is set
|
|
320
|
+
// on the first prop change after the pool entry is acquired.
|
|
321
|
+
private fun applySource() {
|
|
267
322
|
val pooled = backing ?: return
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
323
|
+
val bs = _bridgeScript
|
|
324
|
+
if (isPooled() && !bs.isNullOrEmpty()) {
|
|
325
|
+
pooled.setBridgeScript(bs)
|
|
326
|
+
pooled.warmDriver = this
|
|
327
|
+
pooled.setSource(_uri, _localBundle, _entry, _paramsJson, _assetHost)
|
|
328
|
+
} else if (pooled.owner == this) {
|
|
271
329
|
pooled.setBridgeScript(_bridgeScript ?: "")
|
|
272
|
-
pooled.setSource(_uri, _localBundle, _entry, _paramsJson)
|
|
330
|
+
pooled.setSource(_uri, _localBundle, _entry, _paramsJson, _assetHost)
|
|
273
331
|
}
|
|
274
332
|
}
|
|
275
333
|
|
|
@@ -359,7 +417,7 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp
|
|
|
359
417
|
// WebView so it doesn't leak a Chromium renderer + JavascriptInterface per
|
|
360
418
|
// mount/unmount. Pooled (shared) backing is intentionally left alive in the
|
|
361
419
|
// warm pool — other hosts may still share it; its lifetime is the pool's.
|
|
362
|
-
fun dispose() {
|
|
420
|
+
override fun dispose() {
|
|
363
421
|
stopOwnCapture()
|
|
364
422
|
container.removeCallbacks(reconcileRunnable)
|
|
365
423
|
container.removeCallbacks(revealFallbackRunnable)
|
|
@@ -368,10 +426,19 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp
|
|
|
368
426
|
if (pendingRevealHost == this) pendingRevealHost = null
|
|
369
427
|
val entry = backing
|
|
370
428
|
if (entry != null && entry.owner == this) entry.owner = null
|
|
429
|
+
if (entry != null && entry.warmDriver == this) entry.warmDriver = null
|
|
371
430
|
// Balance the pool adopt() if this host ever joined a pooled key. Kept warm by
|
|
372
431
|
// default (single-instance cache); the release path makes destroy() reachable.
|
|
373
432
|
adoptedPoolKey?.let { ChartWebviewPool.releaseShared(it) }
|
|
374
433
|
adoptedPoolKey = null
|
|
434
|
+
// Pooled host going away: release the shared WebView from THIS host's
|
|
435
|
+
// container, which is about to be dropped from the view tree. The pool keeps
|
|
436
|
+
// the WebView alive (warm) for the next host, but if we leave it parented to
|
|
437
|
+
// our dropped container the next host's attach can't clear that stale parent
|
|
438
|
+
// (removeView on a detached container isn't applied synchronously) and retries
|
|
439
|
+
// forever → blank/white chart. detachFrom is parent-checked, so if another host
|
|
440
|
+
// already re-claimed the WebView we leave it where it is.
|
|
441
|
+
if (entry != null && isPooled()) entry.detachFrom(container)
|
|
375
442
|
// A private (non-pooled) instance is owned solely by this host — tear it down
|
|
376
443
|
// so it doesn't leak a Chromium renderer + JavascriptInterface.
|
|
377
444
|
if (entry != null && !isPooled()) entry.destroy()
|
|
@@ -52,6 +52,9 @@ class PooledChartWebView private constructor(
|
|
|
52
52
|
const val ASSET_HOST = "appassets.androidplatform.net"
|
|
53
53
|
private const val DEFAULT_ENTRY = "index.html"
|
|
54
54
|
|
|
55
|
+
// Operational log tag for this pool (lifecycle + error paths only).
|
|
56
|
+
private const val TAG = "ChartWebviewPool"
|
|
57
|
+
|
|
55
58
|
// Tiny platform transport shim: defines the single hook the shared TS bridge
|
|
56
59
|
// (CHART_BRIDGE_JS) calls. The bulk of the bridge lives in the TS layer and
|
|
57
60
|
// arrives via the `bridgeScript` ctor arg — this is the only platform-specific
|
|
@@ -123,12 +126,12 @@ class PooledChartWebView private constructor(
|
|
|
123
126
|
}
|
|
124
127
|
|
|
125
128
|
// The trusted origin set for the current source: the offline asset origin
|
|
126
|
-
// (https
|
|
127
|
-
// `uri` origin when running in online mode. computeTargetUrl
|
|
128
|
-
// content from
|
|
129
|
-
// the origins the privileged bridge legitimately runs in.
|
|
129
|
+
// (https://<assetHost>, default appassets.androidplatform.net) always, plus the
|
|
130
|
+
// online/fallback `uri` origin when running in online mode. computeTargetUrl
|
|
131
|
+
// serves offline content from `assetHost` and online content from `uri`, so
|
|
132
|
+
// these are exactly the origins the privileged bridge legitimately runs in.
|
|
130
133
|
private fun trustedOriginsFor(uri: String?): Set<String> {
|
|
131
|
-
val origins = linkedSetOf("https://$
|
|
134
|
+
val origins = linkedSetOf("https://$assetHost")
|
|
132
135
|
originOf(uri)?.let { origins.add(it) }
|
|
133
136
|
return origins
|
|
134
137
|
}
|
|
@@ -148,11 +151,74 @@ class PooledChartWebView private constructor(
|
|
|
148
151
|
}
|
|
149
152
|
|
|
150
153
|
/** The host currently displaying this WebView; page events route here. */
|
|
151
|
-
var
|
|
154
|
+
private var _owner: HybridChartWebview? = null
|
|
155
|
+
var owner: HybridChartWebview?
|
|
156
|
+
get() = _owner
|
|
157
|
+
set(value) {
|
|
158
|
+
_owner = value
|
|
159
|
+
}
|
|
160
|
+
// The host that warm-booted the page and can drive its symbol / receive its
|
|
161
|
+
// callbacks while there is no VISIBLE owner yet. Separate from `owner` (the
|
|
162
|
+
// YIELD path clears `owner`); callbacks fall back to this so bars-state /
|
|
163
|
+
// load-end aren't dropped during warm. Mirror of the iOS warmDriver.
|
|
164
|
+
private var _warmDriver: HybridChartWebview? = null
|
|
165
|
+
var warmDriver: HybridChartWebview?
|
|
166
|
+
get() = _warmDriver
|
|
167
|
+
set(value) {
|
|
168
|
+
_warmDriver = value
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// PERF (Android only): Android's in-process WebView/Chromium does NOT throttle
|
|
172
|
+
// an offscreen/unowned page (unlike iOS WKWebView). Left running, the warm
|
|
173
|
+
// pooled page keeps its rAF render loop + websockets + compositing alive
|
|
174
|
+
// FOREVER after the user leaves the chart — pinning a CPU core, growing RAM to
|
|
175
|
+
// OOM, and stealing the GPU from RN's RenderThread (every other screen stalls).
|
|
176
|
+
// We pause the WebView whenever no host owns it (after the first load) and
|
|
177
|
+
// resume on claim. Uses the PER-INSTANCE onPause()/onResume() — NOT the static
|
|
178
|
+
// pauseTimers()/resumeTimers(), which are process-global and would also freeze
|
|
179
|
+
// the app's other WebViews (inpage provider / web-embed / dapp browser).
|
|
180
|
+
private var paused = false
|
|
181
|
+
private var hasLoadedOnce = false
|
|
182
|
+
|
|
183
|
+
fun markLoaded() {
|
|
184
|
+
hasLoadedOnce = true
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Pause the renderer when nobody owns the shared WebView. onPause() stops
|
|
188
|
+
// drawing/compositing/animations (frees the GPU so navigation is smooth again)
|
|
189
|
+
// WITHOUT changing the view's visibility/attachment — we must NOT toggle
|
|
190
|
+
// visibility here, that left the WebView blank/white after re-claim. Skipped
|
|
191
|
+
// until the first load so we never freeze a booting page. Resumed (+ redraw)
|
|
192
|
+
// on the next CLAIM so the chart paints again.
|
|
193
|
+
fun pauseIfIdle() {
|
|
194
|
+
if (paused || !hasLoadedOnce || owner != null) {
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
paused = true
|
|
198
|
+
android.util.Log.i(TAG, "pool[$key] PAUSE (renderer idle)")
|
|
199
|
+
runOnUiThread { webView.onPause() }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
fun resume() {
|
|
203
|
+
if (!paused) return
|
|
204
|
+
paused = false
|
|
205
|
+
android.util.Log.i(TAG, "pool[$key] RESUME")
|
|
206
|
+
runOnUiThread {
|
|
207
|
+
webView.onResume()
|
|
208
|
+
webView.invalidate()
|
|
209
|
+
}
|
|
210
|
+
}
|
|
152
211
|
|
|
153
212
|
private var assetLoader: WebViewAssetLoader? = null
|
|
154
213
|
private var lastLoadedUrl: String? = null
|
|
155
214
|
private var lastLocalBundle: String? = null
|
|
215
|
+
// The host the offline bundle is served under. Defaults to the built-in
|
|
216
|
+
// appassets host (old behavior); when the app passes `assetHost` we serve the
|
|
217
|
+
// bundle under the real chart origin so the WebView reuses that origin's
|
|
218
|
+
// same-origin storage (zero migration). Tracked so an assetHost change rebuilds
|
|
219
|
+
// the asset loader + recomputes the target URL, same as a localBundle change.
|
|
220
|
+
private var assetHost: String = ASSET_HOST
|
|
221
|
+
private var lastAssetHost: String = ASSET_HOST
|
|
156
222
|
|
|
157
223
|
// Last rendered frame, used to mask the brief blank frame while the WebView's
|
|
158
224
|
// surface is torn down and recreated during a reparent (move). Kept until
|
|
@@ -179,9 +245,28 @@ class PooledChartWebView private constructor(
|
|
|
179
245
|
addJavascriptInterface(ChartBridge(), "AndroidChartBridge")
|
|
180
246
|
}
|
|
181
247
|
|
|
248
|
+
// Apply the app's "Enable Native Webview Debugging" dev-mode toggle, mirroring
|
|
249
|
+
// how the main react-native-webview calls WebView.setWebContentsDebuggingEnabled.
|
|
250
|
+
// Called by the host both on the prop change and at host claim, so the toggle is
|
|
251
|
+
// honored even when this entry was created before the prop arrived.
|
|
252
|
+
//
|
|
253
|
+
// CAVEAT (PROCESS-GLOBAL): setWebContentsDebuggingEnabled is a STATIC method that
|
|
254
|
+
// flips remote-debugging for EVERY WebView in the whole process. Once any WebView
|
|
255
|
+
// (this chart, the main react-native-webview, etc.) enables it, it stays enabled
|
|
256
|
+
// process-wide until the app is killed — Android exposes no per-WebView toggle and
|
|
257
|
+
// no way to read the current value. So a null/false preference here cannot turn
|
|
258
|
+
// debugging back OFF once another WebView (or a prior true value) turned it ON; it
|
|
259
|
+
// simply does not re-enable it. We therefore only ever call the setter with `true`
|
|
260
|
+
// when explicitly enabled, leaving the process-global state untouched otherwise.
|
|
261
|
+
fun setInspectable(enabled: Boolean?) {
|
|
262
|
+
if (enabled == true) {
|
|
263
|
+
runOnUiThread { WebView.setWebContentsDebuggingEnabled(true) }
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
182
267
|
init {
|
|
183
268
|
val n = liveCount.incrementAndGet()
|
|
184
|
-
android.util.Log.d(
|
|
269
|
+
android.util.Log.d(TAG, "WebView CREATED key=$key liveCount=$n")
|
|
185
270
|
|
|
186
271
|
webView.webViewClient = object : WebViewClientCompat() {
|
|
187
272
|
override fun shouldInterceptRequest(
|
|
@@ -191,11 +276,12 @@ class PooledChartWebView private constructor(
|
|
|
191
276
|
|
|
192
277
|
override fun onPageFinished(view: WebView, url: String?) {
|
|
193
278
|
super.onPageFinished(view, url)
|
|
279
|
+
markLoaded()
|
|
194
280
|
// The bridge is delivered exclusively via the origin-scoped document-start
|
|
195
281
|
// script (see registerBridgeForOrigins). We deliberately do NOT re-inject
|
|
196
282
|
// it here via evaluateJavascript — that ran on every page event regardless
|
|
197
283
|
// of URL and would expose the privileged bridge to untrusted pages/frames.
|
|
198
|
-
owner?.dispatchLoadEnd()
|
|
284
|
+
(owner ?: warmDriver)?.dispatchLoadEnd()
|
|
199
285
|
// Prime the snapshot so the first move already has a frame to mask with.
|
|
200
286
|
refreshSnapshotSoon()
|
|
201
287
|
}
|
|
@@ -217,7 +303,26 @@ class PooledChartWebView private constructor(
|
|
|
217
303
|
private inner class ChartBridge {
|
|
218
304
|
@JavascriptInterface
|
|
219
305
|
fun postMessage(message: String) {
|
|
220
|
-
runOnUiThread {
|
|
306
|
+
runOnUiThread {
|
|
307
|
+
val target = owner ?: warmDriver
|
|
308
|
+
target?.dispatchMessage(message)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Robustly detach the WebView from [parent], even when [parent] is a disposed
|
|
314
|
+
// host container that is mid-teardown / detached from the window and holds the
|
|
315
|
+
// child in a transition / disappearing-children list — where a plain removeView()
|
|
316
|
+
// is deferred and leaves webView.parent set, stranding the shared WebView and
|
|
317
|
+
// blanking the next chart slot. endViewTransition() clears any pending transition
|
|
318
|
+
// hold; removeViewInLayout() is the in-layout fallback if the child is still held.
|
|
319
|
+
private fun forceDetach(parent: ViewGroup) {
|
|
320
|
+
if (webView.parent !== parent) return
|
|
321
|
+
try { parent.endViewTransition(webView) } catch (e: Throwable) {}
|
|
322
|
+
parent.removeView(webView)
|
|
323
|
+
if (webView.parent === parent) {
|
|
324
|
+
try { parent.removeViewInLayout(webView) } catch (e: Throwable) {}
|
|
325
|
+
parent.requestLayout()
|
|
221
326
|
}
|
|
222
327
|
}
|
|
223
328
|
|
|
@@ -225,23 +330,30 @@ class PooledChartWebView private constructor(
|
|
|
225
330
|
fun attachTo(container: ViewGroup) {
|
|
226
331
|
val generation = attachGeneration.incrementAndGet()
|
|
227
332
|
runOnUiThread {
|
|
228
|
-
attachToContainer(container, generation,
|
|
333
|
+
attachToContainer(container, generation, retriesLeft = 12)
|
|
229
334
|
}
|
|
230
335
|
}
|
|
231
336
|
|
|
232
|
-
private fun attachToContainer(container: ViewGroup, generation: Int,
|
|
337
|
+
private fun attachToContainer(container: ViewGroup, generation: Int, retriesLeft: Int) {
|
|
233
338
|
if (generation != attachGeneration.get()) return
|
|
234
339
|
if (webView.parent === container) return
|
|
235
340
|
|
|
236
|
-
(webView.parent as? ViewGroup)?.
|
|
341
|
+
(webView.parent as? ViewGroup)?.let { forceDetach(it) }
|
|
237
342
|
val currentParent = webView.parent
|
|
238
|
-
if (currentParent != null) {
|
|
239
|
-
|
|
240
|
-
|
|
343
|
+
if (currentParent != null && currentParent !== container) {
|
|
344
|
+
// The old container still hasn't released the WebView (a disposed host's
|
|
345
|
+
// container mid-teardown / detached from window: removeView is deferred and
|
|
346
|
+
// getParent() stays set). Retry — but on the TARGET container's handler,
|
|
347
|
+
// which is attached to the window so its queue keeps draining, instead of the
|
|
348
|
+
// detached webView/old parent whose post() runnables may never run.
|
|
349
|
+
if (retriesLeft > 0) {
|
|
350
|
+
container.post {
|
|
351
|
+
attachToContainer(container, generation, retriesLeft = retriesLeft - 1)
|
|
352
|
+
}
|
|
241
353
|
} else {
|
|
242
354
|
android.util.Log.w(
|
|
243
|
-
|
|
244
|
-
"Skip attach key=$key because WebView parent was not cleared: $currentParent",
|
|
355
|
+
TAG,
|
|
356
|
+
"Skip attach key=$key after retries because WebView parent was not cleared: $currentParent",
|
|
245
357
|
)
|
|
246
358
|
}
|
|
247
359
|
return
|
|
@@ -260,6 +372,27 @@ class PooledChartWebView private constructor(
|
|
|
260
372
|
refreshSnapshotSoon()
|
|
261
373
|
}
|
|
262
374
|
|
|
375
|
+
/**
|
|
376
|
+
* Remove the WebView from [container] ONLY if it is still parented there.
|
|
377
|
+
*
|
|
378
|
+
* Called on a pooled host's final teardown (dispose): that host's container is
|
|
379
|
+
* about to be dropped from the view tree, and a pooled host deliberately does
|
|
380
|
+
* NOT detach on the YIELD path (to avoid racing a rapid re-claim). Without this,
|
|
381
|
+
* the shared WebView stays parented to the dropped/detached container; the next
|
|
382
|
+
* host's [attachTo] then keeps hitting "old parent not cleared" (removeView on a
|
|
383
|
+
* detached container isn't applied synchronously) and retries forever — leaving
|
|
384
|
+
* the new chart slot blank/white. The `parent === container` guard makes this
|
|
385
|
+
* safe against ordering: if a new host has already re-claimed and reparented the
|
|
386
|
+
* WebView, we must NOT rip it back off, so we only remove when WE still hold it.
|
|
387
|
+
*/
|
|
388
|
+
fun detachFrom(container: ViewGroup) {
|
|
389
|
+
runOnUiThread {
|
|
390
|
+
if (webView.parent === container) {
|
|
391
|
+
forceDetach(container)
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
263
396
|
/** Remove the WebView from its current parent (keeps it alive, warm). */
|
|
264
397
|
fun detachFromParent() {
|
|
265
398
|
runOnUiThread {
|
|
@@ -389,13 +522,23 @@ class PooledChartWebView private constructor(
|
|
|
389
522
|
* so reparenting / redundant prop re-applies never reload (which would lose
|
|
390
523
|
* chart state, the whole point of pooling).
|
|
391
524
|
*/
|
|
392
|
-
fun setSource(
|
|
525
|
+
fun setSource(
|
|
526
|
+
uri: String?,
|
|
527
|
+
localBundle: String?,
|
|
528
|
+
entry: String?,
|
|
529
|
+
paramsJson: String?,
|
|
530
|
+
assetHost: String?,
|
|
531
|
+
) {
|
|
393
532
|
// Never load before the document-start bridge is registered — otherwise the
|
|
394
533
|
// page boots without the bridge and its first $private requests are lost. The
|
|
395
534
|
// host re-calls setSource once the bridgeScript prop arrives.
|
|
396
535
|
if (!bridgeRegistered) return
|
|
397
|
-
|
|
536
|
+
// Per-instance asset host: fall back to the built-in appassets host (old
|
|
537
|
+
// behavior) when the app doesn't pass one. Empty string is treated as absent.
|
|
538
|
+
this.assetHost = assetHost?.takeIf { it.isNotEmpty() } ?: ASSET_HOST
|
|
539
|
+
if (localBundle != lastLocalBundle || this.assetHost != lastAssetHost) {
|
|
398
540
|
lastLocalBundle = localBundle
|
|
541
|
+
lastAssetHost = this.assetHost
|
|
399
542
|
rebuildAssetLoader(localBundle)
|
|
400
543
|
}
|
|
401
544
|
// Register the document-start bridge scoped to exactly the origin(s) this load
|
|
@@ -437,7 +580,7 @@ class PooledChartWebView private constructor(
|
|
|
437
580
|
(webView.parent as? ViewGroup)?.removeView(webView)
|
|
438
581
|
webView.destroy()
|
|
439
582
|
val n = liveCount.decrementAndGet()
|
|
440
|
-
android.util.Log.d(
|
|
583
|
+
android.util.Log.d(TAG, "WebView DESTROYED key=$key liveCount=$n")
|
|
441
584
|
}
|
|
442
585
|
}
|
|
443
586
|
|
|
@@ -446,9 +589,28 @@ class PooledChartWebView private constructor(
|
|
|
446
589
|
assetLoader = null
|
|
447
590
|
return
|
|
448
591
|
}
|
|
592
|
+
// Narrow the path handler to ONLY the offline bundle subtree
|
|
593
|
+
// (/<localBundle>/, e.g. /tradingview-assets/) instead of intercepting all of
|
|
594
|
+
// "/". When the bundle is served under the real chart origin (`assetHost`),
|
|
595
|
+
// intercepting "/" would shadow every web path on that domain; with the narrow
|
|
596
|
+
// handler, unmatched paths fall through to the network — so an `assetHost`
|
|
597
|
+
// that is a real public domain keeps behaving normally off-bundle.
|
|
598
|
+
// computeTargetUrl produces https://<assetHost>/<localBundle>/<entry>, which
|
|
599
|
+
// stays inside this handler's path. (Path built as /<localBundle>/ so a
|
|
600
|
+
// localBundle that already has a trailing slash doesn't double it.)
|
|
601
|
+
val bundleDir = localBundle.trim('/')
|
|
602
|
+
val bundlePath = "/$bundleDir/"
|
|
603
|
+
// WebViewAssetLoader strips the registered prefix before calling the handler,
|
|
604
|
+
// so AssetsPathHandler alone would resolve the remainder against the assets
|
|
605
|
+
// ROOT (assets/<entry>) and lose the bundle namespace. Re-prepend <bundleDir>/
|
|
606
|
+
// so we still serve from assets/<localBundle>/<entry>. Returning null on a
|
|
607
|
+
// miss lets WebViewAssetLoader fall through to the network.
|
|
608
|
+
val assets = WebViewAssetLoader.AssetsPathHandler(webView.context)
|
|
609
|
+
val namespacedHandler =
|
|
610
|
+
WebViewAssetLoader.PathHandler { suffix -> assets.handle("$bundleDir/$suffix") }
|
|
449
611
|
assetLoader = WebViewAssetLoader.Builder()
|
|
450
|
-
.setDomain(
|
|
451
|
-
.addPathHandler(
|
|
612
|
+
.setDomain(assetHost)
|
|
613
|
+
.addPathHandler(bundlePath, namespacedHandler)
|
|
452
614
|
.build()
|
|
453
615
|
}
|
|
454
616
|
|
|
@@ -464,7 +626,7 @@ class PooledChartWebView private constructor(
|
|
|
464
626
|
val query = buildQueryFromParamsJson(paramsJson)
|
|
465
627
|
return buildString {
|
|
466
628
|
append("https://")
|
|
467
|
-
append(
|
|
629
|
+
append(assetHost)
|
|
468
630
|
append('/')
|
|
469
631
|
// Serve the offline bundle from assets/<localBundle>/ (namespaced) rather
|
|
470
632
|
// than the assets root, so it doesn't collide with other bundled assets
|