@onekeyfe/react-native-chart-webview 3.0.57 → 3.0.58
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.
|
@@ -25,6 +25,7 @@ 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
|
|
28
29
|
import java.net.URLEncoder
|
|
29
30
|
import java.util.concurrent.atomic.AtomicInteger
|
|
30
31
|
|
|
@@ -151,21 +152,25 @@ class PooledChartWebView private constructor(
|
|
|
151
152
|
}
|
|
152
153
|
|
|
153
154
|
/** The host currently displaying this WebView; page events route here. */
|
|
154
|
-
|
|
155
|
+
// Weak (mirror of iOS): the pool entry is immortal, so a strong ref here would
|
|
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
|
|
155
159
|
var owner: HybridChartWebview?
|
|
156
|
-
get() =
|
|
160
|
+
get() = _ownerRef?.get()
|
|
157
161
|
set(value) {
|
|
158
|
-
|
|
162
|
+
_ownerRef = value?.let { WeakReference(it) }
|
|
159
163
|
}
|
|
160
164
|
// The host that warm-booted the page and can drive its symbol / receive its
|
|
161
165
|
// callbacks while there is no VISIBLE owner yet. Separate from `owner` (the
|
|
162
166
|
// YIELD path clears `owner`); callbacks fall back to this so bars-state /
|
|
163
167
|
// load-end aren't dropped during warm. Mirror of the iOS warmDriver.
|
|
164
|
-
|
|
168
|
+
// Weak for the same immortal-pool reason as `owner` above.
|
|
169
|
+
private var _warmDriverRef: WeakReference<HybridChartWebview>? = null
|
|
165
170
|
var warmDriver: HybridChartWebview?
|
|
166
|
-
get() =
|
|
171
|
+
get() = _warmDriverRef?.get()
|
|
167
172
|
set(value) {
|
|
168
|
-
|
|
173
|
+
_warmDriverRef = value?.let { WeakReference(it) }
|
|
169
174
|
}
|
|
170
175
|
|
|
171
176
|
// PERF (Android only): Android's in-process WebView/Chromium does NOT throttle
|
|
@@ -195,14 +200,14 @@ class PooledChartWebView private constructor(
|
|
|
195
200
|
return
|
|
196
201
|
}
|
|
197
202
|
paused = true
|
|
198
|
-
android.util.Log.
|
|
203
|
+
android.util.Log.d(TAG, "pool[$key] PAUSE (renderer idle)")
|
|
199
204
|
runOnUiThread { webView.onPause() }
|
|
200
205
|
}
|
|
201
206
|
|
|
202
207
|
fun resume() {
|
|
203
208
|
if (!paused) return
|
|
204
209
|
paused = false
|
|
205
|
-
android.util.Log.
|
|
210
|
+
android.util.Log.d(TAG, "pool[$key] RESUME")
|
|
206
211
|
runOnUiThread {
|
|
207
212
|
webView.onResume()
|
|
208
213
|
webView.invalidate()
|
|
@@ -233,6 +238,9 @@ class PooledChartWebView private constructor(
|
|
|
233
238
|
// How long the snapshot overlay stays up after a reparent, giving the WebView
|
|
234
239
|
// time to draw its first frame in the new container (~5 frames).
|
|
235
240
|
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
|
|
236
244
|
private val attachGeneration = AtomicInteger(0)
|
|
237
245
|
|
|
238
246
|
val webView: WebView = WebView(context).apply {
|
|
@@ -318,10 +326,18 @@ class PooledChartWebView private constructor(
|
|
|
318
326
|
// hold; removeViewInLayout() is the in-layout fallback if the child is still held.
|
|
319
327
|
private fun forceDetach(parent: ViewGroup) {
|
|
320
328
|
if (webView.parent !== parent) return
|
|
321
|
-
try {
|
|
329
|
+
try {
|
|
330
|
+
parent.endViewTransition(webView)
|
|
331
|
+
} catch (e: Throwable) {
|
|
332
|
+
android.util.Log.w(TAG, "forceDetach: endViewTransition failed", e)
|
|
333
|
+
}
|
|
322
334
|
parent.removeView(webView)
|
|
323
335
|
if (webView.parent === parent) {
|
|
324
|
-
try {
|
|
336
|
+
try {
|
|
337
|
+
parent.removeViewInLayout(webView)
|
|
338
|
+
} catch (e: Throwable) {
|
|
339
|
+
android.util.Log.w(TAG, "forceDetach: removeViewInLayout failed", e)
|
|
340
|
+
}
|
|
325
341
|
parent.requestLayout()
|
|
326
342
|
}
|
|
327
343
|
}
|
|
@@ -347,14 +363,25 @@ class PooledChartWebView private constructor(
|
|
|
347
363
|
// which is attached to the window so its queue keeps draining, instead of the
|
|
348
364
|
// detached webView/old parent whose post() runnables may never run.
|
|
349
365
|
if (retriesLeft > 0) {
|
|
350
|
-
container.post
|
|
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({
|
|
351
370
|
attachToContainer(container, generation, retriesLeft = retriesLeft - 1)
|
|
352
|
-
}
|
|
371
|
+
}, attachRetryDelayMs)
|
|
353
372
|
} else {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
)
|
|
373
|
+
// Last resort before giving up: the old parent never released the WebView
|
|
374
|
+
// through the normal path. Force-detach it and try the attach once more so
|
|
375
|
+
// we don't leave the WebView unparented (the blank-chart symptom).
|
|
376
|
+
(currentParent as? ViewGroup)?.let { forceDetach(it) }
|
|
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
|
+
}
|
|
358
385
|
}
|
|
359
386
|
return
|
|
360
387
|
}
|
|
@@ -535,7 +562,10 @@ class PooledChartWebView private constructor(
|
|
|
535
562
|
if (!bridgeRegistered) return
|
|
536
563
|
// Per-instance asset host: fall back to the built-in appassets host (old
|
|
537
564
|
// behavior) when the app doesn't pass one. Empty string is treated as absent.
|
|
538
|
-
|
|
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
|
|
539
569
|
if (localBundle != lastLocalBundle || this.assetHost != lastAssetHost) {
|
|
540
570
|
lastLocalBundle = localBundle
|
|
541
571
|
lastAssetHost = this.assetHost
|
|
@@ -584,6 +614,35 @@ class PooledChartWebView private constructor(
|
|
|
584
614
|
}
|
|
585
615
|
}
|
|
586
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()
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
587
646
|
private fun rebuildAssetLoader(localBundle: String?) {
|
|
588
647
|
if (localBundle.isNullOrEmpty()) {
|
|
589
648
|
assetLoader = null
|
|
@@ -624,6 +683,12 @@ class PooledChartWebView private constructor(
|
|
|
624
683
|
if (localBundle.isNullOrEmpty()) return null
|
|
625
684
|
val entryPath = entry?.takeIf { it.isNotEmpty() } ?: DEFAULT_ENTRY
|
|
626
685
|
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('/')
|
|
627
692
|
return buildString {
|
|
628
693
|
append("https://")
|
|
629
694
|
append(assetHost)
|
|
@@ -632,7 +697,7 @@ class PooledChartWebView private constructor(
|
|
|
632
697
|
// than the assets root, so it doesn't collide with other bundled assets
|
|
633
698
|
// (e.g. web-embed). The dist uses relative asset paths (PUBLIC_URL='./'),
|
|
634
699
|
// so loading the entry from a subpath resolves the rest correctly.
|
|
635
|
-
append(
|
|
700
|
+
append(bundleDir)
|
|
636
701
|
append('/')
|
|
637
702
|
append(entryPath)
|
|
638
703
|
if (query.isNotEmpty()) {
|