Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/react-native/ReactAndroid/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,7 @@ dependencies {
api(libs.androidx.autofill)
api(libs.androidx.swiperefreshlayout)
api(libs.androidx.tracing)
api(libs.androidx.window)

api(libs.fbjni)
api(libs.fresco)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,20 @@

package com.facebook.react.modules.deviceinfo

import android.util.DisplayMetrics
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.window.layout.WindowMetricsCalculator
import com.facebook.fbreact.specs.NativeDeviceInfoSpec
import com.facebook.react.bridge.LifecycleEventListener
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactNoCrashSoftException
import com.facebook.react.bridge.ReactSoftExceptionLogger
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.WritableMap
import com.facebook.react.bridge.WritableNativeMap
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.DisplayMetricsHolder.getDisplayMetricsWritableMap
import com.facebook.react.uimanager.DisplayMetricsHolder.getScreenDisplayMetrics
import com.facebook.react.uimanager.DisplayMetricsHolder.initDisplayMetricsIfNotInitialized
import com.facebook.react.views.view.isEdgeToEdgeFeatureFlagOn

Expand All @@ -30,8 +36,56 @@ internal class DeviceInfoModule(reactContext: ReactApplicationContext) :
reactContext.addLifecycleEventListener(this)
}

private fun getWindowDisplayMetrics(): DisplayMetrics {
val windowDisplayMetrics = DisplayMetrics()
windowDisplayMetrics.setTo(reactApplicationContext.resources.displayMetrics)

val activity = reactApplicationContext.currentActivity ?: return windowDisplayMetrics
val bounds = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(activity).bounds

if (isEdgeToEdgeFeatureFlagOn) {
windowDisplayMetrics.widthPixels = bounds.width()
windowDisplayMetrics.heightPixels = bounds.height()
} else {
// WindowMetrics bounds include system bars. When edge-to-edge is not enabled, we subtract
// them so that window dimensions reflect the usable content area. If insets aren't yet
// available (e.g. before the first layout pass), fall back to resources.displayMetrics,
// which already excludes system bars in non-edge-to-edge mode.
ViewCompat.getRootWindowInsets(activity.window.decorView)?.let {
val insets = it.getInsets(WindowInsetsCompat.Type.systemBars())
windowDisplayMetrics.widthPixels = bounds.width() - (insets.left + insets.right)
windowDisplayMetrics.heightPixels = bounds.height() - (insets.top + insets.bottom)
}
}

return windowDisplayMetrics
}

fun getDisplayMetricsWritableMap(): WritableMap =
WritableNativeMap().apply {
putMap(
"windowPhysicalPixels",
getPhysicalPixelsWritableMap(getWindowDisplayMetrics()),
)
putMap(
"screenPhysicalPixels",
getPhysicalPixelsWritableMap(getScreenDisplayMetrics()),
)
}

private fun getPhysicalPixelsWritableMap(
displayMetrics: DisplayMetrics,
): WritableMap =
WritableNativeMap().apply {
putInt("width", displayMetrics.widthPixels)
putInt("height", displayMetrics.heightPixels)
putDouble("scale", displayMetrics.density.toDouble())
putDouble("fontScale", fontScale.toDouble())
putDouble("densityDpi", displayMetrics.densityDpi.toDouble())
}

public override fun getTypedExportedConstants(): Map<String, Any> {
val displayMetrics = getDisplayMetricsWritableMap(fontScale.toDouble())
val displayMetrics = getDisplayMetricsWritableMap()

// Cache the initial dimensions for later comparison in emitUpdateDimensionsEvent
previousDisplayMetrics = displayMetrics.copy()
Expand All @@ -58,7 +112,7 @@ internal class DeviceInfoModule(reactContext: ReactApplicationContext) :
reactApplicationContext.let { context ->
if (context.hasActiveReactInstance()) {
// Don't emit an event to JS if the dimensions haven't changed
val displayMetrics = getDisplayMetricsWritableMap(fontScale.toDouble())
val displayMetrics = getDisplayMetricsWritableMap()
if (previousDisplayMetrics == null) {
previousDisplayMetrics = displayMetrics.copy()
} else if (displayMetrics != previousDisplayMetrics) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ public object DisplayMetricsHolder {
@JvmStatic private var windowDisplayMetrics: DisplayMetrics? = null
@JvmStatic private var screenDisplayMetrics: DisplayMetrics? = null

// TODO(0.87): Remove once we are out of the non-breaking window (see 8d21ffda60)
/** The metrics of the window associated to the Context used to initialize ReactNative */
@JvmStatic
public fun getWindowDisplayMetrics(): DisplayMetrics {
checkNotNull(windowDisplayMetrics) { INITIALIZATION_MISSING_MESSAGE }
return windowDisplayMetrics as DisplayMetrics
}

// TODO(0.87): Remove once we are out of the non-breaking window (see 8d21ffda60)
@JvmStatic
public fun setWindowDisplayMetrics(displayMetrics: DisplayMetrics?) {
windowDisplayMetrics = displayMetrics
Expand Down Expand Up @@ -84,6 +86,7 @@ public object DisplayMetricsHolder {
DisplayMetricsHolder.screenDisplayMetrics = screenDisplayMetrics
}

// TODO(0.87): Remove once we are out of the non-breaking window (see 8d21ffda60)
@JvmStatic
public fun getDisplayMetricsWritableMap(fontScale: Double): WritableMap {
checkNotNull(windowDisplayMetrics) { INITIALIZATION_MISSING_MESSAGE }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,21 @@

package com.facebook.react.modules.deviceinfo

import android.util.DisplayMetrics
import com.facebook.react.bridge.BridgeReactContext
import com.facebook.react.bridge.JavaOnlyMap
import com.facebook.react.bridge.ReactContext
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.testutils.shadows.ShadowNativeLoader
import com.facebook.testutils.shadows.ShadowNativeMap
import com.facebook.testutils.shadows.ShadowReadableNativeMap
import com.facebook.testutils.shadows.ShadowSoLoader
import com.facebook.testutils.shadows.ShadowWritableNativeMap
import junit.framework.TestCase
import org.assertj.core.api.Assertions
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
Expand All @@ -26,13 +32,26 @@ import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers
import org.mockito.MockedStatic
import org.mockito.Mockito.mockStatic
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.spy
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)
@Config(
shadows =
[
ShadowSoLoader::class,
ShadowNativeLoader::class,
ShadowNativeMap::class,
ShadowWritableNativeMap::class,
ShadowReadableNativeMap::class,
]
)
class DeviceInfoModuleTest : TestCase() {

private lateinit var deviceInfoModule: DeviceInfoModule
Expand All @@ -55,7 +74,7 @@ class DeviceInfoModuleTest : TestCase() {
reactContext = spy(BridgeReactContext(RuntimeEnvironment.getApplication()))
val catalystInstanceMock = ReactTestHelper.createMockCatalystInstance()
reactContext.initializeWithInstance(catalystInstanceMock)
deviceInfoModule = DeviceInfoModule(reactContext)
deviceInfoModule = spy(DeviceInfoModule(reactContext))
}

@After
Expand Down Expand Up @@ -110,10 +129,29 @@ class DeviceInfoModuleTest : TestCase() {
)
}

private fun givenDisplayMetricsHolderContains(fakeDisplayMetrics: WritableMap?) {
@Test
fun getDisplayMetricsWritableMap_returnsCorrectMap() {
displayMetricsHolder
.`when`<WritableMap> { DisplayMetricsHolder.getDisplayMetricsWritableMap(1.0) }
.thenAnswer { fakeDisplayMetrics }
.`when`<DisplayMetrics> { DisplayMetricsHolder.getScreenDisplayMetrics() }
.thenAnswer { reactContext.resources.displayMetrics }

// Use the official initialization method to ensure both metrics are set
val map: WritableMap = deviceInfoModule.getDisplayMetricsWritableMap()
assertThat(map.hasKey("windowPhysicalPixels")).isTrue()
assertThat(map.hasKey("screenPhysicalPixels")).isTrue()
val windowMap = map.getMap("windowPhysicalPixels")
val screenMap = map.getMap("screenPhysicalPixels")
checkNotNull(windowMap)
checkNotNull(screenMap)
assertThat(windowMap.hasKey("width")).isTrue()
assertThat(windowMap.hasKey("height")).isTrue()
assertThat(windowMap.hasKey("scale")).isTrue()
assertThat(windowMap.hasKey("fontScale")).isTrue()
assertThat(windowMap.hasKey("densityDpi")).isTrue()
}

private fun givenDisplayMetricsHolderContains(fakeDisplayMetrics: WritableMap?) {
doReturn(fakeDisplayMetrics).whenever(deviceInfoModule).getDisplayMetricsWritableMap()
}

companion object {
Expand All @@ -126,7 +164,7 @@ class DeviceInfoModuleTest : TestCase() {
verify(context, times(expectedEventList.size))
?.emitDeviceEvent(ArgumentMatchers.eq("didUpdateDimensions"), captor.capture())
val actualEvents = captor.allValues
Assertions.assertThat(actualEvents).isEqualTo(expectedEventList)
assertThat(actualEvents).isEqualTo(expectedEventList)
}
}
}
2 changes: 2 additions & 0 deletions packages/react-native/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ androidx-swiperefreshlayout = "1.1.0"
androidx-test = "1.5.0"
androidx-test-junit = "1.2.1"
androidx-tracing = "1.1.0"
androidx-window = "1.5.1"
assertj = "3.21.0"
binary-compatibility-validator = "0.13.2"
download = "5.4.0"
Expand Down Expand Up @@ -64,6 +65,7 @@ androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-
androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test" }
androidx-tracing = { module = "androidx.tracing:tracing", version.ref = "androidx-tracing" }
androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" }
androidx-window = { module = "androidx.window:window", version.ref = "androidx-window" }

fbjni = { module = "com.facebook.fbjni:fbjni", version.ref = "fbjni" }
fresco = { module = "com.facebook.fresco:fresco", version.ref = "fresco" }
Expand Down
Loading