diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/deviceinfo/DeviceInfoModule.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/deviceinfo/DeviceInfoModule.kt index 6919dd78fded..970d6230082c 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/deviceinfo/DeviceInfoModule.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/deviceinfo/DeviceInfoModule.kt @@ -8,6 +8,7 @@ package com.facebook.react.modules.deviceinfo import android.util.DisplayMetrics +import androidx.annotation.VisibleForTesting import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.window.layout.WindowMetricsCalculator @@ -36,7 +37,8 @@ internal class DeviceInfoModule(reactContext: ReactApplicationContext) : reactContext.addLifecycleEventListener(this) } - private fun getWindowDisplayMetrics(): DisplayMetrics { + @VisibleForTesting + internal fun getWindowDisplayMetrics(): DisplayMetrics { val windowDisplayMetrics = DisplayMetrics() windowDisplayMetrics.setTo(reactApplicationContext.resources.displayMetrics) @@ -64,7 +66,8 @@ internal class DeviceInfoModule(reactContext: ReactApplicationContext) : return windowDisplayMetrics } - fun getDisplayMetricsWritableMap(): WritableMap = + @VisibleForTesting + internal fun getDisplayMetricsWritableMap(): WritableMap = WritableNativeMap().apply { putMap( "windowPhysicalPixels", diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/WindowUtil.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/WindowUtil.kt index 1fadc7410290..400860cc17ab 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/WindowUtil.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/WindowUtil.kt @@ -14,6 +14,7 @@ import android.view.View import android.view.Window import android.view.WindowInsetsController import android.view.WindowManager +import androidx.annotation.VisibleForTesting import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat @@ -35,7 +36,7 @@ internal val DarkNavigationBarColor = Color.argb(0x80, 0x1b, 0x1b, 0x1b) * as enabled elsewhere in the application. */ public var isEdgeToEdgeFeatureFlagOn: Boolean = false - private set + @VisibleForTesting internal set public fun setEdgeToEdgeFeatureFlagOn() { isEdgeToEdgeFeatureFlagOn = true diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/deviceinfo/DeviceInfoModuleTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/deviceinfo/DeviceInfoModuleTest.kt index 79d7f7056ece..5182b3ae10f5 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/deviceinfo/DeviceInfoModuleTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/deviceinfo/DeviceInfoModuleTest.kt @@ -9,7 +9,17 @@ package com.facebook.react.modules.deviceinfo +import android.app.Activity +import android.graphics.Rect import android.util.DisplayMetrics +import android.view.View +import android.view.Window +import androidx.core.graphics.Insets +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.window.layout.WindowMetrics +import androidx.window.layout.WindowMetricsCalculator +import androidx.window.layout.WindowMetricsCalculatorDecorator import com.facebook.react.bridge.BridgeReactContext import com.facebook.react.bridge.JavaOnlyMap import com.facebook.react.bridge.ReactContext @@ -17,6 +27,7 @@ import com.facebook.react.bridge.ReactTestHelper import com.facebook.react.bridge.WritableMap import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests import com.facebook.react.uimanager.DisplayMetricsHolder +import com.facebook.react.views.view.isEdgeToEdgeFeatureFlagOn import com.facebook.testutils.shadows.ShadowNativeLoader import com.facebook.testutils.shadows.ShadowNativeMap import com.facebook.testutils.shadows.ShadowReadableNativeMap @@ -33,6 +44,7 @@ import org.mockito.ArgumentMatchers import org.mockito.MockedStatic import org.mockito.Mockito.mockStatic import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock import org.mockito.kotlin.spy import org.mockito.kotlin.times import org.mockito.kotlin.verify @@ -80,6 +92,7 @@ class DeviceInfoModuleTest : TestCase() { @After fun teardown() { displayMetricsHolder.close() + isEdgeToEdgeFeatureFlagOn = false } @Test @@ -150,10 +163,88 @@ class DeviceInfoModuleTest : TestCase() { assertThat(windowMap.hasKey("densityDpi")).isTrue() } + @Test + fun getWindowDisplayMetrics_usesBoundsWhenEdgeToEdgeOn() { + isEdgeToEdgeFeatureFlagOn = true + + val activity = mock() + doReturn(activity).whenever(reactContext).currentActivity + + val bounds = Rect(0, 0, 1080, 2400) + val calculator = mockCalculator(activity, bounds) + + withWindowMetricsCalculator(calculator) { + val metrics = deviceInfoModule.getWindowDisplayMetrics() + assertThat(metrics.widthPixels).isEqualTo(bounds.width()) + assertThat(metrics.heightPixels).isEqualTo(bounds.height()) + } + } + + @Test + fun getWindowDisplayMetrics_subtractsSystemBarsWhenEdgeToEdgeOff() { + isEdgeToEdgeFeatureFlagOn = false + + val window = mock() + val decorView = mock() + whenever(window.decorView).thenReturn(decorView) + val activity = mock() + whenever(activity.window).thenReturn(window) + doReturn(activity).whenever(reactContext).currentActivity + + val bounds = Rect(0, 0, 1080, 2400) + val calculator = mockCalculator(activity, bounds) + val rootInsets = mockRootInsets(Insets.of(20, 80, 30, 100)) + + withWindowMetricsCalculator(calculator) { + mockStatic(ViewCompat::class.java).use { viewCompatStatic -> + viewCompatStatic + .`when` { ViewCompat.getRootWindowInsets(decorView) } + .thenReturn(rootInsets) + + val metrics = deviceInfoModule.getWindowDisplayMetrics() + assertThat(metrics.widthPixels).isEqualTo(bounds.width() - (20 + 30)) + assertThat(metrics.heightPixels).isEqualTo(bounds.height() - (80 + 100)) + } + } + } + private fun givenDisplayMetricsHolderContains(fakeDisplayMetrics: WritableMap?) { doReturn(fakeDisplayMetrics).whenever(deviceInfoModule).getDisplayMetricsWritableMap() } + private fun mockCalculator(activity: Activity, bounds: Rect): WindowMetricsCalculator { + val windowMetrics = mock() + whenever(windowMetrics.bounds).thenReturn(bounds) + val calculator = mock() + whenever(calculator.computeCurrentWindowMetrics(activity)).thenReturn(windowMetrics) + return calculator + } + + private fun mockRootInsets(insets: Insets): WindowInsetsCompat { + val rootInsets = mock() + val type = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout() + whenever(rootInsets.getInsets(type)).thenReturn(insets) + return rootInsets + } + + @Suppress("RestrictedApi") + private fun withWindowMetricsCalculator( + target: WindowMetricsCalculator, + block: () -> Unit, + ) { + WindowMetricsCalculator.overrideDecorator( + object : WindowMetricsCalculatorDecorator { + override fun decorate(calculator: WindowMetricsCalculator): WindowMetricsCalculator = + target + } + ) + try { + block() + } finally { + WindowMetricsCalculator.reset() + } + } + companion object { private fun verifyUpdateDimensionsEventsEmitted( context: ReactContext?,