Skip to content

Commit c18a492

Browse files
jonnyandrewfacebook-github-bot
authored andcommitted
Fix Dimensions not updating on Android (#31973)
Summary: When retrieving the device dimensions through the JS `Dimensions` utility, the result of `Dimensions.get` can be incorrect on Android. ### Related issues - #29105 - #29451 - #29323 The issue is caused by the Android `DeviceInfoModule` that provides initial screen dimensions and then subsequently updates those by emitting `didUpdateDimensions` events. The assumption in that implementation is that the initial display metrics will not have changed prior to the first check for updated metrics. However that is not the case as the device may be rotated (as shown in the attached video). The solution in this PR is to keep track of the initial dimensions for comparison at the first check for updated metrics. ## Changelog [Android] [Fixed] - Fix Dimensions not updating Pull Request resolved: #31973 Test Plan: ### Steps to reproduce 1. Install the RNTester app on Android from the `main` branch. 2. Set the device auto-rotation to ON 3. Start the RNTester app 4. While the app is loading, rotate the device 5. Navigate to the `Dimensions` screen 6. Either a. Observe the screen width and height are reversed, or b. Quit the app and return to step 3. ### Verifying the fix #### Manually Using the above steps, the issue should no longer be reproducible. #### Automatically See unit tests in `ReactAndroid/src/test/java/com/facebook/react/modules/deviceinfo/DeviceInfoModuleTest.java` ### Video https://user-images.githubusercontent.com/4940864/128485453-2ae04724-4ac5-4267-a59a-140cc3af626b.mp4 Reviewed By: JoshuaGross Differential Revision: D30319919 Pulled By: lunaleaps fbshipit-source-id: 52a2faeafc522b1c2a196ca40357027eafa1a84b
1 parent 842bcb9 commit c18a492

File tree

5 files changed

+185
-32
lines changed

5 files changed

+185
-32
lines changed

ReactAndroid/src/main/java/com/facebook/react/modules/deviceinfo/DeviceInfoModule.java

+8-4
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import com.facebook.react.bridge.ReactNoCrashSoftException;
1616
import com.facebook.react.bridge.ReactSoftExceptionLogger;
1717
import com.facebook.react.bridge.ReadableMap;
18-
import com.facebook.react.bridge.WritableNativeMap;
18+
import com.facebook.react.bridge.WritableMap;
1919
import com.facebook.react.module.annotations.ReactModule;
2020
import com.facebook.react.modules.core.DeviceEventManagerModule;
2121
import com.facebook.react.uimanager.DisplayMetricsHolder;
@@ -54,8 +54,13 @@ public String getName() {
5454

5555
@Override
5656
public @Nullable Map<String, Object> getTypedExportedConstants() {
57+
WritableMap displayMetrics = DisplayMetricsHolder.getDisplayMetricsWritableMap(mFontScale);
58+
59+
// Cache the initial dimensions for later comparison in emitUpdateDimensionsEvent
60+
mPreviousDisplayMetrics = displayMetrics.copy();
61+
5762
HashMap<String, Object> constants = new HashMap<>();
58-
constants.put("Dimensions", DisplayMetricsHolder.getDisplayMetricsMap(mFontScale));
63+
constants.put("Dimensions", displayMetrics.toHashMap());
5964
return constants;
6065
}
6166

@@ -85,8 +90,7 @@ public void emitUpdateDimensionsEvent() {
8590

8691
if (mReactApplicationContext.hasActiveReactInstance()) {
8792
// Don't emit an event to JS if the dimensions haven't changed
88-
WritableNativeMap displayMetrics =
89-
DisplayMetricsHolder.getDisplayMetricsNativeMap(mFontScale);
93+
WritableMap displayMetrics = DisplayMetricsHolder.getDisplayMetricsWritableMap(mFontScale);
9094
if (mPreviousDisplayMetrics == null) {
9195
mPreviousDisplayMetrics = displayMetrics.copy();
9296
} else if (!displayMetrics.equals(mPreviousDisplayMetrics)) {

ReactAndroid/src/main/java/com/facebook/react/uimanager/DisplayMetricsHolder.java

+7-28
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,8 @@
1313
import android.view.WindowManager;
1414
import androidx.annotation.Nullable;
1515
import com.facebook.infer.annotation.Assertions;
16+
import com.facebook.react.bridge.WritableMap;
1617
import com.facebook.react.bridge.WritableNativeMap;
17-
import java.util.HashMap;
18-
import java.util.Map;
1918

2019
/**
2120
* Holds an instance of the current DisplayMetrics so we don't have to thread it through all the
@@ -81,40 +80,20 @@ public static DisplayMetrics getScreenDisplayMetrics() {
8180
return sScreenDisplayMetrics;
8281
}
8382

84-
public static Map<String, Map<String, Object>> getDisplayMetricsMap(double fontScale) {
83+
public static WritableMap getDisplayMetricsWritableMap(double fontScale) {
8584
Assertions.assertCondition(
8685
sWindowDisplayMetrics != null && sScreenDisplayMetrics != null,
87-
"DisplayMetricsHolder must be initialized with initDisplayMetricsIfNotInitialized or initDisplayMetrics");
88-
final Map<String, Map<String, Object>> result = new HashMap<>();
89-
result.put("windowPhysicalPixels", getPhysicalPixelsMap(sWindowDisplayMetrics, fontScale));
90-
result.put("screenPhysicalPixels", getPhysicalPixelsMap(sScreenDisplayMetrics, fontScale));
91-
return result;
92-
}
93-
94-
public static WritableNativeMap getDisplayMetricsNativeMap(double fontScale) {
95-
Assertions.assertCondition(
96-
sWindowDisplayMetrics != null && sScreenDisplayMetrics != null,
97-
"DisplayMetricsHolder must be initialized with initDisplayMetricsIfNotInitialized or initDisplayMetrics");
86+
"DisplayMetricsHolder must be initialized with initDisplayMetricsIfNotInitialized or"
87+
+ " initDisplayMetrics");
9888
final WritableNativeMap result = new WritableNativeMap();
9989
result.putMap(
100-
"windowPhysicalPixels", getPhysicalPixelsNativeMap(sWindowDisplayMetrics, fontScale));
90+
"windowPhysicalPixels", getPhysicalPixelsWritableMap(sWindowDisplayMetrics, fontScale));
10191
result.putMap(
102-
"screenPhysicalPixels", getPhysicalPixelsNativeMap(sScreenDisplayMetrics, fontScale));
103-
return result;
104-
}
105-
106-
private static Map<String, Object> getPhysicalPixelsMap(
107-
DisplayMetrics displayMetrics, double fontScale) {
108-
final Map<String, Object> result = new HashMap<>();
109-
result.put("width", displayMetrics.widthPixels);
110-
result.put("height", displayMetrics.heightPixels);
111-
result.put("scale", displayMetrics.density);
112-
result.put("fontScale", fontScale);
113-
result.put("densityDpi", displayMetrics.densityDpi);
92+
"screenPhysicalPixels", getPhysicalPixelsWritableMap(sScreenDisplayMetrics, fontScale));
11493
return result;
11594
}
11695

117-
private static WritableNativeMap getPhysicalPixelsNativeMap(
96+
private static WritableMap getPhysicalPixelsWritableMap(
11897
DisplayMetrics displayMetrics, double fontScale) {
11998
final WritableNativeMap result = new WritableNativeMap();
12099
result.putInt("width", displayMetrics.widthPixels);

ReactAndroid/src/test/java/com/facebook/react/bridge/ReactTestHelper.java

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public void handleException(Exception e) {
5252
when(reactInstance.getReactQueueConfiguration()).thenReturn(ReactQueueConfiguration);
5353
when(reactInstance.getNativeModule(UIManagerModule.class))
5454
.thenReturn(mock(UIManagerModule.class));
55+
when(reactInstance.isDestroyed()).thenReturn(false);
5556

5657
return reactInstance;
5758
}

ReactAndroid/src/test/java/com/facebook/react/modules/BUCK

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ rn_robolectric_test(
3232
react_native_target("java/com/facebook/react/modules/common:common"),
3333
react_native_target("java/com/facebook/react/modules/core:core"),
3434
react_native_target("java/com/facebook/react/modules/debug:debug"),
35+
react_native_target("java/com/facebook/react/modules/deviceinfo:deviceinfo"),
3536
react_native_target("java/com/facebook/react/modules/dialog:dialog"),
3637
react_native_target("java/com/facebook/react/modules/network:network"),
3738
react_native_target("java/com/facebook/react/modules/share:share"),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/*
2+
* Copyright (c) Facebook, Inc. and its 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.modules.deviceinfo;
9+
10+
import static org.fest.assertions.api.Assertions.assertThat;
11+
import static org.mockito.ArgumentMatchers.eq;
12+
import static org.mockito.Mockito.mock;
13+
import static org.mockito.Mockito.spy;
14+
import static org.mockito.Mockito.times;
15+
import static org.mockito.Mockito.verify;
16+
import static org.mockito.Mockito.verifyNoMoreInteractions;
17+
import static org.mockito.Mockito.when;
18+
import static org.powermock.api.mockito.PowerMockito.mockStatic;
19+
20+
import com.facebook.react.bridge.Arguments;
21+
import com.facebook.react.bridge.CatalystInstance;
22+
import com.facebook.react.bridge.JavaOnlyMap;
23+
import com.facebook.react.bridge.ReactApplicationContext;
24+
import com.facebook.react.bridge.ReactTestHelper;
25+
import com.facebook.react.bridge.WritableMap;
26+
import com.facebook.react.modules.core.DeviceEventManagerModule;
27+
import com.facebook.react.uimanager.DisplayMetricsHolder;
28+
import java.util.Arrays;
29+
import java.util.List;
30+
import junit.framework.TestCase;
31+
import org.junit.After;
32+
import org.junit.Before;
33+
import org.junit.Rule;
34+
import org.junit.Test;
35+
import org.junit.runner.RunWith;
36+
import org.mockito.ArgumentCaptor;
37+
import org.mockito.invocation.InvocationOnMock;
38+
import org.mockito.stubbing.Answer;
39+
import org.powermock.core.classloader.annotations.PowerMockIgnore;
40+
import org.powermock.core.classloader.annotations.PrepareForTest;
41+
import org.powermock.modules.junit4.rule.PowerMockRule;
42+
import org.robolectric.RobolectricTestRunner;
43+
import org.robolectric.RuntimeEnvironment;
44+
45+
@RunWith(RobolectricTestRunner.class)
46+
@PrepareForTest({Arguments.class, DisplayMetricsHolder.class})
47+
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "androidx.*", "android.*"})
48+
public class DeviceInfoModuleTest extends TestCase {
49+
50+
@Rule public PowerMockRule rule = new PowerMockRule();
51+
52+
private DeviceInfoModule mDeviceInfoModule;
53+
private DeviceEventManagerModule.RCTDeviceEventEmitter mRCTDeviceEventEmitterMock;
54+
55+
private WritableMap fakePortraitDisplayMetrics;
56+
private WritableMap fakeLandscapeDisplayMetrics;
57+
58+
@Before
59+
public void setUp() {
60+
initTestData();
61+
62+
mockStatic(DisplayMetricsHolder.class);
63+
64+
mRCTDeviceEventEmitterMock = mock(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
65+
66+
final ReactApplicationContext context =
67+
spy(new ReactApplicationContext(RuntimeEnvironment.application));
68+
CatalystInstance catalystInstanceMock = ReactTestHelper.createMockCatalystInstance();
69+
context.initializeWithInstance(catalystInstanceMock);
70+
when(context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class))
71+
.thenReturn(mRCTDeviceEventEmitterMock);
72+
73+
mDeviceInfoModule = new DeviceInfoModule(context);
74+
}
75+
76+
@After
77+
public void teardown() {
78+
DisplayMetricsHolder.setWindowDisplayMetrics(null);
79+
DisplayMetricsHolder.setScreenDisplayMetrics(null);
80+
}
81+
82+
@Test
83+
public void test_itDoesNotEmitAnEvent_whenDisplayMetricsNotChanged() {
84+
givenDisplayMetricsHolderContains(fakePortraitDisplayMetrics);
85+
86+
mDeviceInfoModule.getTypedExportedConstants();
87+
mDeviceInfoModule.emitUpdateDimensionsEvent();
88+
89+
verifyNoMoreInteractions(mRCTDeviceEventEmitterMock);
90+
}
91+
92+
@Test
93+
public void test_itEmitsOneEvent_whenDisplayMetricsChangedOnce() {
94+
givenDisplayMetricsHolderContains(fakePortraitDisplayMetrics);
95+
96+
mDeviceInfoModule.getTypedExportedConstants();
97+
givenDisplayMetricsHolderContains(fakeLandscapeDisplayMetrics);
98+
mDeviceInfoModule.emitUpdateDimensionsEvent();
99+
100+
verifyUpdateDimensionsEventsEmitted(mRCTDeviceEventEmitterMock, fakeLandscapeDisplayMetrics);
101+
}
102+
103+
@Test
104+
public void test_itEmitsJustOneEvent_whenUpdateRequestedMultipleTimes() {
105+
givenDisplayMetricsHolderContains(fakePortraitDisplayMetrics);
106+
mDeviceInfoModule.getTypedExportedConstants();
107+
givenDisplayMetricsHolderContains(fakeLandscapeDisplayMetrics);
108+
mDeviceInfoModule.emitUpdateDimensionsEvent();
109+
mDeviceInfoModule.emitUpdateDimensionsEvent();
110+
111+
verifyUpdateDimensionsEventsEmitted(mRCTDeviceEventEmitterMock, fakeLandscapeDisplayMetrics);
112+
}
113+
114+
@Test
115+
public void test_itEmitsMultipleEvents_whenDisplayMetricsChangedBetweenUpdates() {
116+
givenDisplayMetricsHolderContains(fakePortraitDisplayMetrics);
117+
118+
mDeviceInfoModule.getTypedExportedConstants();
119+
mDeviceInfoModule.emitUpdateDimensionsEvent();
120+
givenDisplayMetricsHolderContains(fakeLandscapeDisplayMetrics);
121+
mDeviceInfoModule.emitUpdateDimensionsEvent();
122+
givenDisplayMetricsHolderContains(fakePortraitDisplayMetrics);
123+
mDeviceInfoModule.emitUpdateDimensionsEvent();
124+
givenDisplayMetricsHolderContains(fakeLandscapeDisplayMetrics);
125+
mDeviceInfoModule.emitUpdateDimensionsEvent();
126+
127+
verifyUpdateDimensionsEventsEmitted(
128+
mRCTDeviceEventEmitterMock,
129+
fakeLandscapeDisplayMetrics,
130+
fakePortraitDisplayMetrics,
131+
fakeLandscapeDisplayMetrics);
132+
}
133+
134+
private static void givenDisplayMetricsHolderContains(final WritableMap fakeDisplayMetrics) {
135+
when(DisplayMetricsHolder.getDisplayMetricsWritableMap(1.0)).thenReturn(fakeDisplayMetrics);
136+
}
137+
138+
private static void verifyUpdateDimensionsEventsEmitted(
139+
DeviceEventManagerModule.RCTDeviceEventEmitter emitter, WritableMap... expectedEvents) {
140+
List<WritableMap> expectedEventList = Arrays.asList(expectedEvents);
141+
ArgumentCaptor<WritableMap> captor = ArgumentCaptor.forClass(WritableMap.class);
142+
verify(emitter, times(expectedEventList.size()))
143+
.emit(eq("didUpdateDimensions"), captor.capture());
144+
145+
List<WritableMap> actualEvents = captor.getAllValues();
146+
assertThat(actualEvents).isEqualTo(expectedEventList);
147+
}
148+
149+
private void initTestData() {
150+
mockStatic(Arguments.class);
151+
when(Arguments.createMap())
152+
.thenAnswer(
153+
new Answer<Object>() {
154+
@Override
155+
public Object answer(InvocationOnMock invocation) throws Throwable {
156+
return new JavaOnlyMap();
157+
}
158+
});
159+
160+
fakePortraitDisplayMetrics = Arguments.createMap();
161+
fakePortraitDisplayMetrics.putInt("width", 100);
162+
fakePortraitDisplayMetrics.putInt("height", 200);
163+
164+
fakeLandscapeDisplayMetrics = Arguments.createMap();
165+
fakeLandscapeDisplayMetrics.putInt("width", 200);
166+
fakeLandscapeDisplayMetrics.putInt("height", 100);
167+
}
168+
}

0 commit comments

Comments
 (0)