@onekeyfe/react-native-chart-webview 3.0.61 → 3.0.63
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 +0 -3
- package/android/src/main/java/com/margelo/nitro/chartwebview/ChartWebview.kt +18 -85
- package/android/src/main/java/com/margelo/nitro/chartwebview/PooledChartWebView.kt +26 -253
- package/ios/ChartWebview.swift +31 -190
- package/lib/nitrogen/generated/android/c++/JHybridChartWebviewSpec.cpp +0 -18
- package/lib/nitrogen/generated/android/c++/JHybridChartWebviewSpec.hpp +0 -4
- package/lib/nitrogen/generated/android/c++/views/JHybridChartWebviewStateUpdater.cpp +0 -8
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/chartwebview/HybridChartWebviewSpec.kt +0 -12
- package/lib/nitrogen/generated/ios/c++/HybridChartWebviewSpecSwift.hpp +0 -14
- package/lib/nitrogen/generated/ios/c++/views/HybridChartWebviewComponent.mm +0 -10
- package/lib/nitrogen/generated/ios/swift/HybridChartWebviewSpec.swift +0 -2
- package/lib/nitrogen/generated/ios/swift/HybridChartWebviewSpec_cxx.swift +0 -48
- package/lib/nitrogen/generated/shared/c++/HybridChartWebviewSpec.cpp +0 -4
- package/lib/nitrogen/generated/shared/c++/HybridChartWebviewSpec.hpp +0 -4
- package/lib/nitrogen/generated/shared/c++/views/HybridChartWebviewComponent.cpp +0 -24
- package/lib/nitrogen/generated/shared/c++/views/HybridChartWebviewComponent.hpp +0 -2
- package/lib/nitrogen/generated/shared/json/ChartWebviewConfig.json +0 -2
- package/lib/nitrogen/nitrogen/generated/android/c++/JHybridChartWebviewSpec.cpp +0 -18
- package/lib/nitrogen/nitrogen/generated/android/c++/JHybridChartWebviewSpec.hpp +0 -4
- package/lib/nitrogen/nitrogen/generated/android/c++/views/JHybridChartWebviewStateUpdater.cpp +0 -8
- package/lib/nitrogen/nitrogen/generated/android/kotlin/com/margelo/nitro/chartwebview/HybridChartWebviewSpec.kt +0 -12
- package/lib/nitrogen/nitrogen/generated/ios/c++/HybridChartWebviewSpecSwift.hpp +0 -14
- package/lib/nitrogen/nitrogen/generated/ios/c++/views/HybridChartWebviewComponent.mm +0 -10
- package/lib/nitrogen/nitrogen/generated/ios/swift/HybridChartWebviewSpec.swift +0 -2
- package/lib/nitrogen/nitrogen/generated/ios/swift/HybridChartWebviewSpec_cxx.swift +0 -48
- package/lib/nitrogen/nitrogen/generated/shared/c++/HybridChartWebviewSpec.cpp +0 -4
- package/lib/nitrogen/nitrogen/generated/shared/c++/HybridChartWebviewSpec.hpp +0 -4
- package/lib/nitrogen/nitrogen/generated/shared/c++/views/HybridChartWebviewComponent.cpp +0 -24
- package/lib/nitrogen/nitrogen/generated/shared/c++/views/HybridChartWebviewComponent.hpp +0 -2
- package/lib/nitrogen/nitrogen/generated/shared/json/ChartWebviewConfig.json +0 -2
- package/lib/typescript/src/ChartWebview.nitro.d.ts +0 -2
- package/lib/typescript/src/ChartWebview.nitro.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +0 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JHybridChartWebviewSpec.cpp +0 -18
- package/nitrogen/generated/android/c++/JHybridChartWebviewSpec.hpp +0 -4
- package/nitrogen/generated/android/c++/views/JHybridChartWebviewStateUpdater.cpp +0 -8
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/chartwebview/HybridChartWebviewSpec.kt +0 -12
- package/nitrogen/generated/ios/c++/HybridChartWebviewSpecSwift.hpp +0 -14
- package/nitrogen/generated/ios/c++/views/HybridChartWebviewComponent.mm +0 -10
- package/nitrogen/generated/ios/swift/HybridChartWebviewSpec.swift +0 -2
- package/nitrogen/generated/ios/swift/HybridChartWebviewSpec_cxx.swift +0 -48
- package/nitrogen/generated/shared/c++/HybridChartWebviewSpec.cpp +0 -4
- package/nitrogen/generated/shared/c++/HybridChartWebviewSpec.hpp +0 -4
- package/nitrogen/generated/shared/c++/views/HybridChartWebviewComponent.cpp +0 -24
- package/nitrogen/generated/shared/c++/views/HybridChartWebviewComponent.hpp +0 -2
- package/nitrogen/generated/shared/json/ChartWebviewConfig.json +0 -2
- package/package.json +1 -1
- package/src/ChartWebview.nitro.ts +0 -18
package/ChartWebview.podspec
CHANGED
|
@@ -21,9 +21,6 @@ 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'
|
|
27
24
|
|
|
28
25
|
load 'nitrogen/generated/ios/ChartWebview+autolinking.rb'
|
|
29
26
|
add_nitrogen_files(s)
|
|
@@ -98,46 +98,34 @@ 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).
|
|
106
101
|
private var _uri: String? = null
|
|
107
102
|
override var uri: String?
|
|
108
103
|
get() = _uri
|
|
109
|
-
set(value) { _uri = value;
|
|
104
|
+
set(value) { _uri = value; applySourceIfOwner() }
|
|
110
105
|
|
|
111
106
|
private var _localBundle: String? = null
|
|
112
107
|
override var localBundle: String?
|
|
113
108
|
get() = _localBundle
|
|
114
|
-
set(value) { _localBundle = value;
|
|
109
|
+
set(value) { _localBundle = value; applySourceIfOwner() }
|
|
115
110
|
|
|
116
111
|
private var _entry: String? = null
|
|
117
112
|
override var entry: String?
|
|
118
113
|
get() = _entry
|
|
119
|
-
set(value) { _entry = value;
|
|
114
|
+
set(value) { _entry = value; applySourceIfOwner() }
|
|
120
115
|
|
|
121
116
|
private var _paramsJson: String? = null
|
|
122
117
|
override var paramsJson: String?
|
|
123
118
|
get() = _paramsJson
|
|
124
|
-
set(value) { _paramsJson = value;
|
|
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() }
|
|
119
|
+
set(value) { _paramsJson = value; applySourceIfOwner() }
|
|
134
120
|
|
|
135
121
|
// Document-start bridge JS (single source of truth in the TS layer). Stored and
|
|
136
122
|
// handed to the pooled WebView when we claim it, before its first load.
|
|
137
123
|
private var _bridgeScript: String? = null
|
|
138
124
|
override var bridgeScript: String?
|
|
139
125
|
get() = _bridgeScript
|
|
140
|
-
|
|
126
|
+
// Re-apply the source: if this prop lands after the source props (a load can't
|
|
127
|
+
// run before the bridge is registered, so setSource deferred), this triggers it.
|
|
128
|
+
set(value) { _bridgeScript = value; applySourceIfOwner() }
|
|
141
129
|
|
|
142
130
|
// --- Singleton props ---
|
|
143
131
|
// `pooled` + non-empty `reuseKey` => the backing WebView is shared (keyed by
|
|
@@ -174,22 +162,6 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp
|
|
|
174
162
|
scheduleReconcile()
|
|
175
163
|
}
|
|
176
164
|
|
|
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
|
-
|
|
193
165
|
// --- Event callbacks ---
|
|
194
166
|
override var onMessage: ((message: String) -> Unit)? = null
|
|
195
167
|
override var onLoadEnd: (() -> Unit)? = null
|
|
@@ -247,9 +219,6 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp
|
|
|
247
219
|
val key = effectiveKey()
|
|
248
220
|
val entry = ChartWebviewPool.acquireShared(key, context)
|
|
249
221
|
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)
|
|
253
222
|
// Refcount the pool entry once per host (reconcile runs many times). Balanced
|
|
254
223
|
// by releaseShared in dispose(). If the reuseKey changed, release the old one.
|
|
255
224
|
if (adoptedPoolKey != key) {
|
|
@@ -257,24 +226,13 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp
|
|
|
257
226
|
ChartWebviewPool.adopt(key)
|
|
258
227
|
adoptedPoolKey = key
|
|
259
228
|
}
|
|
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
|
-
}
|
|
273
229
|
if (wantsOwnership()) {
|
|
274
230
|
entry.owner = this
|
|
275
|
-
//
|
|
276
|
-
|
|
231
|
+
// Register the document-start bridge before the first load (the prop is set
|
|
232
|
+
// by now; the pool may have been created earlier by a window-attach reconcile).
|
|
233
|
+
entry.setBridgeScript(_bridgeScript ?: "")
|
|
277
234
|
entry.attachTo(container)
|
|
235
|
+
entry.setSource(_uri, _localBundle, _entry, _paramsJson)
|
|
278
236
|
// Keep a fresh frame of OUR content while we own the WebView, so that when
|
|
279
237
|
// we later go inactive (and the shared WebView reloads the other slot's
|
|
280
238
|
// chart) our slot freezes to its own last frame, not the other content.
|
|
@@ -290,11 +248,6 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp
|
|
|
290
248
|
if (wasOwner) entry.owner = null
|
|
291
249
|
stopOwnCapture()
|
|
292
250
|
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()
|
|
298
251
|
}
|
|
299
252
|
}
|
|
300
253
|
|
|
@@ -304,30 +257,19 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp
|
|
|
304
257
|
if (!wantsOwnership()) return
|
|
305
258
|
val pooled = backing ?: PooledChartWebView.create(context, effectiveKey())
|
|
306
259
|
backing = pooled
|
|
307
|
-
pooled.setInspectable(_webviewDebuggingEnabled)
|
|
308
260
|
pooled.owner = this
|
|
309
261
|
pooled.setBridgeScript(_bridgeScript ?: "")
|
|
310
262
|
pooled.attachTo(container)
|
|
311
|
-
pooled.setSource(_uri, _localBundle, _entry, _paramsJson
|
|
263
|
+
pooled.setSource(_uri, _localBundle, _entry, _paramsJson)
|
|
312
264
|
}
|
|
313
265
|
|
|
314
|
-
|
|
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() {
|
|
266
|
+
private fun applySourceIfOwner() {
|
|
322
267
|
val pooled = backing ?: return
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
pooled.warmDriver = this
|
|
327
|
-
pooled.setSource(_uri, _localBundle, _entry, _paramsJson, _assetHost)
|
|
328
|
-
} else if (pooled.owner == this) {
|
|
268
|
+
if (pooled.owner == this) {
|
|
269
|
+
// Register the bridge before any load setSource may trigger (setSource
|
|
270
|
+
// defers loading until the bridge is registered).
|
|
329
271
|
pooled.setBridgeScript(_bridgeScript ?: "")
|
|
330
|
-
pooled.setSource(_uri, _localBundle, _entry, _paramsJson
|
|
272
|
+
pooled.setSource(_uri, _localBundle, _entry, _paramsJson)
|
|
331
273
|
}
|
|
332
274
|
}
|
|
333
275
|
|
|
@@ -417,7 +359,7 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp
|
|
|
417
359
|
// WebView so it doesn't leak a Chromium renderer + JavascriptInterface per
|
|
418
360
|
// mount/unmount. Pooled (shared) backing is intentionally left alive in the
|
|
419
361
|
// warm pool — other hosts may still share it; its lifetime is the pool's.
|
|
420
|
-
|
|
362
|
+
fun dispose() {
|
|
421
363
|
stopOwnCapture()
|
|
422
364
|
container.removeCallbacks(reconcileRunnable)
|
|
423
365
|
container.removeCallbacks(revealFallbackRunnable)
|
|
@@ -426,19 +368,10 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp
|
|
|
426
368
|
if (pendingRevealHost == this) pendingRevealHost = null
|
|
427
369
|
val entry = backing
|
|
428
370
|
if (entry != null && entry.owner == this) entry.owner = null
|
|
429
|
-
if (entry != null && entry.warmDriver == this) entry.warmDriver = null
|
|
430
371
|
// Balance the pool adopt() if this host ever joined a pooled key. Kept warm by
|
|
431
372
|
// default (single-instance cache); the release path makes destroy() reachable.
|
|
432
373
|
adoptedPoolKey?.let { ChartWebviewPool.releaseShared(it) }
|
|
433
374
|
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)
|
|
442
375
|
// A private (non-pooled) instance is owned solely by this host — tear it down
|
|
443
376
|
// so it doesn't leak a Chromium renderer + JavascriptInterface.
|
|
444
377
|
if (entry != null && !isPooled()) entry.destroy()
|
|
@@ -25,7 +25,6 @@ import androidx.webkit.WebViewClientCompat
|
|
|
25
25
|
import androidx.webkit.WebViewCompat
|
|
26
26
|
import androidx.webkit.WebViewFeature
|
|
27
27
|
import org.json.JSONObject
|
|
28
|
-
import java.lang.ref.WeakReference
|
|
29
28
|
import java.net.URLEncoder
|
|
30
29
|
import java.util.concurrent.atomic.AtomicInteger
|
|
31
30
|
|
|
@@ -53,9 +52,6 @@ class PooledChartWebView private constructor(
|
|
|
53
52
|
const val ASSET_HOST = "appassets.androidplatform.net"
|
|
54
53
|
private const val DEFAULT_ENTRY = "index.html"
|
|
55
54
|
|
|
56
|
-
// Operational log tag for this pool (lifecycle + error paths only).
|
|
57
|
-
private const val TAG = "ChartWebviewPool"
|
|
58
|
-
|
|
59
55
|
// Tiny platform transport shim: defines the single hook the shared TS bridge
|
|
60
56
|
// (CHART_BRIDGE_JS) calls. The bulk of the bridge lives in the TS layer and
|
|
61
57
|
// arrives via the `bridgeScript` ctor arg — this is the only platform-specific
|
|
@@ -127,12 +123,12 @@ class PooledChartWebView private constructor(
|
|
|
127
123
|
}
|
|
128
124
|
|
|
129
125
|
// The trusted origin set for the current source: the offline asset origin
|
|
130
|
-
// (https
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
126
|
+
// (https://appassets.androidplatform.net) always, plus the online/fallback
|
|
127
|
+
// `uri` origin when running in online mode. computeTargetUrl serves offline
|
|
128
|
+
// content from ASSET_HOST and online content from `uri`, so these are exactly
|
|
129
|
+
// the origins the privileged bridge legitimately runs in.
|
|
134
130
|
private fun trustedOriginsFor(uri: String?): Set<String> {
|
|
135
|
-
val origins = linkedSetOf("https://$
|
|
131
|
+
val origins = linkedSetOf("https://$ASSET_HOST")
|
|
136
132
|
originOf(uri)?.let { origins.add(it) }
|
|
137
133
|
return origins
|
|
138
134
|
}
|
|
@@ -152,78 +148,11 @@ class PooledChartWebView private constructor(
|
|
|
152
148
|
}
|
|
153
149
|
|
|
154
150
|
/** The host currently displaying this WebView; page events route here. */
|
|
155
|
-
|
|
156
|
-
// pin a disposed host — and its ReactContext/Activity — forever. A GC'd host
|
|
157
|
-
// reads back as null, which makes `entry.owner == this` correctly false.
|
|
158
|
-
private var _ownerRef: WeakReference<HybridChartWebview>? = null
|
|
159
|
-
var owner: HybridChartWebview?
|
|
160
|
-
get() = _ownerRef?.get()
|
|
161
|
-
set(value) {
|
|
162
|
-
_ownerRef = value?.let { WeakReference(it) }
|
|
163
|
-
}
|
|
164
|
-
// The host that warm-booted the page and can drive its symbol / receive its
|
|
165
|
-
// callbacks while there is no VISIBLE owner yet. Separate from `owner` (the
|
|
166
|
-
// YIELD path clears `owner`); callbacks fall back to this so bars-state /
|
|
167
|
-
// load-end aren't dropped during warm. Mirror of the iOS warmDriver.
|
|
168
|
-
// Weak for the same immortal-pool reason as `owner` above.
|
|
169
|
-
private var _warmDriverRef: WeakReference<HybridChartWebview>? = null
|
|
170
|
-
var warmDriver: HybridChartWebview?
|
|
171
|
-
get() = _warmDriverRef?.get()
|
|
172
|
-
set(value) {
|
|
173
|
-
_warmDriverRef = value?.let { WeakReference(it) }
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// PERF (Android only): Android's in-process WebView/Chromium does NOT throttle
|
|
177
|
-
// an offscreen/unowned page (unlike iOS WKWebView). Left running, the warm
|
|
178
|
-
// pooled page keeps its rAF render loop + websockets + compositing alive
|
|
179
|
-
// FOREVER after the user leaves the chart — pinning a CPU core, growing RAM to
|
|
180
|
-
// OOM, and stealing the GPU from RN's RenderThread (every other screen stalls).
|
|
181
|
-
// We pause the WebView whenever no host owns it (after the first load) and
|
|
182
|
-
// resume on claim. Uses the PER-INSTANCE onPause()/onResume() — NOT the static
|
|
183
|
-
// pauseTimers()/resumeTimers(), which are process-global and would also freeze
|
|
184
|
-
// the app's other WebViews (inpage provider / web-embed / dapp browser).
|
|
185
|
-
private var paused = false
|
|
186
|
-
private var hasLoadedOnce = false
|
|
187
|
-
|
|
188
|
-
fun markLoaded() {
|
|
189
|
-
hasLoadedOnce = true
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Pause the renderer when nobody owns the shared WebView. onPause() stops
|
|
193
|
-
// drawing/compositing/animations (frees the GPU so navigation is smooth again)
|
|
194
|
-
// WITHOUT changing the view's visibility/attachment — we must NOT toggle
|
|
195
|
-
// visibility here, that left the WebView blank/white after re-claim. Skipped
|
|
196
|
-
// until the first load so we never freeze a booting page. Resumed (+ redraw)
|
|
197
|
-
// on the next CLAIM so the chart paints again.
|
|
198
|
-
fun pauseIfIdle() {
|
|
199
|
-
if (paused || !hasLoadedOnce || owner != null) {
|
|
200
|
-
return
|
|
201
|
-
}
|
|
202
|
-
paused = true
|
|
203
|
-
android.util.Log.d(TAG, "pool[$key] PAUSE (renderer idle)")
|
|
204
|
-
runOnUiThread { webView.onPause() }
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
fun resume() {
|
|
208
|
-
if (!paused) return
|
|
209
|
-
paused = false
|
|
210
|
-
android.util.Log.d(TAG, "pool[$key] RESUME")
|
|
211
|
-
runOnUiThread {
|
|
212
|
-
webView.onResume()
|
|
213
|
-
webView.invalidate()
|
|
214
|
-
}
|
|
215
|
-
}
|
|
151
|
+
var owner: HybridChartWebview? = null
|
|
216
152
|
|
|
217
153
|
private var assetLoader: WebViewAssetLoader? = null
|
|
218
154
|
private var lastLoadedUrl: String? = null
|
|
219
155
|
private var lastLocalBundle: String? = null
|
|
220
|
-
// The host the offline bundle is served under. Defaults to the built-in
|
|
221
|
-
// appassets host (old behavior); when the app passes `assetHost` we serve the
|
|
222
|
-
// bundle under the real chart origin so the WebView reuses that origin's
|
|
223
|
-
// same-origin storage (zero migration). Tracked so an assetHost change rebuilds
|
|
224
|
-
// the asset loader + recomputes the target URL, same as a localBundle change.
|
|
225
|
-
private var assetHost: String = ASSET_HOST
|
|
226
|
-
private var lastAssetHost: String = ASSET_HOST
|
|
227
156
|
|
|
228
157
|
// Last rendered frame, used to mask the brief blank frame while the WebView's
|
|
229
158
|
// surface is torn down and recreated during a reparent (move). Kept until
|
|
@@ -238,9 +167,6 @@ class PooledChartWebView private constructor(
|
|
|
238
167
|
// How long the snapshot overlay stays up after a reparent, giving the WebView
|
|
239
168
|
// time to draw its first frame in the new container (~5 frames).
|
|
240
169
|
private val overlayHideDelayMs = 80L
|
|
241
|
-
// Delay between bounded attach retries (~1 vsync at 60Hz): long enough for the
|
|
242
|
-
// old parent's pending layout / removeView to flush, short enough to stay snappy.
|
|
243
|
-
private val attachRetryDelayMs = 16L
|
|
244
170
|
private val attachGeneration = AtomicInteger(0)
|
|
245
171
|
|
|
246
172
|
val webView: WebView = WebView(context).apply {
|
|
@@ -253,28 +179,9 @@ class PooledChartWebView private constructor(
|
|
|
253
179
|
addJavascriptInterface(ChartBridge(), "AndroidChartBridge")
|
|
254
180
|
}
|
|
255
181
|
|
|
256
|
-
// Apply the app's "Enable Native Webview Debugging" dev-mode toggle, mirroring
|
|
257
|
-
// how the main react-native-webview calls WebView.setWebContentsDebuggingEnabled.
|
|
258
|
-
// Called by the host both on the prop change and at host claim, so the toggle is
|
|
259
|
-
// honored even when this entry was created before the prop arrived.
|
|
260
|
-
//
|
|
261
|
-
// CAVEAT (PROCESS-GLOBAL): setWebContentsDebuggingEnabled is a STATIC method that
|
|
262
|
-
// flips remote-debugging for EVERY WebView in the whole process. Once any WebView
|
|
263
|
-
// (this chart, the main react-native-webview, etc.) enables it, it stays enabled
|
|
264
|
-
// process-wide until the app is killed — Android exposes no per-WebView toggle and
|
|
265
|
-
// no way to read the current value. So a null/false preference here cannot turn
|
|
266
|
-
// debugging back OFF once another WebView (or a prior true value) turned it ON; it
|
|
267
|
-
// simply does not re-enable it. We therefore only ever call the setter with `true`
|
|
268
|
-
// when explicitly enabled, leaving the process-global state untouched otherwise.
|
|
269
|
-
fun setInspectable(enabled: Boolean?) {
|
|
270
|
-
if (enabled == true) {
|
|
271
|
-
runOnUiThread { WebView.setWebContentsDebuggingEnabled(true) }
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
182
|
init {
|
|
276
183
|
val n = liveCount.incrementAndGet()
|
|
277
|
-
android.util.Log.d(
|
|
184
|
+
android.util.Log.d("ChartWebviewPool", "WebView CREATED key=$key liveCount=$n")
|
|
278
185
|
|
|
279
186
|
webView.webViewClient = object : WebViewClientCompat() {
|
|
280
187
|
override fun shouldInterceptRequest(
|
|
@@ -284,12 +191,11 @@ class PooledChartWebView private constructor(
|
|
|
284
191
|
|
|
285
192
|
override fun onPageFinished(view: WebView, url: String?) {
|
|
286
193
|
super.onPageFinished(view, url)
|
|
287
|
-
markLoaded()
|
|
288
194
|
// The bridge is delivered exclusively via the origin-scoped document-start
|
|
289
195
|
// script (see registerBridgeForOrigins). We deliberately do NOT re-inject
|
|
290
196
|
// it here via evaluateJavascript — that ran on every page event regardless
|
|
291
197
|
// of URL and would expose the privileged bridge to untrusted pages/frames.
|
|
292
|
-
|
|
198
|
+
owner?.dispatchLoadEnd()
|
|
293
199
|
// Prime the snapshot so the first move already has a frame to mask with.
|
|
294
200
|
refreshSnapshotSoon()
|
|
295
201
|
}
|
|
@@ -311,34 +217,7 @@ class PooledChartWebView private constructor(
|
|
|
311
217
|
private inner class ChartBridge {
|
|
312
218
|
@JavascriptInterface
|
|
313
219
|
fun postMessage(message: String) {
|
|
314
|
-
runOnUiThread {
|
|
315
|
-
val target = owner ?: warmDriver
|
|
316
|
-
target?.dispatchMessage(message)
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Robustly detach the WebView from [parent], even when [parent] is a disposed
|
|
322
|
-
// host container that is mid-teardown / detached from the window and holds the
|
|
323
|
-
// child in a transition / disappearing-children list — where a plain removeView()
|
|
324
|
-
// is deferred and leaves webView.parent set, stranding the shared WebView and
|
|
325
|
-
// blanking the next chart slot. endViewTransition() clears any pending transition
|
|
326
|
-
// hold; removeViewInLayout() is the in-layout fallback if the child is still held.
|
|
327
|
-
private fun forceDetach(parent: ViewGroup) {
|
|
328
|
-
if (webView.parent !== parent) return
|
|
329
|
-
try {
|
|
330
|
-
parent.endViewTransition(webView)
|
|
331
|
-
} catch (e: Throwable) {
|
|
332
|
-
android.util.Log.w(TAG, "forceDetach: endViewTransition failed", e)
|
|
333
|
-
}
|
|
334
|
-
parent.removeView(webView)
|
|
335
|
-
if (webView.parent === parent) {
|
|
336
|
-
try {
|
|
337
|
-
parent.removeViewInLayout(webView)
|
|
338
|
-
} catch (e: Throwable) {
|
|
339
|
-
android.util.Log.w(TAG, "forceDetach: removeViewInLayout failed", e)
|
|
340
|
-
}
|
|
341
|
-
parent.requestLayout()
|
|
220
|
+
runOnUiThread { owner?.dispatchMessage(message) }
|
|
342
221
|
}
|
|
343
222
|
}
|
|
344
223
|
|
|
@@ -346,42 +225,24 @@ class PooledChartWebView private constructor(
|
|
|
346
225
|
fun attachTo(container: ViewGroup) {
|
|
347
226
|
val generation = attachGeneration.incrementAndGet()
|
|
348
227
|
runOnUiThread {
|
|
349
|
-
attachToContainer(container, generation,
|
|
228
|
+
attachToContainer(container, generation, canRetry = true)
|
|
350
229
|
}
|
|
351
230
|
}
|
|
352
231
|
|
|
353
|
-
private fun attachToContainer(container: ViewGroup, generation: Int,
|
|
232
|
+
private fun attachToContainer(container: ViewGroup, generation: Int, canRetry: Boolean) {
|
|
354
233
|
if (generation != attachGeneration.get()) return
|
|
355
234
|
if (webView.parent === container) return
|
|
356
235
|
|
|
357
|
-
(webView.parent as? ViewGroup)?.
|
|
236
|
+
(webView.parent as? ViewGroup)?.removeView(webView)
|
|
358
237
|
val currentParent = webView.parent
|
|
359
|
-
if (currentParent != null
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
// getParent() stays set). Retry — but on the TARGET container's handler,
|
|
363
|
-
// which is attached to the window so its queue keeps draining, instead of the
|
|
364
|
-
// detached webView/old parent whose post() runnables may never run.
|
|
365
|
-
if (retriesLeft > 0) {
|
|
366
|
-
// postDelayed (not a tight container.post spin): a small delay lets the
|
|
367
|
-
// old parent's pending layout / removeView flush between attempts, instead
|
|
368
|
-
// of re-checking on the very next vsync before anything could change.
|
|
369
|
-
container.postDelayed({
|
|
370
|
-
attachToContainer(container, generation, retriesLeft = retriesLeft - 1)
|
|
371
|
-
}, attachRetryDelayMs)
|
|
238
|
+
if (currentParent != null) {
|
|
239
|
+
if (canRetry) {
|
|
240
|
+
webView.post { attachToContainer(container, generation, canRetry = false) }
|
|
372
241
|
} else {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if (webView.parent == null || webView.parent === container) {
|
|
378
|
-
attachToContainer(container, generation, retriesLeft = 0)
|
|
379
|
-
} else {
|
|
380
|
-
android.util.Log.w(
|
|
381
|
-
TAG,
|
|
382
|
-
"Skip attach key=$key after retries because WebView parent was not cleared: $currentParent",
|
|
383
|
-
)
|
|
384
|
-
}
|
|
242
|
+
android.util.Log.w(
|
|
243
|
+
"ChartWebviewPool",
|
|
244
|
+
"Skip attach key=$key because WebView parent was not cleared: $currentParent",
|
|
245
|
+
)
|
|
385
246
|
}
|
|
386
247
|
return
|
|
387
248
|
}
|
|
@@ -399,27 +260,6 @@ class PooledChartWebView private constructor(
|
|
|
399
260
|
refreshSnapshotSoon()
|
|
400
261
|
}
|
|
401
262
|
|
|
402
|
-
/**
|
|
403
|
-
* Remove the WebView from [container] ONLY if it is still parented there.
|
|
404
|
-
*
|
|
405
|
-
* Called on a pooled host's final teardown (dispose): that host's container is
|
|
406
|
-
* about to be dropped from the view tree, and a pooled host deliberately does
|
|
407
|
-
* NOT detach on the YIELD path (to avoid racing a rapid re-claim). Without this,
|
|
408
|
-
* the shared WebView stays parented to the dropped/detached container; the next
|
|
409
|
-
* host's [attachTo] then keeps hitting "old parent not cleared" (removeView on a
|
|
410
|
-
* detached container isn't applied synchronously) and retries forever — leaving
|
|
411
|
-
* the new chart slot blank/white. The `parent === container` guard makes this
|
|
412
|
-
* safe against ordering: if a new host has already re-claimed and reparented the
|
|
413
|
-
* WebView, we must NOT rip it back off, so we only remove when WE still hold it.
|
|
414
|
-
*/
|
|
415
|
-
fun detachFrom(container: ViewGroup) {
|
|
416
|
-
runOnUiThread {
|
|
417
|
-
if (webView.parent === container) {
|
|
418
|
-
forceDetach(container)
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
263
|
/** Remove the WebView from its current parent (keeps it alive, warm). */
|
|
424
264
|
fun detachFromParent() {
|
|
425
265
|
runOnUiThread {
|
|
@@ -549,26 +389,13 @@ class PooledChartWebView private constructor(
|
|
|
549
389
|
* so reparenting / redundant prop re-applies never reload (which would lose
|
|
550
390
|
* chart state, the whole point of pooling).
|
|
551
391
|
*/
|
|
552
|
-
fun setSource(
|
|
553
|
-
uri: String?,
|
|
554
|
-
localBundle: String?,
|
|
555
|
-
entry: String?,
|
|
556
|
-
paramsJson: String?,
|
|
557
|
-
assetHost: String?,
|
|
558
|
-
) {
|
|
392
|
+
fun setSource(uri: String?, localBundle: String?, entry: String?, paramsJson: String?) {
|
|
559
393
|
// Never load before the document-start bridge is registered — otherwise the
|
|
560
394
|
// page boots without the bridge and its first $private requests are lost. The
|
|
561
395
|
// host re-calls setSource once the bridgeScript prop arrives.
|
|
562
396
|
if (!bridgeRegistered) return
|
|
563
|
-
|
|
564
|
-
// behavior) when the app doesn't pass one. Empty string is treated as absent.
|
|
565
|
-
// Sanitize untrusted values before they reach the privileged-bridge origin
|
|
566
|
-
// ("https://$assetHost") and WebViewAssetLoader.setDomain — see sanitizeAssetHost.
|
|
567
|
-
this.assetHost = assetHost?.takeIf { it.isNotEmpty() }
|
|
568
|
-
?.let { sanitizeAssetHost(it) } ?: ASSET_HOST
|
|
569
|
-
if (localBundle != lastLocalBundle || this.assetHost != lastAssetHost) {
|
|
397
|
+
if (localBundle != lastLocalBundle) {
|
|
570
398
|
lastLocalBundle = localBundle
|
|
571
|
-
lastAssetHost = this.assetHost
|
|
572
399
|
rebuildAssetLoader(localBundle)
|
|
573
400
|
}
|
|
574
401
|
// Register the document-start bridge scoped to exactly the origin(s) this load
|
|
@@ -610,36 +437,7 @@ class PooledChartWebView private constructor(
|
|
|
610
437
|
(webView.parent as? ViewGroup)?.removeView(webView)
|
|
611
438
|
webView.destroy()
|
|
612
439
|
val n = liveCount.decrementAndGet()
|
|
613
|
-
android.util.Log.d(
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// Validate an incoming assetHost prop before it becomes the trusted bridge
|
|
618
|
-
// origin ("https://$assetHost") and WebViewAssetLoader.setDomain(assetHost). A
|
|
619
|
-
// malformed value (scheme, path, '/', '@'/userinfo, whitespace, port, query)
|
|
620
|
-
// could corrupt the privileged-origin allowlist or throw on the UI thread, so a
|
|
621
|
-
// candidate that is not a bare hostname falls back to the built-in ASSET_HOST.
|
|
622
|
-
private fun sanitizeAssetHost(candidate: String): String {
|
|
623
|
-
val invalid = {
|
|
624
|
-
android.util.Log.w(
|
|
625
|
-
TAG,
|
|
626
|
-
"Ignoring invalid assetHost '$candidate'; falling back to default $ASSET_HOST",
|
|
627
|
-
)
|
|
628
|
-
ASSET_HOST
|
|
629
|
-
}
|
|
630
|
-
// Cheap rejects first: a bare hostname has no whitespace and no '/'.
|
|
631
|
-
if (candidate.any { it.isWhitespace() } || candidate.contains('/')) return invalid()
|
|
632
|
-
return try {
|
|
633
|
-
val uri = java.net.URI("https://$candidate")
|
|
634
|
-
val bareHost =
|
|
635
|
-
uri.host == candidate &&
|
|
636
|
-
uri.path.isNullOrEmpty() &&
|
|
637
|
-
uri.userInfo == null &&
|
|
638
|
-
uri.query == null &&
|
|
639
|
-
uri.port == -1
|
|
640
|
-
if (bareHost) candidate else invalid()
|
|
641
|
-
} catch (e: Exception) {
|
|
642
|
-
invalid()
|
|
440
|
+
android.util.Log.d("ChartWebviewPool", "WebView DESTROYED key=$key liveCount=$n")
|
|
643
441
|
}
|
|
644
442
|
}
|
|
645
443
|
|
|
@@ -648,28 +446,9 @@ class PooledChartWebView private constructor(
|
|
|
648
446
|
assetLoader = null
|
|
649
447
|
return
|
|
650
448
|
}
|
|
651
|
-
// Narrow the path handler to ONLY the offline bundle subtree
|
|
652
|
-
// (/<localBundle>/, e.g. /tradingview-assets/) instead of intercepting all of
|
|
653
|
-
// "/". When the bundle is served under the real chart origin (`assetHost`),
|
|
654
|
-
// intercepting "/" would shadow every web path on that domain; with the narrow
|
|
655
|
-
// handler, unmatched paths fall through to the network — so an `assetHost`
|
|
656
|
-
// that is a real public domain keeps behaving normally off-bundle.
|
|
657
|
-
// computeTargetUrl produces https://<assetHost>/<localBundle>/<entry>, which
|
|
658
|
-
// stays inside this handler's path. (Path built as /<localBundle>/ so a
|
|
659
|
-
// localBundle that already has a trailing slash doesn't double it.)
|
|
660
|
-
val bundleDir = localBundle.trim('/')
|
|
661
|
-
val bundlePath = "/$bundleDir/"
|
|
662
|
-
// WebViewAssetLoader strips the registered prefix before calling the handler,
|
|
663
|
-
// so AssetsPathHandler alone would resolve the remainder against the assets
|
|
664
|
-
// ROOT (assets/<entry>) and lose the bundle namespace. Re-prepend <bundleDir>/
|
|
665
|
-
// so we still serve from assets/<localBundle>/<entry>. Returning null on a
|
|
666
|
-
// miss lets WebViewAssetLoader fall through to the network.
|
|
667
|
-
val assets = WebViewAssetLoader.AssetsPathHandler(webView.context)
|
|
668
|
-
val namespacedHandler =
|
|
669
|
-
WebViewAssetLoader.PathHandler { suffix -> assets.handle("$bundleDir/$suffix") }
|
|
670
449
|
assetLoader = WebViewAssetLoader.Builder()
|
|
671
|
-
.setDomain(
|
|
672
|
-
.addPathHandler(
|
|
450
|
+
.setDomain(ASSET_HOST)
|
|
451
|
+
.addPathHandler("/", WebViewAssetLoader.AssetsPathHandler(webView.context))
|
|
673
452
|
.build()
|
|
674
453
|
}
|
|
675
454
|
|
|
@@ -683,21 +462,15 @@ class PooledChartWebView private constructor(
|
|
|
683
462
|
if (localBundle.isNullOrEmpty()) return null
|
|
684
463
|
val entryPath = entry?.takeIf { it.isNotEmpty() } ?: DEFAULT_ENTRY
|
|
685
464
|
val query = buildQueryFromParamsJson(paramsJson)
|
|
686
|
-
// Normalize the bundle dir the SAME way rebuildAssetLoader does (trim '/'), so
|
|
687
|
-
// the URL has exactly single slashes between host, bundle dir and entry. A raw
|
|
688
|
-
// localBundle like "/tradingview-assets/" would otherwise yield
|
|
689
|
-
// https://host//tradingview-assets//index.html, which misses the registered
|
|
690
|
-
// handler prefix → falls through to the network → blank chart.
|
|
691
|
-
val bundleDir = localBundle.trim('/')
|
|
692
465
|
return buildString {
|
|
693
466
|
append("https://")
|
|
694
|
-
append(
|
|
467
|
+
append(ASSET_HOST)
|
|
695
468
|
append('/')
|
|
696
469
|
// Serve the offline bundle from assets/<localBundle>/ (namespaced) rather
|
|
697
470
|
// than the assets root, so it doesn't collide with other bundled assets
|
|
698
471
|
// (e.g. web-embed). The dist uses relative asset paths (PUBLIC_URL='./'),
|
|
699
472
|
// so loading the entry from a subpath resolves the rest correctly.
|
|
700
|
-
append(
|
|
473
|
+
append(localBundle)
|
|
701
474
|
append('/')
|
|
702
475
|
append(entryPath)
|
|
703
476
|
if (query.isNotEmpty()) {
|