Skip to content

Commit f34ffae

Browse files
NickGerlemanfacebook-github-bot
authored andcommitted
Fix Crash When Measuring Text in Stopped Surface (facebook#55995)
Summary: `FabricUIManager.prepareTextLayout()` crashes when measuring text for a surface that has already been stopped. It looks up the surface's `ThemedReactContext` via `surfaceMountingManager.getContext()`, which returns `null` after `stopSurface()` completes. The only thing the `Context` is used for downstream is `context.assets` (the `AssetManager`) for custom font resolution. Since `AssetManager` from the application context is equivalent for this purpose (fonts are app-global, not activity-specific), we refactor the API to pass `AssetManager` directly from `mReactApplicationContext`, eliminating the surface lookup entirely. This also makes the API clearer: the prepared layout does not depend on activity context (e.g. for light/dark theme). Changelog: [Android][Fixed] - Fix crash when measuring text in a stopped surface Reviewed By: javache Differential Revision: D95763252
1 parent 3b8455c commit f34ffae

File tree

6 files changed

+43
-70
lines changed

6 files changed

+43
-70
lines changed

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import androidx.annotation.AnyThread;
2828
import androidx.annotation.Nullable;
2929
import androidx.annotation.UiThread;
30-
import androidx.core.util.Preconditions;
3130
import androidx.core.view.ViewCompat.FocusDirection;
3231
import com.facebook.common.logging.FLog;
3332
import com.facebook.infer.annotation.Assertions;
@@ -537,7 +536,7 @@ private NativeArray measureLines(
537536

538537
return (NativeArray)
539538
TextLayoutManager.measureLines(
540-
mReactApplicationContext,
539+
mReactApplicationContext.getAssets(),
541540
attributedString,
542541
paragraphAttributes,
543542
PixelUtil.toPixelFromDIP(width),
@@ -611,7 +610,6 @@ public long measure(
611610
@ThreadConfined(ANY)
612611
@UnstableReactNativeAPI
613612
public long measureText(
614-
int surfaceId,
615613
ReadableMapBuffer attributedString,
616614
ReadableMapBuffer paragraphAttributes,
617615
float minWidth,
@@ -620,24 +618,10 @@ public long measureText(
620618
float maxHeight,
621619
@Nullable float[] attachmentsPositions) {
622620

623-
ReactContext context;
624-
if (surfaceId > 0) {
625-
SurfaceMountingManager surfaceMountingManager =
626-
mMountingManager.getSurfaceManagerEnforced(surfaceId, "measureText");
627-
if (surfaceMountingManager.isStopped()) {
628-
return 0;
629-
}
630-
context = surfaceMountingManager.getContext();
631-
Assertions.assertNotNull(
632-
context, "Context in SurfaceMountingManager is null. surfaceId: " + surfaceId);
633-
} else {
634-
context = mReactApplicationContext;
635-
}
636-
637621
ViewManager textViewManager = mViewManagerRegistry.get(ReactTextViewManager.REACT_CLASS);
638622

639623
return TextLayoutManager.measureText(
640-
context,
624+
mReactApplicationContext.getAssets(),
641625
attributedString,
642626
paragraphAttributes,
643627
getYogaSize(minWidth, maxWidth),
@@ -654,19 +638,16 @@ public long measureText(
654638
@ThreadConfined(ANY)
655639
@UnstableReactNativeAPI
656640
public PreparedLayout prepareTextLayout(
657-
int surfaceId,
658641
ReadableMapBuffer attributedString,
659642
ReadableMapBuffer paragraphAttributes,
660643
float minWidth,
661644
float maxWidth,
662645
float minHeight,
663646
float maxHeight) {
664-
SurfaceMountingManager surfaceMountingManager =
665-
mMountingManager.getSurfaceManagerEnforced(surfaceId, "prepareTextLayout");
666647
ViewManager textViewManager = mViewManagerRegistry.get(ReactTextViewManager.REACT_CLASS);
667648

668649
return TextLayoutManager.createPreparedLayout(
669-
Preconditions.checkNotNull(surfaceMountingManager.getContext()),
650+
mReactApplicationContext.getAssets(),
670651
attributedString,
671652
paragraphAttributes,
672653
getYogaSize(minWidth, maxWidth),

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/FontMetricsUtil.kt

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77

88
package com.facebook.react.views.text
99

10-
import android.content.Context
1110
import android.graphics.Rect
1211
import android.text.Layout
1312
import android.text.TextPaint
1413
import com.facebook.react.bridge.Arguments
1514
import com.facebook.react.bridge.WritableArray
1615
import com.facebook.react.bridge.buildReadableMap
16+
import com.facebook.react.uimanager.PixelUtil.pxToDp
1717

1818
internal object FontMetricsUtil {
1919

@@ -22,8 +22,7 @@ internal object FontMetricsUtil {
2222
private const val AMPLIFICATION_FACTOR = 100f
2323

2424
@JvmStatic
25-
fun getFontMetrics(text: CharSequence, layout: Layout, context: Context): WritableArray {
26-
val dm = context.resources.displayMetrics
25+
fun getFontMetrics(text: CharSequence, layout: Layout): WritableArray {
2726
val lines = Arguments.createArray()
2827

2928
// To calculate xHeight and capHeight we have to render an "x" and "T" and manually measure
@@ -39,7 +38,7 @@ internal object FontMetricsUtil {
3938
CAP_HEIGHT_MEASUREMENT_TEXT.length,
4039
capHeightBounds,
4140
)
42-
val capHeight = capHeightBounds.height() / AMPLIFICATION_FACTOR / dm.density
41+
val capHeight = (capHeightBounds.height() / AMPLIFICATION_FACTOR).pxToDp()
4342

4443
val xHeightBounds = Rect()
4544
paintCopy.getTextBounds(
@@ -48,21 +47,21 @@ internal object FontMetricsUtil {
4847
X_HEIGHT_MEASUREMENT_TEXT.length,
4948
xHeightBounds,
5049
)
51-
val xHeight = xHeightBounds.height() / AMPLIFICATION_FACTOR / dm.density
50+
val xHeight = (xHeightBounds.height() / AMPLIFICATION_FACTOR).pxToDp()
5251

5352
for (i in 0 until layout.lineCount) {
5453
val endsWithNewLine = text.isNotEmpty() && text[layout.getLineEnd(i) - 1] == '\n'
5554
val lineWidth = if (endsWithNewLine) layout.getLineMax(i) else layout.getLineWidth(i)
5655
val bounds = Rect()
5756
layout.getLineBounds(i, bounds)
5857
val line = buildReadableMap {
59-
put("x", (layout.getLineLeft(i) / dm.density).toDouble())
60-
put("y", (bounds.top / dm.density).toDouble())
61-
put("width", (lineWidth / dm.density).toDouble())
62-
put("height", (bounds.height() / dm.density).toDouble())
63-
put("descender", (layout.getLineDescent(i) / dm.density).toDouble())
64-
put("ascender", (-layout.getLineAscent(i) / dm.density).toDouble())
65-
put("baseline", (layout.getLineBaseline(i) / dm.density).toDouble())
58+
put("x", layout.getLineLeft(i).pxToDp().toDouble())
59+
put("y", bounds.top.pxToDp().toDouble())
60+
put("width", lineWidth.pxToDp().toDouble())
61+
put("height", bounds.height().pxToDp().toDouble())
62+
put("descender", layout.getLineDescent(i).pxToDp().toDouble())
63+
put("ascender", (-layout.getLineAscent(i)).pxToDp().toDouble())
64+
put("baseline", layout.getLineBaseline(i).pxToDp().toDouble())
6665
put("capHeight", capHeight.toDouble())
6766
put("xHeight", xHeight.toDouble())
6867
put("text", text.subSequence(layout.getLineStart(i), layout.getLineEnd(i)).toString())

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ public constructor(
157157
state.getMapBuffer(TX_STATE_KEY_PARAGRAPH_ATTRIBUTES.toInt())
158158
val spanned: Spannable =
159159
TextLayoutManager.getOrCreateSpannableForText(
160-
view.context,
160+
view.context.assets,
161161
attributedString,
162162
reactTextViewManagerCallback,
163163
)

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
package com.facebook.react.views.text
99

10-
import android.content.Context
10+
import android.content.res.AssetManager
1111
import android.graphics.Color
1212
import android.graphics.Typeface
1313
import android.os.Build
@@ -217,7 +217,7 @@ internal object TextLayoutManager {
217217
}
218218

219219
private fun buildSpannableFromFragments(
220-
context: Context,
220+
assets: AssetManager,
221221
fragments: MapBuffer,
222222
sb: SpannableStringBuilder,
223223
ops: MutableList<SetSpanOperation>,
@@ -296,7 +296,7 @@ internal object TextLayoutManager {
296296
textAttributes.fontWeight,
297297
textAttributes.fontFeatureSettings,
298298
textAttributes.fontFamily,
299-
context.assets,
299+
assets,
300300
),
301301
)
302302
)
@@ -352,7 +352,7 @@ internal object TextLayoutManager {
352352
)
353353

354354
private fun buildSpannableFromFragmentsOptimized(
355-
context: Context,
355+
assets: AssetManager,
356356
fragments: MapBuffer,
357357
outputReactTags: IntArray?,
358358
): Spannable {
@@ -472,7 +472,7 @@ internal object TextLayoutManager {
472472
fragment.props.fontWeight,
473473
fragment.props.fontFeatureSettings,
474474
fragment.props.fontFamily,
475-
context.assets,
475+
assets,
476476
),
477477
start,
478478
end,
@@ -528,7 +528,7 @@ internal object TextLayoutManager {
528528
}
529529

530530
fun getOrCreateSpannableForText(
531-
context: Context,
531+
assets: AssetManager,
532532
attributedString: MapBuffer,
533533
reactTextViewManagerCallback: ReactTextViewManagerCallback?,
534534
): Spannable {
@@ -539,7 +539,7 @@ internal object TextLayoutManager {
539539
} else {
540540
text =
541541
createSpannableFromAttributedString(
542-
context,
542+
assets,
543543
attributedString.getMapBuffer(AS_KEY_FRAGMENTS),
544544
reactTextViewManagerCallback,
545545
null,
@@ -550,13 +550,13 @@ internal object TextLayoutManager {
550550
}
551551

552552
private fun createSpannableFromAttributedString(
553-
context: Context,
553+
assets: AssetManager,
554554
fragments: MapBuffer,
555555
reactTextViewManagerCallback: ReactTextViewManagerCallback?,
556556
outputReactTags: IntArray?,
557557
): Spannable {
558558
if (ReactNativeFeatureFlags.enableAndroidTextMeasurementOptimizations()) {
559-
val spannable = buildSpannableFromFragmentsOptimized(context, fragments, outputReactTags)
559+
val spannable = buildSpannableFromFragmentsOptimized(assets, fragments, outputReactTags)
560560

561561
reactTextViewManagerCallback?.onPostProcessSpannable(spannable)
562562
return spannable
@@ -568,7 +568,7 @@ internal object TextLayoutManager {
568568
// a new spannable will be wiped out
569569
val ops: MutableList<SetSpanOperation> = ArrayList()
570570

571-
buildSpannableFromFragments(context, fragments, sb, ops, outputReactTags)
571+
buildSpannableFromFragments(assets, fragments, sb, ops, outputReactTags)
572572

573573
// TODO T31905686: add support for inline Images
574574
// While setting the Spans on the final text, we also check whether any of them are images.
@@ -740,7 +740,7 @@ internal object TextLayoutManager {
740740
private fun updateTextPaint(
741741
paint: TextPaint,
742742
baseTextAttributes: TextAttributeProps,
743-
context: Context,
743+
assets: AssetManager,
744744
) {
745745
if (baseTextAttributes.fontSize != ReactConstants.UNSET) {
746746
paint.textSize = baseTextAttributes.fontSize.toFloat()
@@ -757,7 +757,7 @@ internal object TextLayoutManager {
757757
baseTextAttributes.fontStyle,
758758
baseTextAttributes.fontWeight,
759759
baseTextAttributes.fontFamily,
760-
context.assets,
760+
assets,
761761
)
762762
paint.setTypeface(typeface)
763763

@@ -779,28 +779,28 @@ internal object TextLayoutManager {
779779
*/
780780
private fun scratchPaintWithAttributes(
781781
baseTextAttributes: TextAttributeProps,
782-
context: Context,
782+
assets: AssetManager,
783783
): TextPaint {
784784
val paint = checkNotNull(textPaintInstance.get())
785785
paint.setTypeface(null)
786786
paint.textSize = 12f
787787
paint.isFakeBoldText = false
788788
paint.textSkewX = 0f
789-
updateTextPaint(paint, baseTextAttributes, context)
789+
updateTextPaint(paint, baseTextAttributes, assets)
790790
return paint
791791
}
792792

793793
private fun newPaintWithAttributes(
794794
baseTextAttributes: TextAttributeProps,
795-
context: Context,
795+
assets: AssetManager,
796796
): TextPaint {
797797
val paint = TextPaint(TextPaint.ANTI_ALIAS_FLAG)
798-
updateTextPaint(paint, baseTextAttributes, context)
798+
updateTextPaint(paint, baseTextAttributes, assets)
799799
return paint
800800
}
801801

802802
private fun createLayoutForMeasurement(
803-
context: Context,
803+
assets: AssetManager,
804804
attributedString: MapBuffer,
805805
paragraphAttributes: MapBuffer,
806806
width: Float,
@@ -809,15 +809,15 @@ internal object TextLayoutManager {
809809
heightYogaMeasureMode: YogaMeasureMode,
810810
reactTextViewManagerCallback: ReactTextViewManagerCallback?,
811811
): Layout {
812-
val text = getOrCreateSpannableForText(context, attributedString, reactTextViewManagerCallback)
812+
val text = getOrCreateSpannableForText(assets, attributedString, reactTextViewManagerCallback)
813813

814814
val paint: TextPaint
815815
if (attributedString.contains(AS_KEY_CACHE_ID)) {
816816
paint = text.getSpans(0, 0, ReactTextPaintHolderSpan::class.java)[0].textPaint
817817
} else {
818818
val baseTextAttributes =
819819
TextAttributeProps.fromMapBuffer(attributedString.getMapBuffer(AS_KEY_BASE_ATTRIBUTES))
820-
paint = scratchPaintWithAttributes(baseTextAttributes, context)
820+
paint = scratchPaintWithAttributes(baseTextAttributes, assets)
821821
}
822822

823823
return createLayout(
@@ -922,7 +922,7 @@ internal object TextLayoutManager {
922922

923923
@JvmStatic
924924
fun createPreparedLayout(
925-
context: Context,
925+
assets: AssetManager,
926926
attributedString: ReadableMapBuffer,
927927
paragraphAttributes: ReadableMapBuffer,
928928
width: Float,
@@ -935,7 +935,7 @@ internal object TextLayoutManager {
935935
val reactTags = IntArray(fragments.count)
936936
val text =
937937
createSpannableFromAttributedString(
938-
context,
938+
assets,
939939
fragments,
940940
reactTextViewManagerCallback,
941941
reactTags,
@@ -945,7 +945,7 @@ internal object TextLayoutManager {
945945
val result =
946946
createLayout(
947947
text,
948-
newPaintWithAttributes(baseTextAttributes, context),
948+
newPaintWithAttributes(baseTextAttributes, assets),
949949
attributedString,
950950
paragraphAttributes,
951951
width,
@@ -1085,7 +1085,7 @@ internal object TextLayoutManager {
10851085

10861086
@JvmStatic
10871087
fun measureText(
1088-
context: Context,
1088+
assets: AssetManager,
10891089
attributedString: MapBuffer,
10901090
paragraphAttributes: MapBuffer,
10911091
width: Float,
@@ -1098,7 +1098,7 @@ internal object TextLayoutManager {
10981098
// TODO(5578671): Handle text direction (see View#getTextDirectionHeuristic)
10991099
val layout =
11001100
createLayoutForMeasurement(
1101-
context,
1101+
assets,
11021102
attributedString,
11031103
paragraphAttributes,
11041104
width,
@@ -1355,7 +1355,7 @@ internal object TextLayoutManager {
13551355

13561356
@JvmStatic
13571357
fun measureLines(
1358-
context: Context,
1358+
assetManager: AssetManager,
13591359
attributedString: MapBuffer,
13601360
paragraphAttributes: MapBuffer,
13611361
width: Float,
@@ -1364,7 +1364,7 @@ internal object TextLayoutManager {
13641364
): WritableArray {
13651365
val layout =
13661366
createLayoutForMeasurement(
1367-
context,
1367+
assetManager,
13681368
attributedString,
13691369
paragraphAttributes,
13701370
width,
@@ -1373,7 +1373,7 @@ internal object TextLayoutManager {
13731373
YogaMeasureMode.EXACTLY,
13741374
reactTextViewManagerCallback,
13751375
)
1376-
return FontMetricsUtil.getFontMetrics(layout.text, layout, context)
1376+
return FontMetricsUtil.getFontMetrics(layout.text, layout)
13771377
}
13781378

13791379
private fun isBoring(text: Spannable, paint: TextPaint): BoringLayout.Metrics? =

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1015,7 +1015,7 @@ public open class ReactTextInputManager public constructor() :
10151015

10161016
val spanned =
10171017
TextLayoutManager.getOrCreateSpannableForText(
1018-
view.context,
1018+
view.context.assets,
10191019
attributedString,
10201020
reactTextViewManagerCallback,
10211021
)

0 commit comments

Comments
 (0)