Skip to content

Commit d8789c6

Browse files
YevheniiKotyrlometa-codesync[bot]
authored andcommitted
Stop non-finite elevation and scale values from crashing Android 9 and later (#59314)
Summary: A view whose `elevation` is `NaN` or infinite ends the app on Android 9 and later, and so do a `DrawerLayoutAndroid`'s `elevation` and the deprecated `scaleX` and `scaleY` props. From API 28, `View.setElevation`, `setScaleX` and `setScaleY` pass their value through `sanitizeFloatPropertyValue` ([`View.java`, line 20100](https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-16.0.0_r1/core/java/android/view/View.java#20100)), which throws `IllegalArgumentException: Cannot set 'elevation' to Float.NaN` for an app targeting API 28 or later ([line 5903](https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-16.0.0_r1/core/java/android/view/View.java#5903)). `BaseViewManager.setTransformProperty` already reads every value it hands those setters through its own `sanitizeFloatPropertyValue`, but `BaseViewManager.setElevation`, the deprecated scale setters and `ReactDrawerLayoutManager.setElevation`, which hands `DrawerLayout.setDrawerElevation` its value without `super`, do not. This moves the helper to `com.facebook.react.uimanager.internal.FloatPropertySanitizer`, which the drawer can reach and the public API leaves out, and reads those values through it — the elevations after the dp-to-pixel conversion, so an infinite elevation isn't multiplied back to infinity on a density above 1, and a finite one that overflows in pixels is caught too. A `NaN` takes 0 and ±∞ ±`Float.MAX_VALUE`: the values the framework itself gives an app that does not opt into the throw. A finite value costs the helper's `NaN` test and its two bounds, in setters that run when the prop changes. #59229 makes a transform holding a non-finite value ignored, as the web does. ## Changelog: [ANDROID] [FIXED] - A `NaN` or infinite `elevation`, or deprecated `scaleX` or `scaleY`, no longer crashes the app on Android 9 and later Pull Request resolved: #59314 Test Plan: - `BaseViewManagerTest`: an elevation of `NaN`, `Infinity`, `-Infinity` and `1e39` through the props map, a `NaN` after a finite one, a finite elevation that overflows only in pixels at density 3, and `NaN` and infinite `scaleX` and `scaleY`. `ReactDrawerLayoutManagerTest` (new): a drawer at a `NaN` and an infinite elevation, and at a `Float.MAX_VALUE` elevation at density 3. On `main` the 11 new cases fail on `View`'s own refusal (`Cannot set 'elevation' to Float.NaN`, `Cannot set 'scaleX' to Float.NaN`, …); with this change they pass, and so does the whole ReactAndroid JVM suite (776 tests). - google-java-format and ktfmt pass. - To reproduce, on Android 9 or later: ```jsx <View style={{width: 48, height: 48, backgroundColor: 'white', elevation: NaN}} /> ``` On `main` the app ends with `Cannot set 'elevation' to Float.NaN`, as it does for a `DrawerLayoutAndroid` at `elevation: NaN`, and with `Cannot set 'scaleX' to Float.NaN` for a `View` given `scaleX={NaN}`; with this change each draws. I ran RNTester release builds of `main` and of this change on an Android 14 phone with a root component rendering every case; a screenshot and the scene are in the [evidence comment](#59314 (comment)). Reviewed By: Abbondanzo Differential Revision: D126883976 Pulled By: javache fbshipit-source-id: e87a85ab56a50c5bbb94891888ced851d9a453b9
1 parent 9de6471 commit d8789c6

5 files changed

Lines changed: 171 additions & 27 deletions

File tree

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java‎

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
package com.facebook.react.uimanager;
99

10+
import static com.facebook.react.uimanager.internal.FloatPropertySanitizer.sanitizeFloatPropertyValue;
11+
1012
import android.graphics.Color;
1113
import android.graphics.Matrix;
1214
import android.graphics.Paint;
@@ -298,7 +300,7 @@ public void setOpacity(@NonNull T view, float opacity) {
298300

299301
@ReactProp(name = ViewProps.ELEVATION)
300302
public void setElevation(@NonNull T view, float elevation) {
301-
float px = PixelUtil.toPixelFromDIP(elevation);
303+
float px = sanitizeFloatPropertyValue(PixelUtil.toPixelFromDIP(elevation));
302304
boolean wasElevated = view.getElevation() > 0f;
303305
ViewCompat.setElevation(view, px);
304306
if (wasElevated != (px > 0f)) {
@@ -553,13 +555,13 @@ public void setRotation(@NonNull T view, float rotation) {
553555
@Deprecated
554556
@ReactProp(name = ViewProps.SCALE_X, defaultFloat = 1f)
555557
public void setScaleX(@NonNull T view, float scaleX) {
556-
view.setScaleX(scaleX);
558+
view.setScaleX(sanitizeFloatPropertyValue(scaleX));
557559
}
558560

559561
@Deprecated
560562
@ReactProp(name = ViewProps.SCALE_Y, defaultFloat = 1f)
561563
public void setScaleY(@NonNull T view, float scaleY) {
562-
view.setScaleY(scaleY);
564+
view.setScaleY(sanitizeFloatPropertyValue(scaleY));
563565
}
564566

565567
@Deprecated
@@ -719,29 +721,6 @@ private static <T extends View> void clearSkewAnimationMatrixIfActive(@NonNull T
719721
view.setTag(R.id.skew_animation_matrix, null);
720722
}
721723

722-
/**
723-
* Prior to Android P things like setScaleX() allowed passing float values that were bogus such as
724-
* Float.NaN. If the app is targeting Android P or later then passing these values will result in
725-
* an exception being thrown. Since JS might still send Float.NaN, we want to keep the code
726-
* backward compatible and continue using the fallback value if an invalid float is passed.
727-
*/
728-
private static float sanitizeFloatPropertyValue(float value) {
729-
if (value >= -Float.MAX_VALUE && value <= Float.MAX_VALUE) {
730-
return value;
731-
}
732-
if (value < -Float.MAX_VALUE || value == Float.NEGATIVE_INFINITY) {
733-
return -Float.MAX_VALUE;
734-
}
735-
if (value > Float.MAX_VALUE || value == Float.POSITIVE_INFINITY) {
736-
return Float.MAX_VALUE;
737-
}
738-
if (Float.isNaN(value)) {
739-
return 0;
740-
}
741-
// Shouldn't be possible to reach this point.
742-
throw new IllegalStateException("Invalid float property value: " + value);
743-
}
744-
745724
protected void updateViewAccessibility(@NonNull T view) {
746725
ReactAccessibilityDelegate.setDelegate(
747726
view, view.isFocusable(), view.getImportantForAccessibility());
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.uimanager.internal
9+
10+
/**
11+
* Prior to Android P things like setScaleX() allowed passing float values that were bogus such as
12+
* Float.NaN. If the app is targeting Android P or later then passing these values will result in an
13+
* exception being thrown. Since JS might still send Float.NaN, we want to keep the code backward
14+
* compatible and continue using the fallback value if an invalid float is passed.
15+
*/
16+
public object FloatPropertySanitizer {
17+
18+
@JvmStatic
19+
public fun sanitizeFloatPropertyValue(value: Float): Float =
20+
if (value.isNaN()) 0f else value.coerceIn(-Float.MAX_VALUE, Float.MAX_VALUE)
21+
}

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/drawer/ReactDrawerLayoutManager.kt‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import com.facebook.react.uimanager.ViewGroupManager
2525
import com.facebook.react.uimanager.ViewManagerDelegate
2626
import com.facebook.react.uimanager.annotations.ReactProp
2727
import com.facebook.react.uimanager.events.EventDispatcher
28+
import com.facebook.react.uimanager.internal.FloatPropertySanitizer
2829
import com.facebook.react.viewmanagers.AndroidDrawerLayoutManagerDelegate
2930
import com.facebook.react.viewmanagers.AndroidDrawerLayoutManagerInterface
3031
import com.facebook.react.views.drawer.events.DrawerClosedEvent
@@ -142,7 +143,7 @@ public class ReactDrawerLayoutManager :
142143
public override fun setStatusBarBackgroundColor(view: ReactDrawerLayout, value: Int?): Unit = Unit
143144

144145
public override fun setElevation(view: ReactDrawerLayout, elevation: Float) {
145-
view.drawerElevation = elevation.dpToPx()
146+
view.drawerElevation = FloatPropertySanitizer.sanitizeFloatPropertyValue(elevation.dpToPx())
146147
}
147148

148149
public override fun needsCustomLayoutForChildren(): Boolean {

‎packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/BaseViewManagerTest.kt‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
package com.facebook.react.uimanager
1111

12+
import android.util.DisplayMetrics
1213
import android.view.View
1314
import android.view.View.OnFocusChangeListener
1415
import android.view.accessibility.AccessibilityNodeInfo
@@ -321,6 +322,78 @@ class BaseViewManagerTest {
321322
Assertions.assertThat(recycled.getTag(R.id.original_focusability)).isNull()
322323
}
323324

325+
@Test
326+
fun testNaNElevationLeavesTheViewUnelevated() {
327+
val unelevated = createView(JavaOnlyMap.of(ViewProps.ELEVATION, Double.NaN))
328+
329+
Assertions.assertThat(unelevated.elevation).isEqualTo(0f)
330+
}
331+
332+
@Test
333+
fun testInfiniteElevationTakesTheLargestFiniteElevation() {
334+
val raised = createView(JavaOnlyMap.of(ViewProps.ELEVATION, Double.POSITIVE_INFINITY))
335+
336+
Assertions.assertThat(raised.elevation).isEqualTo(Float.MAX_VALUE)
337+
}
338+
339+
@Test
340+
fun testNegativeInfiniteElevationTakesTheLowestFiniteElevation() {
341+
val lowered = createView(JavaOnlyMap.of(ViewProps.ELEVATION, Double.NEGATIVE_INFINITY))
342+
343+
Assertions.assertThat(lowered.elevation).isEqualTo(-Float.MAX_VALUE)
344+
}
345+
346+
@Test
347+
fun testElevationPastTheLargestFloatTakesTheLargestFiniteElevation() {
348+
val raised = createView(JavaOnlyMap.of(ViewProps.ELEVATION, 1e39))
349+
350+
Assertions.assertThat(raised.elevation).isEqualTo(Float.MAX_VALUE)
351+
}
352+
353+
@Test
354+
fun testElevationPastTheLargestFloatInPixelsTakesTheLargestFiniteElevation() {
355+
DisplayMetricsHolder.setScreenDisplayMetrics(DisplayMetrics().apply { density = 3f })
356+
357+
viewManager.setElevation(view, Float.MAX_VALUE)
358+
359+
Assertions.assertThat(view.elevation).isEqualTo(Float.MAX_VALUE)
360+
}
361+
362+
@Test
363+
fun testNaNScaleTakesTheFrameworksFallback() {
364+
val scaled =
365+
createView(JavaOnlyMap.of(ViewProps.SCALE_X, Double.NaN, ViewProps.SCALE_Y, Double.NaN))
366+
367+
Assertions.assertThat(scaled.scaleX).isEqualTo(0f)
368+
Assertions.assertThat(scaled.scaleY).isEqualTo(0f)
369+
}
370+
371+
@Test
372+
fun testInfiniteScaleTakesTheLargestFiniteScale() {
373+
val scaled =
374+
createView(
375+
JavaOnlyMap.of(
376+
ViewProps.SCALE_X,
377+
Double.POSITIVE_INFINITY,
378+
ViewProps.SCALE_Y,
379+
Double.POSITIVE_INFINITY,
380+
)
381+
)
382+
383+
Assertions.assertThat(scaled.scaleX).isEqualTo(Float.MAX_VALUE)
384+
Assertions.assertThat(scaled.scaleY).isEqualTo(Float.MAX_VALUE)
385+
}
386+
387+
@Test
388+
fun testNaNElevationAfterAFiniteOneLeavesTheViewUnelevated() {
389+
viewManager.setElevation(view, 4f)
390+
Assertions.assertThat(view.elevation).isEqualTo(PixelUtil.toPixelFromDIP(4f))
391+
392+
viewManager.setElevation(view, Float.NaN)
393+
394+
Assertions.assertThat(view.elevation).isEqualTo(0f)
395+
}
396+
324397
private fun recycleView(
325398
previousProps: JavaOnlyMap,
326399
beforeDrop: (ReactViewGroup) -> Unit = {},
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
@file:Suppress("DEPRECATION")
9+
10+
package com.facebook.react.views.drawer
11+
12+
import android.util.DisplayMetrics
13+
import android.view.Gravity
14+
import android.view.View
15+
import android.view.ViewGroup
16+
import androidx.drawerlayout.widget.DrawerLayout
17+
import com.facebook.react.bridge.BridgeReactContext
18+
import com.facebook.react.uimanager.DisplayMetricsHolder
19+
import com.facebook.react.uimanager.ThemedReactContext
20+
import org.assertj.core.api.Assertions.assertThat
21+
import org.junit.After
22+
import org.junit.Before
23+
import org.junit.Test
24+
import org.junit.runner.RunWith
25+
import org.robolectric.RobolectricTestRunner
26+
import org.robolectric.RuntimeEnvironment
27+
28+
@RunWith(RobolectricTestRunner::class)
29+
class ReactDrawerLayoutManagerTest {
30+
private lateinit var drawerLayout: ReactDrawerLayout
31+
32+
@Before
33+
fun setUp() {
34+
DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(RuntimeEnvironment.getApplication())
35+
val context = BridgeReactContext(RuntimeEnvironment.getApplication())
36+
drawerLayout = ReactDrawerLayout(ThemedReactContext(context, context, null, -1))
37+
drawerLayout.addView(
38+
View(drawerLayout.context),
39+
DrawerLayout.LayoutParams(100, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.START),
40+
)
41+
}
42+
43+
@After
44+
fun tearDown() {
45+
DisplayMetricsHolder.setScreenDisplayMetrics(null)
46+
}
47+
48+
@Test
49+
fun testNaNElevationLeavesTheDrawerUnelevated() {
50+
ReactDrawerLayoutManager().setElevation(drawerLayout, Float.NaN)
51+
52+
assertThat(drawerLayout.drawerElevation).isEqualTo(0f)
53+
}
54+
55+
@Test
56+
fun testInfiniteElevationTakesTheLargestFiniteElevation() {
57+
ReactDrawerLayoutManager().setElevation(drawerLayout, Float.POSITIVE_INFINITY)
58+
59+
assertThat(drawerLayout.drawerElevation).isEqualTo(Float.MAX_VALUE)
60+
}
61+
62+
@Test
63+
fun testElevationPastTheLargestFloatInPixelsTakesTheLargestFiniteElevation() {
64+
DisplayMetricsHolder.setScreenDisplayMetrics(DisplayMetrics().apply { density = 3f })
65+
66+
ReactDrawerLayoutManager().setElevation(drawerLayout, Float.MAX_VALUE)
67+
68+
assertThat(drawerLayout.drawerElevation).isEqualTo(Float.MAX_VALUE)
69+
}
70+
}

0 commit comments

Comments
 (0)