From c005609b0155223f3ae778c5cd96bc78c4dfe399 Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Tue, 24 Sep 2024 03:35:15 -0700 Subject: [PATCH 001/296] Properly set `REACTNATIVE_MERGED_SO` for autolinked libraries. (#46606) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46606 This fixes this issue reported here: https://github.com/react-native-community/discussions-and-proposals/discussions/816#discussioncomment-10673136 reported by both SWM and Expo. The problem is that `REACTNATIVE_MERGED_SO` is not properly set for autolinked libraries so they can't access it to understand if the version of ReactNative has merged so libraries or not. This fixes it, I've tested against https://github.com/tomekzaw/repro-reactnative-merged-so reproducer provided by tomekzaw Changelog: [Android] [Fixed] - Properly set `REACTNATIVE_MERGED_SO` for autolinked libraries Reviewed By: rubennorte Differential Revision: D63262687 fbshipit-source-id: c505dce9036bb4cd0366b7ab99412368963273af --- .../tasks/GenerateAutolinkingNewArchitecturesFileTask.kt | 4 ++++ .../GenerateAutolinkingNewArchitecturesFileTaskTest.kt | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GenerateAutolinkingNewArchitecturesFileTask.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GenerateAutolinkingNewArchitecturesFileTask.kt index 751c9269722b9f..0de22fc0eb5af0 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GenerateAutolinkingNewArchitecturesFileTask.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GenerateAutolinkingNewArchitecturesFileTask.kt @@ -166,6 +166,10 @@ abstract class GenerateAutolinkingNewArchitecturesFileTask : DefaultTask() { cmake_minimum_required(VERSION 3.13) set(CMAKE_VERBOSE_MAKEFILE on) + # We set REACTNATIVE_MERGED_SO so libraries/apps can selectively decide to depend on either libreactnative.so + # or link against a old prefab target (this is needed for React Native 0.76 on). + set(REACTNATIVE_MERGED_SO true) + {{ libraryIncludes }} set(AUTOLINKED_LIBRARIES diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GenerateAutolinkingNewArchitecturesFileTaskTest.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GenerateAutolinkingNewArchitecturesFileTaskTest.kt index c9a9a53aba66c3..7abe10595b2e91 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GenerateAutolinkingNewArchitecturesFileTaskTest.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GenerateAutolinkingNewArchitecturesFileTaskTest.kt @@ -115,6 +115,10 @@ class GenerateAutolinkingNewArchitecturesFileTaskTest { cmake_minimum_required(VERSION 3.13) set(CMAKE_VERBOSE_MAKEFILE on) + # We set REACTNATIVE_MERGED_SO so libraries/apps can selectively decide to depend on either libreactnative.so + # or link against a old prefab target (this is needed for React Native 0.76 on). + set(REACTNATIVE_MERGED_SO true) + set(AUTOLINKED_LIBRARIES @@ -137,6 +141,10 @@ class GenerateAutolinkingNewArchitecturesFileTaskTest { cmake_minimum_required(VERSION 3.13) set(CMAKE_VERBOSE_MAKEFILE on) + # We set REACTNATIVE_MERGED_SO so libraries/apps can selectively decide to depend on either libreactnative.so + # or link against a old prefab target (this is needed for React Native 0.76 on). + set(REACTNATIVE_MERGED_SO true) + add_subdirectory(./a/directory/ aPackage_autolinked_build) add_subdirectory(./another/directory/ anotherPackage_autolinked_build) add_subdirectory(./another/directory/cxx/ anotherPackage_cxxmodule_autolinked_build) From a9551641b47ebe9a032754fb0274b57c8ee18ea1 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Tue, 24 Sep 2024 05:34:01 -0700 Subject: [PATCH 002/296] Cleanup Modal test code (#46603) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46603 Was debugging this test for the mounting instruction changes, and found some opportunities to clean this up. Changelog: [Internal] Reviewed By: cortinico Differential Revision: D63258342 fbshipit-source-id: f7e6be58474f112993232ce5859ccb160f050ae8 --- .../js/examples/Modal/ModalOnShow.js | 13 ++++++----- .../js/examples/Modal/ModalPresentation.js | 22 ++++++++++--------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/packages/rn-tester/js/examples/Modal/ModalOnShow.js b/packages/rn-tester/js/examples/Modal/ModalOnShow.js index 8ef9764794a570..ae4315cc4404d2 100644 --- a/packages/rn-tester/js/examples/Modal/ModalOnShow.js +++ b/packages/rn-tester/js/examples/Modal/ModalOnShow.js @@ -11,13 +11,14 @@ import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; import * as React from 'react'; +import {useState} from 'react'; import {Modal, Pressable, StyleSheet, Text, View} from 'react-native'; function ModalOnShowOnDismiss(): React.Node { - const [modalShowComponent, setModalShowComponent] = React.useState(true); - const [modalVisible, setModalVisible] = React.useState(false); - const [onShowCount, setOnShowCount] = React.useState(0); - const [onDismissCount, setOnDismissCount] = React.useState(0); + const [modalShowComponent, setModalShowComponent] = useState(true); + const [modalVisible, setModalVisible] = useState(false); + const [onShowCount, setOnShowCount] = useState(0); + const [onDismissCount, setOnDismissCount] = useState(0); return ( @@ -27,10 +28,10 @@ function ModalOnShowOnDismiss(): React.Node { transparent={true} visible={modalVisible} onShow={() => { - setOnShowCount(onShowCount + 1); + setOnShowCount(showCount => showCount + 1); }} onDismiss={() => { - setOnDismissCount(onDismissCount + 1); + setOnDismissCount(dismissCount => dismissCount + 1); }} onRequestClose={() => { setModalVisible(false); diff --git a/packages/rn-tester/js/examples/Modal/ModalPresentation.js b/packages/rn-tester/js/examples/Modal/ModalPresentation.js index d800fd703170c5..872fbb59a7f959 100644 --- a/packages/rn-tester/js/examples/Modal/ModalPresentation.js +++ b/packages/rn-tester/js/examples/Modal/ModalPresentation.js @@ -15,6 +15,7 @@ import type {Props as ModalProps} from 'react-native/Libraries/Modal/Modal'; import RNTOption from '../../components/RNTOption'; import * as React from 'react'; +import {useCallback, useState} from 'react'; import {Modal, Platform, StyleSheet, Switch, Text, View} from 'react-native'; const RNTesterButton = require('../../components/RNTesterButton'); @@ -37,19 +38,19 @@ const supportedOrientations = [ const backdropColors = ['red', 'blue', undefined]; function ModalPresentation() { - const onDismiss = React.useCallback(() => { + const onDismiss = useCallback(() => { alert('onDismiss'); }, []); - const onShow = React.useCallback(() => { + const onShow = useCallback(() => { alert('onShow'); }, []); - const onRequestClose = React.useCallback(() => { + const onRequestClose = useCallback(() => { console.log('onRequestClose'); }, []); - const [props, setProps] = React.useState({ + const [props, setProps] = useState({ animationType: 'none', transparent: false, hardwareAccelerated: false, @@ -72,11 +73,12 @@ function ModalPresentation() { const statusBarTranslucent = props.statusBarTranslucent; const backdropColor = props.backdropColor; - const [currentOrientation, setCurrentOrientation] = React.useState('unknown'); + const [currentOrientation, setCurrentOrientation] = useState('unknown'); - /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's - * LTI update could not be added via codemod */ - const onOrientationChange = event => + type OrientationChangeEvent = Parameters< + $NonMaybeType['onOrientationChange']>, + >[0]; + const onOrientationChange = (event: OrientationChangeEvent) => setCurrentOrientation(event.nativeEvent.orientation); const controls = ( @@ -220,9 +222,9 @@ function ModalPresentation() { {backdropColors.map(type => ( setProps(prev => ({ From 303e0ed7641409acf2d852c077f6be426afd7a0c Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Tue, 24 Sep 2024 09:24:24 -0700 Subject: [PATCH 003/296] Fix Increment and Decrement accessibility actions (#46617) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46617 This changes fixes the Increment/Decrement accessibility actions on iOS with the New Architecture. ## Changelog [iOS][Fixed] - Make sure that the Increment and Decrement accessibility actions works on iOS Reviewed By: javache Differential Revision: D63263830 fbshipit-source-id: 99dca14a002e098db2d3b0e268af32cdab6ce786 --- .../ComponentViews/View/RCTViewComponentView.mm | 14 ++++++++++++++ .../examples/Accessibility/AccessibilityExample.js | 8 ++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 6cb7f5af086f2a..0f817e2e6e2872 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -1308,6 +1308,20 @@ - (BOOL)accessibilityPerformEscape } } +- (void)accessibilityIncrement +{ + if (_eventEmitter && _props->onAccessibilityAction) { + _eventEmitter->onAccessibilityAction("increment"); + } +} + +- (void)accessibilityDecrement +{ + if (_eventEmitter && _props->onAccessibilityAction) { + _eventEmitter->onAccessibilityAction("decrement"); + } +} + - (BOOL)didActivateAccessibilityCustomAction:(UIAccessibilityCustomAction *)action { if (_eventEmitter && _props->onAccessibilityAction) { diff --git a/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js b/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js index 73b75e81af295b..74394621d066ee 100644 --- a/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js +++ b/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js @@ -1241,7 +1241,7 @@ class FakeSliderExample extends React.Component<{}, FakeSliderExampleState> { max: 100, }}> - Fake Slider + Fake Slider {this.state.current} { accessibilityValue={{text: this.state.textualValue}}> - Equalizer + Equalizer {this.state.textualValue} @@ -1335,7 +1335,7 @@ class FakeSliderExampleForAccessibilityValue extends React.Component< aria-valuetext={'slider aria value text'} aria-valuenow={this.state.current}> - Fake Slider + Fake Slider {this.state.current} - Equalizer + Equalizer {this.state.textualValue} From 0cb32d5ac92960826827db2fc5635387f08ce1a7 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Tue, 24 Sep 2024 13:40:47 -0700 Subject: [PATCH 004/296] Cleanup Fresco SystraceRequestRequestListener (#46626) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46626 Remove `!!` by reusing lookup result. Changelog: [Internal] Reviewed By: mdvacca Differential Revision: D63316175 fbshipit-source-id: 1abfb20490831f26819aef4393c9629253d33ec6 --- .../modules/fresco/SystraceRequestListener.kt | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/SystraceRequestListener.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/SystraceRequestListener.kt index 89fb49f9ce468f..8553e1cbb10b25 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/SystraceRequestListener.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/SystraceRequestListener.kt @@ -15,8 +15,8 @@ import com.facebook.systrace.Systrace /** Logs requests to Systrace */ public class SystraceRequestListener : BaseRequestListener() { private var currentId: Int = 0 - private var producerId: MutableMap> = mutableMapOf() - private var requestsId: MutableMap> = mutableMapOf() + private val producerId: MutableMap> = mutableMapOf() + private val requestsId: MutableMap> = mutableMapOf() override fun onProducerStart(requestId: String, producerName: String) { if (!Systrace.isTracing(Systrace.TRACE_TAG_REACT_FRESCO)) { @@ -39,8 +39,9 @@ public class SystraceRequestListener : BaseRequestListener() { if (!Systrace.isTracing(Systrace.TRACE_TAG_REACT_FRESCO)) { return } - if (producerId.containsKey(requestId)) { - val entry = producerId[requestId]!! + + val entry = producerId[requestId] + if (entry != null) { Systrace.endAsyncSection(Systrace.TRACE_TAG_REACT_FRESCO, entry.second, entry.first) producerId.remove(requestId) } @@ -55,8 +56,9 @@ public class SystraceRequestListener : BaseRequestListener() { if (!Systrace.isTracing(Systrace.TRACE_TAG_REACT_FRESCO)) { return } - if (producerId.containsKey(requestId)) { - val entry = producerId[requestId]!! + + val entry = producerId[requestId] + if (entry != null) { Systrace.endAsyncSection(Systrace.TRACE_TAG_REACT_FRESCO, entry.second, entry.first) producerId.remove(requestId) } @@ -70,8 +72,9 @@ public class SystraceRequestListener : BaseRequestListener() { if (!Systrace.isTracing(Systrace.TRACE_TAG_REACT_FRESCO)) { return } - if (producerId.containsKey(requestId)) { - val entry = producerId[requestId]!! + + val entry = producerId[requestId] + if (entry != null) { Systrace.endAsyncSection(Systrace.TRACE_TAG_REACT_FRESCO, entry.second, entry.first) producerId.remove(requestId) } @@ -101,6 +104,7 @@ public class SystraceRequestListener : BaseRequestListener() { if (!Systrace.isTracing(Systrace.TRACE_TAG_REACT_FRESCO)) { return } + val entryName = StringBuilder() entryName.append("FRESCO_REQUEST_") entryName.append(request.sourceUri.toString().replace(':', '_')) @@ -114,8 +118,9 @@ public class SystraceRequestListener : BaseRequestListener() { if (!Systrace.isTracing(Systrace.TRACE_TAG_REACT_FRESCO)) { return } - if (requestsId.containsKey(requestId)) { - val entry = requestsId[requestId]!! + + val entry = requestsId[requestId] + if (entry != null) { Systrace.endAsyncSection(Systrace.TRACE_TAG_REACT_FRESCO, entry.second, entry.first) requestsId.remove(requestId) } @@ -130,8 +135,9 @@ public class SystraceRequestListener : BaseRequestListener() { if (!Systrace.isTracing(Systrace.TRACE_TAG_REACT_FRESCO)) { return } - if (requestsId.containsKey(requestId)) { - val entry = requestsId[requestId]!! + + val entry = requestsId[requestId] + if (entry != null) { Systrace.endAsyncSection(Systrace.TRACE_TAG_REACT_FRESCO, entry.second, entry.first) requestsId.remove(requestId) } @@ -141,8 +147,9 @@ public class SystraceRequestListener : BaseRequestListener() { if (!Systrace.isTracing(Systrace.TRACE_TAG_REACT_FRESCO)) { return } - if (requestsId.containsKey(requestId)) { - val entry = requestsId[requestId]!! + + val entry = requestsId[requestId] + if (entry != null) { Systrace.endAsyncSection(Systrace.TRACE_TAG_REACT_FRESCO, entry.second, entry.first) requestsId.remove(requestId) } From 68a6b69b27275998b4083797942f3e26a92d3adb Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Tue, 24 Sep 2024 19:49:34 -0700 Subject: [PATCH 005/296] Fix PointerOverOut test (#46634) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46634 The PointerEventPointerOverOut.js test case is broken due to a changed assumption about the name of the native tag property (`_nativeTag` vs. `__nativeTag`). This fixes the broken assumption. ## Changelog [General][Fixed] Fixed issue with W3C PointerEvents tests Reviewed By: NickGerleman Differential Revision: D63336621 fbshipit-source-id: b54270f1c1232de6845ef73cac52f06b9a85539c --- .../W3CPointerEventPlatformTests/PointerEventPointerOverOut.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rn-tester/js/examples/Experimental/W3CPointerEventPlatformTests/PointerEventPointerOverOut.js b/packages/rn-tester/js/examples/Experimental/W3CPointerEventPlatformTests/PointerEventPointerOverOut.js index 5358381de5d147..6ffc64c010fc44 100644 --- a/packages/rn-tester/js/examples/Experimental/W3CPointerEventPlatformTests/PointerEventPointerOverOut.js +++ b/packages/rn-tester/js/examples/Experimental/W3CPointerEventPlatformTests/PointerEventPointerOverOut.js @@ -26,7 +26,7 @@ function getNativeTagFromHostElement( } if (elem != null) { // $FlowExpectedError - accessing non-public property - return elem._nativeTag; + return elem.__nativeTag; } return undefined; } From 65575e8399a08841a0ccbc5b28cafeee6d41714c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Wed, 25 Sep 2024 01:28:23 -0700 Subject: [PATCH 006/296] feat: get react-native package name from package.json in codegen script (#46604) Summary: This PR fixes the retrieval of react native package name to retrieve it from package.json. This fixes the annoying message in codegen that poped up when installing pods: ``` [Codegen] Found react-native-macos [Codegen] CodegenConfig Deprecated Setup for react-native-macos. The configuration file still contains the codegen in the libraries array. If possible, replace it with a single object. BEFORE: { // ... "codegenConfig": { "libraries": [ { "name": "libName1", "type": "all|components|modules", "jsSrcsRoot": "libName1/js" }, { "name": "libName2", "type": "all|components|modules", "jsSrcsRoot": "libName2/src" } ] } } AFTER: { "codegenConfig": { "name": "libraries", "type": "all", "jsSrcsRoot": "." } } ``` ## Changelog: [GENERAL] [CHANGED] - get react-native package name from package.json in codegen script Pull Request resolved: https://github.com/facebook/react-native/pull/46604 Test Plan: Install pods Reviewed By: cortinico Differential Revision: D63335543 Pulled By: arushikesarwani94 fbshipit-source-id: 0470e54f0fc7aa962918c889855c52648d11e32d --- .../__tests__/generate-artifacts-executor-test.js | 9 +++++++++ .../scripts/codegen/generate-artifacts-executor.js | 8 +++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/react-native/scripts/codegen/__tests__/generate-artifacts-executor-test.js b/packages/react-native/scripts/codegen/__tests__/generate-artifacts-executor-test.js index 6c091f18b9868e..b227a3af50682e 100644 --- a/packages/react-native/scripts/codegen/__tests__/generate-artifacts-executor-test.js +++ b/packages/react-native/scripts/codegen/__tests__/generate-artifacts-executor-test.js @@ -16,6 +16,10 @@ const path = require('path'); const rootPath = path.join(__dirname, '../../..'); +const packageJson = JSON.stringify({ + name: 'react-native', +}); + describe('extractLibrariesFromJSON', () => { it('extracts a single dependency when config has no libraries', () => { let configFile = fixtures.noLibrariesConfigFile; @@ -153,6 +157,7 @@ describe('delete empty files and folders', () => { rmdirSync: filepath => { rmdirSyncInvocationCount += 1; }, + readFileSync: () => packageJson, })); underTest._cleanupEmptyFilesAndFolders(targetFilepath); @@ -186,6 +191,7 @@ describe('delete empty files and folders', () => { rmdirSync: filepath => { rmdirSyncInvocationCount += 1; }, + readFileSync: () => packageJson, })); underTest._cleanupEmptyFilesAndFolders(targetFilepath); @@ -224,6 +230,7 @@ describe('delete empty files and folders', () => { readdirInvocationCount += 1; return content; }, + readFileSync: () => packageJson, })); underTest._cleanupEmptyFilesAndFolders(targetFolder); @@ -273,6 +280,7 @@ describe('delete empty files and folders', () => { readdirInvocation.push(filepath); return filepath === targetFolder ? content : emptyContent; }, + readFileSync: () => packageJson, })); underTest._cleanupEmptyFilesAndFolders(targetFolder); @@ -327,6 +335,7 @@ describe('delete empty files and folders', () => { ) : emptyContent; }, + readFileSync: () => packageJson, })); underTest._cleanupEmptyFilesAndFolders(targetFolder); diff --git a/packages/react-native/scripts/codegen/generate-artifacts-executor.js b/packages/react-native/scripts/codegen/generate-artifacts-executor.js index 4853f450b1cf33..bfb5463d214942 100644 --- a/packages/react-native/scripts/codegen/generate-artifacts-executor.js +++ b/packages/react-native/scripts/codegen/generate-artifacts-executor.js @@ -58,7 +58,13 @@ const CORE_LIBRARIES_WITH_OUTPUT_FOLDER = { ), }, }; -const REACT_NATIVE = 'react-native'; + +const packageJsonPath = path.join( + REACT_NATIVE_PACKAGE_ROOT_FOLDER, + 'package.json', +); +const packageJson = JSON.parse(fs.readFileSync(packageJsonPath)); +const REACT_NATIVE = packageJson.name; const MODULES_PROTOCOLS_H_TEMPLATE_PATH = path.join( REACT_NATIVE_PACKAGE_ROOT_FOLDER, From 99ab845a5cf0fe3463ff39b03373b95d4f5c0fac Mon Sep 17 00:00:00 2001 From: Hailey Date: Wed, 25 Sep 2024 04:47:54 -0700 Subject: [PATCH 007/296] convert `NSNull` to `nil` before checking `type` in `readAsDataURL` (#46635) Summary: This issue original arose out of https://github.com/bluesky-social/social-app/issues/5100. Copying the description (with my general understanding of the problem) from the patch PR to here as well. There's a crash that comes up in the following, pretty specific scenario: - Have a response that has an empty body - Do not include a `content-type` header in the response - Set the `x-content-type-options` header to `nosniff` RN handles the response for a request in this block of code: https://github.com/facebook/react-native/blob/303e0ed7641409acf2d852c077f6be426afd7a0c/packages/react-native/Libraries/Blob/RCTBlobManager.mm#L314-L326 Here, we see that values of `nil` - which `[response MIMEType]` will return when no `content-type` is provided in the response and the actual type cannot be determined (https://developer.apple.com/documentation/foundation/nsurlresponse/1411613-mimetype) - gets converted to `NSNull` by `RCTNullIfNil`. When we get back over to `readAsDataURL`, we see that we grab the type from the dictionary and check if its `nil` before calling `length` on the string. https://github.com/facebook/react-native/blob/303e0ed7641409acf2d852c077f6be426afd7a0c/packages/react-native/Libraries/Blob/RCTFileReaderModule.mm#L74-L77 However, this check is dubious, because the value will never actually be `nil`. It will always either be `NSString` or `NSNull` because of the `RCTNullIfNil` call made above and `[RCTConvert NSString]` seems to just return the input if it is `NSNull`. ## Changelog: [IOS] [FIXED] - Convert `NSNull` to `nil` before checking `type` in `readAsDataURL` Pull Request resolved: https://github.com/facebook/react-native/pull/46635 Test Plan: This is a little awkward to test, but essentially this comes up in the following scenario that is described (and "tested" as being fixed by tweaking) in https://github.com/bluesky-social/social-app/issues/5100. I have personally tested by using Cloudflare rules to add/remove that particular header from an empty body response. You could also test this with a little local web server if you want. ### Before https://github.com/user-attachments/assets/deb86c68-2251-4fef-9705-a1c93584e83e ### After https://github.com/user-attachments/assets/9ffab11b-b2c8-4a83-afd6-0a55fed3ae9b Reviewed By: dmytrorykun Differential Revision: D63381947 Pulled By: cipolleschi fbshipit-source-id: b2b4944d998133611592eed8d112faa6195587bd --- .../react-native/Libraries/Blob/RCTFileReaderModule.mm | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/react-native/Libraries/Blob/RCTFileReaderModule.mm b/packages/react-native/Libraries/Blob/RCTFileReaderModule.mm index caa554029f735e..8356a2a8f485a9 100644 --- a/packages/react-native/Libraries/Blob/RCTFileReaderModule.mm +++ b/packages/react-native/Libraries/Blob/RCTFileReaderModule.mm @@ -72,9 +72,10 @@ @implementation RCTFileReaderModule nil); } else { NSString *type = [RCTConvert NSString:blob[@"type"]]; - NSString *text = [NSString stringWithFormat:@"data:%@;base64,%@", - type != nil && [type length] > 0 ? type : @"application/octet-stream", - [data base64EncodedStringWithOptions:0]]; + NSString *text = [NSString + stringWithFormat:@"data:%@;base64,%@", + ![type isEqual:[NSNull null]] && [type length] > 0 ? type : @"application/octet-stream", + [data base64EncodedStringWithOptions:0]]; resolve(text); } From 5bd751bd91ba9ca0fcedbfbb8b6881bb85e0b439 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Wed, 25 Sep 2024 05:51:49 -0700 Subject: [PATCH 008/296] fix: running local tests (#46615) Summary: This PR fixes running local tests. ### Before Tests from Pods/ are executed (including Hermes tests) ![CleanShot 2024-09-24 at 10 09 04@2x](https://github.com/user-attachments/assets/5d766321-1099-4970-bc4d-cf20ec1f5d89) ### After ![CleanShot 2024-09-24 at 10 10 24@2x](https://github.com/user-attachments/assets/356b0db3-9278-4cb3-a216-2ca78b623950) ## Changelog: [INTERNAL] [FIXED] - Make tests run locally Pull Request resolved: https://github.com/facebook/react-native/pull/46615 Test Plan: 1. Run yarn run test (with and without the change). Make sure you have installed pods. Reviewed By: huntie Differential Revision: D63383063 Pulled By: cipolleschi fbshipit-source-id: d7352f80c0fe4efdbb169e1adc4d5f60179f78e8 --- jest.config.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/jest.config.js b/jest.config.js index d4ac1188db346d..5a36fd90a5e975 100644 --- a/jest.config.js +++ b/jest.config.js @@ -11,6 +11,11 @@ const {defaults} = require('jest-config'); +const PODS_LOCATIONS = [ + 'packages/rn-tester/Pods', + 'packages/helloworld/ios/Pods', +]; + module.exports = { transform: { '^.+\\.(bmp|gif|jpg|jpeg|mp4|png|psd|svg|webp)$': @@ -33,6 +38,8 @@ module.exports = { '/packages/react-native/sdks', '/packages/react-native/Libraries/Renderer', '/packages/react-native-test-renderer/src', + '/packages/react-native/sdks/hermes/', + ...PODS_LOCATIONS, ], transformIgnorePatterns: ['node_modules/(?!@react-native/)'], haste: { @@ -40,7 +47,11 @@ module.exports = { platforms: ['ios', 'android'], }, moduleFileExtensions: ['fb.js'].concat(defaults.moduleFileExtensions), - modulePathIgnorePatterns: ['scripts/.*/__fixtures__/'], + modulePathIgnorePatterns: [ + 'scripts/.*/__fixtures__/', + '/packages/react-native/sdks/hermes/', + ...PODS_LOCATIONS, + ], unmockedModulePathPatterns: [ 'node_modules/react/', 'packages/react-native/Libraries/Renderer', From eb7c43965ed8324aa50e5eab4a6184fd73f217e6 Mon Sep 17 00:00:00 2001 From: Blake Friedman Date: Wed, 25 Sep 2024 06:57:00 -0700 Subject: [PATCH 009/296] Add changelog for 0.76.0-rc.2 (#46619) Summary: Add changelog for 0.76.0-rc.2 Changelog: [Internal] - add changelog Pull Request resolved: https://github.com/facebook/react-native/pull/46619 Reviewed By: cipolleschi Differential Revision: D63387314 Pulled By: blakef fbshipit-source-id: c80449ff2043c26a126ec91db39f81093ac2d57f --- CHANGELOG.md | 90 ++++++++++------------------------------------------ 1 file changed, 17 insertions(+), 73 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 595b54f5863798..e087bfbbc7e325 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,74 +1,50 @@ # Changelog -## v0.76.0-rc.1 - -### Breaking +## v0.76.0-rc.2 +### Added +#### iOS specific -#### Android specific +- Fire onMomentumScrollEnd when UIScrollView is removed from window ([b98b9f1fa7](https://github.com/facebook/react-native/commit/b98b9f1fa7717283f368eb182a51d971b8776c80) by [@shubhamguptadream11](https://github.com/shubhamguptadream11)) +### Fixed +- Throttle reload command ([42bad68220](https://github.com/facebook/react-native/commit/42bad68220d288ef2436609ee50ad993c239b362) by [@coado](https://github.com/coado)) #### iOS specific -- Unbreak `RCTHermesInstance` constructor breaking change ([aec6666bfa](https://github.com/facebook/react-native/commit/aec6666bfabf93cde70fa869b3eea68590998dee) by [@tido64](https://github.com/tido64)) - -### Added - -- Unhide new arch layout props ([2d6c59e1d4](https://github.com/facebook/react-native/commit/2d6c59e1d4f6ddb46373ed10c915aed4ce0c030c) by [@NickGerleman](https://github.com/NickGerleman)) - -#### Android specific +- Fixed a crash when navigating away from a screen that contains a scrollView ([c6f32828b9](https://github.com/facebook/react-native/commit/c6f32828b9487381dab27f645aedcdbae9dcbc7e) by [@cipolleschi](https://github.com/cipolleschi)) +- Allow pods mixte type settings on post-install ([1e59f2e3f8](https://github.com/facebook/react-native/commit/1e59f2e3f8f0ab3ee4173bddaa089bbecf61d1eb) by [@MasGaNo](https://github.com/MasGaNo)) +- Add back the BUNDLE_COMMAND ([cf42288181](https://github.com/facebook/react-native/commit/cf422881819decccdd2b486fbb73f2192b9ec522) by [@Vin-Xi](https://github.com/Vin-Xi)) +- Fix SVC for lineBreakModeIOS ([1099c0ccf7](https://github.com/facebook/react-native/commit/1099c0ccf7ea4f5d2e5caafd56b4e92faa367dc6) by [@cipolleschi](https://github.com/cipolleschi)) +## v0.76.0-rc.1 +### Breaking #### iOS specific +- Unbreak `RCTHermesInstance` constructor breaking change ([aec6666bfa](https://github.com/facebook/react-native/commit/aec6666bfabf93cde70fa869b3eea68590998dee) by [@tido64](https://github.com/tido64)) +### Added + +- Unhide new arch layout props ([2d6c59e1d4](https://github.com/facebook/react-native/commit/2d6c59e1d4f6ddb46373ed10c915aed4ce0c030c) by [@NickGerleman](https://github.com/NickGerleman)) ### Changed - AnimatedNode (and its subclasses) once again implement `toJSON()`. ([7bd4a54968](https://github.com/facebook/react-native/commit/7bd4a5496815943b031b68ca46792560d8d798d8) by [@yungsters](https://github.com/yungsters)) - Add official `filter` CSSProperty. ([6b369a40d9](https://github.com/facebook/react-native/commit/6b369a40d98c2bb7f933415f62b1e2b8f4da86ed) by [@jorge-cab](https://github.com/jorge-cab)) - Add official `boxShadow` CSSProperty. ([2241c3146f](https://github.com/facebook/react-native/commit/2241c3146ffbfb8b77f54599b7cebb717537c15a) by [@jorge-cab](https://github.com/jorge-cab)) +- [0.76] Bump Metro to 0.81.0-alpha ([4126ce844d](https://github.com/facebook/react-native/commit/4126ce844d91612d83a1b2118a4859cac5bb480f) by [@robhogan](https://github.com/robhogan)) #### Android specific - Expose jsctooling via prefab ([e91690d929](https://github.com/facebook/react-native/commit/e91690d929d7c8b251964cae282ad8e1d95aa39a) by [@tomekzaw](https://github.com/tomekzaw)) - Expose hermestooling via prefab ([f41af55958](https://github.com/facebook/react-native/commit/f41af55958dfbc39c536d433c3a27db329dd05f1) by [@cortinico](https://github.com/cortinico)) -#### iOS specific - - - -### Deprecated - - - -#### Android specific - - - -#### iOS specific - - - -### Removed - - - -#### Android specific - - - -#### iOS specific - - - ### Fixed - - #### Android specific - Fix Headless Crash `Tried to finish non-existent task with id` ([b4532adad4](https://github.com/facebook/react-native/commit/b4532adad4e38e1237631778f7b8c917a9ccf746) by [@RodolfoGS](https://github.com/RodolfoGS)) @@ -81,38 +57,6 @@ - Fixed warnings when validating SVC ([de39a204c3](https://github.com/facebook/react-native/commit/de39a204c3588a3c02dc2e72464174c75b3a6749) by [@cipolleschi](https://github.com/cipolleschi)) - Solved SVC warnings for RNTester ([fad4a0783b](https://github.com/facebook/react-native/commit/fad4a0783b0a0478c147d9bde2ef9ab082a08297) by [@cipolleschi](https://github.com/cipolleschi)) -### Security - - - -#### Android specific - - - -#### iOS specific - - - -### Unknown - -- Release 0.76.0-rc.1 ([ec9e1718aa](https://github.com/facebook/react-native/commit/ec9e1718aa6179c6f634128fd64c36c9b116fa8f) by [@react-native-bot](https://github.com/react-native-bot)) -- Bump Podfile.lock ([c4714e81d8](https://github.com/facebook/react-native/commit/c4714e81d857845ad9d87b9d49c14eed0db774c0) by [@cipolleschi](https://github.com/cipolleschi)) -- [0.76] Bump Metro to 0.81.0-alpha ([4126ce844d](https://github.com/facebook/react-native/commit/4126ce844d91612d83a1b2118a4859cac5bb480f) by [@robhogan](https://github.com/robhogan)) -- Bump Podfile.lock ([33d175f51b](https://github.com/facebook/react-native/commit/33d175f51bda3f6b773127172d5dde34762725a5) by [@cipolleschi](https://github.com/cipolleschi)) - -#### Android Unknown - - - -#### iOS Unknown - - - -#### Failed to parse - - - - ## v0.76.0-rc.0 ### Breaking From 9ee5fffae0b55bbad2c6c87887f78f4a17c96047 Mon Sep 17 00:00:00 2001 From: Joe Vilches Date: Wed, 25 Sep 2024 13:35:56 -0700 Subject: [PATCH 010/296] Small perf fix for new iOS view clipping (#46629) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46629 If we clipped and had no border or corner radius we would end up hitting this path every time. We can optimize this a bit to avoid that. Changelog: [Internal] Reviewed By: NickGerleman Differential Revision: D63299597 fbshipit-source-id: 90031f964b7669049a4a2efe00a553c888d28cd7 --- .../View/RCTViewComponentView.mm | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 0f817e2e6e2872..04b2ede4f96c96 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -1080,15 +1080,10 @@ - (void)invalidateLayer } // clipping + self.currentContainerView.layer.mask = nil; if (self.currentContainerView.clipsToBounds) { BOOL clipToPaddingBox = ReactNativeFeatureFlags::enableIOSViewClipToPaddingBox(); - if (clipToPaddingBox) { - CALayer *maskLayer = [self createMaskLayer:RCTCGRectFromRect(_layoutMetrics.getPaddingFrame()) - cornerInsets:RCTGetCornerInsets( - RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), - RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths))]; - self.currentContainerView.layer.mask = maskLayer; - } else { + if (!clipToPaddingBox) { if (borderMetrics.borderRadii.isUniform()) { self.currentContainerView.layer.cornerRadius = borderMetrics.borderRadii.topLeft.horizontal; } else { @@ -1110,9 +1105,17 @@ - (void)invalidateLayer subview.layer.mask = [self createMaskLayer:subview.bounds cornerInsets:cornerInsets]; } } + } else if ( + !borderMetrics.borderWidths.isUniform() || borderMetrics.borderWidths.left != 0 || + !borderMetrics.borderRadii.isUniform()) { + CALayer *maskLayer = [self createMaskLayer:RCTCGRectFromRect(_layoutMetrics.getPaddingFrame()) + cornerInsets:RCTGetCornerInsets( + RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), + RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths))]; + self.currentContainerView.layer.mask = maskLayer; + } else { + self.currentContainerView.layer.cornerRadius = borderMetrics.borderRadii.topLeft.horizontal; } - } else { - self.currentContainerView.layer.mask = nil; } } From 8eadb84d32d0b674a4bb0a489123b2260cea51e8 Mon Sep 17 00:00:00 2001 From: Joe Vilches Date: Wed, 25 Sep 2024 15:46:55 -0700 Subject: [PATCH 011/296] Expose box sizing getters and setters in Yoga (#46630) Summary: X-link: https://github.com/facebook/yoga/pull/1701 Pull Request resolved: https://github.com/facebook/react-native/pull/46630 I would like to write some tests for box sizing that will drive a lot of my development as I implement content box. To do that, I need this publicly exposed. Obviously not that ideal since this currently does not do anything. Maybe we can name the value in such a way that its clear it is in development? Changelog: [Internal] Reviewed By: NickGerleman Differential Revision: D63135970 fbshipit-source-id: 7520823bf925364eae45341531e012e80ec92284 --- .../java/com/facebook/yoga/YogaBoxSizing.java | 33 +++++++++++++++ .../java/com/facebook/yoga/YogaNative.java | 2 + .../main/java/com/facebook/yoga/YogaNode.java | 4 ++ .../com/facebook/yoga/YogaNodeJNIBase.java | 8 ++++ .../java/com/facebook/yoga/YogaProps.java | 4 ++ .../first-party/yogajni/jni/YGJNIVanilla.cpp | 7 ++++ .../ReactCommon/yoga/yoga/YGEnums.cpp | 10 +++++ .../ReactCommon/yoga/yoga/YGEnums.h | 5 +++ .../ReactCommon/yoga/yoga/YGNodeStyle.cpp | 9 +++++ .../ReactCommon/yoga/yoga/YGNodeStyle.h | 3 ++ .../ReactCommon/yoga/yoga/enums/BoxSizing.h | 40 +++++++++++++++++++ .../ReactCommon/yoga/yoga/style/Style.h | 9 +++++ 12 files changed, 134 insertions(+) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaBoxSizing.java create mode 100644 packages/react-native/ReactCommon/yoga/yoga/enums/BoxSizing.h diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaBoxSizing.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaBoxSizing.java new file mode 100644 index 00000000000000..a8c08f6543691d --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaBoxSizing.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// @generated by enums.py + +package com.facebook.yoga; + +public enum YogaBoxSizing { + CONTENT_BOX(0), + BORDER_BOX(1); + + private final int mIntValue; + + YogaBoxSizing(int intValue) { + mIntValue = intValue; + } + + public int intValue() { + return mIntValue; + } + + public static YogaBoxSizing fromInt(int value) { + switch (value) { + case 0: return CONTENT_BOX; + case 1: return BORDER_BOX; + default: throw new IllegalArgumentException("Unknown enum value: " + value); + } + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaNative.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaNative.java index 0ab5391331a326..89ef5ef16f536f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaNative.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaNative.java @@ -56,6 +56,8 @@ public class YogaNative { static native void jni_YGNodeStyleSetAlignContentJNI(long nativePointer, int alignContent); static native int jni_YGNodeStyleGetPositionTypeJNI(long nativePointer); static native void jni_YGNodeStyleSetPositionTypeJNI(long nativePointer, int positionType); + static native int jni_YGNodeStyleGetBoxSizingJNI(long nativePointer); + static native void jni_YGNodeStyleSetBoxSizingJNI(long nativePointer, int boxSizing); static native int jni_YGNodeStyleGetFlexWrapJNI(long nativePointer); static native void jni_YGNodeStyleSetFlexWrapJNI(long nativePointer, int wrapType); static native int jni_YGNodeStyleGetOverflowJNI(long nativePointer); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaNode.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaNode.java index c83b57788081e9..ba076846759607 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaNode.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaNode.java @@ -88,6 +88,10 @@ public interface Inputs { public abstract void setPositionType(YogaPositionType positionType); + public abstract YogaBoxSizing getBoxSizing(); + + public abstract void setBoxSizing(YogaBoxSizing boxSizing); + public abstract YogaWrap getWrap(); public abstract void setWrap(YogaWrap flexWrap); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaNodeJNIBase.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaNodeJNIBase.java index 75a318984e1cc4..6e4f96982a5b3c 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaNodeJNIBase.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaNodeJNIBase.java @@ -299,6 +299,14 @@ public void setPositionType(YogaPositionType positionType) { YogaNative.jni_YGNodeStyleSetPositionTypeJNI(mNativePointer, positionType.intValue()); } + public YogaBoxSizing getBoxSizing() { + return YogaBoxSizing.fromInt(YogaNative.jni_YGNodeStyleGetBoxSizingJNI(mNativePointer)); + } + + public void setBoxSizing(YogaBoxSizing boxSizing) { + YogaNative.jni_YGNodeStyleSetBoxSizingJNI(mNativePointer, boxSizing.intValue()); + } + public YogaWrap getWrap() { return YogaWrap.fromInt(YogaNative.jni_YGNodeStyleGetFlexWrapJNI(mNativePointer)); } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaProps.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaProps.java index 398bc8f026a0eb..c596c6a7c1c817 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaProps.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaProps.java @@ -105,6 +105,8 @@ public interface YogaProps { void setBaselineFunction(YogaBaselineFunction yogaBaselineFunction); + void setBoxSizing(YogaBoxSizing boxSizing); + /* Getters */ YogaValue getWidth(); @@ -148,4 +150,6 @@ public interface YogaProps { YogaValue getPosition(YogaEdge edge); float getBorder(YogaEdge edge); + + YogaBoxSizing getBoxSizing(); } diff --git a/packages/react-native/ReactAndroid/src/main/jni/first-party/yogajni/jni/YGJNIVanilla.cpp b/packages/react-native/ReactAndroid/src/main/jni/first-party/yogajni/jni/YGJNIVanilla.cpp index 9f4e211bd059cd..6b8ed14bd694f7 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/first-party/yogajni/jni/YGJNIVanilla.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/first-party/yogajni/jni/YGJNIVanilla.cpp @@ -475,6 +475,7 @@ YG_NODE_JNI_STYLE_PROP(jint, YGAlign, AlignItems); YG_NODE_JNI_STYLE_PROP(jint, YGAlign, AlignSelf); YG_NODE_JNI_STYLE_PROP(jint, YGAlign, AlignContent); YG_NODE_JNI_STYLE_PROP(jint, YGPositionType, PositionType); +YG_NODE_JNI_STYLE_PROP(jint, YGBoxSizing, BoxSizing); YG_NODE_JNI_STYLE_PROP(jint, YGWrap, FlexWrap); YG_NODE_JNI_STYLE_PROP(jint, YGOverflow, Overflow); YG_NODE_JNI_STYLE_PROP(jint, YGDisplay, Display); @@ -819,6 +820,12 @@ static JNINativeMethod methods[] = { {"jni_YGNodeStyleSetPositionTypeJNI", "(JI)V", (void*)jni_YGNodeStyleSetPositionTypeJNI}, + {"jni_YGNodeStyleGetBoxSizingJNI", + "(J)I", + (void*)jni_YGNodeStyleGetBoxSizingJNI}, + {"jni_YGNodeStyleSetBoxSizingJNI", + "(JI)V", + (void*)jni_YGNodeStyleSetBoxSizingJNI}, {"jni_YGNodeStyleGetFlexWrapJNI", "(J)I", (void*)jni_YGNodeStyleGetFlexWrapJNI}, diff --git a/packages/react-native/ReactCommon/yoga/yoga/YGEnums.cpp b/packages/react-native/ReactCommon/yoga/yoga/YGEnums.cpp index 222a5d12a4259d..1d4975b138aa54 100644 --- a/packages/react-native/ReactCommon/yoga/yoga/YGEnums.cpp +++ b/packages/react-native/ReactCommon/yoga/yoga/YGEnums.cpp @@ -33,6 +33,16 @@ const char* YGAlignToString(const YGAlign value) { return "unknown"; } +const char* YGBoxSizingToString(const YGBoxSizing value) { + switch (value) { + case YGBoxSizingContentBox: + return "content-box"; + case YGBoxSizingBorderBox: + return "border-box"; + } + return "unknown"; +} + const char* YGDimensionToString(const YGDimension value) { switch (value) { case YGDimensionWidth: diff --git a/packages/react-native/ReactCommon/yoga/yoga/YGEnums.h b/packages/react-native/ReactCommon/yoga/yoga/YGEnums.h index 44335277fc0d1f..8a2161a9070a21 100644 --- a/packages/react-native/ReactCommon/yoga/yoga/YGEnums.h +++ b/packages/react-native/ReactCommon/yoga/yoga/YGEnums.h @@ -24,6 +24,11 @@ YG_ENUM_DECL( YGAlignSpaceAround, YGAlignSpaceEvenly) +YG_ENUM_DECL( + YGBoxSizing, + YGBoxSizingContentBox, + YGBoxSizingBorderBox) + YG_ENUM_DECL( YGDimension, YGDimensionWidth, diff --git a/packages/react-native/ReactCommon/yoga/yoga/YGNodeStyle.cpp b/packages/react-native/ReactCommon/yoga/yoga/YGNodeStyle.cpp index 67e6e42e77e831..d2a10b0591a1d1 100644 --- a/packages/react-native/ReactCommon/yoga/yoga/YGNodeStyle.cpp +++ b/packages/react-native/ReactCommon/yoga/yoga/YGNodeStyle.cpp @@ -296,6 +296,15 @@ float YGNodeStyleGetAspectRatio(const YGNodeConstRef node) { return op.isUndefined() ? YGUndefined : op.unwrap(); } +void YGNodeStyleSetBoxSizing(YGNodeRef node, YGBoxSizing boxSizing) { + updateStyle<&Style::boxSizing, &Style::setBoxSizing>( + node, scopedEnum(boxSizing)); +} + +YGBoxSizing YGNodeStyleGetBoxSizing(const YGNodeConstRef node) { + return unscopedEnum(resolveRef(node)->style().boxSizing()); +} + void YGNodeStyleSetWidth(YGNodeRef node, float points) { updateStyle<&Style::dimension, &Style::setDimension>( node, Dimension::Width, value::points(points)); diff --git a/packages/react-native/ReactCommon/yoga/yoga/YGNodeStyle.h b/packages/react-native/ReactCommon/yoga/yoga/YGNodeStyle.h index fad71e4f0771c8..2746a4a00abb98 100644 --- a/packages/react-native/ReactCommon/yoga/yoga/YGNodeStyle.h +++ b/packages/react-native/ReactCommon/yoga/yoga/YGNodeStyle.h @@ -95,6 +95,9 @@ YG_EXPORT void YGNodeStyleSetGapPercent(YGNodeRef node, YGGutter gutter, float gapLength); YG_EXPORT float YGNodeStyleGetGap(YGNodeConstRef node, YGGutter gutter); +YG_EXPORT void YGNodeStyleSetBoxSizing(YGNodeRef node, YGBoxSizing boxSizing); +YG_EXPORT YGBoxSizing YGNodeStyleGetBoxSizing(YGNodeConstRef node); + YG_EXPORT void YGNodeStyleSetWidth(YGNodeRef node, float width); YG_EXPORT void YGNodeStyleSetWidthPercent(YGNodeRef node, float width); YG_EXPORT void YGNodeStyleSetWidthAuto(YGNodeRef node); diff --git a/packages/react-native/ReactCommon/yoga/yoga/enums/BoxSizing.h b/packages/react-native/ReactCommon/yoga/yoga/enums/BoxSizing.h new file mode 100644 index 00000000000000..1b6b9a7cd22369 --- /dev/null +++ b/packages/react-native/ReactCommon/yoga/yoga/enums/BoxSizing.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// @generated by enums.py +// clang-format off +#pragma once + +#include +#include +#include + +namespace facebook::yoga { + +enum class BoxSizing : uint8_t { + ContentBox = YGBoxSizingContentBox, + BorderBox = YGBoxSizingBorderBox, +}; + +template <> +constexpr int32_t ordinalCount() { + return 2; +} + +constexpr BoxSizing scopedEnum(YGBoxSizing unscoped) { + return static_cast(unscoped); +} + +constexpr YGBoxSizing unscopedEnum(BoxSizing scoped) { + return static_cast(scoped); +} + +inline const char* toString(BoxSizing e) { + return YGBoxSizingToString(unscopedEnum(e)); +} + +} // namespace facebook::yoga diff --git a/packages/react-native/ReactCommon/yoga/yoga/style/Style.h b/packages/react-native/ReactCommon/yoga/yoga/style/Style.h index 141540cf5ed497..f1600ce070f973 100644 --- a/packages/react-native/ReactCommon/yoga/yoga/style/Style.h +++ b/packages/react-native/ReactCommon/yoga/yoga/style/Style.h @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -206,6 +207,13 @@ class YG_EXPORT Style { value == 0.0f || std::isinf(value.unwrap()) ? FloatOptional{} : value); } + BoxSizing boxSizing() const { + return boxSizing_; + } + void setBoxSizing(BoxSizing value) { + boxSizing_ = value; + } + bool horizontalInsetsDefined() const { return position_[yoga::to_underlying(Edge::Left)].isDefined() || position_[yoga::to_underlying(Edge::Right)].isDefined() || @@ -675,6 +683,7 @@ class YG_EXPORT Style { Wrap flexWrap_ : bitCount() = Wrap::NoWrap; Overflow overflow_ : bitCount() = Overflow::Visible; Display display_ : bitCount() = Display::Flex; + BoxSizing boxSizing_ : bitCount() = BoxSizing::BorderBox; StyleValueHandle flex_{}; StyleValueHandle flexGrow_{}; From 1787726bf9e8cf58f5ccdbe42fad4d54f0081925 Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Wed, 25 Sep 2024 19:28:05 -0700 Subject: [PATCH 012/296] Re-enable integration tests (#46639) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46639 These tests were skipped when we were switching to component stacks, which also hid a bug later in the stack. Re-enable them. Changelog: [Internal] Reviewed By: javache Differential Revision: D63349616 fbshipit-source-id: ccde7d5bb3fcd9a27adf4af2068a160f02f7432a --- .../__tests__/LogBox-integration-test.js | 91 ++++++++++++++----- 1 file changed, 66 insertions(+), 25 deletions(-) diff --git a/packages/react-native/Libraries/LogBox/__tests__/LogBox-integration-test.js b/packages/react-native/Libraries/LogBox/__tests__/LogBox-integration-test.js index 07ea7507d77752..5887e772f130bf 100644 --- a/packages/react-native/Libraries/LogBox/__tests__/LogBox-integration-test.js +++ b/packages/react-native/Libraries/LogBox/__tests__/LogBox-integration-test.js @@ -18,13 +18,13 @@ const LogBoxData = require('../Data/LogBoxData'); const TestRenderer = require('react-test-renderer'); const installLogBox = () => { - const LogBox = require('../LogBox'); + const LogBox = require('../LogBox').default; LogBox.install(); }; const uninstallLogBox = () => { - const LogBox = require('../LogBox'); + const LogBox = require('../LogBox').default; LogBox.uninstall(); }; @@ -46,10 +46,9 @@ const cleanLog = logs => { }); }; -// TODO(T71117418): Re-enable skipped LogBox integration tests once React component -// stack frames are the same internally and in open source. -// eslint-disable-next-line jest/no-disabled-tests -describe.skip('LogBox', () => { +// TODO: we can remove all the symetric matchers once OSS lands component stack frames. +// For now, the component stack parsing differs in ways we can't easily detect in this test. +describe('LogBox', () => { const {error, warn} = console; const mockError = jest.fn(); const mockWarn = jest.fn(); @@ -57,10 +56,10 @@ describe.skip('LogBox', () => { beforeEach(() => { jest.resetModules(); jest.restoreAllMocks(); + jest.spyOn(console, 'error').mockImplementation(() => {}); mockError.mockClear(); mockWarn.mockClear(); - (console: any).error = mockError; (console: any).warn = mockWarn; }); @@ -79,7 +78,10 @@ describe.skip('LogBox', () => { // so we can assert on what React logs. jest.spyOn(console, 'error'); - const output = TestRenderer.create(); + let output; + TestRenderer.act(() => { + output = TestRenderer.create(); + }); // The key error should always be the highest severity. // In LogBox, we expect these errors to: @@ -88,16 +90,37 @@ describe.skip('LogBox', () => { // - Pass to console.error, with a "Warning" prefix so it does not pop a RedBox. expect(output).toBeDefined(); expect(mockWarn).not.toBeCalled(); - expect(console.error.mock.calls[0].map(cleanPath)).toMatchSnapshot( - 'Log sent from React', - ); - expect(cleanLog(spy.mock.calls[0])).toMatchSnapshot('Log added to LogBox'); - expect(mockError.mock.calls[0].map(cleanPath)).toMatchSnapshot( - 'Log passed to console error', - ); + expect(console.error).toBeCalledTimes(1); + expect(console.error.mock.calls[0].map(cleanPath)).toEqual([ + 'Each child in a list should have a unique "key" prop.%s%s See https://react.dev/link/warning-keys for more information.%s', + '\n\nCheck the render method of `DoesNotUseKey`.', + '', + expect.stringMatching('at DoesNotUseKey'), + ]); + expect(spy).toHaveBeenCalledWith({ + level: 'warn', + category: expect.stringContaining( + 'Warning: Each child in a list should have a unique', + ), + componentStack: expect.anything(), + componentStackType: 'stack', + message: { + content: + 'Warning: Each child in a list should have a unique "key" prop.\n\nCheck the render method of `DoesNotUseKey`. See https://react.dev/link/warning-keys for more information.', + substitutions: [ + {length: 45, offset: 62}, + {length: 0, offset: 107}, + ], + }, + }); // The Warning: prefix is added due to a hack in LogBox to prevent double logging. - expect(mockError.mock.calls[0][0].startsWith('Warning: ')).toBe(true); + // We also interpolate the string before passing to the underlying console method. + expect(mockError.mock.calls[0]).toEqual([ + expect.stringMatching( + 'Warning: Each child in a list should have a unique "key" prop.\n\nCheck the render method of `DoesNotUseKey`. See https://react.dev/link/warning-keys for more information.\n at ', + ), + ]); }); it('integrates with React and handles a fragment warning in LogBox', () => { @@ -108,7 +131,10 @@ describe.skip('LogBox', () => { // so we can assert on what React logs. jest.spyOn(console, 'error'); - const output = TestRenderer.create(); + let output; + TestRenderer.act(() => { + output = TestRenderer.create(); + }); // The fragment warning is not as severe. For this warning we don't want to // pop open a dialog, so we show a collapsed error UI. @@ -118,15 +144,30 @@ describe.skip('LogBox', () => { // - Pass to console.error, with a "Warning" prefix so it does not pop a RedBox. expect(output).toBeDefined(); expect(mockWarn).not.toBeCalled(); - expect(console.error.mock.calls[0].map(cleanPath)).toMatchSnapshot( - 'Log sent from React', - ); - expect(cleanLog(spy.mock.calls[0])).toMatchSnapshot('Log added to LogBox'); - expect(mockError.mock.calls[0].map(cleanPath)).toMatchSnapshot( - 'Log passed to console error', - ); + expect(console.error).toBeCalledTimes(1); + expect(console.error.mock.calls[0].map(cleanPath)).toEqual([ + 'Invalid prop `%s` supplied to `React.Fragment`. React.Fragment can only have `key` and `children` props.%s', + 'invalid', + expect.stringMatching('at FragmentWithProp'), + ]); + expect(spy).toHaveBeenCalledWith({ + level: 'warn', + category: expect.stringContaining('Warning: Invalid prop'), + componentStack: expect.anything(), + componentStackType: expect.stringMatching(/(stack|legacy)/), + message: { + content: + 'Warning: Invalid prop `invalid` supplied to `React.Fragment`. React.Fragment can only have `key` and `children` props.', + substitutions: [{length: 7, offset: 23}], + }, + }); // The Warning: prefix is added due to a hack in LogBox to prevent double logging. - expect(mockError.mock.calls[0][0].startsWith('Warning: ')).toBe(true); + // We also interpolate the string before passing to the underlying console method. + expect(mockError.mock.calls[0]).toEqual([ + expect.stringMatching( + 'Warning: Invalid prop `invalid` supplied to `React.Fragment`. React.Fragment can only have `key` and `children` props.\n at FragmentWithProp', + ), + ]); }); }); From 2f85e28cdef87f59fd601a61d5726095b600a556 Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Wed, 25 Sep 2024 19:28:05 -0700 Subject: [PATCH 013/296] Add integration tests for console errors + ExceptionManager (#46636) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46636 Adds more integration tests for LogBox (currently incorrect, but fixed in a later diff). Changelog: [Internal] Reviewed By: javache Differential Revision: D63349614 fbshipit-source-id: 8f5c6545b48a1ed18aea08d4ecbecd7a6b9fa05a --- .../__tests__/LogBox-integration-test.js | 126 +++++++++++++++--- .../__fixtures__/ReactWarningFixtures.js | 24 ++++ 2 files changed, 129 insertions(+), 21 deletions(-) diff --git a/packages/react-native/Libraries/LogBox/__tests__/LogBox-integration-test.js b/packages/react-native/Libraries/LogBox/__tests__/LogBox-integration-test.js index 5887e772f130bf..b6583f74a4a113 100644 --- a/packages/react-native/Libraries/LogBox/__tests__/LogBox-integration-test.js +++ b/packages/react-native/Libraries/LogBox/__tests__/LogBox-integration-test.js @@ -11,15 +11,18 @@ import { DoesNotUseKey, FragmentWithProp, + ManualConsoleError, + ManualConsoleErrorWithStack, } from './__fixtures__/ReactWarningFixtures'; import * as React from 'react'; const LogBoxData = require('../Data/LogBoxData'); const TestRenderer = require('react-test-renderer'); +const ExceptionsManager = require('../../Core/ExceptionsManager.js'); + const installLogBox = () => { const LogBox = require('../LogBox').default; - LogBox.install(); }; @@ -28,24 +31,6 @@ const uninstallLogBox = () => { LogBox.uninstall(); }; -const BEFORE_SLASH_RE = /(?:\/[a-zA-Z]+\/)(.+?)(?:\/.+)\//; - -const cleanPath = message => { - return message.replace(BEFORE_SLASH_RE, '/path/to/'); -}; - -const cleanLog = logs => { - return logs.map(log => { - return { - ...log, - componentStack: log.componentStack.map(stack => ({ - ...stack, - fileName: cleanPath(stack.fileName), - })), - }; - }); -}; - // TODO: we can remove all the symetric matchers once OSS lands component stack frames. // For now, the component stack parsing differs in ways we can't easily detect in this test. describe('LogBox', () => { @@ -60,6 +45,10 @@ describe('LogBox', () => { mockError.mockClear(); mockWarn.mockClear(); + // Reset ExceptionManager patching. + if (console._errorOriginal) { + console._errorOriginal = null; + } (console: any).error = mockError; (console: any).warn = mockWarn; }); @@ -91,7 +80,7 @@ describe('LogBox', () => { expect(output).toBeDefined(); expect(mockWarn).not.toBeCalled(); expect(console.error).toBeCalledTimes(1); - expect(console.error.mock.calls[0].map(cleanPath)).toEqual([ + expect(console.error.mock.calls[0]).toEqual([ 'Each child in a list should have a unique "key" prop.%s%s See https://react.dev/link/warning-keys for more information.%s', '\n\nCheck the render method of `DoesNotUseKey`.', '', @@ -145,7 +134,7 @@ describe('LogBox', () => { expect(output).toBeDefined(); expect(mockWarn).not.toBeCalled(); expect(console.error).toBeCalledTimes(1); - expect(console.error.mock.calls[0].map(cleanPath)).toEqual([ + expect(console.error.mock.calls[0]).toEqual([ 'Invalid prop `%s` supplied to `React.Fragment`. React.Fragment can only have `key` and `children` props.%s', 'invalid', expect.stringMatching('at FragmentWithProp'), @@ -170,4 +159,99 @@ describe('LogBox', () => { ), ]); }); + + it('handles a manual console.error without a component stack in LogBox', () => { + const LogBox = require('../LogBox').default; + const spy = jest.spyOn(LogBox, 'addException'); + installLogBox(); + + // console.error handling depends on installing the ExceptionsManager error reporter. + ExceptionsManager.installConsoleErrorReporter(); + + // Spy console.error after LogBox is installed + // so we can assert on what React logs. + jest.spyOn(console, 'error'); + + let output; + TestRenderer.act(() => { + output = TestRenderer.create(); + }); + + // Manual console errors should show a collapsed error dialog. + // When there is no component stack, we expect these errors to: + // - Go to the LogBox patch and fall through to console.error. + // - Get picked up by the ExceptionsManager console.error override. + // - Get passed back to LogBox via addException (non-fatal). + expect(output).toBeDefined(); + expect(mockWarn).not.toBeCalled(); + expect(spy).toBeCalledTimes(1); + expect(console.error).toBeCalledTimes(1); + expect(console.error.mock.calls[0]).toEqual(['Manual console error']); + expect(spy).toHaveBeenCalledWith({ + id: 1, + isComponentError: false, + isFatal: false, + name: 'console.error', + originalMessage: 'Manual console error', + message: 'console.error: Manual console error', + extraData: expect.anything(), + componentStack: null, + stack: expect.anything(), + }); + + // No Warning: prefix is added due since this is falling through. + expect(mockError.mock.calls[0]).toEqual(['Manual console error']); + }); + + it('handles a manual console.error with a component stack in LogBox', () => { + const spy = jest.spyOn(LogBoxData, 'addLog'); + installLogBox(); + + // console.error handling depends on installing the ExceptionsManager error reporter. + ExceptionsManager.installConsoleErrorReporter(); + + // Spy console.error after LogBox is installed + // so we can assert on what React logs. + jest.spyOn(console, 'error'); + + let output; + TestRenderer.act(() => { + output = TestRenderer.create(); + }); + + // Manual console errors should show a collapsed error dialog. + // When there is a component stack, we expect these errors to: + // - Go to the LogBox patch and be detected as a React error. + // - Check the warning filter to see if there is a fiter setting. + // - Call console.error with the parsed error. + // - Get picked up by ExceptionsManager console.error override. + // - Log to console.error. + expect(output).toBeDefined(); + expect(mockWarn).not.toBeCalled(); + expect(console.error).toBeCalledTimes(1); + expect(spy).toBeCalledTimes(1); + expect(console.error.mock.calls[0]).toEqual([ + expect.stringContaining( + 'Manual console error\n at ManualConsoleErrorWithStack', + ), + ]); + expect(spy).toHaveBeenCalledWith({ + level: 'warn', + category: expect.stringContaining('Warning: Manual console error'), + componentStack: expect.anything(), + componentStackType: 'stack', + message: { + content: 'Warning: Manual console error', + substitutions: [], + }, + }); + + // The Warning: prefix is added due to a hack in LogBox to prevent double logging. + // We also interpolate the string before passing to the underlying console method. + expect(mockError.mock.calls[0]).toEqual([ + expect.stringMatching( + 'Warning: Manual console error\n at ManualConsoleErrorWithStack', + ), + ]); + }); }); diff --git a/packages/react-native/Libraries/LogBox/__tests__/__fixtures__/ReactWarningFixtures.js b/packages/react-native/Libraries/LogBox/__tests__/__fixtures__/ReactWarningFixtures.js index 51b85b1fbe1aef..2d13e41dfe0683 100644 --- a/packages/react-native/Libraries/LogBox/__tests__/__fixtures__/ReactWarningFixtures.js +++ b/packages/react-native/Libraries/LogBox/__tests__/__fixtures__/ReactWarningFixtures.js @@ -30,3 +30,27 @@ export const FragmentWithProp = () => { ); }; + +export const ManualConsoleError = () => { + console.error('Manual console error'); + return ( + + {['foo', 'bar'].map(item => ( + {item} + ))} + + ); +}; + +export const ManualConsoleErrorWithStack = () => { + console.error( + 'Manual console error\n at ManualConsoleErrorWithStack (/path/to/ManualConsoleErrorWithStack:30:175)\n at TestApp', + ); + return ( + + {['foo', 'bar'].map(item => ( + {item} + ))} + + ); +}; From e9ec9fbde480655b2c5a5a9b89e33485fb1335ca Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Wed, 25 Sep 2024 19:28:05 -0700 Subject: [PATCH 014/296] Refactor LogBox tests to spies (#46638) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46638 This is annoying, but in the next diff that fixes a bug I need to test using the default warning filter instead of a mock (really, all this mocking is terrible, idk why I did it this way). Unfortunately, in Jest you can't just reset mocks from `jest.mock`, `restoreMocks` only resets spies and not mocks (wild right). So in this diff I converted all the `jest.mock` calls to `jest.spyOn`. I also corrected some of the mocks that require `monitorEvent: 'warning',` like the warning filter sets. I also added a test that works without the fix. Changelog: [Internal] Reviewed By: javache Differential Revision: D63349615 fbshipit-source-id: 4f2a5a8800c8fe1a10e3613d3c2d0ed02fca773e --- .../Libraries/LogBox/__tests__/LogBox-test.js | 121 +++++++++++++----- 1 file changed, 91 insertions(+), 30 deletions(-) diff --git a/packages/react-native/Libraries/LogBox/__tests__/LogBox-test.js b/packages/react-native/Libraries/LogBox/__tests__/LogBox-test.js index be5ea785bbb5e1..b44c6131ffb376 100644 --- a/packages/react-native/Libraries/LogBox/__tests__/LogBox-test.js +++ b/packages/react-native/Libraries/LogBox/__tests__/LogBox-test.js @@ -13,6 +13,7 @@ const LogBoxData = require('../Data/LogBoxData'); const LogBox = require('../LogBox').default; +const ExceptionsManager = require('../../Core/ExceptionsManager.js'); declare var console: any; @@ -34,15 +35,18 @@ describe('LogBox', () => { beforeEach(() => { jest.resetModules(); + jest.restoreAllMocks(); console.error = jest.fn(); - console.log = jest.fn(); console.warn = jest.fn(); }); afterEach(() => { LogBox.uninstall(); + // Reset ExceptionManager patching. + if (console._errorOriginal) { + console._errorOriginal = null; + } console.error = error; - console.log = log; console.warn = warn; }); @@ -95,7 +99,7 @@ describe('LogBox', () => { }); it('registers warnings', () => { - jest.mock('../Data/LogBoxData'); + jest.spyOn(LogBoxData, 'addLog'); LogBox.install(); @@ -105,13 +109,14 @@ describe('LogBox', () => { }); it('reports a LogBox exception if we fail to add warnings', () => { - jest.mock('../Data/LogBoxData'); - const mockError = new Error('Simulated error'); + jest.spyOn(LogBoxData, 'addLog'); + jest.spyOn(LogBoxData, 'reportLogBoxError'); // Picking a random implementation detail to simulate throwing. - (LogBoxData.isMessageIgnored: any).mockImplementation(() => { + jest.spyOn(LogBoxData, 'isMessageIgnored').mockImplementation(() => { throw mockError; }); + const mockError = new Error('Simulated error'); LogBox.install(); @@ -123,7 +128,8 @@ describe('LogBox', () => { }); it('only registers errors beginning with "Warning: "', () => { - jest.mock('../Data/LogBoxData'); + jest.spyOn(LogBoxData, 'addLog'); + jest.spyOn(LogBoxData, 'checkWarningFilter'); LogBox.install(); @@ -133,7 +139,8 @@ describe('LogBox', () => { }); it('registers react errors with the formatting from filter', () => { - jest.mock('../Data/LogBoxData'); + jest.spyOn(LogBoxData, 'addLog'); + jest.spyOn(LogBoxData, 'checkWarningFilter'); mockFilterResult({ finalFormat: 'Custom format', @@ -157,7 +164,8 @@ describe('LogBox', () => { }); it('registers errors with component stack as errors by default', () => { - jest.mock('../Data/LogBoxData'); + jest.spyOn(LogBoxData, 'addLog'); + jest.spyOn(LogBoxData, 'checkWarningFilter'); mockFilterResult({}); @@ -174,7 +182,8 @@ describe('LogBox', () => { }); it('registers errors with component stack as errors by default if not found in warning filter', () => { - jest.mock('../Data/LogBoxData'); + jest.spyOn(LogBoxData, 'addLog'); + jest.spyOn(LogBoxData, 'checkWarningFilter'); mockFilterResult({ monitorEvent: 'warning_unhandled', @@ -193,10 +202,12 @@ describe('LogBox', () => { }); it('registers errors with component stack with legacy suppression as warning', () => { - jest.mock('../Data/LogBoxData'); + jest.spyOn(LogBoxData, 'addLog'); + jest.spyOn(LogBoxData, 'checkWarningFilter'); mockFilterResult({ suppressDialog_LEGACY: true, + monitorEvent: 'warning', }); LogBox.install(); @@ -211,10 +222,12 @@ describe('LogBox', () => { }); it('registers errors with component stack and a forced dialog as fatals', () => { - jest.mock('../Data/LogBoxData'); + jest.spyOn(LogBoxData, 'addLog'); + jest.spyOn(LogBoxData, 'checkWarningFilter'); mockFilterResult({ forceDialogImmediately: true, + monitorEvent: 'warning', }); LogBox.install(); @@ -229,7 +242,8 @@ describe('LogBox', () => { }); it('registers warning module errors with the formatting from filter', () => { - jest.mock('../Data/LogBoxData'); + jest.spyOn(LogBoxData, 'addLog'); + jest.spyOn(LogBoxData, 'checkWarningFilter'); mockFilterResult({ finalFormat: 'Custom format', @@ -248,7 +262,8 @@ describe('LogBox', () => { }); it('registers warning module errors as errors by default', () => { - jest.mock('../Data/LogBoxData'); + jest.spyOn(LogBoxData, 'addLog'); + jest.spyOn(LogBoxData, 'checkWarningFilter'); mockFilterResult({}); @@ -262,10 +277,12 @@ describe('LogBox', () => { }); it('registers warning module errors with only legacy suppression as warning', () => { - jest.mock('../Data/LogBoxData'); + jest.spyOn(LogBoxData, 'addLog'); + jest.spyOn(LogBoxData, 'checkWarningFilter'); mockFilterResult({ suppressDialog_LEGACY: true, + monitorEvent: 'warning', }); LogBox.install(); @@ -277,10 +294,12 @@ describe('LogBox', () => { }); it('registers warning module errors with a forced dialog as fatals', () => { - jest.mock('../Data/LogBoxData'); + jest.spyOn(LogBoxData, 'addLog'); + jest.spyOn(LogBoxData, 'checkWarningFilter'); mockFilterResult({ forceDialogImmediately: true, + monitorEvent: 'warning', }); LogBox.install(); @@ -292,10 +311,12 @@ describe('LogBox', () => { }); it('ignores warning module errors that are suppressed completely', () => { - jest.mock('../Data/LogBoxData'); + jest.spyOn(LogBoxData, 'addLog'); + jest.spyOn(LogBoxData, 'checkWarningFilter'); mockFilterResult({ suppressCompletely: true, + monitorEvent: 'warning', }); LogBox.install(); @@ -305,10 +326,11 @@ describe('LogBox', () => { }); it('ignores warning module errors that are pattern ignored', () => { - jest.mock('../Data/LogBoxData'); + jest.spyOn(LogBoxData, 'checkWarningFilter'); + jest.spyOn(LogBoxData, 'isMessageIgnored').mockReturnValue(true); + jest.spyOn(LogBoxData, 'addLog'); mockFilterResult({}); - (LogBoxData.isMessageIgnored: any).mockReturnValue(true); LogBox.install(); @@ -317,10 +339,11 @@ describe('LogBox', () => { }); it('ignores warning module errors that are from LogBox itself', () => { - jest.mock('../Data/LogBoxData'); + jest.spyOn(LogBoxData, 'checkWarningFilter'); + jest.spyOn(LogBoxData, 'isLogBoxErrorMessage').mockReturnValue(true); + jest.spyOn(LogBoxData, 'addLog'); mockFilterResult({}); - (LogBoxData.isLogBoxErrorMessage: any).mockReturnValue(true); LogBox.install(); @@ -329,8 +352,9 @@ describe('LogBox', () => { }); it('ignores logs that are pattern ignored"', () => { - jest.mock('../Data/LogBoxData'); - (LogBoxData.isMessageIgnored: any).mockReturnValue(true); + jest.spyOn(LogBoxData, 'checkWarningFilter'); + jest.spyOn(LogBoxData, 'isMessageIgnored').mockReturnValue(true); + jest.spyOn(LogBoxData, 'addLog'); LogBox.install(); @@ -339,8 +363,8 @@ describe('LogBox', () => { }); it('does not add logs that are from LogBox itself"', () => { - jest.mock('../Data/LogBoxData'); - (LogBoxData.isLogBoxErrorMessage: any).mockReturnValue(true); + jest.spyOn(LogBoxData, 'isLogBoxErrorMessage').mockReturnValue(true); + jest.spyOn(LogBoxData, 'addLog'); LogBox.install(); @@ -349,7 +373,7 @@ describe('LogBox', () => { }); it('ignores logs starting with "(ADVICE)"', () => { - jest.mock('../Data/LogBoxData'); + jest.spyOn(LogBoxData, 'addLog'); LogBox.install(); @@ -358,7 +382,7 @@ describe('LogBox', () => { }); it('does not ignore logs formatted to start with "(ADVICE)"', () => { - jest.mock('../Data/LogBoxData'); + jest.spyOn(LogBoxData, 'addLog'); LogBox.install(); @@ -376,7 +400,7 @@ describe('LogBox', () => { }); it('ignores console methods after uninstalling', () => { - jest.mock('../Data/LogBoxData'); + jest.spyOn(LogBoxData, 'addLog'); LogBox.install(); LogBox.uninstall(); @@ -389,7 +413,7 @@ describe('LogBox', () => { }); it('does not add logs after uninstalling', () => { - jest.mock('../Data/LogBoxData'); + jest.spyOn(LogBoxData, 'addLog'); LogBox.install(); LogBox.uninstall(); @@ -406,7 +430,7 @@ describe('LogBox', () => { }); it('does not add exceptions after uninstalling', () => { - jest.mock('../Data/LogBoxData'); + jest.spyOn(LogBoxData, 'addException'); LogBox.install(); LogBox.uninstall(); @@ -482,4 +506,41 @@ describe('LogBox', () => { 'Custom: after installing for the second time', ); }); + it('registers errors without component stack as errors by default, when ExceptionManager is registered first', () => { + jest.spyOn(LogBoxData, 'checkWarningFilter'); + jest.spyOn(LogBoxData, 'addException'); + + ExceptionsManager.installConsoleErrorReporter(); + LogBox.install(); + + console.error('HIT'); + + // Errors without a component stack skip the warning filter and + // fall through to the ExceptionManager, which are then reported + // back to LogBox as non-fatal exceptions, in a convuluted dance + // in the most legacy cruft way. + expect(LogBoxData.addException).toBeCalledWith( + expect.objectContaining({originalMessage: 'HIT'}), + ); + expect(LogBoxData.checkWarningFilter).not.toBeCalled(); + }); + + it('registers errors without component stack as errors by default, when ExceptionManager is registered second', () => { + jest.spyOn(LogBoxData, 'checkWarningFilter'); + jest.spyOn(LogBoxData, 'addException'); + + LogBox.install(); + ExceptionsManager.installConsoleErrorReporter(); + + console.error('HIT'); + + // Errors without a component stack skip the warning filter and + // fall through to the ExceptionManager, which are then reported + // back to LogBox as non-fatal exceptions, in a convuluted dance + // in the most legacy cruft way. + expect(LogBoxData.addException).toBeCalledWith( + expect.objectContaining({originalMessage: 'HIT'}), + ); + expect(LogBoxData.checkWarningFilter).not.toBeCalled(); + }); }); From 256adcab7b02e8a80227d44b76d1a74d8c4973de Mon Sep 17 00:00:00 2001 From: Shawn Dempsey Date: Wed, 25 Sep 2024 23:29:11 -0700 Subject: [PATCH 015/296] Add a new playground component to rntester (#46632) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46632 Changelog: [General][Added] Add Playground to RNTester for testing out features & submitting reproducers. **Context** - Adding the Playground from Catalyst to RNTester - This should help folks test React Native features for a particular version **Change** - Add a new playground component to RNTester - Add a new reducer action for opening an example from navbar press - Fixed typing issues - Add icon from this fb asset pack: https://www.internalfb.com/assets/set/facebook_icons/nucleus-beaker/variant_outline-size_24?q=beaker - Matched background color using this imagemagick script **dark** ``` convert input.png -fill 'rgb(178,180,186)' -colorize 100% output.png ``` **light** ``` convert input.png -fill 'rgb(81,82,84)' -colorize 100% output.png ``` Reviewed By: NickGerleman Differential Revision: D61972594 fbshipit-source-id: 4a5523a84a6ef09d3266d5f56825907bd3fbe4b5 --- packages/rn-tester/js/RNTesterAppShared.js | 24 +++++++++--- .../bottom-nav-playgrounds-icon-dark.png | Bin 0 -> 921 bytes .../bottom-nav-playgrounds-icon-light.png | Bin 0 -> 921 bytes .../rn-tester/js/components/RNTesterNavbar.js | 36 ++++++++++++++++-- .../rn-tester/js/components/RNTesterTheme.js | 6 +++ .../examples/Playground/PlaygroundExample.js | 18 +++++++++ .../examples/Playground/RNTesterPlayground.js | 35 +++++++++++++++++ packages/rn-tester/js/types/RNTesterTypes.js | 2 +- .../js/utils/RNTesterList.android.js | 13 +++++-- .../rn-tester/js/utils/RNTesterList.ios.js | 14 +++++-- .../rn-tester/js/utils/RNTesterList.js.flow | 4 +- .../js/utils/RNTesterNavigationReducer.js | 15 +++++++- .../rn-tester/js/utils/testerStateUtils.js | 1 + 13 files changed, 148 insertions(+), 20 deletions(-) create mode 100644 packages/rn-tester/js/assets/bottom-nav-playgrounds-icon-dark.png create mode 100644 packages/rn-tester/js/assets/bottom-nav-playgrounds-icon-light.png create mode 100644 packages/rn-tester/js/examples/Playground/PlaygroundExample.js create mode 100644 packages/rn-tester/js/examples/Playground/RNTesterPlayground.js diff --git a/packages/rn-tester/js/RNTesterAppShared.js b/packages/rn-tester/js/RNTesterAppShared.js index 336fc9d9839156..a40cbc8c2377d0 100644 --- a/packages/rn-tester/js/RNTesterAppShared.js +++ b/packages/rn-tester/js/RNTesterAppShared.js @@ -8,8 +8,9 @@ * @flow */ -import type {RNTesterModuleInfo} from './types/RNTesterTypes'; +import type {RNTesterModuleInfo, ScreenTypes} from './types/RNTesterTypes'; +import {title as PlaygroundTitle} from './examples/Playground/PlaygroundExample'; import RNTesterModuleContainer from './components/RNTesterModuleContainer'; import RNTesterModuleList from './components/RNTesterModuleList'; import RNTesterNavBar, {navBarHeight} from './components/RNTesterNavbar'; @@ -127,11 +128,22 @@ const RNTesterApp = ({ ); const handleNavBarPress = React.useCallback( - (args: {screen: string}) => { - dispatch({ - type: RNTesterNavigationActionsType.NAVBAR_PRESS, - data: {screen: args.screen}, - }); + (args: {screen: ScreenTypes}) => { + if (args.screen === 'playgrounds') { + dispatch({ + type: RNTesterNavigationActionsType.NAVBAR_OPEN_MODULE_PRESS, + data: { + key: 'PlaygroundExample', + title: PlaygroundTitle, + screen: args.screen, + }, + }); + } else { + dispatch({ + type: RNTesterNavigationActionsType.NAVBAR_PRESS, + data: {screen: args.screen}, + }); + } }, [dispatch], ); diff --git a/packages/rn-tester/js/assets/bottom-nav-playgrounds-icon-dark.png b/packages/rn-tester/js/assets/bottom-nav-playgrounds-icon-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..b027c5abbb08c774035821fa3261cde933f01cd5 GIT binary patch literal 921 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~Lb6AYF9SoB8UsT^3j@P1pisjL z28L1t28LG&3=CE?7#PG0=Ijcz0ZK##_=LCy28GZ9{r~^}*Su#hfwst$1o;IsI5Y&z z_g|p!et*LK0)hDc^9B>@*XJuTI^O&D?qD)2$ODW?-tI24ubB%c0y&%o9+AZi42l*Y z%;-AJFaapYUgGKN%Km~=TuhKzC2ev6P|s~o7srr@!*6F?EIOnh;M%%G$RS6?ZS~Xt z`!@$SYPQ`?y}Z%5_C)QGCn-0pSts3n=dY@=uC*iG;FX1%|eC#uY;Wb^i;pMM|9H@zgd;e}39-9Gn;(v6OW+p02`gvGXsnw+@x zVhdM8{Z7t@Ulv$?_!dxd;@qyD`O_laB&1#=L5YYeY#(Vo9o1a#1RfVlXl=GSM}#)HN~=F*33;u&^>T z&^9ozGBD6RT00FzLvDUbW?ChR2B11)D^nAQhAoN<>wy|H;5L+G=B5^xB<2>N=rOV~ ZGzV&f=sD(9wHv61!PC{xWt~$(695!hZQcL? literal 0 HcmV?d00001 diff --git a/packages/rn-tester/js/assets/bottom-nav-playgrounds-icon-light.png b/packages/rn-tester/js/assets/bottom-nav-playgrounds-icon-light.png new file mode 100644 index 0000000000000000000000000000000000000000..23377fb92997d920ca5ba5e97dfebad6163b18ab GIT binary patch literal 921 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~Lb6AYF9SoB8UsT^3j@P1pisjL z28L1t28LG&3=CE?7#PG0=Ijcz0ZK##_=LD_+Omrl=>Px!$}eS|fVRk#1o;IsI5Y&z z_g|p!et*LK0)hDc^9B>@*XJuTI^O&D?qD)2$ODW?-tI24ubB%c0y&%o9+AZi42l*Y z%;-AJFaapYUgGKN%Km~=Tugvt)%xn4Ks~oTT^vIq4!@movFMP3fNSd#A%`3lx7APo z@82BUsM&Tm_3}pJ+7q=$o}}EYW}S5RoxiHe!oKq_nODv`{nE*~iF1#==aa^zjZE|L1ed%N#%c+W61)c)Xv_1UKQy zd^Tq|9ImDRG!^ui%clG$h{fXXJdeq$56Q#;1c-Qn=6By;a2#>)-scU_gzs4>)j z-5cOlVmH0xoAv%?o~SaXlFi$Xe*S$Z-}I8;h8H?bb^F{WN;f(hZmY^%5*FJkYI5S% zi!EFY^*cEqepz7o;afn-iF3Pp=1+@wlcd9TWz+lP-8biNy|&lHrGho>PGcWiSkT?P z`(1y7E?UI*9uJYW)7Z8Vh-?Jum}IuriBo2mI781t$nt`Q|Ei6yC4$wjF^iowXh$VAt`QrE~h#K_3Xz{1MV zK-<8;%D_PPXzer<4Y~O#nQ4_M8i4AItxQcI8n!4dtOsh)fZI@#nVVW%l9*e7qQ}U} Y&mdKI;Vst02jH9$N&HU literal 0 HcmV?d00001 diff --git a/packages/rn-tester/js/components/RNTesterNavbar.js b/packages/rn-tester/js/components/RNTesterNavbar.js index 2f0621e4a342c6..3b112b14970da8 100644 --- a/packages/rn-tester/js/components/RNTesterNavbar.js +++ b/packages/rn-tester/js/components/RNTesterNavbar.js @@ -9,11 +9,14 @@ */ import type {RNTesterTheme} from './RNTesterTheme'; +import type {ScreenTypes} from '../types/RNTesterTypes'; import {RNTesterThemeContext} from './RNTesterTheme'; import * as React from 'react'; import {Image, Pressable, StyleSheet, Text, View} from 'react-native'; +type NavBarOnPressHandler = ({screen: ScreenTypes}) => void; + /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's * LTI update could not be added via codemod */ const NavbarButton = ({ @@ -54,7 +57,7 @@ const ComponentTab = ({ handleNavBarPress, theme, }: $TEMPORARY$object<{ - handleNavBarPress: (data: {screen: string}) => void, + handleNavBarPress: NavBarOnPressHandler, isComponentActive: boolean, theme: RNTesterTheme, }>) => ( @@ -70,12 +73,33 @@ const ComponentTab = ({ /> ); +const PlaygroundTab = ({ + isComponentActive, + handleNavBarPress, + theme, +}: $TEMPORARY$object<{ + handleNavBarPress: NavBarOnPressHandler, + isComponentActive: boolean, + theme: RNTesterTheme, +}>) => ( + handleNavBarPress({screen: 'playgrounds'})} + activeImage={theme.NavBarPlaygroundActiveIcon} + inactiveImage={theme.NavBarPlaygroundInactiveIcon} + isActive={isComponentActive} + theme={theme} + iconStyle={styles.componentIcon} + /> +); + const APITab = ({ isAPIActive, handleNavBarPress, theme, }: $TEMPORARY$object<{ - handleNavBarPress: (data: {screen: string}) => void, + handleNavBarPress: NavBarOnPressHandler, isAPIActive: boolean, theme: RNTesterTheme, }>) => ( @@ -92,7 +116,7 @@ const APITab = ({ ); type Props = $ReadOnly<{| - handleNavBarPress: (data: {screen: string}) => void, + handleNavBarPress: NavBarOnPressHandler, screen: string, isExamplePageOpen: boolean, |}>; @@ -106,6 +130,7 @@ const RNTesterNavbar = ({ const isAPIActive = screen === 'apis' && !isExamplePageOpen; const isComponentActive = screen === 'components' && !isExamplePageOpen; + const isPlaygroundActive = screen === 'playgrounds'; return ( @@ -115,6 +140,11 @@ const RNTesterNavbar = ({ handleNavBarPress={handleNavBarPress} theme={theme} /> + = [Playground]; diff --git a/packages/rn-tester/js/examples/Playground/RNTesterPlayground.js b/packages/rn-tester/js/examples/Playground/RNTesterPlayground.js new file mode 100644 index 00000000000000..01613e09377899 --- /dev/null +++ b/packages/rn-tester/js/examples/Playground/RNTesterPlayground.js @@ -0,0 +1,35 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + * @oncall react_native + */ + +import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; + +import * as React from 'react'; +import {StyleSheet, Text, View} from 'react-native'; + +function Playground() { + return ( + + Edit "RNTesterPlayground.js" to change this file + + ); +} + +const styles = StyleSheet.create({ + container: { + padding: 10, + }, +}); + +export default ({ + title: 'Playground', + name: 'playground', + render: (): React.Node => , +}: RNTesterModuleExample); diff --git a/packages/rn-tester/js/types/RNTesterTypes.js b/packages/rn-tester/js/types/RNTesterTypes.js index 53500cc1fa04f6..1e24f41da472c2 100644 --- a/packages/rn-tester/js/types/RNTesterTypes.js +++ b/packages/rn-tester/js/types/RNTesterTypes.js @@ -53,7 +53,7 @@ export type ExamplesList = $ReadOnly<{| apis: $ReadOnlyArray>, |}>; -export type ScreenTypes = 'components' | 'apis' | null; +export type ScreenTypes = 'components' | 'apis' | 'playgrounds' | null; export type ComponentList = null | {components: string[], apis: string[]}; diff --git a/packages/rn-tester/js/utils/RNTesterList.android.js b/packages/rn-tester/js/utils/RNTesterList.android.js index 9e649d6bfc562a..df0d329d0c35f8 100644 --- a/packages/rn-tester/js/utils/RNTesterList.android.js +++ b/packages/rn-tester/js/utils/RNTesterList.android.js @@ -10,7 +10,7 @@ 'use strict'; -import type {RNTesterModuleInfo} from '../types/RNTesterTypes'; +import type {RNTesterModuleInfo, RNTesterModule} from '../types/RNTesterTypes'; import ReactNativeFeatureFlags from 'react-native/Libraries/ReactNative/ReactNativeFeatureFlags'; @@ -365,9 +365,16 @@ if (ReactNativeFeatureFlags.shouldEmitW3CPointerEvents()) { }); } -const Modules: any = {}; +const Playgrounds: Array = [ + { + key: 'PlaygroundExample', + module: require('../examples/Playground/PlaygroundExample'), + }, +]; + +const Modules: {[key: string]: RNTesterModule} = {}; -APIs.concat(Components).forEach(Example => { +[...APIs, ...Components, ...Playgrounds].forEach(Example => { Modules[Example.key] = Example.module; }); diff --git a/packages/rn-tester/js/utils/RNTesterList.ios.js b/packages/rn-tester/js/utils/RNTesterList.ios.js index b41392a69a5075..cf88aff1a6fc3a 100644 --- a/packages/rn-tester/js/utils/RNTesterList.ios.js +++ b/packages/rn-tester/js/utils/RNTesterList.ios.js @@ -10,7 +10,7 @@ 'use strict'; -import type {RNTesterModuleInfo} from '../types/RNTesterTypes'; +import type {RNTesterModuleInfo, RNTesterModule} from '../types/RNTesterTypes'; import ReactNativeFeatureFlags from 'react-native/Libraries/ReactNative/ReactNativeFeatureFlags'; @@ -345,10 +345,16 @@ if (ReactNativeFeatureFlags.shouldEmitW3CPointerEvents()) { }); } -const Modules: {...} = {}; +const Playgrounds: Array = [ + { + key: 'PlaygroundExample', + module: require('../examples/Playground/PlaygroundExample'), + }, +]; + +const Modules: {[key: string]: RNTesterModule} = {}; -APIs.concat(Components).forEach(Example => { - // $FlowFixMe[prop-missing] +[...APIs, ...Components, ...Playgrounds].forEach(Example => { Modules[Example.key] = Example.module; }); diff --git a/packages/rn-tester/js/utils/RNTesterList.js.flow b/packages/rn-tester/js/utils/RNTesterList.js.flow index 70d7ed59fac49d..1beaa5559b06d3 100644 --- a/packages/rn-tester/js/utils/RNTesterList.js.flow +++ b/packages/rn-tester/js/utils/RNTesterList.js.flow @@ -10,10 +10,10 @@ 'use strict'; -import type {RNTesterModuleInfo} from '../types/RNTesterTypes'; +import type {RNTesterModuleInfo, RNTesterModule} from '../types/RNTesterTypes'; declare const APIs: Array; declare const Components: Array; -declare const Modules: {...}; +declare const Modules: {[key: string]: RNTesterModule}; module.exports = {APIs, Components, Modules}; diff --git a/packages/rn-tester/js/utils/RNTesterNavigationReducer.js b/packages/rn-tester/js/utils/RNTesterNavigationReducer.js index 4f0f5c335c01df..637ad0dd552ccd 100644 --- a/packages/rn-tester/js/utils/RNTesterNavigationReducer.js +++ b/packages/rn-tester/js/utils/RNTesterNavigationReducer.js @@ -19,6 +19,7 @@ export const RNTesterNavigationActionsType = { MODULE_CARD_PRESS: 'MODULE_CARD_PRESS', EXAMPLE_CARD_PRESS: 'EXAMPLE_CARD_PRESS', EXAMPLE_OPEN_URL_REQUEST: 'EXAMPLE_OPEN_URL_REQUEST', + NAVBAR_OPEN_MODULE_PRESS: 'NAVBAR_OPEN_MODULE_PRESS', }; const getUpdatedRecentlyUsed = ({ @@ -77,6 +78,16 @@ export const RNTesterNavigationReducer = ( hadDeepLink: false, }; + case RNTesterNavigationActionsType.NAVBAR_OPEN_MODULE_PRESS: + return { + ...state, + activeModuleKey: key, + activeModuleTitle: title, + activeModuleExampleKey: null, + screen, + hadDeepLink: true, + }; + case RNTesterNavigationActionsType.MODULE_CARD_PRESS: return { ...state, @@ -99,7 +110,6 @@ export const RNTesterNavigationReducer = ( case RNTesterNavigationActionsType.BACK_BUTTON_PRESS: // Go back to module or list. - // If there was a deeplink navigation, pressing Back should bring us back to the root. return { ...state, activeModuleExampleKey: null, @@ -112,6 +122,8 @@ export const RNTesterNavigationReducer = ( ? state.activeModuleTitle : null, hadDeepLink: false, + // If there was a deeplink navigation, pressing Back should bring us back to the root. + screen: state.hadDeepLink ? 'components' : state.screen, }; case RNTesterNavigationActionsType.EXAMPLE_OPEN_URL_REQUEST: @@ -121,6 +133,7 @@ export const RNTesterNavigationReducer = ( activeModuleTitle: title, activeModuleExampleKey: exampleKey, hadDeepLink: true, + screen: 'components', }; default: diff --git a/packages/rn-tester/js/utils/testerStateUtils.js b/packages/rn-tester/js/utils/testerStateUtils.js index 596af29acfebd3..df8137bc58e50a 100644 --- a/packages/rn-tester/js/utils/testerStateUtils.js +++ b/packages/rn-tester/js/utils/testerStateUtils.js @@ -21,6 +21,7 @@ import RNTesterList from './RNTesterList'; export const Screens = { COMPONENTS: 'components', APIS: 'apis', + PLAYGROUNDS: 'playgrounds', }; export const initialNavigationState: RNTesterNavigationState = { From 7dcb10b6e7e226034a496eaed5351601ae0a1ae2 Mon Sep 17 00:00:00 2001 From: Michael Troger <11340859+michaeltroger@users.noreply.github.com> Date: Wed, 25 Sep 2024 23:53:17 -0700 Subject: [PATCH 016/296] Fix YogaConfig getting garbage collected #1678 (#46651) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46651 Fixes [https://github.com/facebook/yoga/issues/1678](https://github.com/facebook/yoga/issues/1678) As described in the linked Issue, the problem is that the `YogaConfig` can get garbage collected by the JVM, while a `YogaNode` is still referring to it. This at some point leads to unexpected behaviour (0 values for `layoutWidth`/`layoutHeight`). The change coming with this PR makes sure the `YogaConfig` can not get garbage collected while it's used by a `YogaNode`. Demo project to confirm the fix https://github.com/michaeltroger/yogabug Kudos to rtPag, who helped identifying the issue. X-link: https://github.com/facebook/yoga/pull/1703 Reviewed By: mdvacca Differential Revision: D63416127 Pulled By: NickGerleman fbshipit-source-id: efd87dac897e44d3664c228c40cda90f1e11c4f6 --- .../src/main/java/com/facebook/yoga/YogaNodeJNIBase.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaNodeJNIBase.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaNodeJNIBase.java index 6e4f96982a5b3c..a53fe74af00241 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaNodeJNIBase.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaNodeJNIBase.java @@ -32,6 +32,7 @@ public abstract class YogaNodeJNIBase extends YogaNode implements Cloneable { private static final byte LAYOUT_BORDER_START_INDEX = 14; @Nullable private YogaNodeJNIBase mOwner; + @Nullable private YogaConfig mConfig; @Nullable private List mChildren; @Nullable private YogaMeasureFunction mMeasureFunction; @Nullable private YogaBaselineFunction mBaselineFunction; @@ -57,6 +58,7 @@ private YogaNodeJNIBase(long nativePointer) { YogaNodeJNIBase(YogaConfig config) { this(YogaNative.jni_YGNodeNewWithConfigJNI(((YogaConfigJNIBase) config).mNativePointer)); + mConfig = config; // makes sure the YogaConfig is not garbage collected } public void reset() { From 23e738fb6d3d9fe397361e25647fdbb6c4e53ab2 Mon Sep 17 00:00:00 2001 From: Vitali Zaidman Date: Thu, 26 Sep 2024 03:42:49 -0700 Subject: [PATCH 017/296] allow waiting for metro to be torn down (#46620) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46620 The following error was thrown when the test `packages/metro/src/integration_tests/__tests__/server-test.js` was running on Metro: {F1816961963} It led me to investigate why don't we wait for Metro to be torn down. Currently we close metro by closing the http server that it launches. ``` const httpServer = await Metro.runServer(/* ... */); httpServer.close(callback); ``` While we can listen to the callback fired when the server is closed, it only covers one of the systems running internally in metro. The systems that are not covered are: * File watchers * File map workers * Dependency graph * Bundler And many systems that were themselves listening to the above like "eslint file map" or the "dependency analysis". **These systems are closed by us _after_ the server is closed.** This means that a listener to `server.on('close'` would only get the indication where these systems has started to close rather than actually got closed. https://www.internalfb.com/code/fbsource/[17e03bc6bd86]/xplat/js/tools/metro/packages/metro/src/index.flow.js?lines=359-361 This diff introduces a way to wait for all of metro to be closed. In this diff I use that new way of listening to Metro closure to get rid of the jest test warning mentioned above in `packages/metro/src/integration_tests/__tests__/server-test.js`: ``` let serverClosedPromise; beforeEach(async () => { config = await Metro.loadConfig({ config: require.resolve('../metro.config.js'), }); let onCloseResolve; serverClosedPromise = new Promise(resolve => (onCloseResolve = resolve)); httpServer = await Metro.runServer(config, { reporter: {update() {}}, onClose: () => { onCloseResolve(); }, }); }); afterEach(async () => { httpServer.close(); await serverClosedPromise; }); ``` Changelog: [Feature] add `onClose` to `Metro.runServer` configuration allowing to wait for metro and all associated processes to be closed. Reviewed By: huntie Differential Revision: D61594124 fbshipit-source-id: e3c50ef986077503bce0caa42a9f9430efc65272 --- .../community-cli-plugin/src/commands/bundle/buildBundle.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/community-cli-plugin/src/commands/bundle/buildBundle.js b/packages/community-cli-plugin/src/commands/bundle/buildBundle.js index 8cfce1f1699c5c..c4446ea1c8eb05 100644 --- a/packages/community-cli-plugin/src/commands/bundle/buildBundle.js +++ b/packages/community-cli-plugin/src/commands/bundle/buildBundle.js @@ -140,7 +140,7 @@ async function buildBundleWithConfig( args.assetCatalogDest, ); } finally { - server.end(); + await server.end(); } } From 038e6eda6d97d21956153736a677eca6fd53e381 Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Thu, 26 Sep 2024 03:49:09 -0700 Subject: [PATCH 018/296] Fix the trigger e2e on comment workflow (#46653) Summary: This change fixes the workflow by using the right URL. I also added some logging for debugging. ## Changelog: [Internal] - Fix Trigger E2E tests workflow Pull Request resolved: https://github.com/facebook/react-native/pull/46653 Test Plan: Tested the script locally and verified that it triggers the workflow. Reviewed By: cortinico Differential Revision: D63452098 Pulled By: cipolleschi fbshipit-source-id: f44956071edb68046ed1cb74e286e8edbe0eb23a --- .github/workflows/test-all.yml | 12 +++++++----- .github/workflows/trigger-e2e-on-comment.yml | 4 ++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-all.yml b/.github/workflows/test-all.yml index 46e86b6a5f2ffb..48db77b226c9f6 100644 --- a/.github/workflows/test-all.yml +++ b/.github/workflows/test-all.yml @@ -35,6 +35,8 @@ jobs: echo "RELEASE_TYPE=dry-run" >> $GITHUB_OUTPUT fi + echo "Should I run E2E tests? ${{ inputs.run-e2e-tests }}" + prepare_hermes_workspace: runs-on: ubuntu-latest env: @@ -181,7 +183,7 @@ jobs: react-native-version: ${{ needs.prepare_hermes_workspace.outputs.react-native-version }} test_e2e_ios_rntester: - if: ${{ github.ref == 'refs/heads/main' || contains(github.ref, 'stable') || inputs.run-e2e-tests == 'true' }} + if: ${{ github.ref == 'refs/heads/main' || contains(github.ref, 'stable') || inputs.run-e2e-tests }} runs-on: macos-13 needs: [build_apple_slices_hermes, prepare_hermes_workspace, build_hermes_macos] @@ -216,7 +218,7 @@ jobs: maestro-flow: ./packages/rn-tester/.maestro/ test_e2e_ios_templateapp: - if: ${{ github.ref == 'refs/heads/main' || contains(github.ref, 'stable') || inputs.run-e2e-tests == 'true' }} + if: ${{ github.ref == 'refs/heads/main' || contains(github.ref, 'stable') || inputs.run-e2e-tests }} runs-on: macos-13 needs: build_npm_package env: @@ -282,7 +284,7 @@ jobs: maestro-flow: ./scripts/e2e/.maestro/ test_e2e_android_templateapp: - if: ${{ github.ref == 'refs/heads/main' || contains(github.ref, 'stable') || inputs.run-e2e-tests == 'true'}} + if: ${{ github.ref == 'refs/heads/main' || contains(github.ref, 'stable') || inputs.run-e2e-tests }} runs-on: 4-core-ubuntu needs: build_npm_package continue-on-error: true @@ -391,10 +393,10 @@ jobs: uses: ./.github/actions/build-android with: release-type: ${{ needs.set_release_type.outputs.RELEASE_TYPE }} - run-e2e-tests: ${{ github.ref == 'refs/heads/main' || contains(github.ref, 'stable') || inputs.run-e2e-tests == 'true'}} + run-e2e-tests: ${{ github.ref == 'refs/heads/main' || contains(github.ref, 'stable') || inputs.run-e2e-tests }} test_e2e_android_rntester: - if: ${{ github.ref == 'refs/heads/main' || contains(github.ref, 'stable') || inputs.run-e2e-tests == 'true' }} + if: ${{ github.ref == 'refs/heads/main' || contains(github.ref, 'stable') || inputs.run-e2e-tests }} runs-on: ubuntu-latest needs: [build_android] strategy: diff --git a/.github/workflows/trigger-e2e-on-comment.yml b/.github/workflows/trigger-e2e-on-comment.yml index 1995b4474f4ef5..1ec32e38c1c0a5 100644 --- a/.github/workflows/trigger-e2e-on-comment.yml +++ b/.github/workflows/trigger-e2e-on-comment.yml @@ -24,18 +24,22 @@ jobs: # Github does not provide the branch of a PR when a comment on a PR is made # So, given the issue number, which is the PR number, we can retrieve the branch with # a quick API call + echo "Retrieving branch" BRANCH=$(curl -L \ -H "Accept: application/vnd.github+json" \ -H "Authorization: Bearer $GITHUB_TOKEN" \ -H "X-GitHub-Api-Version: 2022-11-28" \ https://api.github.com/repos/facebook/react-native/pulls/$PR_NUMBER | jq -r '.head.ref') + echo "Trigger Test All workflow for branch $BRANCH" curl -L \ -X POST \ -H "Accept: application/vnd.github+json" \ -H "Authorization: Bearer $GITHUB_TOKEN" \ -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/repos/facebook/react-native/actions/workflows/test-all.yml/dispatches \ -d "{\"ref\": \"$BRANCH\", \"inputs\": {\"run-e2e-tests\": \"true\"}}" + env: GITHUB_TOKEN: ${{ secrets.REACT_NATIVE_BOT_GITHUB_TOKEN }} PR_NUMBER: ${{ github.event.issue.number }} From 10a33e04793befbeab6ae82d4068b1ebd7fdffd0 Mon Sep 17 00:00:00 2001 From: David Vacca Date: Thu, 26 Sep 2024 04:29:27 -0700 Subject: [PATCH 019/296] Migrate ReactFeatureFlags.enableFabricRenderer -> ReactNativeFeatureFlags (#46493) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46493 Migrate: - ReactFeatureFlags.enableFabricRenderer -> ReactNativeFeatureFlags.enableFabricRenderer() - ReactFeatureFlags.useTurboModules -> ReactNativeFeatureFlags.useTurboModules() - Rename ReactFeatureFlags.enableBridgelessArchitecture -> ReactNativeFeatureFlags.enableBridgelessArchitecture() changelog: [Android][Breaking] Delete useTurboModules, enableFabricRenderer and enableBridgelessArchitecture fields from ReactFeatureFlags class Reviewed By: rubennorte Differential Revision: D60364016 fbshipit-source-id: e10a44fc08deb8969104b5cb0bd426dfb0a45d30 --- .../ReactAndroid/api/ReactAndroid.api | 3 - .../com/facebook/react/BaseReactPackage.java | 4 +- .../facebook/react/HeadlessJsTaskService.java | 4 +- .../facebook/react/ReactActivityDelegate.java | 6 +- .../com/facebook/react/ReactDelegate.java | 36 ++--- .../facebook/react/ReactInstanceManager.java | 5 +- ...eactPackageTurboModuleManagerDelegate.java | 3 +- .../react/bridge/CatalystInstanceImpl.java | 4 +- .../bridge/interop/InteropModuleRegistry.java | 4 +- .../react/config/ReactFeatureFlags.java | 34 ----- .../DefaultNewArchitectureEntryPoint.kt | 7 +- .../react/fabric/FabricUIManager.java | 3 +- .../featureflags/ReactNativeFeatureFlags.kt | 20 ++- .../ReactNativeFeatureFlagsCxxAccessor.kt | 32 +++- .../ReactNativeFeatureFlagsCxxInterop.kt | 8 +- .../ReactNativeFeatureFlagsDefaults.kt | 8 +- .../ReactNativeFeatureFlagsLocalAccessor.kt | 35 ++++- .../ReactNativeFeatureFlagsProvider.kt | 8 +- ...tiveNewArchitectureFeatureFlagsDefaults.kt | 31 ++-- .../UIManagerModuleConstantsHelper.java | 7 +- .../react/views/image/ReactImageView.kt | 4 +- .../JReactNativeFeatureFlagsCxxInterop.cpp | 44 +++++- .../JReactNativeFeatureFlagsCxxInterop.h | 11 +- .../interop/InteropModuleRegistryTest.kt | 37 ++--- .../deviceinfo/DeviceInfoModuleTest.kt | 2 + .../uimanager/UIManagerModuleConstantsTest.kt | 2 + .../featureflags/ReactNativeFeatureFlags.cpp | 14 +- .../featureflags/ReactNativeFeatureFlags.h | 17 ++- .../ReactNativeFeatureFlagsAccessor.cpp | 140 ++++++++++++------ .../ReactNativeFeatureFlagsAccessor.h | 10 +- .../ReactNativeFeatureFlagsDefaults.h | 14 +- .../ReactNativeFeatureFlagsProvider.h | 5 +- .../NativeReactNativeFeatureFlags.cpp | 17 ++- .../NativeReactNativeFeatureFlags.h | 8 +- .../ReactNativeFeatureFlags.config.js | 23 +++ .../featureflags/ReactNativeFeatureFlags.js | 17 ++- .../specs/NativeReactNativeFeatureFlags.js | 5 +- 37 files changed, 454 insertions(+), 178 deletions(-) diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index c4a2cd600a373a..e1909ad0345138 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -2041,10 +2041,7 @@ public final class com/facebook/react/common/network/OkHttpCallUtil { public class com/facebook/react/config/ReactFeatureFlags { public static field dispatchPointerEvents Z - public static field enableBridgelessArchitecture Z public static field enableCppPropsIteratorSetter Z - public static field enableFabricRenderer Z - public static field useTurboModules Z public fun ()V } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/BaseReactPackage.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/BaseReactPackage.java index 0a606e0c65f44e..2e10cc9dc2989c 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/BaseReactPackage.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/BaseReactPackage.java @@ -13,7 +13,7 @@ import com.facebook.react.bridge.ModuleSpec; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.config.ReactFeatureFlags; +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags; import com.facebook.react.module.model.ReactModuleInfo; import com.facebook.react.module.model.ReactModuleInfoProvider; import com.facebook.react.uimanager.ViewManager; @@ -73,7 +73,7 @@ private void findNext() { // This Iterator is used to create the NativeModule registry. The NativeModule // registry must not have TurboModules. Therefore, if TurboModules are enabled, and // the current NativeModule is a TurboModule, we need to skip iterating over it. - if (ReactFeatureFlags.useTurboModules && reactModuleInfo.isTurboModule()) { + if (ReactNativeFeatureFlags.useTurboModules() && reactModuleInfo.isTurboModule()) { continue; } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/HeadlessJsTaskService.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/HeadlessJsTaskService.java index 9fa6f58fe30775..314e68388372ac 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/HeadlessJsTaskService.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/HeadlessJsTaskService.java @@ -19,7 +19,7 @@ import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.UiThreadUtil; -import com.facebook.react.config.ReactFeatureFlags; +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags; import com.facebook.react.jstasks.HeadlessJsTaskConfig; import com.facebook.react.jstasks.HeadlessJsTaskContext; import com.facebook.react.jstasks.HeadlessJsTaskEventListener; @@ -167,7 +167,7 @@ protected ReactNativeHost getReactNativeHost() { } protected ReactContext getReactContext() { - if (ReactFeatureFlags.enableBridgelessArchitecture) { + if (ReactNativeFeatureFlags.enableBridgelessArchitecture()) { ReactHost reactHost = getReactHost(); Assertions.assertNotNull(reactHost, "getReactHost() is null in New Architecture"); return reactHost.getCurrentReactContext(); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java index 9ca16398b40d4c..48c59f8ee72bc9 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java @@ -18,7 +18,7 @@ import androidx.annotation.Nullable; import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.Callback; -import com.facebook.react.config.ReactFeatureFlags; +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags; import com.facebook.react.modules.core.PermissionListener; /** @@ -107,7 +107,7 @@ public void onCreate(Bundle savedInstanceState) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isWideColorGamutEnabled()) { mActivity.getWindow().setColorMode(ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT); } - if (ReactFeatureFlags.enableBridgelessArchitecture) { + if (ReactNativeFeatureFlags.enableBridgelessArchitecture()) { mReactDelegate = new ReactDelegate(getPlainActivity(), getReactHost(), mainComponentName, launchOptions); } else { @@ -226,7 +226,7 @@ protected Activity getPlainActivity() { * @return true if Fabric is enabled for this Activity, false otherwise. */ protected boolean isFabricEnabled() { - return ReactFeatureFlags.enableFabricRenderer; + return ReactNativeFeatureFlags.enableFabricRenderer(); } /** diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactDelegate.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactDelegate.java index 7c0f4edd6bb598..c7b8ce77297540 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactDelegate.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactDelegate.java @@ -15,11 +15,11 @@ import androidx.annotation.Nullable; import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.UiThreadUtil; -import com.facebook.react.config.ReactFeatureFlags; import com.facebook.react.devsupport.DoubleTapReloadRecognizer; import com.facebook.react.devsupport.ReleaseDevSupportManager; import com.facebook.react.devsupport.interfaces.DevSupportManager; import com.facebook.react.interfaces.fabric.ReactSurface; +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags; import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; /** @@ -43,7 +43,7 @@ public class ReactDelegate { @Nullable private ReactSurface mReactSurface; - private boolean mFabricEnabled = ReactFeatureFlags.enableFabricRenderer; + private boolean mFabricEnabled = ReactNativeFeatureFlags.enableFabricRenderer(); /** * Do not use this constructor as it's not accounting for New Architecture at all. You should @@ -94,7 +94,7 @@ public ReactDelegate( @Nullable private DevSupportManager getDevSupportManager() { - if (ReactFeatureFlags.enableBridgelessArchitecture + if (ReactNativeFeatureFlags.enableBridgelessArchitecture() && mReactHost != null && mReactHost.getDevSupportManager() != null) { return mReactHost.getDevSupportManager(); @@ -111,7 +111,7 @@ public void onHostResume() { throw new ClassCastException( "Host Activity does not implement DefaultHardwareBackBtnHandler"); } - if (ReactFeatureFlags.enableBridgelessArchitecture) { + if (ReactNativeFeatureFlags.enableBridgelessArchitecture()) { mReactHost.onHostResume(mActivity, (DefaultHardwareBackBtnHandler) mActivity); } else { if (getReactNativeHost().hasInstance()) { @@ -123,7 +123,7 @@ public void onHostResume() { } public void onUserLeaveHint() { - if (ReactFeatureFlags.enableBridgelessArchitecture) { + if (ReactNativeFeatureFlags.enableBridgelessArchitecture()) { mReactHost.onHostLeaveHint(mActivity); } else { if (getReactNativeHost().hasInstance()) { @@ -133,7 +133,7 @@ public void onUserLeaveHint() { } public void onHostPause() { - if (ReactFeatureFlags.enableBridgelessArchitecture) { + if (ReactNativeFeatureFlags.enableBridgelessArchitecture()) { mReactHost.onHostPause(mActivity); } else { if (getReactNativeHost().hasInstance()) { @@ -144,7 +144,7 @@ public void onHostPause() { public void onHostDestroy() { unloadApp(); - if (ReactFeatureFlags.enableBridgelessArchitecture) { + if (ReactNativeFeatureFlags.enableBridgelessArchitecture()) { mReactHost.onHostDestroy(mActivity); } else { if (getReactNativeHost().hasInstance()) { @@ -154,7 +154,7 @@ public void onHostDestroy() { } public boolean onBackPressed() { - if (ReactFeatureFlags.enableBridgelessArchitecture) { + if (ReactNativeFeatureFlags.enableBridgelessArchitecture()) { mReactHost.onBackPressed(); return true; } else { @@ -167,7 +167,7 @@ public boolean onBackPressed() { } public boolean onNewIntent(Intent intent) { - if (ReactFeatureFlags.enableBridgelessArchitecture) { + if (ReactNativeFeatureFlags.enableBridgelessArchitecture()) { mReactHost.onNewIntent(intent); return true; } else { @@ -181,7 +181,7 @@ public boolean onNewIntent(Intent intent) { public void onActivityResult( int requestCode, int resultCode, Intent data, boolean shouldForwardToReactInstance) { - if (ReactFeatureFlags.enableBridgelessArchitecture) { + if (ReactNativeFeatureFlags.enableBridgelessArchitecture()) { mReactHost.onActivityResult(mActivity, requestCode, resultCode, data); } else { if (getReactNativeHost().hasInstance() && shouldForwardToReactInstance) { @@ -193,7 +193,7 @@ public void onActivityResult( } public void onWindowFocusChanged(boolean hasFocus) { - if (ReactFeatureFlags.enableBridgelessArchitecture) { + if (ReactNativeFeatureFlags.enableBridgelessArchitecture()) { mReactHost.onWindowFocusChange(hasFocus); } else { if (getReactNativeHost().hasInstance()) { @@ -203,7 +203,7 @@ public void onWindowFocusChanged(boolean hasFocus) { } public void onConfigurationChanged(Configuration newConfig) { - if (ReactFeatureFlags.enableBridgelessArchitecture) { + if (ReactNativeFeatureFlags.enableBridgelessArchitecture()) { mReactHost.onConfigurationChanged(Assertions.assertNotNull(mActivity)); } else { if (getReactNativeHost().hasInstance()) { @@ -215,7 +215,7 @@ public void onConfigurationChanged(Configuration newConfig) { public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD - && ((ReactFeatureFlags.enableBridgelessArchitecture + && ((ReactNativeFeatureFlags.enableBridgelessArchitecture() && mReactHost != null && mReactHost.getDevSupportManager() != null) || (getReactNativeHost().hasInstance() @@ -228,7 +228,7 @@ && getReactNativeHost().getUseDeveloperSupport()))) { public boolean onKeyLongPress(int keyCode) { if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { - if (ReactFeatureFlags.enableBridgelessArchitecture && mReactHost != null) { + if (ReactNativeFeatureFlags.enableBridgelessArchitecture() && mReactHost != null) { DevSupportManager devSupportManager = mReactHost.getDevSupportManager(); // onKeyLongPress is a Dev API and not supported in RELEASE mode. if (devSupportManager != null && !(devSupportManager instanceof ReleaseDevSupportManager)) { @@ -254,7 +254,7 @@ public void reload() { // Reload in RELEASE mode if (devSupportManager instanceof ReleaseDevSupportManager) { // Do not reload the bundle from JS as there is no bundler running in release mode. - if (ReactFeatureFlags.enableBridgelessArchitecture) { + if (ReactNativeFeatureFlags.enableBridgelessArchitecture()) { if (mReactHost != null) { mReactHost.reload("ReactDelegate.reload()"); } @@ -286,7 +286,7 @@ public void loadApp() { */ public void loadApp(String appKey) { // With Bridgeless enabled, create and start the surface - if (ReactFeatureFlags.enableBridgelessArchitecture) { + if (ReactNativeFeatureFlags.enableBridgelessArchitecture()) { if (mReactSurface == null) { // Create a ReactSurface mReactSurface = mReactHost.createSurface(mActivity, appKey, mLaunchOptions); @@ -306,7 +306,7 @@ public void loadApp(String appKey) { /** Stop the React surface started with {@link ReactDelegate#loadApp()}. */ public void unloadApp() { - if (ReactFeatureFlags.enableBridgelessArchitecture) { + if (ReactNativeFeatureFlags.enableBridgelessArchitecture()) { if (mReactSurface != null) { mReactSurface.stop(); mReactSurface = null; @@ -321,7 +321,7 @@ public void unloadApp() { @Nullable public ReactRootView getReactRootView() { - if (ReactFeatureFlags.enableBridgelessArchitecture) { + if (ReactNativeFeatureFlags.enableBridgelessArchitecture()) { return (ReactRootView) mReactSurface.getView(); } else { return mReactRootView; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java index ca0c62ac03b1a3..31cf7f73823bfa 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java @@ -80,7 +80,6 @@ import com.facebook.react.common.SurfaceDelegateFactory; import com.facebook.react.common.annotations.StableReactNativeAPI; import com.facebook.react.common.annotations.VisibleForTesting; -import com.facebook.react.config.ReactFeatureFlags; import com.facebook.react.devsupport.DevSupportManagerFactory; import com.facebook.react.devsupport.InspectorFlags; import com.facebook.react.devsupport.ReactInstanceDevHelper; @@ -355,7 +354,7 @@ public JavaScriptExecutorFactory getJavaScriptExecutorFactory() { Activity currentActivity = getCurrentActivity(); if (currentActivity != null) { ReactRootView rootView = new ReactRootView(currentActivity); - boolean isFabric = ReactFeatureFlags.enableFabricRenderer; + boolean isFabric = ReactNativeFeatureFlags.enableFabricRenderer(); rootView.setIsFabric(isFabric); rootView.startReactApplication(ReactInstanceManager.this, appKey, new Bundle()); return rootView; @@ -1470,7 +1469,7 @@ private ReactApplicationContext createReactContext( // architecture so it will always be there. catalystInstance.getRuntimeScheduler(); - if (ReactFeatureFlags.useTurboModules && mTMMDelegateBuilder != null) { + if (ReactNativeFeatureFlags.useTurboModules() && mTMMDelegateBuilder != null) { TurboModuleManagerDelegate tmmDelegate = mTMMDelegateBuilder .setPackages(mPackages) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactPackageTurboModuleManagerDelegate.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactPackageTurboModuleManagerDelegate.java index fa72014403df61..cfbbd336b194da 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactPackageTurboModuleManagerDelegate.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactPackageTurboModuleManagerDelegate.java @@ -14,7 +14,6 @@ import com.facebook.react.bridge.ModuleSpec; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.config.ReactFeatureFlags; import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags; import com.facebook.react.internal.turbomodule.core.TurboModuleManagerDelegate; import com.facebook.react.module.annotations.ReactModule; @@ -37,7 +36,7 @@ interface ModuleProvider { new HashMap<>(); private final boolean mShouldEnableLegacyModuleInterop = - ReactFeatureFlags.enableBridgelessArchitecture + ReactNativeFeatureFlags.enableBridgelessArchitecture() && ReactNativeFeatureFlags.useTurboModuleInterop(); // Lazy Props diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java index 31fd36010ab2d9..a5f4dd86c0be8f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java @@ -26,7 +26,7 @@ import com.facebook.react.bridge.queue.ReactQueueConfigurationSpec; import com.facebook.react.common.ReactConstants; import com.facebook.react.common.annotations.VisibleForTesting; -import com.facebook.react.config.ReactFeatureFlags; +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags; import com.facebook.react.internal.turbomodule.core.interfaces.TurboModuleRegistry; import com.facebook.react.module.annotations.ReactModule; import com.facebook.react.turbomodule.core.CallInvokerHolderImpl; @@ -459,7 +459,7 @@ public T getNativeModule(Class nativeModuleInterface } private TurboModuleRegistry getTurboModuleRegistry() { - if (ReactFeatureFlags.useTurboModules) { + if (ReactNativeFeatureFlags.useTurboModules()) { return Assertions.assertNotNull( mTurboModuleRegistry, "TurboModules are enabled, but mTurboModuleRegistry hasn't been set."); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/interop/InteropModuleRegistry.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/interop/InteropModuleRegistry.java index d694de0e8ded42..344bc7a1451737 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/interop/InteropModuleRegistry.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/interop/InteropModuleRegistry.java @@ -9,7 +9,6 @@ import androidx.annotation.Nullable; import com.facebook.react.bridge.JavaScriptModule; -import com.facebook.react.config.ReactFeatureFlags; import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags; import java.util.HashMap; @@ -52,6 +51,7 @@ public void registerInteropModule( } private boolean checkReactFeatureFlagsConditions() { - return ReactFeatureFlags.enableFabricRenderer && ReactNativeFeatureFlags.useFabricInterop(); + return ReactNativeFeatureFlags.enableFabricRenderer() + && ReactNativeFeatureFlags.useFabricInterop(); } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java index 11dfa80e682548..68a65b32704a24 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java @@ -21,40 +21,6 @@ @Deprecated(since = "Use com.facebook.react.internal.featureflags.ReactNativeFeatureFlags instead.") @DoNotStripAny public class ReactFeatureFlags { - /** - * Should this application use TurboModules? If yes, then any module that inherits {@link - * com.facebook.react.turbomodule.core.interfaces.TurboModule} will NOT be passed in to C++ - * CatalystInstanceImpl - */ - @Deprecated( - since = - "useTurboModules will be deleted in 0.77, please use" - + " DefaultNewArchitectureEntryPoint.load() to enable TurboModules instead.", - forRemoval = true) - public static volatile boolean useTurboModules = false; - - /** - * Should this application use the new (Fabric) Renderer? If yes, all rendering in this app will - * use Fabric instead of the legacy renderer. - */ - @Deprecated( - since = - "enableFabricRenderer will be deleted in 0.77, please use" - + " DefaultNewArchitectureEntryPoint.load() to enable fabric instead.", - forRemoval = true) - public static volatile boolean enableFabricRenderer = false; - - /** - * Feature flag to enable the new bridgeless architecture. Note: Enabling this will force enable - * the following flags: `useTurboModules` & `enableFabricRenderer`. - */ - @Deprecated( - since = - "enableBridgelessArchitecture will be deleted in 0.77, please use" - + " DefaultNewArchitectureEntryPoint.load() to enable bridgeless architecture" - + " instead.", - forRemoval = true) - public static boolean enableBridgelessArchitecture = false; public static boolean dispatchPointerEvents = false; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/defaults/DefaultNewArchitectureEntryPoint.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/defaults/DefaultNewArchitectureEntryPoint.kt index 80f5e965ca42e1..42dc49a0ffdad3 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/defaults/DefaultNewArchitectureEntryPoint.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/defaults/DefaultNewArchitectureEntryPoint.kt @@ -10,7 +10,6 @@ package com.facebook.react.defaults import com.facebook.react.common.annotations.VisibleForTesting -import com.facebook.react.config.ReactFeatureFlags import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatureFlagsDefaults @@ -39,18 +38,20 @@ public object DefaultNewArchitectureEntryPoint { if (!isValid) { error(errorMessage) } - ReactFeatureFlags.useTurboModules = turboModulesEnabled - ReactFeatureFlags.enableFabricRenderer = fabricEnabled if (bridgelessEnabled) { ReactNativeFeatureFlags.override( object : ReactNativeNewArchitectureFeatureFlagsDefaults() { override fun useFabricInterop(): Boolean = fabricEnabled + override fun enableFabricRenderer(): Boolean = fabricEnabled + // We turn this feature flag to true for OSS to fix #44610 and #45126 and other // similar bugs related to pressable. override fun enableEventEmitterRetentionDuringGesturesOnAndroid(): Boolean = fabricEnabled + + override fun useTurboModules(): Boolean = turboModulesEnabled }) } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java index 870544b54d62c5..82f6fcef57cc5f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java @@ -50,7 +50,6 @@ import com.facebook.react.bridge.WritableMap; import com.facebook.react.common.build.ReactBuildConfig; import com.facebook.react.common.mapbuffer.ReadableMapBuffer; -import com.facebook.react.config.ReactFeatureFlags; import com.facebook.react.fabric.events.EventEmitterWrapper; import com.facebook.react.fabric.events.FabricEventEmitter; import com.facebook.react.fabric.internal.interop.InteropUIBlockListener; @@ -442,7 +441,7 @@ public void invalidate() { // responsible for initializing and deallocating EventDispatcher. StaticViewConfigs is enabled // only in Bridgeless for now. // TODO T83943316: Remove this IF once StaticViewConfigs are enabled by default - if (!ReactFeatureFlags.enableBridgelessArchitecture) { + if (!ReactNativeFeatureFlags.enableBridgelessArchitecture()) { mEventDispatcher.onCatalystInstanceDestroyed(); } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt index 5784404f29fa74..5fb42d9f9ef298 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<73409b567e77f17838ae9681a8e20be6>> + * @generated SignedSource<> */ /** @@ -82,6 +82,12 @@ public object ReactNativeFeatureFlags { @JvmStatic public fun enableBackgroundStyleApplicator(): Boolean = accessor.enableBackgroundStyleApplicator() + /** + * Feature flag to enable the new bridgeless architecture. Note: Enabling this will force enable the following flags: `useTurboModules` & `enableFabricRenderer. + */ + @JvmStatic + public fun enableBridgelessArchitecture(): Boolean = accessor.enableBridgelessArchitecture() + /** * Clean yoga node when does not change. */ @@ -112,6 +118,12 @@ public object ReactNativeFeatureFlags { @JvmStatic public fun enableFabricLogs(): Boolean = accessor.enableFabricLogs() + /** + * Enables the use of the Fabric renderer in the whole app. + */ + @JvmStatic + public fun enableFabricRenderer(): Boolean = accessor.enableFabricRenderer() + /** * When the app is completely migrated to Fabric, set this flag to true to disable parts of Paper infrastructure that are not needed anymore but consume memory and CPU. Specifically, UIViewOperationQueue and EventDispatcherImpl will no longer work as they will not subscribe to ReactChoreographer for updates. */ @@ -334,6 +346,12 @@ public object ReactNativeFeatureFlags { @JvmStatic public fun useTurboModuleInterop(): Boolean = accessor.useTurboModuleInterop() + /** + * When enabled, NativeModules will be executed by using the TurboModule system + */ + @JvmStatic + public fun useTurboModules(): Boolean = accessor.useTurboModules() + /** * Overrides the feature flags with the ones provided by the given provider * (generally one that extends `ReactNativeFeatureFlagsDefaults`). diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt index f851574f87c94d..68e4e60d24c70e 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<9f2978b5a732a3ca4f3c3d74debba782>> + * @generated SignedSource<<19dca512d93d689e927ee5988a43e646>> */ /** @@ -29,11 +29,13 @@ public class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAccesso private var enableAndroidLineHeightCenteringCache: Boolean? = null private var enableAndroidMixBlendModePropCache: Boolean? = null private var enableBackgroundStyleApplicatorCache: Boolean? = null + private var enableBridgelessArchitectureCache: Boolean? = null private var enableCleanTextInputYogaNodeCache: Boolean? = null private var enableDeletionOfUnmountedViewsCache: Boolean? = null private var enableEagerRootViewAttachmentCache: Boolean? = null private var enableEventEmitterRetentionDuringGesturesOnAndroidCache: Boolean? = null private var enableFabricLogsCache: Boolean? = null + private var enableFabricRendererCache: Boolean? = null private var enableFabricRendererExclusivelyCache: Boolean? = null private var enableGranularShadowTreeStateReconciliationCache: Boolean? = null private var enableIOSViewClipToPaddingBoxCache: Boolean? = null @@ -71,6 +73,7 @@ public class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAccesso private var useRuntimeShadowNodeReferenceUpdateOnLayoutCache: Boolean? = null private var useStateAlignmentMechanismCache: Boolean? = null private var useTurboModuleInteropCache: Boolean? = null + private var useTurboModulesCache: Boolean? = null override fun commonTestFlag(): Boolean { var cached = commonTestFlagCache @@ -153,6 +156,15 @@ public class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAccesso return cached } + override fun enableBridgelessArchitecture(): Boolean { + var cached = enableBridgelessArchitectureCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.enableBridgelessArchitecture() + enableBridgelessArchitectureCache = cached + } + return cached + } + override fun enableCleanTextInputYogaNode(): Boolean { var cached = enableCleanTextInputYogaNodeCache if (cached == null) { @@ -198,6 +210,15 @@ public class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAccesso return cached } + override fun enableFabricRenderer(): Boolean { + var cached = enableFabricRendererCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.enableFabricRenderer() + enableFabricRendererCache = cached + } + return cached + } + override fun enableFabricRendererExclusively(): Boolean { var cached = enableFabricRendererExclusivelyCache if (cached == null) { @@ -531,6 +552,15 @@ public class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAccesso return cached } + override fun useTurboModules(): Boolean { + var cached = useTurboModulesCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.useTurboModules() + useTurboModulesCache = cached + } + return cached + } + override fun override(provider: ReactNativeFeatureFlagsProvider): Unit = ReactNativeFeatureFlagsCxxInterop.override(provider as Any) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt index 89950a0f986e4e..7bd8cfdc898324 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<0d2f5427661ce67e468aea47bdd29802>> + * @generated SignedSource<<76eebf045692e945d39a4ea27a63ae02>> */ /** @@ -46,6 +46,8 @@ public object ReactNativeFeatureFlagsCxxInterop { @DoNotStrip @JvmStatic public external fun enableBackgroundStyleApplicator(): Boolean + @DoNotStrip @JvmStatic public external fun enableBridgelessArchitecture(): Boolean + @DoNotStrip @JvmStatic public external fun enableCleanTextInputYogaNode(): Boolean @DoNotStrip @JvmStatic public external fun enableDeletionOfUnmountedViews(): Boolean @@ -56,6 +58,8 @@ public object ReactNativeFeatureFlagsCxxInterop { @DoNotStrip @JvmStatic public external fun enableFabricLogs(): Boolean + @DoNotStrip @JvmStatic public external fun enableFabricRenderer(): Boolean + @DoNotStrip @JvmStatic public external fun enableFabricRendererExclusively(): Boolean @DoNotStrip @JvmStatic public external fun enableGranularShadowTreeStateReconciliation(): Boolean @@ -130,6 +134,8 @@ public object ReactNativeFeatureFlagsCxxInterop { @DoNotStrip @JvmStatic public external fun useTurboModuleInterop(): Boolean + @DoNotStrip @JvmStatic public external fun useTurboModules(): Boolean + @DoNotStrip @JvmStatic public external fun override(provider: Any) @DoNotStrip @JvmStatic public external fun dangerouslyReset() diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt index 83264bfcf22e99..7a844d2e333707 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<72e3e7b5a53e64f8f48310d8b07cdf76>> + * @generated SignedSource<<8155a9c1309145fefdb19feb33c241db>> */ /** @@ -41,6 +41,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi override fun enableBackgroundStyleApplicator(): Boolean = true + override fun enableBridgelessArchitecture(): Boolean = false + override fun enableCleanTextInputYogaNode(): Boolean = false override fun enableDeletionOfUnmountedViews(): Boolean = false @@ -51,6 +53,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi override fun enableFabricLogs(): Boolean = false + override fun enableFabricRenderer(): Boolean = false + override fun enableFabricRendererExclusively(): Boolean = false override fun enableGranularShadowTreeStateReconciliation(): Boolean = false @@ -124,4 +128,6 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi override fun useStateAlignmentMechanism(): Boolean = false override fun useTurboModuleInterop(): Boolean = false + + override fun useTurboModules(): Boolean = false } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt index 119d2f0b8cf0ff..c855a74f49e353 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<5e4e474b62996caec15bbf8af9622a0a>> */ /** @@ -33,11 +33,13 @@ public class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcces private var enableAndroidLineHeightCenteringCache: Boolean? = null private var enableAndroidMixBlendModePropCache: Boolean? = null private var enableBackgroundStyleApplicatorCache: Boolean? = null + private var enableBridgelessArchitectureCache: Boolean? = null private var enableCleanTextInputYogaNodeCache: Boolean? = null private var enableDeletionOfUnmountedViewsCache: Boolean? = null private var enableEagerRootViewAttachmentCache: Boolean? = null private var enableEventEmitterRetentionDuringGesturesOnAndroidCache: Boolean? = null private var enableFabricLogsCache: Boolean? = null + private var enableFabricRendererCache: Boolean? = null private var enableFabricRendererExclusivelyCache: Boolean? = null private var enableGranularShadowTreeStateReconciliationCache: Boolean? = null private var enableIOSViewClipToPaddingBoxCache: Boolean? = null @@ -75,6 +77,7 @@ public class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcces private var useRuntimeShadowNodeReferenceUpdateOnLayoutCache: Boolean? = null private var useStateAlignmentMechanismCache: Boolean? = null private var useTurboModuleInteropCache: Boolean? = null + private var useTurboModulesCache: Boolean? = null override fun commonTestFlag(): Boolean { var cached = commonTestFlagCache @@ -166,6 +169,16 @@ public class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun enableBridgelessArchitecture(): Boolean { + var cached = enableBridgelessArchitectureCache + if (cached == null) { + cached = currentProvider.enableBridgelessArchitecture() + accessedFeatureFlags.add("enableBridgelessArchitecture") + enableBridgelessArchitectureCache = cached + } + return cached + } + override fun enableCleanTextInputYogaNode(): Boolean { var cached = enableCleanTextInputYogaNodeCache if (cached == null) { @@ -216,6 +229,16 @@ public class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun enableFabricRenderer(): Boolean { + var cached = enableFabricRendererCache + if (cached == null) { + cached = currentProvider.enableFabricRenderer() + accessedFeatureFlags.add("enableFabricRenderer") + enableFabricRendererCache = cached + } + return cached + } + override fun enableFabricRendererExclusively(): Boolean { var cached = enableFabricRendererExclusivelyCache if (cached == null) { @@ -586,6 +609,16 @@ public class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun useTurboModules(): Boolean { + var cached = useTurboModulesCache + if (cached == null) { + cached = currentProvider.useTurboModules() + accessedFeatureFlags.add("useTurboModules") + useTurboModulesCache = cached + } + return cached + } + override fun override(provider: ReactNativeFeatureFlagsProvider) { if (accessedFeatureFlags.isNotEmpty()) { val accessedFeatureFlagsStr = accessedFeatureFlags.joinToString(separator = ", ") { it } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt index c3eb8fbe97c4f1..2098020dd96e33 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<0f6129ccbcf7857785724f14b41edb2d>> + * @generated SignedSource<<97eddbbd75ff7cfd0f1c905d72e9eafd>> */ /** @@ -41,6 +41,8 @@ public interface ReactNativeFeatureFlagsProvider { @DoNotStrip public fun enableBackgroundStyleApplicator(): Boolean + @DoNotStrip public fun enableBridgelessArchitecture(): Boolean + @DoNotStrip public fun enableCleanTextInputYogaNode(): Boolean @DoNotStrip public fun enableDeletionOfUnmountedViews(): Boolean @@ -51,6 +53,8 @@ public interface ReactNativeFeatureFlagsProvider { @DoNotStrip public fun enableFabricLogs(): Boolean + @DoNotStrip public fun enableFabricRenderer(): Boolean + @DoNotStrip public fun enableFabricRendererExclusively(): Boolean @DoNotStrip public fun enableGranularShadowTreeStateReconciliation(): Boolean @@ -124,4 +128,6 @@ public interface ReactNativeFeatureFlagsProvider { @DoNotStrip public fun useStateAlignmentMechanism(): Boolean @DoNotStrip public fun useTurboModuleInterop(): Boolean + + @DoNotStrip public fun useTurboModules(): Boolean } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeNewArchitectureFeatureFlagsDefaults.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeNewArchitectureFeatureFlagsDefaults.kt index 261b17d4b23651..97e20f572a0256 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeNewArchitectureFeatureFlagsDefaults.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeNewArchitectureFeatureFlagsDefaults.kt @@ -1,10 +1,14 @@ -// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + @file:Suppress("DEPRECATION") // We want to use ReactFeatureFlags here specifically package com.facebook.react.internal.featureflags -import com.facebook.react.config.ReactFeatureFlags - /** * This class initializes default values for ReactNativeFeatureFlags when the New architecture is * enabled. This class is meant to be overrode only by internal apps migrating to the new @@ -12,23 +16,14 @@ import com.facebook.react.config.ReactFeatureFlags * * NOTE: Be aware that as a side effect this class also modifies static fields in {@link * com.facebook.react.config.ReactFeatureFlags} when newArchitectureEnabled is true. + * + * When the new architecture is enabled, we want to set the default values of the flags for Fabric, + * TurboModules and Bridgeless as enabled by default. */ public open class ReactNativeNewArchitectureFeatureFlagsDefaults( private val newArchitectureEnabled: Boolean = true ) : ReactNativeFeatureFlagsDefaults() { - init { - if (newArchitectureEnabled) { - // When the new architecture is enabled, we want to set the default values of the flags for - // Fabric, TurboModules and Bridgeless as enabled by default. - // ReactFeatureFlags is deprecated and will be deleted in 0.77, this code is temporary to - // support the new architecture before 0.77 cut. - ReactFeatureFlags.enableFabricRenderer = true - ReactFeatureFlags.useTurboModules = true - ReactFeatureFlags.enableBridgelessArchitecture = true - } - } - override fun batchRenderingUpdatesInEventLoop(): Boolean = newArchitectureEnabled || super.batchRenderingUpdatesInEventLoop() @@ -37,7 +32,13 @@ public open class ReactNativeNewArchitectureFeatureFlagsDefaults( override fun useModernRuntimeScheduler(): Boolean = true + override fun enableBridgelessArchitecture(): Boolean = newArchitectureEnabled + override fun enableMicrotasks(): Boolean = true + override fun enableFabricRenderer(): Boolean = newArchitectureEnabled + override fun useNativeViewConfigsInBridgelessMode(): Boolean = true + + override fun useTurboModules(): Boolean = newArchitectureEnabled } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java index d4f70672e9ff25..f45479043bfaa0 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java @@ -13,7 +13,6 @@ import com.facebook.infer.annotation.Nullsafe; import com.facebook.react.common.MapBuilder; import com.facebook.react.common.build.ReactBuildConfig; -import com.facebook.react.config.ReactFeatureFlags; import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags; import java.util.ArrayList; import java.util.HashMap; @@ -135,7 +134,8 @@ private static void validateDirectEventNames( Map viewManagerBubblingEvents = viewManager.getExportedCustomBubblingEventTypeConstants(); if (viewManagerBubblingEvents != null) { - if (ReactFeatureFlags.enableFabricRenderer && ReactNativeFeatureFlags.useFabricInterop()) { + if (ReactNativeFeatureFlags.enableFabricRenderer() + && ReactNativeFeatureFlags.useFabricInterop()) { // For Fabric, events needs to be fired with a "top" prefix. // For the sake of Fabric Interop, here we normalize events adding "top" in their // name if the user hasn't provided it. @@ -151,7 +151,8 @@ private static void validateDirectEventNames( Map viewManagerDirectEvents = viewManager.getExportedCustomDirectEventTypeConstants(); validateDirectEventNames(viewManager.getName(), viewManagerDirectEvents); if (viewManagerDirectEvents != null) { - if (ReactFeatureFlags.enableFabricRenderer && ReactNativeFeatureFlags.useFabricInterop()) { + if (ReactNativeFeatureFlags.enableFabricRenderer() + && ReactNativeFeatureFlags.useFabricInterop()) { // For Fabric, events needs to be fired with a "top" prefix. // For the sake of Fabric Interop, here we normalize events adding "top" in their // name if the user hasn't provided it. diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt index 318dddfd717ff2..b19a9a137b7759 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt @@ -47,7 +47,7 @@ import com.facebook.react.bridge.ReadableMap import com.facebook.react.common.annotations.UnstableReactNativeAPI import com.facebook.react.common.annotations.VisibleForTesting import com.facebook.react.common.build.ReactBuildConfig -import com.facebook.react.config.ReactFeatureFlags +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags.loadVectorDrawablesOnImages import com.facebook.react.modules.fresco.ReactNetworkImageRequest import com.facebook.react.uimanager.BackgroundStyleApplicator @@ -590,7 +590,7 @@ public class ReactImageView( // 3. ReactImageView detects the null src; displays a warning in LogBox (via this code). // 3. LogBox renders an , which fabric preallocates. // 4. Rinse and repeat. - if (ReactBuildConfig.DEBUG && !ReactFeatureFlags.enableBridgelessArchitecture) { + if (ReactBuildConfig.DEBUG && !ReactNativeFeatureFlags.enableBridgelessArchitecture()) { RNLog.w(context as ReactContext, "ReactImageView: Image source \"$uri\" doesn't exist") } } diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp index 326213c7d46a5e..8fe3b974a72e78 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<43b92dd984e985e09e3efad9c8fedf44>> + * @generated SignedSource<<383e9749c506bc2326455ef907e06a4c>> */ /** @@ -93,6 +93,12 @@ class ReactNativeFeatureFlagsProviderHolder return method(javaProvider_); } + bool enableBridgelessArchitecture() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("enableBridgelessArchitecture"); + return method(javaProvider_); + } + bool enableCleanTextInputYogaNode() override { static const auto method = getReactNativeFeatureFlagsProviderJavaClass()->getMethod("enableCleanTextInputYogaNode"); @@ -123,6 +129,12 @@ class ReactNativeFeatureFlagsProviderHolder return method(javaProvider_); } + bool enableFabricRenderer() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("enableFabricRenderer"); + return method(javaProvider_); + } + bool enableFabricRendererExclusively() override { static const auto method = getReactNativeFeatureFlagsProviderJavaClass()->getMethod("enableFabricRendererExclusively"); @@ -345,6 +357,12 @@ class ReactNativeFeatureFlagsProviderHolder return method(javaProvider_); } + bool useTurboModules() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("useTurboModules"); + return method(javaProvider_); + } + private: jni::global_ref javaProvider_; }; @@ -394,6 +412,11 @@ bool JReactNativeFeatureFlagsCxxInterop::enableBackgroundStyleApplicator( return ReactNativeFeatureFlags::enableBackgroundStyleApplicator(); } +bool JReactNativeFeatureFlagsCxxInterop::enableBridgelessArchitecture( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::enableBridgelessArchitecture(); +} + bool JReactNativeFeatureFlagsCxxInterop::enableCleanTextInputYogaNode( facebook::jni::alias_ref /*unused*/) { return ReactNativeFeatureFlags::enableCleanTextInputYogaNode(); @@ -419,6 +442,11 @@ bool JReactNativeFeatureFlagsCxxInterop::enableFabricLogs( return ReactNativeFeatureFlags::enableFabricLogs(); } +bool JReactNativeFeatureFlagsCxxInterop::enableFabricRenderer( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::enableFabricRenderer(); +} + bool JReactNativeFeatureFlagsCxxInterop::enableFabricRendererExclusively( facebook::jni::alias_ref /*unused*/) { return ReactNativeFeatureFlags::enableFabricRendererExclusively(); @@ -604,6 +632,11 @@ bool JReactNativeFeatureFlagsCxxInterop::useTurboModuleInterop( return ReactNativeFeatureFlags::useTurboModuleInterop(); } +bool JReactNativeFeatureFlagsCxxInterop::useTurboModules( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::useTurboModules(); +} + void JReactNativeFeatureFlagsCxxInterop::override( facebook::jni::alias_ref /*unused*/, jni::alias_ref provider) { @@ -648,6 +681,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() { makeNativeMethod( "enableBackgroundStyleApplicator", JReactNativeFeatureFlagsCxxInterop::enableBackgroundStyleApplicator), + makeNativeMethod( + "enableBridgelessArchitecture", + JReactNativeFeatureFlagsCxxInterop::enableBridgelessArchitecture), makeNativeMethod( "enableCleanTextInputYogaNode", JReactNativeFeatureFlagsCxxInterop::enableCleanTextInputYogaNode), @@ -663,6 +699,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() { makeNativeMethod( "enableFabricLogs", JReactNativeFeatureFlagsCxxInterop::enableFabricLogs), + makeNativeMethod( + "enableFabricRenderer", + JReactNativeFeatureFlagsCxxInterop::enableFabricRenderer), makeNativeMethod( "enableFabricRendererExclusively", JReactNativeFeatureFlagsCxxInterop::enableFabricRendererExclusively), @@ -774,6 +813,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() { makeNativeMethod( "useTurboModuleInterop", JReactNativeFeatureFlagsCxxInterop::useTurboModuleInterop), + makeNativeMethod( + "useTurboModules", + JReactNativeFeatureFlagsCxxInterop::useTurboModules), }); } diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h index 02bb54491becc0..c478530d81b695 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<54c94f9bfa07e41d978899dbce03469f>> + * @generated SignedSource<<293ed423f770e7e0ba95b19eb5c28dc2>> */ /** @@ -57,6 +57,9 @@ class JReactNativeFeatureFlagsCxxInterop static bool enableBackgroundStyleApplicator( facebook::jni::alias_ref); + static bool enableBridgelessArchitecture( + facebook::jni::alias_ref); + static bool enableCleanTextInputYogaNode( facebook::jni::alias_ref); @@ -72,6 +75,9 @@ class JReactNativeFeatureFlagsCxxInterop static bool enableFabricLogs( facebook::jni::alias_ref); + static bool enableFabricRenderer( + facebook::jni::alias_ref); + static bool enableFabricRendererExclusively( facebook::jni::alias_ref); @@ -183,6 +189,9 @@ class JReactNativeFeatureFlagsCxxInterop static bool useTurboModuleInterop( facebook::jni::alias_ref); + static bool useTurboModules( + facebook::jni::alias_ref); + static void override( facebook::jni::alias_ref, jni::alias_ref provider); diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/bridge/interop/InteropModuleRegistryTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/bridge/interop/InteropModuleRegistryTest.kt index 3d4e36d6ffc09d..63ef4bef012928 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/bridge/interop/InteropModuleRegistryTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/bridge/interop/InteropModuleRegistryTest.kt @@ -11,7 +11,6 @@ package com.facebook.react.bridge.interop import com.facebook.react.common.annotations.UnstableReactNativeAPI -import com.facebook.react.config.ReactFeatureFlags import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsDefaults import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests @@ -40,38 +39,28 @@ class InteropModuleRegistryTest { @Test fun shouldReturnInteropModule_withFabricDisabled_returnsFalse() { - ReactFeatureFlags.enableFabricRenderer = false + overrideFeatureFlags(false, false) assertThat(underTest.shouldReturnInteropModule(RCTEventEmitter::class.java)).isFalse() } @Test fun shouldReturnInteropModule_withFabricInteropDisabled_returnsFalse() { - ReactFeatureFlags.enableFabricRenderer = true - overrideUseFabricInteropFlag(false) + overrideFeatureFlags(false, true) assertThat(underTest.shouldReturnInteropModule(RCTEventEmitter::class.java)).isFalse() } - private fun overrideUseFabricInteropFlag(value: Boolean) { - ReactNativeFeatureFlags.override( - object : ReactNativeFeatureFlagsDefaults() { - override fun useFabricInterop(): Boolean = value - }) - } - @Test fun shouldReturnInteropModule_withUnregisteredClass_returnsFalse() { - ReactFeatureFlags.enableFabricRenderer = true - overrideUseFabricInteropFlag(true) + overrideFeatureFlags(true, true) assertThat(underTest.shouldReturnInteropModule(JSTimers::class.java)).isFalse() } @Test fun shouldReturnInteropModule_withRegisteredClass_returnsTrue() { - ReactFeatureFlags.enableFabricRenderer = true - overrideUseFabricInteropFlag(true) + overrideFeatureFlags(true, true) underTest.registerInteropModule(RCTEventEmitter::class.java, FakeRCTEventEmitter()) @@ -80,8 +69,7 @@ class InteropModuleRegistryTest { @Test fun getInteropModule_withRegisteredClassAndInvalidFlags_returnsNull() { - ReactFeatureFlags.enableFabricRenderer = false - overrideUseFabricInteropFlag(false) + overrideFeatureFlags(false, false) underTest.registerInteropModule(RCTEventEmitter::class.java, FakeRCTEventEmitter()) val interopModule = underTest.getInteropModule(RCTEventEmitter::class.java) @@ -91,8 +79,7 @@ class InteropModuleRegistryTest { @Test fun getInteropModule_withRegisteredClassAndValidFlags_returnsInteropModule() { - ReactFeatureFlags.enableFabricRenderer = true - overrideUseFabricInteropFlag(true) + overrideFeatureFlags(true, true) underTest.registerInteropModule(RCTEventEmitter::class.java, FakeRCTEventEmitter()) val interopModule = underTest.getInteropModule(RCTEventEmitter::class.java) @@ -102,10 +89,18 @@ class InteropModuleRegistryTest { @Test fun getInteropModule_withUnregisteredClass_returnsNull() { - ReactFeatureFlags.enableFabricRenderer = true - overrideUseFabricInteropFlag(true) + overrideFeatureFlags(true, true) val missingModule = underTest.getInteropModule(JSTimers::class.java) assertThat(missingModule).isNull() } + + private fun overrideFeatureFlags(useFabricInterop: Boolean, enableFabricRenderer: Boolean) { + ReactNativeFeatureFlags.override( + object : ReactNativeFeatureFlagsDefaults() { + override fun useFabricInterop(): Boolean = useFabricInterop + + override fun enableFabricRenderer(): Boolean = enableFabricRenderer + }) + } } 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 dafaf8b1761b97..fb6f22e44ed1de 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 @@ -12,6 +12,7 @@ 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 junit.framework.TestCase import org.assertj.core.api.Assertions @@ -37,6 +38,7 @@ class DeviceInfoModuleTest : TestCase() { @Before public override fun setUp() { + ReactNativeFeatureFlagsForTests.setUp() fakePortraitDisplayMetrics = JavaOnlyMap() fakePortraitDisplayMetrics.putInt("width", 100) fakePortraitDisplayMetrics.putInt("height", 200) diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/UIManagerModuleConstantsTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/UIManagerModuleConstantsTest.kt index ac990680dfa8f0..e1bf44598acae1 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/UIManagerModuleConstantsTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/UIManagerModuleConstantsTest.kt @@ -10,6 +10,7 @@ package com.facebook.react.uimanager import android.view.View import com.facebook.react.bridge.BridgeReactContext import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests import org.assertj.core.api.Assertions import org.assertj.core.data.MapEntry import org.junit.Before @@ -54,6 +55,7 @@ class UIManagerModuleConstantsTest { @Before fun setUp() { + ReactNativeFeatureFlagsForTests.setUp() reactContext = BridgeReactContext(RuntimeEnvironment.getApplication()) } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp index 15b4f233ca2c80..09d76c096ffaf7 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<85cdb5a41317e9671cc23b2c7345c04d>> */ /** @@ -57,6 +57,10 @@ bool ReactNativeFeatureFlags::enableBackgroundStyleApplicator() { return getAccessor().enableBackgroundStyleApplicator(); } +bool ReactNativeFeatureFlags::enableBridgelessArchitecture() { + return getAccessor().enableBridgelessArchitecture(); +} + bool ReactNativeFeatureFlags::enableCleanTextInputYogaNode() { return getAccessor().enableCleanTextInputYogaNode(); } @@ -77,6 +81,10 @@ bool ReactNativeFeatureFlags::enableFabricLogs() { return getAccessor().enableFabricLogs(); } +bool ReactNativeFeatureFlags::enableFabricRenderer() { + return getAccessor().enableFabricRenderer(); +} + bool ReactNativeFeatureFlags::enableFabricRendererExclusively() { return getAccessor().enableFabricRendererExclusively(); } @@ -225,6 +233,10 @@ bool ReactNativeFeatureFlags::useTurboModuleInterop() { return getAccessor().useTurboModuleInterop(); } +bool ReactNativeFeatureFlags::useTurboModules() { + return getAccessor().useTurboModules(); +} + void ReactNativeFeatureFlags::override( std::unique_ptr provider) { getAccessor().override(std::move(provider)); diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h index 646a250ad70c0d..c844d4651574f6 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<2cde82b474ab94cd05bdf5e04e700c13>> + * @generated SignedSource<> */ /** @@ -82,6 +82,11 @@ class ReactNativeFeatureFlags { */ RN_EXPORT static bool enableBackgroundStyleApplicator(); + /** + * Feature flag to enable the new bridgeless architecture. Note: Enabling this will force enable the following flags: `useTurboModules` & `enableFabricRenderer. + */ + RN_EXPORT static bool enableBridgelessArchitecture(); + /** * Clean yoga node when does not change. */ @@ -107,6 +112,11 @@ class ReactNativeFeatureFlags { */ RN_EXPORT static bool enableFabricLogs(); + /** + * Enables the use of the Fabric renderer in the whole app. + */ + RN_EXPORT static bool enableFabricRenderer(); + /** * When the app is completely migrated to Fabric, set this flag to true to disable parts of Paper infrastructure that are not needed anymore but consume memory and CPU. Specifically, UIViewOperationQueue and EventDispatcherImpl will no longer work as they will not subscribe to ReactChoreographer for updates. */ @@ -292,6 +302,11 @@ class ReactNativeFeatureFlags { */ RN_EXPORT static bool useTurboModuleInterop(); + /** + * When enabled, NativeModules will be executed by using the TurboModule system + */ + RN_EXPORT static bool useTurboModules(); + /** * Overrides the feature flags with the ones provided by the given provider * (generally one that extends `ReactNativeFeatureFlagsDefaults`). diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp index 5fa57e0f615624..09f80847d1fd2e 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -191,6 +191,24 @@ bool ReactNativeFeatureFlagsAccessor::enableBackgroundStyleApplicator() { return flagValue.value(); } +bool ReactNativeFeatureFlagsAccessor::enableBridgelessArchitecture() { + auto flagValue = enableBridgelessArchitecture_.load(); + + if (!flagValue.has_value()) { + // This block is not exclusive but it is not necessary. + // If multiple threads try to initialize the feature flag, we would only + // be accessing the provider multiple times but the end state of this + // instance and the returned flag value would be the same. + + markFlagAsAccessed(9, "enableBridgelessArchitecture"); + + flagValue = currentProvider_->enableBridgelessArchitecture(); + enableBridgelessArchitecture_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::enableCleanTextInputYogaNode() { auto flagValue = enableCleanTextInputYogaNode_.load(); @@ -200,7 +218,7 @@ bool ReactNativeFeatureFlagsAccessor::enableCleanTextInputYogaNode() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(9, "enableCleanTextInputYogaNode"); + markFlagAsAccessed(10, "enableCleanTextInputYogaNode"); flagValue = currentProvider_->enableCleanTextInputYogaNode(); enableCleanTextInputYogaNode_ = flagValue; @@ -218,7 +236,7 @@ bool ReactNativeFeatureFlagsAccessor::enableDeletionOfUnmountedViews() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(10, "enableDeletionOfUnmountedViews"); + markFlagAsAccessed(11, "enableDeletionOfUnmountedViews"); flagValue = currentProvider_->enableDeletionOfUnmountedViews(); enableDeletionOfUnmountedViews_ = flagValue; @@ -236,7 +254,7 @@ bool ReactNativeFeatureFlagsAccessor::enableEagerRootViewAttachment() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(11, "enableEagerRootViewAttachment"); + markFlagAsAccessed(12, "enableEagerRootViewAttachment"); flagValue = currentProvider_->enableEagerRootViewAttachment(); enableEagerRootViewAttachment_ = flagValue; @@ -254,7 +272,7 @@ bool ReactNativeFeatureFlagsAccessor::enableEventEmitterRetentionDuringGesturesO // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(12, "enableEventEmitterRetentionDuringGesturesOnAndroid"); + markFlagAsAccessed(13, "enableEventEmitterRetentionDuringGesturesOnAndroid"); flagValue = currentProvider_->enableEventEmitterRetentionDuringGesturesOnAndroid(); enableEventEmitterRetentionDuringGesturesOnAndroid_ = flagValue; @@ -272,7 +290,7 @@ bool ReactNativeFeatureFlagsAccessor::enableFabricLogs() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(13, "enableFabricLogs"); + markFlagAsAccessed(14, "enableFabricLogs"); flagValue = currentProvider_->enableFabricLogs(); enableFabricLogs_ = flagValue; @@ -281,6 +299,24 @@ bool ReactNativeFeatureFlagsAccessor::enableFabricLogs() { return flagValue.value(); } +bool ReactNativeFeatureFlagsAccessor::enableFabricRenderer() { + auto flagValue = enableFabricRenderer_.load(); + + if (!flagValue.has_value()) { + // This block is not exclusive but it is not necessary. + // If multiple threads try to initialize the feature flag, we would only + // be accessing the provider multiple times but the end state of this + // instance and the returned flag value would be the same. + + markFlagAsAccessed(15, "enableFabricRenderer"); + + flagValue = currentProvider_->enableFabricRenderer(); + enableFabricRenderer_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::enableFabricRendererExclusively() { auto flagValue = enableFabricRendererExclusively_.load(); @@ -290,7 +326,7 @@ bool ReactNativeFeatureFlagsAccessor::enableFabricRendererExclusively() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(14, "enableFabricRendererExclusively"); + markFlagAsAccessed(16, "enableFabricRendererExclusively"); flagValue = currentProvider_->enableFabricRendererExclusively(); enableFabricRendererExclusively_ = flagValue; @@ -308,7 +344,7 @@ bool ReactNativeFeatureFlagsAccessor::enableGranularShadowTreeStateReconciliatio // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(15, "enableGranularShadowTreeStateReconciliation"); + markFlagAsAccessed(17, "enableGranularShadowTreeStateReconciliation"); flagValue = currentProvider_->enableGranularShadowTreeStateReconciliation(); enableGranularShadowTreeStateReconciliation_ = flagValue; @@ -326,7 +362,7 @@ bool ReactNativeFeatureFlagsAccessor::enableIOSViewClipToPaddingBox() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(16, "enableIOSViewClipToPaddingBox"); + markFlagAsAccessed(18, "enableIOSViewClipToPaddingBox"); flagValue = currentProvider_->enableIOSViewClipToPaddingBox(); enableIOSViewClipToPaddingBox_ = flagValue; @@ -344,7 +380,7 @@ bool ReactNativeFeatureFlagsAccessor::enableLayoutAnimationsOnIOS() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(17, "enableLayoutAnimationsOnIOS"); + markFlagAsAccessed(19, "enableLayoutAnimationsOnIOS"); flagValue = currentProvider_->enableLayoutAnimationsOnIOS(); enableLayoutAnimationsOnIOS_ = flagValue; @@ -362,7 +398,7 @@ bool ReactNativeFeatureFlagsAccessor::enableLongTaskAPI() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(18, "enableLongTaskAPI"); + markFlagAsAccessed(20, "enableLongTaskAPI"); flagValue = currentProvider_->enableLongTaskAPI(); enableLongTaskAPI_ = flagValue; @@ -380,7 +416,7 @@ bool ReactNativeFeatureFlagsAccessor::enableMicrotasks() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(19, "enableMicrotasks"); + markFlagAsAccessed(21, "enableMicrotasks"); flagValue = currentProvider_->enableMicrotasks(); enableMicrotasks_ = flagValue; @@ -398,7 +434,7 @@ bool ReactNativeFeatureFlagsAccessor::enablePreciseSchedulingForPremountItemsOnA // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(20, "enablePreciseSchedulingForPremountItemsOnAndroid"); + markFlagAsAccessed(22, "enablePreciseSchedulingForPremountItemsOnAndroid"); flagValue = currentProvider_->enablePreciseSchedulingForPremountItemsOnAndroid(); enablePreciseSchedulingForPremountItemsOnAndroid_ = flagValue; @@ -416,7 +452,7 @@ bool ReactNativeFeatureFlagsAccessor::enablePropsUpdateReconciliationAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(21, "enablePropsUpdateReconciliationAndroid"); + markFlagAsAccessed(23, "enablePropsUpdateReconciliationAndroid"); flagValue = currentProvider_->enablePropsUpdateReconciliationAndroid(); enablePropsUpdateReconciliationAndroid_ = flagValue; @@ -434,7 +470,7 @@ bool ReactNativeFeatureFlagsAccessor::enableReportEventPaintTime() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(22, "enableReportEventPaintTime"); + markFlagAsAccessed(24, "enableReportEventPaintTime"); flagValue = currentProvider_->enableReportEventPaintTime(); enableReportEventPaintTime_ = flagValue; @@ -452,7 +488,7 @@ bool ReactNativeFeatureFlagsAccessor::enableSynchronousStateUpdates() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(23, "enableSynchronousStateUpdates"); + markFlagAsAccessed(25, "enableSynchronousStateUpdates"); flagValue = currentProvider_->enableSynchronousStateUpdates(); enableSynchronousStateUpdates_ = flagValue; @@ -470,7 +506,7 @@ bool ReactNativeFeatureFlagsAccessor::enableTextPreallocationOptimisation() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(24, "enableTextPreallocationOptimisation"); + markFlagAsAccessed(26, "enableTextPreallocationOptimisation"); flagValue = currentProvider_->enableTextPreallocationOptimisation(); enableTextPreallocationOptimisation_ = flagValue; @@ -488,7 +524,7 @@ bool ReactNativeFeatureFlagsAccessor::enableUIConsistency() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(25, "enableUIConsistency"); + markFlagAsAccessed(27, "enableUIConsistency"); flagValue = currentProvider_->enableUIConsistency(); enableUIConsistency_ = flagValue; @@ -506,7 +542,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecycling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(26, "enableViewRecycling"); + markFlagAsAccessed(28, "enableViewRecycling"); flagValue = currentProvider_->enableViewRecycling(); enableViewRecycling_ = flagValue; @@ -524,7 +560,7 @@ bool ReactNativeFeatureFlagsAccessor::excludeYogaFromRawProps() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(27, "excludeYogaFromRawProps"); + markFlagAsAccessed(29, "excludeYogaFromRawProps"); flagValue = currentProvider_->excludeYogaFromRawProps(); excludeYogaFromRawProps_ = flagValue; @@ -542,7 +578,7 @@ bool ReactNativeFeatureFlagsAccessor::fetchImagesInViewPreallocation() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(28, "fetchImagesInViewPreallocation"); + markFlagAsAccessed(30, "fetchImagesInViewPreallocation"); flagValue = currentProvider_->fetchImagesInViewPreallocation(); fetchImagesInViewPreallocation_ = flagValue; @@ -560,7 +596,7 @@ bool ReactNativeFeatureFlagsAccessor::fixMappingOfEventPrioritiesBetweenFabricAn // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(29, "fixMappingOfEventPrioritiesBetweenFabricAndReact"); + markFlagAsAccessed(31, "fixMappingOfEventPrioritiesBetweenFabricAndReact"); flagValue = currentProvider_->fixMappingOfEventPrioritiesBetweenFabricAndReact(); fixMappingOfEventPrioritiesBetweenFabricAndReact_ = flagValue; @@ -578,7 +614,7 @@ bool ReactNativeFeatureFlagsAccessor::fixMountingCoordinatorReportedPendingTrans // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(30, "fixMountingCoordinatorReportedPendingTransactionsOnAndroid"); + markFlagAsAccessed(32, "fixMountingCoordinatorReportedPendingTransactionsOnAndroid"); flagValue = currentProvider_->fixMountingCoordinatorReportedPendingTransactionsOnAndroid(); fixMountingCoordinatorReportedPendingTransactionsOnAndroid_ = flagValue; @@ -596,7 +632,7 @@ bool ReactNativeFeatureFlagsAccessor::forceBatchingMountItemsOnAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(31, "forceBatchingMountItemsOnAndroid"); + markFlagAsAccessed(33, "forceBatchingMountItemsOnAndroid"); flagValue = currentProvider_->forceBatchingMountItemsOnAndroid(); forceBatchingMountItemsOnAndroid_ = flagValue; @@ -614,7 +650,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxEnabledDebug() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(32, "fuseboxEnabledDebug"); + markFlagAsAccessed(34, "fuseboxEnabledDebug"); flagValue = currentProvider_->fuseboxEnabledDebug(); fuseboxEnabledDebug_ = flagValue; @@ -632,7 +668,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxEnabledRelease() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(33, "fuseboxEnabledRelease"); + markFlagAsAccessed(35, "fuseboxEnabledRelease"); flagValue = currentProvider_->fuseboxEnabledRelease(); fuseboxEnabledRelease_ = flagValue; @@ -650,7 +686,7 @@ bool ReactNativeFeatureFlagsAccessor::initEagerTurboModulesOnNativeModulesQueueA // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(34, "initEagerTurboModulesOnNativeModulesQueueAndroid"); + markFlagAsAccessed(36, "initEagerTurboModulesOnNativeModulesQueueAndroid"); flagValue = currentProvider_->initEagerTurboModulesOnNativeModulesQueueAndroid(); initEagerTurboModulesOnNativeModulesQueueAndroid_ = flagValue; @@ -668,7 +704,7 @@ bool ReactNativeFeatureFlagsAccessor::lazyAnimationCallbacks() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(35, "lazyAnimationCallbacks"); + markFlagAsAccessed(37, "lazyAnimationCallbacks"); flagValue = currentProvider_->lazyAnimationCallbacks(); lazyAnimationCallbacks_ = flagValue; @@ -686,7 +722,7 @@ bool ReactNativeFeatureFlagsAccessor::loadVectorDrawablesOnImages() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(36, "loadVectorDrawablesOnImages"); + markFlagAsAccessed(38, "loadVectorDrawablesOnImages"); flagValue = currentProvider_->loadVectorDrawablesOnImages(); loadVectorDrawablesOnImages_ = flagValue; @@ -704,7 +740,7 @@ bool ReactNativeFeatureFlagsAccessor::removeNestedCallsToDispatchMountItemsOnAnd // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(37, "removeNestedCallsToDispatchMountItemsOnAndroid"); + markFlagAsAccessed(39, "removeNestedCallsToDispatchMountItemsOnAndroid"); flagValue = currentProvider_->removeNestedCallsToDispatchMountItemsOnAndroid(); removeNestedCallsToDispatchMountItemsOnAndroid_ = flagValue; @@ -722,7 +758,7 @@ bool ReactNativeFeatureFlagsAccessor::setAndroidLayoutDirection() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(38, "setAndroidLayoutDirection"); + markFlagAsAccessed(40, "setAndroidLayoutDirection"); flagValue = currentProvider_->setAndroidLayoutDirection(); setAndroidLayoutDirection_ = flagValue; @@ -740,7 +776,7 @@ bool ReactNativeFeatureFlagsAccessor::traceTurboModulePromiseRejectionsOnAndroid // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(39, "traceTurboModulePromiseRejectionsOnAndroid"); + markFlagAsAccessed(41, "traceTurboModulePromiseRejectionsOnAndroid"); flagValue = currentProvider_->traceTurboModulePromiseRejectionsOnAndroid(); traceTurboModulePromiseRejectionsOnAndroid_ = flagValue; @@ -758,7 +794,7 @@ bool ReactNativeFeatureFlagsAccessor::useFabricInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(40, "useFabricInterop"); + markFlagAsAccessed(42, "useFabricInterop"); flagValue = currentProvider_->useFabricInterop(); useFabricInterop_ = flagValue; @@ -776,7 +812,7 @@ bool ReactNativeFeatureFlagsAccessor::useImmediateExecutorInAndroidBridgeless() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(41, "useImmediateExecutorInAndroidBridgeless"); + markFlagAsAccessed(43, "useImmediateExecutorInAndroidBridgeless"); flagValue = currentProvider_->useImmediateExecutorInAndroidBridgeless(); useImmediateExecutorInAndroidBridgeless_ = flagValue; @@ -794,7 +830,7 @@ bool ReactNativeFeatureFlagsAccessor::useModernRuntimeScheduler() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(42, "useModernRuntimeScheduler"); + markFlagAsAccessed(44, "useModernRuntimeScheduler"); flagValue = currentProvider_->useModernRuntimeScheduler(); useModernRuntimeScheduler_ = flagValue; @@ -812,7 +848,7 @@ bool ReactNativeFeatureFlagsAccessor::useNativeViewConfigsInBridgelessMode() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(43, "useNativeViewConfigsInBridgelessMode"); + markFlagAsAccessed(45, "useNativeViewConfigsInBridgelessMode"); flagValue = currentProvider_->useNativeViewConfigsInBridgelessMode(); useNativeViewConfigsInBridgelessMode_ = flagValue; @@ -830,7 +866,7 @@ bool ReactNativeFeatureFlagsAccessor::useNewReactImageViewBackgroundDrawing() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(44, "useNewReactImageViewBackgroundDrawing"); + markFlagAsAccessed(46, "useNewReactImageViewBackgroundDrawing"); flagValue = currentProvider_->useNewReactImageViewBackgroundDrawing(); useNewReactImageViewBackgroundDrawing_ = flagValue; @@ -848,7 +884,7 @@ bool ReactNativeFeatureFlagsAccessor::useOptimisedViewPreallocationOnAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(45, "useOptimisedViewPreallocationOnAndroid"); + markFlagAsAccessed(47, "useOptimisedViewPreallocationOnAndroid"); flagValue = currentProvider_->useOptimisedViewPreallocationOnAndroid(); useOptimisedViewPreallocationOnAndroid_ = flagValue; @@ -866,7 +902,7 @@ bool ReactNativeFeatureFlagsAccessor::useOptimizedEventBatchingOnAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(46, "useOptimizedEventBatchingOnAndroid"); + markFlagAsAccessed(48, "useOptimizedEventBatchingOnAndroid"); flagValue = currentProvider_->useOptimizedEventBatchingOnAndroid(); useOptimizedEventBatchingOnAndroid_ = flagValue; @@ -884,7 +920,7 @@ bool ReactNativeFeatureFlagsAccessor::useRuntimeShadowNodeReferenceUpdate() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(47, "useRuntimeShadowNodeReferenceUpdate"); + markFlagAsAccessed(49, "useRuntimeShadowNodeReferenceUpdate"); flagValue = currentProvider_->useRuntimeShadowNodeReferenceUpdate(); useRuntimeShadowNodeReferenceUpdate_ = flagValue; @@ -902,7 +938,7 @@ bool ReactNativeFeatureFlagsAccessor::useRuntimeShadowNodeReferenceUpdateOnLayou // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(48, "useRuntimeShadowNodeReferenceUpdateOnLayout"); + markFlagAsAccessed(50, "useRuntimeShadowNodeReferenceUpdateOnLayout"); flagValue = currentProvider_->useRuntimeShadowNodeReferenceUpdateOnLayout(); useRuntimeShadowNodeReferenceUpdateOnLayout_ = flagValue; @@ -920,7 +956,7 @@ bool ReactNativeFeatureFlagsAccessor::useStateAlignmentMechanism() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(49, "useStateAlignmentMechanism"); + markFlagAsAccessed(51, "useStateAlignmentMechanism"); flagValue = currentProvider_->useStateAlignmentMechanism(); useStateAlignmentMechanism_ = flagValue; @@ -938,7 +974,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModuleInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(50, "useTurboModuleInterop"); + markFlagAsAccessed(52, "useTurboModuleInterop"); flagValue = currentProvider_->useTurboModuleInterop(); useTurboModuleInterop_ = flagValue; @@ -947,6 +983,24 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModuleInterop() { return flagValue.value(); } +bool ReactNativeFeatureFlagsAccessor::useTurboModules() { + auto flagValue = useTurboModules_.load(); + + if (!flagValue.has_value()) { + // This block is not exclusive but it is not necessary. + // If multiple threads try to initialize the feature flag, we would only + // be accessing the provider multiple times but the end state of this + // instance and the returned flag value would be the same. + + markFlagAsAccessed(53, "useTurboModules"); + + flagValue = currentProvider_->useTurboModules(); + useTurboModules_ = flagValue; + } + + return flagValue.value(); +} + void ReactNativeFeatureFlagsAccessor::override( std::unique_ptr provider) { if (wasOverridden_) { diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h index bb5798df4dd8e5..c7db3980bfcc49 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<3273d68bcf8118b939e11dde654c3e34>> + * @generated SignedSource<<5f2c9b500fc26e68d56efae08b15575f>> */ /** @@ -40,11 +40,13 @@ class ReactNativeFeatureFlagsAccessor { bool enableAndroidLineHeightCentering(); bool enableAndroidMixBlendModeProp(); bool enableBackgroundStyleApplicator(); + bool enableBridgelessArchitecture(); bool enableCleanTextInputYogaNode(); bool enableDeletionOfUnmountedViews(); bool enableEagerRootViewAttachment(); bool enableEventEmitterRetentionDuringGesturesOnAndroid(); bool enableFabricLogs(); + bool enableFabricRenderer(); bool enableFabricRendererExclusively(); bool enableGranularShadowTreeStateReconciliation(); bool enableIOSViewClipToPaddingBox(); @@ -82,6 +84,7 @@ class ReactNativeFeatureFlagsAccessor { bool useRuntimeShadowNodeReferenceUpdateOnLayout(); bool useStateAlignmentMechanism(); bool useTurboModuleInterop(); + bool useTurboModules(); void override(std::unique_ptr provider); @@ -92,7 +95,7 @@ class ReactNativeFeatureFlagsAccessor { std::unique_ptr currentProvider_; bool wasOverridden_; - std::array, 51> accessedFeatureFlags_; + std::array, 54> accessedFeatureFlags_; std::atomic> commonTestFlag_; std::atomic> allowRecursiveCommitsWithSynchronousMountOnAndroid_; @@ -103,11 +106,13 @@ class ReactNativeFeatureFlagsAccessor { std::atomic> enableAndroidLineHeightCentering_; std::atomic> enableAndroidMixBlendModeProp_; std::atomic> enableBackgroundStyleApplicator_; + std::atomic> enableBridgelessArchitecture_; std::atomic> enableCleanTextInputYogaNode_; std::atomic> enableDeletionOfUnmountedViews_; std::atomic> enableEagerRootViewAttachment_; std::atomic> enableEventEmitterRetentionDuringGesturesOnAndroid_; std::atomic> enableFabricLogs_; + std::atomic> enableFabricRenderer_; std::atomic> enableFabricRendererExclusively_; std::atomic> enableGranularShadowTreeStateReconciliation_; std::atomic> enableIOSViewClipToPaddingBox_; @@ -145,6 +150,7 @@ class ReactNativeFeatureFlagsAccessor { std::atomic> useRuntimeShadowNodeReferenceUpdateOnLayout_; std::atomic> useStateAlignmentMechanism_; std::atomic> useTurboModuleInterop_; + std::atomic> useTurboModules_; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h index 9c9bd460a4416d..5ce07698ba0d8a 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<3ba99bf8dd5f7e146e3488dbc8bc59c8>> + * @generated SignedSource<> */ /** @@ -63,6 +63,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { return true; } + bool enableBridgelessArchitecture() override { + return false; + } + bool enableCleanTextInputYogaNode() override { return false; } @@ -83,6 +87,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { return false; } + bool enableFabricRenderer() override { + return false; + } + bool enableFabricRendererExclusively() override { return false; } @@ -230,6 +238,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { bool useTurboModuleInterop() override { return false; } + + bool useTurboModules() override { + return false; + } }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h index 4e94bf6a34ff45..9fd1367f2c1b36 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<727c5ccb59b23aa6e14d58121e0007bd>> + * @generated SignedSource<> */ /** @@ -34,11 +34,13 @@ class ReactNativeFeatureFlagsProvider { virtual bool enableAndroidLineHeightCentering() = 0; virtual bool enableAndroidMixBlendModeProp() = 0; virtual bool enableBackgroundStyleApplicator() = 0; + virtual bool enableBridgelessArchitecture() = 0; virtual bool enableCleanTextInputYogaNode() = 0; virtual bool enableDeletionOfUnmountedViews() = 0; virtual bool enableEagerRootViewAttachment() = 0; virtual bool enableEventEmitterRetentionDuringGesturesOnAndroid() = 0; virtual bool enableFabricLogs() = 0; + virtual bool enableFabricRenderer() = 0; virtual bool enableFabricRendererExclusively() = 0; virtual bool enableGranularShadowTreeStateReconciliation() = 0; virtual bool enableIOSViewClipToPaddingBox() = 0; @@ -76,6 +78,7 @@ class ReactNativeFeatureFlagsProvider { virtual bool useRuntimeShadowNodeReferenceUpdateOnLayout() = 0; virtual bool useStateAlignmentMechanism() = 0; virtual bool useTurboModuleInterop() = 0; + virtual bool useTurboModules() = 0; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp index b0450475b28edc..5316822e3b2d52 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<2b484558d552981e4ec0644d75849dde>> */ /** @@ -82,6 +82,11 @@ bool NativeReactNativeFeatureFlags::enableBackgroundStyleApplicator( return ReactNativeFeatureFlags::enableBackgroundStyleApplicator(); } +bool NativeReactNativeFeatureFlags::enableBridgelessArchitecture( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::enableBridgelessArchitecture(); +} + bool NativeReactNativeFeatureFlags::enableCleanTextInputYogaNode( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::enableCleanTextInputYogaNode(); @@ -107,6 +112,11 @@ bool NativeReactNativeFeatureFlags::enableFabricLogs( return ReactNativeFeatureFlags::enableFabricLogs(); } +bool NativeReactNativeFeatureFlags::enableFabricRenderer( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::enableFabricRenderer(); +} + bool NativeReactNativeFeatureFlags::enableFabricRendererExclusively( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::enableFabricRendererExclusively(); @@ -292,4 +302,9 @@ bool NativeReactNativeFeatureFlags::useTurboModuleInterop( return ReactNativeFeatureFlags::useTurboModuleInterop(); } +bool NativeReactNativeFeatureFlags::useTurboModules( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::useTurboModules(); +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h index 7bbe7c3df81936..ded551e76c8665 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<596ec10892f79595c352d27574a0c5a3>> + * @generated SignedSource<<94afee988401ba2af586ec8fe310cb8b>> */ /** @@ -53,6 +53,8 @@ class NativeReactNativeFeatureFlags bool enableBackgroundStyleApplicator(jsi::Runtime& runtime); + bool enableBridgelessArchitecture(jsi::Runtime& runtime); + bool enableCleanTextInputYogaNode(jsi::Runtime& runtime); bool enableDeletionOfUnmountedViews(jsi::Runtime& runtime); @@ -63,6 +65,8 @@ class NativeReactNativeFeatureFlags bool enableFabricLogs(jsi::Runtime& runtime); + bool enableFabricRenderer(jsi::Runtime& runtime); + bool enableFabricRendererExclusively(jsi::Runtime& runtime); bool enableGranularShadowTreeStateReconciliation(jsi::Runtime& runtime); @@ -136,6 +140,8 @@ class NativeReactNativeFeatureFlags bool useStateAlignmentMechanism(jsi::Runtime& runtime); bool useTurboModuleInterop(jsi::Runtime& runtime); + + bool useTurboModules(jsi::Runtime& runtime); }; } // namespace facebook::react diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index b340b7c60d0d58..7dc63f49a76dde 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -115,6 +115,14 @@ const definitions: FeatureFlagDefinitions = { purpose: 'experimentation', }, }, + enableBridgelessArchitecture: { + defaultValue: false, + metadata: { + description: + 'Feature flag to enable the new bridgeless architecture. Note: Enabling this will force enable the following flags: `useTurboModules` & `enableFabricRenderer.', + purpose: 'release', + }, + }, enableCleanTextInputYogaNode: { defaultValue: false, metadata: { @@ -157,6 +165,13 @@ const definitions: FeatureFlagDefinitions = { purpose: 'operational', }, }, + enableFabricRenderer: { + defaultValue: false, + metadata: { + description: 'Enables the use of the Fabric renderer in the whole app.', + purpose: 'release', + }, + }, enableFabricRendererExclusively: { defaultValue: false, metadata: { @@ -478,6 +493,14 @@ const definitions: FeatureFlagDefinitions = { purpose: 'experimentation', }, }, + useTurboModules: { + defaultValue: false, + metadata: { + description: + 'When enabled, NativeModules will be executed by using the TurboModule system', + purpose: 'release', + }, + }, }, jsOnly: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index 3e0099e4619970..577d22448e4750 100644 --- a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> * @flow strict */ @@ -58,11 +58,13 @@ export type ReactNativeFeatureFlags = { enableAndroidLineHeightCentering: Getter, enableAndroidMixBlendModeProp: Getter, enableBackgroundStyleApplicator: Getter, + enableBridgelessArchitecture: Getter, enableCleanTextInputYogaNode: Getter, enableDeletionOfUnmountedViews: Getter, enableEagerRootViewAttachment: Getter, enableEventEmitterRetentionDuringGesturesOnAndroid: Getter, enableFabricLogs: Getter, + enableFabricRenderer: Getter, enableFabricRendererExclusively: Getter, enableGranularShadowTreeStateReconciliation: Getter, enableIOSViewClipToPaddingBox: Getter, @@ -100,6 +102,7 @@ export type ReactNativeFeatureFlags = { useRuntimeShadowNodeReferenceUpdateOnLayout: Getter, useStateAlignmentMechanism: Getter, useTurboModuleInterop: Getter, + useTurboModules: Getter, } /** @@ -218,6 +221,10 @@ export const enableAndroidMixBlendModeProp: Getter = createNativeFlagGe * Use BackgroundStyleApplicator in place of other background/border drawing code */ export const enableBackgroundStyleApplicator: Getter = createNativeFlagGetter('enableBackgroundStyleApplicator', true); +/** + * Feature flag to enable the new bridgeless architecture. Note: Enabling this will force enable the following flags: `useTurboModules` & `enableFabricRenderer. + */ +export const enableBridgelessArchitecture: Getter = createNativeFlagGetter('enableBridgelessArchitecture', false); /** * Clean yoga node when does not change. */ @@ -238,6 +245,10 @@ export const enableEventEmitterRetentionDuringGesturesOnAndroid: Getter * This feature flag enables logs for Fabric. */ export const enableFabricLogs: Getter = createNativeFlagGetter('enableFabricLogs', false); +/** + * Enables the use of the Fabric renderer in the whole app. + */ +export const enableFabricRenderer: Getter = createNativeFlagGetter('enableFabricRenderer', false); /** * When the app is completely migrated to Fabric, set this flag to true to disable parts of Paper infrastructure that are not needed anymore but consume memory and CPU. Specifically, UIViewOperationQueue and EventDispatcherImpl will no longer work as they will not subscribe to ReactChoreographer for updates. */ @@ -386,6 +397,10 @@ export const useStateAlignmentMechanism: Getter = createNativeFlagGette * In Bridgeless mode, should legacy NativeModules use the TurboModule system? */ export const useTurboModuleInterop: Getter = createNativeFlagGetter('useTurboModuleInterop', false); +/** + * When enabled, NativeModules will be executed by using the TurboModule system + */ +export const useTurboModules: Getter = createNativeFlagGetter('useTurboModules', false); /** * Overrides the feature flags with the provided methods. diff --git a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js index 7111c3dd041a3b..fb2d28bfff7d50 100644 --- a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<0467a8715892bbeca0a08681c730aeab>> * @flow strict */ @@ -32,11 +32,13 @@ export interface Spec extends TurboModule { +enableAndroidLineHeightCentering?: () => boolean; +enableAndroidMixBlendModeProp?: () => boolean; +enableBackgroundStyleApplicator?: () => boolean; + +enableBridgelessArchitecture?: () => boolean; +enableCleanTextInputYogaNode?: () => boolean; +enableDeletionOfUnmountedViews?: () => boolean; +enableEagerRootViewAttachment?: () => boolean; +enableEventEmitterRetentionDuringGesturesOnAndroid?: () => boolean; +enableFabricLogs?: () => boolean; + +enableFabricRenderer?: () => boolean; +enableFabricRendererExclusively?: () => boolean; +enableGranularShadowTreeStateReconciliation?: () => boolean; +enableIOSViewClipToPaddingBox?: () => boolean; @@ -74,6 +76,7 @@ export interface Spec extends TurboModule { +useRuntimeShadowNodeReferenceUpdateOnLayout?: () => boolean; +useStateAlignmentMechanism?: () => boolean; +useTurboModuleInterop?: () => boolean; + +useTurboModules?: () => boolean; } const NativeReactNativeFeatureFlags: ?Spec = TurboModuleRegistry.get( From 391680fe844aad887e497912378c699aed13464b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Thu, 26 Sep 2024 05:39:01 -0700 Subject: [PATCH 020/296] feat: improve RCTAppDelegate usage for brownfield (#46625) Summary: This PR improves the usage of `RCTAppDelegate` for brownfield scenarios. Currently, when we want to integrate React Native with a brownfield app users might not want to initialize React Native in the main window. They may want to create it later. Example usage: ```swift class AppDelegate: RCTAppDelegate { override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Disable automatically creating react native window self.automaticallyLoadReactNativeWindow = false return super.application(application, didFinishLaunchingWithOptions: launchOptions) } } ``` ```swift import Foundation import React import React_RCTAppDelegate class SettingsViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() self.view = (RCTSharedApplication()?.delegate as? RCTAppDelegate)?.rootViewFactory .view(withModuleName: "Settings", initialProperties: [:]) } } ``` ## Changelog: [IOS] [ADDED] - improve RCTAppDelegate usage for brownfield, add `automaticallyLoadReactNativeWindow` flag Pull Request resolved: https://github.com/facebook/react-native/pull/46625 Test Plan: CI Green Reviewed By: cortinico Differential Revision: D63325397 Pulled By: cipolleschi fbshipit-source-id: 1361bda5fcd91f4933219871c64a84a83c281c34 --- .../Libraries/AppDelegate/RCTAppDelegate.h | 3 ++ .../Libraries/AppDelegate/RCTAppDelegate.mm | 32 +++++++++++++------ 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/packages/react-native/Libraries/AppDelegate/RCTAppDelegate.h b/packages/react-native/Libraries/AppDelegate/RCTAppDelegate.h index fd7310c7f884ab..f9ca86a602aea7 100644 --- a/packages/react-native/Libraries/AppDelegate/RCTAppDelegate.h +++ b/packages/react-native/Libraries/AppDelegate/RCTAppDelegate.h @@ -64,6 +64,9 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, strong, nullable) NSDictionary *initialProps; @property (nonatomic, strong, nonnull) RCTRootViewFactory *rootViewFactory; +/// If `automaticallyLoadReactNativeWindow` is set to `true`, the React Native window will be loaded automatically. +@property (nonatomic, assign) BOOL automaticallyLoadReactNativeWindow; + @property (nonatomic, nullable) RCTSurfacePresenterBridgeAdapter *bridgeAdapter; /** diff --git a/packages/react-native/Libraries/AppDelegate/RCTAppDelegate.mm b/packages/react-native/Libraries/AppDelegate/RCTAppDelegate.mm index 1e87756cad8c80..ca5be139ffb2de 100644 --- a/packages/react-native/Libraries/AppDelegate/RCTAppDelegate.mm +++ b/packages/react-native/Libraries/AppDelegate/RCTAppDelegate.mm @@ -12,6 +12,7 @@ #import #import #import +#include #import #import #import @@ -38,6 +39,14 @@ @interface RCTAppDelegate () Date: Thu, 26 Sep 2024 05:58:54 -0700 Subject: [PATCH 021/296] Fix applying of tintColor and progressViewOffset props for RefreshControl component (#46628) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: While developing my project with New Architecture enabled I've found out that properties `tintColor` and `progressViewOffset` of component `RefreshControl` don't apply on iOS. This happens due to the lack of handling of these properties in the `RCTPullToRefreshViewComponentView.mm` class. The bug can be easily reproduced in RNTester app on RefreshControlExample.js screen, since it has property `tintColor="#ff0000"` (Red color), but RefreshControl renders with gray color: RefreshControlExample.js gray Refresh Control

This PR is opened to fix that by applying `tintColor` and `progressViewOffset` props to `_refreshControl` in `RCTPullToRefreshViewComponentView.mm` class. Fixes https://github.com/facebook/react-native/pull/46628 ## Changelog: [IOS][FIXED] - Fix applying of tintColor and progressViewOffset props for RefreshControl component with New Architecture enabled Pull Request resolved: https://github.com/facebook/react-native/pull/46628 Test Plan: 1. Run rn-tester app with New Architecture enabled on iOS 2. Open screen of RefreshControl component: Снимок экрана 2024-09-24 в 19 48 49 3. Open `/packages/rn-tester/js/examples/RefreshControl/RefreshControlExample.js` file and change properties `tintColor` and `progressViewOffset` of RefreshControl components on the line 85: Снимок экрана 2024-09-24 в 22 01 19 4. check that your changes applied: Снимок экрана 2024-09-24 в 19 54 46 Reviewed By: cortinico Differential Revision: D63381050 Pulled By: cipolleschi fbshipit-source-id: 4f3aed8bd7a1e42ce2a75aa19740fd8be1623c86 --- .../RCTPullToRefreshViewComponentView.mm | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.mm index 86f3f578a7aa7e..704b2e95ec4730 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.mm @@ -49,6 +49,11 @@ - (void)_initializeUIRefreshControl [_refreshControl addTarget:self action:@selector(handleUIControlEventValueChanged) forControlEvents:UIControlEventValueChanged]; + + const auto &concreteProps = static_cast(*_props); + + _refreshControl.tintColor = RCTUIColorFromSharedColor(concreteProps.tintColor); + [self _updateProgressViewOffset:concreteProps.progressViewOffset]; } #pragma mark - RCTComponentViewProtocol @@ -78,6 +83,14 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & } } + if (newConcreteProps.tintColor != oldConcreteProps.tintColor) { + _refreshControl.tintColor = RCTUIColorFromSharedColor(newConcreteProps.tintColor); + } + + if (newConcreteProps.progressViewOffset != oldConcreteProps.progressViewOffset) { + [self _updateProgressViewOffset:newConcreteProps.progressViewOffset]; + } + BOOL needsUpdateTitle = NO; if (newConcreteProps.title != oldConcreteProps.title) { @@ -102,6 +115,15 @@ - (void)handleUIControlEventValueChanged static_cast(*_eventEmitter).onRefresh({}); } +- (void)_updateProgressViewOffset:(Float)progressViewOffset +{ + _refreshControl.bounds = CGRectMake( + _refreshControl.bounds.origin.x, + -progressViewOffset, + _refreshControl.bounds.size.width, + _refreshControl.bounds.size.height); +} + - (void)_updateTitle { const auto &concreteProps = static_cast(*_props); From 94b77938435693792e57c96d76691d58d7361530 Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Thu, 26 Sep 2024 06:01:28 -0700 Subject: [PATCH 022/296] Run Maestro tests also in debug mode (#46573) Summary: This change runs Maestro tests also in Debug mode, by starting Metro in background. ## Changelog: [Internal] - Add E2E tests in Debug mode too Pull Request resolved: https://github.com/facebook/react-native/pull/46573 Test Plan: GHA must be green. Successful run: https://github.com/facebook/react-native/actions/runs/11033322135?pr=46573 Reviewed By: cortinico Differential Revision: D63452169 Pulled By: cipolleschi fbshipit-source-id: e04b87f6a3e7aca8519dc2cb37c982dff3c20100 --- .github/actions/maestro-android/action.yml | 44 +++++--- .github/actions/maestro-ios/action.yml | 33 +++++- .github/actions/test-ios-rntester/action.yml | 2 +- .github/workflow-scripts/maestro-android.js | 108 +++++++++++++++++++ .github/workflows/test-all.yml | 57 +++++++--- 5 files changed, 209 insertions(+), 35 deletions(-) create mode 100644 .github/workflow-scripts/maestro-android.js diff --git a/.github/actions/maestro-android/action.yml b/.github/actions/maestro-android/action.yml index 1b34e6efe0e117..c8882c023b279f 100644 --- a/.github/actions/maestro-android/action.yml +++ b/.github/actions/maestro-android/action.yml @@ -17,6 +17,15 @@ inputs: required: false default: 'true' description: whether this action has to install java 17 or not + flavor: + required: true + description: the flavor we want to run - either debug or release + default: release + working-directory: + required: false + default: "." + description: The directory from which metro should be started + runs: using: composite steps: @@ -25,7 +34,7 @@ runs: run: export MAESTRO_VERSION=1.36.0; curl -Ls "https://get.maestro.mobile.dev" | bash - name: Set up JDK 17 if: ${{ inputs.install-java == 'true' }} - uses: actions/setup-java@v2 + uses: actions/setup-java@v4 with: java-version: '17' distribution: 'zulu' @@ -38,27 +47,32 @@ runs: echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm + - name: Build Codegen + shell: bash + if: ${{ inputs.flavor == 'debug' }} + run: ./packages/react-native-codegen/scripts/oss/build.sh - name: Run e2e tests uses: reactivecircus/android-emulator-runner@v2 with: api-level: 24 arch: x86 - script: | - echo "Install APK from ${{ inputs.app-path }}" - adb install "${{ inputs.app-path }}" - - echo "Start recording to /sdcard/screen.mp4" - adb shell screenrecord /sdcard/screen.mp4 - - echo "Start testing ${{ inputs.maestro-flow }}" - $HOME/.maestro/bin/maestro test ${{ inputs.maestro-flow }} --format junit -e APP_ID=${{ inputs.app-id }} --debug-output /tmp/MaestroLogs - - echo "Stop recording. Saving to screen.mp4" - adb pull /sdcard/screen.mp4 + ram-size: '4096M' + disk-size: '10G' + disable-animations: false + avd-name: e2e_emulator + script: node .github/workflow-scripts/maestro-android.js ${{ inputs.app-path }} ${{ inputs.app-id }} ${{ inputs.maestro-flow }} ${{ inputs.flavor }} ${{ inputs.working-directory }} + - name: Normalize APP_ID + id: normalize-app-id + shell: bash + if: always() + run: | + NORM_APP_ID=$(echo "${{ inputs.app-id }}" | tr '.' '-') + echo "app-id=$NORM_APP_ID" >> $GITHUB_OUTPUT - name: Store tests result uses: actions/upload-artifact@v3 + if: always() with: - name: e2e_android_${{ inputs.app-id }}_report_${{ inputs.jsengine }} + name: e2e_android_${{ steps.normalize-app-id.outputs.app-id }}_report_${{ inputs.jsengine }}_${{ inputs.flavor }} path: | report.xml screen.mp4 @@ -66,5 +80,5 @@ runs: if: failure() && steps.run-tests.outcome == 'failure' uses: actions/upload-artifact@v4.3.4 with: - name: maestro-logs-android-${{ inputs.app-id }}-${{ inputs.jsengine }} + name: maestro-logs-android-${{ steps.normalize-app-id.outputs.app-id }}-${{ inputs.jsengine }}-${{ inputs.flavor }} path: /tmp/MaestroLogs diff --git a/.github/actions/maestro-ios/action.yml b/.github/actions/maestro-ios/action.yml index 8379a6d458f6ab..9a07eb8e7fd6c3 100644 --- a/.github/actions/maestro-ios/action.yml +++ b/.github/actions/maestro-ios/action.yml @@ -13,6 +13,15 @@ inputs: maestro-flow: required: true description: the folder that contains the maestro tests + flavor: + required: true + description: Whether we are building for Debug or Release + default: Release + working-directory: + required: false + default: "." + description: The directory from which metro should be started + runs: using: composite steps: @@ -29,6 +38,12 @@ runs: with: java-version: '17' distribution: 'zulu' + - name: Start Metro in Debug + shell: bash + if: ${{ inputs.flavor == 'Debug' }} + run: | + cd ${{ inputs.working-directory }} + yarn start & - name: Run tests id: run-tests shell: bash @@ -54,17 +69,24 @@ runs: xcrun simctl launch $UDID ${{ inputs.app-id }} echo "Running tests with Maestro" - export MAESTRO_DRIVER_STARTUP_TIMEOUT=1500000 # 25 min. CI is extremely slow + export MAESTRO_DRIVER_STARTUP_TIMEOUT=1800000 # 30 min. CI is extremely slow # Add retries for flakyness - MAX_ATTEMPTS=3 + MAX_ATTEMPTS=5 CURR_ATTEMPT=0 RESULT=1 while [[ $CURR_ATTEMPT -lt $MAX_ATTEMPTS ]] && [[ $RESULT -ne 0 ]]; do + if [[ $CURR_ATTEMPT -ne 0 ]]; then + echo "Rebooting simulator for stability" + xcrun simctl boot "iPhone 15 Pro" + fi + CURR_ATTEMPT=$((CURR_ATTEMPT+1)) echo "Attempt number $CURR_ATTEMPT" + + echo "Start video record using pid: video_record_${{ inputs.jsengine }}_$CURR_ATTEMPT.pid" xcrun simctl io booted recordVideo video_record_$CURR_ATTEMPT.mov & echo $! > video_record_${{ inputs.jsengine }}_$CURR_ATTEMPT.pid @@ -75,6 +97,9 @@ runs: # Stop video kill -SIGINT $(cat video_record_${{ inputs.jsengine }}_$CURR_ATTEMPT.pid) + + echo "Shutting down simulator for stability" + xcrun simctl shutdown "iPhone 15 Pro" done exit $RESULT @@ -82,7 +107,7 @@ runs: if: always() uses: actions/upload-artifact@v4.3.4 with: - name: e2e_ios_${{ inputs.app-id }}_report_${{ inputs.jsengine }} + name: e2e_ios_${{ inputs.app-id }}_report_${{ inputs.jsengine }}_${{ inputs.flavor }} path: | video_record_1.mov video_record_2.mov @@ -92,5 +117,5 @@ runs: if: failure() && steps.run-tests.outcome == 'failure' uses: actions/upload-artifact@v4.3.4 with: - name: maestro-logs-${{ inputs.app-id }}-${{ inputs.jsengine }} + name: maestro-logs-${{ inputs.app-id }}-${{ inputs.jsengine }}-${{ inputs.flavor }} path: /tmp/MaestroLogs diff --git a/.github/actions/test-ios-rntester/action.yml b/.github/actions/test-ios-rntester/action.yml index 1ab333089d0901..61c28f6b8b1642 100644 --- a/.github/actions/test-ios-rntester/action.yml +++ b/.github/actions/test-ios-rntester/action.yml @@ -132,7 +132,7 @@ runs: set -o pipefail && xcodebuild \ -scheme "RNTester" \ -workspace packages/rn-tester/RNTesterPods.xcworkspace \ - -configuration "Release" \ + -configuration "${{ inputs.flavor }}" \ -sdk "iphonesimulator" \ -destination "generic/platform=iOS Simulator" \ -derivedDataPath "/tmp/RNTesterBuild" | xcbeautify diff --git a/.github/workflow-scripts/maestro-android.js b/.github/workflow-scripts/maestro-android.js new file mode 100644 index 00000000000000..462ce25ae42007 --- /dev/null +++ b/.github/workflow-scripts/maestro-android.js @@ -0,0 +1,108 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +const childProcess = require('child_process'); + +const usage = ` +=== Usage === +node maestro-android.js + +@param {string} appPath - Path to the app APK +@param {string} appId - App ID that needs to be launched +@param {string} maestroFlow - Path to the maestro flow to be executed +@param {string} flavor - Flavor of the app to be launched. Can be 'release' or 'debug' +@param {string} workingDirectory - Working directory from where to run Metro +============== +`; + +const args = process.argv.slice(2); + +if (args.length !== 5) { + throw new Error(`Invalid number of arguments.\n${usage}`); +} + +const APP_PATH = args[0]; +const APP_ID = args[1]; +const MAESTRO_FLOW = args[2]; +const IS_DEBUG = args[3] === 'debug'; +const WORKING_DIRECTORY = args[4]; + +async function main() { + console.info('\n=============================='); + console.info('Running tests for Android with the following parameters:'); + console.info(`APP_PATH: ${APP_PATH}`); + console.info(`APP_ID: ${APP_ID}`); + console.info(`MAESTRO_FLOW: ${MAESTRO_FLOW}`); + console.info(`IS_DEBUG: ${IS_DEBUG}`); + console.info(`WORKING_DIRECTORY: ${WORKING_DIRECTORY}`); + console.info('==============================\n'); + + console.info('Install app'); + childProcess.execSync(`adb install ${APP_PATH}`, {stdio: 'ignore'}); + + let metroProcess = null; + if (IS_DEBUG) { + console.info('Start Metro'); + childProcess.execSync(`cd ${WORKING_DIRECTORY}`, {stdio: 'ignore'}); + metroProcess = childProcess.spawn('yarn', ['start', '&'], { + cwd: WORKING_DIRECTORY, + stdio: 'ignore', + detached: true, + }); + console.info(`- Metro PID: ${metroProcess.pid}`); + } + + console.info('Wait For Metro to Start'); + await sleep(5000); + + console.info('Start the app'); + childProcess.execSync(`adb shell monkey -p ${APP_ID} 1`, {stdio: 'ignore'}); + + console.info('Start recording to /sdcard/screen.mp4'); + childProcess + .exec('adb shell screenrecord /sdcard/screen.mp4', { + stdio: 'ignore', + detached: true, + }) + .unref(); + + console.info(`Start testing ${MAESTRO_FLOW}`); + let error = null; + try { + childProcess.execSync( + `MAESTRO_DRIVER_STARTUP_TIMEOUT=120000 $HOME/.maestro/bin/maestro test ${MAESTRO_FLOW} --format junit -e APP_ID=${APP_ID} --debug-output /tmp/MaestroLogs`, + {stdio: 'inherit'}, + ); + } catch (err) { + error = err; + } finally { + console.info('Stop recording'); + childProcess.execSync('adb pull /sdcard/screen.mp4', {stdio: 'ignore'}); + + if (IS_DEBUG && metroProcess != null) { + const pid = metroProcess.pid; + console.info(`Kill Metro. PID: ${pid}`); + process.kill(-pid); + console.info(`Metro Killed`); + process.exit(); + } + } + + if (error) { + throw error; + } +} + +function sleep(ms) { + return new Promise(resolve => { + setTimeout(resolve, ms); + }); +} + +main(); diff --git a/.github/workflows/test-all.yml b/.github/workflows/test-all.yml index 48db77b226c9f6..eb456dcc3a0589 100644 --- a/.github/workflows/test-all.yml +++ b/.github/workflows/test-all.yml @@ -196,6 +196,7 @@ jobs: matrix: jsengine: [Hermes, JSC] architecture: [NewArch] + flavor: [Debug, Release] steps: - name: Checkout uses: actions/checkout@v4 @@ -209,13 +210,15 @@ jobs: hermes-version: ${{ needs.prepare_hermes_workspace.outputs.hermes-version }} react-native-version: ${{ needs.prepare_hermes_workspace.outputs.react-native-version }} run-e2e-tests: "true" + flavor: ${{ matrix.flavor }} - name: Run E2E Tests uses: ./.github/actions/maestro-ios with: - app-path: "/tmp/RNTesterBuild/Build/Products/Release-iphonesimulator/RNTester.app" + app-path: "/tmp/RNTesterBuild/Build/Products/${{ matrix.flavor }}-iphonesimulator/RNTester.app" app-id: com.meta.RNTester.localDevelopment jsengine: ${{ matrix.jsengine }} maestro-flow: ./packages/rn-tester/.maestro/ + flavor: ${{ matrix.flavor }} test_e2e_ios_templateapp: if: ${{ github.ref == 'refs/heads/main' || contains(github.ref, 'stable') || inputs.run-e2e-tests }} @@ -229,6 +232,7 @@ jobs: fail-fast: false matrix: jsengine: [Hermes, JSC] + flavor: [Debug, Release] steps: - name: Checkout uses: actions/checkout@v4 @@ -245,7 +249,7 @@ jobs: - name: Download Hermes uses: actions/download-artifact@v4 with: - name: hermes-darwin-bin-Release + name: hermes-darwin-bin-${{matrix.flavor}} path: /tmp/react-native-tmp - name: Download React Native Package uses: actions/download-artifact@v4 @@ -262,26 +266,35 @@ jobs: HERMES_PATH=$(find /tmp/react-native-tmp -type f -name "*.tar.gz") echo "Hermes path is $HERMES_PATH" - node ./scripts/e2e/init-project-e2e.js --projectName RNTestProject --currentBranch ${{ github.ref_name }} --directory /tmp/RNTestProject --pathToLocalReactNative $REACT_NATIVE_PKG + # For stable branches, we want to use the stable branch of the template + # In all the other cases, we want to use "main" + BRANCH=${{ github.ref_name }} + if ! [[ $BRANCH == *-stable* ]]; then + BRANCH=main + fi + + node ./scripts/e2e/init-project-e2e.js --projectName RNTestProject --currentBranch $BRANCH --directory /tmp/RNTestProject --pathToLocalReactNative $REACT_NATIVE_PKG cd /tmp/RNTestProject/ios bundle install HERMES_ENGINE_TARBALL_PATH=$HERMES_PATH bundle exec pod install - set -o pipefail && xcodebuild \ + xcodebuild \ -scheme "RNTestProject" \ -workspace RNTestProject.xcworkspace \ - -configuration "Release" \ + -configuration "${{ matrix.flavor }}" \ -sdk "iphonesimulator" \ -destination "generic/platform=iOS Simulator" \ - -derivedDataPath "/tmp/RNTestProject" | xcbeautify + -derivedDataPath "/tmp/RNTestProject" - name: Run E2E Tests uses: ./.github/actions/maestro-ios with: - app-path: "/tmp/RNTestProject/Build/Products/Release-iphonesimulator/RNTestProject.app" + app-path: "/tmp/RNTestProject/Build/Products/${{ matrix.flavor }}-iphonesimulator/RNTestProject.app" app-id: org.reactjs.native.example.RNTestProject jsengine: ${{ matrix.jsengine }} maestro-flow: ./scripts/e2e/.maestro/ + flavor: ${{ matrix.flavor }} + working-directory: /tmp/RNTestProject test_e2e_android_templateapp: if: ${{ github.ref == 'refs/heads/main' || contains(github.ref, 'stable') || inputs.run-e2e-tests }} @@ -292,6 +305,7 @@ jobs: fail-fast: false matrix: jsengine: [Hermes, JSC] + flavor: [debug, release] steps: - name: Checkout uses: actions/checkout@v4 @@ -317,6 +331,7 @@ jobs: - name: Print /tmp folder run: ls -lR /tmp/react-native-tmp - name: Prepare artifacts + id: prepare-artifacts run: | REACT_NATIVE_PKG=$(find /tmp/react-native-tmp -type f -name "*.tgz") echo "React Native tgs is $REACT_NATIVE_PKG" @@ -324,7 +339,13 @@ jobs: MAVEN_LOCAL=/tmp/react-native-tmp/maven-local echo "Maven local path is $MAVEN_LOCAL" - node ./scripts/e2e/init-project-e2e.js --projectName RNTestProject --currentBranch ${{ github.ref_name }} --directory /tmp/RNTestProject --pathToLocalReactNative $REACT_NATIVE_PKG + # For stable branches, we want to use the stable branch of the template + # In all the other cases, we want to use "main" + BRANCH=${{ github.ref_name }} + if ! [[ $BRANCH == *-stable* ]]; then + BRANCH=main + fi + node ./scripts/e2e/init-project-e2e.js --projectName RNTestProject --currentBranch $BRANCH --directory /tmp/RNTestProject --pathToLocalReactNative $REACT_NATIVE_PKG echo "Feed maven local to gradle.properties" cd /tmp/RNTestProject @@ -332,15 +353,19 @@ jobs: # Build cd android - ./gradlew assembleRelease --no-daemon -PreactNativeArchitectures=x86 + CAPITALIZED_FLAVOR=$(echo "${{ matrix.flavor }}" | awk '{print toupper(substr($0, 1, 1)) substr($0, 2)}') + ./gradlew assemble$CAPITALIZED_FLAVOR --no-daemon -PreactNativeArchitectures=x86 + - name: Run E2E Tests uses: ./.github/actions/maestro-android with: - app-path: /tmp/RNTestProject/android/app/build/outputs/apk/release/app-release.apk + app-path: /tmp/RNTestProject/android/app/build/outputs/apk/${{ matrix.flavor }}/app-${{ matrix.flavor }}.apk app-id: com.rntestproject jsengine: ${{ matrix.jsengine }} maestro-flow: ./scripts/e2e/.maestro/ install-java: 'false' + flavor: ${{ matrix.flavor }} + working-directory: /tmp/RNTestProject build_hermesc_linux: runs-on: ubuntu-latest @@ -403,6 +428,7 @@ jobs: fail-fast: false matrix: jsengine: [hermes, jsc] + flavor: [debug, release] steps: - name: Checkout uses: actions/checkout@v4 @@ -413,17 +439,18 @@ jobs: - name: Download APK uses: actions/download-artifact@v4 with: - name: rntester-${{ matrix.jsengine }}-release - path: ./packages/rn-tester/android/app/build/outputs/apk/${{ matrix.jsengine }}/release/ + name: rntester-${{ matrix.jsengine }}-${{ matrix.flavor }} + path: ./packages/rn-tester/android/app/build/outputs/apk/${{ matrix.jsengine }}/${{ matrix.flavor }}/ - name: Print folder structure - run: ls -lR ./packages/rn-tester/android/app/build/outputs/apk/${{ matrix.jsengine }}/release/ + run: ls -lR ./packages/rn-tester/android/app/build/outputs/apk/${{ matrix.jsengine }}/${{ matrix.flavor }}/ - name: Run E2E Tests uses: ./.github/actions/maestro-android with: - app-path: ./packages/rn-tester/android/app/build/outputs/apk/${{ matrix.jsengine }}/release/app-${{ matrix.jsengine }}-x86-release.apk + app-path: ./packages/rn-tester/android/app/build/outputs/apk/${{ matrix.jsengine }}/${{ matrix.flavor }}/app-${{ matrix.jsengine }}-x86-${{ matrix.flavor }}.apk app-id: com.facebook.react.uiapp jsengine: ${{ matrix.jsengine }} - maestro-flow: ./packages/rn-tester/.maestro/ + maestro-flow: ./packages/rn-tester/.maestro + flavor: ${{ matrix.flavor }} build_npm_package: runs-on: 8-core-ubuntu From 38b3b2195c2a2839f1751edb8e0d1855b18ac5bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Pasi=C5=84ski?= Date: Thu, 26 Sep 2024 09:16:03 -0700 Subject: [PATCH 023/296] New implementation of PerformanceObserver and related APIs (fixes #45122) (#45206) Summary: Changelog: [internal] (this functionality hasn't been enabled in OSS yet, so no changelog necessary). Fix https://github.com/facebook/react-native/issues/45122. performance.mark is currently O(1) but performance.clearMark is O(n) (being n the number of entries in the buffer), which makes this operation very slow. ### Changes overview - Created new `PerformanceEntryBuffer` abstraction with the following subtypes, that differ on how entries are stored: - `PerformanceEntryCircularBuffer` - stores them in a ring buffer that was already implemented, removed key lookup cache (`BoundedConsumableBuffer`) - `PerformanceEntryKeyedBuffer` - stores them in a `unordered_map`, allowing for faster retrieval by type - `PerformanceEntryLinearBuffer` - a simple infinite buffer based on `std::vector`, currently used in a `PerformanceObserver` - Created `PerformanceObserver` abstraction on native side. - Created `PerformanceObserverRegistry` that collects active observers and forwards entries to observers that should retrieve them - Moved some method implementations to `.cpp` files to reduce potential compilation time slowdown. As the `PerformanceEntryReporter` can be included from anywhere in the code, it will be beneficial to make header files as light as possible. - Add comments to methods that note which standard is the method from/for. - Added some `[[nodiscard]]` attributes - Since the logic of routing entries to observers is moved to native side, JS side of the code got much simplified - If ever needed, `PerformanceObserver` can be created from native-side Standards covered: - https://www.w3.org/TR/performance-timeline - https://www.w3.org/TR/event-timing/ - https://w3c.github.io/timing-entrytypes-registry - https://w3c.github.io/user-timing/ Pull Request resolved: https://github.com/facebook/react-native/pull/45206 Test Plan: I've tested this e2e on IGVR and in the RNTester playground for the performance APIs. Everything works as expected. There are also new unit tests for this. C++ test results: https://www.internalfb.com/intern/testinfra/testconsole/testrun/8725724513169247/ Reviewed By: rshest Differential Revision: D63101520 Pulled By: rubennorte fbshipit-source-id: 5970b5c14692ff33ffda44a9f09067f6a758bdbe --- .../webperformance/NativePerformance.cpp | 9 + .../webperformance/NativePerformance.h | 6 + .../NativePerformanceObserver.cpp | 182 ++++--- .../NativePerformanceObserver.h | 64 ++- .../timeline/BoundedConsumableBuffer.h | 247 ---------- .../performance/timeline/CircularBuffer.h | 133 ++++++ .../performance/timeline/PerformanceEntry.h | 54 +++ .../timeline/PerformanceEntryBuffer.h | 40 ++ .../PerformanceEntryCircularBuffer.cpp | 38 ++ .../timeline/PerformanceEntryCircularBuffer.h | 33 ++ .../timeline/PerformanceEntryKeyedBuffer.cpp | 59 +++ .../timeline/PerformanceEntryKeyedBuffer.h | 38 ++ .../timeline/PerformanceEntryReporter.cpp | 325 +++++-------- .../timeline/PerformanceEntryReporter.h | 226 ++++----- .../timeline/PerformanceObserver.cpp | 92 ++++ .../timeline/PerformanceObserver.h | 139 ++++++ .../timeline/PerformanceObserverRegistry.cpp | 34 ++ .../timeline/PerformanceObserverRegistry.h | 54 +++ .../tests/BoundedConsumableBufferTest.cpp | 219 --------- .../timeline/tests/CircularBufferTest.cpp | 127 +++++ .../tests/PerformanceEntryReporterTest.cpp | 447 +++++++++--------- .../tests/PerformanceObserverTest.cpp | 272 +++++++++++ .../tests/RuntimeSchedulerTest.cpp | 34 +- .../webapis/performance/EventTiming.js | 2 +- .../webapis/performance/Performance.js | 24 +- .../performance/PerformanceObserver.js | 223 +++------ .../private/webapis/performance/Utilities.js | 25 + .../performance/__tests__/EventCounts-test.js | 59 ++- .../__tests__/PerformanceObserver-test.js | 262 +--------- .../specs/NativePerformanceObserver.js | 40 +- .../specs/__mocks__/NativePerformance.js | 20 +- .../__mocks__/NativePerformanceObserver.js | 143 +++--- .../__tests__/NativePerformanceMock-test.js | 32 +- .../NativePerformanceObserverMock-test.js | 70 +-- .../Performance/PerformanceApiExample.js | 2 +- 35 files changed, 2042 insertions(+), 1732 deletions(-) delete mode 100644 packages/react-native/ReactCommon/react/performance/timeline/BoundedConsumableBuffer.h create mode 100644 packages/react-native/ReactCommon/react/performance/timeline/CircularBuffer.h create mode 100644 packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntry.h create mode 100644 packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryBuffer.h create mode 100644 packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryCircularBuffer.cpp create mode 100644 packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryCircularBuffer.h create mode 100644 packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryKeyedBuffer.cpp create mode 100644 packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryKeyedBuffer.h create mode 100644 packages/react-native/ReactCommon/react/performance/timeline/PerformanceObserver.cpp create mode 100644 packages/react-native/ReactCommon/react/performance/timeline/PerformanceObserver.h create mode 100644 packages/react-native/ReactCommon/react/performance/timeline/PerformanceObserverRegistry.cpp create mode 100644 packages/react-native/ReactCommon/react/performance/timeline/PerformanceObserverRegistry.h delete mode 100644 packages/react-native/ReactCommon/react/performance/timeline/tests/BoundedConsumableBufferTest.cpp create mode 100644 packages/react-native/ReactCommon/react/performance/timeline/tests/CircularBufferTest.cpp create mode 100644 packages/react-native/ReactCommon/react/performance/timeline/tests/PerformanceObserverTest.cpp create mode 100644 packages/react-native/src/private/webapis/performance/Utilities.js diff --git a/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.cpp b/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.cpp index 7592ad3cacf88c..2b933d897d0b4e 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.cpp @@ -15,9 +15,15 @@ #include #include #include +#if __has_include() #include +#define HAS_FUSEBOX +#endif #include "NativePerformance.h" + +#ifdef RN_DISABLE_OSS_PLUGIN_HEADER #include "Plugins.h" +#endif #ifdef WITH_PERFETTO #include @@ -106,8 +112,11 @@ void NativePerformance::measure( std::optional endMark) { auto [trackName, eventName] = parseTrackName(name); +#ifdef HAS_FUSEBOX FuseboxTracer::getFuseboxTracer().addEvent( eventName, (uint64_t)startTime, (uint64_t)endTime, trackName); +#endif + PerformanceEntryReporter::getInstance()->measure( eventName, startTime, endTime, duration, startMark, endMark); diff --git a/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.h b/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.h index 1c95d80abd4118..fae3bf6c0dc4f9 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.h +++ b/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.h @@ -7,7 +7,13 @@ #pragma once +#if __has_include("rncoreJSI.h") // Cmake headers on Android +#include "rncoreJSI.h" +#elif __has_include("FBReactNativeSpecJSI.h") // CocoaPod headers on Apple +#include "FBReactNativeSpecJSI.h" +#else #include +#endif #include #include diff --git a/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformanceObserver.cpp b/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformanceObserver.cpp index 6ec448ad426b6e..688ef0d058474d 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformanceObserver.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformanceObserver.cpp @@ -5,16 +5,18 @@ * LICENSE file in the root directory of this source tree. */ -#include - #include "NativePerformanceObserver.h" - +#include #include #include +#include #include #include +#include +#ifdef RN_DISABLE_OSS_PLUGIN_HEADER #include "Plugins.h" +#endif std::shared_ptr NativePerformanceObserverModuleProvider( @@ -25,90 +27,152 @@ NativePerformanceObserverModuleProvider( namespace facebook::react { +class PerformanceObserverWrapper : public jsi::NativeState { + public: + explicit PerformanceObserverWrapper( + const std::shared_ptr observer) + : observer(observer) {} + + std::shared_ptr observer; +}; + NativePerformanceObserver::NativePerformanceObserver( std::shared_ptr jsInvoker) : NativePerformanceObserverCxxSpec(std::move(jsInvoker)) {} -void NativePerformanceObserver::startReporting( - jsi::Runtime& /*rt*/, - PerformanceEntryType entryType) { - auto reporter = PerformanceEntryReporter::getInstance(); +jsi::Object NativePerformanceObserver::createObserver( + jsi::Runtime& rt, + NativePerformanceObserverCallback callback) { + // The way we dispatch performance observer callbacks is a bit different from + // the spec. The specification requires us to queue a single task that + // dispatches observer callbacks. Instead, we are queuing all callbacks as + // separate tasks in the scheduler. + PerformanceObserverCallback cb = [callback = std::move(callback)]() { + callback.callWithPriority(SchedulerPriority::IdlePriority); + }; - reporter->startReporting(entryType); + auto& registry = + PerformanceEntryReporter::getInstance()->getObserverRegistry(); + + auto observer = PerformanceObserver::create(registry, std::move(cb)); + auto observerWrapper = std::make_shared(observer); + jsi::Object observerObj{rt}; + observerObj.setNativeState(rt, observerWrapper); + return observerObj; } -void NativePerformanceObserver::stopReporting( - jsi::Runtime& /*rt*/, - PerformanceEntryType entryType) { - auto reporter = PerformanceEntryReporter::getInstance(); +double NativePerformanceObserver::getDroppedEntriesCount( + jsi::Runtime& rt, + jsi::Object observerObj) { + auto observerWrapper = std::dynamic_pointer_cast( + observerObj.getNativeState(rt)); - reporter->stopReporting(entryType); + if (!observerWrapper) { + return 0; + } + + auto observer = observerWrapper->observer; + return observer->getDroppedEntriesCount(); } -void NativePerformanceObserver::setIsBuffered( - jsi::Runtime& /*rt*/, - const std::vector entryTypes, - bool isBuffered) { - for (const PerformanceEntryType entryType : entryTypes) { - PerformanceEntryReporter::getInstance()->setAlwaysLogged( - entryType, isBuffered); +void NativePerformanceObserver::observe( + jsi::Runtime& rt, + jsi::Object observerObj, + NativePerformanceObserverObserveOptions options) { + auto observerWrapper = std::dynamic_pointer_cast( + observerObj.getNativeState(rt)); + + if (!observerWrapper) { + return; } -} -PerformanceEntryReporter::PopPendingEntriesResult -NativePerformanceObserver::popPendingEntries(jsi::Runtime& /*rt*/) { - return PerformanceEntryReporter::getInstance()->popPendingEntries(); + auto observer = observerWrapper->observer; + auto durationThreshold = options.durationThreshold.value_or(0.0); + + // observer of type multiple + if (options.entryTypes.has_value()) { + std::unordered_set entryTypes; + auto rawTypes = options.entryTypes.value(); + + for (auto rawType : rawTypes) { + entryTypes.insert(Bridging::fromJs(rt, rawType)); + } + + observer->observe(entryTypes); + } else { // single + auto buffered = options.buffered.value_or(false); + if (options.type.has_value()) { + observer->observe( + static_cast(options.type.value()), + {.buffered = buffered, .durationThreshold = durationThreshold}); + } + } } -void NativePerformanceObserver::setOnPerformanceEntryCallback( - jsi::Runtime& /*rt*/, - std::optional> callback) { - if (callback) { - PerformanceEntryReporter::getInstance()->setReportingCallback( - [callback = std::move(callback)]() { - callback->callWithPriority(SchedulerPriority::IdlePriority); - }); - } else { - PerformanceEntryReporter::getInstance()->setReportingCallback(nullptr); +void NativePerformanceObserver::disconnect( + jsi::Runtime& rt, + jsi::Object observerObj) { + auto observerWrapper = std::dynamic_pointer_cast( + observerObj.getNativeState(rt)); + + if (!observerWrapper) { + return; } -} -void NativePerformanceObserver::logRawEntry( - jsi::Runtime& /*rt*/, - const PerformanceEntry entry) { - PerformanceEntryReporter::getInstance()->logEntry(entry); + auto observer = observerWrapper->observer; + observer->disconnect(); } -std::vector> -NativePerformanceObserver::getEventCounts(jsi::Runtime& /*rt*/) { - const auto& eventCounts = - PerformanceEntryReporter::getInstance()->getEventCounts(); - return std::vector>( - eventCounts.begin(), eventCounts.end()); -} +std::vector NativePerformanceObserver::takeRecords( + jsi::Runtime& rt, + jsi::Object observerObj, + bool sort) { + auto observerWrapper = std::dynamic_pointer_cast( + observerObj.getNativeState(rt)); -void NativePerformanceObserver::setDurationThreshold( - jsi::Runtime& /*rt*/, - PerformanceEntryType entryType, - double durationThreshold) { - PerformanceEntryReporter::getInstance()->setDurationThreshold( - entryType, durationThreshold); + if (!observerWrapper) { + return {}; + } + + auto observer = observerWrapper->observer; + auto records = observer->takeRecords(); + if (sort) { + std::stable_sort(records.begin(), records.end(), PerformanceEntrySorter{}); + } + return records; } void NativePerformanceObserver::clearEntries( jsi::Runtime& /*rt*/, PerformanceEntryType entryType, std::optional entryName) { - PerformanceEntryReporter::getInstance()->clearEntries( - entryType, entryName ? entryName->c_str() : std::string_view{}); + PerformanceEntryReporter::getInstance()->clearEntries(entryType, entryName); } std::vector NativePerformanceObserver::getEntries( jsi::Runtime& /*rt*/, std::optional entryType, std::optional entryName) { - return PerformanceEntryReporter::getInstance()->getEntries( - entryType, entryName ? entryName->c_str() : std::string_view{}); + const auto reporter = PerformanceEntryReporter::getInstance(); + + std::vector entries; + + if (entryType.has_value()) { + if (entryName.has_value()) { + entries = + reporter->getEntriesByName(entryName.value(), entryType.value()); + } else { + entries = reporter->getEntriesByType(entryType.value()); + } + } else if (entryName.has_value()) { + entries = reporter->getEntriesByName(entryName.value()); + } else { + entries = reporter->getEntries(); + } + + std::stable_sort(entries.begin(), entries.end(), PerformanceEntrySorter{}); + + return entries; } std::vector @@ -127,4 +191,10 @@ NativePerformanceObserver::getSupportedPerformanceEntryTypes( return supportedEntries; } +std::vector> +NativePerformanceObserver::getEventCounts(jsi::Runtime& /*rt*/) { + const auto& eventCounts = + PerformanceEntryReporter::getInstance()->getEventCounts(); + return {eventCounts.begin(), eventCounts.end()}; +} } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformanceObserver.h b/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformanceObserver.h index 5c57e2ca83a90d..35d1e7cf40a402 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformanceObserver.h +++ b/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformanceObserver.h @@ -7,7 +7,14 @@ #pragma once +#if __has_include("rncoreJSI.h") // Cmake headers on Android +#include "rncoreJSI.h" +#elif __has_include("FBReactNativeSpecJSI.h") // CocoaPod headers on Apple +#include "FBReactNativeSpecJSI.h" +#else #include +#endif + #include #include #include @@ -15,6 +22,18 @@ namespace facebook::react { +using NativePerformanceObserverCallback = AsyncCallback<>; +using NativePerformanceObserverObserveOptions = + NativePerformanceObserverPerformanceObserverInit< + // entryTypes + std::optional>, + // type + std::optional, + // buffered + std::optional, + // durationThreshold + std::optional>; + #pragma mark - Structs template <> @@ -33,47 +52,42 @@ struct Bridging { }; template <> -struct Bridging - : NativePerformanceObserverRawPerformanceEntryBridging {}; +struct Bridging + : NativePerformanceObserverPerformanceObserverInitBridging< + NativePerformanceObserverObserveOptions> {}; template <> -struct Bridging - : NativePerformanceObserverGetPendingEntriesResultBridging< - PerformanceEntryReporter::PopPendingEntriesResult> {}; +struct Bridging + : NativePerformanceObserverRawPerformanceEntryBridging {}; #pragma mark - implementation class NativePerformanceObserver : public NativePerformanceObserverCxxSpec { public: - NativePerformanceObserver(std::shared_ptr jsInvoker); - - void startReporting(jsi::Runtime& rt, PerformanceEntryType entryType); - - void stopReporting(jsi::Runtime& rt, PerformanceEntryType entryType); + explicit NativePerformanceObserver(std::shared_ptr jsInvoker); - void setIsBuffered( + jsi::Object createObserver( jsi::Runtime& rt, - const std::vector entryTypes, - bool isBuffered); + NativePerformanceObserverCallback callback); + double getDroppedEntriesCount(jsi::Runtime& rt, jsi::Object observerObj); - PerformanceEntryReporter::PopPendingEntriesResult popPendingEntries( - jsi::Runtime& rt); - - void setOnPerformanceEntryCallback( + void observe( jsi::Runtime& rt, - std::optional> callback); - - void logRawEntry(jsi::Runtime& rt, const PerformanceEntry entry); + jsi::Object observer, + NativePerformanceObserverObserveOptions options); + void disconnect(jsi::Runtime& rt, jsi::Object observer); + std::vector takeRecords( + jsi::Runtime& rt, + jsi::Object observerObj, + // When called via `observer.takeRecords` it should be in insertion order. + // When called via the observer callback, it should be in chronological + // order with respect to `startTime`. + bool sort); std::vector> getEventCounts( jsi::Runtime& rt); - void setDurationThreshold( - jsi::Runtime& rt, - PerformanceEntryType entryType, - DOMHighResTimeStamp durationThreshold); - void clearEntries( jsi::Runtime& rt, PerformanceEntryType entryType, diff --git a/packages/react-native/ReactCommon/react/performance/timeline/BoundedConsumableBuffer.h b/packages/react-native/ReactCommon/react/performance/timeline/BoundedConsumableBuffer.h deleted file mode 100644 index 2918436fccd14e..00000000000000 --- a/packages/react-native/ReactCommon/react/performance/timeline/BoundedConsumableBuffer.h +++ /dev/null @@ -1,247 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -#pragma once - -#include -#include -#include - -namespace facebook::react { - -constexpr size_t DEFAULT_MAX_SIZE = 1024; - -/** - * A container for storing entries of type T, with the following properties: - * - It can only grow up to a specified max size - * - It's a circular buffer (the oldest elements are dropped if reached max - * size and adding a new element) - * - The entries can be "consumed" (once), which from the point of view of - * the consumer effectively clears the buffer - * - Even after the entries are consumed, all of the non-overwritten entries - * can still be independently retrieved an arbitrary amount of times - * - * Note that the space for maxSize elements is reserved on construction. This - * ensures that pointers to elements remain stable across add() operations. - */ -template -class BoundedConsumableBuffer { - public: - /** - * Status of the add/push operation for the `BoundedConsumableBuffer` - * container - */ - enum class PushStatus { - // There was free space in the buffer, element was successfully pushed: - OK = 0, - - // Element was pushed, but had to overwrite some already consumed elements: - OVERWRITE = 1, - - // Element wasn't pushed, as buffer size limit has been reached and it's - // not possible to overwrite already consumed elements anymore: - DROP = 2, - }; - - BoundedConsumableBuffer(size_t maxSize = DEFAULT_MAX_SIZE) - : maxSize_(maxSize) { - entries_.reserve(maxSize_); - } - - /** - * Adds (pushes) element into the buffer. Returns the result/status of the - * operation, which will depend on whether the buffer reached the max allowed - * size and how many are there unconsumed elements. - */ - PushStatus add(const T&& el) { - if (entries_.size() < maxSize_) { - // Haven't reached max buffer size yet, just add and grow the buffer - entries_.emplace_back(el); - cursorEnd_++; - numToConsume_++; - return PushStatus::OK; - } else if (numToConsume_ == maxSize_) { - // Drop the oldest (yet unconsumed) element in the buffer - entries_[position_] = el; - cursorEnd_ = (cursorEnd_ + 1) % maxSize_; - position_ = (position_ + 1) % maxSize_; - cursorStart_ = position_; - return PushStatus::DROP; - } else { - // Overwrite the oldest (but already consumed) element in the buffer - entries_[position_] = el; - position_ = (position_ + 1) % entries_.size(); - cursorEnd_ = position_; - numToConsume_++; - return PushStatus::OVERWRITE; - } - } - - /** - * Returns pointer to next entry which would be overwritten or dropped if - * added a new element. Null if no entry will be dropped. - */ - const T* getNextOverwriteCandidate() const { - if (entries_.size() < maxSize_) { - return nullptr; - } else { - return &entries_[position_]; - } - } - - T& operator[](size_t idx) { - return entries_[(position_ + idx) % entries_.size()]; - } - - /** - * Returns reference to the last unconsumed element - */ - T& back() { - return entries_[(cursorEnd_ - 1 + entries_.size()) % entries_.size()]; - } - - size_t size() const { - return entries_.size(); - } - - size_t getNumToConsume() const { - return numToConsume_; - } - - void clear() { - entries_.clear(); - position_ = 0; - cursorStart_ = 0; - cursorEnd_ = 0; - numToConsume_ = 0; - } - - /** - * Clears buffer entries by predicate - */ - void clear(std::function predicate) { - std::vector entries; - int numToConsume = 0; - - entries.reserve(maxSize_); - for (size_t i = 0; i < entries_.size(); i++) { - T& el = entries_[(i + position_) % entries_.size()]; - if (predicate(el)) { - continue; - } - - entries.push_back(std::move(el)); - if (i + numToConsume_ >= entries_.size()) { // el is unconsumed - numToConsume++; - } - } - - numToConsume_ = numToConsume; - cursorEnd_ = entries.size() % maxSize_; - cursorStart_ = (cursorEnd_ - numToConsume_ + maxSize_) % maxSize_; - position_ = 0; - - entries.swap(entries_); - } - - /** - * Retrieves buffer entries, whether consumed or not - */ - std::vector getEntries() const { - std::vector res; - getEntries(res); - return res; - } - - /** - * Retrieves buffer entries, whether consumed or not, with predicate - */ - std::vector getEntries(std::function predicate) const { - std::vector res; - getEntries(res, predicate); - return res; - } - - void getEntries(std::vector& res) const { - const size_t oldSize = res.size(); - res.resize(oldSize + entries_.size()); - std::copy( - entries_.begin() + position_, entries_.end(), res.begin() + oldSize); - std::copy( - entries_.begin(), - entries_.begin() + position_, - res.begin() + oldSize + entries_.size() - position_); - } - - void getEntries(std::vector& res, std::function predicate) - const { - for (size_t i = 0; i < entries_.size(); i++) { - const T& el = entries_[(i + position_) % entries_.size()]; - if (predicate(el)) { - res.push_back(el); - } - } - } - - /** - * "Consumes" all the currently unconsumed entries in the buffer and returns - * these entries. Note that even if the buffer may not have unconsumed - * elements currently, it's still possible to retrieve all buffer elements - * via `getEntries`. - */ - std::vector consume() { - std::vector res; - consume(res); - return res; - } - - void consume(std::vector& res) { - if (numToConsume_ == 0) { - return; - } - - const size_t resStart = res.size(); - res.resize(res.size() + numToConsume_); - if (cursorEnd_ > cursorStart_) { - std::copy( - entries_.begin() + cursorStart_, - entries_.begin() + cursorEnd_, - res.begin() + resStart); - } else { - std::copy( - entries_.begin() + cursorStart_, - entries_.end(), - res.begin() + resStart); - std::copy( - entries_.begin(), - entries_.begin() + cursorEnd_, - res.begin() + resStart + static_cast(entries_.size()) - - cursorStart_); - } - - cursorStart_ = cursorEnd_; - numToConsume_ = 0; - } - - private: - std::vector entries_; - - const size_t maxSize_; - - // Current starting position in the circular buffer: - size_t position_{0}; - - // Current "cursor" - positions of the first and after last unconsumed - // element, relative to the starting position: - size_t cursorStart_{0}; - size_t cursorEnd_{0}; - - // Number of currently unconsumed elements: - size_t numToConsume_{0}; -}; - -} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/performance/timeline/CircularBuffer.h b/packages/react-native/ReactCommon/react/performance/timeline/CircularBuffer.h new file mode 100644 index 00000000000000..34e18be4d144a4 --- /dev/null +++ b/packages/react-native/ReactCommon/react/performance/timeline/CircularBuffer.h @@ -0,0 +1,133 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include "PerformanceEntry.h" + +namespace facebook::react { + +/** + * A container for storing entries of type T, with the following properties: + * - It can only grow up to a specified max size + * - It's a circular buffer (the oldest elements are dropped if reached max + * size and adding a new element) + * + * Note that the space for maxSize elements is reserved on construction. This + * ensures that pointers to elements remain stable across add() operations. + */ +template +class CircularBuffer { + public: + explicit CircularBuffer(size_t maxSize) : maxSize_(maxSize) { + entries_.reserve(maxSize_); + } + + /** + * Adds (pushes) element into the buffer. + * + * Returns the result of the operation, which will depend on whether the + * buffer reached the max allowed size, in which case `true` is returned. If + * no items were overridden `false` is returned. + */ + bool add(const T& el) { + if (entries_.size() < maxSize_) { + // Haven't reached max buffer size yet, just add and grow the buffer + entries_.emplace_back(el); + return false; + } else { + // Overwrite the oldest (but already consumed) element in the buffer + entries_[position_] = el; + position_ = (position_ + 1) % entries_.size(); + return true; + } + } + + T& operator[](size_t idx) { + return entries_[(position_ + idx) % entries_.size()]; + } + + size_t size() const { + return entries_.size(); + } + + void clear() { + entries_.clear(); + position_ = 0; + } + + /** + * Clears buffer entries by predicate + */ + void clear(std::function predicate) { + std::vector entries; + + entries.reserve(maxSize_); + for (size_t i = 0; i < entries_.size(); i++) { + T& el = entries_[(i + position_) % entries_.size()]; + if (predicate(el)) { + continue; + } + + entries.push_back(std::move(el)); + } + + position_ = 0; + entries.swap(entries_); + } + + /** + * Retrieves buffer entries, whether consumed or not + */ + std::vector getEntries() const { + std::vector res; + getEntries(res); + return res; + } + + /** + * Retrieves buffer entries, whether consumed or not, with predicate + */ + std::vector getEntries(std::function predicate) const { + std::vector res; + getEntries(res, predicate); + return res; + } + + void getEntries(std::vector& res) const { + const size_t oldSize = res.size(); + res.resize(oldSize + entries_.size()); + std::copy( + entries_.begin() + position_, entries_.end(), res.begin() + oldSize); + std::copy( + entries_.begin(), + entries_.begin() + position_, + res.begin() + oldSize + entries_.size() - position_); + } + + void getEntries(std::vector& res, std::function predicate) + const { + for (size_t i = 0; i < entries_.size(); i++) { + const T& el = entries_[(i + position_) % entries_.size()]; + if (predicate(el)) { + res.push_back(el); + } + } + } + + private: + std::vector entries_; + const size_t maxSize_; + + // Current starting position in the circular buffer: + size_t position_{0}; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntry.h b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntry.h new file mode 100644 index 00000000000000..ff292871aedf8b --- /dev/null +++ b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntry.h @@ -0,0 +1,54 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace facebook::react { + +using PerformanceEntryInteractionId = uint32_t; + +enum class PerformanceEntryType { + // We need to preserve these values for backwards compatibility. + MARK = 1, + MEASURE = 2, + EVENT = 3, + LONGTASK = 4, + _NEXT = 5, +}; + +struct PerformanceEntry { + std::string name; + PerformanceEntryType entryType; + DOMHighResTimeStamp startTime; + DOMHighResTimeStamp duration = 0; + + // For "event" entries only: + std::optional processingStart; + std::optional processingEnd; + std::optional interactionId; +}; + +constexpr size_t NUM_PERFORMANCE_ENTRY_TYPES = + (size_t)PerformanceEntryType::_NEXT - 1; // Valid types start from 1. + +struct PerformanceEntrySorter { + bool operator()(const PerformanceEntry& lhs, const PerformanceEntry& rhs) { + if (lhs.startTime != rhs.startTime) { + return lhs.startTime < rhs.startTime; + } else { + return lhs.duration < rhs.duration; + } + } +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryBuffer.h b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryBuffer.h new file mode 100644 index 00000000000000..5d31d94ce79c29 --- /dev/null +++ b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryBuffer.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include "PerformanceEntry.h" + +namespace facebook::react { + +// Default duration threshold for reporting performance entries (0 means "report +// all") +constexpr double DEFAULT_DURATION_THRESHOLD = 0.0; + +/** + * Abstract performance entry buffer with reporting flags. + * Subtypes differ on how entries are stored. + */ +class PerformanceEntryBuffer { + public: + double durationThreshold{DEFAULT_DURATION_THRESHOLD}; + size_t droppedEntriesCount{0}; + + explicit PerformanceEntryBuffer() = default; + virtual ~PerformanceEntryBuffer() = default; + + virtual void add(const PerformanceEntry& entry) = 0; + virtual void getEntries( + std::string_view name, + std::vector& target) const = 0; + virtual void getEntries(std::vector& target) const = 0; + virtual void clear() = 0; + virtual void clear(std::string_view name) = 0; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryCircularBuffer.cpp b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryCircularBuffer.cpp new file mode 100644 index 00000000000000..fadbe0e756fc4b --- /dev/null +++ b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryCircularBuffer.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "PerformanceEntryCircularBuffer.h" + +namespace facebook::react { + +void PerformanceEntryCircularBuffer::add(const PerformanceEntry& entry) { + if (buffer_.add(entry)) { + droppedEntriesCount += 1; + } +} + +void PerformanceEntryCircularBuffer::getEntries( + std::vector& target) const { + buffer_.getEntries(target); +} + +void PerformanceEntryCircularBuffer::getEntries( + std::string_view name, + std::vector& target) const { + buffer_.getEntries( + target, [&](const PerformanceEntry& e) { return e.name == name; }); +} + +void PerformanceEntryCircularBuffer::clear() { + buffer_.clear(); +} + +void PerformanceEntryCircularBuffer::clear(std::string_view name) { + buffer_.clear([&](const PerformanceEntry& e) { return e.name == name; }); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryCircularBuffer.h b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryCircularBuffer.h new file mode 100644 index 00000000000000..e9e4e54ace74c6 --- /dev/null +++ b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryCircularBuffer.h @@ -0,0 +1,33 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include "CircularBuffer.h" +#include "PerformanceEntryBuffer.h" + +namespace facebook::react { + +class PerformanceEntryCircularBuffer : public PerformanceEntryBuffer { + public: + explicit PerformanceEntryCircularBuffer(size_t size) : buffer_(size) {} + ~PerformanceEntryCircularBuffer() override = default; + + void add(const PerformanceEntry& entry) override; + + void getEntries(std::vector& target) const override; + void getEntries(std::string_view name, std::vector& target) + const override; + + void clear() override; + void clear(std::string_view name) override; + + private: + CircularBuffer buffer_; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryKeyedBuffer.cpp b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryKeyedBuffer.cpp new file mode 100644 index 00000000000000..0b11279a8a347c --- /dev/null +++ b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryKeyedBuffer.cpp @@ -0,0 +1,59 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "PerformanceEntryKeyedBuffer.h" +#include + +namespace facebook::react { + +void PerformanceEntryKeyedBuffer::add(const PerformanceEntry& entry) { + auto node = entryMap_.find(entry.name); + + if (node != entryMap_.end()) { + node->second.push_back(entry); + } else { + entryMap_.emplace(entry.name, std::vector{entry}); + } +} + +void PerformanceEntryKeyedBuffer::getEntries( + std::vector& target) const { + for (const auto& [_, entries] : entryMap_) { + target.insert(target.end(), entries.begin(), entries.end()); + } +} + +void PerformanceEntryKeyedBuffer::getEntries( + std::string_view name, + std::vector& target) const { + std::string nameStr{name}; + + if (auto node = entryMap_.find(nameStr); node != entryMap_.end()) { + target.insert(target.end(), node->second.begin(), node->second.end()); + } +} + +void PerformanceEntryKeyedBuffer::clear() { + entryMap_.clear(); +} + +void PerformanceEntryKeyedBuffer::clear(std::string_view nameView) { + entryMap_.erase(std::string(nameView)); +} + +std::optional PerformanceEntryKeyedBuffer::find( + const std::string& name) const { + if (auto node = entryMap_.find(name); node != entryMap_.end()) { + if (!node->second.empty()) { + return std::make_optional(node->second.back()); + } + } + + return std::nullopt; +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryKeyedBuffer.h b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryKeyedBuffer.h new file mode 100644 index 00000000000000..fe57f18091afd3 --- /dev/null +++ b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryKeyedBuffer.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include "PerformanceEntryBuffer.h" + +namespace facebook::react { + +class PerformanceEntryKeyedBuffer : public PerformanceEntryBuffer { + public: + PerformanceEntryKeyedBuffer() = default; + + void add(const PerformanceEntry& entry) override; + + void getEntries(std::vector& target) const override; + + void getEntries(std::string_view name, std::vector& target) + const override; + + void clear() override; + void clear(std::string_view name) override; + + std::optional find(const std::string& name) const; + + private: + std::unordered_map> entryMap_{}; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.cpp b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.cpp index 79c47cf4a006e1..0b0aec8aed4616 100644 --- a/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.cpp +++ b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.cpp @@ -17,217 +17,104 @@ PerformanceEntryReporter::getInstance() { return instance; } -PerformanceEntryReporter::PerformanceEntryReporter() { - // For mark entry types we also want to keep the lookup by name, to make - // sure that marks can be referenced by measures - getBuffer(PerformanceEntryType::MARK).hasNameLookup = true; -} - -void PerformanceEntryReporter::setReportingCallback( - std::function callback) { - callback_ = std::move(callback); -} +PerformanceEntryReporter::PerformanceEntryReporter() + : observerRegistry_(std::make_unique()) {} DOMHighResTimeStamp PerformanceEntryReporter::getCurrentTimeStamp() const { return timeStampProvider_ != nullptr ? timeStampProvider_() : JSExecutor::performanceNow(); } -void PerformanceEntryReporter::startReporting(PerformanceEntryType entryType) { - auto& buffer = getBuffer(entryType); - buffer.isReporting = true; - buffer.durationThreshold = DEFAULT_DURATION_THRESHOLD; -} - -void PerformanceEntryReporter::setAlwaysLogged( - PerformanceEntryType entryType, - bool isAlwaysLogged) { - auto& buffer = getBuffer(entryType); - buffer.isAlwaysLogged = isAlwaysLogged; -} - -void PerformanceEntryReporter::setDurationThreshold( - PerformanceEntryType entryType, - DOMHighResTimeStamp durationThreshold) { - getBuffer(entryType).durationThreshold = durationThreshold; -} - -void PerformanceEntryReporter::stopReporting(PerformanceEntryType entryType) { - getBuffer(entryType).isReporting = false; +uint32_t PerformanceEntryReporter::getDroppedEntriesCount( + PerformanceEntryType type) const noexcept { + return getBuffer(type).droppedEntriesCount; } -void PerformanceEntryReporter::stopReporting() { - for (auto& buffer : buffers_) { - buffer.isReporting = false; - } -} +void PerformanceEntryReporter::mark( + const std::string& name, + const std::optional& startTime) { + const auto entry = PerformanceEntry{ + .name = name, + .entryType = PerformanceEntryType::MARK, + .startTime = startTime ? *startTime : getCurrentTimeStamp()}; -PerformanceEntryReporter::PopPendingEntriesResult -PerformanceEntryReporter::popPendingEntries() { - std::lock_guard lock(entriesMutex_); - PopPendingEntriesResult res = { - .entries = std::vector(), - .droppedEntriesCount = droppedEntriesCount_}; - for (auto& buffer : buffers_) { - buffer.entries.consume(res.entries); + { + std::lock_guard lock(buffersMutex_); + markBuffer_.add(entry); } - // Sort by starting time (or ending time, if starting times are equal) - std::stable_sort( - res.entries.begin(), - res.entries.end(), - [](const PerformanceEntry& lhs, const PerformanceEntry& rhs) { - if (lhs.startTime != rhs.startTime) { - return lhs.startTime < rhs.startTime; - } else { - return lhs.duration < rhs.duration; - } - }); - - droppedEntriesCount_ = 0; - return res; + observerRegistry_->queuePerformanceEntry(entry); } -void PerformanceEntryReporter::logEntry(const PerformanceEntry& entry) { - if (entry.entryType == PerformanceEntryType::EVENT) { - eventCounts_[entry.name]++; - } - - if (!isReporting(entry.entryType) && !isAlwaysLogged(entry.entryType)) { - return; - } - - std::lock_guard lock(entriesMutex_); - - auto& buffer = getBuffer(entry.entryType); - - if (entry.duration < buffer.durationThreshold) { - // The entries duration is lower than the desired reporting threshold, skip - return; - } - - if (buffer.hasNameLookup) { - // If we need to remove an entry because the buffer is null, - // we also need to remove it from the name lookup. - auto overwriteCandidate = buffer.entries.getNextOverwriteCandidate(); - if (overwriteCandidate != nullptr) { - std::lock_guard lock2(nameLookupMutex_); - auto it = buffer.nameLookup.find(overwriteCandidate); - if (it != buffer.nameLookup.end() && *it == overwriteCandidate) { - buffer.nameLookup.erase(it); - } - } - } - - auto pushResult = buffer.entries.add(std::move(entry)); - if (pushResult == - BoundedConsumableBuffer::PushStatus::DROP) { - // Start dropping entries once reached maximum buffer size. - // The number of dropped entries will be reported back to the corresponding - // PerformanceObserver callback. - droppedEntriesCount_ += 1; - } +void PerformanceEntryReporter::clearEntries( + std::optional entryType, + std::optional entryName) { + std::lock_guard lock(buffersMutex_); - if (buffer.hasNameLookup) { - std::lock_guard lock2(nameLookupMutex_); - auto currentEntry = &buffer.entries.back(); - auto it = buffer.nameLookup.find(currentEntry); - if (it != buffer.nameLookup.end()) { - buffer.nameLookup.erase(it); + // Clear all entry types + if (!entryType) { + if (entryName.has_value()) { + markBuffer_.clear(*entryName); + measureBuffer_.clear(*entryName); + eventBuffer_.clear(*entryName); + longTaskBuffer_.clear(*entryName); + } else { + markBuffer_.clear(); + measureBuffer_.clear(); + eventBuffer_.clear(); + longTaskBuffer_.clear(); } - buffer.nameLookup.insert(currentEntry); + return; } - if (buffer.entries.getNumToConsume() == 1) { - // If the buffer was empty, it signals that JS side just has possibly - // consumed it and is ready to get more - scheduleFlushBuffer(); + auto& buffer = getBufferRef(*entryType); + if (entryName.has_value()) { + buffer.clear(*entryName); + } else { + buffer.clear(); } } -void PerformanceEntryReporter::mark( - const std::string& name, - const std::optional& startTime) { - logEntry(PerformanceEntry{ - .name = name, - .entryType = PerformanceEntryType::MARK, - .startTime = startTime ? *startTime : getCurrentTimeStamp()}); +std::vector PerformanceEntryReporter::getEntries() const { + std::vector res; + // Collect all entry types + for (int i = 1; i <= NUM_PERFORMANCE_ENTRY_TYPES; i++) { + getBuffer(static_cast(i)).getEntries(res); + } + return res; } -void PerformanceEntryReporter::clearEntries( - std::optional entryType, - std::string_view entryName) { - if (!entryType) { - // Clear all entry types - for (int i = 1; i < NUM_PERFORMANCE_ENTRY_TYPES; i++) { - clearEntries(static_cast(i), entryName); - } - } else { - auto& buffer = getBuffer(*entryType); - if (!entryName.empty()) { - if (buffer.hasNameLookup) { - std::lock_guard lock2(nameLookupMutex_); - buffer.nameLookup.clear(); - } - - std::lock_guard lock(entriesMutex_); - buffer.entries.clear([entryName](const PerformanceEntry& entry) { - return entry.name == entryName; - }); - - if (buffer.hasNameLookup) { - std::lock_guard lock2(nameLookupMutex_); - // BoundedConsumableBuffer::clear() invalidates existing references; we - // need to rebuild the lookup table. If there are multiple entries with - // the same name, make sure the last one gets inserted. - for (int i = static_cast(buffer.entries.size()) - 1; i >= 0; i--) { - const auto& entry = buffer.entries[i]; - buffer.nameLookup.insert(&entry); - } - } - } else { - { - std::lock_guard lock(entriesMutex_); - buffer.entries.clear(); - } - { - std::lock_guard lock2(nameLookupMutex_); - buffer.nameLookup.clear(); - } - } - } +std::vector PerformanceEntryReporter::getEntriesByType( + PerformanceEntryType entryType) const { + std::vector res; + getEntriesByType(entryType, res); + return res; } -void PerformanceEntryReporter::getEntries( +void PerformanceEntryReporter::getEntriesByType( PerformanceEntryType entryType, - std::string_view entryName, - std::vector& res) const { - std::lock_guard lock(entriesMutex_); - const auto& entries = getBuffer(entryType).entries; - if (entryName.empty()) { - entries.getEntries(res); - } else { - entries.getEntries(res, [entryName](const PerformanceEntry& entry) { - return entry.name == entryName; - }); - } + std::vector& target) const { + getBuffer(entryType).getEntries(target); } -std::vector PerformanceEntryReporter::getEntries( - std::optional entryType, +std::vector PerformanceEntryReporter::getEntriesByName( std::string_view entryName) const { std::vector res; - if (!entryType) { - // Collect all entry types - for (int i = 1; i < NUM_PERFORMANCE_ENTRY_TYPES; i++) { - getEntries(static_cast(i), entryName, res); - } - } else { - getEntries(*entryType, entryName, res); + // Collect all entry types + for (int i = 1; i <= NUM_PERFORMANCE_ENTRY_TYPES; i++) { + getBuffer(static_cast(i)).getEntries(entryName, res); } return res; } +std::vector PerformanceEntryReporter::getEntriesByName( + std::string_view entryName, + PerformanceEntryType entryType) const { + std::vector res; + getBuffer(entryType).getEntries(entryName, res); + return res; +} + void PerformanceEntryReporter::measure( const std::string_view& name, DOMHighResTimeStamp startTime, @@ -248,23 +135,26 @@ void PerformanceEntryReporter::measure( DOMHighResTimeStamp durationVal = duration ? *duration : endTimeVal - startTimeVal; - logEntry( - {.name = std::string(name), - .entryType = PerformanceEntryType::MEASURE, - .startTime = startTimeVal, - .duration = durationVal}); + const auto entry = PerformanceEntry{ + .name = std::string(name), + .entryType = PerformanceEntryType::MEASURE, + .startTime = startTimeVal, + .duration = durationVal}; + + { + std::lock_guard lock(buffersMutex_); + measureBuffer_.add(entry); + } + + observerRegistry_->queuePerformanceEntry(entry); } DOMHighResTimeStamp PerformanceEntryReporter::getMarkTime( const std::string& markName) const { - PerformanceEntry mark{ - .name = markName, .entryType = PerformanceEntryType::MARK}; - - std::lock_guard lock(nameLookupMutex_); - const auto& marksBuffer = getBuffer(PerformanceEntryType::MARK); - auto it = marksBuffer.nameLookup.find(&mark); - if (it != marksBuffer.nameLookup.end()) { - return (*it)->startTime; + std::lock_guard lock(buffersMutex_); + + if (auto it = markBuffer_.find(markName); it) { + return it->startTime; } else { return 0.0; } @@ -277,30 +167,47 @@ void PerformanceEntryReporter::logEventEntry( DOMHighResTimeStamp processingStart, DOMHighResTimeStamp processingEnd, uint32_t interactionId) { - logEntry( - {.name = std::move(name), - .entryType = PerformanceEntryType::EVENT, - .startTime = startTime, - .duration = duration, - .processingStart = processingStart, - .processingEnd = processingEnd, - .interactionId = interactionId}); + eventCounts_[name]++; + + const auto entry = PerformanceEntry{ + .name = std::move(name), + .entryType = PerformanceEntryType::EVENT, + .startTime = startTime, + .duration = duration, + .processingStart = processingStart, + .processingEnd = processingEnd, + .interactionId = interactionId}; + + { + std::lock_guard lock(buffersMutex_); + + if (entry.duration < eventBuffer_.durationThreshold) { + // The entries duration is lower than the desired reporting threshold, + // skip + return; + } + + eventBuffer_.add(entry); + } + + observerRegistry_->queuePerformanceEntry(entry); } void PerformanceEntryReporter::logLongTaskEntry( DOMHighResTimeStamp startTime, DOMHighResTimeStamp duration) { - logEntry( - {.name = std::string{"self"}, - .entryType = PerformanceEntryType::LONGTASK, - .startTime = startTime, - .duration = duration}); -} - -void PerformanceEntryReporter::scheduleFlushBuffer() { - if (callback_) { - callback_(); + const auto entry = PerformanceEntry{ + .name = std::string{"self"}, + .entryType = PerformanceEntryType::LONGTASK, + .startTime = startTime, + .duration = duration}; + + { + std::lock_guard lock(buffersMutex_); + longTaskBuffer_.add(entry); } + + observerRegistry_->queuePerformanceEntry(entry); } } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.h b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.h index 9f8bb0b79ffd87..dc42c6d7b78ae1 100644 --- a/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.h +++ b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.h @@ -8,78 +8,17 @@ #pragma once #include -#include "BoundedConsumableBuffer.h" - -#include -#include #include #include #include -#include -#include -#include +#include "PerformanceEntryCircularBuffer.h" +#include "PerformanceEntryKeyedBuffer.h" +#include "PerformanceObserverRegistry.h" namespace facebook::react { -using PerformanceEntryInteractionId = uint32_t; - -enum class PerformanceEntryType { - // We need to preserve these values for backwards compatibility. - MARK = 1, - MEASURE = 2, - EVENT = 3, - LONGTASK = 4, - _NEXT = 5, -}; - -struct PerformanceEntry { - std::string name; - PerformanceEntryType entryType; - DOMHighResTimeStamp startTime; - DOMHighResTimeStamp duration = 0; - - // For "event" entries only: - std::optional processingStart; - std::optional processingEnd; - std::optional interactionId; -}; - -struct PerformanceEntryHash { - size_t operator()(const PerformanceEntry* entry) const { - return std::hash()(entry->name); - } -}; - -struct PerformanceEntryEqual { - bool operator()(const PerformanceEntry* lhs, const PerformanceEntry* rhs) - const { - return lhs->name == rhs->name; - } -}; - -using PerformanceEntryRegistryType = std::unordered_set< - const PerformanceEntry*, - PerformanceEntryHash, - PerformanceEntryEqual>; - -// Default duration threshold for reporting performance entries (0 means "report -// all") -constexpr double DEFAULT_DURATION_THRESHOLD = 0.0; - -// Default buffer size limit, per entry type -constexpr size_t DEFAULT_MAX_BUFFER_SIZE = 1024; - -struct PerformanceEntryBuffer { - BoundedConsumableBuffer entries{DEFAULT_MAX_BUFFER_SIZE}; - bool isReporting{false}; - bool isAlwaysLogged{false}; - double durationThreshold{DEFAULT_DURATION_THRESHOLD}; - bool hasNameLookup{false}; - PerformanceEntryRegistryType nameLookup; -}; - -constexpr size_t NUM_PERFORMANCE_ENTRY_TYPES = - (size_t)PerformanceEntryType::_NEXT - 1; // Valid types start from 1. +constexpr size_t EVENT_BUFFER_SIZE = 150; +constexpr size_t LONG_TASK_BUFFER_SIZE = 200; constexpr DOMHighResTimeStamp LONG_TASK_DURATION_THRESHOLD_MS = 50.0; @@ -93,49 +32,67 @@ class PerformanceEntryReporter { // creation time instead of having the singleton. static std::shared_ptr& getInstance(); - struct PopPendingEntriesResult { - std::vector entries; - uint32_t droppedEntriesCount; - }; - - void setReportingCallback(std::function callback); - void startReporting(PerformanceEntryType entryType); - void stopReporting(PerformanceEntryType entryType); - void stopReporting(); - void setAlwaysLogged(PerformanceEntryType entryType, bool isAlwaysLogged); - void setDurationThreshold( - PerformanceEntryType entryType, - double durationThreshold); + PerformanceObserverRegistry& getObserverRegistry() { + return *observerRegistry_; + } - PopPendingEntriesResult popPendingEntries(); + uint32_t getDroppedEntriesCount(PerformanceEntryType type) const noexcept; - void logEntry(const PerformanceEntry& entry); + /* + * DOM Performance (High Resolution Time) + * https://www.w3.org/TR/hr-time-3/#dom-performance + */ + // https://www.w3.org/TR/hr-time-3/#now-method + DOMHighResTimeStamp getCurrentTimeStamp() const; - PerformanceEntryBuffer& getBuffer(PerformanceEntryType entryType) { - return buffers_[static_cast(entryType) - 1]; + void setTimeStampProvider(std::function provider) { + timeStampProvider_ = std::move(provider); } - const PerformanceEntryBuffer& getBuffer( - PerformanceEntryType entryType) const { - return buffers_[static_cast(entryType) - 1]; - } + // https://www.w3.org/TR/performance-timeline/#getentries-method + // https://www.w3.org/TR/performance-timeline/#getentriesbytype-method + // https://www.w3.org/TR/performance-timeline/#getentriesbyname-method + std::vector getEntries() const; + std::vector getEntriesByType( + PerformanceEntryType entryType) const; + void getEntriesByType( + PerformanceEntryType entryType, + std::vector& target) const; + std::vector getEntriesByName( + std::string_view entryName) const; + std::vector getEntriesByName( + std::string_view entryName, + PerformanceEntryType entryType) const; - bool isReporting(PerformanceEntryType entryType) const { - return getBuffer(entryType).isReporting; - } + void logEventEntry( + std::string name, + double startTime, + double duration, + double processingStart, + double processingEnd, + uint32_t interactionId); - bool isAlwaysLogged(PerformanceEntryType entryType) const { - return getBuffer(entryType).isAlwaysLogged; - } + void logLongTaskEntry(double startTime, double duration); - uint32_t getDroppedEntriesCount() const { - return droppedEntriesCount_; + /* + * Event Timing API functions + * https://www.w3.org/TR/event-timing/ + */ + // https://www.w3.org/TR/event-timing/#dom-performance-eventcounts + const std::unordered_map& getEventCounts() const { + return eventCounts_; } + /* + * User Timing Level 3 functions + * https://w3c.github.io/user-timing/ + */ + // https://w3c.github.io/user-timing/#mark-method void mark( const std::string& name, - const std::optional& startTime = std::nullopt); + const std::optional& startTime = std::nullopt); + // https://w3c.github.io/user-timing/#measure-method void measure( const std::string_view& name, double startTime, @@ -144,54 +101,57 @@ class PerformanceEntryReporter { const std::optional& startMark = std::nullopt, const std::optional& endMark = std::nullopt); + // https://w3c.github.io/user-timing/#clearmarks-method + // https://w3c.github.io/user-timing/#clearmeasures-method void clearEntries( std::optional entryType = std::nullopt, - std::string_view entryName = {}); - - std::vector getEntries( - std::optional entryType = std::nullopt, - std::string_view entryName = {}) const; - - void logEventEntry( - std::string name, - double startTime, - double duration, - double processingStart, - double processingEnd, - uint32_t interactionId); - - void logLongTaskEntry(double startTime, double duration); - - const std::unordered_map& getEventCounts() const { - return eventCounts_; - } - - DOMHighResTimeStamp getCurrentTimeStamp() const; - - void setTimeStampProvider(std::function provider) { - timeStampProvider_ = std::move(provider); - } + std::optional entryName = std::nullopt); private: - std::function callback_; + std::unique_ptr observerRegistry_; - mutable std::mutex entriesMutex_; - std::array buffers_; - std::unordered_map eventCounts_; + mutable std::mutex buffersMutex_; + PerformanceEntryCircularBuffer eventBuffer_{EVENT_BUFFER_SIZE}; + PerformanceEntryCircularBuffer longTaskBuffer_{LONG_TASK_BUFFER_SIZE}; + PerformanceEntryKeyedBuffer markBuffer_; + PerformanceEntryKeyedBuffer measureBuffer_; - uint32_t droppedEntriesCount_{0}; + std::unordered_map eventCounts_; std::function timeStampProvider_ = nullptr; - mutable std::mutex nameLookupMutex_; - double getMarkTime(const std::string& markName) const; - void scheduleFlushBuffer(); - void getEntries( - PerformanceEntryType entryType, - std::string_view entryName, - std::vector& res) const; + inline PerformanceEntryBuffer& getBufferRef(PerformanceEntryType entryType) { + switch (entryType) { + case PerformanceEntryType::EVENT: + return eventBuffer_; + case PerformanceEntryType::MARK: + return markBuffer_; + case PerformanceEntryType::MEASURE: + return measureBuffer_; + case PerformanceEntryType::LONGTASK: + return longTaskBuffer_; + case PerformanceEntryType::_NEXT: + throw std::logic_error("Cannot get buffer for _NEXT entry type"); + } + } + + const inline PerformanceEntryBuffer& getBuffer( + PerformanceEntryType entryType) const { + switch (entryType) { + case PerformanceEntryType::EVENT: + return eventBuffer_; + case PerformanceEntryType::MARK: + return markBuffer_; + case PerformanceEntryType::MEASURE: + return measureBuffer_; + case PerformanceEntryType::LONGTASK: + return longTaskBuffer_; + case PerformanceEntryType::_NEXT: + throw std::logic_error("Cannot get buffer for _NEXT entry type"); + } + } }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/performance/timeline/PerformanceObserver.cpp b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceObserver.cpp new file mode 100644 index 00000000000000..077e20132ddb05 --- /dev/null +++ b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceObserver.cpp @@ -0,0 +1,92 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "PerformanceObserver.h" +#include "PerformanceEntryReporter.h" + +namespace facebook::react { + +void PerformanceObserver::handleEntry(const PerformanceEntry& entry) { + if (observedTypes_.contains(entry.entryType)) { + // https://www.w3.org/TR/event-timing/#should-add-performanceeventtiming + if (entry.entryType == PerformanceEntryType::EVENT && + entry.duration < durationThreshold_) { + // The entries duration is lower than the desired reporting threshold, + // skip + return; + } + + buffer_.push_back(entry); + scheduleFlushBuffer(); + } +} + +std::vector PerformanceObserver::takeRecords() { + std::vector result; + buffer_.swap(result); + + didScheduleFlushBuffer_ = false; + + return result; +} + +void PerformanceObserver::observe( + PerformanceEntryType type, + PerformanceObserverObserveSingleOptions options) { + observedTypes_.insert(type); + + durationThreshold_ = options.durationThreshold; + requiresDroppedEntries_ = true; + + if (options.buffered) { + auto& reporter = PerformanceEntryReporter::getInstance(); + + auto bufferedEntries = reporter->getEntriesByType(type); + for (auto& bufferedEntry : bufferedEntries) { + handleEntry(bufferedEntry); + } + } + + registry_.addObserver(shared_from_this()); +} + +void PerformanceObserver::observe( + std::unordered_set types) { + observedTypes_ = std::move(types); + requiresDroppedEntries_ = false; + registry_.addObserver(shared_from_this()); +} + +uint32_t PerformanceObserver::getDroppedEntriesCount() noexcept { + uint32_t droppedEntriesCount = 0; + + if (requiresDroppedEntries_) { + auto reporter = PerformanceEntryReporter::getInstance(); + + for (auto& entryType : observedTypes_) { + droppedEntriesCount += reporter->getDroppedEntriesCount(entryType); + } + + requiresDroppedEntries_ = false; + } + + return droppedEntriesCount; +} + +void PerformanceObserver::disconnect() noexcept { + registry_.removeObserver(shared_from_this()); +} + +void PerformanceObserver::scheduleFlushBuffer() { + if (!didScheduleFlushBuffer_) { + didScheduleFlushBuffer_ = true; + + callback_(); + } +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/performance/timeline/PerformanceObserver.h b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceObserver.h new file mode 100644 index 00000000000000..a79d8d0a99feb4 --- /dev/null +++ b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceObserver.h @@ -0,0 +1,139 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include "PerformanceEntryBuffer.h" +#include "PerformanceObserverRegistry.h" + +namespace facebook::react { + +using PerformanceObserverEntryTypeFilter = + std::unordered_set; +using PerformanceObserverCallback = std::function; + +/** + * Represents subset of spec's `PerformanceObserverInit` that is allowed for + * multiple types. + * + * https://w3c.github.io/performance-timeline/#performanceobserverinit-dictionary + */ +struct PerformanceObserverObserveMultipleOptions { + double durationThreshold = 0.0; +}; + +/** + * Represents subset of spec's `PerformanceObserverInit` that is allowed for + * single type. + * + * https://w3c.github.io/performance-timeline/#performanceobserverinit-dictionary + */ +struct PerformanceObserverObserveSingleOptions { + bool buffered = false; + double durationThreshold = 0.0; +}; + +/** + * Represents native counterpart of performance timeline PerformanceObserver + * class. Each instance has its own entry buffer and can listen for different + * performance entry types. + * + * Entries are pushed to the observer by the `PerformanceEntryReporter` class, + * through the `PerformanceObserverRegistry` class which acts as a central hub. + */ +class PerformanceObserver + : public std::enable_shared_from_this { + private: + struct PrivateUseCreateMethod { + explicit PrivateUseCreateMethod() = default; + }; + + public: + explicit PerformanceObserver( + PrivateUseCreateMethod /*unused*/, + PerformanceObserverRegistry& registry, + PerformanceObserverCallback&& callback) + : registry_(registry), callback_(std::move(callback)) {} + + static std::shared_ptr create( + PerformanceObserverRegistry& registry, + PerformanceObserverCallback&& callback) { + return std::make_shared( + PrivateUseCreateMethod(), registry, std::move(callback)); + } + + ~PerformanceObserver() = default; + + /** + * Append entry to the buffer if this observer should handle this entry. + */ + void handleEntry(const PerformanceEntry& entry); + + /** + * Returns current observer buffer and clears it. + * + * Spec: + * https://w3c.github.io/performance-timeline/#takerecords-method + */ + [[nodiscard]] std::vector takeRecords(); + + /** + * Configures the observer to watch for specified entry type. + * + * This operation resets and overrides previous configurations. So consecutive + * calls to this methods remove any previous watch configuration (as per + * spec). + */ + void observe( + PerformanceEntryType type, + PerformanceObserverObserveSingleOptions options = {}); + + /** + * Configures the observer to watch for specified entry type. + * + * This operation resets and overrides previous configurations. So consecutive + * calls to this methods remove any previous watch configuration (as per + * spec). + */ + void observe(std::unordered_set types); + + /** + * Disconnects observer from the registry + */ + void disconnect() noexcept; + + /** + * Internal function called by JS bridge to get number of dropped entries + * count counted at call time. + */ + uint32_t getDroppedEntriesCount() noexcept; + + private: + void scheduleFlushBuffer(); + + PerformanceObserverRegistry& registry_; + PerformanceObserverCallback callback_; + PerformanceObserverEntryTypeFilter observedTypes_; + + /// https://www.w3.org/TR/event-timing/#sec-modifications-perf-timeline + double durationThreshold_{DEFAULT_DURATION_THRESHOLD}; + std::vector buffer_; + bool didScheduleFlushBuffer_ = false; + bool requiresDroppedEntries_ = false; +}; + +inline bool operator==( + const PerformanceObserver& lhs, + const PerformanceObserver& rhs) { + return &lhs == &rhs; +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/performance/timeline/PerformanceObserverRegistry.cpp b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceObserverRegistry.cpp new file mode 100644 index 00000000000000..da962692fb370d --- /dev/null +++ b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceObserverRegistry.cpp @@ -0,0 +1,34 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "PerformanceObserverRegistry.h" +#include "PerformanceObserver.h" + +namespace facebook::react { + +void PerformanceObserverRegistry::addObserver( + std::shared_ptr observer) { + std::lock_guard guard(observersMutex_); + observers_.insert(observer); +} + +void PerformanceObserverRegistry::removeObserver( + std::shared_ptr observer) { + std::lock_guard guard(observersMutex_); + observers_.erase(observer); +} + +void PerformanceObserverRegistry::queuePerformanceEntry( + const PerformanceEntry& entry) { + std::lock_guard lock(observersMutex_); + + for (auto& observer : observers_) { + observer->handleEntry(entry); + } +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/performance/timeline/PerformanceObserverRegistry.h b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceObserverRegistry.h new file mode 100644 index 00000000000000..78c01c43697599 --- /dev/null +++ b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceObserverRegistry.h @@ -0,0 +1,54 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include "PerformanceEntry.h" + +namespace facebook::react { + +class PerformanceObserver; + +/** + * PerformanceObserverRegistry acts as a container for known performance + * observer instances. + * + * You can queue performance entries through this registry, which then delegates + * the entry to all registered observers. + */ +class PerformanceObserverRegistry { + public: + PerformanceObserverRegistry() = default; + + /** + * Adds observer to the registry. + */ + void addObserver(std::shared_ptr observer); + + /** + * Removes observer from the registry. + */ + void removeObserver(std::shared_ptr observer); + + /** + * Delegates specified performance `entry` to all registered observers + * in this registry. + */ + void queuePerformanceEntry(const PerformanceEntry& entry); + + private: + mutable std::mutex observersMutex_; + std::set< + std::shared_ptr, + std::owner_less>> + observers_; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/performance/timeline/tests/BoundedConsumableBufferTest.cpp b/packages/react-native/ReactCommon/react/performance/timeline/tests/BoundedConsumableBufferTest.cpp deleted file mode 100644 index 19ef891e287a2d..00000000000000 --- a/packages/react-native/ReactCommon/react/performance/timeline/tests/BoundedConsumableBufferTest.cpp +++ /dev/null @@ -1,219 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -#include - -#include - -#include "../BoundedConsumableBuffer.h" - -namespace facebook::react { - -using namespace facebook::react; - -constexpr auto OK = BoundedConsumableBuffer::PushStatus::OK; -constexpr auto DROP = BoundedConsumableBuffer::PushStatus::DROP; -constexpr auto OVERWRITE = BoundedConsumableBuffer::PushStatus::OVERWRITE; - -TEST(BoundedConsumableBuffer, CanAddAndRetrieveElements) { - BoundedConsumableBuffer buffer; - - ASSERT_EQ(OK, buffer.add(1)); - ASSERT_EQ(OK, buffer.add(2)); - - ASSERT_EQ(1, buffer[0]); - ASSERT_EQ(2, buffer[1]); - ASSERT_EQ(2, buffer.size()); - ASSERT_EQ(std::vector({1, 2}), buffer.getEntries()); - - ASSERT_EQ(OK, buffer.add(3)); - ASSERT_EQ(3, buffer.size()); - ASSERT_EQ(std::vector({1, 2, 3}), buffer.getEntries()); - - ASSERT_EQ(1, buffer[0]); - ASSERT_EQ(2, buffer[1]); - ASSERT_EQ(3, buffer[2]); -} - -TEST(BoundedConsumableBuffer, CanConsumeElements) { - BoundedConsumableBuffer buffer; - - ASSERT_EQ(OK, buffer.add(1)); - ASSERT_EQ(OK, buffer.add(2)); - - ASSERT_EQ(std::vector({1, 2}), buffer.getEntries()); - - auto elems1 = buffer.consume(); - - ASSERT_EQ(std::vector({1, 2}), elems1); - ASSERT_EQ(std::vector({1, 2}), buffer.getEntries()); - - auto elems2 = buffer.consume(); - ASSERT_TRUE(elems2.empty()); - - ASSERT_EQ(std::vector({1, 2}), buffer.getEntries()); - - ASSERT_EQ(OK, buffer.add(3)); - ASSERT_EQ(std::vector({1, 2, 3}), buffer.getEntries()); - auto elems3 = buffer.consume(); - ASSERT_EQ(std::vector({3}), elems3); - - auto elems4 = buffer.consume(); - ASSERT_TRUE(elems4.empty()); - - ASSERT_EQ(OK, buffer.add(4)); - ASSERT_EQ(OK, buffer.add(5)); - - ASSERT_EQ(std::vector({1, 2, 3, 4, 5}), buffer.getEntries()); - auto elems5 = buffer.consume(); - ASSERT_EQ(std::vector({4, 5}), elems5); - - auto elems6 = buffer.consume(); - ASSERT_TRUE(elems6.empty()); -} - -TEST(BoundedConsumableBuffer, WrapsAroundCorrectly) { - BoundedConsumableBuffer buffer(3); - - ASSERT_EQ(OK, buffer.add(1)); - ASSERT_EQ(OK, buffer.add(2)); - - auto elems1 = buffer.consume(); - - ASSERT_EQ(std::vector({1, 2}), buffer.getEntries()); - ASSERT_EQ(std::vector({1, 2}), elems1); - - auto elems2 = buffer.consume(); - ASSERT_TRUE(elems2.empty()); - - ASSERT_EQ(OK, buffer.add(3)); - ASSERT_EQ(std::vector({1, 2, 3}), buffer.getEntries()); - auto elems3 = buffer.consume(); - ASSERT_EQ(std::vector({3}), elems3); - - auto elems4 = buffer.consume(); - ASSERT_TRUE(elems4.empty()); - - ASSERT_EQ(OVERWRITE, buffer.add(4)); - ASSERT_EQ(OVERWRITE, buffer.add(5)); - - ASSERT_EQ(std::vector({3, 4, 5}), buffer.getEntries()); - auto elems5 = buffer.consume(); - ASSERT_EQ(std::vector({4, 5}), elems5); - - auto elems6 = buffer.consume(); - ASSERT_TRUE(elems6.empty()); - - ASSERT_EQ(OVERWRITE, buffer.add(6)); - ASSERT_EQ(OVERWRITE, buffer.add(7)); - - ASSERT_EQ(std::vector({5, 6, 7}), buffer.getEntries()); - - auto elems7 = buffer.consume(); - - ASSERT_EQ(std::vector({5, 6, 7}), buffer.getEntries()); - ASSERT_EQ(std::vector({6, 7}), elems7); - - ASSERT_EQ(OVERWRITE, buffer.add(8)); - ASSERT_EQ(OVERWRITE, buffer.add(9)); - ASSERT_EQ(OVERWRITE, buffer.add(10)); - ASSERT_EQ(DROP, buffer.add(11)); - ASSERT_EQ(std::vector({9, 10, 11}), buffer.getEntries()); - - ASSERT_EQ(DROP, buffer.add(12)); - ASSERT_EQ(std::vector({10, 11, 12}), buffer.getEntries()); - - ASSERT_EQ(10, buffer[0]); - ASSERT_EQ(11, buffer[1]); - ASSERT_EQ(12, buffer[2]); - - ASSERT_EQ(DROP, buffer.add(13)); - ASSERT_EQ(std::vector({11, 12, 13}), buffer.getEntries()); - auto elems8 = buffer.consume(); - ASSERT_EQ(std::vector({11, 12, 13}), elems8); - - ASSERT_EQ(11, buffer[0]); - ASSERT_EQ(12, buffer[1]); - ASSERT_EQ(13, buffer[2]); - ASSERT_EQ(11, *buffer.getNextOverwriteCandidate()); - - ASSERT_EQ(OVERWRITE, buffer.add(14)); - ASSERT_EQ(14, buffer.back()); - - ASSERT_EQ(std::vector({12, 13, 14}), buffer.getEntries()); - auto elems9 = buffer.consume(); - ASSERT_EQ(std::vector({14}), elems9); -} - -TEST(BoundedConsumableBuffer, CanClearByPredicate) { - BoundedConsumableBuffer buffer(5); - - buffer.add(1); - buffer.add(0); - buffer.add(2); - buffer.add(0); - - buffer.consume(); - buffer.add(3); - - buffer.add(0); - buffer.add(4); - - buffer.clear([](const int& el) { return el == 0; }); - - ASSERT_EQ(std::vector({2, 3, 4}), buffer.getEntries()); - auto elems = buffer.consume(); - ASSERT_EQ(std::vector({3, 4}), elems); - ASSERT_EQ(std::vector({2, 3, 4}), buffer.getEntries()); -} - -TEST(BoundedConsumableBuffer, CanClearBeforeReachingMaxSize) { - BoundedConsumableBuffer buffer(5); - - buffer.add(1); - buffer.add(2); - buffer.consume(); - buffer.add(3); - - buffer.clear([](const int&) { return false; }); // no-op clear - ASSERT_EQ(std::vector({1, 2, 3}), buffer.getEntries()); - - buffer.add(4); - buffer.add(5); - - ASSERT_EQ(std::vector({3, 4, 5}), buffer.consume()); - ASSERT_EQ(std::vector({1, 2, 3, 4, 5}), buffer.getEntries()); -} - -TEST(BoundedConsumableBuffer, CanGetByPredicate) { - BoundedConsumableBuffer buffer(5); - - buffer.add(1); - buffer.add(0); - buffer.add(2); - buffer.add(0); - - buffer.consume(); - buffer.add(3); - - ASSERT_EQ(std::vector({1, 2, 3}), buffer.getEntries([](const int& el) { - return el != 0; - })); - - buffer.add(0); - buffer.add(4); - - ASSERT_EQ(std::vector({2, 3, 4}), buffer.getEntries([](const int& el) { - return el != 0; - })); - auto elems = buffer.consume(); - ASSERT_EQ(std::vector({2, 3, 4}), buffer.getEntries([](const int& el) { - return el != 0; - })); -} - -} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/performance/timeline/tests/CircularBufferTest.cpp b/packages/react-native/ReactCommon/react/performance/timeline/tests/CircularBufferTest.cpp new file mode 100644 index 00000000000000..a198b98e6b0715 --- /dev/null +++ b/packages/react-native/ReactCommon/react/performance/timeline/tests/CircularBufferTest.cpp @@ -0,0 +1,127 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include + +#include + +#include "../CircularBuffer.h" + +namespace facebook::react { + +using namespace facebook::react; + +constexpr auto OK = false; +constexpr auto OVERWRITE = true; + +TEST(CircularBuffer, CanAddAndRetrieveElements) { + CircularBuffer buffer{5}; + + ASSERT_EQ(OK, buffer.add(1)); + ASSERT_EQ(OK, buffer.add(2)); + + ASSERT_EQ(1, buffer[0]); + ASSERT_EQ(2, buffer[1]); + ASSERT_EQ(2, buffer.size()); + ASSERT_EQ(std::vector({1, 2}), buffer.getEntries()); + + ASSERT_EQ(OK, buffer.add(3)); + ASSERT_EQ(3, buffer.size()); + ASSERT_EQ(std::vector({1, 2, 3}), buffer.getEntries()); + + ASSERT_EQ(1, buffer[0]); + ASSERT_EQ(2, buffer[1]); + ASSERT_EQ(3, buffer[2]); +} + +TEST(BoundedConsumableBuffer, WrapsAroundCorrectly) { + CircularBuffer buffer(3); + + ASSERT_EQ(OK, buffer.add(1)); + ASSERT_EQ(OK, buffer.add(2)); + + ASSERT_EQ(std::vector({1, 2}), buffer.getEntries()); + + ASSERT_EQ(OK, buffer.add(3)); + ASSERT_EQ(std::vector({1, 2, 3}), buffer.getEntries()); + + ASSERT_EQ(OVERWRITE, buffer.add(4)); + ASSERT_EQ(OVERWRITE, buffer.add(5)); + + ASSERT_EQ(std::vector({3, 4, 5}), buffer.getEntries()); + + ASSERT_EQ(OVERWRITE, buffer.add(6)); + ASSERT_EQ(OVERWRITE, buffer.add(7)); + + ASSERT_EQ(std::vector({5, 6, 7}), buffer.getEntries()); + + ASSERT_EQ(OVERWRITE, buffer.add(8)); + ASSERT_EQ(OVERWRITE, buffer.add(9)); + ASSERT_EQ(OVERWRITE, buffer.add(10)); + ASSERT_EQ(std::vector({8, 9, 10}), buffer.getEntries()); + + ASSERT_EQ(8, buffer[0]); + ASSERT_EQ(9, buffer[1]); + ASSERT_EQ(10, buffer[2]); +} + +TEST(BoundedConsumableBuffer, CanClearByPredicate) { + CircularBuffer buffer(5); + + buffer.add(1); + buffer.add(0); + buffer.add(2); + buffer.add(0); + buffer.add(3); + + buffer.clear([](const int& el) { return el == 0; }); + ASSERT_EQ(std::vector({1, 2, 3}), buffer.getEntries()); + + buffer.add(0); + buffer.add(4); + buffer.clear([](const int& el) { return el == 0; }); + ASSERT_EQ(std::vector({1, 2, 3, 4}), buffer.getEntries()); +} + +TEST(BoundedConsumableBuffer, CanClearBeforeReachingMaxSize) { + CircularBuffer buffer(5); + + buffer.add(1); + buffer.add(2); + buffer.add(3); + + buffer.clear([](const int&) { return false; }); // no-op clear + ASSERT_EQ(std::vector({1, 2, 3}), buffer.getEntries()); + + buffer.add(4); + buffer.add(5); + + ASSERT_EQ(std::vector({1, 2, 3, 4, 5}), buffer.getEntries()); +} + +TEST(BoundedConsumableBuffer, CanGetByPredicate) { + CircularBuffer buffer(5); + + buffer.add(1); + buffer.add(0); + buffer.add(2); + buffer.add(0); + buffer.add(3); + + ASSERT_EQ(std::vector({1, 2, 3}), buffer.getEntries([](const int& el) { + return el != 0; + })); + + buffer.add(0); + buffer.add(4); + + ASSERT_EQ(std::vector({2, 3, 4}), buffer.getEntries([](const int& el) { + return el != 0; + })); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/performance/timeline/tests/PerformanceEntryReporterTest.cpp b/packages/react-native/ReactCommon/react/performance/timeline/tests/PerformanceEntryReporterTest.cpp index 65cb1a97887b25..c247e59922f849 100644 --- a/packages/react-native/ReactCommon/react/performance/timeline/tests/PerformanceEntryReporterTest.cpp +++ b/packages/react-native/ReactCommon/react/performance/timeline/tests/PerformanceEntryReporterTest.cpp @@ -27,90 +27,40 @@ namespace facebook::react { std::ostream& os, const PerformanceEntry& entry) { static constexpr const char* entryTypeNames[] = { - "UNDEFINED", - "MARK", - "MEASURE", - "EVENT", + "PerformanceEntryType::UNDEFINED", + "PerformanceEntryType::MARK", + "PerformanceEntryType::MEASURE", + "PerformanceEntryType::EVENT", }; - return os << "{ name: " << entry.name - << ", type: " << entryTypeNames[static_cast(entry.entryType)] - << ", startTime: " << entry.startTime - << ", duration: " << entry.duration << " }"; + return os << "{ .name = \"" << entry.name << "\"" << ", .entryType = " + << entryTypeNames[static_cast(entry.entryType)] + << ", .startTime = " << entry.startTime + << ", .duration = " << entry.duration << " }"; } -} // namespace facebook::react - -using namespace facebook::react; - -TEST(PerformanceEntryReporter, PerformanceEntryReporterTestStartReporting) { - auto reporter = PerformanceEntryReporter::getInstance(); - - reporter->stopReporting(); - reporter->clearEntries(); - - reporter->startReporting(PerformanceEntryType::MARK); - reporter->startReporting(PerformanceEntryType::MEASURE); - ASSERT_TRUE(reporter->isReporting(PerformanceEntryType::MARK)); - ASSERT_TRUE(reporter->isReporting(PerformanceEntryType::MEASURE)); - - ASSERT_FALSE(reporter->isReporting(PerformanceEntryType::EVENT)); +static std::vector toSorted( + const std::vector& originalEntries) { + std::vector entries = originalEntries; + std::stable_sort(entries.begin(), entries.end(), PerformanceEntrySorter{}); + return entries; } +} // namespace facebook::react -TEST(PerformanceEntryReporter, PerformanceEntryReporterTestStopReporting) { - auto reporter = PerformanceEntryReporter::getInstance(); - - reporter->stopReporting(); - reporter->clearEntries(); - - reporter->startReporting(PerformanceEntryType::MARK); - - reporter->mark("mark0", 0.0); - reporter->mark("mark1", 0.0); - reporter->mark("mark2", 0.0); - reporter->measure("measure0", 0.0, 0.0); - - auto res = reporter->popPendingEntries(); - const auto& entries = res.entries; - - ASSERT_EQ(0, res.droppedEntriesCount); - ASSERT_EQ(3, entries.size()); - - res = reporter->popPendingEntries(); - - ASSERT_EQ(0, res.droppedEntriesCount); - ASSERT_EQ(0, res.entries.size()); - - reporter->stopReporting(PerformanceEntryType::MARK); - reporter->startReporting(PerformanceEntryType::MEASURE); - - reporter->mark("mark3"); - reporter->measure("measure1", 0.0, 0.0); - - res = reporter->popPendingEntries(); - - ASSERT_EQ(0, res.droppedEntriesCount); - ASSERT_EQ(1, res.entries.size()); - ASSERT_STREQ("measure1", res.entries[0].name.c_str()); -} +using namespace facebook::react; TEST(PerformanceEntryReporter, PerformanceEntryReporterTestReportMarks) { auto reporter = PerformanceEntryReporter::getInstance(); - reporter->stopReporting(); reporter->clearEntries(); - reporter->startReporting(PerformanceEntryType::MARK); - reporter->mark("mark0", 0.0); reporter->mark("mark1", 1.0); reporter->mark("mark2", 2.0); // Report mark0 again reporter->mark("mark0", 3.0); - auto res = reporter->popPendingEntries(); - const auto& entries = res.entries; + const auto entries = toSorted(reporter->getEntries()); - ASSERT_EQ(0, res.droppedEntriesCount); ASSERT_EQ(4, entries.size()); const std::vector expected = { @@ -133,13 +83,8 @@ TEST(PerformanceEntryReporter, PerformanceEntryReporterTestReportMarks) { TEST(PerformanceEntryReporter, PerformanceEntryReporterTestReportMeasures) { auto reporter = PerformanceEntryReporter::getInstance(); - - reporter->stopReporting(); reporter->clearEntries(); - reporter->startReporting(PerformanceEntryType::MARK); - reporter->startReporting(PerformanceEntryType::MEASURE); - reporter->mark("mark0", 0.0); reporter->mark("mark1", 1.0); reporter->mark("mark2", 2.0); @@ -153,109 +98,84 @@ TEST(PerformanceEntryReporter, PerformanceEntryReporterTestReportMeasures) { reporter->setTimeStampProvider([]() { return 3.5; }); reporter->measure("measure5", 0.0, 0.0, std::nullopt, "mark2"); - reporter->mark("mark3", 2.0); + reporter->mark("mark3", 2.5); reporter->measure("measure6", 2.0, 2.0); - reporter->mark("mark4", 2.0); + reporter->mark("mark4", 2.1); reporter->mark("mark4", 3.0); // Uses the last reported time for mark4 reporter->measure("measure7", 0.0, 0.0, std::nullopt, "mark1", "mark4"); - auto res = reporter->popPendingEntries(); - const auto& entries = res.entries; - - ASSERT_EQ(0, res.droppedEntriesCount); + const auto entries = toSorted(reporter->getEntries()); const std::vector expected = { {.name = "mark0", .entryType = PerformanceEntryType::MARK, - .startTime = 0.0}, + .startTime = 0, + .duration = 0}, {.name = "measure0", .entryType = PerformanceEntryType::MEASURE, - .startTime = 0.0, - .duration = 2.0}, + .startTime = 0, + .duration = 2}, {.name = "measure1", .entryType = PerformanceEntryType::MEASURE, - .startTime = 0.0, - .duration = 4.0}, + .startTime = 0, + .duration = 4}, {.name = "mark1", .entryType = PerformanceEntryType::MARK, - .startTime = 1.0}, + .startTime = 1, + .duration = 0}, {.name = "measure2", .entryType = PerformanceEntryType::MEASURE, - .startTime = 1.0, - .duration = 1.0}, + .startTime = 1, + .duration = 1}, {.name = "measure7", .entryType = PerformanceEntryType::MEASURE, - .startTime = 1.0, - .duration = 2.0}, + .startTime = 1, + .duration = 2}, {.name = "measure3", .entryType = PerformanceEntryType::MEASURE, - .startTime = 1.0, - .duration = 5.0}, + .startTime = 1, + .duration = 5}, {.name = "measure4", .entryType = PerformanceEntryType::MEASURE, .startTime = 1.5, .duration = 0.5}, {.name = "mark2", .entryType = PerformanceEntryType::MARK, - .startTime = 2.0}, - {.name = "mark3", - .entryType = PerformanceEntryType::MARK, - .startTime = 2.0}, - {.name = "mark4", - .entryType = PerformanceEntryType::MARK, - .startTime = 2.0}, + .startTime = 2, + .duration = 0}, {.name = "measure6", .entryType = PerformanceEntryType::MEASURE, - .startTime = 2.0, - .duration = 0.0}, + .startTime = 2, + .duration = 0}, {.name = "measure5", .entryType = PerformanceEntryType::MEASURE, - .startTime = 2.0, + .startTime = 2, .duration = 1.5}, {.name = "mark4", .entryType = PerformanceEntryType::MARK, - .startTime = 3.0}}; + .startTime = 2.1, + .duration = 0}, + {.name = "mark3", + .entryType = PerformanceEntryType::MARK, + .startTime = 2.5, + .duration = 0}, + {.name = "mark4", + .entryType = PerformanceEntryType::MARK, + .startTime = 3, + .duration = 0}}; ASSERT_EQ(expected, entries); } -static std::vector getNames( - const std::vector& entries) { - std::vector res; - std::transform( - entries.begin(), - entries.end(), - std::back_inserter(res), - [](const PerformanceEntry& e) { return e.name; }); - return res; -} - -static std::vector getTypes( - const std::vector& entries) { - std::vector res; - std::transform( - entries.begin(), - entries.end(), - std::back_inserter(res), - [](const PerformanceEntry& e) { return e.entryType; }); - return res; -} - TEST(PerformanceEntryReporter, PerformanceEntryReporterTestGetEntries) { auto reporter = PerformanceEntryReporter::getInstance(); - - reporter->stopReporting(); reporter->clearEntries(); - auto res = reporter->popPendingEntries(); - const auto& entries = res.entries; - - ASSERT_EQ(0, res.droppedEntriesCount); - ASSERT_EQ(0, entries.size()); - - reporter->startReporting(PerformanceEntryType::MARK); - reporter->startReporting(PerformanceEntryType::MEASURE); + { + const auto entries = reporter->getEntries(); + ASSERT_EQ(0, entries.size()); + } reporter->mark("common_name", 0.0); reporter->mark("mark1", 1.0); @@ -267,66 +187,110 @@ TEST(PerformanceEntryReporter, PerformanceEntryReporterTestGetEntries) { reporter->measure("measure3", 0.0, 0.0, 5.0, "mark1"); reporter->measure("measure4", 1.5, 0.0, std::nullopt, std::nullopt, "mark2"); - res = reporter->popPendingEntries(); - ASSERT_EQ(0, res.droppedEntriesCount); - ASSERT_EQ(8, res.entries.size()); - - reporter->getEntries(PerformanceEntryType::MARK); - const auto marks = reporter->getEntries(PerformanceEntryType::MARK); - - const auto measures = reporter->getEntries(PerformanceEntryType::MEASURE); - const auto common_name = reporter->getEntries(std::nullopt, "common_name"); - - reporter->getEntries(); - const auto all = reporter->getEntries(); - - ASSERT_EQ( - std::vector( - {PerformanceEntryType::MEASURE, - PerformanceEntryType::MEASURE, - PerformanceEntryType::MEASURE, - PerformanceEntryType::MEASURE, - PerformanceEntryType::MEASURE}), - getTypes(measures)); - ASSERT_EQ( - std::vector({PerformanceEntryType::MARK, PerformanceEntryType::MEASURE}), - getTypes(common_name)); - ASSERT_EQ( - std::vector( - {PerformanceEntryType::MARK, - PerformanceEntryType::MARK, - PerformanceEntryType::MARK, - PerformanceEntryType::MEASURE, - PerformanceEntryType::MEASURE, - PerformanceEntryType::MEASURE, - PerformanceEntryType::MEASURE, - PerformanceEntryType::MEASURE}), - getTypes(all)); - ASSERT_EQ( - std::vector( - {PerformanceEntryType::MARK, - PerformanceEntryType::MARK, - PerformanceEntryType::MARK}), - getTypes(marks)); - - ASSERT_EQ( - std::vector({"common_name", "mark1", "mark2"}), - getNames(marks)); - - ASSERT_EQ( - std::vector({"common_name", "common_name"}), - getNames(common_name)); + { + const auto allEntries = toSorted(reporter->getEntries()); + const std::vector expected = { + {.name = "common_name", + .entryType = PerformanceEntryType::MARK, + .startTime = 0, + .duration = 0}, + {.name = "common_name", + .entryType = PerformanceEntryType::MEASURE, + .startTime = 0, + .duration = 2}, + {.name = "measure1", + .entryType = PerformanceEntryType::MEASURE, + .startTime = 0, + .duration = 4}, + {.name = "mark1", + .entryType = PerformanceEntryType::MARK, + .startTime = 1, + .duration = 0}, + {.name = "measure2", + .entryType = PerformanceEntryType::MEASURE, + .startTime = 1, + .duration = 1}, + {.name = "measure3", + .entryType = PerformanceEntryType::MEASURE, + .startTime = 1, + .duration = 5}, + {.name = "measure4", + .entryType = PerformanceEntryType::MEASURE, + .startTime = 1.5, + .duration = 0.5}, + {.name = "mark2", + .entryType = PerformanceEntryType::MARK, + .startTime = 2, + .duration = 0}}; + ASSERT_EQ(expected, allEntries); + } + + { + const auto marks = + toSorted(reporter->getEntriesByType(PerformanceEntryType::MARK)); + const std::vector expected = { + {.name = "common_name", + .entryType = PerformanceEntryType::MARK, + .startTime = 0, + .duration = 0}, + {.name = "mark1", + .entryType = PerformanceEntryType::MARK, + .startTime = 1, + .duration = 0}, + {.name = "mark2", + .entryType = PerformanceEntryType::MARK, + .startTime = 2, + .duration = 0}}; + ASSERT_EQ(expected, marks); + } + + { + const auto measures = + toSorted(reporter->getEntriesByType(PerformanceEntryType::MEASURE)); + const std::vector expected = { + {.name = "common_name", + .entryType = PerformanceEntryType::MEASURE, + .startTime = 0, + .duration = 2}, + {.name = "measure1", + .entryType = PerformanceEntryType::MEASURE, + .startTime = 0, + .duration = 4}, + {.name = "measure2", + .entryType = PerformanceEntryType::MEASURE, + .startTime = 1, + .duration = 1}, + {.name = "measure3", + .entryType = PerformanceEntryType::MEASURE, + .startTime = 1, + .duration = 5}, + {.name = "measure4", + .entryType = PerformanceEntryType::MEASURE, + .startTime = 1.5, + .duration = 0.5}, + }; + ASSERT_EQ(expected, measures); + } + + { + const std::vector expected = { + {.name = "common_name", + .entryType = PerformanceEntryType::MARK, + .startTime = 0, + .duration = 0}, + {.name = "common_name", + .entryType = PerformanceEntryType::MEASURE, + .startTime = 0, + .duration = 2}}; + const auto commonName = toSorted(reporter->getEntriesByName("common_name")); + ASSERT_EQ(expected, commonName); + } } TEST(PerformanceEntryReporter, PerformanceEntryReporterTestClearEntries) { auto reporter = PerformanceEntryReporter::getInstance(); - - reporter->stopReporting(); reporter->clearEntries(); - reporter->startReporting(PerformanceEntryType::MARK); - reporter->startReporting(PerformanceEntryType::MEASURE); - reporter->mark("common_name", 0.0); reporter->mark("mark1", 1.0); reporter->mark("mark2", 2.0); @@ -337,32 +301,87 @@ TEST(PerformanceEntryReporter, PerformanceEntryReporterTestClearEntries) { reporter->measure("measure3", 0.0, 0.0, 5.0, "mark1"); reporter->measure("measure4", 1.5, 0.0, std::nullopt, std::nullopt, "mark2"); - reporter->clearEntries(std::nullopt, "common_name"); - auto e1 = reporter->getEntries(); - - ASSERT_EQ(6, e1.size()); - ASSERT_EQ( - std::vector( - {"mark1", "mark2", "measure1", "measure2", "measure3", "measure4"}), - getNames(e1)); - - reporter->clearEntries(PerformanceEntryType::MARK, "mark1"); - auto e2 = reporter->getEntries(); - - ASSERT_EQ(5, e2.size()); - ASSERT_EQ( - std::vector( - {"mark2", "measure1", "measure2", "measure3", "measure4"}), - getNames(e2)); - - reporter->clearEntries(PerformanceEntryType::MEASURE); - auto e3 = reporter->getEntries(); - - ASSERT_EQ(1, e3.size()); - ASSERT_EQ(std::vector({"mark2"}), getNames(e3)); - - reporter->clearEntries(); - auto e4 = reporter->getEntries(); - - ASSERT_EQ(0, e4.size()); + { + reporter->clearEntries(std::nullopt, "common_name"); + auto entriesWithoutCommonName = toSorted(reporter->getEntries()); + std::vector expected = { + {.name = "measure1", + .entryType = PerformanceEntryType::MEASURE, + .startTime = 0, + .duration = 4}, + {.name = "mark1", + .entryType = PerformanceEntryType::MARK, + .startTime = 1, + .duration = 0}, + {.name = "measure2", + .entryType = PerformanceEntryType::MEASURE, + .startTime = 1, + .duration = 1}, + {.name = "measure3", + .entryType = PerformanceEntryType::MEASURE, + .startTime = 1, + .duration = 5}, + {.name = "measure4", + .entryType = PerformanceEntryType::MEASURE, + .startTime = 1.5, + .duration = 0.5}, + {.name = "mark2", + .entryType = PerformanceEntryType::MARK, + .startTime = 2, + .duration = 0}}; + + ASSERT_EQ(6, entriesWithoutCommonName.size()); + ASSERT_EQ(expected, entriesWithoutCommonName); + } + + { + reporter->clearEntries(PerformanceEntryType::MARK, "mark1"); + auto entriesWithoutMark1 = toSorted(reporter->getEntries()); + + ASSERT_EQ(5, entriesWithoutMark1.size()); + ASSERT_EQ( + std::vector( + {{.name = "measure1", + .entryType = PerformanceEntryType::MEASURE, + .startTime = 0, + .duration = 4}, + {.name = "measure2", + .entryType = PerformanceEntryType::MEASURE, + .startTime = 1, + .duration = 1}, + {.name = "measure3", + .entryType = PerformanceEntryType::MEASURE, + .startTime = 1, + .duration = 5}, + {.name = "measure4", + .entryType = PerformanceEntryType::MEASURE, + .startTime = 1.5, + .duration = 0.5}, + {.name = "mark2", + .entryType = PerformanceEntryType::MARK, + .startTime = 2, + .duration = 0}}), + entriesWithoutMark1); + } + + { + reporter->clearEntries(PerformanceEntryType::MEASURE); + auto entriesWithoutMeasures = toSorted(reporter->getEntries()); + + ASSERT_EQ(1, entriesWithoutMeasures.size()); + ASSERT_EQ( + std::vector( + {{.name = "mark2", + .entryType = PerformanceEntryType::MARK, + .startTime = 2, + .duration = 0}}), + entriesWithoutMeasures); + } + + { + reporter->clearEntries(); + auto emptyEntries = reporter->getEntries(); + + ASSERT_EQ(0, emptyEntries.size()); + } } diff --git a/packages/react-native/ReactCommon/react/performance/timeline/tests/PerformanceObserverTest.cpp b/packages/react-native/ReactCommon/react/performance/timeline/tests/PerformanceObserverTest.cpp new file mode 100644 index 00000000000000..8b31a27519f31c --- /dev/null +++ b/packages/react-native/ReactCommon/react/performance/timeline/tests/PerformanceObserverTest.cpp @@ -0,0 +1,272 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include + +#include "../PerformanceEntryReporter.h" +#include "../PerformanceObserver.h" + +namespace facebook::react { + +[[maybe_unused]] static bool operator==( + const PerformanceEntry& lhs, + const PerformanceEntry& rhs) { + return lhs.name == rhs.name && lhs.entryType == rhs.entryType && + lhs.startTime == rhs.startTime && lhs.duration == rhs.duration && + lhs.processingStart == rhs.processingStart && + lhs.processingEnd == rhs.processingEnd && + lhs.interactionId == rhs.interactionId; +} +} // namespace facebook::react + +using namespace facebook::react; + +TEST(PerformanceObserver, PerformanceObserverTestObserveFlushes) { + auto reporter = PerformanceEntryReporter::getInstance(); + reporter->clearEntries(); + + bool callbackCalled = false; + auto observer = PerformanceObserver::create( + reporter->getObserverRegistry(), [&]() { callbackCalled = true; }); + observer->observe(PerformanceEntryType::MARK); + + // buffer is empty + ASSERT_FALSE(callbackCalled); + + reporter->mark("test", 10); + ASSERT_TRUE(callbackCalled); + + observer->disconnect(); +} + +TEST(PerformanceObserver, PerformanceObserverTestFilteredSingle) { + auto reporter = PerformanceEntryReporter::getInstance(); + reporter->clearEntries(); + + auto observer = + PerformanceObserver::create(reporter->getObserverRegistry(), [&]() {}); + observer->observe(PerformanceEntryType::MEASURE); + reporter->mark("test", 10); + + // wrong type + ASSERT_EQ(observer->takeRecords().size(), 0); + + observer->disconnect(); +} + +TEST(PerformanceObserver, PerformanceObserverTestFilterMulti) { + auto reporter = PerformanceEntryReporter::getInstance(); + reporter->clearEntries(); + + auto callbackCalled = false; + auto observer = PerformanceObserver::create( + reporter->getObserverRegistry(), [&]() { callbackCalled = true; }); + observer->observe( + {PerformanceEntryType::MEASURE, PerformanceEntryType::MARK}); + + reporter->logEventEntry("test1", 10, 10, 0, 0, 0); + reporter->logEventEntry("test2", 10, 10, 0, 0, 0); + reporter->logEventEntry("test3", 10, 10, 0, 0, 0); + + ASSERT_EQ(observer->takeRecords().size(), 0); + ASSERT_FALSE(callbackCalled); + + observer->disconnect(); +} + +TEST( + PerformanceObserver, + PerformanceObserverTestFilterSingleCallbackNotCalled) { + auto reporter = PerformanceEntryReporter::getInstance(); + reporter->clearEntries(); + + auto callbackCalled = false; + auto observer = PerformanceObserver::create( + reporter->getObserverRegistry(), [&]() { callbackCalled = true; }); + observer->observe(PerformanceEntryType::MEASURE); + reporter->mark("test", 10); + + ASSERT_FALSE(callbackCalled); + + observer->disconnect(); +} + +TEST(PerformanceObserver, PerformanceObserverTestFilterMultiCallbackNotCalled) { + auto reporter = PerformanceEntryReporter::getInstance(); + reporter->clearEntries(); + + auto callbackCalled = false; + auto observer = PerformanceObserver::create( + reporter->getObserverRegistry(), [&]() { callbackCalled = true; }); + observer->observe( + {PerformanceEntryType::MEASURE, PerformanceEntryType::MARK}); + reporter->logEventEntry("test1", 10, 10, 0, 0, 0); + reporter->logEventEntry("test2", 10, 10, 0, 0, 0); + reporter->logEventEntry("off3", 10, 10, 0, 0, 0); + + ASSERT_FALSE(callbackCalled); + + observer->disconnect(); +} + +TEST(PerformanceObserver, PerformanceObserverTestObserveTakeRecords) { + auto reporter = PerformanceEntryReporter::getInstance(); + reporter->clearEntries(); + + auto observer = + PerformanceObserver::create(reporter->getObserverRegistry(), [&]() {}); + observer->observe(PerformanceEntryType::MARK); + reporter->mark("test1", 10); + reporter->measure("off", 10, 20); + reporter->mark("test2", 20); + reporter->mark("test3", 30); + + const std::vector expected = { + {.name = "test1", + .entryType = PerformanceEntryType::MARK, + .startTime = 10}, + {.name = "test2", + .entryType = PerformanceEntryType::MARK, + .startTime = 20}, + {.name = "test3", + .entryType = PerformanceEntryType::MARK, + .startTime = 30}, + }; + + ASSERT_EQ(expected, observer->takeRecords()); + + observer->disconnect(); +} + +TEST(PerformanceObserver, PerformanceObserverTestObserveDurationThreshold) { + auto reporter = PerformanceEntryReporter::getInstance(); + reporter->clearEntries(); + + auto observer = + PerformanceObserver::create(reporter->getObserverRegistry(), [&]() {}); + observer->observe(PerformanceEntryType::EVENT, {.durationThreshold = 50}); + reporter->logEventEntry("test1", 0, 50, 0, 0, 0); + reporter->logEventEntry("test2", 0, 100, 0, 0, 0); + reporter->logEventEntry("off1", 0, 40, 0, 0, 0); + reporter->mark("off2", 100); + reporter->logEventEntry("test3", 0, 60, 0, 0, 0); + + const std::vector expected = { + {.name = "test1", + .entryType = PerformanceEntryType::EVENT, + .duration = 50, + .processingStart = 0, + .processingEnd = 0, + .interactionId = 0}, + {.name = "test2", + .entryType = PerformanceEntryType::EVENT, + .duration = 100, + .processingStart = 0, + .processingEnd = 0, + .interactionId = 0}, + {.name = "test3", + .entryType = PerformanceEntryType::EVENT, + .duration = 60, + .processingStart = 0, + .processingEnd = 0, + .interactionId = 0}, + }; + + ASSERT_EQ(expected, observer->takeRecords()); + + observer->disconnect(); +} + +TEST(PerformanceObserver, PerformanceObserverTestObserveBuffered) { + auto reporter = PerformanceEntryReporter::getInstance(); + reporter->clearEntries(); + + reporter->logEventEntry("test1", 0, 50, 0, 0, 0); + reporter->logEventEntry("test2", 0, 100, 0, 0, 0); + reporter->logEventEntry("test3", 0, 40, 0, 0, 0); + reporter->logEventEntry("test4", 0, 100, 0, 0, 0); + + auto observer = + PerformanceObserver::create(reporter->getObserverRegistry(), [&]() {}); + observer->observe( + PerformanceEntryType::EVENT, {.buffered = true, .durationThreshold = 50}); + + const std::vector expected = { + {.name = "test1", + .entryType = PerformanceEntryType::EVENT, + .startTime = 0, + .duration = 50, + .processingStart = 0, + .processingEnd = 0, + .interactionId = 0}, + {.name = "test2", + .entryType = PerformanceEntryType::EVENT, + .startTime = 0, + .duration = 100, + .processingStart = 0, + .processingEnd = 0, + .interactionId = 0}, + {.name = "test4", + .entryType = PerformanceEntryType::EVENT, + .startTime = 0, + .duration = 100, + .processingStart = 0, + .processingEnd = 0, + .interactionId = 0}}; + + ASSERT_EQ(expected, observer->takeRecords()); + + observer->disconnect(); +} + +TEST(PerformanceObserver, PerformanceObserverTestMultiple) { + auto reporter = PerformanceEntryReporter::getInstance(); + reporter->clearEntries(); + + auto observer1 = + PerformanceObserver::create(reporter->getObserverRegistry(), [&]() {}); + auto observer2 = + PerformanceObserver::create(reporter->getObserverRegistry(), [&]() {}); + observer1->observe(PerformanceEntryType::EVENT, {.durationThreshold = 50}); + observer2->observe(PerformanceEntryType::EVENT, {.durationThreshold = 80}); + + reporter->measure("measure", 0, 50); + reporter->logEventEntry("event1", 0, 100, 0, 0, 0); + reporter->logEventEntry("event2", 0, 40, 0, 0, 0); + reporter->mark("mark1", 100); + reporter->logEventEntry("event3", 0, 60, 0, 0, 0); + + const std::vector expected1 = { + {.name = "event1", + .entryType = PerformanceEntryType::EVENT, + .duration = 100, + .processingStart = 0, + .processingEnd = 0, + .interactionId = 0}, + {.name = "event3", + .entryType = PerformanceEntryType::EVENT, + .duration = 60, + .processingStart = 0, + .processingEnd = 0, + .interactionId = 0}, + }; + + const std::vector expected2 = { + {.name = "event1", + .entryType = PerformanceEntryType::EVENT, + .duration = 100, + .processingStart = 0, + .processingEnd = 0, + .interactionId = 0}}; + + ASSERT_EQ(expected1, observer1->takeRecords()); + ASSERT_EQ(expected2, observer2->takeRecords()); + + observer1->disconnect(); + observer2->disconnect(); +} diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/RuntimeSchedulerTest.cpp b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/RuntimeSchedulerTest.cpp index d8a04d17c0e1fa..b29907d0e97d4f 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/RuntimeSchedulerTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/RuntimeSchedulerTest.cpp @@ -84,8 +84,6 @@ class RuntimeSchedulerTest : public testing::TestWithParam { performanceEntryReporter_ = PerformanceEntryReporter::getInstance().get(); - performanceEntryReporter_->startReporting(PerformanceEntryType::LONGTASK); - runtimeScheduler_ = std::make_unique(runtimeExecutor, stubNow); @@ -94,7 +92,7 @@ class RuntimeSchedulerTest : public testing::TestWithParam { void TearDown() override { ReactNativeFeatureFlags::dangerouslyReset(); - performanceEntryReporter_->popPendingEntries(); + performanceEntryReporter_->clearEntries(); } jsi::Function createHostFunctionFromLambda( @@ -1244,8 +1242,8 @@ TEST_P(RuntimeSchedulerTest, reportsLongTasks) { EXPECT_EQ(didRunTask1, 1); EXPECT_EQ(stubQueue_->size(), 0); - auto pendingEntries = performanceEntryReporter_->popPendingEntries(); - EXPECT_EQ(pendingEntries.entries.size(), 0); + auto pendingEntries = performanceEntryReporter_->getEntries(); + EXPECT_EQ(pendingEntries.size(), 0); bool didRunTask2 = false; stubClock_->setTimePoint(100ms); @@ -1265,12 +1263,11 @@ TEST_P(RuntimeSchedulerTest, reportsLongTasks) { EXPECT_EQ(didRunTask2, 1); EXPECT_EQ(stubQueue_->size(), 0); - pendingEntries = performanceEntryReporter_->popPendingEntries(); - EXPECT_EQ(pendingEntries.entries.size(), 1); - EXPECT_EQ( - pendingEntries.entries[0].entryType, PerformanceEntryType::LONGTASK); - EXPECT_EQ(pendingEntries.entries[0].startTime, 100); - EXPECT_EQ(pendingEntries.entries[0].duration, 50); + pendingEntries = performanceEntryReporter_->getEntries(); + EXPECT_EQ(pendingEntries.size(), 1); + EXPECT_EQ(pendingEntries[0].entryType, PerformanceEntryType::LONGTASK); + EXPECT_EQ(pendingEntries[0].startTime, 100); + EXPECT_EQ(pendingEntries[0].duration, 50); } TEST_P(RuntimeSchedulerTest, reportsLongTasksWithYielding) { @@ -1311,8 +1308,8 @@ TEST_P(RuntimeSchedulerTest, reportsLongTasksWithYielding) { EXPECT_EQ(didRunTask1, 1); EXPECT_EQ(stubQueue_->size(), 0); - auto pendingEntries = performanceEntryReporter_->popPendingEntries(); - EXPECT_EQ(pendingEntries.entries.size(), 0); + auto pendingEntries = performanceEntryReporter_->getEntries(); + EXPECT_EQ(pendingEntries.size(), 0); bool didRunTask2 = false; stubClock_->setTimePoint(100ms); @@ -1346,12 +1343,11 @@ TEST_P(RuntimeSchedulerTest, reportsLongTasksWithYielding) { EXPECT_EQ(didRunTask2, 1); EXPECT_EQ(stubQueue_->size(), 0); - pendingEntries = performanceEntryReporter_->popPendingEntries(); - EXPECT_EQ(pendingEntries.entries.size(), 1); - EXPECT_EQ( - pendingEntries.entries[0].entryType, PerformanceEntryType::LONGTASK); - EXPECT_EQ(pendingEntries.entries[0].startTime, 100); - EXPECT_EQ(pendingEntries.entries[0].duration, 120); + pendingEntries = performanceEntryReporter_->getEntries(); + EXPECT_EQ(pendingEntries.size(), 1); + EXPECT_EQ(pendingEntries[0].entryType, PerformanceEntryType::LONGTASK); + EXPECT_EQ(pendingEntries[0].startTime, 100); + EXPECT_EQ(pendingEntries[0].duration, 120); } INSTANTIATE_TEST_SUITE_P( diff --git a/packages/react-native/src/private/webapis/performance/EventTiming.js b/packages/react-native/src/private/webapis/performance/EventTiming.js index 110689a0601187..1a485f2bd25ea2 100644 --- a/packages/react-native/src/private/webapis/performance/EventTiming.js +++ b/packages/react-native/src/private/webapis/performance/EventTiming.js @@ -16,7 +16,7 @@ import type { } from './PerformanceEntry'; import {PerformanceEntry} from './PerformanceEntry'; -import {warnNoNativePerformanceObserver} from './PerformanceObserver'; +import {warnNoNativePerformanceObserver} from './Utilities'; import NativePerformanceObserver from './specs/NativePerformanceObserver'; export type PerformanceEventTimingJSON = { diff --git a/packages/react-native/src/private/webapis/performance/Performance.js b/packages/react-native/src/private/webapis/performance/Performance.js index 5861d3582803b8..8546493c222f64 100644 --- a/packages/react-native/src/private/webapis/performance/Performance.js +++ b/packages/react-native/src/private/webapis/performance/Performance.js @@ -17,11 +17,10 @@ import type { import type {PerformanceEntryList} from './PerformanceObserver'; import type {DetailType, PerformanceMarkOptions} from './UserTiming'; -import warnOnce from '../../../../Libraries/Utilities/warnOnce'; import {EventCounts} from './EventTiming'; import MemoryInfo from './MemoryInfo'; import {ALWAYS_LOGGED_ENTRY_TYPES} from './PerformanceEntry'; -import {warnNoNativePerformanceObserver} from './PerformanceObserver'; +import {warnNoNativePerformanceObserver} from './Utilities'; import { performanceEntryTypeToRaw, rawToPerformanceEntry, @@ -31,6 +30,7 @@ import ReactNativeStartupTiming from './ReactNativeStartupTiming'; import NativePerformance from './specs/NativePerformance'; import NativePerformanceObserver from './specs/NativePerformanceObserver'; import {PerformanceMark, PerformanceMeasure} from './UserTiming'; +import {warnNoNativePerformance} from './Utilities'; declare var global: { // This value is defined directly via JSI, if available. @@ -40,24 +40,6 @@ declare var global: { const getCurrentTimeStamp: () => DOMHighResTimeStamp = NativePerformance?.now ?? global.nativePerformanceNow ?? (() => Date.now()); -// We want some of the performance entry types to be always logged, -// even if they are not currently observed - this is either to be able to -// retrieve them at any time via Performance.getEntries* or to refer by other entries -// (such as when measures may refer to marks, even if the latter are not observed) -if (NativePerformanceObserver?.setIsBuffered) { - NativePerformanceObserver?.setIsBuffered( - ALWAYS_LOGGED_ENTRY_TYPES.map(performanceEntryTypeToRaw), - true, - ); -} - -function warnNoNativePerformance() { - warnOnce( - 'missing-native-performance', - 'Missing native implementation of Performance', - ); -} - export type PerformanceMeasureOptions = { detail?: DetailType, start?: DOMHighResTimeStamp, @@ -145,7 +127,7 @@ export default class Performance { return; } - NativePerformanceObserver?.clearEntries( + NativePerformanceObserver.clearEntries( RawPerformanceEntryTypeValues.MARK, markName, ); diff --git a/packages/react-native/src/private/webapis/performance/PerformanceObserver.js b/packages/react-native/src/private/webapis/performance/PerformanceObserver.js index 4f1d90f080ffa0..e0cd7ebf53f151 100644 --- a/packages/react-native/src/private/webapis/performance/PerformanceObserver.js +++ b/packages/react-native/src/private/webapis/performance/PerformanceObserver.js @@ -13,7 +13,6 @@ import type { PerformanceEntryType, } from './PerformanceEntry'; -import warnOnce from '../../../../Libraries/Utilities/warnOnce'; import {PerformanceEventTiming} from './EventTiming'; import {PerformanceEntry} from './PerformanceEntry'; import { @@ -22,6 +21,8 @@ import { rawToPerformanceEntryType, } from './RawPerformanceEntry'; import NativePerformanceObserver from './specs/NativePerformanceObserver'; +import type {OpaqueNativeObserverHandle} from './specs/NativePerformanceObserver'; +import {warnNoNativePerformanceObserver} from './Utilities'; export type PerformanceEntryList = $ReadOnlyArray; @@ -56,89 +57,24 @@ export class PerformanceObserverEntryList { } } +export type PerformanceObserverCallbackOptions = { + droppedEntriesCount: number, +}; + export type PerformanceObserverCallback = ( list: PerformanceObserverEntryList, observer: PerformanceObserver, // The number of buffered entries which got dropped from the buffer due to the buffer being full: - droppedEntryCount?: number, + options?: PerformanceObserverCallbackOptions, ) => void; -export type PerformanceObserverInit = - | { - entryTypes: Array, - } - | { - type: PerformanceEntryType, - durationThreshold?: DOMHighResTimeStamp, - }; - -type PerformanceObserverConfig = {| - callback: PerformanceObserverCallback, - entryTypes: $ReadOnlySet, - durationThreshold: ?number, -|}; - -const observerCountPerEntryType: Map = new Map(); -const registeredObservers: Map = - new Map(); -let isOnPerformanceEntryCallbackSet: boolean = false; - -// This is a callback that gets scheduled and periodically called from the native side -const onPerformanceEntry = () => { - if (!NativePerformanceObserver) { - return; - } - const entryResult = NativePerformanceObserver.popPendingEntries(); - const rawEntries = entryResult?.entries ?? []; - const droppedEntriesCount = entryResult?.droppedEntriesCount; - if (rawEntries.length === 0) { - return; - } - const entries = rawEntries.map(rawToPerformanceEntry); - for (const [observer, observerConfig] of registeredObservers.entries()) { - const entriesForObserver: PerformanceEntryList = entries.filter(entry => { - if (!observerConfig.entryTypes.has(entry.entryType)) { - return false; - } - - if ( - entry.entryType === 'event' && - observerConfig.durationThreshold != null - ) { - return entry.duration >= observerConfig.durationThreshold; - } - - return true; - }); - if (entriesForObserver.length !== 0) { - try { - observerConfig.callback( - new PerformanceObserverEntryList(entriesForObserver), - observer, - droppedEntriesCount, - ); - } catch (error) { - console.error(error); - } - } - } +export type PerformanceObserverInit = { + entryTypes?: Array, + type?: PerformanceEntryType, + buffered?: boolean, + durationThreshold?: DOMHighResTimeStamp, }; -export function warnNoNativePerformanceObserver() { - warnOnce( - 'missing-native-performance-observer', - 'Missing native implementation of PerformanceObserver', - ); -} - -function applyDurationThresholds() { - const durationThresholds = Array.from(registeredObservers.values()) - .map(observerConfig => observerConfig.durationThreshold) - .filter(Boolean); - - return Math.min(...durationThresholds); -} - function getSupportedPerformanceEntryTypes(): $ReadOnlyArray { if (!NativePerformanceObserver) { return Object.freeze([]); @@ -175,111 +111,96 @@ function getSupportedPerformanceEntryTypes(): $ReadOnlyArray { + // $FlowNotNull + const rawEntries = NativePerformanceObserver.takeRecords?.( + this.#nativeObserverHandle, + true, // sort records + ); + if (!rawEntries) { + return; } - } - // Disconnect all observers if this was the last one - registeredObservers.delete(this); - if (registeredObservers.size === 0) { - NativePerformanceObserver.setOnPerformanceEntryCallback(undefined); - isOnPerformanceEntryCallbackSet = false; - } + const entries = rawEntries.map(rawToPerformanceEntry); + const entryList = new PerformanceObserverEntryList(entries); + + let droppedEntriesCount = 0; + if (!this.#calledAtLeastOnce) { + droppedEntriesCount = + NativePerformanceObserver.getDroppedEntriesCount?.( + this.#nativeObserverHandle, + ) ?? 0; + this.#calledAtLeastOnce = true; + } - applyDurationThresholds(); + this.#callback(entryList, this, {droppedEntriesCount}); + }); } #validateObserveOptions(options: PerformanceObserverInit): void { @@ -309,7 +230,7 @@ export class PerformanceObserver { ); } - if (entryTypes && durationThreshold !== undefined) { + if (entryTypes && durationThreshold != null) { throw new TypeError( "Failed to execute 'observe' on 'PerformanceObserver': An observe() call must not include both entryTypes and durationThreshold arguments.", ); @@ -320,12 +241,4 @@ export class PerformanceObserver { getSupportedPerformanceEntryTypes(); } -function union(a: $ReadOnlySet, b: $ReadOnlySet): Set { - return new Set([...a, ...b]); -} - -function difference(a: $ReadOnlySet, b: $ReadOnlySet): Set { - return new Set([...a].filter(x => !b.has(x))); -} - export {PerformanceEventTiming}; diff --git a/packages/react-native/src/private/webapis/performance/Utilities.js b/packages/react-native/src/private/webapis/performance/Utilities.js new file mode 100644 index 00000000000000..cf4632e99335cb --- /dev/null +++ b/packages/react-native/src/private/webapis/performance/Utilities.js @@ -0,0 +1,25 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict + */ + +import warnOnce from '../../../../Libraries/Utilities/warnOnce'; + +export function warnNoNativePerformance() { + warnOnce( + 'missing-native-performance', + 'Missing native implementation of Performance', + ); +} + +export function warnNoNativePerformanceObserver() { + warnOnce( + 'missing-native-performance-observer', + 'Missing native implementation of PerformanceObserver', + ); +} diff --git a/packages/react-native/src/private/webapis/performance/__tests__/EventCounts-test.js b/packages/react-native/src/private/webapis/performance/__tests__/EventCounts-test.js index 3e7a951686600d..33f1cfe2bf1261 100644 --- a/packages/react-native/src/private/webapis/performance/__tests__/EventCounts-test.js +++ b/packages/react-native/src/private/webapis/performance/__tests__/EventCounts-test.js @@ -11,17 +11,22 @@ import {RawPerformanceEntryTypeValues} from '../RawPerformanceEntry'; -// NOTE: Jest mocks of transitive dependencies don't appear to work with -// ES6 module imports, therefore forced to use commonjs style imports here. -const Performance = require('../Performance').default; -const NativePerformanceObserverMock = - require('../specs/__mocks__/NativePerformanceObserver').default; +jest.mock( + '../specs/NativePerformance', + () => require('../specs/__mocks__/NativePerformance').default, +); jest.mock( '../specs/NativePerformanceObserver', () => require('../specs/__mocks__/NativePerformanceObserver').default, ); +// NOTE: Jest mocks of transitive dependencies don't appear to work with +// ES6 module imports, therefore forced to use commonjs style imports here. +const Performance = require('../Performance').default; +const logMockEntry = + require('../specs/__mocks__/NativePerformanceObserver').logMockEntry; + describe('EventCounts', () => { it('defines EventCounts for Performance', () => { const eventCounts = new Performance().eventCounts; @@ -29,40 +34,44 @@ describe('EventCounts', () => { }); it('consistently implements the API for EventCounts', async () => { + let interactionId = 0; const eventDefaultValues = { entryType: RawPerformanceEntryTypeValues.EVENT, startTime: 0, duration: 100, + processingStart: 0, + processingEnd: 100, }; - NativePerformanceObserverMock.logRawEntry({ + logMockEntry({ name: 'click', ...eventDefaultValues, + interactionId: interactionId++, }); - - NativePerformanceObserverMock.logRawEntry({ + logMockEntry({ name: 'input', ...eventDefaultValues, + interactionId: interactionId++, }); - - NativePerformanceObserverMock.logRawEntry({ + logMockEntry({ name: 'input', ...eventDefaultValues, + interactionId: interactionId++, }); - - NativePerformanceObserverMock.logRawEntry({ + logMockEntry({ name: 'keyup', ...eventDefaultValues, + interactionId: interactionId++, }); - - NativePerformanceObserverMock.logRawEntry({ + logMockEntry({ name: 'keyup', ...eventDefaultValues, + interactionId: interactionId++, }); - - NativePerformanceObserverMock.logRawEntry({ + logMockEntry({ name: 'keyup', ...eventDefaultValues, + interactionId: interactionId++, }); const eventCounts = new Performance().eventCounts; @@ -89,34 +98,36 @@ describe('EventCounts', () => { expect(Array.from(eventCounts.values())).toStrictEqual([1, 2, 3]); await jest.runAllTicks(); - NativePerformanceObserverMock.logRawEntry({ + logMockEntry({ name: 'input', ...eventDefaultValues, + interactionId: interactionId++, }); - - NativePerformanceObserverMock.logRawEntry({ + logMockEntry({ name: 'keyup', ...eventDefaultValues, + interactionId: interactionId++, }); - - NativePerformanceObserverMock.logRawEntry({ + logMockEntry({ name: 'keyup', ...eventDefaultValues, + interactionId: interactionId++, }); expect(Array.from(eventCounts.values())).toStrictEqual([1, 3, 5]); await jest.runAllTicks(); - - NativePerformanceObserverMock.logRawEntry({ + logMockEntry({ name: 'click', ...eventDefaultValues, + interactionId: interactionId++, }); await jest.runAllTicks(); - NativePerformanceObserverMock.logRawEntry({ + logMockEntry({ name: 'keyup', ...eventDefaultValues, + interactionId: interactionId++, }); expect(Array.from(eventCounts.values())).toStrictEqual([2, 3, 6]); diff --git a/packages/react-native/src/private/webapis/performance/__tests__/PerformanceObserver-test.js b/packages/react-native/src/private/webapis/performance/__tests__/PerformanceObserver-test.js index 1c4bc1d60d992a..4c26bc0ca0431b 100644 --- a/packages/react-native/src/private/webapis/performance/__tests__/PerformanceObserver-test.js +++ b/packages/react-native/src/private/webapis/performance/__tests__/PerformanceObserver-test.js @@ -11,50 +11,23 @@ import type {PerformanceEntryList} from '../PerformanceObserver'; -import {RawPerformanceEntryTypeValues} from '../RawPerformanceEntry'; - -// NOTE: Jest mocks of transitive dependencies don't appear to work with -// ES6 module imports, therefore forced to use commonjs style imports here. -const {PerformanceObserver} = require('../PerformanceObserver'); -const NativePerformanceObserverMock = - require('../specs/__mocks__/NativePerformanceObserver').default; -const NativePerformanceObserver = require('../specs/NativePerformanceObserver'); +jest.mock( + '../specs/NativePerformance', + () => require('../specs/__mocks__/NativePerformance').default, +); jest.mock( '../specs/NativePerformanceObserver', () => require('../specs/__mocks__/NativePerformanceObserver').default, ); -describe('PerformanceObserver', () => { - it('can be mocked by a reference NativePerformanceObserver implementation', async () => { - expect(NativePerformanceObserver).not.toBe(undefined); - - let totalEntries = 0; - const observer: PerformanceObserver = new PerformanceObserver( - (list, _observer) => { - expect(_observer).toBe(observer); - const entries = list.getEntries(); - expect(entries).toHaveLength(1); - const entry = entries[0]; - expect(entry.name).toBe('mark1'); - expect(entry.entryType).toBe('mark'); - totalEntries += entries.length; - }, - ); - expect(() => observer.observe({entryTypes: ['mark']})).not.toThrow(); - - NativePerformanceObserverMock.logRawEntry({ - name: 'mark1', - entryType: RawPerformanceEntryTypeValues.MARK, - startTime: 0, - duration: 0, - }); - - await jest.runAllTicks(); - expect(totalEntries).toBe(1); - observer.disconnect(); - }); +// // NOTE: Jest mocks of transitive dependencies don't appear to work with +// // ES6 module imports, therefore forced to use commonjs style imports here. +const {PerformanceObserver} = require('../PerformanceObserver'); +const NativePerformanceMock = + require('../specs/__mocks__/NativePerformance').default; +describe('PerformanceObserver', () => { it('prevents durationThreshold to be used together with entryTypes', async () => { const observer = new PerformanceObserver((list, _observer) => {}); @@ -73,223 +46,10 @@ describe('PerformanceObserver', () => { observer.observe({type: 'measure', durationThreshold: 100}); - NativePerformanceObserverMock.logRawEntry({ - name: 'measure1', - entryType: RawPerformanceEntryTypeValues.MEASURE, - startTime: 0, - duration: 200, - }); + NativePerformanceMock?.measure('measure1', 0, 200); await jest.runAllTicks(); expect(entries).toHaveLength(1); expect(entries.map(e => e.name)).toStrictEqual(['measure1']); }); - - it('handles durationThreshold argument as expected', async () => { - let entries: PerformanceEntryList = []; - const observer = new PerformanceObserver((list, _observer) => { - entries = [...entries, ...list.getEntries()]; - }); - - observer.observe({type: 'event', durationThreshold: 100}); - - NativePerformanceObserverMock.logRawEntry({ - name: 'event1', - entryType: RawPerformanceEntryTypeValues.EVENT, - startTime: 0, - duration: 200, - }); - - NativePerformanceObserverMock.logRawEntry({ - name: 'event2', - entryType: RawPerformanceEntryTypeValues.EVENT, - startTime: 0, - duration: 20, - }); - - NativePerformanceObserverMock.logRawEntry({ - name: 'event3', - entryType: RawPerformanceEntryTypeValues.EVENT, - startTime: 0, - duration: 100, - }); - - NativePerformanceObserverMock.logRawEntry({ - name: 'event4', - entryType: RawPerformanceEntryTypeValues.EVENT, - startTime: 0, - duration: 500, - }); - - await jest.runAllTicks(); - expect(entries).toHaveLength(3); - expect(entries.map(e => e.name)).toStrictEqual([ - 'event1', - 'event3', - 'event4', - ]); - }); - - it('correctly works with multiple PerformanceObservers with durationThreshold', async () => { - let entries1: PerformanceEntryList = []; - const observer1 = new PerformanceObserver((list, _observer) => { - entries1 = [...entries1, ...list.getEntries()]; - }); - - let entries2: PerformanceEntryList = []; - const observer2 = new PerformanceObserver((list, _observer) => { - entries2 = [...entries2, ...list.getEntries()]; - }); - - let entries3: PerformanceEntryList = []; - const observer3 = new PerformanceObserver((list, _observer) => { - entries3 = [...entries3, ...list.getEntries()]; - }); - - let entries4: PerformanceEntryList = []; - const observer4 = new PerformanceObserver((list, _observer) => { - entries4 = [...entries4, ...list.getEntries()]; - }); - - observer2.observe({type: 'event', durationThreshold: 200}); - observer1.observe({type: 'event', durationThreshold: 100}); - observer3.observe({type: 'event', durationThreshold: 300}); - observer3.observe({type: 'event', durationThreshold: 500}); - observer4.observe({entryTypes: ['event']}); - - NativePerformanceObserverMock.logRawEntry({ - name: 'event1', - entryType: RawPerformanceEntryTypeValues.EVENT, - startTime: 0, - duration: 200, - }); - - NativePerformanceObserverMock.logRawEntry({ - name: 'event2', - entryType: RawPerformanceEntryTypeValues.EVENT, - startTime: 0, - duration: 20, - }); - - NativePerformanceObserverMock.logRawEntry({ - name: 'event3', - entryType: RawPerformanceEntryTypeValues.EVENT, - startTime: 0, - duration: 100, - }); - - NativePerformanceObserverMock.logRawEntry({ - name: 'event4', - entryType: RawPerformanceEntryTypeValues.EVENT, - startTime: 0, - duration: 500, - }); - - await jest.runAllTicks(); - observer1.disconnect(); - - NativePerformanceObserverMock.logRawEntry({ - name: 'event5', - entryType: RawPerformanceEntryTypeValues.EVENT, - startTime: 0, - duration: 200, - }); - - NativePerformanceObserverMock.logRawEntry({ - name: 'event6', - entryType: RawPerformanceEntryTypeValues.EVENT, - startTime: 0, - duration: 300, - }); - - await jest.runAllTicks(); - observer3.disconnect(); - - NativePerformanceObserverMock.logRawEntry({ - name: 'event7', - entryType: RawPerformanceEntryTypeValues.EVENT, - startTime: 0, - duration: 200, - }); - - await jest.runAllTicks(); - observer4.disconnect(); - - expect(entries1.map(e => e.name)).toStrictEqual([ - 'event1', - 'event3', - 'event4', - ]); - expect(entries2.map(e => e.name)).toStrictEqual([ - 'event1', - 'event4', - 'event5', - 'event6', - 'event7', - ]); - expect(entries3.map(e => e.name)).toStrictEqual(['event4', 'event6']); - expect(entries4.map(e => e.name)).toStrictEqual([ - 'event1', - 'event2', - 'event3', - 'event4', - 'event5', - 'event6', - 'event7', - ]); - }); - - it('should guard against errors in observer callbacks', () => { - jest.spyOn(console, 'error').mockImplementation(() => {}); - - const observer1Callback = jest.fn((_entries, _observer, _options) => { - throw new Error('observer 1 callback'); - }); - const observer1 = new PerformanceObserver(observer1Callback); - - const observer2Callback = jest.fn(); - const observer2 = new PerformanceObserver(observer2Callback); - - observer1.observe({type: 'mark'}); - observer2.observe({type: 'mark'}); - - NativePerformanceObserverMock.logRawEntry({ - name: 'mark1', - entryType: RawPerformanceEntryTypeValues.MARK, - startTime: 0, - duration: 200, - }); - - jest.runAllTicks(); - - expect(observer1Callback).toHaveBeenCalled(); - expect(observer2Callback).toHaveBeenCalled(); - - expect(console.error).toHaveBeenCalledWith( - new Error('observer 1 callback'), - ); - }); - - it('should not invoke observers with non-matching entries', () => { - const observer1Callback = jest.fn(); - const observer1 = new PerformanceObserver(observer1Callback); - - const observer2Callback = jest.fn(); - const observer2 = new PerformanceObserver(observer2Callback); - - observer1.observe({type: 'mark'}); - observer2.observe({type: 'measure'}); - - NativePerformanceObserverMock.logRawEntry({ - name: 'mark1', - entryType: RawPerformanceEntryTypeValues.MARK, - startTime: 0, - duration: 200, - }); - - jest.runAllTicks(); - - expect(observer1Callback).toHaveBeenCalled(); - expect(observer2Callback).not.toHaveBeenCalled(); - }); }); diff --git a/packages/react-native/src/private/webapis/performance/specs/NativePerformanceObserver.js b/packages/react-native/src/private/webapis/performance/specs/NativePerformanceObserver.js index 503d4d812690b8..f06cacd104a34c 100644 --- a/packages/react-native/src/private/webapis/performance/specs/NativePerformanceObserver.js +++ b/packages/react-native/src/private/webapis/performance/specs/NativePerformanceObserver.js @@ -14,6 +14,10 @@ import * as TurboModuleRegistry from '../../../../../Libraries/TurboModule/Turbo export type RawPerformanceEntryType = number; +export type OpaqueNativeObserverHandle = mixed; + +export type NativeBatchedObserverCallback = () => void; + export type RawPerformanceEntry = {| name: string, entryType: RawPerformanceEntryType, @@ -25,26 +29,30 @@ export type RawPerformanceEntry = {| interactionId?: number, |}; -export type GetPendingEntriesResult = {| - entries: $ReadOnlyArray, - droppedEntriesCount: number, -|}; +export type PerformanceObserverInit = { + entryTypes?: $ReadOnlyArray, + type?: number, + buffered?: boolean, + durationThreshold?: number, +}; export interface Spec extends TurboModule { - +startReporting: (entryType: RawPerformanceEntryType) => void; - +stopReporting: (entryType: RawPerformanceEntryType) => void; - +setIsBuffered: ( - entryTypes: $ReadOnlyArray, - isBuffered: boolean, - ) => void; - +popPendingEntries: () => GetPendingEntriesResult; - +setOnPerformanceEntryCallback: (callback?: () => void) => void; - +logRawEntry: (entry: RawPerformanceEntry) => void; +getEventCounts: () => $ReadOnlyArray<[string, number]>; - +setDurationThreshold: ( - entryType: RawPerformanceEntryType, - durationThreshold: number, + +createObserver?: ( + callback: NativeBatchedObserverCallback, + ) => OpaqueNativeObserverHandle; + +getDroppedEntriesCount?: (observer: OpaqueNativeObserverHandle) => number; + + +observe?: ( + observer: OpaqueNativeObserverHandle, + options: PerformanceObserverInit, ) => void; + +disconnect?: (observer: OpaqueNativeObserverHandle) => void; + +takeRecords?: ( + observer: OpaqueNativeObserverHandle, + sort: boolean, + ) => $ReadOnlyArray; + +clearEntries: ( entryType?: RawPerformanceEntryType, entryName?: string, diff --git a/packages/react-native/src/private/webapis/performance/specs/__mocks__/NativePerformance.js b/packages/react-native/src/private/webapis/performance/specs/__mocks__/NativePerformance.js index b1e7072584fbf8..3389da339be986 100644 --- a/packages/react-native/src/private/webapis/performance/specs/__mocks__/NativePerformance.js +++ b/packages/react-native/src/private/webapis/performance/specs/__mocks__/NativePerformance.js @@ -11,23 +11,24 @@ import type { NativeMemoryInfo, ReactNativeStartupTiming, - Spec as NativePerformance, } from '../NativePerformance'; import {RawPerformanceEntryTypeValues} from '../../RawPerformanceEntry'; -import NativePerformanceObserver from '../NativePerformanceObserver'; +import NativePerformance from '../NativePerformance'; +import {logMockEntry} from './NativePerformanceObserver'; const marks: Map = new Map(); -const NativePerformanceMock: NativePerformance = { +const NativePerformanceMock: typeof NativePerformance = { mark: (name: string, startTime: number): void => { - NativePerformanceObserver?.logRawEntry({ - name, + NativePerformance?.mark(name, startTime); + marks.set(name, startTime); + logMockEntry({ entryType: RawPerformanceEntryTypeValues.MARK, + name, startTime, duration: 0, }); - marks.set(name, startTime); }, measure: ( @@ -40,11 +41,12 @@ const NativePerformanceMock: NativePerformance = { ): void => { const start = startMark != null ? marks.get(startMark) ?? 0 : startTime; const end = endMark != null ? marks.get(endMark) ?? 0 : endTime; - NativePerformanceObserver?.logRawEntry({ - name, + NativePerformance?.measure(name, start, end); + logMockEntry({ entryType: RawPerformanceEntryTypeValues.MEASURE, + name, startTime: start, - duration: duration ?? (end ? end - start : 0), + duration: duration ?? end - start, }); }, diff --git a/packages/react-native/src/private/webapis/performance/specs/__mocks__/NativePerformanceObserver.js b/packages/react-native/src/private/webapis/performance/specs/__mocks__/NativePerformanceObserver.js index 1104198efbfb7f..a9f95ecea4a9d5 100644 --- a/packages/react-native/src/private/webapis/performance/specs/__mocks__/NativePerformanceObserver.js +++ b/packages/react-native/src/private/webapis/performance/specs/__mocks__/NativePerformanceObserver.js @@ -9,90 +9,117 @@ */ import type { - GetPendingEntriesResult, + NativeBatchedObserverCallback, RawPerformanceEntry, RawPerformanceEntryType, + OpaqueNativeObserverHandle, + PerformanceObserverInit, Spec as NativePerformanceObserver, } from '../NativePerformanceObserver'; import {RawPerformanceEntryTypeValues} from '../../RawPerformanceEntry'; -const reportingType: Set = new Set(); -const isAlwaysLogged: Set = new Set(); +jest.mock( + '../NativePerformance', + () => require('../__mocks__/NativePerformance').default, +); + +jest.mock( + '../NativePerformanceObserver', + () => require('../__mocks__/NativePerformanceObserver').default, +); + const eventCounts: Map = new Map(); -const durationThresholds: Map = new Map(); +let observers: MockObserver[] = []; let entries: Array = []; -let onPerformanceEntryCallback: ?() => void; -const NativePerformanceObserverMock: NativePerformanceObserver = { - startReporting: (entryType: RawPerformanceEntryType) => { - reportingType.add(entryType); - }, +export function logMockEntry(entry: RawPerformanceEntry) { + entries.push(entry); - stopReporting: (entryType: RawPerformanceEntryType) => { - reportingType.delete(entryType); - durationThresholds.delete(entryType); - }, + if (entry.entryType === RawPerformanceEntryTypeValues.EVENT) { + eventCounts.set(entry.name, (eventCounts.get(entry.name) ?? 0) + 1); + } + + for (const observer of observers) { + if ( + observer.options.type !== entry.entryType && + !observer.options.entryTypes?.includes(entry.entryType) + ) { + continue; + } - setIsBuffered: ( - entryTypes: $ReadOnlyArray, - isBuffered: boolean, - ) => { - for (const entryType of entryTypes) { - if (isBuffered) { - isAlwaysLogged.add(entryType); - } else { - isAlwaysLogged.delete(entryType); + if (entry.entryType === RawPerformanceEntryTypeValues.EVENT) { + const {durationThreshold = 0} = observer.options; + if (durationThreshold > 0 && entry.duration < durationThreshold) { + continue; } } + + observer.entries.push(entry); + + // $FlowFixMe[incompatible-call] + global.queueMicrotask(() => { + // We want to emulate the way it's done in native (i.e. async/batched) + observer.callback(); + }); + } +} + +type MockObserver = { + callback: NativeBatchedObserverCallback, + entries: Array, + options: PerformanceObserverInit, + droppedEntriesCount: number, +}; + +const NativePerformanceObserverMock: NativePerformanceObserver = { + getEventCounts: (): $ReadOnlyArray<[string, number]> => { + return Array.from(eventCounts.entries()); }, - popPendingEntries: (): GetPendingEntriesResult => { - const res = entries; - entries = []; - return { + createObserver: ( + callback: NativeBatchedObserverCallback, + ): OpaqueNativeObserverHandle => { + const observer: MockObserver = { + callback, + entries: [], + options: {}, droppedEntriesCount: 0, - entries: res, }; + + return observer; }, - setOnPerformanceEntryCallback: (callback?: () => void) => { - onPerformanceEntryCallback = callback; + getDroppedEntriesCount: (observer: OpaqueNativeObserverHandle): number => { + // $FlowFixMe + const mockObserver = (observer: any) as MockObserver; + return mockObserver.droppedEntriesCount; }, - logRawEntry: (entry: RawPerformanceEntry) => { - if ( - reportingType.has(entry.entryType) || - isAlwaysLogged.has(entry.entryType) - ) { - const durationThreshold = durationThresholds.get(entry.entryType); - if ( - durationThreshold !== undefined && - entry.duration < durationThreshold - ) { - return; - } - entries.push(entry); - // $FlowFixMe[incompatible-call] - global.queueMicrotask(() => { - // We want to emulate the way it's done in native (i.e. async/batched) - onPerformanceEntryCallback?.(); - }); - } - if (entry.entryType === RawPerformanceEntryTypeValues.EVENT) { - eventCounts.set(entry.name, (eventCounts.get(entry.name) ?? 0) + 1); - } + observe: ( + observer: OpaqueNativeObserverHandle, + options: PerformanceObserverInit, + ): void => { + // $FlowFixMe + const mockObserver = (observer: any) as MockObserver; + mockObserver.options = options; + observers.push(mockObserver); }, - getEventCounts: (): $ReadOnlyArray<[string, number]> => { - return Array.from(eventCounts.entries()); + disconnect: (observer: OpaqueNativeObserverHandle): void => { + // $FlowFixMe + const mockObserver = (observer: any) as MockObserver; + observers = observers.filter(e => e !== mockObserver); }, - setDurationThreshold: ( - entryType: RawPerformanceEntryType, - durationThreshold: number, - ) => { - durationThresholds.set(entryType, durationThreshold); + takeRecords: ( + observer: OpaqueNativeObserverHandle, + ): $ReadOnlyArray => { + // $FlowFixMe + const mockObserver = (observer: any) as MockObserver; + const observerEntries = mockObserver.entries; + mockObserver.entries = []; + return observerEntries; }, clearEntries: (entryType?: RawPerformanceEntryType, entryName?: string) => { diff --git a/packages/react-native/src/private/webapis/performance/specs/__tests__/NativePerformanceMock-test.js b/packages/react-native/src/private/webapis/performance/specs/__tests__/NativePerformanceMock-test.js index be57b29e2bb7bc..ac41cbc9b48ba1 100644 --- a/packages/react-native/src/private/webapis/performance/specs/__tests__/NativePerformanceMock-test.js +++ b/packages/react-native/src/private/webapis/performance/specs/__tests__/NativePerformanceMock-test.js @@ -11,16 +11,16 @@ import type {PerformanceEntryList} from '../../PerformanceObserver'; +jest.mock( + '../NativePerformanceObserver', + () => require('../__mocks__/NativePerformanceObserver').default, +); + const NativePerformanceMock = require('../__mocks__/NativePerformance').default; const PerformanceObserver = require('../../PerformanceObserver').PerformanceObserver; describe('NativePerformanceMock', () => { - jest.mock( - '../NativePerformanceObserver', - () => require('../__mocks__/NativePerformanceObserver').default, - ); - it('marks get reported', async () => { let entries: PerformanceEntryList = []; const observer = new PerformanceObserver((list, _observer) => { @@ -29,9 +29,9 @@ describe('NativePerformanceMock', () => { observer.observe({type: 'mark'}); - NativePerformanceMock.mark('mark1', 0); - NativePerformanceMock.mark('mark2', 5); - NativePerformanceMock.mark('mark3', 10); + NativePerformanceMock?.mark('mark1', 0); + NativePerformanceMock?.mark('mark2', 5); + NativePerformanceMock?.mark('mark3', 10); await jest.runAllTicks(); expect(entries).toHaveLength(3); @@ -47,13 +47,13 @@ describe('NativePerformanceMock', () => { observer.observe({entryTypes: ['measure']}); - NativePerformanceMock.mark('mark0', 0.0); - NativePerformanceMock.mark('mark1', 1.0); - NativePerformanceMock.mark('mark2', 2.0); + NativePerformanceMock?.mark('mark0', 0.0); + NativePerformanceMock?.mark('mark1', 1.0); + NativePerformanceMock?.mark('mark2', 2.0); - NativePerformanceMock.measure('measure0', 0, 2); - NativePerformanceMock.measure('measure1', 0, 2, 4); - NativePerformanceMock.measure( + NativePerformanceMock?.measure('measure0', 0, 2); + NativePerformanceMock?.measure('measure1', 0, 2, 4); + NativePerformanceMock?.measure( 'measure2', 0, 0, @@ -61,8 +61,8 @@ describe('NativePerformanceMock', () => { 'mark1', 'mark2', ); - NativePerformanceMock.measure('measure3', 0, 0, 5, 'mark1'); - NativePerformanceMock.measure( + NativePerformanceMock?.measure('measure3', 0, 0, 5, 'mark1'); + NativePerformanceMock?.measure( 'measure4', 1.5, 0, diff --git a/packages/react-native/src/private/webapis/performance/specs/__tests__/NativePerformanceObserverMock-test.js b/packages/react-native/src/private/webapis/performance/specs/__tests__/NativePerformanceObserverMock-test.js index 3e3205ce115235..6753251ec3c903 100644 --- a/packages/react-native/src/private/webapis/performance/specs/__tests__/NativePerformanceObserverMock-test.js +++ b/packages/react-native/src/private/webapis/performance/specs/__tests__/NativePerformanceObserverMock-test.js @@ -9,96 +9,50 @@ * @oncall react_native */ -import NativePerformanceObserverMock from '../__mocks__/NativePerformanceObserver'; +import NativePerformanceObserverMock, { + logMockEntry, +} from '../__mocks__/NativePerformanceObserver'; import {RawPerformanceEntryTypeValues} from '../../RawPerformanceEntry'; describe('NativePerformanceObserver', () => { - it('correctly starts and stops listening to entries in a nominal scenario', async () => { - NativePerformanceObserverMock.startReporting( - RawPerformanceEntryTypeValues.MARK, - ); - - NativePerformanceObserverMock.logRawEntry({ - name: 'mark1', - entryType: RawPerformanceEntryTypeValues.MARK, - startTime: 0, - duration: 10, - }); - - NativePerformanceObserverMock.logRawEntry({ - name: 'mark2', - entryType: RawPerformanceEntryTypeValues.MARK, - startTime: 0, - duration: 20, - }); - - NativePerformanceObserverMock.logRawEntry({ - name: 'event1', - entryType: RawPerformanceEntryTypeValues.EVENT, - startTime: 0, - duration: 20, - }); - - const entriesResult = NativePerformanceObserverMock.popPendingEntries(); - expect(entriesResult).not.toBe(undefined); - const entries = entriesResult.entries; - - expect(entries.length).toBe(2); - expect(entries[0].name).toBe('mark1'); - expect(entries[1].name).toBe('mark2'); - - const entriesResult1 = NativePerformanceObserverMock.popPendingEntries(); - expect(entriesResult1).not.toBe(undefined); - const entries1 = entriesResult1.entries; - expect(entries1.length).toBe(0); - - NativePerformanceObserverMock.stopReporting( - RawPerformanceEntryTypeValues.MARK, - ); - }); - it('correctly clears/gets entries', async () => { - NativePerformanceObserverMock.startReporting( - RawPerformanceEntryTypeValues.MARK, - ); - - NativePerformanceObserverMock.startReporting( - RawPerformanceEntryTypeValues.EVENT, - ); - - NativePerformanceObserverMock.logRawEntry({ + logMockEntry({ name: 'mark1', entryType: RawPerformanceEntryTypeValues.MARK, startTime: 0, duration: 0, }); - NativePerformanceObserverMock.logRawEntry({ + logMockEntry({ name: 'event1', entryType: RawPerformanceEntryTypeValues.EVENT, startTime: 0, duration: 0, }); + expect( + NativePerformanceObserverMock.getEntries().map(e => e.name), + ).toStrictEqual(['mark1', 'event1']); + NativePerformanceObserverMock.clearEntries(); expect(NativePerformanceObserverMock.getEntries()).toStrictEqual([]); - NativePerformanceObserverMock.logRawEntry({ + logMockEntry({ name: 'entry1', entryType: RawPerformanceEntryTypeValues.MARK, startTime: 0, duration: 0, }); - NativePerformanceObserverMock.logRawEntry({ + logMockEntry({ name: 'entry2', entryType: RawPerformanceEntryTypeValues.MARK, startTime: 0, duration: 0, }); - NativePerformanceObserverMock.logRawEntry({ + logMockEntry({ name: 'entry1', entryType: RawPerformanceEntryTypeValues.EVENT, startTime: 0, diff --git a/packages/rn-tester/js/examples/Performance/PerformanceApiExample.js b/packages/rn-tester/js/examples/Performance/PerformanceApiExample.js index 2129b8624c523e..d051b1e6b9ebc8 100644 --- a/packages/rn-tester/js/examples/Performance/PerformanceApiExample.js +++ b/packages/rn-tester/js/examples/Performance/PerformanceApiExample.js @@ -167,7 +167,7 @@ function PerformanceObserverEventTimingExample(): React.Node { setEntries(newEntries); }); - observer.observe({entryTypes: ['event']}); + observer.observe({type: 'event'}); return () => observer.disconnect(); }, []); From 633ad4933e9514d4168d6dcdb7e56c9a1859482a Mon Sep 17 00:00:00 2001 From: "hoxyq (Meta Employee)" Date: Thu, 26 Sep 2024 09:58:18 -0700 Subject: [PATCH 024/296] fix: use public instance in Fiber renderer and expose it from getInspectorDataForViewAtPoint (#31068) (#46674) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46674 bypass-github-export-checks React DevTools no longer operates with just Fibers, it now builds its own Shadow Tree, which represents the tree on the Host (Fabric on Native, DOM on Web). We have to keep track of public instances for a select-to-inspect feature. We've recently changed this logic in https://github.com/facebook/react/pull/30831, and looks like we've been incorrectly getting a public instance for Fabric case. Not only this, turns out that all `getInspectorData...` APIs are returning Fibers, and not public instances. I have to expose it, so that React DevTools can correctly identify the element, which was selected. Changes for React Native are in [D63421463](https://www.internalfb.com/diff/D63421463) DiffTrain build for commit https://github.com/facebook/react/commit/d66fa02a303fc53d901bdb0d7bbdaec3e6774b19. Test Plan: Sandcastle tests Reviewed By: poteto Differential Revision: D63453667 Pulled By: hoxyq fbshipit-source-id: 21b1d5d4cd68b42748d4a785e0f28eaf5db57f21 --- .../react-native/Libraries/Renderer/shims/ReactNativeTypes.js | 3 ++- .../Libraries/__tests__/__snapshots__/public-api-test.js.snap | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/react-native/Libraries/Renderer/shims/ReactNativeTypes.js b/packages/react-native/Libraries/Renderer/shims/ReactNativeTypes.js index dce315677107bb..4cdb435dbe5b88 100644 --- a/packages/react-native/Libraries/Renderer/shims/ReactNativeTypes.js +++ b/packages/react-native/Libraries/Renderer/shims/ReactNativeTypes.js @@ -7,7 +7,7 @@ * @noformat * @nolint * @flow strict - * @generated SignedSource<<3eb929731c259569c7af3b6479e486fe>> + * @generated SignedSource<> */ import type { @@ -180,6 +180,7 @@ export type TouchedViewDataAtPoint = $ReadOnly<{ width: number, height: number, }>, + closestPublicInstance?: PublicInstance, ...InspectorData, }>; diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index 88b07421e9d12b..8910afc37cacee 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -7838,6 +7838,7 @@ export type TouchedViewDataAtPoint = $ReadOnly<{ width: number, height: number, }>, + closestPublicInstance?: PublicInstance, ...InspectorData, }>; export type RenderRootOptions = { From 04da3d784b4d8bb3dacff0dbf6410d9a8114a9f5 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Thu, 26 Sep 2024 11:32:00 -0700 Subject: [PATCH 025/296] Error in reportMount when surface is not found (#46678) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46678 I'm investigating some issues with surface management and noticed `surfaceHandlerRegistry_` is not updated in the bridgeless path when using ReactSurfaceView. Adding a warning to help us validate this is resolved when we do eventually fix it. Changelog: [Internal] Reviewed By: fabriziocucci Differential Revision: D63463521 fbshipit-source-id: 38995924588f1d71b9fc517c76a6e0c572fd0699 --- .../ReactAndroid/src/main/jni/react/fabric/Binding.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp index b3915ff3afe8b6..6cb64b8049ab6a 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp @@ -109,11 +109,13 @@ void Binding::reportMount(SurfaceId surfaceId) { // incorrectly. This is due to the push model used on Android and can be // removed when we migrate to a pull model. std::shared_lock lock(surfaceHandlerRegistryMutex_); - auto iterator = surfaceHandlerRegistry_.find(surfaceId); if (iterator != surfaceHandlerRegistry_.end()) { auto& surfaceHandler = iterator->second; surfaceHandler.getMountingCoordinator()->didPerformAsyncTransactions(); + } else { + LOG(ERROR) << "Binding::reportMount: Surface with id " << surfaceId + << " is not found"; } } From 3fbf5c72f3464bb089993ec1787af04949d6e4aa Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Thu, 26 Sep 2024 11:32:00 -0700 Subject: [PATCH 026/296] Fix ReactSurfaceView-backed roots not reporting the end of pending transactions correctly (#46676) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46676 Changelog: [internal] ## Context We recently "fixed" a problem in `MountingCoordinator` on Android where it would report that it doesn't have any pending transactions when, in fact, it does. The fix introduces a new method in that class to delay marking transactions as done until a mount hook is invoked for that surface. That fixed the issue... by always reporting that there were pending transactions accidentally. The reason for this bug is that the mount hook doesn't have access to the mounting coordinator of the surface if the surface is registered through some of the methods in `Binding.cpp` that don't add the surface to a registry. In that case, we can never mark the transactions as done and the mounting coordinator for those surfaces always report pending transactions incorrectly. NOTE: this bug only affects apps that have the `fixMountingCoordinatorReportedPendingTransactionsOnAndroid` feature flag enabled. ## Changes This fixes the issue by making sure that surfaces are always registered in the registry and that we can access their mounting coordinators in the mount hook to report the transactions as done. Reviewed By: rubennorte Differential Revision: D63466672 fbshipit-source-id: a621a12cda89a3ab7331d3c6a16c6cdfa9341821 --- .../src/main/jni/react/fabric/Binding.cpp | 62 ++++++++++++++----- .../src/main/jni/react/fabric/Binding.h | 18 ++++-- 2 files changed, 61 insertions(+), 19 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp index 6cb64b8049ab6a..d87d4dcfd32bbe 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp @@ -13,7 +13,6 @@ #include "EventEmitterWrapper.h" #include "FabricMountingManager.h" #include "ReactNativeConfigHolder.h" -#include "SurfaceHandlerBinding.h" #include #include @@ -111,8 +110,20 @@ void Binding::reportMount(SurfaceId surfaceId) { std::shared_lock lock(surfaceHandlerRegistryMutex_); auto iterator = surfaceHandlerRegistry_.find(surfaceId); if (iterator != surfaceHandlerRegistry_.end()) { - auto& surfaceHandler = iterator->second; - surfaceHandler.getMountingCoordinator()->didPerformAsyncTransactions(); + const auto* surfaceHandler = + std::get_if(&iterator->second); + if (surfaceHandler == nullptr) { + auto javaSurfaceHandler = + std::get>( + iterator->second) + .lockLocal(); + if (javaSurfaceHandler) { + surfaceHandler = &javaSurfaceHandler->cthis()->getSurfaceHandler(); + } + } + if (surfaceHandler != nullptr) { + surfaceHandler->getMountingCoordinator()->didPerformAsyncTransactions(); + } } else { LOG(ERROR) << "Binding::reportMount: Surface with id " << surfaceId << " is not found"; @@ -258,16 +269,19 @@ void Binding::stopSurface(jint surfaceId) { std::unique_lock lock(surfaceHandlerRegistryMutex_); auto iterator = surfaceHandlerRegistry_.find(surfaceId); - if (iterator == surfaceHandlerRegistry_.end()) { LOG(ERROR) << "Binding::stopSurface: Surface with given id is not found"; return; } - auto surfaceHandler = std::move(iterator->second); + auto* surfaceHandler = std::get_if(&iterator->second); + if (surfaceHandler != nullptr) { + surfaceHandler->stop(); + scheduler->unregisterSurface(*surfaceHandler); + } else { + LOG(ERROR) << "Java-owned SurfaceHandler found in stopSurface"; + } surfaceHandlerRegistry_.erase(iterator); - surfaceHandler.stop(); - scheduler->unregisterSurface(surfaceHandler); } auto mountingManager = getMountingManager("stopSurface"); @@ -277,8 +291,12 @@ void Binding::stopSurface(jint surfaceId) { mountingManager->onSurfaceStop(surfaceId); } -void Binding::registerSurface(SurfaceHandlerBinding* surfaceHandlerBinding) { - const auto& surfaceHandler = surfaceHandlerBinding->getSurfaceHandler(); +void Binding::registerSurface( + jni::alias_ref + surfaceHandlerBinding) { + const auto& surfaceHandler = + surfaceHandlerBinding->cthis()->getSurfaceHandler(); + auto scheduler = getScheduler(); if (!scheduler) { LOG(ERROR) << "Binding::registerSurface: scheduler disappeared"; @@ -286,6 +304,12 @@ void Binding::registerSurface(SurfaceHandlerBinding* surfaceHandlerBinding) { } scheduler->registerSurface(surfaceHandler); + { + std::unique_lock lock(surfaceHandlerRegistryMutex_); + surfaceHandlerRegistry_.emplace( + surfaceHandler.getSurfaceId(), jni::make_weak(surfaceHandlerBinding)); + } + auto mountingManager = getMountingManager("registerSurface"); if (!mountingManager) { return; @@ -293,8 +317,11 @@ void Binding::registerSurface(SurfaceHandlerBinding* surfaceHandlerBinding) { mountingManager->onSurfaceStart(surfaceHandler.getSurfaceId()); } -void Binding::unregisterSurface(SurfaceHandlerBinding* surfaceHandlerBinding) { - const auto& surfaceHandler = surfaceHandlerBinding->getSurfaceHandler(); +void Binding::unregisterSurface( + jni::alias_ref + surfaceHandlerBinding) { + const auto& surfaceHandler = + surfaceHandlerBinding->cthis()->getSurfaceHandler(); auto scheduler = getScheduler(); if (!scheduler) { LOG(ERROR) << "Binding::unregisterSurface: scheduler disappeared"; @@ -302,6 +329,11 @@ void Binding::unregisterSurface(SurfaceHandlerBinding* surfaceHandlerBinding) { } scheduler->unregisterSurface(surfaceHandler); + { + std::unique_lock lock(surfaceHandlerRegistryMutex_); + surfaceHandlerRegistry_.erase(surfaceHandler.getSurfaceId()); + } + auto mountingManager = getMountingManager("unregisterSurface"); if (!mountingManager) { return; @@ -347,15 +379,15 @@ void Binding::setConstraints( std::shared_lock lock(surfaceHandlerRegistryMutex_); auto iterator = surfaceHandlerRegistry_.find(surfaceId); - if (iterator == surfaceHandlerRegistry_.end()) { LOG(ERROR) << "Binding::setConstraints: Surface with given id is not found"; return; } - - auto& surfaceHandler = iterator->second; - surfaceHandler.constraintLayout(constraints, context); + auto* surfaceHandler = std::get_if(&iterator->second); + if (surfaceHandler != nullptr) { + surfaceHandler->constraintLayout(constraints, context); + } } } diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.h b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.h index 17c09e46d9b766..22ad8ae3a03522 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.h @@ -23,6 +23,7 @@ #include "EventEmitterWrapper.h" #include "JFabricUIManager.h" +#include "SurfaceHandlerBinding.h" namespace facebook::react { @@ -33,7 +34,6 @@ class Instance; class LayoutAnimationDriver; class ReactNativeConfig; class Scheduler; -class SurfaceHandlerBinding; struct JBinding : public jni::JavaClass { constexpr static auto kJavaDescriptor = "Lcom/facebook/react/fabric/Binding;"; @@ -95,9 +95,11 @@ class Binding : public jni::HybridClass, void stopSurface(jint surfaceId); - void registerSurface(SurfaceHandlerBinding* surfaceHandler); + void registerSurface( + jni::alias_ref surfaceHandler); - void unregisterSurface(SurfaceHandlerBinding* surfaceHandler); + void unregisterSurface( + jni::alias_ref surfaceHandler); void schedulerDidFinishTransaction( const MountingCoordinator::Shared& mountingCoordinator) override; @@ -148,7 +150,15 @@ class Binding : public jni::HybridClass, BackgroundExecutor backgroundExecutor_; - std::unordered_map surfaceHandlerRegistry_{}; + // Roots not created through ReactSurface (non-bridgeless) will store their + // SurfaceHandler here, for other roots we keep a weak reference to the Java + // owner + std::unordered_map< + SurfaceId, + std::variant< + SurfaceHandler, + jni::weak_ref>> + surfaceHandlerRegistry_{}; std::shared_mutex surfaceHandlerRegistryMutex_; // Protects `surfaceHandlerRegistry_`. From f775f6919f3890824a5d8357b203591ad401e6de Mon Sep 17 00:00:00 2001 From: Thomas Nardone Date: Thu, 26 Sep 2024 13:11:07 -0700 Subject: [PATCH 027/296] Redo "[RN][Android] Convert ReactViewManager, ReactClippingViewManager to Kotlin" (#46667) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46667 Pre-req for converting ReactViewGroup without affecting API visibility. Changelog: [Internal] Reviewed By: javache Differential Revision: D63129925 fbshipit-source-id: 3b31df7b5450adcc961bb677114fb94d944f1ce4 --- .../ReactAndroid/api/ReactAndroid.api | 9 +- .../views/view/ReactClippingViewManager.java | 95 ---- .../views/view/ReactClippingViewManager.kt | 73 +++ .../react/views/view/ReactViewManager.java | 425 ------------------ .../react/views/view/ReactViewManager.kt | 390 ++++++++++++++++ 5 files changed, 470 insertions(+), 522 deletions(-) delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewManager.java create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewManager.kt delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index e1909ad0345138..1f02b1355d8b6a 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -8363,6 +8363,8 @@ public class com/facebook/react/views/view/ReactViewGroup : android/view/ViewGro } public class com/facebook/react/views/view/ReactViewManager : com/facebook/react/views/view/ReactClippingViewManager { + public static final field Companion Lcom/facebook/react/views/view/ReactViewManager$Companion; + public static final field REACT_CLASS Ljava/lang/String; public fun ()V public synthetic fun createViewInstance (Lcom/facebook/react/uimanager/ThemedReactContext;)Landroid/view/View; public fun createViewInstance (Lcom/facebook/react/uimanager/ThemedReactContext;)Lcom/facebook/react/views/view/ReactViewGroup; @@ -8373,7 +8375,7 @@ public class com/facebook/react/views/view/ReactViewManager : com/facebook/react public fun nextFocusLeft (Lcom/facebook/react/views/view/ReactViewGroup;I)V public fun nextFocusRight (Lcom/facebook/react/views/view/ReactViewGroup;I)V public fun nextFocusUp (Lcom/facebook/react/views/view/ReactViewGroup;I)V - protected synthetic fun prepareToRecycleView (Lcom/facebook/react/uimanager/ThemedReactContext;Landroid/view/View;)Landroid/view/View; + public synthetic fun prepareToRecycleView (Lcom/facebook/react/uimanager/ThemedReactContext;Landroid/view/View;)Landroid/view/View; protected fun prepareToRecycleView (Lcom/facebook/react/uimanager/ThemedReactContext;Lcom/facebook/react/views/view/ReactViewGroup;)Lcom/facebook/react/views/view/ReactViewGroup; public synthetic fun receiveCommand (Landroid/view/View;ILcom/facebook/react/bridge/ReadableArray;)V public synthetic fun receiveCommand (Landroid/view/View;Ljava/lang/String;Lcom/facebook/react/bridge/ReadableArray;)V @@ -8402,7 +8404,7 @@ public class com/facebook/react/views/view/ReactViewManager : com/facebook/react public fun setOverflow (Lcom/facebook/react/views/view/ReactViewGroup;Ljava/lang/String;)V public fun setPointerEvents (Lcom/facebook/react/views/view/ReactViewGroup;Ljava/lang/String;)V public fun setTVPreferredFocus (Lcom/facebook/react/views/view/ReactViewGroup;Z)V - protected synthetic fun setTransformProperty (Landroid/view/View;Lcom/facebook/react/bridge/ReadableArray;Lcom/facebook/react/bridge/ReadableArray;)V + public synthetic fun setTransformProperty (Landroid/view/View;Lcom/facebook/react/bridge/ReadableArray;Lcom/facebook/react/bridge/ReadableArray;)V protected fun setTransformProperty (Lcom/facebook/react/views/view/ReactViewGroup;Lcom/facebook/react/bridge/ReadableArray;Lcom/facebook/react/bridge/ReadableArray;)V } @@ -8413,6 +8415,9 @@ public class com/facebook/react/views/view/ReactViewManager$$PropsSetter : com/f public fun setProperty (Lcom/facebook/react/views/view/ReactViewManager;Lcom/facebook/react/views/view/ReactViewGroup;Ljava/lang/String;Ljava/lang/Object;)V } +public final class com/facebook/react/views/view/ReactViewManager$Companion { +} + public final class com/facebook/react/views/view/ViewGroupClickEvent : com/facebook/react/uimanager/events/Event { public fun (I)V public fun (II)V diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewManager.java deleted file mode 100644 index 525183053a0ef0..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewManager.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.views.view; - -import android.view.View; -import androidx.annotation.Nullable; -import com.facebook.infer.annotation.Nullsafe; -import com.facebook.react.bridge.UiThreadUtil; -import com.facebook.react.uimanager.ViewGroupManager; -import com.facebook.react.uimanager.annotations.ReactProp; - -/** - * View manager which handles clipped subviews. Useful for custom views which extends from {@link - * com.facebook.react.views.view.ReactViewGroup} - */ -@Nullsafe(Nullsafe.Mode.LOCAL) -public abstract class ReactClippingViewManager - extends ViewGroupManager { - - @ReactProp( - name = com.facebook.react.uimanager.ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS) - public void setRemoveClippedSubviews(T view, boolean removeClippedSubviews) { - UiThreadUtil.assertOnUiThread(); - - view.setRemoveClippedSubviews(removeClippedSubviews); - } - - @Override - public void addView(T parent, View child, int index) { - UiThreadUtil.assertOnUiThread(); - - boolean removeClippedSubviews = parent.getRemoveClippedSubviews(); - if (removeClippedSubviews) { - parent.addViewWithSubviewClippingEnabled(child, index); - } else { - parent.addView(child, index); - } - } - - @Override - public int getChildCount(T parent) { - boolean removeClippedSubviews = parent.getRemoveClippedSubviews(); - if (removeClippedSubviews) { - return parent.getAllChildrenCount(); - } else { - return parent.getChildCount(); - } - } - - @Override - @Nullable - public View getChildAt(T parent, int index) { - boolean removeClippedSubviews = parent.getRemoveClippedSubviews(); - if (removeClippedSubviews) { - return parent.getChildAtWithSubviewClippingEnabled(index); - } else { - return parent.getChildAt(index); - } - } - - @Override - public void removeViewAt(T parent, int index) { - UiThreadUtil.assertOnUiThread(); - - boolean removeClippedSubviews = parent.getRemoveClippedSubviews(); - if (removeClippedSubviews) { - View child = getChildAt(parent, index); - if (child != null) { - if (child.getParent() != null) { - parent.removeView(child); - } - parent.removeViewWithSubviewClippingEnabled(child); - } - } else { - parent.removeViewAt(index); - } - } - - @Override - public void removeAllViews(T parent) { - UiThreadUtil.assertOnUiThread(); - - boolean removeClippedSubviews = parent.getRemoveClippedSubviews(); - if (removeClippedSubviews) { - parent.removeAllViewsWithSubviewClippingEnabled(); - } else { - parent.removeAllViews(); - } - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewManager.kt new file mode 100644 index 00000000000000..d77075ed705586 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewManager.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.view + +import android.view.View +import com.facebook.react.bridge.UiThreadUtil +import com.facebook.react.uimanager.ReactClippingViewGroupHelper +import com.facebook.react.uimanager.ViewGroupManager +import com.facebook.react.uimanager.annotations.ReactProp + +/** + * View manager which handles clipped subviews. Useful for custom views which extends from + * [com.facebook.react.views.view.ReactViewGroup] + */ +public abstract class ReactClippingViewManager : ViewGroupManager() { + + @ReactProp(name = ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS) + public open fun setRemoveClippedSubviews(view: T, removeClippedSubviews: Boolean) { + UiThreadUtil.assertOnUiThread() + view.removeClippedSubviews = removeClippedSubviews + } + + override fun addView(parent: T, child: View, index: Int) { + UiThreadUtil.assertOnUiThread() + if (parent.removeClippedSubviews) { + parent.addViewWithSubviewClippingEnabled(child, index) + } else { + parent.addView(child, index) + } + } + + override fun getChildCount(parent: T): Int = + if (parent.removeClippedSubviews) { + parent.allChildrenCount + } else { + parent.childCount + } + + override fun getChildAt(parent: T, index: Int): View? = + if (parent.removeClippedSubviews) { + parent.getChildAtWithSubviewClippingEnabled(index) + } else { + parent.getChildAt(index) + } + + override fun removeViewAt(parent: T, index: Int) { + UiThreadUtil.assertOnUiThread() + if (parent.removeClippedSubviews) { + val child = getChildAt(parent, index) ?: return + if (child.parent != null) { + parent.removeView(child) + } else { + parent.removeViewWithSubviewClippingEnabled(child) + } + } else { + parent.removeViewAt(index) + } + } + + override fun removeAllViews(parent: T) { + UiThreadUtil.assertOnUiThread() + if (parent.removeClippedSubviews) { + parent.removeAllViewsWithSubviewClippingEnabled() + } else { + parent.removeAllViews() + } + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java deleted file mode 100644 index 548fe524399cf9..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java +++ /dev/null @@ -1,425 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.views.view; - -import android.graphics.Rect; -import android.view.View; -import androidx.annotation.ColorInt; -import androidx.annotation.Nullable; -import com.facebook.common.logging.FLog; -import com.facebook.infer.annotation.Nullsafe; -import com.facebook.react.bridge.Dynamic; -import com.facebook.react.bridge.DynamicFromObject; -import com.facebook.react.bridge.JSApplicationIllegalArgumentException; -import com.facebook.react.bridge.ReactContext; -import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.common.MapBuilder; -import com.facebook.react.common.ReactConstants; -import com.facebook.react.common.annotations.VisibleForTesting; -import com.facebook.react.module.annotations.ReactModule; -import com.facebook.react.uimanager.BackgroundStyleApplicator; -import com.facebook.react.uimanager.LengthPercentage; -import com.facebook.react.uimanager.LengthPercentageType; -import com.facebook.react.uimanager.PixelUtil; -import com.facebook.react.uimanager.PointerEvents; -import com.facebook.react.uimanager.Spacing; -import com.facebook.react.uimanager.ThemedReactContext; -import com.facebook.react.uimanager.UIManagerHelper; -import com.facebook.react.uimanager.ViewProps; -import com.facebook.react.uimanager.annotations.ReactProp; -import com.facebook.react.uimanager.annotations.ReactPropGroup; -import com.facebook.react.uimanager.common.UIManagerType; -import com.facebook.react.uimanager.common.ViewUtil; -import com.facebook.react.uimanager.events.EventDispatcher; -import com.facebook.react.uimanager.style.BackgroundImageLayer; -import com.facebook.react.uimanager.style.BorderRadiusProp; -import com.facebook.react.uimanager.style.BorderStyle; -import com.facebook.react.uimanager.style.LogicalEdge; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** View manager for AndroidViews (plain React Views). */ -@ReactModule(name = ReactViewManager.REACT_CLASS) -@Nullsafe(Nullsafe.Mode.LOCAL) -public class ReactViewManager extends ReactClippingViewManager { - - @VisibleForTesting public static final String REACT_CLASS = ViewProps.VIEW_CLASS_NAME; - - private static final int[] SPACING_TYPES = { - Spacing.ALL, - Spacing.LEFT, - Spacing.RIGHT, - Spacing.TOP, - Spacing.BOTTOM, - Spacing.START, - Spacing.END, - Spacing.BLOCK, - Spacing.BLOCK_END, - Spacing.BLOCK_START - }; - private static final int CMD_HOTSPOT_UPDATE = 1; - private static final int CMD_SET_PRESSED = 2; - private static final String HOTSPOT_UPDATE_KEY = "hotspotUpdate"; - - public ReactViewManager() { - super(); - - setupViewRecycling(); - } - - @Override - protected @Nullable ReactViewGroup prepareToRecycleView( - ThemedReactContext reactContext, ReactViewGroup view) { - // BaseViewManager - ReactViewGroup preparedView = super.prepareToRecycleView(reactContext, view); - if (preparedView != null) { - preparedView.recycleView(); - } - return view; - } - - @ReactProp(name = "accessible") - public void setAccessible(ReactViewGroup view, boolean accessible) { - view.setFocusable(accessible); - } - - @ReactProp(name = "hasTVPreferredFocus") - public void setTVPreferredFocus(ReactViewGroup view, boolean hasTVPreferredFocus) { - if (hasTVPreferredFocus) { - view.setFocusable(true); - view.setFocusableInTouchMode(true); - view.requestFocus(); - } - } - - @ReactProp(name = ViewProps.BACKGROUND_IMAGE, customType = "BackgroundImage") - public void setBackgroundImage(ReactViewGroup view, @Nullable ReadableArray backgroundImage) { - if (ViewUtil.getUIManagerType(view) == UIManagerType.FABRIC) { - if (backgroundImage != null && backgroundImage.size() > 0) { - List backgroundImageLayers = new ArrayList<>(backgroundImage.size()); - for (int i = 0; i < backgroundImage.size(); i++) { - ReadableMap backgroundImageMap = backgroundImage.getMap(i); - BackgroundImageLayer layer = - new BackgroundImageLayer(backgroundImageMap, view.getContext()); - backgroundImageLayers.add(layer); - } - BackgroundStyleApplicator.setBackgroundImage(view, backgroundImageLayers); - } else { - BackgroundStyleApplicator.setBackgroundImage(view, null); - } - } - } - - @ReactProp(name = "nextFocusDown", defaultInt = View.NO_ID) - public void nextFocusDown(ReactViewGroup view, int viewId) { - view.setNextFocusDownId(viewId); - } - - @ReactProp(name = "nextFocusForward", defaultInt = View.NO_ID) - public void nextFocusForward(ReactViewGroup view, int viewId) { - view.setNextFocusForwardId(viewId); - } - - @ReactProp(name = "nextFocusLeft", defaultInt = View.NO_ID) - public void nextFocusLeft(ReactViewGroup view, int viewId) { - view.setNextFocusLeftId(viewId); - } - - @ReactProp(name = "nextFocusRight", defaultInt = View.NO_ID) - public void nextFocusRight(ReactViewGroup view, int viewId) { - view.setNextFocusRightId(viewId); - } - - @ReactProp(name = "nextFocusUp", defaultInt = View.NO_ID) - public void nextFocusUp(ReactViewGroup view, int viewId) { - view.setNextFocusUpId(viewId); - } - - @ReactPropGroup( - names = { - ViewProps.BORDER_RADIUS, - ViewProps.BORDER_TOP_LEFT_RADIUS, - ViewProps.BORDER_TOP_RIGHT_RADIUS, - ViewProps.BORDER_BOTTOM_RIGHT_RADIUS, - ViewProps.BORDER_BOTTOM_LEFT_RADIUS, - ViewProps.BORDER_TOP_START_RADIUS, - ViewProps.BORDER_TOP_END_RADIUS, - ViewProps.BORDER_BOTTOM_START_RADIUS, - ViewProps.BORDER_BOTTOM_END_RADIUS, - ViewProps.BORDER_END_END_RADIUS, - ViewProps.BORDER_END_START_RADIUS, - ViewProps.BORDER_START_END_RADIUS, - ViewProps.BORDER_START_START_RADIUS, - }) - public void setBorderRadius(ReactViewGroup view, int index, Dynamic rawBorderRadius) { - @Nullable LengthPercentage borderRadius = LengthPercentage.setFromDynamic(rawBorderRadius); - - // We do not support percentage border radii on Paper in order to be consistent with iOS (to - // avoid developer surprise if it works on one platform but not another). - if (ViewUtil.getUIManagerType(view) != UIManagerType.FABRIC - && borderRadius != null - && borderRadius.getType() == LengthPercentageType.PERCENT) { - borderRadius = null; - } - - BackgroundStyleApplicator.setBorderRadius(view, BorderRadiusProp.values()[index], borderRadius); - } - - /** - * @deprecated Use {@link #setBorderRadius(ReactViewGroup, int, Dynamic)} instead. - */ - @Deprecated(since = "0.75.0", forRemoval = true) - public void setBorderRadius(ReactViewGroup view, int index, float borderRadius) { - setBorderRadius(view, index, new DynamicFromObject(borderRadius)); - } - - @ReactProp(name = "borderStyle") - public void setBorderStyle(ReactViewGroup view, @Nullable String borderStyle) { - @Nullable - BorderStyle parsedBorderStyle = - borderStyle == null ? null : BorderStyle.fromString(borderStyle); - BackgroundStyleApplicator.setBorderStyle(view, parsedBorderStyle); - } - - @ReactProp(name = "hitSlop") - public void setHitSlop(final ReactViewGroup view, Dynamic hitSlop) { - switch (hitSlop.getType()) { - case Map: - ReadableMap hitSlopMap = hitSlop.asMap(); - view.setHitSlopRect( - new Rect( - hitSlopMap.hasKey("left") - ? (int) PixelUtil.toPixelFromDIP(hitSlopMap.getDouble("left")) - : 0, - hitSlopMap.hasKey("top") - ? (int) PixelUtil.toPixelFromDIP(hitSlopMap.getDouble("top")) - : 0, - hitSlopMap.hasKey("right") - ? (int) PixelUtil.toPixelFromDIP(hitSlopMap.getDouble("right")) - : 0, - hitSlopMap.hasKey("bottom") - ? (int) PixelUtil.toPixelFromDIP(hitSlopMap.getDouble("bottom")) - : 0)); - break; - case Number: - int hitSlopValue = (int) PixelUtil.toPixelFromDIP(hitSlop.asDouble()); - view.setHitSlopRect(new Rect(hitSlopValue, hitSlopValue, hitSlopValue, hitSlopValue)); - break; - default: - FLog.w(ReactConstants.TAG, "Invalid type for 'hitSlop' value " + hitSlop.getType()); - /* falls through */ - case Null: - view.setHitSlopRect(null); - break; - } - } - - @ReactProp(name = ViewProps.POINTER_EVENTS) - public void setPointerEvents(ReactViewGroup view, @Nullable String pointerEventsStr) { - view.setPointerEvents(PointerEvents.parsePointerEvents(pointerEventsStr)); - } - - @ReactProp(name = "nativeBackgroundAndroid") - public void setNativeBackground(ReactViewGroup view, @Nullable ReadableMap bg) { - view.setTranslucentBackgroundDrawable( - bg == null - ? null - : ReactDrawableHelper.createDrawableFromJSDescription(view.getContext(), bg)); - } - - @ReactProp(name = "nativeForegroundAndroid") - public void setNativeForeground(ReactViewGroup view, @Nullable ReadableMap fg) { - view.setForeground( - fg == null - ? null - : ReactDrawableHelper.createDrawableFromJSDescription(view.getContext(), fg)); - } - - @ReactProp(name = ViewProps.NEEDS_OFFSCREEN_ALPHA_COMPOSITING) - public void setNeedsOffscreenAlphaCompositing( - ReactViewGroup view, boolean needsOffscreenAlphaCompositing) { - view.setNeedsOffscreenAlphaCompositing(needsOffscreenAlphaCompositing); - } - - @ReactPropGroup( - names = { - ViewProps.BORDER_WIDTH, - ViewProps.BORDER_LEFT_WIDTH, - ViewProps.BORDER_RIGHT_WIDTH, - ViewProps.BORDER_TOP_WIDTH, - ViewProps.BORDER_BOTTOM_WIDTH, - ViewProps.BORDER_START_WIDTH, - ViewProps.BORDER_END_WIDTH, - }, - defaultFloat = Float.NaN) - public void setBorderWidth(ReactViewGroup view, int index, float width) { - BackgroundStyleApplicator.setBorderWidth(view, LogicalEdge.values()[index], width); - } - - @ReactPropGroup( - names = { - ViewProps.BORDER_COLOR, - ViewProps.BORDER_LEFT_COLOR, - ViewProps.BORDER_RIGHT_COLOR, - ViewProps.BORDER_TOP_COLOR, - ViewProps.BORDER_BOTTOM_COLOR, - ViewProps.BORDER_START_COLOR, - ViewProps.BORDER_END_COLOR, - ViewProps.BORDER_BLOCK_COLOR, - ViewProps.BORDER_BLOCK_END_COLOR, - ViewProps.BORDER_BLOCK_START_COLOR - }, - customType = "Color") - public void setBorderColor(ReactViewGroup view, int index, @Nullable Integer color) { - BackgroundStyleApplicator.setBorderColor( - view, LogicalEdge.fromSpacingType(SPACING_TYPES[index]), color); - } - - @ReactProp(name = ViewProps.COLLAPSABLE) - public void setCollapsable(ReactViewGroup view, boolean collapsable) { - // no-op: it's here only so that "collapsable" property is exported to JS. The value is actually - // handled in NativeViewHierarchyOptimizer - } - - @ReactProp(name = ViewProps.COLLAPSABLE_CHILDREN) - public void setCollapsableChildren(ReactViewGroup view, boolean collapsableChildren) { - // no-op: it's here only so that "collapsableChildren" property is exported to JS. - } - - @ReactProp(name = "focusable") - public void setFocusable(final ReactViewGroup view, boolean focusable) { - if (focusable) { - view.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - final EventDispatcher mEventDispatcher = - UIManagerHelper.getEventDispatcherForReactTag( - (ReactContext) view.getContext(), view.getId()); - if (mEventDispatcher == null) { - return; - } - mEventDispatcher.dispatchEvent( - new ViewGroupClickEvent( - UIManagerHelper.getSurfaceId(view.getContext()), view.getId())); - } - }); - - // Clickable elements are focusable. On API 26, this is taken care by setClickable. - // Explicitly calling setFocusable here for backward compatibility. - view.setFocusable(true /*isFocusable*/); - } else { - view.setOnClickListener(null); - view.setClickable(false); - // Don't set view.setFocusable(false) because we might still want it to be focusable for - // accessibility reasons - } - } - - @ReactProp(name = ViewProps.OVERFLOW) - public void setOverflow(ReactViewGroup view, String overflow) { - view.setOverflow(overflow); - } - - @ReactProp(name = "backfaceVisibility") - public void setBackfaceVisibility(ReactViewGroup view, String backfaceVisibility) { - view.setBackfaceVisibility(backfaceVisibility); - } - - @Override - public void setOpacity(ReactViewGroup view, float opacity) { - view.setOpacityIfPossible(opacity); - } - - @Override - protected void setTransformProperty( - ReactViewGroup view, - @Nullable ReadableArray transforms, - @Nullable ReadableArray transformOrigin) { - super.setTransformProperty(view, transforms, transformOrigin); - view.setBackfaceVisibilityDependantOpacity(); - } - - @ReactProp(name = ViewProps.BOX_SHADOW, customType = "BoxShadow") - public void setBoxShadow(ReactViewGroup view, @Nullable ReadableArray shadows) { - BackgroundStyleApplicator.setBoxShadow(view, shadows); - } - - @Override - public void setBackgroundColor(ReactViewGroup view, @ColorInt int backgroundColor) { - BackgroundStyleApplicator.setBackgroundColor(view, backgroundColor); - } - - @Override - public String getName() { - return REACT_CLASS; - } - - @Override - public ReactViewGroup createViewInstance(ThemedReactContext context) { - return new ReactViewGroup(context); - } - - @Override - public Map getCommandsMap() { - return MapBuilder.of(HOTSPOT_UPDATE_KEY, CMD_HOTSPOT_UPDATE, "setPressed", CMD_SET_PRESSED); - } - - @Override - public void receiveCommand(ReactViewGroup root, int commandId, @Nullable ReadableArray args) { - switch (commandId) { - case CMD_HOTSPOT_UPDATE: - { - handleHotspotUpdate(root, args); - break; - } - case CMD_SET_PRESSED: - { - handleSetPressed(root, args); - break; - } - } - } - - @Override - public void receiveCommand(ReactViewGroup root, String commandId, @Nullable ReadableArray args) { - switch (commandId) { - case HOTSPOT_UPDATE_KEY: - { - handleHotspotUpdate(root, args); - break; - } - case "setPressed": - { - handleSetPressed(root, args); - break; - } - } - } - - private void handleSetPressed(ReactViewGroup root, @Nullable ReadableArray args) { - if (args == null || args.size() != 1) { - throw new JSApplicationIllegalArgumentException( - "Illegal number of arguments for 'setPressed' command"); - } - root.setPressed(args.getBoolean(0)); - } - - private void handleHotspotUpdate(ReactViewGroup root, @Nullable ReadableArray args) { - if (args == null || args.size() != 2) { - throw new JSApplicationIllegalArgumentException( - "Illegal number of arguments for 'updateHotspot' command"); - } - - float x = PixelUtil.toPixelFromDIP(args.getDouble(0)); - float y = PixelUtil.toPixelFromDIP(args.getDouble(1)); - root.drawableHotspotChanged(x, y); - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt new file mode 100644 index 00000000000000..4b6dc1957e26f1 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt @@ -0,0 +1,390 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.view + +import android.graphics.Rect +import android.view.View +import androidx.annotation.ColorInt +import com.facebook.common.logging.FLog +import com.facebook.react.bridge.Dynamic +import com.facebook.react.bridge.DynamicFromObject +import com.facebook.react.bridge.JSApplicationIllegalArgumentException +import com.facebook.react.bridge.ReactContext +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.ReadableType +import com.facebook.react.common.ReactConstants +import com.facebook.react.common.annotations.UnstableReactNativeAPI +import com.facebook.react.module.annotations.ReactModule +import com.facebook.react.uimanager.BackgroundStyleApplicator +import com.facebook.react.uimanager.LengthPercentage.Companion.setFromDynamic +import com.facebook.react.uimanager.LengthPercentageType +import com.facebook.react.uimanager.PixelUtil.dpToPx +import com.facebook.react.uimanager.PointerEvents.Companion.parsePointerEvents +import com.facebook.react.uimanager.Spacing +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.UIManagerHelper +import com.facebook.react.uimanager.ViewProps +import com.facebook.react.uimanager.annotations.ReactProp +import com.facebook.react.uimanager.annotations.ReactPropGroup +import com.facebook.react.uimanager.common.UIManagerType +import com.facebook.react.uimanager.common.ViewUtil +import com.facebook.react.uimanager.style.BackgroundImageLayer +import com.facebook.react.uimanager.style.BorderRadiusProp +import com.facebook.react.uimanager.style.BorderStyle +import com.facebook.react.uimanager.style.BorderStyle.Companion.fromString +import com.facebook.react.uimanager.style.LogicalEdge +import com.facebook.react.uimanager.style.LogicalEdge.Companion.fromSpacingType + +/** View manager for AndroidViews (plain React Views). */ +@ReactModule(name = ReactViewManager.REACT_CLASS) +public open class ReactViewManager : ReactClippingViewManager() { + + init { + setupViewRecycling() + } + + override fun prepareToRecycleView( + reactContext: ThemedReactContext, + view: ReactViewGroup + ): ReactViewGroup { + // BaseViewManager + super.prepareToRecycleView(reactContext, view)?.recycleView() + return view + } + + @ReactProp(name = "accessible") + public open fun setAccessible(view: ReactViewGroup, accessible: Boolean): Unit { + view.isFocusable = accessible + } + + @ReactProp(name = "hasTVPreferredFocus") + public open fun setTVPreferredFocus(view: ReactViewGroup, hasTVPreferredFocus: Boolean): Unit { + if (hasTVPreferredFocus) { + view.isFocusable = true + view.isFocusableInTouchMode = true + view.requestFocus() + } + } + + @OptIn(UnstableReactNativeAPI::class) + @ReactProp(name = ViewProps.BACKGROUND_IMAGE, customType = "BackgroundImage") + public open fun setBackgroundImage(view: ReactViewGroup, backgroundImage: ReadableArray?): Unit { + if (ViewUtil.getUIManagerType(view) == UIManagerType.FABRIC) { + val size = backgroundImage?.size() + if (size != null && size > 0) { + val backgroundImageLayers = ArrayList(size) + repeat(size) { i -> + backgroundImageLayers.add(BackgroundImageLayer(backgroundImage.getMap(i), view.context)) + } + BackgroundStyleApplicator.setBackgroundImage(view, backgroundImageLayers) + } else { + BackgroundStyleApplicator.setBackgroundImage(view, null) + } + } + } + + @ReactProp(name = "nextFocusDown", defaultInt = View.NO_ID) + public open fun nextFocusDown(view: ReactViewGroup, viewId: Int): Unit { + view.nextFocusDownId = viewId + } + + @ReactProp(name = "nextFocusForward", defaultInt = View.NO_ID) + public open fun nextFocusForward(view: ReactViewGroup, viewId: Int): Unit { + view.nextFocusForwardId = viewId + } + + @ReactProp(name = "nextFocusLeft", defaultInt = View.NO_ID) + public open fun nextFocusLeft(view: ReactViewGroup, viewId: Int): Unit { + view.nextFocusLeftId = viewId + } + + @ReactProp(name = "nextFocusRight", defaultInt = View.NO_ID) + public open fun nextFocusRight(view: ReactViewGroup, viewId: Int): Unit { + view.nextFocusRightId = viewId + } + + @ReactProp(name = "nextFocusUp", defaultInt = View.NO_ID) + public open fun nextFocusUp(view: ReactViewGroup, viewId: Int): Unit { + view.nextFocusUpId = viewId + } + + @ReactPropGroup( + names = + [ + ViewProps.BORDER_RADIUS, + ViewProps.BORDER_TOP_LEFT_RADIUS, + ViewProps.BORDER_TOP_RIGHT_RADIUS, + ViewProps.BORDER_BOTTOM_RIGHT_RADIUS, + ViewProps.BORDER_BOTTOM_LEFT_RADIUS, + ViewProps.BORDER_TOP_START_RADIUS, + ViewProps.BORDER_TOP_END_RADIUS, + ViewProps.BORDER_BOTTOM_START_RADIUS, + ViewProps.BORDER_BOTTOM_END_RADIUS, + ViewProps.BORDER_END_END_RADIUS, + ViewProps.BORDER_END_START_RADIUS, + ViewProps.BORDER_START_END_RADIUS, + ViewProps.BORDER_START_START_RADIUS]) + public open fun setBorderRadius( + view: ReactViewGroup, + index: Int, + rawBorderRadius: Dynamic + ): Unit { + var borderRadius = setFromDynamic(rawBorderRadius) + + // We do not support percentage border radii on Paper in order to be consistent with iOS (to + // avoid developer surprise if it works on one platform but not another). + if (ViewUtil.getUIManagerType(view) != UIManagerType.FABRIC && + borderRadius?.type == LengthPercentageType.PERCENT) { + borderRadius = null + } + BackgroundStyleApplicator.setBorderRadius(view, BorderRadiusProp.values()[index], borderRadius) + } + + @Deprecated( + "Use setBorderRadius(ReactViewGroup, int, Dynamic) instead.", + ReplaceWith( + "setBorderRadius(view, index, DynamicFromObject(borderRadius))", + "com.facebook.react.bridge.DynamicFromObject.DynamicFromObject")) + public open fun setBorderRadius(view: ReactViewGroup, index: Int, borderRadius: Float): Unit { + this.setBorderRadius(view, index, DynamicFromObject(borderRadius)) + } + + @ReactProp(name = "borderStyle") + public open fun setBorderStyle(view: ReactViewGroup, borderStyle: String?): Unit { + val parsedBorderStyle = borderStyle?.let { BorderStyle.fromString(it) } + BackgroundStyleApplicator.setBorderStyle(view, parsedBorderStyle) + } + + @ReactProp(name = "hitSlop") + public open fun setHitSlop(view: ReactViewGroup, hitSlop: Dynamic): Unit { + when (hitSlop.type) { + ReadableType.Map -> { + val hitSlopMap = hitSlop.asMap() + view.setHitSlopRect( + Rect( + getPixels(hitSlopMap, "left"), + getPixels(hitSlopMap, "top"), + getPixels(hitSlopMap, "right"), + getPixels(hitSlopMap, "bottom"))) + } + + ReadableType.Number -> { + val hitSlopValue = hitSlop.asDouble().dpToPx().toInt() + view.setHitSlopRect(Rect(hitSlopValue, hitSlopValue, hitSlopValue, hitSlopValue)) + } + + ReadableType.Null -> view.setHitSlopRect(null) + else -> { + FLog.w(ReactConstants.TAG, "Invalid type for 'hitSlop' value ${hitSlop.type}") + view.setHitSlopRect(null) + } + } + } + + private fun getPixels(map: ReadableMap, key: String): Int = + if (map.hasKey(key)) { + map.getDouble(key).dpToPx().toInt() + } else { + 0 + } + + @ReactProp(name = ViewProps.POINTER_EVENTS) + public open fun setPointerEvents(view: ReactViewGroup, pointerEventsStr: String?): Unit { + view.setPointerEvents(parsePointerEvents(pointerEventsStr)) + } + + @ReactProp(name = "nativeBackgroundAndroid") + public open fun setNativeBackground(view: ReactViewGroup, background: ReadableMap?): Unit { + val translucentBg = + background?.let { ReactDrawableHelper.createDrawableFromJSDescription(view.context, it) } + BackgroundStyleApplicator.setFeedbackUnderlay(view, translucentBg) + } + + @ReactProp(name = "nativeForegroundAndroid") + public open fun setNativeForeground(view: ReactViewGroup, foreground: ReadableMap?): Unit { + view.foreground = + foreground?.let { ReactDrawableHelper.createDrawableFromJSDescription(view.context, it) } + } + + @ReactProp(name = ViewProps.NEEDS_OFFSCREEN_ALPHA_COMPOSITING) + public open fun setNeedsOffscreenAlphaCompositing( + view: ReactViewGroup, + needsOffscreenAlphaCompositing: Boolean + ): Unit { + view.setNeedsOffscreenAlphaCompositing(needsOffscreenAlphaCompositing) + } + + @ReactPropGroup( + names = + [ + ViewProps.BORDER_WIDTH, + ViewProps.BORDER_LEFT_WIDTH, + ViewProps.BORDER_RIGHT_WIDTH, + ViewProps.BORDER_TOP_WIDTH, + ViewProps.BORDER_BOTTOM_WIDTH, + ViewProps.BORDER_START_WIDTH, + ViewProps.BORDER_END_WIDTH], + defaultFloat = Float.NaN) + public open fun setBorderWidth(view: ReactViewGroup, index: Int, width: Float): Unit { + BackgroundStyleApplicator.setBorderWidth(view, LogicalEdge.values()[index], width) + } + + @ReactPropGroup( + names = + [ + ViewProps.BORDER_COLOR, + ViewProps.BORDER_LEFT_COLOR, + ViewProps.BORDER_RIGHT_COLOR, + ViewProps.BORDER_TOP_COLOR, + ViewProps.BORDER_BOTTOM_COLOR, + ViewProps.BORDER_START_COLOR, + ViewProps.BORDER_END_COLOR, + ViewProps.BORDER_BLOCK_COLOR, + ViewProps.BORDER_BLOCK_END_COLOR, + ViewProps.BORDER_BLOCK_START_COLOR], + customType = "Color") + public open fun setBorderColor(view: ReactViewGroup, index: Int, color: Int?): Unit { + BackgroundStyleApplicator.setBorderColor( + view, LogicalEdge.fromSpacingType(SPACING_TYPES[index]), color) + } + + @ReactProp(name = ViewProps.COLLAPSABLE) + @Suppress("UNUSED_PARAMETER") + public open fun setCollapsable(view: ReactViewGroup?, collapsable: Boolean): Unit { + // no-op: it's here only so that "collapsable" property is exported to JS. The value is actually + // handled in NativeViewHierarchyOptimizer + } + + @ReactProp(name = ViewProps.COLLAPSABLE_CHILDREN) + @Suppress("UNUSED_PARAMETER") + public open fun setCollapsableChildren( + view: ReactViewGroup?, + collapsableChildren: Boolean + ): Unit { + // no-op: it's here only so that "collapsableChildren" property is exported to JS. + } + + @ReactProp(name = "focusable") + public open fun setFocusable(view: ReactViewGroup, focusable: Boolean): Unit { + if (focusable) { + view.setOnClickListener { + val eventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag((view.context as ReactContext), view.id) + eventDispatcher?.dispatchEvent( + ViewGroupClickEvent(UIManagerHelper.getSurfaceId(view.context), view.id)) + } + + // Clickable elements are focusable. On API 26, this is taken care by setClickable. + // Explicitly calling setFocusable here for backward compatibility. + view.isFocusable = true + } else { + view.setOnClickListener(null) + view.isClickable = false + // Don't set view.setFocusable(false) because we might still want it to be focusable for + // accessibility reasons + } + } + + @ReactProp(name = ViewProps.OVERFLOW) + public open fun setOverflow(view: ReactViewGroup, overflow: String): Unit { + view.overflow = overflow + } + + @ReactProp(name = "backfaceVisibility") + public open fun setBackfaceVisibility(view: ReactViewGroup, backfaceVisibility: String): Unit { + view.setBackfaceVisibility(backfaceVisibility) + } + + override fun setOpacity(view: ReactViewGroup, opacity: Float) { + view.setOpacityIfPossible(opacity) + } + + override fun setTransformProperty( + view: ReactViewGroup, + transforms: ReadableArray?, + transformOrigin: ReadableArray? + ) { + super.setTransformProperty(view, transforms, transformOrigin) + view.setBackfaceVisibilityDependantOpacity() + } + + @ReactProp(name = ViewProps.BOX_SHADOW, customType = "BoxShadow") + public open fun setBoxShadow(view: ReactViewGroup, shadows: ReadableArray?): Unit { + BackgroundStyleApplicator.setBoxShadow(view, shadows) + } + + override fun setBackgroundColor(view: ReactViewGroup, @ColorInt backgroundColor: Int) { + BackgroundStyleApplicator.setBackgroundColor(view, backgroundColor) + } + + override fun getName(): String = REACT_CLASS + + public override fun createViewInstance(context: ThemedReactContext): ReactViewGroup = + ReactViewGroup(context) + + override fun getCommandsMap(): Map = + mapOf( + HOTSPOT_UPDATE_KEY to CMD_HOTSPOT_UPDATE, + "setPressed" to CMD_SET_PRESSED, + ) + + @Deprecated("Deprecated in ViewManager") + override fun receiveCommand(root: ReactViewGroup, commandId: Int, args: ReadableArray?) { + when (commandId) { + CMD_HOTSPOT_UPDATE -> handleHotspotUpdate(root, args) + CMD_SET_PRESSED -> handleSetPressed(root, args) + else -> {} + } + } + + override fun receiveCommand(root: ReactViewGroup, commandId: String, args: ReadableArray?) { + when (commandId) { + HOTSPOT_UPDATE_KEY -> handleHotspotUpdate(root, args) + "setPressed" -> handleSetPressed(root, args) + else -> {} + } + } + + private fun handleSetPressed(root: ReactViewGroup, args: ReadableArray?) { + if (args?.size() != 1) { + throw JSApplicationIllegalArgumentException( + "Illegal number of arguments for 'setPressed' command") + } + root.isPressed = args.getBoolean(0) + } + + private fun handleHotspotUpdate(root: ReactViewGroup, args: ReadableArray?) { + if (args?.size() != 2) { + throw JSApplicationIllegalArgumentException( + "Illegal number of arguments for 'updateHotspot' command") + } + val x = args.getDouble(0).dpToPx() + val y = args.getDouble(1).dpToPx() + root.drawableHotspotChanged(x, y) + } + + public companion object { + public const val REACT_CLASS: String = ViewProps.VIEW_CLASS_NAME + private val SPACING_TYPES = + intArrayOf( + Spacing.ALL, + Spacing.LEFT, + Spacing.RIGHT, + Spacing.TOP, + Spacing.BOTTOM, + Spacing.START, + Spacing.END, + Spacing.BLOCK, + Spacing.BLOCK_END, + Spacing.BLOCK_START) + private const val CMD_HOTSPOT_UPDATE = 1 + private const val CMD_SET_PRESSED = 2 + private const val HOTSPOT_UPDATE_KEY = "hotspotUpdate" + } +} From be5e2b5827f4a961c2f14d80fbd96f8df0d003e8 Mon Sep 17 00:00:00 2001 From: Thomas Nardone Date: Thu, 26 Sep 2024 13:11:07 -0700 Subject: [PATCH 028/296] Redo "[RN][Android] Convert ReactViewGroup to Kotlin" (#46672) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46672 Changelog: [Internal] Reviewed By: javache Differential Revision: D63129923 fbshipit-source-id: 0734f564b38e3adba704edffe45511e671ae74ac --- .../ReactAndroid/api/ReactAndroid.api | 1 - .../react/views/modal/ReactModalHostView.kt | 16 +- .../react/views/view/ReactViewGroup.java | 966 ------------------ .../react/views/view/ReactViewGroup.kt | 844 +++++++++++++++ .../react/views/view/ReactViewManager.kt | 10 +- .../react/views/view/ReactViewGroupTest.kt | 79 ++ 6 files changed, 936 insertions(+), 980 deletions(-) delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt create mode 100644 packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/view/ReactViewGroupTest.kt diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 1f02b1355d8b6a..3e8222f3bd054d 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -8318,7 +8318,6 @@ public class com/facebook/react/views/view/ReactViewGroup : android/view/ViewGro public fun draw (Landroid/graphics/Canvas;)V protected fun drawChild (Landroid/graphics/Canvas;Landroid/view/View;J)Z protected fun getChildDrawingOrder (II)I - public fun getChildVisibleRect (Landroid/view/View;Landroid/graphics/Rect;Landroid/graphics/Point;)Z public fun getClippingRect (Landroid/graphics/Rect;)V public fun getHitSlopRect ()Landroid/graphics/Rect; public fun getOverflow ()Ljava/lang/String; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.kt index 316efcd44b177f..e6403c31fb16e2 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.kt @@ -432,21 +432,21 @@ public class ReactModalHostView(context: ThemedReactContext) : reactContext.reactApplicationContext.handleException(RuntimeException(t)) } - override fun onInterceptTouchEvent(event: MotionEvent): Boolean { + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { eventDispatcher?.let { eventDispatcher -> - jSTouchDispatcher.handleTouchEvent(event, eventDispatcher, reactContext) - jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, true) + jSTouchDispatcher.handleTouchEvent(ev, eventDispatcher, reactContext) + jSPointerDispatcher?.handleMotionEvent(ev, eventDispatcher, true) } - return super.onInterceptTouchEvent(event) + return super.onInterceptTouchEvent(ev) } @SuppressLint("ClickableViewAccessibility") - override fun onTouchEvent(event: MotionEvent): Boolean { + override fun onTouchEvent(ev: MotionEvent): Boolean { eventDispatcher?.let { eventDispatcher -> - jSTouchDispatcher.handleTouchEvent(event, eventDispatcher, reactContext) - jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, false) + jSTouchDispatcher.handleTouchEvent(ev, eventDispatcher, reactContext) + jSPointerDispatcher?.handleMotionEvent(ev, eventDispatcher, false) } - super.onTouchEvent(event) + super.onTouchEvent(ev) // In case when there is no children interested in handling touch event, we return true from // the root view in order to receive subsequent events related to that gesture return true diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java deleted file mode 100644 index 7f7a799c02e9b1..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java +++ /dev/null @@ -1,966 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.views.view; - -import static com.facebook.infer.annotation.Assertions.nullsafeFIXME; -import static com.facebook.react.common.ReactConstants.TAG; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.content.Context; -import android.graphics.BlendMode; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Path; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.os.Build; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewStructure; -import android.view.animation.Animation; -import androidx.annotation.Nullable; -import com.facebook.common.logging.FLog; -import com.facebook.infer.annotation.Assertions; -import com.facebook.infer.annotation.Nullsafe; -import com.facebook.react.R; -import com.facebook.react.bridge.ReactNoCrashSoftException; -import com.facebook.react.bridge.ReactSoftExceptionLogger; -import com.facebook.react.bridge.UiThreadUtil; -import com.facebook.react.common.annotations.VisibleForTesting; -import com.facebook.react.config.ReactFeatureFlags; -import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags; -import com.facebook.react.touch.OnInterceptTouchEventListener; -import com.facebook.react.touch.ReactHitSlopView; -import com.facebook.react.touch.ReactInterceptingViewGroup; -import com.facebook.react.uimanager.BackgroundStyleApplicator; -import com.facebook.react.uimanager.LengthPercentage; -import com.facebook.react.uimanager.LengthPercentageType; -import com.facebook.react.uimanager.MeasureSpecAssertions; -import com.facebook.react.uimanager.PixelUtil; -import com.facebook.react.uimanager.PointerEvents; -import com.facebook.react.uimanager.ReactClippingProhibitedView; -import com.facebook.react.uimanager.ReactClippingViewGroup; -import com.facebook.react.uimanager.ReactClippingViewGroupHelper; -import com.facebook.react.uimanager.ReactOverflowViewWithInset; -import com.facebook.react.uimanager.ReactPointerEventsView; -import com.facebook.react.uimanager.ReactZIndexedViewGroup; -import com.facebook.react.uimanager.ViewGroupDrawingOrderHelper; -import com.facebook.react.uimanager.common.UIManagerType; -import com.facebook.react.uimanager.common.ViewUtil; -import com.facebook.react.uimanager.drawable.CSSBackgroundDrawable; -import com.facebook.react.uimanager.style.BorderRadiusProp; -import com.facebook.react.uimanager.style.BorderStyle; -import com.facebook.react.uimanager.style.LogicalEdge; -import com.facebook.react.uimanager.style.Overflow; - -/** - * Backing for a React View. Has support for borders, but since borders aren't common, lazy - * initializes most of the storage needed for them. - */ -@Nullsafe(Nullsafe.Mode.LOCAL) -public class ReactViewGroup extends ViewGroup - implements ReactInterceptingViewGroup, - ReactClippingViewGroup, - ReactPointerEventsView, - ReactHitSlopView, - ReactZIndexedViewGroup, - ReactOverflowViewWithInset { - - private static final int ARRAY_CAPACITY_INCREMENT = 12; - private static final int DEFAULT_BACKGROUND_COLOR = Color.TRANSPARENT; - private static final LayoutParams sDefaultLayoutParam = new ViewGroup.LayoutParams(0, 0); - private final Rect mOverflowInset = new Rect(); - /* should only be used in {@link #updateClippingToRect} */ - private static final Rect sHelperRect = new Rect(); - - /** - * This listener will be set for child views when removeClippedSubview property is enabled. When - * children layout is updated, it will call {@link #updateSubviewClipStatus} to notify parent view - * about that fact so that view can be attached/detached if necessary. - * - *

TODO(7728005): Attach/detach views in batch - once per frame in case when multiple children - * update their layout. - */ - private static final class ChildrenLayoutChangeListener implements View.OnLayoutChangeListener { - - private final ReactViewGroup mParent; - - private ChildrenLayoutChangeListener(ReactViewGroup parent) { - mParent = parent; - } - - @Override - public void onLayoutChange( - View v, - int left, - int top, - int right, - int bottom, - int oldLeft, - int oldTop, - int oldRight, - int oldBottom) { - if (mParent.getRemoveClippedSubviews()) { - mParent.updateSubviewClipStatus(v); - } - } - } - - // Following properties are here to support the option {@code removeClippedSubviews}. This is a - // temporary optimization/hack that is mainly applicable to the large list of images. The way - // it's implemented is that we store an additional array of children in view node. We selectively - // remove some of the views (detach) from it while still storing them in that additional array. - // We override all possible add methods for {@link ViewGroup} so that we can control this process - // whenever the option is set. We also override {@link ViewGroup#getChildAt} and - // {@link ViewGroup#getChildCount} so those methods may return views that are not attached. - // This is risky but allows us to perform a correct cleanup in {@link NativeViewHierarchyManager}. - private boolean mRemoveClippedSubviews; - private @Nullable View[] mAllChildren; - private int mAllChildrenCount; - private @Nullable Rect mClippingRect; - private @Nullable Rect mHitSlopRect; - private Overflow mOverflow; - private PointerEvents mPointerEvents; - private @Nullable ChildrenLayoutChangeListener mChildrenLayoutChangeListener; - private @Nullable CSSBackgroundDrawable mCSSBackgroundDrawable; - private @Nullable OnInterceptTouchEventListener mOnInterceptTouchEventListener; - private boolean mNeedsOffscreenAlphaCompositing; - private @Nullable ViewGroupDrawingOrderHelper mDrawingOrderHelper; - private @Nullable Path mPath; - private float mBackfaceOpacity; - private String mBackfaceVisibility; - - public ReactViewGroup(Context context) { - super(context); - initView(); - } - - /** - * Set all default values here as opposed to in the constructor or field defaults. It is important - * that these properties are set during the constructor, but also on-demand whenever an existing - * ReactTextView is recycled. - */ - private void initView() { - setClipChildren(false); - - mRemoveClippedSubviews = false; - mAllChildren = null; - mAllChildrenCount = 0; - mClippingRect = null; - mHitSlopRect = null; - mOverflow = Overflow.VISIBLE; - mPointerEvents = PointerEvents.AUTO; - mChildrenLayoutChangeListener = null; - mCSSBackgroundDrawable = null; - mOnInterceptTouchEventListener = null; - mNeedsOffscreenAlphaCompositing = false; - mDrawingOrderHelper = null; - mPath = null; - mBackfaceOpacity = 1.f; - mBackfaceVisibility = "visible"; - } - - /* package */ void recycleView() { - // Remove dangling listeners - if (mAllChildren != null && mChildrenLayoutChangeListener != null) { - for (int i = 0; i < mAllChildrenCount; i++) { - mAllChildren[i].removeOnLayoutChangeListener(mChildrenLayoutChangeListener); - } - } - - // Set default field values - initView(); - mOverflowInset.setEmpty(); - sHelperRect.setEmpty(); - - // Remove any children - removeAllViews(); - - // Reset background, borders - updateBackgroundDrawable(null); - - resetPointerEvents(); - } - - private ViewGroupDrawingOrderHelper getDrawingOrderHelper() { - if (mDrawingOrderHelper == null) { - mDrawingOrderHelper = new ViewGroupDrawingOrderHelper(this); - } - return mDrawingOrderHelper; - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - MeasureSpecAssertions.assertExplicitMeasureSpec(widthMeasureSpec, heightMeasureSpec); - - setMeasuredDimension( - MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec)); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - // No-op since UIManagerModule handles actually laying out children. - } - - @Override - @SuppressLint("MissingSuperCall") - public void requestLayout() { - // No-op, terminate `requestLayout` here, UIManagerModule handles laying out children and - // `layout` is called on all RN-managed views by `NativeViewHierarchyManager` - } - - @TargetApi(23) - @Override - public void dispatchProvideStructure(ViewStructure structure) { - try { - super.dispatchProvideStructure(structure); - } catch (NullPointerException e) { - FLog.e(TAG, "NullPointerException when executing dispatchProvideStructure", e); - } - } - - @Override - public void setBackgroundColor(int color) { - BackgroundStyleApplicator.setBackgroundColor(this, color); - } - - @Deprecated(since = "0.76.0", forRemoval = true) - public void setTranslucentBackgroundDrawable(@Nullable Drawable background) { - BackgroundStyleApplicator.setFeedbackUnderlay(this, background); - } - - @Override - public void setOnInterceptTouchEventListener(OnInterceptTouchEventListener listener) { - mOnInterceptTouchEventListener = listener; - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - if (mOnInterceptTouchEventListener != null - && mOnInterceptTouchEventListener.onInterceptTouchEvent(this, ev)) { - return true; - } - // We intercept the touch event if the children are not supposed to receive it. - if (!PointerEvents.canChildrenBeTouchTarget(mPointerEvents)) { - return true; - } - return super.onInterceptTouchEvent(ev); - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - // We do not accept the touch event if this view is not supposed to receive it. - if (!PointerEvents.canBeTouchTarget(mPointerEvents)) { - return false; - } - // The root view always assumes any view that was tapped wants the touch - // and sends the event to JS as such. - // We don't need to do bubbling in native (it's already happening in JS). - // For an explanation of bubbling and capturing, see - // http://javascript.info/tutorial/bubbling-and-capturing#capturing - return true; - } - - @Override - public boolean onHoverEvent(MotionEvent event) { - if (ReactFeatureFlags.dispatchPointerEvents) { - // Match the logic from onTouchEvent if pointer events are enabled - return PointerEvents.canBeTouchTarget(mPointerEvents); - } - return super.onHoverEvent(event); - } - - @Override - public boolean dispatchGenericMotionEvent(MotionEvent ev) { - // We do not dispatch the motion event if its children are not supposed to receive it - if (!PointerEvents.canChildrenBeTouchTarget(mPointerEvents)) { - return false; - } - - return super.dispatchGenericMotionEvent(ev); - } - - /** - * We override this to allow developers to determine whether they need offscreen alpha compositing - * or not. See the documentation of needsOffscreenAlphaCompositing in View.js. - */ - @Override - public boolean hasOverlappingRendering() { - return mNeedsOffscreenAlphaCompositing; - } - - /** See the documentation of needsOffscreenAlphaCompositing in View.js. */ - public void setNeedsOffscreenAlphaCompositing(boolean needsOffscreenAlphaCompositing) { - mNeedsOffscreenAlphaCompositing = needsOffscreenAlphaCompositing; - } - - public void setBorderWidth(int position, float width) { - BackgroundStyleApplicator.setBorderWidth( - this, LogicalEdge.values()[position], PixelUtil.toDIPFromPixel(width)); - } - - public void setBorderColor(int position, @Nullable Integer color) { - BackgroundStyleApplicator.setBorderColor(this, LogicalEdge.values()[position], color); - } - - /** - * @deprecated Use {@link #setBorderRadius(BorderRadiusProp, Float)} instead. - */ - @Deprecated(since = "0.75.0", forRemoval = true) - public void setBorderRadius(float borderRadius) { - setBorderRadius(borderRadius, BorderRadiusProp.BORDER_RADIUS.ordinal()); - } - - /** - * @deprecated Use {@link #setBorderRadius(BorderRadiusProp, Float)} instead. - */ - @Deprecated(since = "0.75.0", forRemoval = true) - public void setBorderRadius(float borderRadius, int position) { - @Nullable - LengthPercentage radius = - Float.isNaN(borderRadius) - ? null - : new LengthPercentage(borderRadius, LengthPercentageType.POINT); - BackgroundStyleApplicator.setBorderRadius(this, BorderRadiusProp.values()[position], radius); - } - - public void setBorderRadius(BorderRadiusProp property, @Nullable LengthPercentage borderRadius) { - BackgroundStyleApplicator.setBorderRadius(this, property, borderRadius); - } - - public void setBorderStyle(@Nullable String style) { - BackgroundStyleApplicator.setBorderStyle( - this, style == null ? null : BorderStyle.fromString(style)); - } - - @Override - public void setRemoveClippedSubviews(boolean removeClippedSubviews) { - if (removeClippedSubviews == mRemoveClippedSubviews) { - return; - } - mRemoveClippedSubviews = removeClippedSubviews; - if (removeClippedSubviews) { - mClippingRect = new Rect(); - ReactClippingViewGroupHelper.calculateClippingRect(this, mClippingRect); - mAllChildrenCount = getChildCount(); - int initialSize = Math.max(12, mAllChildrenCount); - mAllChildren = new View[initialSize]; - mChildrenLayoutChangeListener = new ChildrenLayoutChangeListener(this); - for (int i = 0; i < mAllChildrenCount; i++) { - View child = getChildAt(i); - mAllChildren[i] = child; - child.addOnLayoutChangeListener(mChildrenLayoutChangeListener); - } - updateClippingRect(); - } else { - // Add all clipped views back, deallocate additional arrays, remove layoutChangeListener - Assertions.assertNotNull(mClippingRect); - Assertions.assertNotNull(mAllChildren); - Assertions.assertNotNull(mChildrenLayoutChangeListener); - for (int i = 0; i < mAllChildrenCount; i++) { - mAllChildren[i].removeOnLayoutChangeListener(mChildrenLayoutChangeListener); - } - getDrawingRect(mClippingRect); - updateClippingToRect(mClippingRect); - mAllChildren = null; - mClippingRect = null; - mAllChildrenCount = 0; - mChildrenLayoutChangeListener = null; - } - } - - @Override - public boolean getRemoveClippedSubviews() { - return mRemoveClippedSubviews; - } - - @Override - public void getClippingRect(Rect outClippingRect) { - outClippingRect.set(nullsafeFIXME(mClippingRect, "Fix in Kotlin")); - } - - @Override - public void updateClippingRect() { - if (!mRemoveClippedSubviews) { - return; - } - - Assertions.assertNotNull(mClippingRect); - Assertions.assertNotNull(mAllChildren); - - ReactClippingViewGroupHelper.calculateClippingRect(this, mClippingRect); - updateClippingToRect(mClippingRect); - } - - private void updateClippingToRect(Rect clippingRect) { - Assertions.assertNotNull(mAllChildren); - int clippedSoFar = 0; - for (int i = 0; i < mAllChildrenCount; i++) { - updateSubviewClipStatus(clippingRect, i, clippedSoFar); - if (mAllChildren[i].getParent() == null) { - clippedSoFar++; - } - } - } - - private void updateSubviewClipStatus(Rect clippingRect, int idx, int clippedSoFar) { - UiThreadUtil.assertOnUiThread(); - - View child = Assertions.assertNotNull(mAllChildren)[idx]; - sHelperRect.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom()); - boolean intersects = - clippingRect.intersects( - sHelperRect.left, sHelperRect.top, sHelperRect.right, sHelperRect.bottom); - boolean needUpdateClippingRecursive = false; - // We never want to clip children that are being animated, as this can easily break layout : - // when layout animation changes size and/or position of views contained inside a listview that - // clips offscreen children, we need to ensure that, when view exits the viewport, final size - // and position is set prior to removing the view from its listview parent. - // Otherwise, when view gets re-attached again, i.e when it re-enters the viewport after scroll, - // it won't be size and located properly. - Animation animation = child.getAnimation(); - boolean isAnimating = animation != null && !animation.hasEnded(); - if (!intersects && child.getParent() != null && !isAnimating) { - // We can try saving on invalidate call here as the view that we remove is out of visible area - // therefore invalidation is not necessary. - removeViewInLayout(child); - needUpdateClippingRecursive = true; - } else if (intersects && child.getParent() == null) { - addViewInLayout(child, idx - clippedSoFar, sDefaultLayoutParam, true); - invalidate(); - needUpdateClippingRecursive = true; - } else if (intersects) { - // If there is any intersection we need to inform the child to update its clipping rect - needUpdateClippingRecursive = true; - } - if (needUpdateClippingRecursive) { - if (child instanceof ReactClippingViewGroup) { - // we don't use {@link sHelperRect} until the end of this loop, therefore it's safe - // to call this method that may write to the same {@link sHelperRect} object. - ReactClippingViewGroup clippingChild = (ReactClippingViewGroup) child; - if (clippingChild.getRemoveClippedSubviews()) { - clippingChild.updateClippingRect(); - } - } - } - } - - private void updateSubviewClipStatus(View subview) { - if (!mRemoveClippedSubviews || getParent() == null) { - return; - } - - Assertions.assertNotNull(mClippingRect); - Assertions.assertNotNull(mAllChildren); - - // do fast check whether intersect state changed - sHelperRect.set(subview.getLeft(), subview.getTop(), subview.getRight(), subview.getBottom()); - boolean intersects = - mClippingRect.intersects( - sHelperRect.left, sHelperRect.top, sHelperRect.right, sHelperRect.bottom); - - // If it was intersecting before, should be attached to the parent - boolean oldIntersects = (subview.getParent() != null); - - if (intersects != oldIntersects) { - int clippedSoFar = 0; - for (int i = 0; i < mAllChildrenCount; i++) { - if (mAllChildren[i] == subview) { - updateSubviewClipStatus(mClippingRect, i, clippedSoFar); - break; - } - if (mAllChildren[i].getParent() == null) { - clippedSoFar++; - } - } - } - } - - @Override - public boolean getChildVisibleRect(View child, Rect r, android.graphics.Point offset) { - return super.getChildVisibleRect(child, r, offset); - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - super.onSizeChanged(w, h, oldw, oldh); - if (mRemoveClippedSubviews) { - updateClippingRect(); - } - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - if (mRemoveClippedSubviews) { - updateClippingRect(); - } - } - - private boolean customDrawOrderDisabled() { - if (getId() == NO_ID) { - return false; - } - - // Custom draw order is disabled for Fabric. - return ViewUtil.getUIManagerType(getId()) == UIManagerType.FABRIC; - } - - private void handleAddView(View view) { - UiThreadUtil.assertOnUiThread(); - - if (!customDrawOrderDisabled()) { - getDrawingOrderHelper().handleAddView(view); - setChildrenDrawingOrderEnabled(getDrawingOrderHelper().shouldEnableCustomDrawingOrder()); - } else { - setChildrenDrawingOrderEnabled(false); - } - } - - private void handleRemoveView(@Nullable View view) { - UiThreadUtil.assertOnUiThread(); - - if (!customDrawOrderDisabled()) { - if (indexOfChild(view) == -1) { - return; - } - getDrawingOrderHelper().handleRemoveView(view); - setChildrenDrawingOrderEnabled(getDrawingOrderHelper().shouldEnableCustomDrawingOrder()); - } else { - setChildrenDrawingOrderEnabled(false); - } - } - - private void handleRemoveViews(int start, int count) { - int endIndex = start + count; - for (int index = start; index < endIndex; index++) { - if (index < getChildCount()) { - handleRemoveView(getChildAt(index)); - } - } - } - - @Override - public void addView(View child, int index, @Nullable ViewGroup.LayoutParams params) { - // This will get called for every overload of addView so there is not need to override every - // method. - handleAddView(child); - super.addView(child, index, params); - } - - @Override - protected boolean addViewInLayout( - View child, int index, LayoutParams params, boolean preventRequestLayout) { - handleAddView(child); - return super.addViewInLayout(child, index, params, preventRequestLayout); - } - - @Override - public void removeView(@Nullable View view) { - handleRemoveView(view); - super.removeView(view); - } - - @Override - public void removeViewAt(int index) { - handleRemoveView(getChildAt(index)); - super.removeViewAt(index); - } - - @Override - public void removeViewInLayout(View view) { - handleRemoveView(view); - super.removeViewInLayout(view); - } - - @Override - public void removeViewsInLayout(int start, int count) { - handleRemoveViews(start, count); - super.removeViewsInLayout(start, count); - } - - @Override - public void removeViews(int start, int count) { - handleRemoveViews(start, count); - super.removeViews(start, count); - } - - @Override - protected int getChildDrawingOrder(int childCount, int index) { - UiThreadUtil.assertOnUiThread(); - - if (!customDrawOrderDisabled()) { - return getDrawingOrderHelper().getChildDrawingOrder(childCount, index); - } else { - return index; - } - } - - @Override - public int getZIndexMappedChildIndex(int index) { - UiThreadUtil.assertOnUiThread(); - - if (!customDrawOrderDisabled() && getDrawingOrderHelper().shouldEnableCustomDrawingOrder()) { - return getDrawingOrderHelper().getChildDrawingOrder(getChildCount(), index); - } - - // Fabric behavior - return index; - } - - @Override - public void updateDrawingOrder() { - if (customDrawOrderDisabled()) { - return; - } - - getDrawingOrderHelper().update(); - setChildrenDrawingOrderEnabled(getDrawingOrderHelper().shouldEnableCustomDrawingOrder()); - invalidate(); - } - - @Override - public PointerEvents getPointerEvents() { - return mPointerEvents; - } - - @Override - protected void dispatchSetPressed(boolean pressed) { - // Prevents the ViewGroup from dispatching the pressed state - // to it's children. - } - - public void setPointerEvents(PointerEvents pointerEvents) { - mPointerEvents = pointerEvents; - } - - /*package*/ void resetPointerEvents() { - mPointerEvents = PointerEvents.AUTO; - } - - /*package*/ int getAllChildrenCount() { - return mAllChildrenCount; - } - - /*package*/ @Nullable - View getChildAtWithSubviewClippingEnabled(int index) { - return index >= 0 && index < mAllChildrenCount - ? Assertions.assertNotNull(mAllChildren)[index] - : null; - } - - /*package*/ void addViewWithSubviewClippingEnabled(View child, int index) { - addViewWithSubviewClippingEnabled(child, index, sDefaultLayoutParam); - } - - /*package*/ void addViewWithSubviewClippingEnabled( - final View child, int index, ViewGroup.LayoutParams params) { - Assertions.assertCondition(mRemoveClippedSubviews); - Assertions.assertNotNull(mClippingRect); - Assertions.assertNotNull(mAllChildren); - addInArray(child, index); - // we add view as "clipped" and then run {@link #updateSubviewClipStatus} to conditionally - // attach it - int clippedSoFar = 0; - for (int i = 0; i < index; i++) { - if (mAllChildren[i].getParent() == null) { - clippedSoFar++; - } - } - updateSubviewClipStatus(mClippingRect, index, clippedSoFar); - child.addOnLayoutChangeListener(mChildrenLayoutChangeListener); - - if (child instanceof ReactClippingProhibitedView) { - UiThreadUtil.runOnUiThread( - new Runnable() { - @Override - public void run() { - if (!child.isShown()) { - ReactSoftExceptionLogger.logSoftException( - TAG, - new ReactNoCrashSoftException( - "Child view has been added to Parent view in which it is clipped and not" - + " visible. This is not legal for this particular child view. Child: [" - + child.getId() - + "] " - + child.toString() - + " Parent: [" - + getId() - + "] " - + toString())); - } - } - }); - } - } - - /*package*/ void removeViewWithSubviewClippingEnabled(View view) { - UiThreadUtil.assertOnUiThread(); - - Assertions.assertCondition(mRemoveClippedSubviews); - Assertions.assertNotNull(mClippingRect); - Assertions.assertNotNull(mAllChildren); - view.removeOnLayoutChangeListener(mChildrenLayoutChangeListener); - int index = indexOfChildInAllChildren(view); - if (mAllChildren[index].getParent() != null) { - int clippedSoFar = 0; - for (int i = 0; i < index; i++) { - if (mAllChildren[i].getParent() == null) { - clippedSoFar++; - } - } - removeViewsInLayout(index - clippedSoFar, 1); - } - removeFromArray(index); - } - - /*package*/ void removeAllViewsWithSubviewClippingEnabled() { - Assertions.assertCondition(mRemoveClippedSubviews); - Assertions.assertNotNull(mAllChildren); - for (int i = 0; i < mAllChildrenCount; i++) { - mAllChildren[i].removeOnLayoutChangeListener(mChildrenLayoutChangeListener); - } - removeAllViewsInLayout(); - mAllChildrenCount = 0; - } - - private int indexOfChildInAllChildren(View child) { - final int count = mAllChildrenCount; - final View[] children = Assertions.assertNotNull(mAllChildren); - for (int i = 0; i < count; i++) { - if (children[i] == child) { - return i; - } - } - return -1; - } - - private void addInArray(View child, int index) { - View[] children = Assertions.assertNotNull(mAllChildren); - final int count = mAllChildrenCount; - final int size = children.length; - if (index == count) { - if (size == count) { - mAllChildren = new View[size + ARRAY_CAPACITY_INCREMENT]; - System.arraycopy(children, 0, mAllChildren, 0, size); - children = mAllChildren; - } - children[mAllChildrenCount++] = child; - } else if (index < count) { - if (size == count) { - mAllChildren = new View[size + ARRAY_CAPACITY_INCREMENT]; - System.arraycopy(children, 0, mAllChildren, 0, index); - System.arraycopy(children, index, mAllChildren, index + 1, count - index); - children = mAllChildren; - } else { - System.arraycopy(children, index, children, index + 1, count - index); - } - children[index] = child; - mAllChildrenCount++; - } else { - throw new IndexOutOfBoundsException("index=" + index + " count=" + count); - } - } - - private void removeFromArray(int index) { - final View[] children = Assertions.assertNotNull(mAllChildren); - final int count = mAllChildrenCount; - if (index == count - 1) { - children[--mAllChildrenCount] = null; - } else if (index >= 0 && index < count) { - System.arraycopy(children, index + 1, children, index, count - index - 1); - children[--mAllChildrenCount] = null; - } else { - throw new IndexOutOfBoundsException(); - } - } - - private boolean needsIsolatedLayer() { - if (!ReactNativeFeatureFlags.enableAndroidMixBlendModeProp()) { - return false; - } - - for (int i = 0; i < getChildCount(); i++) { - if (getChildAt(i).getTag(R.id.mix_blend_mode) != null) { - return true; - } - } - - return false; - } - - @VisibleForTesting - public int getBackgroundColor() { - @Nullable Integer color = BackgroundStyleApplicator.getBackgroundColor(this); - return color == null ? DEFAULT_BACKGROUND_COLOR : color; - } - - @Override - public @Nullable Rect getHitSlopRect() { - return mHitSlopRect; - } - - public void setHitSlopRect(@Nullable Rect rect) { - mHitSlopRect = rect; - } - - public void setOverflow(@Nullable String overflow) { - if (overflow == null) { - mOverflow = Overflow.VISIBLE; - } else { - @Nullable Overflow parsedOverflow = Overflow.fromString(overflow); - mOverflow = parsedOverflow == null ? Overflow.VISIBLE : parsedOverflow; - } - - invalidate(); - } - - @Override - public @Nullable String getOverflow() { - switch (mOverflow) { - case HIDDEN: - return "hidden"; - case SCROLL: - return "scroll"; - case VISIBLE: - return "visible"; - } - - return null; - } - - @Override - public void setOverflowInset(int left, int top, int right, int bottom) { - if (needsIsolatedLayer() - && (mOverflowInset.left != left - || mOverflowInset.top != top - || mOverflowInset.right != right - || mOverflowInset.bottom != bottom)) { - invalidate(); - } - mOverflowInset.set(left, top, right, bottom); - } - - @Override - public Rect getOverflowInset() { - return mOverflowInset; - } - - /** - * Set the background for the view or remove the background. It calls {@link - * #setBackground(Drawable)} or {@link #setBackgroundDrawable(Drawable)} based on the sdk version. - * - * @param drawable {@link Drawable} The Drawable to use as the background, or null to remove the - * background - */ - /* package */ void updateBackgroundDrawable(@Nullable Drawable drawable) { - super.setBackground(drawable); - } - - @Override - public void draw(Canvas canvas) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q - && ViewUtil.getUIManagerType(this) == UIManagerType.FABRIC - && needsIsolatedLayer()) { - - // Check if the view is a stacking context and has children, if it does, do the rendering - // offscreen and then composite back. This follows the idea of group isolation on blending - // https://www.w3.org/TR/compositing-1/#isolationblending - Rect overflowInset = getOverflowInset(); - canvas.saveLayer( - overflowInset.left, - overflowInset.top, - getWidth() + -overflowInset.right, - getHeight() + -overflowInset.bottom, - null); - super.draw(canvas); - canvas.restore(); - } else { - super.draw(canvas); - } - } - - @Override - protected void dispatchDraw(Canvas canvas) { - if (mOverflow != Overflow.VISIBLE || getTag(R.id.filter) != null) { - BackgroundStyleApplicator.clipToPaddingBox(this, canvas); - } - super.dispatchDraw(canvas); - } - - @Override - protected boolean drawChild(Canvas canvas, View child, long drawingTime) { - boolean drawWithZ = child.getElevation() > 0; - - if (drawWithZ) { - CanvasUtil.enableZ(canvas, true); - } - - BlendMode mixBlendMode = null; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && needsIsolatedLayer()) { - mixBlendMode = (BlendMode) child.getTag(R.id.mix_blend_mode); - if (mixBlendMode != null) { - Paint p = new Paint(); - p.setBlendMode(mixBlendMode); - Rect overflowInset = getOverflowInset(); - canvas.saveLayer( - overflowInset.left, - overflowInset.top, - getWidth() + -overflowInset.right, - getHeight() + -overflowInset.bottom, - p); - } - } - - boolean result = super.drawChild(canvas, child, drawingTime); - - if (mixBlendMode != null) { - canvas.restore(); - } - - if (drawWithZ) { - CanvasUtil.enableZ(canvas, false); - } - return result; - } - - public void setOpacityIfPossible(float opacity) { - mBackfaceOpacity = opacity; - setBackfaceVisibilityDependantOpacity(); - } - - public void setBackfaceVisibility(String backfaceVisibility) { - mBackfaceVisibility = backfaceVisibility; - setBackfaceVisibilityDependantOpacity(); - } - - public void setBackfaceVisibilityDependantOpacity() { - boolean isBackfaceVisible = mBackfaceVisibility.equals("visible"); - - if (isBackfaceVisible) { - setAlpha(mBackfaceOpacity); - return; - } - - float rotationX = getRotationX(); - float rotationY = getRotationY(); - - boolean isFrontfaceVisible = - (rotationX >= -90.f && rotationX < 90.f) && (rotationY >= -90.f && rotationY < 90.f); - - if (isFrontfaceVisible) { - setAlpha(mBackfaceOpacity); - return; - } - - setAlpha(0); - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt new file mode 100644 index 00000000000000..43e1e4f8ac80cd --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt @@ -0,0 +1,844 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +@file:Suppress("DEPRECATION") // ReactFeatureFlags + +package com.facebook.react.views.view + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.BlendMode +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.os.Build +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.view.ViewStructure +import com.facebook.common.logging.FLog +import com.facebook.infer.annotation.Assertions +import com.facebook.react.R +import com.facebook.react.bridge.ReactNoCrashSoftException +import com.facebook.react.bridge.ReactSoftExceptionLogger +import com.facebook.react.bridge.UiThreadUtil +import com.facebook.react.common.ReactConstants +import com.facebook.react.common.annotations.UnstableReactNativeAPI +import com.facebook.react.common.annotations.VisibleForTesting +import com.facebook.react.config.ReactFeatureFlags +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags +import com.facebook.react.touch.OnInterceptTouchEventListener +import com.facebook.react.touch.ReactHitSlopView +import com.facebook.react.touch.ReactInterceptingViewGroup +import com.facebook.react.uimanager.BackgroundStyleApplicator +import com.facebook.react.uimanager.LengthPercentage +import com.facebook.react.uimanager.LengthPercentageType +import com.facebook.react.uimanager.MeasureSpecAssertions +import com.facebook.react.uimanager.PixelUtil.pxToDp +import com.facebook.react.uimanager.PointerEvents +import com.facebook.react.uimanager.ReactClippingProhibitedView +import com.facebook.react.uimanager.ReactClippingViewGroup +import com.facebook.react.uimanager.ReactClippingViewGroupHelper +import com.facebook.react.uimanager.ReactOverflowViewWithInset +import com.facebook.react.uimanager.ReactPointerEventsView +import com.facebook.react.uimanager.ReactZIndexedViewGroup +import com.facebook.react.uimanager.ViewGroupDrawingOrderHelper +import com.facebook.react.uimanager.common.UIManagerType +import com.facebook.react.uimanager.common.ViewUtil +import com.facebook.react.uimanager.style.BorderRadiusProp +import com.facebook.react.uimanager.style.BorderStyle +import com.facebook.react.uimanager.style.LogicalEdge +import com.facebook.react.uimanager.style.Overflow +import kotlin.math.max + +/** + * Backing for a React View. Has support for borders, but since borders aren't common, lazy + * initializes most of the storage needed for them. + */ +@OptIn(UnstableReactNativeAPI::class) +public open class ReactViewGroup(context: Context) : + ViewGroup(context), + ReactInterceptingViewGroup, + ReactClippingViewGroup, + ReactPointerEventsView, + ReactHitSlopView, + ReactZIndexedViewGroup, + ReactOverflowViewWithInset { + + private companion object { + private const val ARRAY_CAPACITY_INCREMENT = 12 + private const val DEFAULT_BACKGROUND_COLOR = Color.TRANSPARENT + private val defaultLayoutParam = LayoutParams(0, 0) + } + + private val _overflowInset = Rect() + + /** + * This listener will be set for child views when removeClippedSubview property is enabled. When + * children layout is updated, it will call [updateSubviewClipStatus] to notify parent view about + * that fact so that view can be attached/detached if necessary. + * + * TODO(7728005): Attach/detach views in batch - once per frame in case when multiple children + * update their layout. + */ + private class ChildrenLayoutChangeListener(private val parent: ReactViewGroup) : + OnLayoutChangeListener { + override fun onLayoutChange( + v: View, + left: Int, + top: Int, + right: Int, + bottom: Int, + oldLeft: Int, + oldTop: Int, + oldRight: Int, + oldBottom: Int + ) { + if (parent.removeClippedSubviews) { + parent.updateSubviewClipStatus(v) + } + } + } + + // Following properties are here to support the option {@code removeClippedSubviews}. This is a + // temporary optimization/hack that is mainly applicable to the large list of images. The way + // it's implemented is that we store an additional array of children in view node. We selectively + // remove some of the views (detach) from it while still storing them in that additional array. + // We override all possible add methods for [ViewGroup] so that we can control this process + // whenever the option is set. We also override [ViewGroup#getChildAt] and + // [ViewGroup#getChildCount] so those methods may return views that are not attached. + // This is risky but allows us to perform a correct cleanup in [NativeViewHierarchyManager]. + private var _removeClippedSubviews = false + + private var allChildren: Array? = null + internal var allChildrenCount: Int = 0 + private set + + private var _clippingRect: Rect? = null + public override var hitSlopRect: Rect? = null + private var _overflow: Overflow = Overflow.VISIBLE + private var _pointerEvents: PointerEvents = PointerEvents.AUTO + private var childrenLayoutChangeListener: ChildrenLayoutChangeListener? = null + private var onInterceptTouchEventListener: OnInterceptTouchEventListener? = null + private var needsOffscreenAlphaCompositing = false + private var _drawingOrderHelper: ViewGroupDrawingOrderHelper? = null + private var backfaceOpacity = 1f + private var backfaceVisibility: String? = "visible" + + /** + * Set all default values here as opposed to in the constructor or field defaults. It is important + * that these properties are set during the constructor, but also on-demand whenever an existing + * ReactTextView is recycled. + */ + private fun initView() { + clipChildren = false + _removeClippedSubviews = false + allChildren = null + allChildrenCount = 0 + _clippingRect = null + hitSlopRect = null + _overflow = Overflow.VISIBLE + resetPointerEvents() + childrenLayoutChangeListener = null + onInterceptTouchEventListener = null + needsOffscreenAlphaCompositing = false + _drawingOrderHelper = null + backfaceOpacity = 1f + backfaceVisibility = "visible" + } + + internal open fun recycleView(): Unit { + // Remove dangling listeners + val children = allChildren + val listener = childrenLayoutChangeListener + if (children != null && listener != null) { + for (i in 0 until allChildrenCount) { + children[i]?.removeOnLayoutChangeListener(listener) + } + } + + // Set default field values + initView() + _overflowInset.setEmpty() + + // Remove any children + removeAllViews() + + // Reset background, borders + updateBackgroundDrawable(null) + resetPointerEvents() + } + + private val drawingOrderHelper: ViewGroupDrawingOrderHelper + get() { + return _drawingOrderHelper + ?: ViewGroupDrawingOrderHelper(this).also { _drawingOrderHelper = it } + } + + init { + initView() + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + MeasureSpecAssertions.assertExplicitMeasureSpec(widthMeasureSpec, heightMeasureSpec) + setMeasuredDimension( + MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec)) + } + + // No-op since UIManagerModule handles actually laying out children. + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int): Unit = Unit + + @SuppressLint("MissingSuperCall") + // No-op, terminate `requestLayout` here, UIManagerModule handles laying out children and `layout` + // is called on all RN-managed views by `NativeViewHierarchyManager` + override fun requestLayout(): Unit = Unit + + override fun dispatchProvideStructure(structure: ViewStructure) { + try { + super.dispatchProvideStructure(structure) + } catch (e: NullPointerException) { + FLog.e(ReactConstants.TAG, "NullPointerException when executing dispatchProvideStructure", e) + } + } + + override fun setBackgroundColor(color: Int) { + BackgroundStyleApplicator.setBackgroundColor(this, color) + } + + @Deprecated( + "Don't use setTranslucentBackgroundDrawable as it was deprecated in React Native 0.76.0.", + ReplaceWith( + "BackgroundStyleApplicator.setFeedbackUnderlay(this, background)", + "com.facebook.react.uimanager.BackgroundStyleApplicator")) + public open fun setTranslucentBackgroundDrawable(background: Drawable?): Unit { + BackgroundStyleApplicator.setFeedbackUnderlay(this, background) + } + + override fun setOnInterceptTouchEventListener(listener: OnInterceptTouchEventListener) { + onInterceptTouchEventListener = listener + } + + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { + if (onInterceptTouchEventListener?.onInterceptTouchEvent(this, ev) == true) { + return true + } + // We intercept the touch event if the children are not supposed to receive it. + return !PointerEvents.canChildrenBeTouchTarget(_pointerEvents) || + super.onInterceptTouchEvent(ev) + } + + override fun onTouchEvent(ev: MotionEvent): Boolean { + // We do not accept the touch event if this view is not supposed to receive it. + // The root view always assumes any view that was tapped wants the touch + // and sends the event to JS as such. + // We don't need to do bubbling in native (it's already happening in JS). + // For an explanation of bubbling and capturing, see + // http://javascript.info/tutorial/bubbling-and-capturing#capturing + return PointerEvents.canBeTouchTarget(_pointerEvents) + } + + override fun onHoverEvent(event: MotionEvent): Boolean = + if (ReactFeatureFlags.dispatchPointerEvents) { + // Match the logic from onTouchEvent if pointer events are enabled + PointerEvents.canBeTouchTarget(_pointerEvents) + } else { + super.onHoverEvent(event) + } + + override fun dispatchGenericMotionEvent(ev: MotionEvent): Boolean = + // We do not dispatch the motion event if its children are not supposed to receive it + PointerEvents.canChildrenBeTouchTarget(_pointerEvents) || super.dispatchGenericMotionEvent(ev) + + /** + * We override this to allow developers to determine whether they need offscreen alpha compositing + * or not. See the documentation of needsOffscreenAlphaCompositing in View.js. + */ + override fun hasOverlappingRendering(): Boolean = needsOffscreenAlphaCompositing + + /** See the documentation of needsOffscreenAlphaCompositing in View.js. */ + public open fun setNeedsOffscreenAlphaCompositing(needsOffscreenAlphaCompositing: Boolean): Unit { + this.needsOffscreenAlphaCompositing = needsOffscreenAlphaCompositing + } + + public open fun setBorderWidth(position: Int, width: Float): Unit { + BackgroundStyleApplicator.setBorderWidth(this, LogicalEdge.entries[position], width.pxToDp()) + } + + public open fun setBorderColor(position: Int, color: Int?): Unit { + BackgroundStyleApplicator.setBorderColor(this, LogicalEdge.entries[position], color) + } + + @Deprecated( + "Deprecated in React Native 0.75.0, in favor of setBorderRadius(BorderRadiusProp, Float)", + ReplaceWith( + "setBorderRadius(BorderRadiusProp.BORDER_RADIUS, borderRadius)", + "com.facebook.react.uimanager.style.BorderRadiusProp", + )) + @Suppress("DEPRECATION") + public open fun setBorderRadius(borderRadius: Float): Unit { + this.setBorderRadius(borderRadius, BorderRadiusProp.BORDER_RADIUS.ordinal) + } + + @Deprecated( + "Deprecated in React Native 0.75.0, in favor of setBorderRadius(BorderRadiusProp, Float)", + ReplaceWith( + "setBorderRadius(BorderRadiusProp.entries[position], borderRadius)", + "com.facebook.react.uimanager.style.BorderRadiusProp", + )) + public open fun setBorderRadius(borderRadius: Float, position: Int): Unit { + val radius = + when { + borderRadius.isNaN() -> null + else -> LengthPercentage(borderRadius, LengthPercentageType.POINT) + } + BackgroundStyleApplicator.setBorderRadius(this, BorderRadiusProp.entries[position], radius) + } + + public open fun setBorderRadius( + property: BorderRadiusProp, + borderRadius: LengthPercentage? + ): Unit { + BackgroundStyleApplicator.setBorderRadius(this, property, borderRadius) + } + + public open fun setBorderStyle(style: String?): Unit { + BackgroundStyleApplicator.setBorderStyle(this, style?.let { BorderStyle.fromString(style) }) + } + + override fun setRemoveClippedSubviews(removeClippedSubviews: Boolean) { + if (removeClippedSubviews == _removeClippedSubviews) { + return + } + _removeClippedSubviews = removeClippedSubviews + if (removeClippedSubviews) { + val clippingRect = Rect() + ReactClippingViewGroupHelper.calculateClippingRect(this, clippingRect) + allChildrenCount = childCount + val initialSize = max(12, allChildrenCount) + val children = arrayOfNulls(initialSize) + childrenLayoutChangeListener = ChildrenLayoutChangeListener(this) + for (i in 0 until allChildrenCount) { + children[i] = + getChildAt(i).apply { addOnLayoutChangeListener(childrenLayoutChangeListener) } + } + _clippingRect = clippingRect + allChildren = children + updateClippingRect() + } else { + // Add all clipped views back, deallocate additional arrays, remove layoutChangeListener + val clippingRect = checkNotNull(_clippingRect) + val children = checkNotNull(allChildren) + val listener = checkNotNull(childrenLayoutChangeListener) + for (i in 0 until allChildrenCount) { + children[i]?.removeOnLayoutChangeListener(listener) + } + getDrawingRect(clippingRect) + updateClippingToRect(clippingRect) + allChildren = null + _clippingRect = null + allChildrenCount = 0 + childrenLayoutChangeListener = null + } + } + + override fun getRemoveClippedSubviews(): Boolean = _removeClippedSubviews + + override fun getClippingRect(outClippingRect: Rect) { + outClippingRect.set( + checkNotNull(_clippingRect) { "getClippingRect called when removeClippedSubviews not set" }) + } + + override fun updateClippingRect() { + if (!_removeClippedSubviews) { + return + } + val clippingRect = checkNotNull(_clippingRect) + checkNotNull(allChildren) + ReactClippingViewGroupHelper.calculateClippingRect(this, clippingRect) + updateClippingToRect(clippingRect) + } + + private fun updateClippingToRect(clippingRect: Rect) { + val children = checkNotNull(allChildren) + var clippedSoFar = 0 + for (i in 0 until allChildrenCount) { + updateSubviewClipStatus(clippingRect, i, clippedSoFar) + if (children[i]?.parent == null) { + clippedSoFar++ + } + } + } + + private fun updateSubviewClipStatus(clippingRect: Rect, idx: Int, clippedSoFar: Int) { + UiThreadUtil.assertOnUiThread() + val child = checkNotNull(allChildren?.get(idx)) + val intersects = clippingRect.intersects(child.left, child.top, child.right, child.bottom) + var needUpdateClippingRecursive = false + // We never want to clip children that are being animated, as this can easily break layout : + // when layout animation changes size and/or position of views contained inside a listview that + // clips offscreen children, we need to ensure that, when view exits the viewport, final size + // and position is set prior to removing the view from its listview parent. + // Otherwise, when view gets re-attached again, i.e when it re-enters the viewport after scroll, + // it won't be size and located properly. + val animation = child.animation + val isAnimating = animation?.hasEnded() == false + if (!intersects && child.parent != null && !isAnimating) { + // We can try saving on invalidate call here as the view that we remove is out of visible + // area + // therefore invalidation is not necessary. + removeViewInLayout(child) + needUpdateClippingRecursive = true + } else if (intersects && child.parent == null) { + addViewInLayout(child, idx - clippedSoFar, defaultLayoutParam, true) + invalidate() + needUpdateClippingRecursive = true + } else if (intersects) { + // If there is any intersection we need to inform the child to update its clipping rect + needUpdateClippingRecursive = true + } + if (needUpdateClippingRecursive && + child is ReactClippingViewGroup && + child.removeClippedSubviews) { + child.updateClippingRect() + } + } + + private fun updateSubviewClipStatus(subview: View) { + if (!_removeClippedSubviews || parent == null) { + return + } + val clippingRect = checkNotNull(_clippingRect) + val children = checkNotNull(allChildren) + + // do fast check whether intersect state changed + val intersects = + clippingRect.intersects(subview.left, subview.top, subview.right, subview.bottom) + + // If it was intersecting before, should be attached to the parent + val oldIntersects = subview.parent != null + if (intersects != oldIntersects) { + var clippedSoFar = 0 + for (i in 0 until allChildrenCount) { + if (children[i] === subview) { + updateSubviewClipStatus(clippingRect, i, clippedSoFar) + break + } + if (children[i]?.parent == null) { + clippedSoFar++ + } + } + } + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + if (_removeClippedSubviews) { + updateClippingRect() + } + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + if (_removeClippedSubviews) { + updateClippingRect() + } + } + + private fun customDrawOrderDisabled(): Boolean = + // Custom draw order is disabled for Fabric. + id != NO_ID && ViewUtil.getUIManagerType(id) == UIManagerType.FABRIC + + private fun handleAddView(view: View) { + UiThreadUtil.assertOnUiThread() + if (!customDrawOrderDisabled()) { + drawingOrderHelper.handleAddView(view) + isChildrenDrawingOrderEnabled = drawingOrderHelper.shouldEnableCustomDrawingOrder() + } else { + isChildrenDrawingOrderEnabled = false + } + } + + private fun handleRemoveView(view: View?) { + UiThreadUtil.assertOnUiThread() + if (!customDrawOrderDisabled()) { + if (indexOfChild(view) == -1) { + return + } + drawingOrderHelper.handleRemoveView(view) + isChildrenDrawingOrderEnabled = drawingOrderHelper.shouldEnableCustomDrawingOrder() + } else { + isChildrenDrawingOrderEnabled = false + } + } + + private fun handleRemoveViews(start: Int, count: Int) { + val endIndex = start + count + for (index in start until endIndex) { + if (index < childCount) { + handleRemoveView(getChildAt(index)) + } + } + } + + override fun addView(child: View, index: Int, params: LayoutParams?) { + // This will get called for every overload of addView so there is not need to override every + // method. + handleAddView(child) + super.addView(child, index, params) + } + + override fun addViewInLayout( + child: View, + index: Int, + params: LayoutParams, + preventRequestLayout: Boolean + ): Boolean { + handleAddView(child) + return super.addViewInLayout(child, index, params, preventRequestLayout) + } + + override fun removeView(view: View?) { + handleRemoveView(view) + super.removeView(view) + } + + override fun removeViewAt(index: Int) { + handleRemoveView(getChildAt(index)) + super.removeViewAt(index) + } + + override fun removeViewInLayout(view: View) { + handleRemoveView(view) + super.removeViewInLayout(view) + } + + override fun removeViewsInLayout(start: Int, count: Int) { + handleRemoveViews(start, count) + super.removeViewsInLayout(start, count) + } + + override fun removeViews(start: Int, count: Int) { + handleRemoveViews(start, count) + super.removeViews(start, count) + } + + override fun getChildDrawingOrder(childCount: Int, index: Int): Int { + UiThreadUtil.assertOnUiThread() + return if (!customDrawOrderDisabled()) { + drawingOrderHelper.getChildDrawingOrder(childCount, index) + } else { + index + } + } + + override fun getZIndexMappedChildIndex(index: Int): Int { + UiThreadUtil.assertOnUiThread() + return if (!customDrawOrderDisabled() && drawingOrderHelper.shouldEnableCustomDrawingOrder()) { + drawingOrderHelper.getChildDrawingOrder(childCount, index) + } else { + // Fabric behavior + index + } + } + + override fun updateDrawingOrder() { + if (customDrawOrderDisabled()) { + return + } + drawingOrderHelper.update() + isChildrenDrawingOrderEnabled = drawingOrderHelper.shouldEnableCustomDrawingOrder() + invalidate() + } + + override fun getPointerEvents(): PointerEvents = _pointerEvents + + override fun dispatchSetPressed(pressed: Boolean) { + // Prevents the ViewGroup from dispatching the pressed state + // to it's children. + } + + public open fun setPointerEvents(pointerEvents: PointerEvents?): Unit { + if (pointerEvents != null) { + _pointerEvents = pointerEvents + } else { + resetPointerEvents() + } + } + + internal fun resetPointerEvents(): Unit { + _pointerEvents = PointerEvents.AUTO + } + + internal open fun getChildAtWithSubviewClippingEnabled(index: Int): View? = + if (index in 0 until allChildrenCount) { + checkNotNull(allChildren)[index] + } else { + null + } + + internal open fun addViewWithSubviewClippingEnabled(child: View, index: Int): Unit { + Assertions.assertCondition(_removeClippedSubviews) + val clippingRect = checkNotNull(_clippingRect) + val children = checkNotNull(allChildren) + addInArray(child, index) + // we add view as "clipped" and then run [updateSubviewClipStatus] to conditionally + // attach it + var clippedSoFar = 0 + for (i in 0 until index) { + if (children[i]?.parent == null) { + clippedSoFar++ + } + } + updateSubviewClipStatus(clippingRect, index, clippedSoFar) + child.addOnLayoutChangeListener(childrenLayoutChangeListener) + if (child is ReactClippingProhibitedView) { + UiThreadUtil.runOnUiThread { + if (!child.isShown) { + ReactSoftExceptionLogger.logSoftException( + ReactConstants.TAG, + ReactNoCrashSoftException( + """ + |Child view has been added to Parent view in which it is clipped and not + |visible. This is not legal for this particular child view. Child: [${child.id}] + | $child Parent: [$id] $parent""" + .trimMargin())) + } + } + } + } + + internal open fun removeViewWithSubviewClippingEnabled(view: View): Unit { + UiThreadUtil.assertOnUiThread() + Assertions.assertCondition(_removeClippedSubviews) + checkNotNull(_clippingRect) + val children = checkNotNull(allChildren) + view.removeOnLayoutChangeListener(childrenLayoutChangeListener) + val index = indexOfChildInAllChildren(view) + if (children[index]?.parent != null) { + var clippedSoFar = 0 + for (i in 0 until index) { + if (children[i]?.parent == null) { + clippedSoFar++ + } + } + removeViewsInLayout(index - clippedSoFar, 1) + } + removeFromArray(index) + } + + internal open fun removeAllViewsWithSubviewClippingEnabled(): Unit { + Assertions.assertCondition(_removeClippedSubviews) + val children = checkNotNull(allChildren) + for (i in 0 until allChildrenCount) { + children[i]?.removeOnLayoutChangeListener(childrenLayoutChangeListener) + } + removeAllViewsInLayout() + allChildrenCount = 0 + } + + private fun indexOfChildInAllChildren(child: View): Int { + val count = allChildrenCount + val children = checkNotNull(allChildren) + return (0 until count).firstOrNull { i -> children[i] === child } ?: -1 + } + + private fun addInArray(child: View, index: Int) { + val children = growAllChildrenIfNeeded(index) + children[index] = child + allChildrenCount++ + } + + /** + * Grow the [allChildren] array if it's run out of space + * + * @param insertIndex index where child is being inserted, must be <= [allChildrenCount] + * @return the non-null array that's backing [allChildren] after any potential resize, with a null + * slot at [insertIndex] + */ + private fun growAllChildrenIfNeeded(insertIndex: Int): Array { + val children = checkNotNull(allChildren) + val count = allChildrenCount + if (insertIndex > count) { + throw IndexOutOfBoundsException("index=$insertIndex count=$count") + } + if (children.size > count) { + // no need to resize, ensure index is free + if (insertIndex < count) { + System.arraycopy(children, insertIndex, children, insertIndex + 1, count - insertIndex) + } + return children + } + // need to resize the array + val newArray = + if (insertIndex == count) { + // inserting at the end of the array + children.copyOf(children.size + ARRAY_CAPACITY_INCREMENT) + } else { + // inserting within the array + arrayOfNulls(children.size + ARRAY_CAPACITY_INCREMENT).apply { + System.arraycopy(children, 0, this, 0, insertIndex) + System.arraycopy(children, insertIndex, this, insertIndex + 1, count - insertIndex) + } + } + allChildren = newArray + return newArray + } + + private fun removeFromArray(index: Int) { + val children = checkNotNull(allChildren) + val count = allChildrenCount + if (index == count - 1) { + children[--allChildrenCount] = null + } else if (index in 0 until count) { + System.arraycopy(children, index + 1, children, index, count - index - 1) + children[--allChildrenCount] = null + } else { + throw IndexOutOfBoundsException() + } + } + + private fun needsIsolatedLayer(): Boolean { + if (!ReactNativeFeatureFlags.enableAndroidMixBlendModeProp()) { + return false + } + return (0 until childCount).any { i -> getChildAt(i).getTag(R.id.mix_blend_mode) != null } + } + + @VisibleForTesting + protected open fun getBackgroundColor(): Int = + BackgroundStyleApplicator.getBackgroundColor(this) ?: DEFAULT_BACKGROUND_COLOR + + // TODO: convert to val + public open fun setOverflow(overflow: String?): Unit { + _overflow = + if (overflow == null) { + Overflow.VISIBLE + } else { + Overflow.fromString(overflow) ?: Overflow.VISIBLE + } + invalidate() + } + + override fun getOverflow(): String? = + when (_overflow) { + Overflow.HIDDEN -> "hidden" + Overflow.SCROLL -> "scroll" + Overflow.VISIBLE -> "visible" + } + + override fun setOverflowInset(left: Int, top: Int, right: Int, bottom: Int) { + if (needsIsolatedLayer() && + (_overflowInset.left != left || + _overflowInset.top != top || + _overflowInset.right != right || + _overflowInset.bottom != bottom)) { + invalidate() + } + _overflowInset.set(left, top, right, bottom) + } + + override fun getOverflowInset(): Rect = _overflowInset + + /** + * Set the background for the view or remove the background. It calls [setBackground(Drawable)] or + * [setBackgroundDrawable(Drawable)] based on the sdk version. + * + * @param drawable [Drawable] The Drawable to use as the background, or null to remove the + * background + */ + internal fun updateBackgroundDrawable(drawable: Drawable?): Unit { + super.setBackground(drawable) + } + + override fun draw(canvas: Canvas) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && + ViewUtil.getUIManagerType(this) == UIManagerType.FABRIC && + needsIsolatedLayer()) { + + // Check if the view is a stacking context and has children, if it does, do the rendering + // offscreen and then composite back. This follows the idea of group isolation on blending + // https://www.w3.org/TR/compositing-1/#isolationblending + val overflowInset = this.overflowInset + canvas.saveLayer( + overflowInset.left.toFloat(), + overflowInset.top.toFloat(), + (width + -overflowInset.right).toFloat(), + (height + -overflowInset.bottom).toFloat(), + null) + super.draw(canvas) + canvas.restore() + } else { + super.draw(canvas) + } + } + + override fun dispatchDraw(canvas: Canvas) { + if (_overflow != Overflow.VISIBLE || getTag(R.id.filter) != null) { + BackgroundStyleApplicator.clipToPaddingBox(this, canvas) + } + super.dispatchDraw(canvas) + } + + override fun drawChild(canvas: Canvas, child: View, drawingTime: Long): Boolean { + val drawWithZ = child.elevation > 0 + if (drawWithZ) { + CanvasUtil.enableZ(canvas, true) + } + var mixBlendMode: BlendMode? = null + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && needsIsolatedLayer()) { + mixBlendMode = child.getTag(R.id.mix_blend_mode) as? BlendMode + if (mixBlendMode != null) { + val p = Paint() + p.blendMode = mixBlendMode + val overflowInset = this.overflowInset + canvas.saveLayer( + overflowInset.left.toFloat(), + overflowInset.top.toFloat(), + (width + -overflowInset.right).toFloat(), + (height + -overflowInset.bottom).toFloat(), + p) + } + } + val result = super.drawChild(canvas, child, drawingTime) + if (mixBlendMode != null) { + canvas.restore() + } + if (drawWithZ) { + CanvasUtil.enableZ(canvas, false) + } + return result + } + + public open fun setOpacityIfPossible(opacity: Float): Unit { + backfaceOpacity = opacity + setBackfaceVisibilityDependantOpacity() + } + + public open fun setBackfaceVisibility(backfaceVisibility: String?): Unit { + this.backfaceVisibility = backfaceVisibility + setBackfaceVisibilityDependantOpacity() + } + + public open fun setBackfaceVisibilityDependantOpacity(): Unit { + val isBackfaceVisible = backfaceVisibility == "visible" + if (isBackfaceVisible) { + alpha = backfaceOpacity + return + } + val rotationX = rotationX + val rotationY = rotationY + val isFrontfaceVisible = + rotationX >= -90f && rotationX < 90f && rotationY >= -90f && rotationY < 90f + if (isFrontfaceVisible) { + alpha = backfaceOpacity + return + } + alpha = 0f + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt index 4b6dc1957e26f1..aac6e9347b7389 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt @@ -166,23 +166,23 @@ public open class ReactViewManager : ReactClippingViewManager() when (hitSlop.type) { ReadableType.Map -> { val hitSlopMap = hitSlop.asMap() - view.setHitSlopRect( + view.hitSlopRect = Rect( getPixels(hitSlopMap, "left"), getPixels(hitSlopMap, "top"), getPixels(hitSlopMap, "right"), - getPixels(hitSlopMap, "bottom"))) + getPixels(hitSlopMap, "bottom")) } ReadableType.Number -> { val hitSlopValue = hitSlop.asDouble().dpToPx().toInt() - view.setHitSlopRect(Rect(hitSlopValue, hitSlopValue, hitSlopValue, hitSlopValue)) + view.hitSlopRect = Rect(hitSlopValue, hitSlopValue, hitSlopValue, hitSlopValue) } - ReadableType.Null -> view.setHitSlopRect(null) + ReadableType.Null -> view.hitSlopRect = null else -> { FLog.w(ReactConstants.TAG, "Invalid type for 'hitSlop' value ${hitSlop.type}") - view.setHitSlopRect(null) + view.hitSlopRect = null } } } diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/view/ReactViewGroupTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/view/ReactViewGroupTest.kt new file mode 100644 index 00000000000000..588d92963e388f --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/view/ReactViewGroupTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.view + +import android.app.Activity +import android.content.Context +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class ReactViewGroupTest { + + private lateinit var context: Context + + @Before + fun setUp() { + ReactNativeFeatureFlagsForTests.setUp() + context = Robolectric.buildActivity(Activity::class.java).create().get() + } + + @Test + fun `View clipping - ensure allChildren properly resizes when adding views in sequence`() { + val rvg = ReactViewGroup(context) + rvg.left = 0 + rvg.right = 100 + rvg.top = 0 + rvg.bottom = 100 + FrameLayout(context).addView(rvg) + rvg.removeClippedSubviews = true + for (i in 0..20) { + rvg.addViewWithSubviewClippingEnabled(TestView(context, i * 10), i) + } + rvg.updateClippingRect() + assertThat(rvg.childCount).isEqualTo(10) + } + + @Test + fun `View clipping - ensure allChildren properly resizes when adding views out of sequence`() { + val rvg = ReactViewGroup(context) + rvg.left = 0 + rvg.right = 100 + rvg.top = 0 + rvg.bottom = 100 + FrameLayout(context).addView(rvg) + rvg.removeClippedSubviews = true + for (i in 0..10) { + rvg.addViewWithSubviewClippingEnabled(TestView(context, i * 10), i) + } + repeat(10) { rvg.addViewWithSubviewClippingEnabled(TestView(context, 90), 10) } + rvg.updateClippingRect() + assertThat(rvg.childCount).isEqualTo(20) + } +} + +class TestView(context: Context, yPos: Int) : View(context) { + init { + left = 0 + right = 100 + top = yPos + bottom = top + 10 + } +} + +class TestParent(context: Context) : ViewGroup(context) { + override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) = Unit +} From 9cd83fa9ab118943c4bd63e8b284c72a33ddac9b Mon Sep 17 00:00:00 2001 From: Thomas Nardone Date: Thu, 26 Sep 2024 13:11:07 -0700 Subject: [PATCH 029/296] ReactViewGroup post-conversion cleanup (#46668) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46668 Small tweaks post-Kotlin-conversion: - Make `overflow` a var - Replace `+ -` with `-` - Clean up properties and move up init block - Iterate over entire allChildren array to clean up listeners Changelog: [Internal] Reviewed By: cortinico Differential Revision: D63343964 fbshipit-source-id: 2e9022e2d7e54ac338d1003419d8959771f7f270 --- .../ReactAndroid/api/ReactAndroid.api | 1 + .../react/uimanager/ReactOverflowView.java | 25 ------- .../react/uimanager/ReactOverflowView.kt | 23 ++++++ .../react/views/view/ReactViewGroup.kt | 70 ++++++++----------- 4 files changed, 54 insertions(+), 65 deletions(-) delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactOverflowView.java create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactOverflowView.kt diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 3e8222f3bd054d..5b503227f03db8 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -4742,6 +4742,7 @@ public final class com/facebook/react/uimanager/ReactInvalidPropertyException : public abstract interface class com/facebook/react/uimanager/ReactOverflowView { public abstract fun getOverflow ()Ljava/lang/String; + public abstract fun setOverflow (Ljava/lang/String;)V } public abstract interface class com/facebook/react/uimanager/ReactOverflowViewWithInset : com/facebook/react/uimanager/ReactOverflowView { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactOverflowView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactOverflowView.java deleted file mode 100644 index 6958e361ac267e..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactOverflowView.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.uimanager; - -import android.view.View; -import androidx.annotation.Nullable; - -/** - * Interface that should be implemented by {@link View} subclasses that support {@code overflow} - * style. This allows the overflow information to be used by {@link TouchTargetHelper} to determine - * if a View is touchable. - */ -public interface ReactOverflowView { - /** - * Gets the overflow state of a view. If set, this should be one of {@link ViewProps#HIDDEN}, - * {@link ViewProps#VISIBLE} or {@link ViewProps#SCROLL}. - */ - @Nullable - String getOverflow(); -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactOverflowView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactOverflowView.kt new file mode 100644 index 00000000000000..415eeba7fd327d --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactOverflowView.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.uimanager + +/** + * Interface that should be implemented by [View] subclasses that support `overflow` style. This + * allows the overflow information to be used by [TouchTargetHelper] to determine if a View is + * touchable. + */ +public interface ReactOverflowView { + /** + * The overflow state of a view. If set, this should be one of: + * - [ViewProps.HIDDEN], + * - [ViewProps.VISIBLE] + * - [ViewProps.SCROLL]. + */ + public var overflow: String? +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt index 43e1e4f8ac80cd..900473014d1fa9 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt @@ -127,9 +127,16 @@ public open class ReactViewGroup(context: Context) : private var childrenLayoutChangeListener: ChildrenLayoutChangeListener? = null private var onInterceptTouchEventListener: OnInterceptTouchEventListener? = null private var needsOffscreenAlphaCompositing = false - private var _drawingOrderHelper: ViewGroupDrawingOrderHelper? = null private var backfaceOpacity = 1f private var backfaceVisibility: String? = "visible" + private var _drawingOrderHelper: ViewGroupDrawingOrderHelper? = null + private val drawingOrderHelper: ViewGroupDrawingOrderHelper + get() = + _drawingOrderHelper ?: ViewGroupDrawingOrderHelper(this).also { _drawingOrderHelper = it } + + init { + initView() + } /** * Set all default values here as opposed to in the constructor or field defaults. It is important @@ -158,9 +165,7 @@ public open class ReactViewGroup(context: Context) : val children = allChildren val listener = childrenLayoutChangeListener if (children != null && listener != null) { - for (i in 0 until allChildrenCount) { - children[i]?.removeOnLayoutChangeListener(listener) - } + children.forEach { child -> child?.removeOnLayoutChangeListener(listener) } } // Set default field values @@ -175,16 +180,6 @@ public open class ReactViewGroup(context: Context) : resetPointerEvents() } - private val drawingOrderHelper: ViewGroupDrawingOrderHelper - get() { - return _drawingOrderHelper - ?: ViewGroupDrawingOrderHelper(this).also { _drawingOrderHelper = it } - } - - init { - initView() - } - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { MeasureSpecAssertions.assertExplicitMeasureSpec(widthMeasureSpec, heightMeasureSpec) setMeasuredDimension( @@ -335,9 +330,7 @@ public open class ReactViewGroup(context: Context) : val clippingRect = checkNotNull(_clippingRect) val children = checkNotNull(allChildren) val listener = checkNotNull(childrenLayoutChangeListener) - for (i in 0 until allChildrenCount) { - children[i]?.removeOnLayoutChangeListener(listener) - } + children.forEach { child -> child?.removeOnLayoutChangeListener(listener) } getDrawingRect(clippingRect) updateClippingToRect(clippingRect) allChildren = null @@ -635,9 +628,7 @@ public open class ReactViewGroup(context: Context) : internal open fun removeAllViewsWithSubviewClippingEnabled(): Unit { Assertions.assertCondition(_removeClippedSubviews) val children = checkNotNull(allChildren) - for (i in 0 until allChildrenCount) { - children[i]?.removeOnLayoutChangeListener(childrenLayoutChangeListener) - } + children.forEach { child -> child?.removeOnLayoutChangeListener(childrenLayoutChangeListener) } removeAllViewsInLayout() allChildrenCount = 0 } @@ -714,23 +705,22 @@ public open class ReactViewGroup(context: Context) : protected open fun getBackgroundColor(): Int = BackgroundStyleApplicator.getBackgroundColor(this) ?: DEFAULT_BACKGROUND_COLOR - // TODO: convert to val - public open fun setOverflow(overflow: String?): Unit { - _overflow = - if (overflow == null) { - Overflow.VISIBLE - } else { - Overflow.fromString(overflow) ?: Overflow.VISIBLE + override var overflow: String? + get() = + when (_overflow) { + Overflow.HIDDEN -> "hidden" + Overflow.SCROLL -> "scroll" + Overflow.VISIBLE -> "visible" } - invalidate() - } - - override fun getOverflow(): String? = - when (_overflow) { - Overflow.HIDDEN -> "hidden" - Overflow.SCROLL -> "scroll" - Overflow.VISIBLE -> "visible" - } + set(value) { + _overflow = + if (value == null) { + Overflow.VISIBLE + } else { + Overflow.fromString(value) ?: Overflow.VISIBLE + } + invalidate() + } override fun setOverflowInset(left: Int, top: Int, right: Int, bottom: Int) { if (needsIsolatedLayer() && @@ -768,8 +758,8 @@ public open class ReactViewGroup(context: Context) : canvas.saveLayer( overflowInset.left.toFloat(), overflowInset.top.toFloat(), - (width + -overflowInset.right).toFloat(), - (height + -overflowInset.bottom).toFloat(), + (width - overflowInset.right).toFloat(), + (height - overflowInset.bottom).toFloat(), null) super.draw(canvas) canvas.restore() @@ -800,8 +790,8 @@ public open class ReactViewGroup(context: Context) : canvas.saveLayer( overflowInset.left.toFloat(), overflowInset.top.toFloat(), - (width + -overflowInset.right).toFloat(), - (height + -overflowInset.bottom).toFloat(), + (width - overflowInset.right).toFloat(), + (height - overflowInset.bottom).toFloat(), p) } } From fc9fcb597672957aba8cf9f2cc778ba4abebb09c Mon Sep 17 00:00:00 2001 From: David Vacca Date: Thu, 26 Sep 2024 14:23:04 -0700 Subject: [PATCH 030/296] Fix initialization of FeatureFlags in DefaultNewArchitectureEntryPoint (#46681) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46681 Fix initialization of FeatureFlags in DefaultNewArchitectureEntryPoint D60364016 introduced a bug, disabling Fabric and TurboModules when DefaultNewArchitectureEntryPoint is initialized with bridgless disabled changelog: [internal] internal Reviewed By: shwanton Differential Revision: D63474698 fbshipit-source-id: 8b5aaeeda564d3463d13448b0adc7e964015000e --- .../DefaultNewArchitectureEntryPoint.kt | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/defaults/DefaultNewArchitectureEntryPoint.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/defaults/DefaultNewArchitectureEntryPoint.kt index 42dc49a0ffdad3..f90ad0246a36f9 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/defaults/DefaultNewArchitectureEntryPoint.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/defaults/DefaultNewArchitectureEntryPoint.kt @@ -39,21 +39,19 @@ public object DefaultNewArchitectureEntryPoint { error(errorMessage) } - if (bridgelessEnabled) { - ReactNativeFeatureFlags.override( - object : ReactNativeNewArchitectureFeatureFlagsDefaults() { - override fun useFabricInterop(): Boolean = fabricEnabled + ReactNativeFeatureFlags.override( + object : ReactNativeNewArchitectureFeatureFlagsDefaults(bridgelessEnabled) { + override fun useFabricInterop(): Boolean = bridgelessEnabled || fabricEnabled - override fun enableFabricRenderer(): Boolean = fabricEnabled + override fun enableFabricRenderer(): Boolean = bridgelessEnabled || fabricEnabled - // We turn this feature flag to true for OSS to fix #44610 and #45126 and other - // similar bugs related to pressable. - override fun enableEventEmitterRetentionDuringGesturesOnAndroid(): Boolean = - fabricEnabled + // We turn this feature flag to true for OSS to fix #44610 and #45126 and other + // similar bugs related to pressable. + override fun enableEventEmitterRetentionDuringGesturesOnAndroid(): Boolean = + bridgelessEnabled || fabricEnabled - override fun useTurboModules(): Boolean = turboModulesEnabled - }) - } + override fun useTurboModules(): Boolean = bridgelessEnabled || turboModulesEnabled + }) privateFabricEnabled = fabricEnabled privateTurboModulesEnabled = turboModulesEnabled From 1dcaf823f5e7d9b114dd803ce3181aa0b8f827ad Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Thu, 26 Sep 2024 14:45:44 -0700 Subject: [PATCH 031/296] Fix conflicting assertions for click events (#46633) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46633 There is an assertion in the "basic click test" that [checks](https://github.com/facebook/react-native/blob/d6e0bc7/packages/rn-tester/js/examples/Experimental/W3CPointerEventPlatformTests/PointerEventClickTouch.js#L36-L40) that `isPrimary` is set to false. The same assertion suite also does a mouse event [validation](https://github.com/facebook/react-native/blob/d6e0bc7/packages/rn-tester/js/examples/Experimental/W3CPointerEventPlatformTests/PointerEventSupport.js#L201-L204) that `isPrimary` is set to true. This fixes the conflicting assertions, favoring the behavior observed on Firefox, which appears to be more closely aligned with the W3C spec. ## Changelog [General][Fixed] Fixed issues with W3C PointerEvents testsx Reviewed By: vincentriemer Differential Revision: D63336622 fbshipit-source-id: adbfe5e1bc2a5fd7db18ae067f39e719c19c8cda --- .../W3CPointerEventPlatformTests/PointerEventClickTouch.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/rn-tester/js/examples/Experimental/W3CPointerEventPlatformTests/PointerEventClickTouch.js b/packages/rn-tester/js/examples/Experimental/W3CPointerEventPlatformTests/PointerEventClickTouch.js index 69d448082c8a06..627d913990e015 100644 --- a/packages/rn-tester/js/examples/Experimental/W3CPointerEventPlatformTests/PointerEventClickTouch.js +++ b/packages/rn-tester/js/examples/Experimental/W3CPointerEventPlatformTests/PointerEventClickTouch.js @@ -33,11 +33,6 @@ function checkClickEventProperties( assert_equals(event.nativeEvent.tiltX, 0, 'default tiltX is 0'); assert_equals(event.nativeEvent.tiltY, 0, 'default tiltY is 0'); assert_equals(event.nativeEvent.twist, 0, 'default twist is 0'); - assert_equals( - event.nativeEvent.isPrimary, - false, - 'default isPrimary is false', - ); } function PointerEventClickTouchTestCase(props: PlatformTestComponentBaseProps) { From a886a2fa05ca2e0c8793c570d9f19caf4062b892 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Thu, 26 Sep 2024 15:50:26 -0700 Subject: [PATCH 032/296] Back out "ReactViewGroup post-conversion cleanup" Summary: Backing out the stack since a same crash that previously effected many apps appeared again, and there are changes soon landing that will add more conflicts. Reviewed By: Abbondanzo Differential Revision: D63493334 fbshipit-source-id: 175fc7b5b69aa2874c867e460ab102bb077a7cd8 --- .../ReactAndroid/api/ReactAndroid.api | 1 - .../react/uimanager/ReactOverflowView.java | 25 +++++++ .../react/uimanager/ReactOverflowView.kt | 23 ------ .../react/views/view/ReactViewGroup.kt | 70 +++++++++++-------- 4 files changed, 65 insertions(+), 54 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactOverflowView.java delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactOverflowView.kt diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 5b503227f03db8..3e8222f3bd054d 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -4742,7 +4742,6 @@ public final class com/facebook/react/uimanager/ReactInvalidPropertyException : public abstract interface class com/facebook/react/uimanager/ReactOverflowView { public abstract fun getOverflow ()Ljava/lang/String; - public abstract fun setOverflow (Ljava/lang/String;)V } public abstract interface class com/facebook/react/uimanager/ReactOverflowViewWithInset : com/facebook/react/uimanager/ReactOverflowView { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactOverflowView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactOverflowView.java new file mode 100644 index 00000000000000..6958e361ac267e --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactOverflowView.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.uimanager; + +import android.view.View; +import androidx.annotation.Nullable; + +/** + * Interface that should be implemented by {@link View} subclasses that support {@code overflow} + * style. This allows the overflow information to be used by {@link TouchTargetHelper} to determine + * if a View is touchable. + */ +public interface ReactOverflowView { + /** + * Gets the overflow state of a view. If set, this should be one of {@link ViewProps#HIDDEN}, + * {@link ViewProps#VISIBLE} or {@link ViewProps#SCROLL}. + */ + @Nullable + String getOverflow(); +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactOverflowView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactOverflowView.kt deleted file mode 100644 index 415eeba7fd327d..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactOverflowView.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.uimanager - -/** - * Interface that should be implemented by [View] subclasses that support `overflow` style. This - * allows the overflow information to be used by [TouchTargetHelper] to determine if a View is - * touchable. - */ -public interface ReactOverflowView { - /** - * The overflow state of a view. If set, this should be one of: - * - [ViewProps.HIDDEN], - * - [ViewProps.VISIBLE] - * - [ViewProps.SCROLL]. - */ - public var overflow: String? -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt index 900473014d1fa9..43e1e4f8ac80cd 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt @@ -127,16 +127,9 @@ public open class ReactViewGroup(context: Context) : private var childrenLayoutChangeListener: ChildrenLayoutChangeListener? = null private var onInterceptTouchEventListener: OnInterceptTouchEventListener? = null private var needsOffscreenAlphaCompositing = false + private var _drawingOrderHelper: ViewGroupDrawingOrderHelper? = null private var backfaceOpacity = 1f private var backfaceVisibility: String? = "visible" - private var _drawingOrderHelper: ViewGroupDrawingOrderHelper? = null - private val drawingOrderHelper: ViewGroupDrawingOrderHelper - get() = - _drawingOrderHelper ?: ViewGroupDrawingOrderHelper(this).also { _drawingOrderHelper = it } - - init { - initView() - } /** * Set all default values here as opposed to in the constructor or field defaults. It is important @@ -165,7 +158,9 @@ public open class ReactViewGroup(context: Context) : val children = allChildren val listener = childrenLayoutChangeListener if (children != null && listener != null) { - children.forEach { child -> child?.removeOnLayoutChangeListener(listener) } + for (i in 0 until allChildrenCount) { + children[i]?.removeOnLayoutChangeListener(listener) + } } // Set default field values @@ -180,6 +175,16 @@ public open class ReactViewGroup(context: Context) : resetPointerEvents() } + private val drawingOrderHelper: ViewGroupDrawingOrderHelper + get() { + return _drawingOrderHelper + ?: ViewGroupDrawingOrderHelper(this).also { _drawingOrderHelper = it } + } + + init { + initView() + } + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { MeasureSpecAssertions.assertExplicitMeasureSpec(widthMeasureSpec, heightMeasureSpec) setMeasuredDimension( @@ -330,7 +335,9 @@ public open class ReactViewGroup(context: Context) : val clippingRect = checkNotNull(_clippingRect) val children = checkNotNull(allChildren) val listener = checkNotNull(childrenLayoutChangeListener) - children.forEach { child -> child?.removeOnLayoutChangeListener(listener) } + for (i in 0 until allChildrenCount) { + children[i]?.removeOnLayoutChangeListener(listener) + } getDrawingRect(clippingRect) updateClippingToRect(clippingRect) allChildren = null @@ -628,7 +635,9 @@ public open class ReactViewGroup(context: Context) : internal open fun removeAllViewsWithSubviewClippingEnabled(): Unit { Assertions.assertCondition(_removeClippedSubviews) val children = checkNotNull(allChildren) - children.forEach { child -> child?.removeOnLayoutChangeListener(childrenLayoutChangeListener) } + for (i in 0 until allChildrenCount) { + children[i]?.removeOnLayoutChangeListener(childrenLayoutChangeListener) + } removeAllViewsInLayout() allChildrenCount = 0 } @@ -705,22 +714,23 @@ public open class ReactViewGroup(context: Context) : protected open fun getBackgroundColor(): Int = BackgroundStyleApplicator.getBackgroundColor(this) ?: DEFAULT_BACKGROUND_COLOR - override var overflow: String? - get() = - when (_overflow) { - Overflow.HIDDEN -> "hidden" - Overflow.SCROLL -> "scroll" - Overflow.VISIBLE -> "visible" + // TODO: convert to val + public open fun setOverflow(overflow: String?): Unit { + _overflow = + if (overflow == null) { + Overflow.VISIBLE + } else { + Overflow.fromString(overflow) ?: Overflow.VISIBLE } - set(value) { - _overflow = - if (value == null) { - Overflow.VISIBLE - } else { - Overflow.fromString(value) ?: Overflow.VISIBLE - } - invalidate() - } + invalidate() + } + + override fun getOverflow(): String? = + when (_overflow) { + Overflow.HIDDEN -> "hidden" + Overflow.SCROLL -> "scroll" + Overflow.VISIBLE -> "visible" + } override fun setOverflowInset(left: Int, top: Int, right: Int, bottom: Int) { if (needsIsolatedLayer() && @@ -758,8 +768,8 @@ public open class ReactViewGroup(context: Context) : canvas.saveLayer( overflowInset.left.toFloat(), overflowInset.top.toFloat(), - (width - overflowInset.right).toFloat(), - (height - overflowInset.bottom).toFloat(), + (width + -overflowInset.right).toFloat(), + (height + -overflowInset.bottom).toFloat(), null) super.draw(canvas) canvas.restore() @@ -790,8 +800,8 @@ public open class ReactViewGroup(context: Context) : canvas.saveLayer( overflowInset.left.toFloat(), overflowInset.top.toFloat(), - (width - overflowInset.right).toFloat(), - (height - overflowInset.bottom).toFloat(), + (width + -overflowInset.right).toFloat(), + (height + -overflowInset.bottom).toFloat(), p) } } From 8d22e3b2490be0c5708d2bee94bdfd0637f28ef8 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Thu, 26 Sep 2024 15:50:26 -0700 Subject: [PATCH 033/296] Back out "Redo "[RN][Android] Convert ReactViewGroup to Kotlin"" Summary: Backing out the stack since a same crash that previously effected many apps appeared again, and there are changes soon landing that will add more conflicts. Reviewed By: Abbondanzo Differential Revision: D63493331 fbshipit-source-id: 44658ffc99eb4ebc947f95bb6e6bde105ac88c93 --- .../ReactAndroid/api/ReactAndroid.api | 1 + .../react/views/modal/ReactModalHostView.kt | 16 +- .../react/views/view/ReactViewGroup.java | 966 ++++++++++++++++++ .../react/views/view/ReactViewGroup.kt | 844 --------------- .../react/views/view/ReactViewManager.kt | 10 +- .../react/views/view/ReactViewGroupTest.kt | 79 -- 6 files changed, 980 insertions(+), 936 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt delete mode 100644 packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/view/ReactViewGroupTest.kt diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 3e8222f3bd054d..1f02b1355d8b6a 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -8318,6 +8318,7 @@ public class com/facebook/react/views/view/ReactViewGroup : android/view/ViewGro public fun draw (Landroid/graphics/Canvas;)V protected fun drawChild (Landroid/graphics/Canvas;Landroid/view/View;J)Z protected fun getChildDrawingOrder (II)I + public fun getChildVisibleRect (Landroid/view/View;Landroid/graphics/Rect;Landroid/graphics/Point;)Z public fun getClippingRect (Landroid/graphics/Rect;)V public fun getHitSlopRect ()Landroid/graphics/Rect; public fun getOverflow ()Ljava/lang/String; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.kt index e6403c31fb16e2..316efcd44b177f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.kt @@ -432,21 +432,21 @@ public class ReactModalHostView(context: ThemedReactContext) : reactContext.reactApplicationContext.handleException(RuntimeException(t)) } - override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { + override fun onInterceptTouchEvent(event: MotionEvent): Boolean { eventDispatcher?.let { eventDispatcher -> - jSTouchDispatcher.handleTouchEvent(ev, eventDispatcher, reactContext) - jSPointerDispatcher?.handleMotionEvent(ev, eventDispatcher, true) + jSTouchDispatcher.handleTouchEvent(event, eventDispatcher, reactContext) + jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, true) } - return super.onInterceptTouchEvent(ev) + return super.onInterceptTouchEvent(event) } @SuppressLint("ClickableViewAccessibility") - override fun onTouchEvent(ev: MotionEvent): Boolean { + override fun onTouchEvent(event: MotionEvent): Boolean { eventDispatcher?.let { eventDispatcher -> - jSTouchDispatcher.handleTouchEvent(ev, eventDispatcher, reactContext) - jSPointerDispatcher?.handleMotionEvent(ev, eventDispatcher, false) + jSTouchDispatcher.handleTouchEvent(event, eventDispatcher, reactContext) + jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, false) } - super.onTouchEvent(ev) + super.onTouchEvent(event) // In case when there is no children interested in handling touch event, we return true from // the root view in order to receive subsequent events related to that gesture return true diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java new file mode 100644 index 00000000000000..7f7a799c02e9b1 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java @@ -0,0 +1,966 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.view; + +import static com.facebook.infer.annotation.Assertions.nullsafeFIXME; +import static com.facebook.react.common.ReactConstants.TAG; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.BlendMode; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewStructure; +import android.view.animation.Animation; +import androidx.annotation.Nullable; +import com.facebook.common.logging.FLog; +import com.facebook.infer.annotation.Assertions; +import com.facebook.infer.annotation.Nullsafe; +import com.facebook.react.R; +import com.facebook.react.bridge.ReactNoCrashSoftException; +import com.facebook.react.bridge.ReactSoftExceptionLogger; +import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.common.annotations.VisibleForTesting; +import com.facebook.react.config.ReactFeatureFlags; +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags; +import com.facebook.react.touch.OnInterceptTouchEventListener; +import com.facebook.react.touch.ReactHitSlopView; +import com.facebook.react.touch.ReactInterceptingViewGroup; +import com.facebook.react.uimanager.BackgroundStyleApplicator; +import com.facebook.react.uimanager.LengthPercentage; +import com.facebook.react.uimanager.LengthPercentageType; +import com.facebook.react.uimanager.MeasureSpecAssertions; +import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.uimanager.PointerEvents; +import com.facebook.react.uimanager.ReactClippingProhibitedView; +import com.facebook.react.uimanager.ReactClippingViewGroup; +import com.facebook.react.uimanager.ReactClippingViewGroupHelper; +import com.facebook.react.uimanager.ReactOverflowViewWithInset; +import com.facebook.react.uimanager.ReactPointerEventsView; +import com.facebook.react.uimanager.ReactZIndexedViewGroup; +import com.facebook.react.uimanager.ViewGroupDrawingOrderHelper; +import com.facebook.react.uimanager.common.UIManagerType; +import com.facebook.react.uimanager.common.ViewUtil; +import com.facebook.react.uimanager.drawable.CSSBackgroundDrawable; +import com.facebook.react.uimanager.style.BorderRadiusProp; +import com.facebook.react.uimanager.style.BorderStyle; +import com.facebook.react.uimanager.style.LogicalEdge; +import com.facebook.react.uimanager.style.Overflow; + +/** + * Backing for a React View. Has support for borders, but since borders aren't common, lazy + * initializes most of the storage needed for them. + */ +@Nullsafe(Nullsafe.Mode.LOCAL) +public class ReactViewGroup extends ViewGroup + implements ReactInterceptingViewGroup, + ReactClippingViewGroup, + ReactPointerEventsView, + ReactHitSlopView, + ReactZIndexedViewGroup, + ReactOverflowViewWithInset { + + private static final int ARRAY_CAPACITY_INCREMENT = 12; + private static final int DEFAULT_BACKGROUND_COLOR = Color.TRANSPARENT; + private static final LayoutParams sDefaultLayoutParam = new ViewGroup.LayoutParams(0, 0); + private final Rect mOverflowInset = new Rect(); + /* should only be used in {@link #updateClippingToRect} */ + private static final Rect sHelperRect = new Rect(); + + /** + * This listener will be set for child views when removeClippedSubview property is enabled. When + * children layout is updated, it will call {@link #updateSubviewClipStatus} to notify parent view + * about that fact so that view can be attached/detached if necessary. + * + *

TODO(7728005): Attach/detach views in batch - once per frame in case when multiple children + * update their layout. + */ + private static final class ChildrenLayoutChangeListener implements View.OnLayoutChangeListener { + + private final ReactViewGroup mParent; + + private ChildrenLayoutChangeListener(ReactViewGroup parent) { + mParent = parent; + } + + @Override + public void onLayoutChange( + View v, + int left, + int top, + int right, + int bottom, + int oldLeft, + int oldTop, + int oldRight, + int oldBottom) { + if (mParent.getRemoveClippedSubviews()) { + mParent.updateSubviewClipStatus(v); + } + } + } + + // Following properties are here to support the option {@code removeClippedSubviews}. This is a + // temporary optimization/hack that is mainly applicable to the large list of images. The way + // it's implemented is that we store an additional array of children in view node. We selectively + // remove some of the views (detach) from it while still storing them in that additional array. + // We override all possible add methods for {@link ViewGroup} so that we can control this process + // whenever the option is set. We also override {@link ViewGroup#getChildAt} and + // {@link ViewGroup#getChildCount} so those methods may return views that are not attached. + // This is risky but allows us to perform a correct cleanup in {@link NativeViewHierarchyManager}. + private boolean mRemoveClippedSubviews; + private @Nullable View[] mAllChildren; + private int mAllChildrenCount; + private @Nullable Rect mClippingRect; + private @Nullable Rect mHitSlopRect; + private Overflow mOverflow; + private PointerEvents mPointerEvents; + private @Nullable ChildrenLayoutChangeListener mChildrenLayoutChangeListener; + private @Nullable CSSBackgroundDrawable mCSSBackgroundDrawable; + private @Nullable OnInterceptTouchEventListener mOnInterceptTouchEventListener; + private boolean mNeedsOffscreenAlphaCompositing; + private @Nullable ViewGroupDrawingOrderHelper mDrawingOrderHelper; + private @Nullable Path mPath; + private float mBackfaceOpacity; + private String mBackfaceVisibility; + + public ReactViewGroup(Context context) { + super(context); + initView(); + } + + /** + * Set all default values here as opposed to in the constructor or field defaults. It is important + * that these properties are set during the constructor, but also on-demand whenever an existing + * ReactTextView is recycled. + */ + private void initView() { + setClipChildren(false); + + mRemoveClippedSubviews = false; + mAllChildren = null; + mAllChildrenCount = 0; + mClippingRect = null; + mHitSlopRect = null; + mOverflow = Overflow.VISIBLE; + mPointerEvents = PointerEvents.AUTO; + mChildrenLayoutChangeListener = null; + mCSSBackgroundDrawable = null; + mOnInterceptTouchEventListener = null; + mNeedsOffscreenAlphaCompositing = false; + mDrawingOrderHelper = null; + mPath = null; + mBackfaceOpacity = 1.f; + mBackfaceVisibility = "visible"; + } + + /* package */ void recycleView() { + // Remove dangling listeners + if (mAllChildren != null && mChildrenLayoutChangeListener != null) { + for (int i = 0; i < mAllChildrenCount; i++) { + mAllChildren[i].removeOnLayoutChangeListener(mChildrenLayoutChangeListener); + } + } + + // Set default field values + initView(); + mOverflowInset.setEmpty(); + sHelperRect.setEmpty(); + + // Remove any children + removeAllViews(); + + // Reset background, borders + updateBackgroundDrawable(null); + + resetPointerEvents(); + } + + private ViewGroupDrawingOrderHelper getDrawingOrderHelper() { + if (mDrawingOrderHelper == null) { + mDrawingOrderHelper = new ViewGroupDrawingOrderHelper(this); + } + return mDrawingOrderHelper; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + MeasureSpecAssertions.assertExplicitMeasureSpec(widthMeasureSpec, heightMeasureSpec); + + setMeasuredDimension( + MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec)); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + // No-op since UIManagerModule handles actually laying out children. + } + + @Override + @SuppressLint("MissingSuperCall") + public void requestLayout() { + // No-op, terminate `requestLayout` here, UIManagerModule handles laying out children and + // `layout` is called on all RN-managed views by `NativeViewHierarchyManager` + } + + @TargetApi(23) + @Override + public void dispatchProvideStructure(ViewStructure structure) { + try { + super.dispatchProvideStructure(structure); + } catch (NullPointerException e) { + FLog.e(TAG, "NullPointerException when executing dispatchProvideStructure", e); + } + } + + @Override + public void setBackgroundColor(int color) { + BackgroundStyleApplicator.setBackgroundColor(this, color); + } + + @Deprecated(since = "0.76.0", forRemoval = true) + public void setTranslucentBackgroundDrawable(@Nullable Drawable background) { + BackgroundStyleApplicator.setFeedbackUnderlay(this, background); + } + + @Override + public void setOnInterceptTouchEventListener(OnInterceptTouchEventListener listener) { + mOnInterceptTouchEventListener = listener; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (mOnInterceptTouchEventListener != null + && mOnInterceptTouchEventListener.onInterceptTouchEvent(this, ev)) { + return true; + } + // We intercept the touch event if the children are not supposed to receive it. + if (!PointerEvents.canChildrenBeTouchTarget(mPointerEvents)) { + return true; + } + return super.onInterceptTouchEvent(ev); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + // We do not accept the touch event if this view is not supposed to receive it. + if (!PointerEvents.canBeTouchTarget(mPointerEvents)) { + return false; + } + // The root view always assumes any view that was tapped wants the touch + // and sends the event to JS as such. + // We don't need to do bubbling in native (it's already happening in JS). + // For an explanation of bubbling and capturing, see + // http://javascript.info/tutorial/bubbling-and-capturing#capturing + return true; + } + + @Override + public boolean onHoverEvent(MotionEvent event) { + if (ReactFeatureFlags.dispatchPointerEvents) { + // Match the logic from onTouchEvent if pointer events are enabled + return PointerEvents.canBeTouchTarget(mPointerEvents); + } + return super.onHoverEvent(event); + } + + @Override + public boolean dispatchGenericMotionEvent(MotionEvent ev) { + // We do not dispatch the motion event if its children are not supposed to receive it + if (!PointerEvents.canChildrenBeTouchTarget(mPointerEvents)) { + return false; + } + + return super.dispatchGenericMotionEvent(ev); + } + + /** + * We override this to allow developers to determine whether they need offscreen alpha compositing + * or not. See the documentation of needsOffscreenAlphaCompositing in View.js. + */ + @Override + public boolean hasOverlappingRendering() { + return mNeedsOffscreenAlphaCompositing; + } + + /** See the documentation of needsOffscreenAlphaCompositing in View.js. */ + public void setNeedsOffscreenAlphaCompositing(boolean needsOffscreenAlphaCompositing) { + mNeedsOffscreenAlphaCompositing = needsOffscreenAlphaCompositing; + } + + public void setBorderWidth(int position, float width) { + BackgroundStyleApplicator.setBorderWidth( + this, LogicalEdge.values()[position], PixelUtil.toDIPFromPixel(width)); + } + + public void setBorderColor(int position, @Nullable Integer color) { + BackgroundStyleApplicator.setBorderColor(this, LogicalEdge.values()[position], color); + } + + /** + * @deprecated Use {@link #setBorderRadius(BorderRadiusProp, Float)} instead. + */ + @Deprecated(since = "0.75.0", forRemoval = true) + public void setBorderRadius(float borderRadius) { + setBorderRadius(borderRadius, BorderRadiusProp.BORDER_RADIUS.ordinal()); + } + + /** + * @deprecated Use {@link #setBorderRadius(BorderRadiusProp, Float)} instead. + */ + @Deprecated(since = "0.75.0", forRemoval = true) + public void setBorderRadius(float borderRadius, int position) { + @Nullable + LengthPercentage radius = + Float.isNaN(borderRadius) + ? null + : new LengthPercentage(borderRadius, LengthPercentageType.POINT); + BackgroundStyleApplicator.setBorderRadius(this, BorderRadiusProp.values()[position], radius); + } + + public void setBorderRadius(BorderRadiusProp property, @Nullable LengthPercentage borderRadius) { + BackgroundStyleApplicator.setBorderRadius(this, property, borderRadius); + } + + public void setBorderStyle(@Nullable String style) { + BackgroundStyleApplicator.setBorderStyle( + this, style == null ? null : BorderStyle.fromString(style)); + } + + @Override + public void setRemoveClippedSubviews(boolean removeClippedSubviews) { + if (removeClippedSubviews == mRemoveClippedSubviews) { + return; + } + mRemoveClippedSubviews = removeClippedSubviews; + if (removeClippedSubviews) { + mClippingRect = new Rect(); + ReactClippingViewGroupHelper.calculateClippingRect(this, mClippingRect); + mAllChildrenCount = getChildCount(); + int initialSize = Math.max(12, mAllChildrenCount); + mAllChildren = new View[initialSize]; + mChildrenLayoutChangeListener = new ChildrenLayoutChangeListener(this); + for (int i = 0; i < mAllChildrenCount; i++) { + View child = getChildAt(i); + mAllChildren[i] = child; + child.addOnLayoutChangeListener(mChildrenLayoutChangeListener); + } + updateClippingRect(); + } else { + // Add all clipped views back, deallocate additional arrays, remove layoutChangeListener + Assertions.assertNotNull(mClippingRect); + Assertions.assertNotNull(mAllChildren); + Assertions.assertNotNull(mChildrenLayoutChangeListener); + for (int i = 0; i < mAllChildrenCount; i++) { + mAllChildren[i].removeOnLayoutChangeListener(mChildrenLayoutChangeListener); + } + getDrawingRect(mClippingRect); + updateClippingToRect(mClippingRect); + mAllChildren = null; + mClippingRect = null; + mAllChildrenCount = 0; + mChildrenLayoutChangeListener = null; + } + } + + @Override + public boolean getRemoveClippedSubviews() { + return mRemoveClippedSubviews; + } + + @Override + public void getClippingRect(Rect outClippingRect) { + outClippingRect.set(nullsafeFIXME(mClippingRect, "Fix in Kotlin")); + } + + @Override + public void updateClippingRect() { + if (!mRemoveClippedSubviews) { + return; + } + + Assertions.assertNotNull(mClippingRect); + Assertions.assertNotNull(mAllChildren); + + ReactClippingViewGroupHelper.calculateClippingRect(this, mClippingRect); + updateClippingToRect(mClippingRect); + } + + private void updateClippingToRect(Rect clippingRect) { + Assertions.assertNotNull(mAllChildren); + int clippedSoFar = 0; + for (int i = 0; i < mAllChildrenCount; i++) { + updateSubviewClipStatus(clippingRect, i, clippedSoFar); + if (mAllChildren[i].getParent() == null) { + clippedSoFar++; + } + } + } + + private void updateSubviewClipStatus(Rect clippingRect, int idx, int clippedSoFar) { + UiThreadUtil.assertOnUiThread(); + + View child = Assertions.assertNotNull(mAllChildren)[idx]; + sHelperRect.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom()); + boolean intersects = + clippingRect.intersects( + sHelperRect.left, sHelperRect.top, sHelperRect.right, sHelperRect.bottom); + boolean needUpdateClippingRecursive = false; + // We never want to clip children that are being animated, as this can easily break layout : + // when layout animation changes size and/or position of views contained inside a listview that + // clips offscreen children, we need to ensure that, when view exits the viewport, final size + // and position is set prior to removing the view from its listview parent. + // Otherwise, when view gets re-attached again, i.e when it re-enters the viewport after scroll, + // it won't be size and located properly. + Animation animation = child.getAnimation(); + boolean isAnimating = animation != null && !animation.hasEnded(); + if (!intersects && child.getParent() != null && !isAnimating) { + // We can try saving on invalidate call here as the view that we remove is out of visible area + // therefore invalidation is not necessary. + removeViewInLayout(child); + needUpdateClippingRecursive = true; + } else if (intersects && child.getParent() == null) { + addViewInLayout(child, idx - clippedSoFar, sDefaultLayoutParam, true); + invalidate(); + needUpdateClippingRecursive = true; + } else if (intersects) { + // If there is any intersection we need to inform the child to update its clipping rect + needUpdateClippingRecursive = true; + } + if (needUpdateClippingRecursive) { + if (child instanceof ReactClippingViewGroup) { + // we don't use {@link sHelperRect} until the end of this loop, therefore it's safe + // to call this method that may write to the same {@link sHelperRect} object. + ReactClippingViewGroup clippingChild = (ReactClippingViewGroup) child; + if (clippingChild.getRemoveClippedSubviews()) { + clippingChild.updateClippingRect(); + } + } + } + } + + private void updateSubviewClipStatus(View subview) { + if (!mRemoveClippedSubviews || getParent() == null) { + return; + } + + Assertions.assertNotNull(mClippingRect); + Assertions.assertNotNull(mAllChildren); + + // do fast check whether intersect state changed + sHelperRect.set(subview.getLeft(), subview.getTop(), subview.getRight(), subview.getBottom()); + boolean intersects = + mClippingRect.intersects( + sHelperRect.left, sHelperRect.top, sHelperRect.right, sHelperRect.bottom); + + // If it was intersecting before, should be attached to the parent + boolean oldIntersects = (subview.getParent() != null); + + if (intersects != oldIntersects) { + int clippedSoFar = 0; + for (int i = 0; i < mAllChildrenCount; i++) { + if (mAllChildren[i] == subview) { + updateSubviewClipStatus(mClippingRect, i, clippedSoFar); + break; + } + if (mAllChildren[i].getParent() == null) { + clippedSoFar++; + } + } + } + } + + @Override + public boolean getChildVisibleRect(View child, Rect r, android.graphics.Point offset) { + return super.getChildVisibleRect(child, r, offset); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + if (mRemoveClippedSubviews) { + updateClippingRect(); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (mRemoveClippedSubviews) { + updateClippingRect(); + } + } + + private boolean customDrawOrderDisabled() { + if (getId() == NO_ID) { + return false; + } + + // Custom draw order is disabled for Fabric. + return ViewUtil.getUIManagerType(getId()) == UIManagerType.FABRIC; + } + + private void handleAddView(View view) { + UiThreadUtil.assertOnUiThread(); + + if (!customDrawOrderDisabled()) { + getDrawingOrderHelper().handleAddView(view); + setChildrenDrawingOrderEnabled(getDrawingOrderHelper().shouldEnableCustomDrawingOrder()); + } else { + setChildrenDrawingOrderEnabled(false); + } + } + + private void handleRemoveView(@Nullable View view) { + UiThreadUtil.assertOnUiThread(); + + if (!customDrawOrderDisabled()) { + if (indexOfChild(view) == -1) { + return; + } + getDrawingOrderHelper().handleRemoveView(view); + setChildrenDrawingOrderEnabled(getDrawingOrderHelper().shouldEnableCustomDrawingOrder()); + } else { + setChildrenDrawingOrderEnabled(false); + } + } + + private void handleRemoveViews(int start, int count) { + int endIndex = start + count; + for (int index = start; index < endIndex; index++) { + if (index < getChildCount()) { + handleRemoveView(getChildAt(index)); + } + } + } + + @Override + public void addView(View child, int index, @Nullable ViewGroup.LayoutParams params) { + // This will get called for every overload of addView so there is not need to override every + // method. + handleAddView(child); + super.addView(child, index, params); + } + + @Override + protected boolean addViewInLayout( + View child, int index, LayoutParams params, boolean preventRequestLayout) { + handleAddView(child); + return super.addViewInLayout(child, index, params, preventRequestLayout); + } + + @Override + public void removeView(@Nullable View view) { + handleRemoveView(view); + super.removeView(view); + } + + @Override + public void removeViewAt(int index) { + handleRemoveView(getChildAt(index)); + super.removeViewAt(index); + } + + @Override + public void removeViewInLayout(View view) { + handleRemoveView(view); + super.removeViewInLayout(view); + } + + @Override + public void removeViewsInLayout(int start, int count) { + handleRemoveViews(start, count); + super.removeViewsInLayout(start, count); + } + + @Override + public void removeViews(int start, int count) { + handleRemoveViews(start, count); + super.removeViews(start, count); + } + + @Override + protected int getChildDrawingOrder(int childCount, int index) { + UiThreadUtil.assertOnUiThread(); + + if (!customDrawOrderDisabled()) { + return getDrawingOrderHelper().getChildDrawingOrder(childCount, index); + } else { + return index; + } + } + + @Override + public int getZIndexMappedChildIndex(int index) { + UiThreadUtil.assertOnUiThread(); + + if (!customDrawOrderDisabled() && getDrawingOrderHelper().shouldEnableCustomDrawingOrder()) { + return getDrawingOrderHelper().getChildDrawingOrder(getChildCount(), index); + } + + // Fabric behavior + return index; + } + + @Override + public void updateDrawingOrder() { + if (customDrawOrderDisabled()) { + return; + } + + getDrawingOrderHelper().update(); + setChildrenDrawingOrderEnabled(getDrawingOrderHelper().shouldEnableCustomDrawingOrder()); + invalidate(); + } + + @Override + public PointerEvents getPointerEvents() { + return mPointerEvents; + } + + @Override + protected void dispatchSetPressed(boolean pressed) { + // Prevents the ViewGroup from dispatching the pressed state + // to it's children. + } + + public void setPointerEvents(PointerEvents pointerEvents) { + mPointerEvents = pointerEvents; + } + + /*package*/ void resetPointerEvents() { + mPointerEvents = PointerEvents.AUTO; + } + + /*package*/ int getAllChildrenCount() { + return mAllChildrenCount; + } + + /*package*/ @Nullable + View getChildAtWithSubviewClippingEnabled(int index) { + return index >= 0 && index < mAllChildrenCount + ? Assertions.assertNotNull(mAllChildren)[index] + : null; + } + + /*package*/ void addViewWithSubviewClippingEnabled(View child, int index) { + addViewWithSubviewClippingEnabled(child, index, sDefaultLayoutParam); + } + + /*package*/ void addViewWithSubviewClippingEnabled( + final View child, int index, ViewGroup.LayoutParams params) { + Assertions.assertCondition(mRemoveClippedSubviews); + Assertions.assertNotNull(mClippingRect); + Assertions.assertNotNull(mAllChildren); + addInArray(child, index); + // we add view as "clipped" and then run {@link #updateSubviewClipStatus} to conditionally + // attach it + int clippedSoFar = 0; + for (int i = 0; i < index; i++) { + if (mAllChildren[i].getParent() == null) { + clippedSoFar++; + } + } + updateSubviewClipStatus(mClippingRect, index, clippedSoFar); + child.addOnLayoutChangeListener(mChildrenLayoutChangeListener); + + if (child instanceof ReactClippingProhibitedView) { + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + if (!child.isShown()) { + ReactSoftExceptionLogger.logSoftException( + TAG, + new ReactNoCrashSoftException( + "Child view has been added to Parent view in which it is clipped and not" + + " visible. This is not legal for this particular child view. Child: [" + + child.getId() + + "] " + + child.toString() + + " Parent: [" + + getId() + + "] " + + toString())); + } + } + }); + } + } + + /*package*/ void removeViewWithSubviewClippingEnabled(View view) { + UiThreadUtil.assertOnUiThread(); + + Assertions.assertCondition(mRemoveClippedSubviews); + Assertions.assertNotNull(mClippingRect); + Assertions.assertNotNull(mAllChildren); + view.removeOnLayoutChangeListener(mChildrenLayoutChangeListener); + int index = indexOfChildInAllChildren(view); + if (mAllChildren[index].getParent() != null) { + int clippedSoFar = 0; + for (int i = 0; i < index; i++) { + if (mAllChildren[i].getParent() == null) { + clippedSoFar++; + } + } + removeViewsInLayout(index - clippedSoFar, 1); + } + removeFromArray(index); + } + + /*package*/ void removeAllViewsWithSubviewClippingEnabled() { + Assertions.assertCondition(mRemoveClippedSubviews); + Assertions.assertNotNull(mAllChildren); + for (int i = 0; i < mAllChildrenCount; i++) { + mAllChildren[i].removeOnLayoutChangeListener(mChildrenLayoutChangeListener); + } + removeAllViewsInLayout(); + mAllChildrenCount = 0; + } + + private int indexOfChildInAllChildren(View child) { + final int count = mAllChildrenCount; + final View[] children = Assertions.assertNotNull(mAllChildren); + for (int i = 0; i < count; i++) { + if (children[i] == child) { + return i; + } + } + return -1; + } + + private void addInArray(View child, int index) { + View[] children = Assertions.assertNotNull(mAllChildren); + final int count = mAllChildrenCount; + final int size = children.length; + if (index == count) { + if (size == count) { + mAllChildren = new View[size + ARRAY_CAPACITY_INCREMENT]; + System.arraycopy(children, 0, mAllChildren, 0, size); + children = mAllChildren; + } + children[mAllChildrenCount++] = child; + } else if (index < count) { + if (size == count) { + mAllChildren = new View[size + ARRAY_CAPACITY_INCREMENT]; + System.arraycopy(children, 0, mAllChildren, 0, index); + System.arraycopy(children, index, mAllChildren, index + 1, count - index); + children = mAllChildren; + } else { + System.arraycopy(children, index, children, index + 1, count - index); + } + children[index] = child; + mAllChildrenCount++; + } else { + throw new IndexOutOfBoundsException("index=" + index + " count=" + count); + } + } + + private void removeFromArray(int index) { + final View[] children = Assertions.assertNotNull(mAllChildren); + final int count = mAllChildrenCount; + if (index == count - 1) { + children[--mAllChildrenCount] = null; + } else if (index >= 0 && index < count) { + System.arraycopy(children, index + 1, children, index, count - index - 1); + children[--mAllChildrenCount] = null; + } else { + throw new IndexOutOfBoundsException(); + } + } + + private boolean needsIsolatedLayer() { + if (!ReactNativeFeatureFlags.enableAndroidMixBlendModeProp()) { + return false; + } + + for (int i = 0; i < getChildCount(); i++) { + if (getChildAt(i).getTag(R.id.mix_blend_mode) != null) { + return true; + } + } + + return false; + } + + @VisibleForTesting + public int getBackgroundColor() { + @Nullable Integer color = BackgroundStyleApplicator.getBackgroundColor(this); + return color == null ? DEFAULT_BACKGROUND_COLOR : color; + } + + @Override + public @Nullable Rect getHitSlopRect() { + return mHitSlopRect; + } + + public void setHitSlopRect(@Nullable Rect rect) { + mHitSlopRect = rect; + } + + public void setOverflow(@Nullable String overflow) { + if (overflow == null) { + mOverflow = Overflow.VISIBLE; + } else { + @Nullable Overflow parsedOverflow = Overflow.fromString(overflow); + mOverflow = parsedOverflow == null ? Overflow.VISIBLE : parsedOverflow; + } + + invalidate(); + } + + @Override + public @Nullable String getOverflow() { + switch (mOverflow) { + case HIDDEN: + return "hidden"; + case SCROLL: + return "scroll"; + case VISIBLE: + return "visible"; + } + + return null; + } + + @Override + public void setOverflowInset(int left, int top, int right, int bottom) { + if (needsIsolatedLayer() + && (mOverflowInset.left != left + || mOverflowInset.top != top + || mOverflowInset.right != right + || mOverflowInset.bottom != bottom)) { + invalidate(); + } + mOverflowInset.set(left, top, right, bottom); + } + + @Override + public Rect getOverflowInset() { + return mOverflowInset; + } + + /** + * Set the background for the view or remove the background. It calls {@link + * #setBackground(Drawable)} or {@link #setBackgroundDrawable(Drawable)} based on the sdk version. + * + * @param drawable {@link Drawable} The Drawable to use as the background, or null to remove the + * background + */ + /* package */ void updateBackgroundDrawable(@Nullable Drawable drawable) { + super.setBackground(drawable); + } + + @Override + public void draw(Canvas canvas) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + && ViewUtil.getUIManagerType(this) == UIManagerType.FABRIC + && needsIsolatedLayer()) { + + // Check if the view is a stacking context and has children, if it does, do the rendering + // offscreen and then composite back. This follows the idea of group isolation on blending + // https://www.w3.org/TR/compositing-1/#isolationblending + Rect overflowInset = getOverflowInset(); + canvas.saveLayer( + overflowInset.left, + overflowInset.top, + getWidth() + -overflowInset.right, + getHeight() + -overflowInset.bottom, + null); + super.draw(canvas); + canvas.restore(); + } else { + super.draw(canvas); + } + } + + @Override + protected void dispatchDraw(Canvas canvas) { + if (mOverflow != Overflow.VISIBLE || getTag(R.id.filter) != null) { + BackgroundStyleApplicator.clipToPaddingBox(this, canvas); + } + super.dispatchDraw(canvas); + } + + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + boolean drawWithZ = child.getElevation() > 0; + + if (drawWithZ) { + CanvasUtil.enableZ(canvas, true); + } + + BlendMode mixBlendMode = null; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && needsIsolatedLayer()) { + mixBlendMode = (BlendMode) child.getTag(R.id.mix_blend_mode); + if (mixBlendMode != null) { + Paint p = new Paint(); + p.setBlendMode(mixBlendMode); + Rect overflowInset = getOverflowInset(); + canvas.saveLayer( + overflowInset.left, + overflowInset.top, + getWidth() + -overflowInset.right, + getHeight() + -overflowInset.bottom, + p); + } + } + + boolean result = super.drawChild(canvas, child, drawingTime); + + if (mixBlendMode != null) { + canvas.restore(); + } + + if (drawWithZ) { + CanvasUtil.enableZ(canvas, false); + } + return result; + } + + public void setOpacityIfPossible(float opacity) { + mBackfaceOpacity = opacity; + setBackfaceVisibilityDependantOpacity(); + } + + public void setBackfaceVisibility(String backfaceVisibility) { + mBackfaceVisibility = backfaceVisibility; + setBackfaceVisibilityDependantOpacity(); + } + + public void setBackfaceVisibilityDependantOpacity() { + boolean isBackfaceVisible = mBackfaceVisibility.equals("visible"); + + if (isBackfaceVisible) { + setAlpha(mBackfaceOpacity); + return; + } + + float rotationX = getRotationX(); + float rotationY = getRotationY(); + + boolean isFrontfaceVisible = + (rotationX >= -90.f && rotationX < 90.f) && (rotationY >= -90.f && rotationY < 90.f); + + if (isFrontfaceVisible) { + setAlpha(mBackfaceOpacity); + return; + } + + setAlpha(0); + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt deleted file mode 100644 index 43e1e4f8ac80cd..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt +++ /dev/null @@ -1,844 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -@file:Suppress("DEPRECATION") // ReactFeatureFlags - -package com.facebook.react.views.view - -import android.annotation.SuppressLint -import android.content.Context -import android.graphics.BlendMode -import android.graphics.Canvas -import android.graphics.Color -import android.graphics.Paint -import android.graphics.Rect -import android.graphics.drawable.Drawable -import android.os.Build -import android.view.MotionEvent -import android.view.View -import android.view.ViewGroup -import android.view.ViewStructure -import com.facebook.common.logging.FLog -import com.facebook.infer.annotation.Assertions -import com.facebook.react.R -import com.facebook.react.bridge.ReactNoCrashSoftException -import com.facebook.react.bridge.ReactSoftExceptionLogger -import com.facebook.react.bridge.UiThreadUtil -import com.facebook.react.common.ReactConstants -import com.facebook.react.common.annotations.UnstableReactNativeAPI -import com.facebook.react.common.annotations.VisibleForTesting -import com.facebook.react.config.ReactFeatureFlags -import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags -import com.facebook.react.touch.OnInterceptTouchEventListener -import com.facebook.react.touch.ReactHitSlopView -import com.facebook.react.touch.ReactInterceptingViewGroup -import com.facebook.react.uimanager.BackgroundStyleApplicator -import com.facebook.react.uimanager.LengthPercentage -import com.facebook.react.uimanager.LengthPercentageType -import com.facebook.react.uimanager.MeasureSpecAssertions -import com.facebook.react.uimanager.PixelUtil.pxToDp -import com.facebook.react.uimanager.PointerEvents -import com.facebook.react.uimanager.ReactClippingProhibitedView -import com.facebook.react.uimanager.ReactClippingViewGroup -import com.facebook.react.uimanager.ReactClippingViewGroupHelper -import com.facebook.react.uimanager.ReactOverflowViewWithInset -import com.facebook.react.uimanager.ReactPointerEventsView -import com.facebook.react.uimanager.ReactZIndexedViewGroup -import com.facebook.react.uimanager.ViewGroupDrawingOrderHelper -import com.facebook.react.uimanager.common.UIManagerType -import com.facebook.react.uimanager.common.ViewUtil -import com.facebook.react.uimanager.style.BorderRadiusProp -import com.facebook.react.uimanager.style.BorderStyle -import com.facebook.react.uimanager.style.LogicalEdge -import com.facebook.react.uimanager.style.Overflow -import kotlin.math.max - -/** - * Backing for a React View. Has support for borders, but since borders aren't common, lazy - * initializes most of the storage needed for them. - */ -@OptIn(UnstableReactNativeAPI::class) -public open class ReactViewGroup(context: Context) : - ViewGroup(context), - ReactInterceptingViewGroup, - ReactClippingViewGroup, - ReactPointerEventsView, - ReactHitSlopView, - ReactZIndexedViewGroup, - ReactOverflowViewWithInset { - - private companion object { - private const val ARRAY_CAPACITY_INCREMENT = 12 - private const val DEFAULT_BACKGROUND_COLOR = Color.TRANSPARENT - private val defaultLayoutParam = LayoutParams(0, 0) - } - - private val _overflowInset = Rect() - - /** - * This listener will be set for child views when removeClippedSubview property is enabled. When - * children layout is updated, it will call [updateSubviewClipStatus] to notify parent view about - * that fact so that view can be attached/detached if necessary. - * - * TODO(7728005): Attach/detach views in batch - once per frame in case when multiple children - * update their layout. - */ - private class ChildrenLayoutChangeListener(private val parent: ReactViewGroup) : - OnLayoutChangeListener { - override fun onLayoutChange( - v: View, - left: Int, - top: Int, - right: Int, - bottom: Int, - oldLeft: Int, - oldTop: Int, - oldRight: Int, - oldBottom: Int - ) { - if (parent.removeClippedSubviews) { - parent.updateSubviewClipStatus(v) - } - } - } - - // Following properties are here to support the option {@code removeClippedSubviews}. This is a - // temporary optimization/hack that is mainly applicable to the large list of images. The way - // it's implemented is that we store an additional array of children in view node. We selectively - // remove some of the views (detach) from it while still storing them in that additional array. - // We override all possible add methods for [ViewGroup] so that we can control this process - // whenever the option is set. We also override [ViewGroup#getChildAt] and - // [ViewGroup#getChildCount] so those methods may return views that are not attached. - // This is risky but allows us to perform a correct cleanup in [NativeViewHierarchyManager]. - private var _removeClippedSubviews = false - - private var allChildren: Array? = null - internal var allChildrenCount: Int = 0 - private set - - private var _clippingRect: Rect? = null - public override var hitSlopRect: Rect? = null - private var _overflow: Overflow = Overflow.VISIBLE - private var _pointerEvents: PointerEvents = PointerEvents.AUTO - private var childrenLayoutChangeListener: ChildrenLayoutChangeListener? = null - private var onInterceptTouchEventListener: OnInterceptTouchEventListener? = null - private var needsOffscreenAlphaCompositing = false - private var _drawingOrderHelper: ViewGroupDrawingOrderHelper? = null - private var backfaceOpacity = 1f - private var backfaceVisibility: String? = "visible" - - /** - * Set all default values here as opposed to in the constructor or field defaults. It is important - * that these properties are set during the constructor, but also on-demand whenever an existing - * ReactTextView is recycled. - */ - private fun initView() { - clipChildren = false - _removeClippedSubviews = false - allChildren = null - allChildrenCount = 0 - _clippingRect = null - hitSlopRect = null - _overflow = Overflow.VISIBLE - resetPointerEvents() - childrenLayoutChangeListener = null - onInterceptTouchEventListener = null - needsOffscreenAlphaCompositing = false - _drawingOrderHelper = null - backfaceOpacity = 1f - backfaceVisibility = "visible" - } - - internal open fun recycleView(): Unit { - // Remove dangling listeners - val children = allChildren - val listener = childrenLayoutChangeListener - if (children != null && listener != null) { - for (i in 0 until allChildrenCount) { - children[i]?.removeOnLayoutChangeListener(listener) - } - } - - // Set default field values - initView() - _overflowInset.setEmpty() - - // Remove any children - removeAllViews() - - // Reset background, borders - updateBackgroundDrawable(null) - resetPointerEvents() - } - - private val drawingOrderHelper: ViewGroupDrawingOrderHelper - get() { - return _drawingOrderHelper - ?: ViewGroupDrawingOrderHelper(this).also { _drawingOrderHelper = it } - } - - init { - initView() - } - - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - MeasureSpecAssertions.assertExplicitMeasureSpec(widthMeasureSpec, heightMeasureSpec) - setMeasuredDimension( - MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec)) - } - - // No-op since UIManagerModule handles actually laying out children. - override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int): Unit = Unit - - @SuppressLint("MissingSuperCall") - // No-op, terminate `requestLayout` here, UIManagerModule handles laying out children and `layout` - // is called on all RN-managed views by `NativeViewHierarchyManager` - override fun requestLayout(): Unit = Unit - - override fun dispatchProvideStructure(structure: ViewStructure) { - try { - super.dispatchProvideStructure(structure) - } catch (e: NullPointerException) { - FLog.e(ReactConstants.TAG, "NullPointerException when executing dispatchProvideStructure", e) - } - } - - override fun setBackgroundColor(color: Int) { - BackgroundStyleApplicator.setBackgroundColor(this, color) - } - - @Deprecated( - "Don't use setTranslucentBackgroundDrawable as it was deprecated in React Native 0.76.0.", - ReplaceWith( - "BackgroundStyleApplicator.setFeedbackUnderlay(this, background)", - "com.facebook.react.uimanager.BackgroundStyleApplicator")) - public open fun setTranslucentBackgroundDrawable(background: Drawable?): Unit { - BackgroundStyleApplicator.setFeedbackUnderlay(this, background) - } - - override fun setOnInterceptTouchEventListener(listener: OnInterceptTouchEventListener) { - onInterceptTouchEventListener = listener - } - - override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { - if (onInterceptTouchEventListener?.onInterceptTouchEvent(this, ev) == true) { - return true - } - // We intercept the touch event if the children are not supposed to receive it. - return !PointerEvents.canChildrenBeTouchTarget(_pointerEvents) || - super.onInterceptTouchEvent(ev) - } - - override fun onTouchEvent(ev: MotionEvent): Boolean { - // We do not accept the touch event if this view is not supposed to receive it. - // The root view always assumes any view that was tapped wants the touch - // and sends the event to JS as such. - // We don't need to do bubbling in native (it's already happening in JS). - // For an explanation of bubbling and capturing, see - // http://javascript.info/tutorial/bubbling-and-capturing#capturing - return PointerEvents.canBeTouchTarget(_pointerEvents) - } - - override fun onHoverEvent(event: MotionEvent): Boolean = - if (ReactFeatureFlags.dispatchPointerEvents) { - // Match the logic from onTouchEvent if pointer events are enabled - PointerEvents.canBeTouchTarget(_pointerEvents) - } else { - super.onHoverEvent(event) - } - - override fun dispatchGenericMotionEvent(ev: MotionEvent): Boolean = - // We do not dispatch the motion event if its children are not supposed to receive it - PointerEvents.canChildrenBeTouchTarget(_pointerEvents) || super.dispatchGenericMotionEvent(ev) - - /** - * We override this to allow developers to determine whether they need offscreen alpha compositing - * or not. See the documentation of needsOffscreenAlphaCompositing in View.js. - */ - override fun hasOverlappingRendering(): Boolean = needsOffscreenAlphaCompositing - - /** See the documentation of needsOffscreenAlphaCompositing in View.js. */ - public open fun setNeedsOffscreenAlphaCompositing(needsOffscreenAlphaCompositing: Boolean): Unit { - this.needsOffscreenAlphaCompositing = needsOffscreenAlphaCompositing - } - - public open fun setBorderWidth(position: Int, width: Float): Unit { - BackgroundStyleApplicator.setBorderWidth(this, LogicalEdge.entries[position], width.pxToDp()) - } - - public open fun setBorderColor(position: Int, color: Int?): Unit { - BackgroundStyleApplicator.setBorderColor(this, LogicalEdge.entries[position], color) - } - - @Deprecated( - "Deprecated in React Native 0.75.0, in favor of setBorderRadius(BorderRadiusProp, Float)", - ReplaceWith( - "setBorderRadius(BorderRadiusProp.BORDER_RADIUS, borderRadius)", - "com.facebook.react.uimanager.style.BorderRadiusProp", - )) - @Suppress("DEPRECATION") - public open fun setBorderRadius(borderRadius: Float): Unit { - this.setBorderRadius(borderRadius, BorderRadiusProp.BORDER_RADIUS.ordinal) - } - - @Deprecated( - "Deprecated in React Native 0.75.0, in favor of setBorderRadius(BorderRadiusProp, Float)", - ReplaceWith( - "setBorderRadius(BorderRadiusProp.entries[position], borderRadius)", - "com.facebook.react.uimanager.style.BorderRadiusProp", - )) - public open fun setBorderRadius(borderRadius: Float, position: Int): Unit { - val radius = - when { - borderRadius.isNaN() -> null - else -> LengthPercentage(borderRadius, LengthPercentageType.POINT) - } - BackgroundStyleApplicator.setBorderRadius(this, BorderRadiusProp.entries[position], radius) - } - - public open fun setBorderRadius( - property: BorderRadiusProp, - borderRadius: LengthPercentage? - ): Unit { - BackgroundStyleApplicator.setBorderRadius(this, property, borderRadius) - } - - public open fun setBorderStyle(style: String?): Unit { - BackgroundStyleApplicator.setBorderStyle(this, style?.let { BorderStyle.fromString(style) }) - } - - override fun setRemoveClippedSubviews(removeClippedSubviews: Boolean) { - if (removeClippedSubviews == _removeClippedSubviews) { - return - } - _removeClippedSubviews = removeClippedSubviews - if (removeClippedSubviews) { - val clippingRect = Rect() - ReactClippingViewGroupHelper.calculateClippingRect(this, clippingRect) - allChildrenCount = childCount - val initialSize = max(12, allChildrenCount) - val children = arrayOfNulls(initialSize) - childrenLayoutChangeListener = ChildrenLayoutChangeListener(this) - for (i in 0 until allChildrenCount) { - children[i] = - getChildAt(i).apply { addOnLayoutChangeListener(childrenLayoutChangeListener) } - } - _clippingRect = clippingRect - allChildren = children - updateClippingRect() - } else { - // Add all clipped views back, deallocate additional arrays, remove layoutChangeListener - val clippingRect = checkNotNull(_clippingRect) - val children = checkNotNull(allChildren) - val listener = checkNotNull(childrenLayoutChangeListener) - for (i in 0 until allChildrenCount) { - children[i]?.removeOnLayoutChangeListener(listener) - } - getDrawingRect(clippingRect) - updateClippingToRect(clippingRect) - allChildren = null - _clippingRect = null - allChildrenCount = 0 - childrenLayoutChangeListener = null - } - } - - override fun getRemoveClippedSubviews(): Boolean = _removeClippedSubviews - - override fun getClippingRect(outClippingRect: Rect) { - outClippingRect.set( - checkNotNull(_clippingRect) { "getClippingRect called when removeClippedSubviews not set" }) - } - - override fun updateClippingRect() { - if (!_removeClippedSubviews) { - return - } - val clippingRect = checkNotNull(_clippingRect) - checkNotNull(allChildren) - ReactClippingViewGroupHelper.calculateClippingRect(this, clippingRect) - updateClippingToRect(clippingRect) - } - - private fun updateClippingToRect(clippingRect: Rect) { - val children = checkNotNull(allChildren) - var clippedSoFar = 0 - for (i in 0 until allChildrenCount) { - updateSubviewClipStatus(clippingRect, i, clippedSoFar) - if (children[i]?.parent == null) { - clippedSoFar++ - } - } - } - - private fun updateSubviewClipStatus(clippingRect: Rect, idx: Int, clippedSoFar: Int) { - UiThreadUtil.assertOnUiThread() - val child = checkNotNull(allChildren?.get(idx)) - val intersects = clippingRect.intersects(child.left, child.top, child.right, child.bottom) - var needUpdateClippingRecursive = false - // We never want to clip children that are being animated, as this can easily break layout : - // when layout animation changes size and/or position of views contained inside a listview that - // clips offscreen children, we need to ensure that, when view exits the viewport, final size - // and position is set prior to removing the view from its listview parent. - // Otherwise, when view gets re-attached again, i.e when it re-enters the viewport after scroll, - // it won't be size and located properly. - val animation = child.animation - val isAnimating = animation?.hasEnded() == false - if (!intersects && child.parent != null && !isAnimating) { - // We can try saving on invalidate call here as the view that we remove is out of visible - // area - // therefore invalidation is not necessary. - removeViewInLayout(child) - needUpdateClippingRecursive = true - } else if (intersects && child.parent == null) { - addViewInLayout(child, idx - clippedSoFar, defaultLayoutParam, true) - invalidate() - needUpdateClippingRecursive = true - } else if (intersects) { - // If there is any intersection we need to inform the child to update its clipping rect - needUpdateClippingRecursive = true - } - if (needUpdateClippingRecursive && - child is ReactClippingViewGroup && - child.removeClippedSubviews) { - child.updateClippingRect() - } - } - - private fun updateSubviewClipStatus(subview: View) { - if (!_removeClippedSubviews || parent == null) { - return - } - val clippingRect = checkNotNull(_clippingRect) - val children = checkNotNull(allChildren) - - // do fast check whether intersect state changed - val intersects = - clippingRect.intersects(subview.left, subview.top, subview.right, subview.bottom) - - // If it was intersecting before, should be attached to the parent - val oldIntersects = subview.parent != null - if (intersects != oldIntersects) { - var clippedSoFar = 0 - for (i in 0 until allChildrenCount) { - if (children[i] === subview) { - updateSubviewClipStatus(clippingRect, i, clippedSoFar) - break - } - if (children[i]?.parent == null) { - clippedSoFar++ - } - } - } - } - - override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { - super.onSizeChanged(w, h, oldw, oldh) - if (_removeClippedSubviews) { - updateClippingRect() - } - } - - override fun onAttachedToWindow() { - super.onAttachedToWindow() - if (_removeClippedSubviews) { - updateClippingRect() - } - } - - private fun customDrawOrderDisabled(): Boolean = - // Custom draw order is disabled for Fabric. - id != NO_ID && ViewUtil.getUIManagerType(id) == UIManagerType.FABRIC - - private fun handleAddView(view: View) { - UiThreadUtil.assertOnUiThread() - if (!customDrawOrderDisabled()) { - drawingOrderHelper.handleAddView(view) - isChildrenDrawingOrderEnabled = drawingOrderHelper.shouldEnableCustomDrawingOrder() - } else { - isChildrenDrawingOrderEnabled = false - } - } - - private fun handleRemoveView(view: View?) { - UiThreadUtil.assertOnUiThread() - if (!customDrawOrderDisabled()) { - if (indexOfChild(view) == -1) { - return - } - drawingOrderHelper.handleRemoveView(view) - isChildrenDrawingOrderEnabled = drawingOrderHelper.shouldEnableCustomDrawingOrder() - } else { - isChildrenDrawingOrderEnabled = false - } - } - - private fun handleRemoveViews(start: Int, count: Int) { - val endIndex = start + count - for (index in start until endIndex) { - if (index < childCount) { - handleRemoveView(getChildAt(index)) - } - } - } - - override fun addView(child: View, index: Int, params: LayoutParams?) { - // This will get called for every overload of addView so there is not need to override every - // method. - handleAddView(child) - super.addView(child, index, params) - } - - override fun addViewInLayout( - child: View, - index: Int, - params: LayoutParams, - preventRequestLayout: Boolean - ): Boolean { - handleAddView(child) - return super.addViewInLayout(child, index, params, preventRequestLayout) - } - - override fun removeView(view: View?) { - handleRemoveView(view) - super.removeView(view) - } - - override fun removeViewAt(index: Int) { - handleRemoveView(getChildAt(index)) - super.removeViewAt(index) - } - - override fun removeViewInLayout(view: View) { - handleRemoveView(view) - super.removeViewInLayout(view) - } - - override fun removeViewsInLayout(start: Int, count: Int) { - handleRemoveViews(start, count) - super.removeViewsInLayout(start, count) - } - - override fun removeViews(start: Int, count: Int) { - handleRemoveViews(start, count) - super.removeViews(start, count) - } - - override fun getChildDrawingOrder(childCount: Int, index: Int): Int { - UiThreadUtil.assertOnUiThread() - return if (!customDrawOrderDisabled()) { - drawingOrderHelper.getChildDrawingOrder(childCount, index) - } else { - index - } - } - - override fun getZIndexMappedChildIndex(index: Int): Int { - UiThreadUtil.assertOnUiThread() - return if (!customDrawOrderDisabled() && drawingOrderHelper.shouldEnableCustomDrawingOrder()) { - drawingOrderHelper.getChildDrawingOrder(childCount, index) - } else { - // Fabric behavior - index - } - } - - override fun updateDrawingOrder() { - if (customDrawOrderDisabled()) { - return - } - drawingOrderHelper.update() - isChildrenDrawingOrderEnabled = drawingOrderHelper.shouldEnableCustomDrawingOrder() - invalidate() - } - - override fun getPointerEvents(): PointerEvents = _pointerEvents - - override fun dispatchSetPressed(pressed: Boolean) { - // Prevents the ViewGroup from dispatching the pressed state - // to it's children. - } - - public open fun setPointerEvents(pointerEvents: PointerEvents?): Unit { - if (pointerEvents != null) { - _pointerEvents = pointerEvents - } else { - resetPointerEvents() - } - } - - internal fun resetPointerEvents(): Unit { - _pointerEvents = PointerEvents.AUTO - } - - internal open fun getChildAtWithSubviewClippingEnabled(index: Int): View? = - if (index in 0 until allChildrenCount) { - checkNotNull(allChildren)[index] - } else { - null - } - - internal open fun addViewWithSubviewClippingEnabled(child: View, index: Int): Unit { - Assertions.assertCondition(_removeClippedSubviews) - val clippingRect = checkNotNull(_clippingRect) - val children = checkNotNull(allChildren) - addInArray(child, index) - // we add view as "clipped" and then run [updateSubviewClipStatus] to conditionally - // attach it - var clippedSoFar = 0 - for (i in 0 until index) { - if (children[i]?.parent == null) { - clippedSoFar++ - } - } - updateSubviewClipStatus(clippingRect, index, clippedSoFar) - child.addOnLayoutChangeListener(childrenLayoutChangeListener) - if (child is ReactClippingProhibitedView) { - UiThreadUtil.runOnUiThread { - if (!child.isShown) { - ReactSoftExceptionLogger.logSoftException( - ReactConstants.TAG, - ReactNoCrashSoftException( - """ - |Child view has been added to Parent view in which it is clipped and not - |visible. This is not legal for this particular child view. Child: [${child.id}] - | $child Parent: [$id] $parent""" - .trimMargin())) - } - } - } - } - - internal open fun removeViewWithSubviewClippingEnabled(view: View): Unit { - UiThreadUtil.assertOnUiThread() - Assertions.assertCondition(_removeClippedSubviews) - checkNotNull(_clippingRect) - val children = checkNotNull(allChildren) - view.removeOnLayoutChangeListener(childrenLayoutChangeListener) - val index = indexOfChildInAllChildren(view) - if (children[index]?.parent != null) { - var clippedSoFar = 0 - for (i in 0 until index) { - if (children[i]?.parent == null) { - clippedSoFar++ - } - } - removeViewsInLayout(index - clippedSoFar, 1) - } - removeFromArray(index) - } - - internal open fun removeAllViewsWithSubviewClippingEnabled(): Unit { - Assertions.assertCondition(_removeClippedSubviews) - val children = checkNotNull(allChildren) - for (i in 0 until allChildrenCount) { - children[i]?.removeOnLayoutChangeListener(childrenLayoutChangeListener) - } - removeAllViewsInLayout() - allChildrenCount = 0 - } - - private fun indexOfChildInAllChildren(child: View): Int { - val count = allChildrenCount - val children = checkNotNull(allChildren) - return (0 until count).firstOrNull { i -> children[i] === child } ?: -1 - } - - private fun addInArray(child: View, index: Int) { - val children = growAllChildrenIfNeeded(index) - children[index] = child - allChildrenCount++ - } - - /** - * Grow the [allChildren] array if it's run out of space - * - * @param insertIndex index where child is being inserted, must be <= [allChildrenCount] - * @return the non-null array that's backing [allChildren] after any potential resize, with a null - * slot at [insertIndex] - */ - private fun growAllChildrenIfNeeded(insertIndex: Int): Array { - val children = checkNotNull(allChildren) - val count = allChildrenCount - if (insertIndex > count) { - throw IndexOutOfBoundsException("index=$insertIndex count=$count") - } - if (children.size > count) { - // no need to resize, ensure index is free - if (insertIndex < count) { - System.arraycopy(children, insertIndex, children, insertIndex + 1, count - insertIndex) - } - return children - } - // need to resize the array - val newArray = - if (insertIndex == count) { - // inserting at the end of the array - children.copyOf(children.size + ARRAY_CAPACITY_INCREMENT) - } else { - // inserting within the array - arrayOfNulls(children.size + ARRAY_CAPACITY_INCREMENT).apply { - System.arraycopy(children, 0, this, 0, insertIndex) - System.arraycopy(children, insertIndex, this, insertIndex + 1, count - insertIndex) - } - } - allChildren = newArray - return newArray - } - - private fun removeFromArray(index: Int) { - val children = checkNotNull(allChildren) - val count = allChildrenCount - if (index == count - 1) { - children[--allChildrenCount] = null - } else if (index in 0 until count) { - System.arraycopy(children, index + 1, children, index, count - index - 1) - children[--allChildrenCount] = null - } else { - throw IndexOutOfBoundsException() - } - } - - private fun needsIsolatedLayer(): Boolean { - if (!ReactNativeFeatureFlags.enableAndroidMixBlendModeProp()) { - return false - } - return (0 until childCount).any { i -> getChildAt(i).getTag(R.id.mix_blend_mode) != null } - } - - @VisibleForTesting - protected open fun getBackgroundColor(): Int = - BackgroundStyleApplicator.getBackgroundColor(this) ?: DEFAULT_BACKGROUND_COLOR - - // TODO: convert to val - public open fun setOverflow(overflow: String?): Unit { - _overflow = - if (overflow == null) { - Overflow.VISIBLE - } else { - Overflow.fromString(overflow) ?: Overflow.VISIBLE - } - invalidate() - } - - override fun getOverflow(): String? = - when (_overflow) { - Overflow.HIDDEN -> "hidden" - Overflow.SCROLL -> "scroll" - Overflow.VISIBLE -> "visible" - } - - override fun setOverflowInset(left: Int, top: Int, right: Int, bottom: Int) { - if (needsIsolatedLayer() && - (_overflowInset.left != left || - _overflowInset.top != top || - _overflowInset.right != right || - _overflowInset.bottom != bottom)) { - invalidate() - } - _overflowInset.set(left, top, right, bottom) - } - - override fun getOverflowInset(): Rect = _overflowInset - - /** - * Set the background for the view or remove the background. It calls [setBackground(Drawable)] or - * [setBackgroundDrawable(Drawable)] based on the sdk version. - * - * @param drawable [Drawable] The Drawable to use as the background, or null to remove the - * background - */ - internal fun updateBackgroundDrawable(drawable: Drawable?): Unit { - super.setBackground(drawable) - } - - override fun draw(canvas: Canvas) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && - ViewUtil.getUIManagerType(this) == UIManagerType.FABRIC && - needsIsolatedLayer()) { - - // Check if the view is a stacking context and has children, if it does, do the rendering - // offscreen and then composite back. This follows the idea of group isolation on blending - // https://www.w3.org/TR/compositing-1/#isolationblending - val overflowInset = this.overflowInset - canvas.saveLayer( - overflowInset.left.toFloat(), - overflowInset.top.toFloat(), - (width + -overflowInset.right).toFloat(), - (height + -overflowInset.bottom).toFloat(), - null) - super.draw(canvas) - canvas.restore() - } else { - super.draw(canvas) - } - } - - override fun dispatchDraw(canvas: Canvas) { - if (_overflow != Overflow.VISIBLE || getTag(R.id.filter) != null) { - BackgroundStyleApplicator.clipToPaddingBox(this, canvas) - } - super.dispatchDraw(canvas) - } - - override fun drawChild(canvas: Canvas, child: View, drawingTime: Long): Boolean { - val drawWithZ = child.elevation > 0 - if (drawWithZ) { - CanvasUtil.enableZ(canvas, true) - } - var mixBlendMode: BlendMode? = null - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && needsIsolatedLayer()) { - mixBlendMode = child.getTag(R.id.mix_blend_mode) as? BlendMode - if (mixBlendMode != null) { - val p = Paint() - p.blendMode = mixBlendMode - val overflowInset = this.overflowInset - canvas.saveLayer( - overflowInset.left.toFloat(), - overflowInset.top.toFloat(), - (width + -overflowInset.right).toFloat(), - (height + -overflowInset.bottom).toFloat(), - p) - } - } - val result = super.drawChild(canvas, child, drawingTime) - if (mixBlendMode != null) { - canvas.restore() - } - if (drawWithZ) { - CanvasUtil.enableZ(canvas, false) - } - return result - } - - public open fun setOpacityIfPossible(opacity: Float): Unit { - backfaceOpacity = opacity - setBackfaceVisibilityDependantOpacity() - } - - public open fun setBackfaceVisibility(backfaceVisibility: String?): Unit { - this.backfaceVisibility = backfaceVisibility - setBackfaceVisibilityDependantOpacity() - } - - public open fun setBackfaceVisibilityDependantOpacity(): Unit { - val isBackfaceVisible = backfaceVisibility == "visible" - if (isBackfaceVisible) { - alpha = backfaceOpacity - return - } - val rotationX = rotationX - val rotationY = rotationY - val isFrontfaceVisible = - rotationX >= -90f && rotationX < 90f && rotationY >= -90f && rotationY < 90f - if (isFrontfaceVisible) { - alpha = backfaceOpacity - return - } - alpha = 0f - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt index aac6e9347b7389..4b6dc1957e26f1 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt @@ -166,23 +166,23 @@ public open class ReactViewManager : ReactClippingViewManager() when (hitSlop.type) { ReadableType.Map -> { val hitSlopMap = hitSlop.asMap() - view.hitSlopRect = + view.setHitSlopRect( Rect( getPixels(hitSlopMap, "left"), getPixels(hitSlopMap, "top"), getPixels(hitSlopMap, "right"), - getPixels(hitSlopMap, "bottom")) + getPixels(hitSlopMap, "bottom"))) } ReadableType.Number -> { val hitSlopValue = hitSlop.asDouble().dpToPx().toInt() - view.hitSlopRect = Rect(hitSlopValue, hitSlopValue, hitSlopValue, hitSlopValue) + view.setHitSlopRect(Rect(hitSlopValue, hitSlopValue, hitSlopValue, hitSlopValue)) } - ReadableType.Null -> view.hitSlopRect = null + ReadableType.Null -> view.setHitSlopRect(null) else -> { FLog.w(ReactConstants.TAG, "Invalid type for 'hitSlop' value ${hitSlop.type}") - view.hitSlopRect = null + view.setHitSlopRect(null) } } } diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/view/ReactViewGroupTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/view/ReactViewGroupTest.kt deleted file mode 100644 index 588d92963e388f..00000000000000 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/view/ReactViewGroupTest.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.views.view - -import android.app.Activity -import android.content.Context -import android.view.View -import android.view.ViewGroup -import android.widget.FrameLayout -import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests -import org.assertj.core.api.Assertions.assertThat -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.Robolectric -import org.robolectric.RobolectricTestRunner - -@RunWith(RobolectricTestRunner::class) -class ReactViewGroupTest { - - private lateinit var context: Context - - @Before - fun setUp() { - ReactNativeFeatureFlagsForTests.setUp() - context = Robolectric.buildActivity(Activity::class.java).create().get() - } - - @Test - fun `View clipping - ensure allChildren properly resizes when adding views in sequence`() { - val rvg = ReactViewGroup(context) - rvg.left = 0 - rvg.right = 100 - rvg.top = 0 - rvg.bottom = 100 - FrameLayout(context).addView(rvg) - rvg.removeClippedSubviews = true - for (i in 0..20) { - rvg.addViewWithSubviewClippingEnabled(TestView(context, i * 10), i) - } - rvg.updateClippingRect() - assertThat(rvg.childCount).isEqualTo(10) - } - - @Test - fun `View clipping - ensure allChildren properly resizes when adding views out of sequence`() { - val rvg = ReactViewGroup(context) - rvg.left = 0 - rvg.right = 100 - rvg.top = 0 - rvg.bottom = 100 - FrameLayout(context).addView(rvg) - rvg.removeClippedSubviews = true - for (i in 0..10) { - rvg.addViewWithSubviewClippingEnabled(TestView(context, i * 10), i) - } - repeat(10) { rvg.addViewWithSubviewClippingEnabled(TestView(context, 90), 10) } - rvg.updateClippingRect() - assertThat(rvg.childCount).isEqualTo(20) - } -} - -class TestView(context: Context, yPos: Int) : View(context) { - init { - left = 0 - right = 100 - top = yPos - bottom = top + 10 - } -} - -class TestParent(context: Context) : ViewGroup(context) { - override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) = Unit -} From 3f880991aa32a504dafb7c1cf9af69273dfd6d3a Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Thu, 26 Sep 2024 15:50:26 -0700 Subject: [PATCH 034/296] Back out "Redo "[RN][Android] Convert ReactViewManager, ReactClippingViewManager to Kotlin"" Summary: Backing out the stack since a same crash that previously effected many apps appeared again, and there are changes soon landing that will add more conflicts. Reviewed By: Abbondanzo Differential Revision: D63493332 fbshipit-source-id: 4423bf41c793e00a0aa22d12a77bca69d3b1ae77 --- .../ReactAndroid/api/ReactAndroid.api | 9 +- .../views/view/ReactClippingViewManager.java | 95 ++++ .../views/view/ReactClippingViewManager.kt | 73 --- .../react/views/view/ReactViewManager.java | 425 ++++++++++++++++++ .../react/views/view/ReactViewManager.kt | 390 ---------------- 5 files changed, 522 insertions(+), 470 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewManager.java delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewManager.kt create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 1f02b1355d8b6a..e1909ad0345138 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -8363,8 +8363,6 @@ public class com/facebook/react/views/view/ReactViewGroup : android/view/ViewGro } public class com/facebook/react/views/view/ReactViewManager : com/facebook/react/views/view/ReactClippingViewManager { - public static final field Companion Lcom/facebook/react/views/view/ReactViewManager$Companion; - public static final field REACT_CLASS Ljava/lang/String; public fun ()V public synthetic fun createViewInstance (Lcom/facebook/react/uimanager/ThemedReactContext;)Landroid/view/View; public fun createViewInstance (Lcom/facebook/react/uimanager/ThemedReactContext;)Lcom/facebook/react/views/view/ReactViewGroup; @@ -8375,7 +8373,7 @@ public class com/facebook/react/views/view/ReactViewManager : com/facebook/react public fun nextFocusLeft (Lcom/facebook/react/views/view/ReactViewGroup;I)V public fun nextFocusRight (Lcom/facebook/react/views/view/ReactViewGroup;I)V public fun nextFocusUp (Lcom/facebook/react/views/view/ReactViewGroup;I)V - public synthetic fun prepareToRecycleView (Lcom/facebook/react/uimanager/ThemedReactContext;Landroid/view/View;)Landroid/view/View; + protected synthetic fun prepareToRecycleView (Lcom/facebook/react/uimanager/ThemedReactContext;Landroid/view/View;)Landroid/view/View; protected fun prepareToRecycleView (Lcom/facebook/react/uimanager/ThemedReactContext;Lcom/facebook/react/views/view/ReactViewGroup;)Lcom/facebook/react/views/view/ReactViewGroup; public synthetic fun receiveCommand (Landroid/view/View;ILcom/facebook/react/bridge/ReadableArray;)V public synthetic fun receiveCommand (Landroid/view/View;Ljava/lang/String;Lcom/facebook/react/bridge/ReadableArray;)V @@ -8404,7 +8402,7 @@ public class com/facebook/react/views/view/ReactViewManager : com/facebook/react public fun setOverflow (Lcom/facebook/react/views/view/ReactViewGroup;Ljava/lang/String;)V public fun setPointerEvents (Lcom/facebook/react/views/view/ReactViewGroup;Ljava/lang/String;)V public fun setTVPreferredFocus (Lcom/facebook/react/views/view/ReactViewGroup;Z)V - public synthetic fun setTransformProperty (Landroid/view/View;Lcom/facebook/react/bridge/ReadableArray;Lcom/facebook/react/bridge/ReadableArray;)V + protected synthetic fun setTransformProperty (Landroid/view/View;Lcom/facebook/react/bridge/ReadableArray;Lcom/facebook/react/bridge/ReadableArray;)V protected fun setTransformProperty (Lcom/facebook/react/views/view/ReactViewGroup;Lcom/facebook/react/bridge/ReadableArray;Lcom/facebook/react/bridge/ReadableArray;)V } @@ -8415,9 +8413,6 @@ public class com/facebook/react/views/view/ReactViewManager$$PropsSetter : com/f public fun setProperty (Lcom/facebook/react/views/view/ReactViewManager;Lcom/facebook/react/views/view/ReactViewGroup;Ljava/lang/String;Ljava/lang/Object;)V } -public final class com/facebook/react/views/view/ReactViewManager$Companion { -} - public final class com/facebook/react/views/view/ViewGroupClickEvent : com/facebook/react/uimanager/events/Event { public fun (I)V public fun (II)V diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewManager.java new file mode 100644 index 00000000000000..525183053a0ef0 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewManager.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.view; + +import android.view.View; +import androidx.annotation.Nullable; +import com.facebook.infer.annotation.Nullsafe; +import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.uimanager.ViewGroupManager; +import com.facebook.react.uimanager.annotations.ReactProp; + +/** + * View manager which handles clipped subviews. Useful for custom views which extends from {@link + * com.facebook.react.views.view.ReactViewGroup} + */ +@Nullsafe(Nullsafe.Mode.LOCAL) +public abstract class ReactClippingViewManager + extends ViewGroupManager { + + @ReactProp( + name = com.facebook.react.uimanager.ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS) + public void setRemoveClippedSubviews(T view, boolean removeClippedSubviews) { + UiThreadUtil.assertOnUiThread(); + + view.setRemoveClippedSubviews(removeClippedSubviews); + } + + @Override + public void addView(T parent, View child, int index) { + UiThreadUtil.assertOnUiThread(); + + boolean removeClippedSubviews = parent.getRemoveClippedSubviews(); + if (removeClippedSubviews) { + parent.addViewWithSubviewClippingEnabled(child, index); + } else { + parent.addView(child, index); + } + } + + @Override + public int getChildCount(T parent) { + boolean removeClippedSubviews = parent.getRemoveClippedSubviews(); + if (removeClippedSubviews) { + return parent.getAllChildrenCount(); + } else { + return parent.getChildCount(); + } + } + + @Override + @Nullable + public View getChildAt(T parent, int index) { + boolean removeClippedSubviews = parent.getRemoveClippedSubviews(); + if (removeClippedSubviews) { + return parent.getChildAtWithSubviewClippingEnabled(index); + } else { + return parent.getChildAt(index); + } + } + + @Override + public void removeViewAt(T parent, int index) { + UiThreadUtil.assertOnUiThread(); + + boolean removeClippedSubviews = parent.getRemoveClippedSubviews(); + if (removeClippedSubviews) { + View child = getChildAt(parent, index); + if (child != null) { + if (child.getParent() != null) { + parent.removeView(child); + } + parent.removeViewWithSubviewClippingEnabled(child); + } + } else { + parent.removeViewAt(index); + } + } + + @Override + public void removeAllViews(T parent) { + UiThreadUtil.assertOnUiThread(); + + boolean removeClippedSubviews = parent.getRemoveClippedSubviews(); + if (removeClippedSubviews) { + parent.removeAllViewsWithSubviewClippingEnabled(); + } else { + parent.removeAllViews(); + } + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewManager.kt deleted file mode 100644 index d77075ed705586..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewManager.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.views.view - -import android.view.View -import com.facebook.react.bridge.UiThreadUtil -import com.facebook.react.uimanager.ReactClippingViewGroupHelper -import com.facebook.react.uimanager.ViewGroupManager -import com.facebook.react.uimanager.annotations.ReactProp - -/** - * View manager which handles clipped subviews. Useful for custom views which extends from - * [com.facebook.react.views.view.ReactViewGroup] - */ -public abstract class ReactClippingViewManager : ViewGroupManager() { - - @ReactProp(name = ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS) - public open fun setRemoveClippedSubviews(view: T, removeClippedSubviews: Boolean) { - UiThreadUtil.assertOnUiThread() - view.removeClippedSubviews = removeClippedSubviews - } - - override fun addView(parent: T, child: View, index: Int) { - UiThreadUtil.assertOnUiThread() - if (parent.removeClippedSubviews) { - parent.addViewWithSubviewClippingEnabled(child, index) - } else { - parent.addView(child, index) - } - } - - override fun getChildCount(parent: T): Int = - if (parent.removeClippedSubviews) { - parent.allChildrenCount - } else { - parent.childCount - } - - override fun getChildAt(parent: T, index: Int): View? = - if (parent.removeClippedSubviews) { - parent.getChildAtWithSubviewClippingEnabled(index) - } else { - parent.getChildAt(index) - } - - override fun removeViewAt(parent: T, index: Int) { - UiThreadUtil.assertOnUiThread() - if (parent.removeClippedSubviews) { - val child = getChildAt(parent, index) ?: return - if (child.parent != null) { - parent.removeView(child) - } else { - parent.removeViewWithSubviewClippingEnabled(child) - } - } else { - parent.removeViewAt(index) - } - } - - override fun removeAllViews(parent: T) { - UiThreadUtil.assertOnUiThread() - if (parent.removeClippedSubviews) { - parent.removeAllViewsWithSubviewClippingEnabled() - } else { - parent.removeAllViews() - } - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java new file mode 100644 index 00000000000000..548fe524399cf9 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java @@ -0,0 +1,425 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.view; + +import android.graphics.Rect; +import android.view.View; +import androidx.annotation.ColorInt; +import androidx.annotation.Nullable; +import com.facebook.common.logging.FLog; +import com.facebook.infer.annotation.Nullsafe; +import com.facebook.react.bridge.Dynamic; +import com.facebook.react.bridge.DynamicFromObject; +import com.facebook.react.bridge.JSApplicationIllegalArgumentException; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.common.MapBuilder; +import com.facebook.react.common.ReactConstants; +import com.facebook.react.common.annotations.VisibleForTesting; +import com.facebook.react.module.annotations.ReactModule; +import com.facebook.react.uimanager.BackgroundStyleApplicator; +import com.facebook.react.uimanager.LengthPercentage; +import com.facebook.react.uimanager.LengthPercentageType; +import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.uimanager.PointerEvents; +import com.facebook.react.uimanager.Spacing; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.UIManagerHelper; +import com.facebook.react.uimanager.ViewProps; +import com.facebook.react.uimanager.annotations.ReactProp; +import com.facebook.react.uimanager.annotations.ReactPropGroup; +import com.facebook.react.uimanager.common.UIManagerType; +import com.facebook.react.uimanager.common.ViewUtil; +import com.facebook.react.uimanager.events.EventDispatcher; +import com.facebook.react.uimanager.style.BackgroundImageLayer; +import com.facebook.react.uimanager.style.BorderRadiusProp; +import com.facebook.react.uimanager.style.BorderStyle; +import com.facebook.react.uimanager.style.LogicalEdge; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** View manager for AndroidViews (plain React Views). */ +@ReactModule(name = ReactViewManager.REACT_CLASS) +@Nullsafe(Nullsafe.Mode.LOCAL) +public class ReactViewManager extends ReactClippingViewManager { + + @VisibleForTesting public static final String REACT_CLASS = ViewProps.VIEW_CLASS_NAME; + + private static final int[] SPACING_TYPES = { + Spacing.ALL, + Spacing.LEFT, + Spacing.RIGHT, + Spacing.TOP, + Spacing.BOTTOM, + Spacing.START, + Spacing.END, + Spacing.BLOCK, + Spacing.BLOCK_END, + Spacing.BLOCK_START + }; + private static final int CMD_HOTSPOT_UPDATE = 1; + private static final int CMD_SET_PRESSED = 2; + private static final String HOTSPOT_UPDATE_KEY = "hotspotUpdate"; + + public ReactViewManager() { + super(); + + setupViewRecycling(); + } + + @Override + protected @Nullable ReactViewGroup prepareToRecycleView( + ThemedReactContext reactContext, ReactViewGroup view) { + // BaseViewManager + ReactViewGroup preparedView = super.prepareToRecycleView(reactContext, view); + if (preparedView != null) { + preparedView.recycleView(); + } + return view; + } + + @ReactProp(name = "accessible") + public void setAccessible(ReactViewGroup view, boolean accessible) { + view.setFocusable(accessible); + } + + @ReactProp(name = "hasTVPreferredFocus") + public void setTVPreferredFocus(ReactViewGroup view, boolean hasTVPreferredFocus) { + if (hasTVPreferredFocus) { + view.setFocusable(true); + view.setFocusableInTouchMode(true); + view.requestFocus(); + } + } + + @ReactProp(name = ViewProps.BACKGROUND_IMAGE, customType = "BackgroundImage") + public void setBackgroundImage(ReactViewGroup view, @Nullable ReadableArray backgroundImage) { + if (ViewUtil.getUIManagerType(view) == UIManagerType.FABRIC) { + if (backgroundImage != null && backgroundImage.size() > 0) { + List backgroundImageLayers = new ArrayList<>(backgroundImage.size()); + for (int i = 0; i < backgroundImage.size(); i++) { + ReadableMap backgroundImageMap = backgroundImage.getMap(i); + BackgroundImageLayer layer = + new BackgroundImageLayer(backgroundImageMap, view.getContext()); + backgroundImageLayers.add(layer); + } + BackgroundStyleApplicator.setBackgroundImage(view, backgroundImageLayers); + } else { + BackgroundStyleApplicator.setBackgroundImage(view, null); + } + } + } + + @ReactProp(name = "nextFocusDown", defaultInt = View.NO_ID) + public void nextFocusDown(ReactViewGroup view, int viewId) { + view.setNextFocusDownId(viewId); + } + + @ReactProp(name = "nextFocusForward", defaultInt = View.NO_ID) + public void nextFocusForward(ReactViewGroup view, int viewId) { + view.setNextFocusForwardId(viewId); + } + + @ReactProp(name = "nextFocusLeft", defaultInt = View.NO_ID) + public void nextFocusLeft(ReactViewGroup view, int viewId) { + view.setNextFocusLeftId(viewId); + } + + @ReactProp(name = "nextFocusRight", defaultInt = View.NO_ID) + public void nextFocusRight(ReactViewGroup view, int viewId) { + view.setNextFocusRightId(viewId); + } + + @ReactProp(name = "nextFocusUp", defaultInt = View.NO_ID) + public void nextFocusUp(ReactViewGroup view, int viewId) { + view.setNextFocusUpId(viewId); + } + + @ReactPropGroup( + names = { + ViewProps.BORDER_RADIUS, + ViewProps.BORDER_TOP_LEFT_RADIUS, + ViewProps.BORDER_TOP_RIGHT_RADIUS, + ViewProps.BORDER_BOTTOM_RIGHT_RADIUS, + ViewProps.BORDER_BOTTOM_LEFT_RADIUS, + ViewProps.BORDER_TOP_START_RADIUS, + ViewProps.BORDER_TOP_END_RADIUS, + ViewProps.BORDER_BOTTOM_START_RADIUS, + ViewProps.BORDER_BOTTOM_END_RADIUS, + ViewProps.BORDER_END_END_RADIUS, + ViewProps.BORDER_END_START_RADIUS, + ViewProps.BORDER_START_END_RADIUS, + ViewProps.BORDER_START_START_RADIUS, + }) + public void setBorderRadius(ReactViewGroup view, int index, Dynamic rawBorderRadius) { + @Nullable LengthPercentage borderRadius = LengthPercentage.setFromDynamic(rawBorderRadius); + + // We do not support percentage border radii on Paper in order to be consistent with iOS (to + // avoid developer surprise if it works on one platform but not another). + if (ViewUtil.getUIManagerType(view) != UIManagerType.FABRIC + && borderRadius != null + && borderRadius.getType() == LengthPercentageType.PERCENT) { + borderRadius = null; + } + + BackgroundStyleApplicator.setBorderRadius(view, BorderRadiusProp.values()[index], borderRadius); + } + + /** + * @deprecated Use {@link #setBorderRadius(ReactViewGroup, int, Dynamic)} instead. + */ + @Deprecated(since = "0.75.0", forRemoval = true) + public void setBorderRadius(ReactViewGroup view, int index, float borderRadius) { + setBorderRadius(view, index, new DynamicFromObject(borderRadius)); + } + + @ReactProp(name = "borderStyle") + public void setBorderStyle(ReactViewGroup view, @Nullable String borderStyle) { + @Nullable + BorderStyle parsedBorderStyle = + borderStyle == null ? null : BorderStyle.fromString(borderStyle); + BackgroundStyleApplicator.setBorderStyle(view, parsedBorderStyle); + } + + @ReactProp(name = "hitSlop") + public void setHitSlop(final ReactViewGroup view, Dynamic hitSlop) { + switch (hitSlop.getType()) { + case Map: + ReadableMap hitSlopMap = hitSlop.asMap(); + view.setHitSlopRect( + new Rect( + hitSlopMap.hasKey("left") + ? (int) PixelUtil.toPixelFromDIP(hitSlopMap.getDouble("left")) + : 0, + hitSlopMap.hasKey("top") + ? (int) PixelUtil.toPixelFromDIP(hitSlopMap.getDouble("top")) + : 0, + hitSlopMap.hasKey("right") + ? (int) PixelUtil.toPixelFromDIP(hitSlopMap.getDouble("right")) + : 0, + hitSlopMap.hasKey("bottom") + ? (int) PixelUtil.toPixelFromDIP(hitSlopMap.getDouble("bottom")) + : 0)); + break; + case Number: + int hitSlopValue = (int) PixelUtil.toPixelFromDIP(hitSlop.asDouble()); + view.setHitSlopRect(new Rect(hitSlopValue, hitSlopValue, hitSlopValue, hitSlopValue)); + break; + default: + FLog.w(ReactConstants.TAG, "Invalid type for 'hitSlop' value " + hitSlop.getType()); + /* falls through */ + case Null: + view.setHitSlopRect(null); + break; + } + } + + @ReactProp(name = ViewProps.POINTER_EVENTS) + public void setPointerEvents(ReactViewGroup view, @Nullable String pointerEventsStr) { + view.setPointerEvents(PointerEvents.parsePointerEvents(pointerEventsStr)); + } + + @ReactProp(name = "nativeBackgroundAndroid") + public void setNativeBackground(ReactViewGroup view, @Nullable ReadableMap bg) { + view.setTranslucentBackgroundDrawable( + bg == null + ? null + : ReactDrawableHelper.createDrawableFromJSDescription(view.getContext(), bg)); + } + + @ReactProp(name = "nativeForegroundAndroid") + public void setNativeForeground(ReactViewGroup view, @Nullable ReadableMap fg) { + view.setForeground( + fg == null + ? null + : ReactDrawableHelper.createDrawableFromJSDescription(view.getContext(), fg)); + } + + @ReactProp(name = ViewProps.NEEDS_OFFSCREEN_ALPHA_COMPOSITING) + public void setNeedsOffscreenAlphaCompositing( + ReactViewGroup view, boolean needsOffscreenAlphaCompositing) { + view.setNeedsOffscreenAlphaCompositing(needsOffscreenAlphaCompositing); + } + + @ReactPropGroup( + names = { + ViewProps.BORDER_WIDTH, + ViewProps.BORDER_LEFT_WIDTH, + ViewProps.BORDER_RIGHT_WIDTH, + ViewProps.BORDER_TOP_WIDTH, + ViewProps.BORDER_BOTTOM_WIDTH, + ViewProps.BORDER_START_WIDTH, + ViewProps.BORDER_END_WIDTH, + }, + defaultFloat = Float.NaN) + public void setBorderWidth(ReactViewGroup view, int index, float width) { + BackgroundStyleApplicator.setBorderWidth(view, LogicalEdge.values()[index], width); + } + + @ReactPropGroup( + names = { + ViewProps.BORDER_COLOR, + ViewProps.BORDER_LEFT_COLOR, + ViewProps.BORDER_RIGHT_COLOR, + ViewProps.BORDER_TOP_COLOR, + ViewProps.BORDER_BOTTOM_COLOR, + ViewProps.BORDER_START_COLOR, + ViewProps.BORDER_END_COLOR, + ViewProps.BORDER_BLOCK_COLOR, + ViewProps.BORDER_BLOCK_END_COLOR, + ViewProps.BORDER_BLOCK_START_COLOR + }, + customType = "Color") + public void setBorderColor(ReactViewGroup view, int index, @Nullable Integer color) { + BackgroundStyleApplicator.setBorderColor( + view, LogicalEdge.fromSpacingType(SPACING_TYPES[index]), color); + } + + @ReactProp(name = ViewProps.COLLAPSABLE) + public void setCollapsable(ReactViewGroup view, boolean collapsable) { + // no-op: it's here only so that "collapsable" property is exported to JS. The value is actually + // handled in NativeViewHierarchyOptimizer + } + + @ReactProp(name = ViewProps.COLLAPSABLE_CHILDREN) + public void setCollapsableChildren(ReactViewGroup view, boolean collapsableChildren) { + // no-op: it's here only so that "collapsableChildren" property is exported to JS. + } + + @ReactProp(name = "focusable") + public void setFocusable(final ReactViewGroup view, boolean focusable) { + if (focusable) { + view.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + final EventDispatcher mEventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag( + (ReactContext) view.getContext(), view.getId()); + if (mEventDispatcher == null) { + return; + } + mEventDispatcher.dispatchEvent( + new ViewGroupClickEvent( + UIManagerHelper.getSurfaceId(view.getContext()), view.getId())); + } + }); + + // Clickable elements are focusable. On API 26, this is taken care by setClickable. + // Explicitly calling setFocusable here for backward compatibility. + view.setFocusable(true /*isFocusable*/); + } else { + view.setOnClickListener(null); + view.setClickable(false); + // Don't set view.setFocusable(false) because we might still want it to be focusable for + // accessibility reasons + } + } + + @ReactProp(name = ViewProps.OVERFLOW) + public void setOverflow(ReactViewGroup view, String overflow) { + view.setOverflow(overflow); + } + + @ReactProp(name = "backfaceVisibility") + public void setBackfaceVisibility(ReactViewGroup view, String backfaceVisibility) { + view.setBackfaceVisibility(backfaceVisibility); + } + + @Override + public void setOpacity(ReactViewGroup view, float opacity) { + view.setOpacityIfPossible(opacity); + } + + @Override + protected void setTransformProperty( + ReactViewGroup view, + @Nullable ReadableArray transforms, + @Nullable ReadableArray transformOrigin) { + super.setTransformProperty(view, transforms, transformOrigin); + view.setBackfaceVisibilityDependantOpacity(); + } + + @ReactProp(name = ViewProps.BOX_SHADOW, customType = "BoxShadow") + public void setBoxShadow(ReactViewGroup view, @Nullable ReadableArray shadows) { + BackgroundStyleApplicator.setBoxShadow(view, shadows); + } + + @Override + public void setBackgroundColor(ReactViewGroup view, @ColorInt int backgroundColor) { + BackgroundStyleApplicator.setBackgroundColor(view, backgroundColor); + } + + @Override + public String getName() { + return REACT_CLASS; + } + + @Override + public ReactViewGroup createViewInstance(ThemedReactContext context) { + return new ReactViewGroup(context); + } + + @Override + public Map getCommandsMap() { + return MapBuilder.of(HOTSPOT_UPDATE_KEY, CMD_HOTSPOT_UPDATE, "setPressed", CMD_SET_PRESSED); + } + + @Override + public void receiveCommand(ReactViewGroup root, int commandId, @Nullable ReadableArray args) { + switch (commandId) { + case CMD_HOTSPOT_UPDATE: + { + handleHotspotUpdate(root, args); + break; + } + case CMD_SET_PRESSED: + { + handleSetPressed(root, args); + break; + } + } + } + + @Override + public void receiveCommand(ReactViewGroup root, String commandId, @Nullable ReadableArray args) { + switch (commandId) { + case HOTSPOT_UPDATE_KEY: + { + handleHotspotUpdate(root, args); + break; + } + case "setPressed": + { + handleSetPressed(root, args); + break; + } + } + } + + private void handleSetPressed(ReactViewGroup root, @Nullable ReadableArray args) { + if (args == null || args.size() != 1) { + throw new JSApplicationIllegalArgumentException( + "Illegal number of arguments for 'setPressed' command"); + } + root.setPressed(args.getBoolean(0)); + } + + private void handleHotspotUpdate(ReactViewGroup root, @Nullable ReadableArray args) { + if (args == null || args.size() != 2) { + throw new JSApplicationIllegalArgumentException( + "Illegal number of arguments for 'updateHotspot' command"); + } + + float x = PixelUtil.toPixelFromDIP(args.getDouble(0)); + float y = PixelUtil.toPixelFromDIP(args.getDouble(1)); + root.drawableHotspotChanged(x, y); + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt deleted file mode 100644 index 4b6dc1957e26f1..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt +++ /dev/null @@ -1,390 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.views.view - -import android.graphics.Rect -import android.view.View -import androidx.annotation.ColorInt -import com.facebook.common.logging.FLog -import com.facebook.react.bridge.Dynamic -import com.facebook.react.bridge.DynamicFromObject -import com.facebook.react.bridge.JSApplicationIllegalArgumentException -import com.facebook.react.bridge.ReactContext -import com.facebook.react.bridge.ReadableArray -import com.facebook.react.bridge.ReadableMap -import com.facebook.react.bridge.ReadableType -import com.facebook.react.common.ReactConstants -import com.facebook.react.common.annotations.UnstableReactNativeAPI -import com.facebook.react.module.annotations.ReactModule -import com.facebook.react.uimanager.BackgroundStyleApplicator -import com.facebook.react.uimanager.LengthPercentage.Companion.setFromDynamic -import com.facebook.react.uimanager.LengthPercentageType -import com.facebook.react.uimanager.PixelUtil.dpToPx -import com.facebook.react.uimanager.PointerEvents.Companion.parsePointerEvents -import com.facebook.react.uimanager.Spacing -import com.facebook.react.uimanager.ThemedReactContext -import com.facebook.react.uimanager.UIManagerHelper -import com.facebook.react.uimanager.ViewProps -import com.facebook.react.uimanager.annotations.ReactProp -import com.facebook.react.uimanager.annotations.ReactPropGroup -import com.facebook.react.uimanager.common.UIManagerType -import com.facebook.react.uimanager.common.ViewUtil -import com.facebook.react.uimanager.style.BackgroundImageLayer -import com.facebook.react.uimanager.style.BorderRadiusProp -import com.facebook.react.uimanager.style.BorderStyle -import com.facebook.react.uimanager.style.BorderStyle.Companion.fromString -import com.facebook.react.uimanager.style.LogicalEdge -import com.facebook.react.uimanager.style.LogicalEdge.Companion.fromSpacingType - -/** View manager for AndroidViews (plain React Views). */ -@ReactModule(name = ReactViewManager.REACT_CLASS) -public open class ReactViewManager : ReactClippingViewManager() { - - init { - setupViewRecycling() - } - - override fun prepareToRecycleView( - reactContext: ThemedReactContext, - view: ReactViewGroup - ): ReactViewGroup { - // BaseViewManager - super.prepareToRecycleView(reactContext, view)?.recycleView() - return view - } - - @ReactProp(name = "accessible") - public open fun setAccessible(view: ReactViewGroup, accessible: Boolean): Unit { - view.isFocusable = accessible - } - - @ReactProp(name = "hasTVPreferredFocus") - public open fun setTVPreferredFocus(view: ReactViewGroup, hasTVPreferredFocus: Boolean): Unit { - if (hasTVPreferredFocus) { - view.isFocusable = true - view.isFocusableInTouchMode = true - view.requestFocus() - } - } - - @OptIn(UnstableReactNativeAPI::class) - @ReactProp(name = ViewProps.BACKGROUND_IMAGE, customType = "BackgroundImage") - public open fun setBackgroundImage(view: ReactViewGroup, backgroundImage: ReadableArray?): Unit { - if (ViewUtil.getUIManagerType(view) == UIManagerType.FABRIC) { - val size = backgroundImage?.size() - if (size != null && size > 0) { - val backgroundImageLayers = ArrayList(size) - repeat(size) { i -> - backgroundImageLayers.add(BackgroundImageLayer(backgroundImage.getMap(i), view.context)) - } - BackgroundStyleApplicator.setBackgroundImage(view, backgroundImageLayers) - } else { - BackgroundStyleApplicator.setBackgroundImage(view, null) - } - } - } - - @ReactProp(name = "nextFocusDown", defaultInt = View.NO_ID) - public open fun nextFocusDown(view: ReactViewGroup, viewId: Int): Unit { - view.nextFocusDownId = viewId - } - - @ReactProp(name = "nextFocusForward", defaultInt = View.NO_ID) - public open fun nextFocusForward(view: ReactViewGroup, viewId: Int): Unit { - view.nextFocusForwardId = viewId - } - - @ReactProp(name = "nextFocusLeft", defaultInt = View.NO_ID) - public open fun nextFocusLeft(view: ReactViewGroup, viewId: Int): Unit { - view.nextFocusLeftId = viewId - } - - @ReactProp(name = "nextFocusRight", defaultInt = View.NO_ID) - public open fun nextFocusRight(view: ReactViewGroup, viewId: Int): Unit { - view.nextFocusRightId = viewId - } - - @ReactProp(name = "nextFocusUp", defaultInt = View.NO_ID) - public open fun nextFocusUp(view: ReactViewGroup, viewId: Int): Unit { - view.nextFocusUpId = viewId - } - - @ReactPropGroup( - names = - [ - ViewProps.BORDER_RADIUS, - ViewProps.BORDER_TOP_LEFT_RADIUS, - ViewProps.BORDER_TOP_RIGHT_RADIUS, - ViewProps.BORDER_BOTTOM_RIGHT_RADIUS, - ViewProps.BORDER_BOTTOM_LEFT_RADIUS, - ViewProps.BORDER_TOP_START_RADIUS, - ViewProps.BORDER_TOP_END_RADIUS, - ViewProps.BORDER_BOTTOM_START_RADIUS, - ViewProps.BORDER_BOTTOM_END_RADIUS, - ViewProps.BORDER_END_END_RADIUS, - ViewProps.BORDER_END_START_RADIUS, - ViewProps.BORDER_START_END_RADIUS, - ViewProps.BORDER_START_START_RADIUS]) - public open fun setBorderRadius( - view: ReactViewGroup, - index: Int, - rawBorderRadius: Dynamic - ): Unit { - var borderRadius = setFromDynamic(rawBorderRadius) - - // We do not support percentage border radii on Paper in order to be consistent with iOS (to - // avoid developer surprise if it works on one platform but not another). - if (ViewUtil.getUIManagerType(view) != UIManagerType.FABRIC && - borderRadius?.type == LengthPercentageType.PERCENT) { - borderRadius = null - } - BackgroundStyleApplicator.setBorderRadius(view, BorderRadiusProp.values()[index], borderRadius) - } - - @Deprecated( - "Use setBorderRadius(ReactViewGroup, int, Dynamic) instead.", - ReplaceWith( - "setBorderRadius(view, index, DynamicFromObject(borderRadius))", - "com.facebook.react.bridge.DynamicFromObject.DynamicFromObject")) - public open fun setBorderRadius(view: ReactViewGroup, index: Int, borderRadius: Float): Unit { - this.setBorderRadius(view, index, DynamicFromObject(borderRadius)) - } - - @ReactProp(name = "borderStyle") - public open fun setBorderStyle(view: ReactViewGroup, borderStyle: String?): Unit { - val parsedBorderStyle = borderStyle?.let { BorderStyle.fromString(it) } - BackgroundStyleApplicator.setBorderStyle(view, parsedBorderStyle) - } - - @ReactProp(name = "hitSlop") - public open fun setHitSlop(view: ReactViewGroup, hitSlop: Dynamic): Unit { - when (hitSlop.type) { - ReadableType.Map -> { - val hitSlopMap = hitSlop.asMap() - view.setHitSlopRect( - Rect( - getPixels(hitSlopMap, "left"), - getPixels(hitSlopMap, "top"), - getPixels(hitSlopMap, "right"), - getPixels(hitSlopMap, "bottom"))) - } - - ReadableType.Number -> { - val hitSlopValue = hitSlop.asDouble().dpToPx().toInt() - view.setHitSlopRect(Rect(hitSlopValue, hitSlopValue, hitSlopValue, hitSlopValue)) - } - - ReadableType.Null -> view.setHitSlopRect(null) - else -> { - FLog.w(ReactConstants.TAG, "Invalid type for 'hitSlop' value ${hitSlop.type}") - view.setHitSlopRect(null) - } - } - } - - private fun getPixels(map: ReadableMap, key: String): Int = - if (map.hasKey(key)) { - map.getDouble(key).dpToPx().toInt() - } else { - 0 - } - - @ReactProp(name = ViewProps.POINTER_EVENTS) - public open fun setPointerEvents(view: ReactViewGroup, pointerEventsStr: String?): Unit { - view.setPointerEvents(parsePointerEvents(pointerEventsStr)) - } - - @ReactProp(name = "nativeBackgroundAndroid") - public open fun setNativeBackground(view: ReactViewGroup, background: ReadableMap?): Unit { - val translucentBg = - background?.let { ReactDrawableHelper.createDrawableFromJSDescription(view.context, it) } - BackgroundStyleApplicator.setFeedbackUnderlay(view, translucentBg) - } - - @ReactProp(name = "nativeForegroundAndroid") - public open fun setNativeForeground(view: ReactViewGroup, foreground: ReadableMap?): Unit { - view.foreground = - foreground?.let { ReactDrawableHelper.createDrawableFromJSDescription(view.context, it) } - } - - @ReactProp(name = ViewProps.NEEDS_OFFSCREEN_ALPHA_COMPOSITING) - public open fun setNeedsOffscreenAlphaCompositing( - view: ReactViewGroup, - needsOffscreenAlphaCompositing: Boolean - ): Unit { - view.setNeedsOffscreenAlphaCompositing(needsOffscreenAlphaCompositing) - } - - @ReactPropGroup( - names = - [ - ViewProps.BORDER_WIDTH, - ViewProps.BORDER_LEFT_WIDTH, - ViewProps.BORDER_RIGHT_WIDTH, - ViewProps.BORDER_TOP_WIDTH, - ViewProps.BORDER_BOTTOM_WIDTH, - ViewProps.BORDER_START_WIDTH, - ViewProps.BORDER_END_WIDTH], - defaultFloat = Float.NaN) - public open fun setBorderWidth(view: ReactViewGroup, index: Int, width: Float): Unit { - BackgroundStyleApplicator.setBorderWidth(view, LogicalEdge.values()[index], width) - } - - @ReactPropGroup( - names = - [ - ViewProps.BORDER_COLOR, - ViewProps.BORDER_LEFT_COLOR, - ViewProps.BORDER_RIGHT_COLOR, - ViewProps.BORDER_TOP_COLOR, - ViewProps.BORDER_BOTTOM_COLOR, - ViewProps.BORDER_START_COLOR, - ViewProps.BORDER_END_COLOR, - ViewProps.BORDER_BLOCK_COLOR, - ViewProps.BORDER_BLOCK_END_COLOR, - ViewProps.BORDER_BLOCK_START_COLOR], - customType = "Color") - public open fun setBorderColor(view: ReactViewGroup, index: Int, color: Int?): Unit { - BackgroundStyleApplicator.setBorderColor( - view, LogicalEdge.fromSpacingType(SPACING_TYPES[index]), color) - } - - @ReactProp(name = ViewProps.COLLAPSABLE) - @Suppress("UNUSED_PARAMETER") - public open fun setCollapsable(view: ReactViewGroup?, collapsable: Boolean): Unit { - // no-op: it's here only so that "collapsable" property is exported to JS. The value is actually - // handled in NativeViewHierarchyOptimizer - } - - @ReactProp(name = ViewProps.COLLAPSABLE_CHILDREN) - @Suppress("UNUSED_PARAMETER") - public open fun setCollapsableChildren( - view: ReactViewGroup?, - collapsableChildren: Boolean - ): Unit { - // no-op: it's here only so that "collapsableChildren" property is exported to JS. - } - - @ReactProp(name = "focusable") - public open fun setFocusable(view: ReactViewGroup, focusable: Boolean): Unit { - if (focusable) { - view.setOnClickListener { - val eventDispatcher = - UIManagerHelper.getEventDispatcherForReactTag((view.context as ReactContext), view.id) - eventDispatcher?.dispatchEvent( - ViewGroupClickEvent(UIManagerHelper.getSurfaceId(view.context), view.id)) - } - - // Clickable elements are focusable. On API 26, this is taken care by setClickable. - // Explicitly calling setFocusable here for backward compatibility. - view.isFocusable = true - } else { - view.setOnClickListener(null) - view.isClickable = false - // Don't set view.setFocusable(false) because we might still want it to be focusable for - // accessibility reasons - } - } - - @ReactProp(name = ViewProps.OVERFLOW) - public open fun setOverflow(view: ReactViewGroup, overflow: String): Unit { - view.overflow = overflow - } - - @ReactProp(name = "backfaceVisibility") - public open fun setBackfaceVisibility(view: ReactViewGroup, backfaceVisibility: String): Unit { - view.setBackfaceVisibility(backfaceVisibility) - } - - override fun setOpacity(view: ReactViewGroup, opacity: Float) { - view.setOpacityIfPossible(opacity) - } - - override fun setTransformProperty( - view: ReactViewGroup, - transforms: ReadableArray?, - transformOrigin: ReadableArray? - ) { - super.setTransformProperty(view, transforms, transformOrigin) - view.setBackfaceVisibilityDependantOpacity() - } - - @ReactProp(name = ViewProps.BOX_SHADOW, customType = "BoxShadow") - public open fun setBoxShadow(view: ReactViewGroup, shadows: ReadableArray?): Unit { - BackgroundStyleApplicator.setBoxShadow(view, shadows) - } - - override fun setBackgroundColor(view: ReactViewGroup, @ColorInt backgroundColor: Int) { - BackgroundStyleApplicator.setBackgroundColor(view, backgroundColor) - } - - override fun getName(): String = REACT_CLASS - - public override fun createViewInstance(context: ThemedReactContext): ReactViewGroup = - ReactViewGroup(context) - - override fun getCommandsMap(): Map = - mapOf( - HOTSPOT_UPDATE_KEY to CMD_HOTSPOT_UPDATE, - "setPressed" to CMD_SET_PRESSED, - ) - - @Deprecated("Deprecated in ViewManager") - override fun receiveCommand(root: ReactViewGroup, commandId: Int, args: ReadableArray?) { - when (commandId) { - CMD_HOTSPOT_UPDATE -> handleHotspotUpdate(root, args) - CMD_SET_PRESSED -> handleSetPressed(root, args) - else -> {} - } - } - - override fun receiveCommand(root: ReactViewGroup, commandId: String, args: ReadableArray?) { - when (commandId) { - HOTSPOT_UPDATE_KEY -> handleHotspotUpdate(root, args) - "setPressed" -> handleSetPressed(root, args) - else -> {} - } - } - - private fun handleSetPressed(root: ReactViewGroup, args: ReadableArray?) { - if (args?.size() != 1) { - throw JSApplicationIllegalArgumentException( - "Illegal number of arguments for 'setPressed' command") - } - root.isPressed = args.getBoolean(0) - } - - private fun handleHotspotUpdate(root: ReactViewGroup, args: ReadableArray?) { - if (args?.size() != 2) { - throw JSApplicationIllegalArgumentException( - "Illegal number of arguments for 'updateHotspot' command") - } - val x = args.getDouble(0).dpToPx() - val y = args.getDouble(1).dpToPx() - root.drawableHotspotChanged(x, y) - } - - public companion object { - public const val REACT_CLASS: String = ViewProps.VIEW_CLASS_NAME - private val SPACING_TYPES = - intArrayOf( - Spacing.ALL, - Spacing.LEFT, - Spacing.RIGHT, - Spacing.TOP, - Spacing.BOTTOM, - Spacing.START, - Spacing.END, - Spacing.BLOCK, - Spacing.BLOCK_END, - Spacing.BLOCK_START) - private const val CMD_HOTSPOT_UPDATE = 1 - private const val CMD_SET_PRESSED = 2 - private const val HOTSPOT_UPDATE_KEY = "hotspotUpdate" - } -} From d93e67d02c472b997ceec79e2c6c700cbb902165 Mon Sep 17 00:00:00 2001 From: David Vacca Date: Thu, 26 Sep 2024 22:02:42 -0700 Subject: [PATCH 035/296] Enable Microtasks, ModernRuntimeScheduler and NativeViewConfigsInBridgelessMode by default only for New Architecture (#46683) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46683 Enabling these Microtask, ModernRuntimeScheduler and NativeViewConfigsInBridgelessMode in BridgeMode is risky and leads to bugs. In this diff I'm ensuring we only enable these flags when newArchitecture is enabled changelog: [internal] internal Reviewed By: shwanton Differential Revision: D63503519 fbshipit-source-id: 4ef757834b8f7fba595b3394735f4b91335d7c98 --- .../ReactNativeNewArchitectureFeatureFlagsDefaults.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeNewArchitectureFeatureFlagsDefaults.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeNewArchitectureFeatureFlagsDefaults.kt index 97e20f572a0256..985dd7e0777a4f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeNewArchitectureFeatureFlagsDefaults.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeNewArchitectureFeatureFlagsDefaults.kt @@ -30,15 +30,17 @@ public open class ReactNativeNewArchitectureFeatureFlagsDefaults( override fun useTurboModuleInterop(): Boolean = newArchitectureEnabled || super.useTurboModuleInterop() - override fun useModernRuntimeScheduler(): Boolean = true + override fun useModernRuntimeScheduler(): Boolean = + newArchitectureEnabled || super.useModernRuntimeScheduler() override fun enableBridgelessArchitecture(): Boolean = newArchitectureEnabled - override fun enableMicrotasks(): Boolean = true + override fun enableMicrotasks(): Boolean = newArchitectureEnabled || super.enableMicrotasks() override fun enableFabricRenderer(): Boolean = newArchitectureEnabled - override fun useNativeViewConfigsInBridgelessMode(): Boolean = true + override fun useNativeViewConfigsInBridgelessMode(): Boolean = + newArchitectureEnabled || super.useNativeViewConfigsInBridgelessMode() override fun useTurboModules(): Boolean = newArchitectureEnabled } From c2c9b78e496d6f863604a015eeee2748372b38b2 Mon Sep 17 00:00:00 2001 From: Sam Zhou Date: Thu, 26 Sep 2024 22:25:21 -0700 Subject: [PATCH 036/296] Pre-suppress errors ahead of v0.247 release Summary: drop-conflicts Changelog: [Internal] Reviewed By: panagosg7 Differential Revision: D63506234 fbshipit-source-id: 186d60e42a8a2a3604058692ed844ba764d6ad3f --- .../Components/ProgressBarAndroid/ProgressBarAndroid.android.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-native/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js b/packages/react-native/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js index 3987a7ea3e5e37..dd18cc4d2dce8a 100644 --- a/packages/react-native/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js +++ b/packages/react-native/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js @@ -80,6 +80,7 @@ export type ProgressBarAndroidProps = $ReadOnly<{| */ const ProgressBarAndroid = ( { + // $FlowFixMe[incompatible-type] styleAttr = 'Normal', indeterminate = true, animating = true, From efe51044b81b58687b99f5a219028b162cf561a2 Mon Sep 17 00:00:00 2001 From: Sam Zhou Date: Fri, 27 Sep 2024 00:23:22 -0700 Subject: [PATCH 037/296] Deploy 0.247.0 to fbsource Summary: Changelog: [Internal] Reviewed By: panagosg7 Differential Revision: D63506281 fbshipit-source-id: a992a1786a570f5d9562fbec71e0d2628c139bf3 --- .flowconfig | 2 +- package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.flowconfig b/.flowconfig index 03ad08d820a74e..caeeaffe306de6 100644 --- a/.flowconfig +++ b/.flowconfig @@ -90,4 +90,4 @@ untyped-import untyped-type-import [version] -^0.246.0 +^0.247.0 diff --git a/package.json b/package.json index bdb2ebbf07ed8c..2e6906fbd608b7 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "eslint-plugin-redundant-undefined": "^0.4.0", "eslint-plugin-relay": "^1.8.3", "flow-api-translator": "0.23.1", - "flow-bin": "^0.246.0", + "flow-bin": "^0.247.0", "glob": "^7.1.1", "hermes-eslint": "0.23.1", "hermes-transform": "0.23.1", diff --git a/yarn.lock b/yarn.lock index c23b9122df8de7..e2fce1b5c1d98a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4484,10 +4484,10 @@ flow-api-translator@0.23.1: hermes-transform "0.23.1" typescript "5.3.2" -flow-bin@^0.246.0: - version "0.246.0" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.246.0.tgz#d08a696e6f796c497f7a7e98b22057dd0df0c38d" - integrity sha512-RB0gaPxrehIE0WoUbUMKgX/XpaY55JbtBhNhix5WzKFIDmRTVojJa+EJEbrTlo2BWRAZnnqOl13wanHmWwb1eA== +flow-bin@^0.247.0: + version "0.247.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.247.0.tgz#2ccac4f28e43472afd7cee5c18455ef499eafad8" + integrity sha512-FVRHfwUWvnIzJvHG4dertQfJbNXz+W3jyspJPTu3pJacYCddu4xiuYo4F0vjqTvn3Sa13rkjt2EWuqjWSJuz9Q== flow-enums-runtime@^0.0.6: version "0.0.6" From ecd660984ce4109bc1ddeb6aece03aaf707ef596 Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Fri, 27 Sep 2024 00:43:25 -0700 Subject: [PATCH 038/296] Gradle to 8.10.2 (#46656) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46656 This contains the fix for: - https://github.com/gradle/gradle/issues/30472 Changelog: [Internal] [Changed] - Gradle to 8.10.2 Reviewed By: tdn120 Differential Revision: D63457979 fbshipit-source-id: 1439a9ce198c1df0dafa8f5088c079c3fb3d1543 --- gradle/wrapper/gradle-wrapper.properties | 2 +- packages/gradle-plugin/gradle/wrapper/gradle-wrapper.properties | 2 +- .../helloworld/android/gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1ed247ec13c26c..79eb9d003feaef 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/packages/gradle-plugin/gradle/wrapper/gradle-wrapper.properties b/packages/gradle-plugin/gradle/wrapper/gradle-wrapper.properties index 1ed247ec13c26c..79eb9d003feaef 100644 --- a/packages/gradle-plugin/gradle/wrapper/gradle-wrapper.properties +++ b/packages/gradle-plugin/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/packages/helloworld/android/gradle/wrapper/gradle-wrapper.properties b/packages/helloworld/android/gradle/wrapper/gradle-wrapper.properties index 1ed247ec13c26c..79eb9d003feaef 100644 --- a/packages/helloworld/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/helloworld/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 7176d11ce468ca37e9cbe8afcc1f6344c250dd0c Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Fri, 27 Sep 2024 01:49:38 -0700 Subject: [PATCH 039/296] ReactFragment should properly instantiate ReactDelegate on Bridgeless (#46623) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46623 I've just noticed that ReactFragment is not properly instantiating the `ReactDelegate` with a ReactHost when on Bridgeless. This causes Fragments to crash when the app is on bridgeless mode. Fixes https://github.com/facebook/react-native/issues/46566 Changelog: [Android] [Fixed] - ReactFragment should properly instantiate ReactDelegate on Bridgeless Reviewed By: mdvacca Differential Revision: D63319977 fbshipit-source-id: 08256e35b2769e18df2d24f870ec5d98e5574f85 --- .../ReactAndroid/api/ReactAndroid.api | 1 + .../com/facebook/react/ReactFragment.java | 40 +++++++++++++++++-- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index e1909ad0345138..ec0187c019c11d 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -192,6 +192,7 @@ public class com/facebook/react/ReactFragment : androidx/fragment/app/Fragment, public fun checkPermission (Ljava/lang/String;II)I public fun checkSelfPermission (Ljava/lang/String;)I protected fun getReactDelegate ()Lcom/facebook/react/ReactDelegate; + protected fun getReactHost ()Lcom/facebook/react/ReactHost; protected fun getReactNativeHost ()Lcom/facebook/react/ReactNativeHost; public fun onActivityResult (IILandroid/content/Intent;)V public fun onBackPressed ()Z diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactFragment.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactFragment.java index 894f46c06bcaa2..b16f286f4f6649 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactFragment.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactFragment.java @@ -17,6 +17,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags; import com.facebook.react.modules.core.PermissionAwareActivity; import com.facebook.react.modules.core.PermissionListener; @@ -80,9 +81,14 @@ public void onCreate(Bundle savedInstanceState) { if (mainComponentName == null) { throw new IllegalStateException("Cannot loadApp if component name is null"); } - mReactDelegate = - new ReactDelegate( - getActivity(), getReactNativeHost(), mainComponentName, launchOptions, fabricEnabled); + if (ReactNativeFeatureFlags.enableBridgelessArchitecture()) { + mReactDelegate = + new ReactDelegate(getActivity(), getReactHost(), mainComponentName, launchOptions); + } else { + mReactDelegate = + new ReactDelegate( + getActivity(), getReactNativeHost(), mainComponentName, launchOptions, fabricEnabled); + } } /** @@ -92,8 +98,34 @@ public void onCreate(Bundle savedInstanceState) { * implement {@code ReactApplication} or you simply have a different mechanism for storing a * {@code ReactNativeHost}, e.g. as a static field somewhere. */ + @Nullable protected ReactNativeHost getReactNativeHost() { - return ((ReactApplication) getActivity().getApplication()).getReactNativeHost(); + ReactApplication application = ((ReactApplication) getActivity().getApplication()); + if (application != null) { + return application.getReactNativeHost(); + } else { + return null; + } + } + + /** + * Get the {@link ReactHost} used by this app. By default, assumes {@link + * Activity#getApplication()} is an instance of {@link ReactApplication} and calls {@link + * ReactApplication#getReactHost()}. Override this method if your application class does not + * implement {@code ReactApplication} or you simply have a different mechanism for storing a + * {@code ReactHost}, e.g. as a static field somewhere. + * + *

If you're using Old Architecture/Bridge Mode, this method should return null as {@link + * ReactHost} is a Bridgeless-only concept. + */ + @Nullable + protected ReactHost getReactHost() { + ReactApplication application = ((ReactApplication) getActivity().getApplication()); + if (application != null) { + return application.getReactHost(); + } else { + return null; + } } protected ReactDelegate getReactDelegate() { From 12dda31bc12f4a04ec9df70d30328b1001bb2cec Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Fri, 27 Sep 2024 01:49:38 -0700 Subject: [PATCH 040/296] fix: ReactDelegate crashing New Architecture apps by invoking `setContentView` (#46671) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46671 Fixes https://github.com/facebook/react-native/issues/46566 Currently `ReactFragment` and `ReactDelegate` don't work in OSS + New Architecture because we call `Activity.setContentView` on the host activity. That result on us replacing the whole activity layout, even when the user wants to use a Fragment. As we do have `ReactActivityDelegate` that already does this: https://github.com/facebook/react-native/blob/94b77938435693792e57c96d76691d58d7361530/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java#L138 So this call is unncessary. I've also updated the relative documentation here: https://github.com/facebook/react-native-website/pull/4232 Changelog: [Android] [Fixed] - fix: ReactDelegate/ReactFragment crashing on New Architecture apps Reviewed By: rozele Differential Revision: D63464367 fbshipit-source-id: acbfbf7d68eb79657b811a5a9a0d3f72660ec94a --- .../src/main/java/com/facebook/react/ReactDelegate.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactDelegate.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactDelegate.java index c7b8ce77297540..090b55f59ee129 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactDelegate.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactDelegate.java @@ -288,10 +288,7 @@ public void loadApp(String appKey) { // With Bridgeless enabled, create and start the surface if (ReactNativeFeatureFlags.enableBridgelessArchitecture()) { if (mReactSurface == null) { - // Create a ReactSurface mReactSurface = mReactHost.createSurface(mActivity, appKey, mLaunchOptions); - // Set main Activity's content view - mActivity.setContentView(mReactSurface.getView()); } mReactSurface.start(); } else { From 8237e237ce6ee62a3d0579c7b1c237f996958f27 Mon Sep 17 00:00:00 2001 From: Blake Friedman Date: Fri, 27 Sep 2024 03:38:19 -0700 Subject: [PATCH 041/296] cli: fix init when called as npx react-native init (#46677) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46677 Since removing `react-native-community/cli` as a dependency in 0.76 the `npx react-native init` command isn't working. This is the deprecated way to run this command, but users should still expect it to work for now. This now forks this kind of request to `npx react-native-community/cli init ` as described in the warning logs to the user. Changelog: [Internal] Issue: reactwg/react-native-releases#508 Reviewed By: cortinico Differential Revision: D63467046 fbshipit-source-id: 84560bdae8d6f62629dee61da3cbbf544b9a83b2 --- packages/react-native/cli.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/react-native/cli.js b/packages/react-native/cli.js index 9ad4ba91b45cfe..23a9f1779ac60a 100755 --- a/packages/react-native/cli.js +++ b/packages/react-native/cli.js @@ -15,6 +15,7 @@ const chalk = require('chalk'); const {get} = require('https'); const semver = require('semver'); const {URL} = require('url'); +const {spawn} = require('child_process'); const deprecated = () => { throw new Error( @@ -200,6 +201,19 @@ async function main() { warnWithDeprecationSchedule(); } warnWhenRunningInit(); + + const proc = spawn( + 'npx', + ['@react-native-community/cli', ...process.argv.slice(2)], + { + stdio: 'inherit', + }, + ); + + const code = await new Promise(resolve => { + proc.on('exit', resolve); + }); + process.exit(code); } try { From 8bfd7e10393e649554c7246df430019c4f78d5e0 Mon Sep 17 00:00:00 2001 From: zhongwuzw Date: Fri, 27 Sep 2024 06:47:53 -0700 Subject: [PATCH 042/296] Fixes Appearance getColorScheme sync method UI thread issue (#46344) Summary: If the module sets the method queue to the main queue, we should call it on the main queue if it contains some UI operations, otherwise it may lead to some undefined behavior. ## Changelog: [IOS] [FIXED] - Fixes the exported synchronous method not being called on the method queue when it's the main queue Pull Request resolved: https://github.com/facebook/react-native/pull/46344 Test Plan: The sync method should be called on the main queue if the module's method queue is main queue. Reviewed By: cipolleschi Differential Revision: D63532525 Pulled By: javache fbshipit-source-id: 55baaa60af96bb1355d3641174f23bccd8eb9344 --- packages/react-native/React/CoreModules/RCTAppearance.mm | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/react-native/React/CoreModules/RCTAppearance.mm b/packages/react-native/React/CoreModules/RCTAppearance.mm index b5bcc313a826f1..fee33751b910cf 100644 --- a/packages/react-native/React/CoreModules/RCTAppearance.mm +++ b/packages/react-native/React/CoreModules/RCTAppearance.mm @@ -130,7 +130,10 @@ - (dispatch_queue_t)methodQueue RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSString *, getColorScheme) { if (!sIsAppearancePreferenceSet) { - UITraitCollection *traitCollection = RCTKeyWindow().traitCollection; + __block UITraitCollection *traitCollection = nil; + RCTUnsafeExecuteOnMainQueueSync(^{ + traitCollection = RCTKeyWindow().traitCollection; + }); _currentColorScheme = RCTColorSchemePreference(traitCollection); } return _currentColorScheme; From 1bdae07d89fdda486d3f9dbcc4aa5cbb026fc8b6 Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Fri, 27 Sep 2024 07:49:52 -0700 Subject: [PATCH 043/296] Add Android implementation for DevMenu Module (#46694) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46694 The DevMenu module was never implemented on Android. This adds its implementation by mirroring the iOS implementation. Fixes https://github.com/facebook/react-native/issues/46679 Changelog: [Android] [Fixed] - Add missing Android implementation for DevMenu Module Reviewed By: cipolleschi Differential Revision: D63535172 fbshipit-source-id: 791e72b46b7d3264b98e85a73f2d9025dc3a2c7d --- .../ReactAndroid/api/ReactAndroid.api | 9 ++++ .../facebook/react/CoreModulesPackage.java | 5 ++ .../react/modules/debug/DevMenuModule.kt | 46 +++++++++++++++++++ .../react/runtime/CoreReactPackage.java | 5 ++ 4 files changed, 65 insertions(+) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/debug/DevMenuModule.kt diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index ec0187c019c11d..fecd4764fdd3a1 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -3310,6 +3310,15 @@ public final class com/facebook/react/modules/core/TimingModule : com/facebook/f public final class com/facebook/react/modules/core/TimingModule$Companion { } +public final class com/facebook/react/modules/debug/DevMenuModule : com/facebook/fbreact/specs/NativeDevMenuSpec { + public fun (Lcom/facebook/react/bridge/ReactApplicationContext;Lcom/facebook/react/devsupport/interfaces/DevSupportManager;)V + public fun debugRemotely (Z)V + public fun reload ()V + public fun setHotLoadingEnabled (Z)V + public fun setProfilingEnabled (Z)V + public fun show ()V +} + public final class com/facebook/react/modules/debug/DevSettingsModule : com/facebook/fbreact/specs/NativeDevSettingsSpec { public fun (Lcom/facebook/react/bridge/ReactApplicationContext;Lcom/facebook/react/devsupport/interfaces/DevSupportManager;)V public fun addListener (Ljava/lang/String;)V diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java index 3e82450cb0c5b9..189b517a2a0796 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java @@ -27,6 +27,7 @@ import com.facebook.react.modules.core.ExceptionsManagerModule; import com.facebook.react.modules.core.HeadlessJsTaskSupportModule; import com.facebook.react.modules.core.TimingModule; +import com.facebook.react.modules.debug.DevMenuModule; import com.facebook.react.modules.debug.DevSettingsModule; import com.facebook.react.modules.debug.SourceCodeModule; import com.facebook.react.modules.deviceinfo.DeviceInfoModule; @@ -49,6 +50,7 @@ AndroidInfoModule.class, DeviceEventManagerModule.class, DeviceInfoModule.class, + DevMenuModule.class, DevSettingsModule.class, ExceptionsManagerModule.class, LogBoxModule.class, @@ -108,6 +110,7 @@ private ReactModuleInfoProvider fallbackForMissingClass() { AndroidInfoModule.class, DeviceEventManagerModule.class, DeviceInfoModule.class, + DevMenuModule.class, DevSettingsModule.class, ExceptionsManagerModule.class, LogBoxModule.class, @@ -142,6 +145,8 @@ public NativeModule getModule(String name, ReactApplicationContext reactContext) return new AndroidInfoModule(reactContext); case DeviceEventManagerModule.NAME: return new DeviceEventManagerModule(reactContext, mHardwareBackBtnHandler); + case DevMenuModule.NAME: + return new DevMenuModule(reactContext, mReactInstanceManager.getDevSupportManager()); case DevSettingsModule.NAME: return new DevSettingsModule(reactContext, mReactInstanceManager.getDevSupportManager()); case ExceptionsManagerModule.NAME: diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/debug/DevMenuModule.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/debug/DevMenuModule.kt new file mode 100644 index 00000000000000..50fc4013cd7b46 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/debug/DevMenuModule.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.modules.debug + +import com.facebook.fbreact.specs.NativeDevMenuSpec +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.UiThreadUtil +import com.facebook.react.devsupport.interfaces.DevSupportManager +import com.facebook.react.module.annotations.ReactModule + +/** Module that exposes the DevMenu to JS so that it can be used to programmatically open it. */ +@ReactModule(name = NativeDevMenuSpec.NAME) +public class DevMenuModule( + reactContext: ReactApplicationContext?, + private val devSupportManager: DevSupportManager +) : NativeDevMenuSpec(reactContext) { + + override fun show() { + if (devSupportManager.devSupportEnabled) { + devSupportManager.showDevOptionsDialog() + } + } + + override fun reload() { + if (devSupportManager.devSupportEnabled) { + UiThreadUtil.runOnUiThread { devSupportManager.handleReloadJS() } + } + } + + override fun debugRemotely(enableDebug: Boolean) { + devSupportManager.setRemoteJSDebugEnabled(enableDebug) + } + + override fun setProfilingEnabled(enabled: Boolean) { + // iOS only + } + + override fun setHotLoadingEnabled(enabled: Boolean) { + devSupportManager.setHotModuleReplacementEnabled(enabled) + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/CoreReactPackage.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/CoreReactPackage.java index e1cc76bd0dbb0e..612010bea97b08 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/CoreReactPackage.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/CoreReactPackage.java @@ -23,6 +23,7 @@ import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; import com.facebook.react.modules.core.DeviceEventManagerModule; import com.facebook.react.modules.core.ExceptionsManagerModule; +import com.facebook.react.modules.debug.DevMenuModule; import com.facebook.react.modules.debug.DevSettingsModule; import com.facebook.react.modules.debug.SourceCodeModule; import com.facebook.react.modules.deviceinfo.DeviceInfoModule; @@ -35,6 +36,7 @@ nativeModules = { AndroidInfoModule.class, DeviceInfoModule.class, + DevMenuModule.class, DevSettingsModule.class, SourceCodeModule.class, LogBoxModule.class, @@ -61,6 +63,8 @@ public CoreReactPackage( return new DeviceInfoModule(reactContext); case SourceCodeModule.NAME: return new SourceCodeModule(reactContext); + case DevMenuModule.NAME: + return new DevMenuModule(reactContext, mDevSupportManager); case DevSettingsModule.NAME: return new DevSettingsModule(reactContext, mDevSupportManager); case DeviceEventManagerModule.NAME: @@ -108,6 +112,7 @@ private ReactModuleInfoProvider fallbackForMissingClass() { AndroidInfoModule.class, DeviceInfoModule.class, SourceCodeModule.class, + DevMenuModule.class, DevSettingsModule.class, DeviceEventManagerModule.class, LogBoxModule.class, From ed4f6d68910677ab050e3fef5aaa87e42d582fe0 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Fri, 27 Sep 2024 08:12:45 -0700 Subject: [PATCH 044/296] upgrade[react-devtools-core]: 6.0.0 (#46670) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46670 # Changelog: [General] [Changed] - upgrade React DevTools to 6.0.0. allow-large-files Reviewed By: huntie Differential Revision: D63460308 fbshipit-source-id: 159b0a0a074aae52c3710730a4fc2cbccb04970d --- packages/react-native/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/react-native/package.json b/packages/react-native/package.json index 14430ccf69f85e..0999c7eb31bfa7 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -136,7 +136,7 @@ "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", - "react-devtools-core": "^5.3.1", + "react-devtools-core": "^6.0.0", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.25.0-rc-fb9a90fa48-20240614", diff --git a/yarn.lock b/yarn.lock index e2fce1b5c1d98a..cfa87e7c651eec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7379,10 +7379,10 @@ range-parser@~1.2.1: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -react-devtools-core@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-5.3.1.tgz#d57f5b8f74f16e622bd6a7bc270161e4ba162666" - integrity sha512-7FSb9meX0btdBQLwdFOwt6bGqvRPabmVMMslv8fgoSPqXyuGpgQe36kx8gR86XPw7aV1yVouTp6fyZ0EH+NfUw== +react-devtools-core@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-6.0.0.tgz#a1e3964677fb0c81ca391ed66e5b5d22621d9304" + integrity sha512-VmW+VhR9mR2difSed80BNpYLkYyWYzsZgdLPlakoFPYGiD7fwC36tp4HLw45N4sRqY+Te6ZWD9MVSex8d4gFxg== dependencies: shell-quote "^1.6.1" ws "^7" From 6173d6ca42db4b2fdc7d6ea594d7ab945cb04632 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Fri, 27 Sep 2024 08:12:45 -0700 Subject: [PATCH 045/296] Update debugger-frontend from e8c7943...87b2feb (#46673) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46673 Changelog: [Internal] - Update `react-native/debugger-frontend` from e8c7943...87b2feb Resyncs `react-native/debugger-frontend` from GitHub - see `rn-chrome-devtools-frontend` [changelog](https://github.com/facebookexperimental/rn-chrome-devtools-frontend/compare/e8c79432972029c625c91d16967b07fe61f04a61...87b2feb14627b3e8713bc2637992b9a8c76bbe61). Reviewed By: huntie Differential Revision: D63455520 fbshipit-source-id: 07776c17f368e895a2d526cbb9abb8e1104bb29c --- packages/debugger-frontend/BUILD_INFO | 4 ++-- .../front_end/core/i18n/locales/en-US.json | 2 +- .../third_party/react-devtools/react-devtools.js | 12 ++++++------ .../dist/third-party/front_end/ui/legacy/legacy.js | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/debugger-frontend/BUILD_INFO b/packages/debugger-frontend/BUILD_INFO index f53b6b480c7205..e5a3e14dea5cbe 100644 --- a/packages/debugger-frontend/BUILD_INFO +++ b/packages/debugger-frontend/BUILD_INFO @@ -1,5 +1,5 @@ -@generated SignedSource<<85fb986aee1b601b649481b64d2b5dc8>> -Git revision: e8c79432972029c625c91d16967b07fe61f04a61 +@generated SignedSource<> +Git revision: 87b2feb14627b3e8713bc2637992b9a8c76bbe61 Built with --nohooks: false Is local checkout: false Remote URL: https://github.com/facebookexperimental/rn-chrome-devtools-frontend diff --git a/packages/debugger-frontend/dist/third-party/front_end/core/i18n/locales/en-US.json b/packages/debugger-frontend/dist/third-party/front_end/core/i18n/locales/en-US.json index 3f152f4fd9b5d4..d623b25a93ff32 100644 --- a/packages/debugger-frontend/dist/third-party/front_end/core/i18n/locales/en-US.json +++ b/packages/debugger-frontend/dist/third-party/front_end/core/i18n/locales/en-US.json @@ -1 +1 @@ -{"core/common/ResourceType.ts | cspviolationreport":{"message":"CSPViolationReport"},"core/common/ResourceType.ts | css":{"message":"CSS"},"core/common/ResourceType.ts | doc":{"message":"Doc"},"core/common/ResourceType.ts | document":{"message":"Document"},"core/common/ResourceType.ts | eventsource":{"message":"EventSource"},"core/common/ResourceType.ts | fetch":{"message":"Fetch"},"core/common/ResourceType.ts | fetchAndXHR":{"message":"Fetch and XHR"},"core/common/ResourceType.ts | font":{"message":"Font"},"core/common/ResourceType.ts | image":{"message":"Image"},"core/common/ResourceType.ts | img":{"message":"Img"},"core/common/ResourceType.ts | javascript":{"message":"JavaScript"},"core/common/ResourceType.ts | js":{"message":"JS"},"core/common/ResourceType.ts | manifest":{"message":"Manifest"},"core/common/ResourceType.ts | media":{"message":"Media"},"core/common/ResourceType.ts | other":{"message":"Other"},"core/common/ResourceType.ts | ping":{"message":"Ping"},"core/common/ResourceType.ts | preflight":{"message":"Preflight"},"core/common/ResourceType.ts | script":{"message":"Script"},"core/common/ResourceType.ts | signedexchange":{"message":"SignedExchange"},"core/common/ResourceType.ts | stylesheet":{"message":"Stylesheet"},"core/common/ResourceType.ts | texttrack":{"message":"TextTrack"},"core/common/ResourceType.ts | wasm":{"message":"Wasm"},"core/common/ResourceType.ts | webassembly":{"message":"WebAssembly"},"core/common/ResourceType.ts | webbundle":{"message":"WebBundle"},"core/common/ResourceType.ts | websocket":{"message":"WebSocket"},"core/common/ResourceType.ts | webtransport":{"message":"WebTransport"},"core/common/ResourceType.ts | ws":{"message":"WS"},"core/common/Revealer.ts | applicationPanel":{"message":"Application panel"},"core/common/Revealer.ts | changesDrawer":{"message":"Changes drawer"},"core/common/Revealer.ts | elementsPanel":{"message":"Elements panel"},"core/common/Revealer.ts | issuesView":{"message":"Issues view"},"core/common/Revealer.ts | memoryInspectorPanel":{"message":"Memory inspector panel"},"core/common/Revealer.ts | networkPanel":{"message":"Network panel"},"core/common/Revealer.ts | sourcesPanel":{"message":"Sources panel"},"core/common/Revealer.ts | stylesSidebar":{"message":"styles sidebar"},"core/common/SettingRegistration.ts | adorner":{"message":"Adorner"},"core/common/SettingRegistration.ts | appearance":{"message":"Appearance"},"core/common/SettingRegistration.ts | console":{"message":"Console"},"core/common/SettingRegistration.ts | debugger":{"message":"Debugger"},"core/common/SettingRegistration.ts | elements":{"message":"Elements"},"core/common/SettingRegistration.ts | extension":{"message":"Extension"},"core/common/SettingRegistration.ts | global":{"message":"Global"},"core/common/SettingRegistration.ts | grid":{"message":"Grid"},"core/common/SettingRegistration.ts | memory":{"message":"Memory"},"core/common/SettingRegistration.ts | mobile":{"message":"Mobile"},"core/common/SettingRegistration.ts | network":{"message":"Network"},"core/common/SettingRegistration.ts | performance":{"message":"Performance"},"core/common/SettingRegistration.ts | persistence":{"message":"Persistence"},"core/common/SettingRegistration.ts | rendering":{"message":"Rendering"},"core/common/SettingRegistration.ts | sources":{"message":"Sources"},"core/common/SettingRegistration.ts | sync":{"message":"Sync"},"core/host/InspectorFrontendHost.ts | devtoolsS":{"message":"DevTools - {PH1}"},"core/host/ResourceLoader.ts | cacheError":{"message":"Cache error"},"core/host/ResourceLoader.ts | certificateError":{"message":"Certificate error"},"core/host/ResourceLoader.ts | certificateManagerError":{"message":"Certificate manager error"},"core/host/ResourceLoader.ts | connectionError":{"message":"Connection error"},"core/host/ResourceLoader.ts | decodingDataUrlFailed":{"message":"Decoding Data URL failed"},"core/host/ResourceLoader.ts | dnsResolverError":{"message":"DNS resolver error"},"core/host/ResourceLoader.ts | ftpError":{"message":"FTP error"},"core/host/ResourceLoader.ts | httpError":{"message":"HTTP error"},"core/host/ResourceLoader.ts | httpErrorStatusCodeSS":{"message":"HTTP error: status code {PH1}, {PH2}"},"core/host/ResourceLoader.ts | invalidUrl":{"message":"Invalid URL"},"core/host/ResourceLoader.ts | signedExchangeError":{"message":"Signed Exchange error"},"core/host/ResourceLoader.ts | systemError":{"message":"System error"},"core/host/ResourceLoader.ts | unknownError":{"message":"Unknown error"},"core/i18n/time-utilities.ts | fdays":{"message":"{PH1} days"},"core/i18n/time-utilities.ts | fhrs":{"message":"{PH1} hrs"},"core/i18n/time-utilities.ts | fmin":{"message":"{PH1} min"},"core/i18n/time-utilities.ts | fmms":{"message":"{PH1} μs"},"core/i18n/time-utilities.ts | fms":{"message":"{PH1} ms"},"core/i18n/time-utilities.ts | fs":{"message":"{PH1} s"},"core/sdk/ChildTargetManager.ts | main":{"message":"Main"},"core/sdk/CompilerSourceMappingContentProvider.ts | couldNotLoadContentForSS":{"message":"Could not load content for {PH1} ({PH2})"},"core/sdk/ConsoleModel.ts | bfcacheNavigation":{"message":"Navigation to {PH1} was restored from back/forward cache (see https://web.dev/bfcache/)"},"core/sdk/ConsoleModel.ts | failedToSaveToTempVariable":{"message":"Failed to save to temp variable."},"core/sdk/ConsoleModel.ts | navigatedToS":{"message":"Navigated to {PH1}"},"core/sdk/ConsoleModel.ts | profileSFinished":{"message":"Profile ''{PH1}'' finished."},"core/sdk/ConsoleModel.ts | profileSStarted":{"message":"Profile ''{PH1}'' started."},"core/sdk/CPUProfilerModel.ts | profileD":{"message":"Profile {PH1}"},"core/sdk/CSSStyleSheetHeader.ts | couldNotFindTheOriginalStyle":{"message":"Could not find the original style sheet."},"core/sdk/CSSStyleSheetHeader.ts | thereWasAnErrorRetrievingThe":{"message":"There was an error retrieving the source styles."},"core/sdk/DebuggerModel.ts | block":{"message":"Block"},"core/sdk/DebuggerModel.ts | catchBlock":{"message":"Catch block"},"core/sdk/DebuggerModel.ts | closure":{"message":"Closure"},"core/sdk/DebuggerModel.ts | expression":{"message":"Expression"},"core/sdk/DebuggerModel.ts | global":{"message":"Global"},"core/sdk/DebuggerModel.ts | local":{"message":"Local"},"core/sdk/DebuggerModel.ts | module":{"message":"Module"},"core/sdk/DebuggerModel.ts | script":{"message":"Script"},"core/sdk/DebuggerModel.ts | withBlock":{"message":"With block"},"core/sdk/NetworkManager.ts | fastG":{"message":"Fast 3G"},"core/sdk/NetworkManager.ts | noContentForPreflight":{"message":"No content available for preflight request"},"core/sdk/NetworkManager.ts | noContentForRedirect":{"message":"No content available because this request was redirected"},"core/sdk/NetworkManager.ts | noContentForWebSocket":{"message":"Content for WebSockets is currently not supported"},"core/sdk/NetworkManager.ts | noThrottling":{"message":"No throttling"},"core/sdk/NetworkManager.ts | offline":{"message":"Offline"},"core/sdk/NetworkManager.ts | requestWasBlockedByDevtoolsS":{"message":"Request was blocked by DevTools: \"{PH1}\""},"core/sdk/NetworkManager.ts | sFailedLoadingSS":{"message":"{PH1} failed loading: {PH2} \"{PH3}\"."},"core/sdk/NetworkManager.ts | sFinishedLoadingSS":{"message":"{PH1} finished loading: {PH2} \"{PH3}\"."},"core/sdk/NetworkManager.ts | slowG":{"message":"Slow 3G"},"core/sdk/NetworkRequest.ts | anUnknownErrorWasEncounteredWhenTrying":{"message":"An unknown error was encountered when trying to store this cookie."},"core/sdk/NetworkRequest.ts | binary":{"message":"(binary)"},"core/sdk/NetworkRequest.ts | blockedReasonInvalidDomain":{"message":"This attempt to set a cookie via a Set-Cookie header was blocked because its Domain attribute was invalid with regards to the current host url."},"core/sdk/NetworkRequest.ts | blockedReasonInvalidPrefix":{"message":"This attempt to set a cookie via a Set-Cookie header was blocked because it used the \"__Secure-\" or \"__Host-\" prefix in its name and broke the additional rules applied to cookies with these prefixes as defined in https://tools.ietf.org/html/draft-west-cookie-prefixes-05."},"core/sdk/NetworkRequest.ts | blockedReasonOverwriteSecure":{"message":"This attempt to set a cookie via a Set-Cookie header was blocked because it was not sent over a secure connection and would have overwritten a cookie with the Secure attribute."},"core/sdk/NetworkRequest.ts | blockedReasonSameSiteNoneInsecure":{"message":"This attempt to set a cookie via a Set-Cookie header was blocked because it had the \"SameSite=None\" attribute but did not have the \"Secure\" attribute, which is required in order to use \"SameSite=None\"."},"core/sdk/NetworkRequest.ts | blockedReasonSameSiteStrictLax":{"message":"This attempt to set a cookie via a Set-Cookie header was blocked because it had the \"{PH1}\" attribute but came from a cross-site response which was not the response to a top-level navigation."},"core/sdk/NetworkRequest.ts | blockedReasonSameSiteUnspecifiedTreatedAsLax":{"message":"This Set-Cookie header didn't specify a \"SameSite\" attribute and was defaulted to \"SameSite=Lax,\" and was blocked because it came from a cross-site response which was not the response to a top-level navigation. The Set-Cookie had to have been set with \"SameSite=None\" to enable cross-site usage."},"core/sdk/NetworkRequest.ts | blockedReasonSecureOnly":{"message":"This attempt to set a cookie via a Set-Cookie header was blocked because it had the \"Secure\" attribute but was not received over a secure connection."},"core/sdk/NetworkRequest.ts | domainMismatch":{"message":"This cookie was blocked because neither did the request URL's domain exactly match the cookie's domain, nor was the request URL's domain a subdomain of the cookie's Domain attribute value."},"core/sdk/NetworkRequest.ts | exemptionReasonCorsOptIn":{"message":"This cookie is allowed by CORS opt-in. Learn more: goo.gle/cors"},"core/sdk/NetworkRequest.ts | exemptionReasonEnterprisePolicy":{"message":"This cookie is allowed by Chrome Enterprise policy. Learn more: goo.gle/ce-3pc"},"core/sdk/NetworkRequest.ts | exemptionReasonStorageAccessAPI":{"message":"This cookie is allowed by the Storage Access API. Learn more: goo.gle/saa"},"core/sdk/NetworkRequest.ts | exemptionReasonTopLevelStorageAccessAPI":{"message":"This cookie is allowed by the top-level Storage Access API. Learn more: goo.gle/saa-top"},"core/sdk/NetworkRequest.ts | exemptionReasonTPCDDeprecationTrial":{"message":"This cookie is allowed by third-party cookie phaseout deprecation trial."},"core/sdk/NetworkRequest.ts | exemptionReasonTPCDHeuristics":{"message":"This cookie is allowed by third-party cookie phaseout heuristics. Learn more: goo.gle/hbe"},"core/sdk/NetworkRequest.ts | exemptionReasonTPCDMetadata":{"message":"This cookie is allowed by a third-party cookie deprecation trial grace period. Learn more: goo.gle/ps-dt."},"core/sdk/NetworkRequest.ts | exemptionReasonUserSetting":{"message":"This cookie is allowed by user preference."},"core/sdk/NetworkRequest.ts | nameValuePairExceedsMaxSize":{"message":"This cookie was blocked because it was too large. The combined size of the name and value must be less than or equal to 4096 characters."},"core/sdk/NetworkRequest.ts | notOnPath":{"message":"This cookie was blocked because its path was not an exact match for or a superdirectory of the request url's path."},"core/sdk/NetworkRequest.ts | samePartyFromCrossPartyContext":{"message":"This cookie was blocked because it had the \"SameParty\" attribute but the request was cross-party. The request was considered cross-party because the domain of the resource's URL and the domains of the resource's enclosing frames/documents are neither owners nor members in the same First-Party Set."},"core/sdk/NetworkRequest.ts | sameSiteLax":{"message":"This cookie was blocked because it had the \"SameSite=Lax\" attribute and the request was made from a different site and was not initiated by a top-level navigation."},"core/sdk/NetworkRequest.ts | sameSiteNoneInsecure":{"message":"This cookie was blocked because it had the \"SameSite=None\" attribute but was not marked \"Secure\". Cookies without SameSite restrictions must be marked \"Secure\" and sent over a secure connection."},"core/sdk/NetworkRequest.ts | sameSiteStrict":{"message":"This cookie was blocked because it had the \"SameSite=Strict\" attribute and the request was made from a different site. This includes top-level navigation requests initiated by other sites."},"core/sdk/NetworkRequest.ts | sameSiteUnspecifiedTreatedAsLax":{"message":"This cookie didn't specify a \"SameSite\" attribute when it was stored and was defaulted to \"SameSite=Lax,\" and was blocked because the request was made from a different site and was not initiated by a top-level navigation. The cookie had to have been set with \"SameSite=None\" to enable cross-site usage."},"core/sdk/NetworkRequest.ts | schemefulSameSiteLax":{"message":"This cookie was blocked because it had the \"SameSite=Lax\" attribute but the request was cross-site and was not initiated by a top-level navigation. This request is considered cross-site because the URL has a different scheme than the current site."},"core/sdk/NetworkRequest.ts | schemefulSameSiteStrict":{"message":"This cookie was blocked because it had the \"SameSite=Strict\" attribute but the request was cross-site. This includes top-level navigation requests initiated by other sites. This request is considered cross-site because the URL has a different scheme than the current site."},"core/sdk/NetworkRequest.ts | schemefulSameSiteUnspecifiedTreatedAsLax":{"message":"This cookie didn't specify a \"SameSite\" attribute when it was stored, was defaulted to \"SameSite=Lax\", and was blocked because the request was cross-site and was not initiated by a top-level navigation. This request is considered cross-site because the URL has a different scheme than the current site."},"core/sdk/NetworkRequest.ts | secureOnly":{"message":"This cookie was blocked because it had the \"Secure\" attribute and the connection was not secure."},"core/sdk/NetworkRequest.ts | setcookieHeaderIsIgnoredIn":{"message":"Set-Cookie header is ignored in response from url: {PH1}. The combined size of the name and value must be less than or equal to 4096 characters."},"core/sdk/NetworkRequest.ts | theSchemeOfThisConnectionIsNot":{"message":"The scheme of this connection is not allowed to store cookies."},"core/sdk/NetworkRequest.ts | thirdPartyPhaseout":{"message":"This cookie was blocked due to third-party cookie phaseout. Learn more in the Issues tab."},"core/sdk/NetworkRequest.ts | thisSetcookieDidntSpecifyASamesite":{"message":"This Set-Cookie header didn't specify a \"SameSite\" attribute, was defaulted to \"SameSite=Lax\", and was blocked because it came from a cross-site response which was not the response to a top-level navigation. This response is considered cross-site because the URL has a different scheme than the current site."},"core/sdk/NetworkRequest.ts | thisSetcookieHadADisallowedCharacter":{"message":"This Set-Cookie header contained a disallowed character (a forbidden ASCII control character, or the tab character if it appears in the middle of the cookie name, value, an attribute name, or an attribute value)."},"core/sdk/NetworkRequest.ts | thisSetcookieHadInvalidSyntax":{"message":"This Set-Cookie header had invalid syntax."},"core/sdk/NetworkRequest.ts | thisSetcookieWasBlockedBecauseItHadTheSameparty":{"message":"This attempt to set a cookie via a Set-Cookie header was blocked because it had the \"SameParty\" attribute but the request was cross-party. The request was considered cross-party because the domain of the resource's URL and the domains of the resource's enclosing frames/documents are neither owners nor members in the same First-Party Set."},"core/sdk/NetworkRequest.ts | thisSetcookieWasBlockedBecauseItHadTheSamepartyAttribute":{"message":"This attempt to set a cookie via a Set-Cookie header was blocked because it had the \"SameParty\" attribute but also had other conflicting attributes. Chrome requires cookies that use the \"SameParty\" attribute to also have the \"Secure\" attribute, and to not be restricted to \"SameSite=Strict\"."},"core/sdk/NetworkRequest.ts | thisSetcookieWasBlockedBecauseItHadTheSamesiteStrictLax":{"message":"This attempt to set a cookie via a Set-Cookie header was blocked because it had the \"{PH1}\" attribute but came from a cross-site response which was not the response to a top-level navigation. This response is considered cross-site because the URL has a different scheme than the current site."},"core/sdk/NetworkRequest.ts | thisSetcookieWasBlockedBecauseTheNameValuePairExceedsMaxSize":{"message":"This attempt to set a cookie via a Set-Cookie header was blocked because the cookie was too large. The combined size of the name and value must be less than or equal to 4096 characters."},"core/sdk/NetworkRequest.ts | thisSetcookieWasBlockedDueThirdPartyPhaseout":{"message":"Setting this cookie was blocked due to third-party cookie phaseout. Learn more in the Issues tab."},"core/sdk/NetworkRequest.ts | thisSetcookieWasBlockedDueToUser":{"message":"This attempt to set a cookie via a Set-Cookie header was blocked due to user preferences."},"core/sdk/NetworkRequest.ts | unknownError":{"message":"An unknown error was encountered when trying to send this cookie."},"core/sdk/NetworkRequest.ts | userPreferences":{"message":"This cookie was blocked due to user preferences."},"core/sdk/OverlayModel.ts | pausedInDebugger":{"message":"Paused in debugger"},"core/sdk/PageResourceLoader.ts | loadCanceledDueToReloadOf":{"message":"Load canceled due to reload of inspected page"},"core/sdk/Script.ts | scriptRemovedOrDeleted":{"message":"Script removed or deleted."},"core/sdk/Script.ts | unableToFetchScriptSource":{"message":"Unable to fetch script source."},"core/sdk/sdk-meta.ts | achromatopsia":{"message":"Achromatopsia (no color)"},"core/sdk/sdk-meta.ts | blurredVision":{"message":"Blurred vision"},"core/sdk/sdk-meta.ts | captureAsyncStackTraces":{"message":"Capture async stack traces"},"core/sdk/sdk-meta.ts | customFormatters":{"message":"Custom formatters"},"core/sdk/sdk-meta.ts | deuteranopia":{"message":"Deuteranopia (no green)"},"core/sdk/sdk-meta.ts | disableAsyncStackTraces":{"message":"Disable async stack traces"},"core/sdk/sdk-meta.ts | disableAvifFormat":{"message":"Disable AVIF format"},"core/sdk/sdk-meta.ts | disableCache":{"message":"Disable cache (while DevTools is open)"},"core/sdk/sdk-meta.ts | disableJavascript":{"message":"Disable JavaScript"},"core/sdk/sdk-meta.ts | disableLocalFonts":{"message":"Disable local fonts"},"core/sdk/sdk-meta.ts | disableNetworkRequestBlocking":{"message":"Disable network request blocking"},"core/sdk/sdk-meta.ts | disableWebpFormat":{"message":"Disable WebP format"},"core/sdk/sdk-meta.ts | doNotCaptureAsyncStackTraces":{"message":"Do not capture async stack traces"},"core/sdk/sdk-meta.ts | doNotEmulateAFocusedPage":{"message":"Do not emulate a focused page"},"core/sdk/sdk-meta.ts | doNotEmulateAnyVisionDeficiency":{"message":"Do not emulate any vision deficiency"},"core/sdk/sdk-meta.ts | doNotEmulateCss":{"message":"Do not emulate CSS {PH1}"},"core/sdk/sdk-meta.ts | doNotEmulateCssMediaType":{"message":"Do not emulate CSS media type"},"core/sdk/sdk-meta.ts | doNotExtendGridLines":{"message":"Do not extend grid lines"},"core/sdk/sdk-meta.ts | doNotHighlightAdFrames":{"message":"Do not highlight ad frames"},"core/sdk/sdk-meta.ts | doNotPauseOnExceptions":{"message":"Do not pause on exceptions"},"core/sdk/sdk-meta.ts | doNotPreserveLogUponNavigation":{"message":"Do not preserve log upon navigation"},"core/sdk/sdk-meta.ts | doNotShowGridNamedAreas":{"message":"Do not show grid named areas"},"core/sdk/sdk-meta.ts | doNotShowGridTrackSizes":{"message":"Do not show grid track sizes"},"core/sdk/sdk-meta.ts | doNotShowRulersOnHover":{"message":"Do not show rulers on hover"},"core/sdk/sdk-meta.ts | emulateAchromatopsia":{"message":"Emulate achromatopsia (no color)"},"core/sdk/sdk-meta.ts | emulateAFocusedPage":{"message":"Emulate a focused page"},"core/sdk/sdk-meta.ts | emulateAutoDarkMode":{"message":"Emulate auto dark mode"},"core/sdk/sdk-meta.ts | emulateBlurredVision":{"message":"Emulate blurred vision"},"core/sdk/sdk-meta.ts | emulateCss":{"message":"Emulate CSS {PH1}"},"core/sdk/sdk-meta.ts | emulateCssMediaFeature":{"message":"Emulate CSS media feature {PH1}"},"core/sdk/sdk-meta.ts | emulateCssMediaType":{"message":"Emulate CSS media type"},"core/sdk/sdk-meta.ts | emulateCssPrintMediaType":{"message":"Emulate CSS print media type"},"core/sdk/sdk-meta.ts | emulateCssScreenMediaType":{"message":"Emulate CSS screen media type"},"core/sdk/sdk-meta.ts | emulateDeuteranopia":{"message":"Emulate deuteranopia (no green)"},"core/sdk/sdk-meta.ts | emulateProtanopia":{"message":"Emulate protanopia (no red)"},"core/sdk/sdk-meta.ts | emulateReducedContrast":{"message":"Emulate reduced contrast"},"core/sdk/sdk-meta.ts | emulateTritanopia":{"message":"Emulate tritanopia (no blue)"},"core/sdk/sdk-meta.ts | emulateVisionDeficiencies":{"message":"Emulate vision deficiencies"},"core/sdk/sdk-meta.ts | enableAvifFormat":{"message":"Enable AVIF format"},"core/sdk/sdk-meta.ts | enableCache":{"message":"Enable cache"},"core/sdk/sdk-meta.ts | enableJavascript":{"message":"Enable JavaScript"},"core/sdk/sdk-meta.ts | enableLocalFonts":{"message":"Enable local fonts"},"core/sdk/sdk-meta.ts | enableNetworkRequestBlocking":{"message":"Enable network request blocking"},"core/sdk/sdk-meta.ts | enableRemoteFileLoading":{"message":"Allow DevTools to load resources, such as source maps, from remote file paths. Disabled by default for security reasons."},"core/sdk/sdk-meta.ts | enableWebpFormat":{"message":"Enable WebP format"},"core/sdk/sdk-meta.ts | extendGridLines":{"message":"Extend grid lines"},"core/sdk/sdk-meta.ts | hideCoreWebVitalsOverlay":{"message":"Hide Core Web Vitals overlay"},"core/sdk/sdk-meta.ts | hideFramesPerSecondFpsMeter":{"message":"Hide frames per second (FPS) meter"},"core/sdk/sdk-meta.ts | hideLayerBorders":{"message":"Hide layer borders"},"core/sdk/sdk-meta.ts | hideLayoutShiftRegions":{"message":"Hide layout shift regions"},"core/sdk/sdk-meta.ts | hideLineLabels":{"message":"Hide line labels"},"core/sdk/sdk-meta.ts | hidePaintFlashingRectangles":{"message":"Hide paint flashing rectangles"},"core/sdk/sdk-meta.ts | hideScrollPerformanceBottlenecks":{"message":"Hide scroll performance bottlenecks"},"core/sdk/sdk-meta.ts | highlightAdFrames":{"message":"Highlight ad frames"},"core/sdk/sdk-meta.ts | networkRequestBlocking":{"message":"Network request blocking"},"core/sdk/sdk-meta.ts | noEmulation":{"message":"No emulation"},"core/sdk/sdk-meta.ts | pauseOnExceptions":{"message":"Pause on exceptions"},"core/sdk/sdk-meta.ts | preserveLogUponNavigation":{"message":"Preserve log upon navigation"},"core/sdk/sdk-meta.ts | print":{"message":"print"},"core/sdk/sdk-meta.ts | protanopia":{"message":"Protanopia (no red)"},"core/sdk/sdk-meta.ts | query":{"message":"query"},"core/sdk/sdk-meta.ts | reducedContrast":{"message":"Reduced contrast"},"core/sdk/sdk-meta.ts | screen":{"message":"screen"},"core/sdk/sdk-meta.ts | showAreaNames":{"message":"Show area names"},"core/sdk/sdk-meta.ts | showCoreWebVitalsOverlay":{"message":"Show Core Web Vitals overlay"},"core/sdk/sdk-meta.ts | showFramesPerSecondFpsMeter":{"message":"Show frames per second (FPS) meter"},"core/sdk/sdk-meta.ts | showGridNamedAreas":{"message":"Show grid named areas"},"core/sdk/sdk-meta.ts | showGridTrackSizes":{"message":"Show grid track sizes"},"core/sdk/sdk-meta.ts | showLayerBorders":{"message":"Show layer borders"},"core/sdk/sdk-meta.ts | showLayoutShiftRegions":{"message":"Show layout shift regions"},"core/sdk/sdk-meta.ts | showLineLabels":{"message":"Show line labels"},"core/sdk/sdk-meta.ts | showLineNames":{"message":"Show line names"},"core/sdk/sdk-meta.ts | showLineNumbers":{"message":"Show line numbers"},"core/sdk/sdk-meta.ts | showPaintFlashingRectangles":{"message":"Show paint flashing rectangles"},"core/sdk/sdk-meta.ts | showRulersOnHover":{"message":"Show rulers on hover"},"core/sdk/sdk-meta.ts | showScrollPerformanceBottlenecks":{"message":"Show scroll performance bottlenecks"},"core/sdk/sdk-meta.ts | showTrackSizes":{"message":"Show track sizes"},"core/sdk/sdk-meta.ts | tritanopia":{"message":"Tritanopia (no blue)"},"core/sdk/ServerTiming.ts | deprecatedSyntaxFoundPleaseUse":{"message":"Deprecated syntax found. Please use: ;dur=;desc="},"core/sdk/ServerTiming.ts | duplicateParameterSIgnored":{"message":"Duplicate parameter \"{PH1}\" ignored."},"core/sdk/ServerTiming.ts | extraneousTrailingCharacters":{"message":"Extraneous trailing characters."},"core/sdk/ServerTiming.ts | noValueFoundForParameterS":{"message":"No value found for parameter \"{PH1}\"."},"core/sdk/ServerTiming.ts | unableToParseSValueS":{"message":"Unable to parse \"{PH1}\" value \"{PH2}\"."},"core/sdk/ServerTiming.ts | unrecognizedParameterS":{"message":"Unrecognized parameter \"{PH1}\"."},"core/sdk/ServiceWorkerCacheModel.ts | serviceworkercacheagentError":{"message":"ServiceWorkerCacheAgent error deleting cache entry {PH1} in cache: {PH2}"},"core/sdk/ServiceWorkerManager.ts | activated":{"message":"activated"},"core/sdk/ServiceWorkerManager.ts | activating":{"message":"activating"},"core/sdk/ServiceWorkerManager.ts | installed":{"message":"installed"},"core/sdk/ServiceWorkerManager.ts | installing":{"message":"installing"},"core/sdk/ServiceWorkerManager.ts | new":{"message":"new"},"core/sdk/ServiceWorkerManager.ts | redundant":{"message":"redundant"},"core/sdk/ServiceWorkerManager.ts | running":{"message":"running"},"core/sdk/ServiceWorkerManager.ts | sSS":{"message":"{PH1} #{PH2} ({PH3})"},"core/sdk/ServiceWorkerManager.ts | starting":{"message":"starting"},"core/sdk/ServiceWorkerManager.ts | stopped":{"message":"stopped"},"core/sdk/ServiceWorkerManager.ts | stopping":{"message":"stopping"},"entrypoints/inspector_main/inspector_main-meta.ts | autoOpenDevTools":{"message":"Auto-open DevTools for popups"},"entrypoints/inspector_main/inspector_main-meta.ts | blockAds":{"message":"Block ads on this site"},"entrypoints/inspector_main/inspector_main-meta.ts | colorVisionDeficiency":{"message":"color vision deficiency"},"entrypoints/inspector_main/inspector_main-meta.ts | cssMediaFeature":{"message":"CSS media feature"},"entrypoints/inspector_main/inspector_main-meta.ts | cssMediaType":{"message":"CSS media type"},"entrypoints/inspector_main/inspector_main-meta.ts | disablePaused":{"message":"Disable paused state overlay"},"entrypoints/inspector_main/inspector_main-meta.ts | doNotAutoOpen":{"message":"Do not auto-open DevTools for popups"},"entrypoints/inspector_main/inspector_main-meta.ts | forceAdBlocking":{"message":"Force ad blocking on this site"},"entrypoints/inspector_main/inspector_main-meta.ts | fps":{"message":"fps"},"entrypoints/inspector_main/inspector_main-meta.ts | hardReloadPage":{"message":"Hard reload page"},"entrypoints/inspector_main/inspector_main-meta.ts | layout":{"message":"layout"},"entrypoints/inspector_main/inspector_main-meta.ts | paint":{"message":"paint"},"entrypoints/inspector_main/inspector_main-meta.ts | reloadPage":{"message":"Reload page"},"entrypoints/inspector_main/inspector_main-meta.ts | rendering":{"message":"Rendering"},"entrypoints/inspector_main/inspector_main-meta.ts | showAds":{"message":"Show ads on this site, if allowed"},"entrypoints/inspector_main/inspector_main-meta.ts | showRendering":{"message":"Show Rendering"},"entrypoints/inspector_main/inspector_main-meta.ts | toggleCssPrefersColorSchemeMedia":{"message":"Toggle CSS media feature prefers-color-scheme"},"entrypoints/inspector_main/inspector_main-meta.ts | visionDeficiency":{"message":"vision deficiency"},"entrypoints/inspector_main/InspectorMain.ts | javascriptIsDisabled":{"message":"JavaScript is disabled"},"entrypoints/inspector_main/InspectorMain.ts | main":{"message":"Main"},"entrypoints/inspector_main/InspectorMain.ts | openDedicatedTools":{"message":"Open dedicated DevTools for Node.js"},"entrypoints/inspector_main/InspectorMain.ts | tab":{"message":"Tab"},"entrypoints/inspector_main/OutermostTargetSelector.ts | targetNotSelected":{"message":"Page: Not selected"},"entrypoints/inspector_main/OutermostTargetSelector.ts | targetS":{"message":"Page: {PH1}"},"entrypoints/inspector_main/RenderingOptions.ts | coreWebVitals":{"message":"Core Web Vitals"},"entrypoints/inspector_main/RenderingOptions.ts | disableAvifImageFormat":{"message":"Disable AVIF image format"},"entrypoints/inspector_main/RenderingOptions.ts | disableLocalFonts":{"message":"Disable local fonts"},"entrypoints/inspector_main/RenderingOptions.ts | disablesLocalSourcesInFontface":{"message":"Disables local() sources in @font-face rules. Requires a page reload to apply."},"entrypoints/inspector_main/RenderingOptions.ts | disableWebpImageFormat":{"message":"Disable WebP image format"},"entrypoints/inspector_main/RenderingOptions.ts | emulateAFocusedPage":{"message":"Emulate a focused page"},"entrypoints/inspector_main/RenderingOptions.ts | emulateAutoDarkMode":{"message":"Enable automatic dark mode"},"entrypoints/inspector_main/RenderingOptions.ts | emulatesAFocusedPage":{"message":"Keep page focused. Commonly used for debugging disappearing elements."},"entrypoints/inspector_main/RenderingOptions.ts | emulatesAutoDarkMode":{"message":"Enables automatic dark mode and sets prefers-color-scheme to dark."},"entrypoints/inspector_main/RenderingOptions.ts | forcesCssColorgamutMediaFeature":{"message":"Forces CSS color-gamut media feature"},"entrypoints/inspector_main/RenderingOptions.ts | forcesCssForcedColors":{"message":"Forces CSS forced-colors media feature"},"entrypoints/inspector_main/RenderingOptions.ts | forcesCssPreferscolorschemeMedia":{"message":"Forces CSS prefers-color-scheme media feature"},"entrypoints/inspector_main/RenderingOptions.ts | forcesCssPreferscontrastMedia":{"message":"Forces CSS prefers-contrast media feature"},"entrypoints/inspector_main/RenderingOptions.ts | forcesCssPrefersreduceddataMedia":{"message":"Forces CSS prefers-reduced-data media feature"},"entrypoints/inspector_main/RenderingOptions.ts | forcesCssPrefersreducedmotion":{"message":"Forces CSS prefers-reduced-motion media feature"},"entrypoints/inspector_main/RenderingOptions.ts | forcesCssPrefersreducedtransparencyMedia":{"message":"Forces CSS prefers-reduced-transparency media feature"},"entrypoints/inspector_main/RenderingOptions.ts | forcesMediaTypeForTestingPrint":{"message":"Forces media type for testing print and screen styles"},"entrypoints/inspector_main/RenderingOptions.ts | forcesVisionDeficiencyEmulation":{"message":"Forces vision deficiency emulation"},"entrypoints/inspector_main/RenderingOptions.ts | frameRenderingStats":{"message":"Frame Rendering Stats"},"entrypoints/inspector_main/RenderingOptions.ts | highlightAdFrames":{"message":"Highlight ad frames"},"entrypoints/inspector_main/RenderingOptions.ts | highlightsAreasOfThePageBlueThat":{"message":"Highlights areas of the page (blue) that were shifted. May not be suitable for people prone to photosensitive epilepsy."},"entrypoints/inspector_main/RenderingOptions.ts | highlightsAreasOfThePageGreen":{"message":"Highlights areas of the page (green) that need to be repainted. May not be suitable for people prone to photosensitive epilepsy."},"entrypoints/inspector_main/RenderingOptions.ts | highlightsElementsTealThatCan":{"message":"Highlights elements (teal) that can slow down scrolling, including touch & wheel event handlers and other main-thread scrolling situations."},"entrypoints/inspector_main/RenderingOptions.ts | highlightsFramesRedDetectedToBe":{"message":"Highlights frames (red) detected to be ads."},"entrypoints/inspector_main/RenderingOptions.ts | layerBorders":{"message":"Layer borders"},"entrypoints/inspector_main/RenderingOptions.ts | layoutShiftRegions":{"message":"Layout Shift Regions"},"entrypoints/inspector_main/RenderingOptions.ts | paintFlashing":{"message":"Paint flashing"},"entrypoints/inspector_main/RenderingOptions.ts | plotsFrameThroughputDropped":{"message":"Plots frame throughput, dropped frames distribution, and GPU memory."},"entrypoints/inspector_main/RenderingOptions.ts | requiresAPageReloadToApplyAnd":{"message":"Requires a page reload to apply and disables caching for image requests."},"entrypoints/inspector_main/RenderingOptions.ts | scrollingPerformanceIssues":{"message":"Scrolling performance issues"},"entrypoints/inspector_main/RenderingOptions.ts | showsAnOverlayWithCoreWebVitals":{"message":"Shows an overlay with Core Web Vitals."},"entrypoints/inspector_main/RenderingOptions.ts | showsLayerBordersOrangeoliveAnd":{"message":"Shows layer borders (orange/olive) and tiles (cyan)."},"entrypoints/js_app/js_app.ts | main":{"message":"Main"},"entrypoints/js_app/js_app.ts | networkTitle":{"message":"Scripts"},"entrypoints/js_app/js_app.ts | showNode":{"message":"Show Scripts"},"entrypoints/main/main-meta.ts | auto":{"message":"auto"},"entrypoints/main/main-meta.ts | bottom":{"message":"Bottom"},"entrypoints/main/main-meta.ts | browserLanguage":{"message":"Browser UI language"},"entrypoints/main/main-meta.ts | cancelSearch":{"message":"Cancel search"},"entrypoints/main/main-meta.ts | darkCapital":{"message":"Dark"},"entrypoints/main/main-meta.ts | darkLower":{"message":"dark"},"entrypoints/main/main-meta.ts | devtoolsDefault":{"message":"DevTools (Default)"},"entrypoints/main/main-meta.ts | dockToBottom":{"message":"Dock to bottom"},"entrypoints/main/main-meta.ts | dockToLeft":{"message":"Dock to left"},"entrypoints/main/main-meta.ts | dockToRight":{"message":"Dock to right"},"entrypoints/main/main-meta.ts | enableCtrlShortcutToSwitchPanels":{"message":"Enable Ctrl + 1-9 shortcut to switch panels"},"entrypoints/main/main-meta.ts | enableShortcutToSwitchPanels":{"message":"Enable ⌘ + 1-9 shortcut to switch panels"},"entrypoints/main/main-meta.ts | enableSync":{"message":"Enable settings sync"},"entrypoints/main/main-meta.ts | findNextResult":{"message":"Find next result"},"entrypoints/main/main-meta.ts | findPreviousResult":{"message":"Find previous result"},"entrypoints/main/main-meta.ts | focusDebuggee":{"message":"Focus page"},"entrypoints/main/main-meta.ts | horizontal":{"message":"horizontal"},"entrypoints/main/main-meta.ts | language":{"message":"Language:"},"entrypoints/main/main-meta.ts | left":{"message":"Left"},"entrypoints/main/main-meta.ts | lightCapital":{"message":"Light"},"entrypoints/main/main-meta.ts | lightLower":{"message":"light"},"entrypoints/main/main-meta.ts | nextPanel":{"message":"Next panel"},"entrypoints/main/main-meta.ts | panelLayout":{"message":"Panel layout:"},"entrypoints/main/main-meta.ts | previousPanel":{"message":"Previous panel"},"entrypoints/main/main-meta.ts | reloadDevtools":{"message":"Reload DevTools"},"entrypoints/main/main-meta.ts | resetZoomLevel":{"message":"Reset zoom level"},"entrypoints/main/main-meta.ts | restoreLastDockPosition":{"message":"Restore last dock position"},"entrypoints/main/main-meta.ts | right":{"message":"Right"},"entrypoints/main/main-meta.ts | searchAsYouTypeCommand":{"message":"Enable search as you type"},"entrypoints/main/main-meta.ts | searchAsYouTypeSetting":{"message":"Search as you type"},"entrypoints/main/main-meta.ts | searchInPanel":{"message":"Search in panel"},"entrypoints/main/main-meta.ts | searchOnEnterCommand":{"message":"Disable search as you type (press Enter to search)"},"entrypoints/main/main-meta.ts | switchToDarkTheme":{"message":"Switch to dark theme"},"entrypoints/main/main-meta.ts | switchToLightTheme":{"message":"Switch to light theme"},"entrypoints/main/main-meta.ts | switchToSystemPreferredColor":{"message":"Switch to system preferred color theme"},"entrypoints/main/main-meta.ts | systemPreference":{"message":"System preference"},"entrypoints/main/main-meta.ts | theme":{"message":"Theme:"},"entrypoints/main/main-meta.ts | toggleDrawer":{"message":"Toggle drawer"},"entrypoints/main/main-meta.ts | undocked":{"message":"Undocked"},"entrypoints/main/main-meta.ts | undockIntoSeparateWindow":{"message":"Undock into separate window"},"entrypoints/main/main-meta.ts | useAutomaticPanelLayout":{"message":"Use automatic panel layout"},"entrypoints/main/main-meta.ts | useHorizontalPanelLayout":{"message":"Use horizontal panel layout"},"entrypoints/main/main-meta.ts | useVerticalPanelLayout":{"message":"Use vertical panel layout"},"entrypoints/main/main-meta.ts | vertical":{"message":"vertical"},"entrypoints/main/main-meta.ts | zoomIn":{"message":"Zoom in"},"entrypoints/main/main-meta.ts | zoomOut":{"message":"Zoom out"},"entrypoints/main/MainImpl.ts | customizeAndControlDevtools":{"message":"Customize and control DevTools"},"entrypoints/main/MainImpl.ts | dockSide":{"message":"Dock side"},"entrypoints/main/MainImpl.ts | dockSideNaviation":{"message":"Use left and right arrow keys to navigate the options"},"entrypoints/main/MainImpl.ts | dockToBottom":{"message":"Dock to bottom"},"entrypoints/main/MainImpl.ts | dockToLeft":{"message":"Dock to left"},"entrypoints/main/MainImpl.ts | dockToRight":{"message":"Dock to right"},"entrypoints/main/MainImpl.ts | focusDebuggee":{"message":"Focus page"},"entrypoints/main/MainImpl.ts | help":{"message":"Help"},"entrypoints/main/MainImpl.ts | hideConsoleDrawer":{"message":"Hide console drawer"},"entrypoints/main/MainImpl.ts | moreTools":{"message":"More tools"},"entrypoints/main/MainImpl.ts | placementOfDevtoolsRelativeToThe":{"message":"Placement of DevTools relative to the page. ({PH1} to restore last position)"},"entrypoints/main/MainImpl.ts | showConsoleDrawer":{"message":"Show console drawer"},"entrypoints/main/MainImpl.ts | undockIntoSeparateWindow":{"message":"Undock into separate window"},"entrypoints/node_app/node_app.ts | connection":{"message":"Connection"},"entrypoints/node_app/node_app.ts | networkTitle":{"message":"Node"},"entrypoints/node_app/node_app.ts | node":{"message":"node"},"entrypoints/node_app/node_app.ts | showConnection":{"message":"Show Connection"},"entrypoints/node_app/node_app.ts | showNode":{"message":"Show Node"},"entrypoints/node_app/NodeConnectionsPanel.ts | addConnection":{"message":"Add connection"},"entrypoints/node_app/NodeConnectionsPanel.ts | networkAddressEgLocalhost":{"message":"Network address (e.g. localhost:9229)"},"entrypoints/node_app/NodeConnectionsPanel.ts | noConnectionsSpecified":{"message":"No connections specified"},"entrypoints/node_app/NodeConnectionsPanel.ts | nodejsDebuggingGuide":{"message":"Node.js debugging guide"},"entrypoints/node_app/NodeConnectionsPanel.ts | specifyNetworkEndpointAnd":{"message":"Specify network endpoint and DevTools will connect to it automatically. Read {PH1} to learn more."},"entrypoints/node_app/NodeMain.ts | main":{"message":"Main"},"entrypoints/node_app/NodeMain.ts | nodejsS":{"message":"Node.js: {PH1}"},"entrypoints/rn_fusebox/rn_fusebox.ts | connectionStatusDisconnectedLabel":{"message":"Reconnect DevTools"},"entrypoints/rn_fusebox/rn_fusebox.ts | connectionStatusDisconnectedTooltip":{"message":"Debugging connection was closed"},"entrypoints/rn_fusebox/rn_fusebox.ts | networkTitle":{"message":"React Native"},"entrypoints/rn_fusebox/rn_fusebox.ts | sendFeedback":{"message":"[FB-only] Send feedback"},"entrypoints/rn_fusebox/rn_fusebox.ts | showReactNative":{"message":"Show React Native"},"entrypoints/rn_inspector/rn_inspector.ts | networkTitle":{"message":"React Native"},"entrypoints/rn_inspector/rn_inspector.ts | showReactNative":{"message":"Show React Native"},"entrypoints/worker_app/WorkerMain.ts | main":{"message":"Main"},"generated/Deprecation.ts | AuthorizationCoveredByWildcard":{"message":"Authorization will not be covered by the wildcard symbol (*) in CORS Access-Control-Allow-Headers handling."},"generated/Deprecation.ts | CanRequestURLHTTPContainingNewline":{"message":"Resource requests whose URLs contained both removed whitespace \\(n|r|t) characters and less-than characters (<) are blocked. Please remove newlines and encode less-than characters from places like element attribute values in order to load these resources."},"generated/Deprecation.ts | ChromeLoadTimesConnectionInfo":{"message":"chrome.loadTimes() is deprecated, instead use standardized API: Navigation Timing 2."},"generated/Deprecation.ts | ChromeLoadTimesFirstPaintAfterLoadTime":{"message":"chrome.loadTimes() is deprecated, instead use standardized API: Paint Timing."},"generated/Deprecation.ts | ChromeLoadTimesWasAlternateProtocolAvailable":{"message":"chrome.loadTimes() is deprecated, instead use standardized API: nextHopProtocol in Navigation Timing 2."},"generated/Deprecation.ts | CookieWithTruncatingChar":{"message":"Cookies containing a \\(0|r|n) character will be rejected instead of truncated."},"generated/Deprecation.ts | CrossOriginAccessBasedOnDocumentDomain":{"message":"Relaxing the same-origin policy by setting document.domain is deprecated, and will be disabled by default. This deprecation warning is for a cross-origin access that was enabled by setting document.domain."},"generated/Deprecation.ts | CrossOriginWindowAlert":{"message":"Triggering window.alert from cross origin iframes has been deprecated and will be removed in the future."},"generated/Deprecation.ts | CrossOriginWindowConfirm":{"message":"Triggering window.confirm from cross origin iframes has been deprecated and will be removed in the future."},"generated/Deprecation.ts | CSSCustomStateDeprecatedSyntax":{"message":":--customstatename is deprecated. Please use the :state(customstatename) syntax instead."},"generated/Deprecation.ts | CSSSelectorInternalMediaControlsOverlayCastButton":{"message":"The disableRemotePlayback attribute should be used in order to disable the default Cast integration instead of using -internal-media-controls-overlay-cast-button selector."},"generated/Deprecation.ts | CSSValueAppearanceNonStandard":{"message":"CSS appearance values inner-spin-button, media-slider, media-sliderthumb, media-volume-slider, media-volume-sliderthumb, push-button, searchfield-cancel-button, slider-horizontal, sliderthumb-horizontal, sliderthumb-vertical, square-button are not standardized and will be removed."},"generated/Deprecation.ts | CSSValueAppearanceSliderVertical":{"message":"CSS appearance value slider-vertical is not standardized and will be removed."},"generated/Deprecation.ts | DataUrlInSvgUse":{"message":"Support for data: URLs in SVGUseElement is deprecated and it will be removed in the future."},"generated/Deprecation.ts | DocumentDomainSettingWithoutOriginAgentClusterHeader":{"message":"Relaxing the same-origin policy by setting document.domain is deprecated, and will be disabled by default. To continue using this feature, please opt-out of origin-keyed agent clusters by sending an Origin-Agent-Cluster: ?0 header along with the HTTP response for the document and frames. See https://developer.chrome.com/blog/immutable-document-domain/ for more details."},"generated/Deprecation.ts | DOMMutationEvents":{"message":"DOM Mutation Events, including DOMSubtreeModified, DOMNodeInserted, DOMNodeRemoved, DOMNodeRemovedFromDocument, DOMNodeInsertedIntoDocument, and DOMCharacterDataModified are deprecated (https://w3c.github.io/uievents/#legacy-event-types) and will be removed. Please use MutationObserver instead."},"generated/Deprecation.ts | GeolocationInsecureOrigin":{"message":"getCurrentPosition() and watchPosition() no longer work on insecure origins. To use this feature, you should consider switching your application to a secure origin, such as HTTPS. See https://goo.gle/chrome-insecure-origins for more details."},"generated/Deprecation.ts | GeolocationInsecureOriginDeprecatedNotRemoved":{"message":"getCurrentPosition() and watchPosition() are deprecated on insecure origins. To use this feature, you should consider switching your application to a secure origin, such as HTTPS. See https://goo.gle/chrome-insecure-origins for more details."},"generated/Deprecation.ts | GetUserMediaInsecureOrigin":{"message":"getUserMedia() no longer works on insecure origins. To use this feature, you should consider switching your application to a secure origin, such as HTTPS. See https://goo.gle/chrome-insecure-origins for more details."},"generated/Deprecation.ts | HostCandidateAttributeGetter":{"message":"RTCPeerConnectionIceErrorEvent.hostCandidate is deprecated. Please use RTCPeerConnectionIceErrorEvent.address or RTCPeerConnectionIceErrorEvent.port instead."},"generated/Deprecation.ts | IdentityInCanMakePaymentEvent":{"message":"The merchant origin and arbitrary data from the canmakepayment service worker event are deprecated and will be removed: topOrigin, paymentRequestOrigin, methodData, modifiers."},"generated/Deprecation.ts | InsecurePrivateNetworkSubresourceRequest":{"message":"The website requested a subresource from a network that it could only access because of its users' privileged network position. These requests expose non-public devices and servers to the internet, increasing the risk of a cross-site request forgery (CSRF) attack, and/or information leakage. To mitigate these risks, Chrome deprecates requests to non-public subresources when initiated from non-secure contexts, and will start blocking them."},"generated/Deprecation.ts | InterestGroupDailyUpdateUrl":{"message":"The dailyUpdateUrl field of InterestGroups passed to joinAdInterestGroup() has been renamed to updateUrl, to more accurately reflect its behavior."},"generated/Deprecation.ts | LocalCSSFileExtensionRejected":{"message":"CSS cannot be loaded from file: URLs unless they end in a .css file extension."},"generated/Deprecation.ts | MediaSourceAbortRemove":{"message":"Using SourceBuffer.abort() to abort remove()'s asynchronous range removal is deprecated due to specification change. Support will be removed in the future. You should listen to the updateend event instead. abort() is intended to only abort an asynchronous media append or reset parser state."},"generated/Deprecation.ts | MediaSourceDurationTruncatingBuffered":{"message":"Setting MediaSource.duration below the highest presentation timestamp of any buffered coded frames is deprecated due to specification change. Support for implicit removal of truncated buffered media will be removed in the future. You should instead perform explicit remove(newDuration, oldDuration) on all sourceBuffers, where newDuration < oldDuration."},"generated/Deprecation.ts | NoSysexWebMIDIWithoutPermission":{"message":"Web MIDI will ask a permission to use even if the sysex is not specified in the MIDIOptions."},"generated/Deprecation.ts | NotificationInsecureOrigin":{"message":"The Notification API may no longer be used from insecure origins. You should consider switching your application to a secure origin, such as HTTPS. See https://goo.gle/chrome-insecure-origins for more details."},"generated/Deprecation.ts | NotificationPermissionRequestedIframe":{"message":"Permission for the Notification API may no longer be requested from a cross-origin iframe. You should consider requesting permission from a top-level frame or opening a new window instead."},"generated/Deprecation.ts | ObsoleteCreateImageBitmapImageOrientationNone":{"message":"Option imageOrientation: 'none' in createImageBitmap is deprecated. Please use createImageBitmap with option {imageOrientation: 'from-image'} instead."},"generated/Deprecation.ts | ObsoleteWebRtcCipherSuite":{"message":"Your partner is negotiating an obsolete (D)TLS version. Please check with your partner to have this fixed."},"generated/Deprecation.ts | OverflowVisibleOnReplacedElement":{"message":"Specifying overflow: visible on img, video and canvas tags may cause them to produce visual content outside of the element bounds. See https://github.com/WICG/shared-element-transitions/blob/main/debugging_overflow_on_images.md."},"generated/Deprecation.ts | PaymentInstruments":{"message":"paymentManager.instruments is deprecated. Please use just-in-time install for payment handlers instead."},"generated/Deprecation.ts | PaymentRequestCSPViolation":{"message":"Your PaymentRequest call bypassed Content-Security-Policy (CSP) connect-src directive. This bypass is deprecated. Please add the payment method identifier from the PaymentRequest API (in supportedMethods field) to your CSP connect-src directive."},"generated/Deprecation.ts | PersistentQuotaType":{"message":"StorageType.persistent is deprecated. Please use standardized navigator.storage instead."},"generated/Deprecation.ts | PictureSourceSrc":{"message":" with a parent is invalid and therefore ignored. Please use instead."},"generated/Deprecation.ts | PrefixedCancelAnimationFrame":{"message":"webkitCancelAnimationFrame is vendor-specific. Please use the standard cancelAnimationFrame instead."},"generated/Deprecation.ts | PrefixedRequestAnimationFrame":{"message":"webkitRequestAnimationFrame is vendor-specific. Please use the standard requestAnimationFrame instead."},"generated/Deprecation.ts | PrefixedVideoDisplayingFullscreen":{"message":"HTMLVideoElement.webkitDisplayingFullscreen is deprecated. Please use Document.fullscreenElement instead."},"generated/Deprecation.ts | PrefixedVideoEnterFullscreen":{"message":"HTMLVideoElement.webkitEnterFullscreen() is deprecated. Please use Element.requestFullscreen() instead."},"generated/Deprecation.ts | PrefixedVideoEnterFullScreen":{"message":"HTMLVideoElement.webkitEnterFullScreen() is deprecated. Please use Element.requestFullscreen() instead."},"generated/Deprecation.ts | PrefixedVideoExitFullscreen":{"message":"HTMLVideoElement.webkitExitFullscreen() is deprecated. Please use Document.exitFullscreen() instead."},"generated/Deprecation.ts | PrefixedVideoExitFullScreen":{"message":"HTMLVideoElement.webkitExitFullScreen() is deprecated. Please use Document.exitFullscreen() instead."},"generated/Deprecation.ts | PrefixedVideoSupportsFullscreen":{"message":"HTMLVideoElement.webkitSupportsFullscreen is deprecated. Please use Document.fullscreenEnabled instead."},"generated/Deprecation.ts | PrivacySandboxExtensionsAPI":{"message":"We're deprecating the API chrome.privacy.websites.privacySandboxEnabled, though it will remain active for backward compatibility until release M113. Instead, please use chrome.privacy.websites.topicsEnabled, chrome.privacy.websites.fledgeEnabled and chrome.privacy.websites.adMeasurementEnabled. See https://developer.chrome.com/docs/extensions/reference/privacy/#property-websites-privacySandboxEnabled."},"generated/Deprecation.ts | RangeExpand":{"message":"Range.expand() is deprecated. Please use Selection.modify() instead."},"generated/Deprecation.ts | RequestedSubresourceWithEmbeddedCredentials":{"message":"Subresource requests whose URLs contain embedded credentials (e.g. https://user:pass@host/) are blocked."},"generated/Deprecation.ts | RTCConstraintEnableDtlsSrtpFalse":{"message":"The constraint DtlsSrtpKeyAgreement is removed. You have specified a false value for this constraint, which is interpreted as an attempt to use the removed SDES key negotiation method. This functionality is removed; use a service that supports DTLS key negotiation instead."},"generated/Deprecation.ts | RTCConstraintEnableDtlsSrtpTrue":{"message":"The constraint DtlsSrtpKeyAgreement is removed. You have specified a true value for this constraint, which had no effect, but you can remove this constraint for tidiness."},"generated/Deprecation.ts | RTCPeerConnectionGetStatsLegacyNonCompliant":{"message":"The callback-based getStats() is deprecated and will be removed. Use the spec-compliant getStats() instead."},"generated/Deprecation.ts | RtcpMuxPolicyNegotiate":{"message":"The rtcpMuxPolicy option is deprecated and will be removed."},"generated/Deprecation.ts | SharedArrayBufferConstructedWithoutIsolation":{"message":"SharedArrayBuffer will require cross-origin isolation. See https://developer.chrome.com/blog/enabling-shared-array-buffer/ for more details."},"generated/Deprecation.ts | TextToSpeech_DisallowedByAutoplay":{"message":"speechSynthesis.speak() without user activation is deprecated and will be removed."},"generated/Deprecation.ts | UnloadHandler":{"message":"Unload event listeners are deprecated and will be removed."},"generated/Deprecation.ts | V8SharedArrayBufferConstructedInExtensionWithoutIsolation":{"message":"Extensions should opt into cross-origin isolation to continue using SharedArrayBuffer. See https://developer.chrome.com/docs/extensions/mv3/cross-origin-isolation/."},"generated/Deprecation.ts | WebSQL":{"message":"Web SQL is deprecated. Please use SQLite WebAssembly or Indexed Database"},"generated/Deprecation.ts | WindowPlacementPermissionDescriptorUsed":{"message":"The permission descriptor window-placement is deprecated. Use window-management instead. For more help, check https://bit.ly/window-placement-rename."},"generated/Deprecation.ts | WindowPlacementPermissionPolicyParsed":{"message":"The permission policy window-placement is deprecated. Use window-management instead. For more help, check https://bit.ly/window-placement-rename."},"generated/Deprecation.ts | XHRJSONEncodingDetection":{"message":"UTF-16 is not supported by response json in XMLHttpRequest"},"generated/Deprecation.ts | XMLHttpRequestSynchronousInNonWorkerOutsideBeforeUnload":{"message":"Synchronous XMLHttpRequest on the main thread is deprecated because of its detrimental effects to the end user's experience. For more help, check https://xhr.spec.whatwg.org/."},"generated/Deprecation.ts | XRSupportsSession":{"message":"supportsSession() is deprecated. Please use isSessionSupported() and check the resolved boolean value instead."},"models/bindings/ContentProviderBasedProject.ts | unknownErrorLoadingFile":{"message":"Unknown error loading file"},"models/bindings/DebuggerLanguagePlugins.ts | debugSymbolsIncomplete":{"message":"The debug information for function {PH1} is incomplete"},"models/bindings/DebuggerLanguagePlugins.ts | errorInDebuggerLanguagePlugin":{"message":"Error in debugger language plugin: {PH1}"},"models/bindings/DebuggerLanguagePlugins.ts | failedToLoadDebugSymbolsFor":{"message":"[{PH1}] Failed to load debug symbols for {PH2} ({PH3})"},"models/bindings/DebuggerLanguagePlugins.ts | failedToLoadDebugSymbolsForFunction":{"message":"No debug information for function \"{PH1}\""},"models/bindings/DebuggerLanguagePlugins.ts | loadedDebugSymbolsForButDidnt":{"message":"[{PH1}] Loaded debug symbols for {PH2}, but didn't find any source files"},"models/bindings/DebuggerLanguagePlugins.ts | loadedDebugSymbolsForFound":{"message":"[{PH1}] Loaded debug symbols for {PH2}, found {PH3} source file(s)"},"models/bindings/DebuggerLanguagePlugins.ts | loadingDebugSymbolsFor":{"message":"[{PH1}] Loading debug symbols for {PH2}..."},"models/bindings/DebuggerLanguagePlugins.ts | loadingDebugSymbolsForVia":{"message":"[{PH1}] Loading debug symbols for {PH2} (via {PH3})..."},"models/bindings/IgnoreListManager.ts | addAllContentScriptsToIgnoreList":{"message":"Add all extension scripts to ignore list"},"models/bindings/IgnoreListManager.ts | addAllThirdPartyScriptsToIgnoreList":{"message":"Add all third-party scripts to ignore list"},"models/bindings/IgnoreListManager.ts | addDirectoryToIgnoreList":{"message":"Add directory to ignore list"},"models/bindings/IgnoreListManager.ts | addScriptToIgnoreList":{"message":"Add script to ignore list"},"models/bindings/IgnoreListManager.ts | removeFromIgnoreList":{"message":"Remove from ignore list"},"models/bindings/ResourceScriptMapping.ts | liveEditCompileFailed":{"message":"LiveEdit compile failed: {PH1}"},"models/bindings/ResourceScriptMapping.ts | liveEditFailed":{"message":"LiveEdit failed: {PH1}"},"models/emulation/DeviceModeModel.ts | devicePixelRatioMustBeANumberOr":{"message":"Device pixel ratio must be a number or blank."},"models/emulation/DeviceModeModel.ts | devicePixelRatioMustBeGreater":{"message":"Device pixel ratio must be greater than or equal to {PH1}."},"models/emulation/DeviceModeModel.ts | devicePixelRatioMustBeLessThanOr":{"message":"Device pixel ratio must be less than or equal to {PH1}."},"models/emulation/DeviceModeModel.ts | heightCannotBeEmpty":{"message":"Height cannot be empty."},"models/emulation/DeviceModeModel.ts | heightMustBeANumber":{"message":"Height must be a number."},"models/emulation/DeviceModeModel.ts | heightMustBeGreaterThanOrEqualTo":{"message":"Height must be greater than or equal to {PH1}."},"models/emulation/DeviceModeModel.ts | heightMustBeLessThanOrEqualToS":{"message":"Height must be less than or equal to {PH1}."},"models/emulation/DeviceModeModel.ts | widthCannotBeEmpty":{"message":"Width cannot be empty."},"models/emulation/DeviceModeModel.ts | widthMustBeANumber":{"message":"Width must be a number."},"models/emulation/DeviceModeModel.ts | widthMustBeGreaterThanOrEqualToS":{"message":"Width must be greater than or equal to {PH1}."},"models/emulation/DeviceModeModel.ts | widthMustBeLessThanOrEqualToS":{"message":"Width must be less than or equal to {PH1}."},"models/emulation/EmulatedDevices.ts | laptopWithHiDPIScreen":{"message":"Laptop with HiDPI screen"},"models/emulation/EmulatedDevices.ts | laptopWithMDPIScreen":{"message":"Laptop with MDPI screen"},"models/emulation/EmulatedDevices.ts | laptopWithTouch":{"message":"Laptop with touch"},"models/har/Writer.ts | collectingContent":{"message":"Collecting content…"},"models/har/Writer.ts | writingFile":{"message":"Writing file…"},"models/issues_manager/BounceTrackingIssue.ts | bounceTrackingMitigations":{"message":"Bounce tracking mitigations"},"models/issues_manager/ClientHintIssue.ts | clientHintsInfrastructure":{"message":"Client Hints Infrastructure"},"models/issues_manager/ContentSecurityPolicyIssue.ts | contentSecurityPolicyEval":{"message":"Content Security Policy - Eval"},"models/issues_manager/ContentSecurityPolicyIssue.ts | contentSecurityPolicyInlineCode":{"message":"Content Security Policy - Inline Code"},"models/issues_manager/ContentSecurityPolicyIssue.ts | contentSecurityPolicySource":{"message":"Content Security Policy - Source Allowlists"},"models/issues_manager/ContentSecurityPolicyIssue.ts | trustedTypesFixViolations":{"message":"Trusted Types - Fix violations"},"models/issues_manager/ContentSecurityPolicyIssue.ts | trustedTypesPolicyViolation":{"message":"Trusted Types - Policy violation"},"models/issues_manager/CookieDeprecationMetadataIssue.ts | thirdPartyPhaseoutExplained":{"message":"Prepare for phasing out third-party cookies"},"models/issues_manager/CookieIssue.ts | anInsecure":{"message":"an insecure"},"models/issues_manager/CookieIssue.ts | aSecure":{"message":"a secure"},"models/issues_manager/CookieIssue.ts | fileCrosSiteRedirectBug":{"message":"File a bug"},"models/issues_manager/CookieIssue.ts | firstPartySetsExplained":{"message":"First-Party Sets and the SameParty attribute"},"models/issues_manager/CookieIssue.ts | howSchemefulSamesiteWorks":{"message":"How Schemeful Same-Site Works"},"models/issues_manager/CookieIssue.ts | samesiteCookiesExplained":{"message":"SameSite cookies explained"},"models/issues_manager/CookieIssue.ts | thirdPartyPhaseoutExplained":{"message":"Prepare for phasing out third-party cookies"},"models/issues_manager/CorsIssue.ts | CORS":{"message":"Cross-Origin Resource Sharing (CORS)"},"models/issues_manager/CorsIssue.ts | corsPrivateNetworkAccess":{"message":"Private Network Access"},"models/issues_manager/CrossOriginEmbedderPolicyIssue.ts | coopAndCoep":{"message":"COOP and COEP"},"models/issues_manager/CrossOriginEmbedderPolicyIssue.ts | samesiteAndSameorigin":{"message":"Same-Site and Same-Origin"},"models/issues_manager/DeprecationIssue.ts | feature":{"message":"Check the feature status page for more details."},"models/issues_manager/DeprecationIssue.ts | milestone":{"message":"This change will go into effect with milestone {milestone}."},"models/issues_manager/DeprecationIssue.ts | title":{"message":"Deprecated feature used"},"models/issues_manager/FederatedAuthRequestIssue.ts | fedCm":{"message":"Federated Credential Management API"},"models/issues_manager/FederatedAuthUserInfoRequestIssue.ts | fedCmUserInfo":{"message":"Federated Credential Management User Info API"},"models/issues_manager/GenericIssue.ts | autocompleteAttributePageTitle":{"message":"HTML attribute: autocomplete"},"models/issues_manager/GenericIssue.ts | corbExplainerPageTitle":{"message":"CORB explainer"},"models/issues_manager/GenericIssue.ts | crossOriginPortalPostMessage":{"message":"Portals - Same-origin communication channels"},"models/issues_manager/GenericIssue.ts | howDoesAutofillWorkPageTitle":{"message":"How does autofill work?"},"models/issues_manager/GenericIssue.ts | inputFormElementPageTitle":{"message":"The form input element"},"models/issues_manager/GenericIssue.ts | labelFormlementsPageTitle":{"message":"The label elements"},"models/issues_manager/HeavyAdIssue.ts | handlingHeavyAdInterventions":{"message":"Handling Heavy Ad Interventions"},"models/issues_manager/Issue.ts | breakingChangeIssue":{"message":"A breaking change issue: the page may stop working in an upcoming version of Chrome"},"models/issues_manager/Issue.ts | breakingChanges":{"message":"Breaking Changes"},"models/issues_manager/Issue.ts | improvementIssue":{"message":"An improvement issue: there is an opportunity to improve the page"},"models/issues_manager/Issue.ts | improvements":{"message":"Improvements"},"models/issues_manager/Issue.ts | pageErrorIssue":{"message":"A page error issue: the page is not working correctly"},"models/issues_manager/Issue.ts | pageErrors":{"message":"Page Errors"},"models/issues_manager/LowTextContrastIssue.ts | colorAndContrastAccessibility":{"message":"Color and contrast accessibility"},"models/issues_manager/MixedContentIssue.ts | preventingMixedContent":{"message":"Preventing mixed content"},"models/issues_manager/QuirksModeIssue.ts | documentCompatibilityMode":{"message":"Document compatibility mode"},"models/issues_manager/SharedArrayBufferIssue.ts | enablingSharedArrayBuffer":{"message":"Enabling SharedArrayBuffer"},"models/logs/logs-meta.ts | clear":{"message":"clear"},"models/logs/logs-meta.ts | doNotPreserveLogOnPageReload":{"message":"Do not preserve log on page reload / navigation"},"models/logs/logs-meta.ts | preserve":{"message":"preserve"},"models/logs/logs-meta.ts | preserveLog":{"message":"Preserve log"},"models/logs/logs-meta.ts | preserveLogOnPageReload":{"message":"Preserve log on page reload / navigation"},"models/logs/logs-meta.ts | recordNetworkLog":{"message":"Record network log"},"models/logs/logs-meta.ts | reset":{"message":"reset"},"models/logs/NetworkLog.ts | anonymous":{"message":""},"models/persistence/EditFileSystemView.ts | add":{"message":"Add"},"models/persistence/EditFileSystemView.ts | enterAPath":{"message":"Enter a path"},"models/persistence/EditFileSystemView.ts | enterAUniquePath":{"message":"Enter a unique path"},"models/persistence/EditFileSystemView.ts | excludedFolders":{"message":"Excluded folders"},"models/persistence/EditFileSystemView.ts | folderPath":{"message":"Folder path"},"models/persistence/EditFileSystemView.ts | none":{"message":"None"},"models/persistence/EditFileSystemView.ts | sViaDevtools":{"message":"{PH1} (via .devtools)"},"models/persistence/IsolatedFileSystem.ts | blobCouldNotBeLoaded":{"message":"Blob could not be loaded."},"models/persistence/IsolatedFileSystem.ts | cantReadFileSS":{"message":"Can't read file: {PH1}: {PH2}"},"models/persistence/IsolatedFileSystem.ts | fileSystemErrorS":{"message":"File system error: {PH1}"},"models/persistence/IsolatedFileSystem.ts | linkedToS":{"message":"Linked to {PH1}"},"models/persistence/IsolatedFileSystem.ts | unknownErrorReadingFileS":{"message":"Unknown error reading file: {PH1}"},"models/persistence/IsolatedFileSystemManager.ts | unableToAddFilesystemS":{"message":"Unable to add filesystem: {PH1}"},"models/persistence/persistence-meta.ts | disableOverrideNetworkRequests":{"message":"Disable override network requests"},"models/persistence/persistence-meta.ts | enableLocalOverrides":{"message":"Enable Local Overrides"},"models/persistence/persistence-meta.ts | enableOverrideNetworkRequests":{"message":"Enable override network requests"},"models/persistence/persistence-meta.ts | interception":{"message":"interception"},"models/persistence/persistence-meta.ts | network":{"message":"network"},"models/persistence/persistence-meta.ts | override":{"message":"override"},"models/persistence/persistence-meta.ts | request":{"message":"request"},"models/persistence/persistence-meta.ts | rewrite":{"message":"rewrite"},"models/persistence/persistence-meta.ts | showWorkspace":{"message":"Show Workspace settings"},"models/persistence/persistence-meta.ts | workspace":{"message":"Workspace"},"models/persistence/PersistenceActions.ts | openInContainingFolder":{"message":"Open in containing folder"},"models/persistence/PersistenceActions.ts | overrideContent":{"message":"Override content"},"models/persistence/PersistenceActions.ts | overrideSourceMappedFileExplanation":{"message":"‘{PH1}’ is a source mapped file and cannot be overridden."},"models/persistence/PersistenceActions.ts | overrideSourceMappedFileWarning":{"message":"Override ‘{PH1}’ instead?"},"models/persistence/PersistenceActions.ts | saveAs":{"message":"Save as..."},"models/persistence/PersistenceActions.ts | saveImage":{"message":"Save image"},"models/persistence/PersistenceActions.ts | showOverrides":{"message":"Show all overrides"},"models/persistence/PersistenceUtils.ts | linkedToS":{"message":"Linked to {PH1}"},"models/persistence/PersistenceUtils.ts | linkedToSourceMapS":{"message":"Linked to source map: {PH1}"},"models/persistence/PlatformFileSystem.ts | unableToReadFilesWithThis":{"message":"PlatformFileSystem cannot read files."},"models/persistence/WorkspaceSettingsTab.ts | addFolder":{"message":"Add folder…"},"models/persistence/WorkspaceSettingsTab.ts | folderExcludePattern":{"message":"Folder exclude pattern"},"models/persistence/WorkspaceSettingsTab.ts | mappingsAreInferredAutomatically":{"message":"Mappings are inferred automatically."},"models/persistence/WorkspaceSettingsTab.ts | remove":{"message":"Remove"},"models/persistence/WorkspaceSettingsTab.ts | workspace":{"message":"Workspace"},"models/timeline_model/TimelineJSProfile.ts | threadS":{"message":"Thread {PH1}"},"models/timeline_model/TimelineModel.ts | dedicatedWorker":{"message":"Dedicated Worker"},"models/timeline_model/TimelineModel.ts | threadS":{"message":"Thread {PH1}"},"models/timeline_model/TimelineModel.ts | workerS":{"message":"Worker — {PH1}"},"models/timeline_model/TimelineModel.ts | workerSS":{"message":"Worker: {PH1} — {PH2}"},"models/workspace/UISourceCode.ts | index":{"message":"(index)"},"models/workspace/UISourceCode.ts | thisFileWasChangedExternally":{"message":"This file was changed externally. Would you like to reload it?"},"panels/accessibility/accessibility-meta.ts | accessibility":{"message":"Accessibility"},"panels/accessibility/accessibility-meta.ts | shoAccessibility":{"message":"Show Accessibility"},"panels/accessibility/AccessibilityNodeView.ts | accessibilityNodeNotExposed":{"message":"Accessibility node not exposed"},"panels/accessibility/AccessibilityNodeView.ts | ancestorChildrenAreAll":{"message":"Ancestor's children are all presentational: "},"panels/accessibility/AccessibilityNodeView.ts | computedProperties":{"message":"Computed Properties"},"panels/accessibility/AccessibilityNodeView.ts | elementHasEmptyAltText":{"message":"Element has empty alt text."},"panels/accessibility/AccessibilityNodeView.ts | elementHasPlaceholder":{"message":"Element has {PH1}."},"panels/accessibility/AccessibilityNodeView.ts | elementIsHiddenBy":{"message":"Element is hidden by active modal dialog: "},"panels/accessibility/AccessibilityNodeView.ts | elementIsHiddenByChildTree":{"message":"Element is hidden by child tree: "},"panels/accessibility/AccessibilityNodeView.ts | elementIsInAnInertSubTree":{"message":"Element is in an inert subtree from "},"panels/accessibility/AccessibilityNodeView.ts | elementIsInert":{"message":"Element is inert."},"panels/accessibility/AccessibilityNodeView.ts | elementIsNotRendered":{"message":"Element is not rendered."},"panels/accessibility/AccessibilityNodeView.ts | elementIsNotVisible":{"message":"Element is not visible."},"panels/accessibility/AccessibilityNodeView.ts | elementIsPlaceholder":{"message":"Element is {PH1}."},"panels/accessibility/AccessibilityNodeView.ts | elementIsPresentational":{"message":"Element is presentational."},"panels/accessibility/AccessibilityNodeView.ts | elementNotInteresting":{"message":"Element not interesting for accessibility."},"panels/accessibility/AccessibilityNodeView.ts | elementsInheritsPresentational":{"message":"Element inherits presentational role from "},"panels/accessibility/AccessibilityNodeView.ts | invalidSource":{"message":"Invalid source."},"panels/accessibility/AccessibilityNodeView.ts | labelFor":{"message":"Label for "},"panels/accessibility/AccessibilityNodeView.ts | noAccessibilityNode":{"message":"No accessibility node"},"panels/accessibility/AccessibilityNodeView.ts | noNodeWithThisId":{"message":"No node with this ID."},"panels/accessibility/AccessibilityNodeView.ts | noTextContent":{"message":"No text content."},"panels/accessibility/AccessibilityNodeView.ts | notSpecified":{"message":"Not specified"},"panels/accessibility/AccessibilityNodeView.ts | partOfLabelElement":{"message":"Part of label element: "},"panels/accessibility/AccessibilityNodeView.ts | placeholderIsPlaceholderOnAncestor":{"message":"{PH1} is {PH2} on ancestor: "},"panels/accessibility/AccessibilityStrings.ts | activeDescendant":{"message":"Active descendant"},"panels/accessibility/AccessibilityStrings.ts | aHumanreadableVersionOfTheValue":{"message":"A human-readable version of the value of a range widget (where necessary)."},"panels/accessibility/AccessibilityStrings.ts | atomicLiveRegions":{"message":"Atomic (live regions)"},"panels/accessibility/AccessibilityStrings.ts | busyLiveRegions":{"message":"Busy (live regions)"},"panels/accessibility/AccessibilityStrings.ts | canSetValue":{"message":"Can set value"},"panels/accessibility/AccessibilityStrings.ts | checked":{"message":"Checked"},"panels/accessibility/AccessibilityStrings.ts | contents":{"message":"Contents"},"panels/accessibility/AccessibilityStrings.ts | controls":{"message":"Controls"},"panels/accessibility/AccessibilityStrings.ts | describedBy":{"message":"Described by"},"panels/accessibility/AccessibilityStrings.ts | description":{"message":"Description"},"panels/accessibility/AccessibilityStrings.ts | disabled":{"message":"Disabled"},"panels/accessibility/AccessibilityStrings.ts | editable":{"message":"Editable"},"panels/accessibility/AccessibilityStrings.ts | elementOrElementsWhichFormThe":{"message":"Element or elements which form the description of this element."},"panels/accessibility/AccessibilityStrings.ts | elementOrElementsWhichMayFormThe":{"message":"Element or elements which may form the name of this element."},"panels/accessibility/AccessibilityStrings.ts | elementOrElementsWhichShouldBe":{"message":"Element or elements which should be considered descendants of this element, despite not being descendants in the DOM."},"panels/accessibility/AccessibilityStrings.ts | elementOrElementsWhoseContentOr":{"message":"Element or elements whose content or presence is/are controlled by this widget."},"panels/accessibility/AccessibilityStrings.ts | elementToWhichTheUserMayChooseTo":{"message":"Element to which the user may choose to navigate after this one, instead of the next element in the DOM order."},"panels/accessibility/AccessibilityStrings.ts | expanded":{"message":"Expanded"},"panels/accessibility/AccessibilityStrings.ts | focusable":{"message":"Focusable"},"panels/accessibility/AccessibilityStrings.ts | focused":{"message":"Focused"},"panels/accessibility/AccessibilityStrings.ts | forARangeWidgetTheMaximumAllowed":{"message":"For a range widget, the maximum allowed value."},"panels/accessibility/AccessibilityStrings.ts | forARangeWidgetTheMinimumAllowed":{"message":"For a range widget, the minimum allowed value."},"panels/accessibility/AccessibilityStrings.ts | fromAttribute":{"message":"From attribute"},"panels/accessibility/AccessibilityStrings.ts | fromCaption":{"message":"From caption"},"panels/accessibility/AccessibilityStrings.ts | fromDescription":{"message":"From description"},"panels/accessibility/AccessibilityStrings.ts | fromLabel":{"message":"From label"},"panels/accessibility/AccessibilityStrings.ts | fromLabelFor":{"message":"From label (for= attribute)"},"panels/accessibility/AccessibilityStrings.ts | fromLabelWrapped":{"message":"From label (wrapped)"},"panels/accessibility/AccessibilityStrings.ts | fromLegend":{"message":"From legend"},"panels/accessibility/AccessibilityStrings.ts | fromNativeHtml":{"message":"From native HTML"},"panels/accessibility/AccessibilityStrings.ts | fromPlaceholderAttribute":{"message":"From placeholder attribute"},"panels/accessibility/AccessibilityStrings.ts | fromRubyAnnotation":{"message":"From ruby annotation"},"panels/accessibility/AccessibilityStrings.ts | fromStyle":{"message":"From style"},"panels/accessibility/AccessibilityStrings.ts | fromTitle":{"message":"From title"},"panels/accessibility/AccessibilityStrings.ts | hasAutocomplete":{"message":"Has autocomplete"},"panels/accessibility/AccessibilityStrings.ts | hasPopup":{"message":"Has popup"},"panels/accessibility/AccessibilityStrings.ts | help":{"message":"Help"},"panels/accessibility/AccessibilityStrings.ts | ifAndHowThisElementCanBeEdited":{"message":"If and how this element can be edited."},"panels/accessibility/AccessibilityStrings.ts | ifThisElementMayReceiveLive":{"message":"If this element may receive live updates, whether the entire live region should be presented to the user on changes, or only changed nodes."},"panels/accessibility/AccessibilityStrings.ts | ifThisElementMayReceiveLiveUpdates":{"message":"If this element may receive live updates, what type of updates should trigger a notification."},"panels/accessibility/AccessibilityStrings.ts | ifThisElementMayReceiveLiveUpdatesThe":{"message":"If this element may receive live updates, the root element of the containing live region."},"panels/accessibility/AccessibilityStrings.ts | ifTrueThisElementCanReceiveFocus":{"message":"If true, this element can receive focus."},"panels/accessibility/AccessibilityStrings.ts | ifTrueThisElementCurrentlyCannot":{"message":"If true, this element currently cannot be interacted with."},"panels/accessibility/AccessibilityStrings.ts | ifTrueThisElementCurrentlyHas":{"message":"If true, this element currently has focus."},"panels/accessibility/AccessibilityStrings.ts | ifTrueThisElementMayBeInteracted":{"message":"If true, this element may be interacted with, but its value cannot be changed."},"panels/accessibility/AccessibilityStrings.ts | ifTrueThisElementsUserentered":{"message":"If true, this element's user-entered value does not conform to validation requirement."},"panels/accessibility/AccessibilityStrings.ts | implicit":{"message":"Implicit"},"panels/accessibility/AccessibilityStrings.ts | implicitValue":{"message":"Implicit value."},"panels/accessibility/AccessibilityStrings.ts | indicatesThePurposeOfThisElement":{"message":"Indicates the purpose of this element, such as a user interface idiom for a widget, or structural role within a document."},"panels/accessibility/AccessibilityStrings.ts | invalidUserEntry":{"message":"Invalid user entry"},"panels/accessibility/AccessibilityStrings.ts | labeledBy":{"message":"Labeled by"},"panels/accessibility/AccessibilityStrings.ts | level":{"message":"Level"},"panels/accessibility/AccessibilityStrings.ts | liveRegion":{"message":"Live region"},"panels/accessibility/AccessibilityStrings.ts | liveRegionRoot":{"message":"Live region root"},"panels/accessibility/AccessibilityStrings.ts | maximumValue":{"message":"Maximum value"},"panels/accessibility/AccessibilityStrings.ts | minimumValue":{"message":"Minimum value"},"panels/accessibility/AccessibilityStrings.ts | multiline":{"message":"Multi-line"},"panels/accessibility/AccessibilityStrings.ts | multiselectable":{"message":"Multi-selectable"},"panels/accessibility/AccessibilityStrings.ts | orientation":{"message":"Orientation"},"panels/accessibility/AccessibilityStrings.ts | pressed":{"message":"Pressed"},"panels/accessibility/AccessibilityStrings.ts | readonlyString":{"message":"Read-only"},"panels/accessibility/AccessibilityStrings.ts | relatedElement":{"message":"Related element"},"panels/accessibility/AccessibilityStrings.ts | relevantLiveRegions":{"message":"Relevant (live regions)"},"panels/accessibility/AccessibilityStrings.ts | requiredString":{"message":"Required"},"panels/accessibility/AccessibilityStrings.ts | role":{"message":"Role"},"panels/accessibility/AccessibilityStrings.ts | selectedString":{"message":"Selected"},"panels/accessibility/AccessibilityStrings.ts | theAccessibleDescriptionForThis":{"message":"The accessible description for this element."},"panels/accessibility/AccessibilityStrings.ts | theComputedHelpTextForThis":{"message":"The computed help text for this element."},"panels/accessibility/AccessibilityStrings.ts | theComputedNameOfThisElement":{"message":"The computed name of this element."},"panels/accessibility/AccessibilityStrings.ts | theDescendantOfThisElementWhich":{"message":"The descendant of this element which is active; i.e. the element to which focus should be delegated."},"panels/accessibility/AccessibilityStrings.ts | theHierarchicalLevelOfThis":{"message":"The hierarchical level of this element."},"panels/accessibility/AccessibilityStrings.ts | theValueOfThisElementThisMayBe":{"message":"The value of this element; this may be user-provided or developer-provided, depending on the element."},"panels/accessibility/AccessibilityStrings.ts | value":{"message":"Value"},"panels/accessibility/AccessibilityStrings.ts | valueDescription":{"message":"Value description"},"panels/accessibility/AccessibilityStrings.ts | valueFromAttribute":{"message":"Value from attribute."},"panels/accessibility/AccessibilityStrings.ts | valueFromDescriptionElement":{"message":"Value from description element."},"panels/accessibility/AccessibilityStrings.ts | valueFromElementContents":{"message":"Value from element contents."},"panels/accessibility/AccessibilityStrings.ts | valueFromFigcaptionElement":{"message":"Value from figcaption element."},"panels/accessibility/AccessibilityStrings.ts | valueFromLabelElement":{"message":"Value from label element."},"panels/accessibility/AccessibilityStrings.ts | valueFromLabelElementWithFor":{"message":"Value from label element with for= attribute."},"panels/accessibility/AccessibilityStrings.ts | valueFromLabelElementWrapped":{"message":"Value from a wrapping label element."},"panels/accessibility/AccessibilityStrings.ts | valueFromLegendElement":{"message":"Value from legend element."},"panels/accessibility/AccessibilityStrings.ts | valueFromNativeHtmlRuby":{"message":"Value from plain HTML ruby annotation."},"panels/accessibility/AccessibilityStrings.ts | valueFromNativeHtmlUnknownSource":{"message":"Value from native HTML (unknown source)."},"panels/accessibility/AccessibilityStrings.ts | valueFromPlaceholderAttribute":{"message":"Value from placeholder attribute."},"panels/accessibility/AccessibilityStrings.ts | valueFromRelatedElement":{"message":"Value from related element."},"panels/accessibility/AccessibilityStrings.ts | valueFromStyle":{"message":"Value from style."},"panels/accessibility/AccessibilityStrings.ts | valueFromTableCaption":{"message":"Value from table caption."},"panels/accessibility/AccessibilityStrings.ts | valueFromTitleAttribute":{"message":"Value from title attribute."},"panels/accessibility/AccessibilityStrings.ts | whetherAndWhatPriorityOfLive":{"message":"Whether and what priority of live updates may be expected for this element."},"panels/accessibility/AccessibilityStrings.ts | whetherAndWhatTypeOfAutocomplete":{"message":"Whether and what type of autocomplete suggestions are currently provided by this element."},"panels/accessibility/AccessibilityStrings.ts | whetherAUserMaySelectMoreThanOne":{"message":"Whether a user may select more than one option from this widget."},"panels/accessibility/AccessibilityStrings.ts | whetherTheOptionRepresentedBy":{"message":"Whether the option represented by this element is currently selected."},"panels/accessibility/AccessibilityStrings.ts | whetherTheValueOfThisElementCan":{"message":"Whether the value of this element can be set."},"panels/accessibility/AccessibilityStrings.ts | whetherThisCheckboxRadioButtonOr":{"message":"Whether this checkbox, radio button or tree item is checked, unchecked, or mixed (e.g. has both checked and un-checked children)."},"panels/accessibility/AccessibilityStrings.ts | whetherThisElementHasCausedSome":{"message":"Whether this element has caused some kind of pop-up (such as a menu) to appear."},"panels/accessibility/AccessibilityStrings.ts | whetherThisElementIsARequired":{"message":"Whether this element is a required field in a form."},"panels/accessibility/AccessibilityStrings.ts | whetherThisElementOrAnother":{"message":"Whether this element, or another grouping element it controls, is expanded."},"panels/accessibility/AccessibilityStrings.ts | whetherThisElementOrItsSubtree":{"message":"Whether this element or its subtree are currently being updated (and thus may be in an inconsistent state)."},"panels/accessibility/AccessibilityStrings.ts | whetherThisLinearElements":{"message":"Whether this linear element's orientation is horizontal or vertical."},"panels/accessibility/AccessibilityStrings.ts | whetherThisTextBoxMayHaveMore":{"message":"Whether this text box may have more than one line."},"panels/accessibility/AccessibilityStrings.ts | whetherThisToggleButtonIs":{"message":"Whether this toggle button is currently in a pressed state."},"panels/accessibility/ARIAAttributesView.ts | ariaAttributes":{"message":"ARIA Attributes"},"panels/accessibility/ARIAAttributesView.ts | noAriaAttributes":{"message":"No ARIA attributes"},"panels/accessibility/AXBreadcrumbsPane.ts | accessibilityTree":{"message":"Accessibility Tree"},"panels/accessibility/AXBreadcrumbsPane.ts | fullTreeExperimentDescription":{"message":"The accessibility tree moved to the top right corner of the DOM tree."},"panels/accessibility/AXBreadcrumbsPane.ts | fullTreeExperimentName":{"message":"Enable full-page accessibility tree"},"panels/accessibility/AXBreadcrumbsPane.ts | ignored":{"message":"Ignored"},"panels/accessibility/AXBreadcrumbsPane.ts | reloadRequired":{"message":"Reload required before the change takes effect."},"panels/accessibility/AXBreadcrumbsPane.ts | scrollIntoView":{"message":"Scroll into view"},"panels/accessibility/SourceOrderView.ts | noSourceOrderInformation":{"message":"No source order information available"},"panels/accessibility/SourceOrderView.ts | showSourceOrder":{"message":"Show source order"},"panels/accessibility/SourceOrderView.ts | sourceOrderViewer":{"message":"Source Order Viewer"},"panels/accessibility/SourceOrderView.ts | thereMayBeADelayInDisplaying":{"message":"There may be a delay in displaying source order for elements with many children"},"panels/animation/animation-meta.ts | animations":{"message":"Animations"},"panels/animation/animation-meta.ts | showAnimations":{"message":"Show Animations"},"panels/animation/AnimationTimeline.ts | animationPreviews":{"message":"Animation previews"},"panels/animation/AnimationTimeline.ts | animationPreviewS":{"message":"Animation Preview {PH1}"},"panels/animation/AnimationTimeline.ts | clearAll":{"message":"Clear all"},"panels/animation/AnimationTimeline.ts | pause":{"message":"Pause"},"panels/animation/AnimationTimeline.ts | pauseAll":{"message":"Pause all"},"panels/animation/AnimationTimeline.ts | pauseTimeline":{"message":"Pause timeline"},"panels/animation/AnimationTimeline.ts | playbackRatePlaceholder":{"message":"{PH1}%"},"panels/animation/AnimationTimeline.ts | playbackRates":{"message":"Playback rates"},"panels/animation/AnimationTimeline.ts | playTimeline":{"message":"Play timeline"},"panels/animation/AnimationTimeline.ts | replayTimeline":{"message":"Replay timeline"},"panels/animation/AnimationTimeline.ts | resumeAll":{"message":"Resume all"},"panels/animation/AnimationTimeline.ts | selectAnEffectAboveToInspectAnd":{"message":"Select an effect above to inspect and modify."},"panels/animation/AnimationTimeline.ts | setSpeedToS":{"message":"Set speed to {PH1}"},"panels/animation/AnimationTimeline.ts | waitingForAnimations":{"message":"Waiting for animations..."},"panels/animation/AnimationUI.ts | animationEndpointSlider":{"message":"Animation Endpoint slider"},"panels/animation/AnimationUI.ts | animationKeyframeSlider":{"message":"Animation Keyframe slider"},"panels/animation/AnimationUI.ts | sSlider":{"message":"{PH1} slider"},"panels/application/application-meta.ts | application":{"message":"Application"},"panels/application/application-meta.ts | clearSiteData":{"message":"Clear site data"},"panels/application/application-meta.ts | clearSiteDataIncludingThirdparty":{"message":"Clear site data (including third-party cookies)"},"panels/application/application-meta.ts | pwa":{"message":"pwa"},"panels/application/application-meta.ts | showApplication":{"message":"Show Application"},"panels/application/application-meta.ts | startRecordingEvents":{"message":"Start recording events"},"panels/application/application-meta.ts | stopRecordingEvents":{"message":"Stop recording events"},"panels/application/ApplicationPanelSidebar.ts | application":{"message":"Application"},"panels/application/ApplicationPanelSidebar.ts | applicationSidebarPanel":{"message":"Application panel sidebar"},"panels/application/ApplicationPanelSidebar.ts | appManifest":{"message":"App Manifest"},"panels/application/ApplicationPanelSidebar.ts | backgroundServices":{"message":"Background services"},"panels/application/ApplicationPanelSidebar.ts | beforeInvokeAlert":{"message":"{PH1}: Invoke to scroll to this section in manifest"},"panels/application/ApplicationPanelSidebar.ts | clear":{"message":"Clear"},"panels/application/ApplicationPanelSidebar.ts | cookies":{"message":"Cookies"},"panels/application/ApplicationPanelSidebar.ts | cookiesUsedByFramesFromS":{"message":"Cookies used by frames from {PH1}"},"panels/application/ApplicationPanelSidebar.ts | documentNotAvailable":{"message":"Document not available"},"panels/application/ApplicationPanelSidebar.ts | frames":{"message":"Frames"},"panels/application/ApplicationPanelSidebar.ts | indexeddb":{"message":"IndexedDB"},"panels/application/ApplicationPanelSidebar.ts | keyPathS":{"message":"Key path: {PH1}"},"panels/application/ApplicationPanelSidebar.ts | localFiles":{"message":"Local Files"},"panels/application/ApplicationPanelSidebar.ts | localStorage":{"message":"Local storage"},"panels/application/ApplicationPanelSidebar.ts | manifest":{"message":"Manifest"},"panels/application/ApplicationPanelSidebar.ts | noManifestDetected":{"message":"No manifest detected"},"panels/application/ApplicationPanelSidebar.ts | onInvokeAlert":{"message":"Scrolled to {PH1}"},"panels/application/ApplicationPanelSidebar.ts | onInvokeManifestAlert":{"message":"Manifest: Invoke to scroll to the top of manifest"},"panels/application/ApplicationPanelSidebar.ts | openedWindows":{"message":"Opened Windows"},"panels/application/ApplicationPanelSidebar.ts | refreshIndexeddb":{"message":"Refresh IndexedDB"},"panels/application/ApplicationPanelSidebar.ts | sessionStorage":{"message":"Session storage"},"panels/application/ApplicationPanelSidebar.ts | storage":{"message":"Storage"},"panels/application/ApplicationPanelSidebar.ts | theContentOfThisDocumentHasBeen":{"message":"The content of this document has been generated dynamically via 'document.write()'."},"panels/application/ApplicationPanelSidebar.ts | thirdPartyPhaseout":{"message":"Cookies from {PH1} may have been blocked due to third-party cookie phaseout."},"panels/application/ApplicationPanelSidebar.ts | versionS":{"message":"Version: {PH1}"},"panels/application/ApplicationPanelSidebar.ts | versionSEmpty":{"message":"Version: {PH1} (empty)"},"panels/application/ApplicationPanelSidebar.ts | webWorkers":{"message":"Web Workers"},"panels/application/ApplicationPanelSidebar.ts | windowWithoutTitle":{"message":"Window without title"},"panels/application/ApplicationPanelSidebar.ts | worker":{"message":"worker"},"panels/application/AppManifestView.ts | actualHeightSpxOfSSDoesNotMatch":{"message":"Actual height ({PH1}px) of {PH2} {PH3} does not match specified height ({PH4}px)"},"panels/application/AppManifestView.ts | actualSizeSspxOfSSDoesNotMatch":{"message":"Actual size ({PH1}×{PH2})px of {PH3} {PH4} does not match specified size ({PH5}×{PH6}px)"},"panels/application/AppManifestView.ts | actualWidthSpxOfSSDoesNotMatch":{"message":"Actual width ({PH1}px) of {PH2} {PH3} does not match specified width ({PH4}px)"},"panels/application/AppManifestView.ts | appIdExplainer":{"message":"This is used by the browser to know whether the manifest should be updating an existing application, or whether it refers to a new web app that can be installed."},"panels/application/AppManifestView.ts | appIdNote":{"message":"{PH1} {PH2} is not specified in the manifest, {PH3} is used instead. To specify an App ID that matches the current identity, set the {PH4} field to {PH5} {PH6}."},"panels/application/AppManifestView.ts | aUrlInTheManifestContainsA":{"message":"A URL in the manifest contains a username, password, or port"},"panels/application/AppManifestView.ts | avoidPurposeAnyAndMaskable":{"message":"Declaring an icon with 'purpose' of 'any maskable' is discouraged. It is likely to look incorrect on some platforms due to too much or too little padding."},"panels/application/AppManifestView.ts | backgroundColor":{"message":"Background color"},"panels/application/AppManifestView.ts | computedAppId":{"message":"Computed App ID"},"panels/application/AppManifestView.ts | copiedToClipboard":{"message":"Copied suggested ID {PH1} to clipboard"},"panels/application/AppManifestView.ts | copyToClipboard":{"message":"Copy suggested ID to clipboard"},"panels/application/AppManifestView.ts | couldNotCheckServiceWorker":{"message":"Could not check service worker without a 'start_url' field in the manifest"},"panels/application/AppManifestView.ts | couldNotDownloadARequiredIcon":{"message":"Could not download a required icon from the manifest"},"panels/application/AppManifestView.ts | customizePwaTitleBar":{"message":"Customize the window controls overlay of your PWA's title bar"},"panels/application/AppManifestView.ts | darkBackgroundColor":{"message":"Dark background color"},"panels/application/AppManifestView.ts | darkThemeColor":{"message":"Dark theme color"},"panels/application/AppManifestView.ts | description":{"message":"Description"},"panels/application/AppManifestView.ts | descriptionMayBeTruncated":{"message":"Description may be truncated."},"panels/application/AppManifestView.ts | display":{"message":"Display"},"panels/application/AppManifestView.ts | documentationOnMaskableIcons":{"message":"documentation on maskable icons"},"panels/application/AppManifestView.ts | downloadedIconWasEmptyOr":{"message":"Downloaded icon was empty or corrupted"},"panels/application/AppManifestView.ts | errorsAndWarnings":{"message":"Errors and warnings"},"panels/application/AppManifestView.ts | formFactor":{"message":"Form factor"},"panels/application/AppManifestView.ts | icon":{"message":"Icon"},"panels/application/AppManifestView.ts | icons":{"message":"Icons"},"panels/application/AppManifestView.ts | identity":{"message":"Identity"},"panels/application/AppManifestView.ts | imageFromS":{"message":"Image from {PH1}"},"panels/application/AppManifestView.ts | installability":{"message":"Installability"},"panels/application/AppManifestView.ts | label":{"message":"Label"},"panels/application/AppManifestView.ts | learnMore":{"message":"Learn more"},"panels/application/AppManifestView.ts | manifestContainsDisplayoverride":{"message":"Manifest contains 'display_override' field, and the first supported display mode must be one of 'standalone', 'fullscreen', or 'minimal-ui'"},"panels/application/AppManifestView.ts | manifestCouldNotBeFetchedIsEmpty":{"message":"Manifest could not be fetched, is empty, or could not be parsed"},"panels/application/AppManifestView.ts | manifestDisplayPropertyMustBeOne":{"message":"Manifest 'display' property must be one of 'standalone', 'fullscreen', or 'minimal-ui'"},"panels/application/AppManifestView.ts | manifestDoesNotContainANameOr":{"message":"Manifest does not contain a 'name' or 'short_name' field"},"panels/application/AppManifestView.ts | manifestDoesNotContainASuitable":{"message":"Manifest does not contain a suitable icon—PNG, SVG, or WebP format of at least {PH1}px is required, the 'sizes' attribute must be set, and the 'purpose' attribute, if set, must include 'any'."},"panels/application/AppManifestView.ts | manifestSpecifies":{"message":"Manifest specifies 'prefer_related_applications: true'"},"panels/application/AppManifestView.ts | manifestStartUrlIsNotValid":{"message":"Manifest 'start_url' is not valid"},"panels/application/AppManifestView.ts | name":{"message":"Name"},"panels/application/AppManifestView.ts | needHelpReadOurS":{"message":"Need help? Read the {PH1}."},"panels/application/AppManifestView.ts | newNoteUrl":{"message":"New note URL"},"panels/application/AppManifestView.ts | noPlayStoreIdProvided":{"message":"No Play store ID provided"},"panels/application/AppManifestView.ts | noScreenshotsForRicherPWAInstallOnDesktop":{"message":"Richer PWA Install UI won’t be available on desktop. Please add at least one screenshot with the form_factor set to wide."},"panels/application/AppManifestView.ts | noScreenshotsForRicherPWAInstallOnMobile":{"message":"Richer PWA Install UI won’t be available on mobile. Please add at least one screenshot for which form_factor is not set or set to a value other than wide."},"panels/application/AppManifestView.ts | noSuppliedIconIsAtLeastSpxSquare":{"message":"No supplied icon is at least {PH1} pixels square in PNG, SVG, or WebP format, with the purpose attribute unset or set to 'any'."},"panels/application/AppManifestView.ts | note":{"message":"Note:"},"panels/application/AppManifestView.ts | orientation":{"message":"Orientation"},"panels/application/AppManifestView.ts | pageDoesNotWorkOffline":{"message":"Page does not work offline"},"panels/application/AppManifestView.ts | pageDoesNotWorkOfflineThePage":{"message":"Page does not work offline. Starting in Chrome 93, the installability criteria are changing, and this site will not be installable. See {PH1} for more information."},"panels/application/AppManifestView.ts | pageHasNoManifestLinkUrl":{"message":"Page has no manifest URL"},"panels/application/AppManifestView.ts | pageIsLoadedInAnIncognitoWindow":{"message":"Page is loaded in an incognito window"},"panels/application/AppManifestView.ts | pageIsNotLoadedInTheMainFrame":{"message":"Page is not loaded in the main frame"},"panels/application/AppManifestView.ts | pageIsNotServedFromASecureOrigin":{"message":"Page is not served from a secure origin"},"panels/application/AppManifestView.ts | platform":{"message":"Platform"},"panels/application/AppManifestView.ts | preferrelatedapplicationsIsOnly":{"message":"'prefer_related_applications' is only supported on Chrome Beta and Stable channels on Android."},"panels/application/AppManifestView.ts | presentation":{"message":"Presentation"},"panels/application/AppManifestView.ts | protocolHandlers":{"message":"Protocol Handlers"},"panels/application/AppManifestView.ts | screenshot":{"message":"Screenshot"},"panels/application/AppManifestView.ts | screenshotPixelSize":{"message":"Screenshot {url} should specify a pixel size [width]x[height] instead of any as first size."},"panels/application/AppManifestView.ts | screenshotS":{"message":"Screenshot #{PH1}"},"panels/application/AppManifestView.ts | screenshotsMustHaveSameAspectRatio":{"message":"All screenshots with the same form_factor must have the same aspect ratio as the first screenshot with that form_factor. Some screenshots will be ignored."},"panels/application/AppManifestView.ts | selectWindowControlsOverlayEmulationOs":{"message":"Emulate the Window Controls Overlay on"},"panels/application/AppManifestView.ts | shortcutS":{"message":"Shortcut #{PH1}"},"panels/application/AppManifestView.ts | shortcutsMayBeNotAvailable":{"message":"The maximum number of shortcuts is platform dependent. Some shortcuts may be not available."},"panels/application/AppManifestView.ts | shortcutSShouldIncludeAXPixel":{"message":"Shortcut #{PH1} should include a 96×96 pixel icon"},"panels/application/AppManifestView.ts | shortName":{"message":"Short name"},"panels/application/AppManifestView.ts | showOnlyTheMinimumSafeAreaFor":{"message":"Show only the minimum safe area for maskable icons"},"panels/application/AppManifestView.ts | sSDoesNotSpecifyItsSizeInThe":{"message":"{PH1} {PH2} does not specify its size in the manifest"},"panels/application/AppManifestView.ts | sSFailedToLoad":{"message":"{PH1} {PH2} failed to load"},"panels/application/AppManifestView.ts | sSHeightDoesNotComplyWithRatioRequirement":{"message":"{PH1} {PH2} height can't be more than 2.3 times as long as the width"},"panels/application/AppManifestView.ts | sSrcIsNotSet":{"message":"{PH1} 'src' is not set"},"panels/application/AppManifestView.ts | sSShouldHaveSquareIcon":{"message":"Most operating systems require square icons. Please include at least one square icon in the array."},"panels/application/AppManifestView.ts | sSShouldSpecifyItsSizeAs":{"message":"{PH1} {PH2} should specify its size as [width]x[height]"},"panels/application/AppManifestView.ts | sSSizeShouldBeAtLeast320":{"message":"{PH1} {PH2} size should be at least 320×320"},"panels/application/AppManifestView.ts | sSSizeShouldBeAtMost3840":{"message":"{PH1} {PH2} size should be at most 3840×3840"},"panels/application/AppManifestView.ts | sSWidthDoesNotComplyWithRatioRequirement":{"message":"{PH1} {PH2} width can't be more than 2.3 times as long as the height"},"panels/application/AppManifestView.ts | startUrl":{"message":"Start URL"},"panels/application/AppManifestView.ts | sUrlSFailedToParse":{"message":"{PH1} URL ''{PH2}'' failed to parse"},"panels/application/AppManifestView.ts | theAppIsAlreadyInstalled":{"message":"The app is already installed"},"panels/application/AppManifestView.ts | themeColor":{"message":"Theme color"},"panels/application/AppManifestView.ts | thePlayStoreAppUrlAndPlayStoreId":{"message":"The Play Store app URL and Play Store ID do not match"},"panels/application/AppManifestView.ts | theSpecifiedApplicationPlatform":{"message":"The specified application platform is not supported on Android"},"panels/application/AppManifestView.ts | tooManyScreenshotsForDesktop":{"message":"No more than 8 screenshots will be displayed on desktop. The rest will be ignored."},"panels/application/AppManifestView.ts | tooManyScreenshotsForMobile":{"message":"No more than 5 screenshots will be displayed on mobile. The rest will be ignored."},"panels/application/AppManifestView.ts | url":{"message":"URL"},"panels/application/AppManifestView.ts | wcoFound":{"message":"Chrome has successfully found the {PH1} value for the {PH2} field in the {PH3}."},"panels/application/AppManifestView.ts | wcoNeedHelpReadMore":{"message":"Need help? Read {PH1}."},"panels/application/AppManifestView.ts | wcoNotFound":{"message":"Define {PH1} in the manifest to use the Window Controls Overlay API and customize your app's title bar."},"panels/application/AppManifestView.ts | windowControlsOverlay":{"message":"Window Controls Overlay"},"panels/application/BackForwardCacheTreeElement.ts | backForwardCache":{"message":"Back/forward cache"},"panels/application/BackgroundServiceView.ts | backgroundFetch":{"message":"Background fetch"},"panels/application/BackgroundServiceView.ts | backgroundServices":{"message":"Background services"},"panels/application/BackgroundServiceView.ts | backgroundSync":{"message":"Background sync"},"panels/application/BackgroundServiceView.ts | clear":{"message":"Clear"},"panels/application/BackgroundServiceView.ts | clickTheRecordButtonSOrHitSTo":{"message":"Click the record button {PH1} or hit {PH2} to start recording."},"panels/application/BackgroundServiceView.ts | devtoolsWillRecordAllSActivity":{"message":"DevTools will record all {PH1} activity for up to 3 days, even when closed."},"panels/application/BackgroundServiceView.ts | empty":{"message":"empty"},"panels/application/BackgroundServiceView.ts | event":{"message":"Event"},"panels/application/BackgroundServiceView.ts | instanceId":{"message":"Instance ID"},"panels/application/BackgroundServiceView.ts | learnMore":{"message":"Learn more"},"panels/application/BackgroundServiceView.ts | noMetadataForThisEvent":{"message":"No metadata for this event"},"panels/application/BackgroundServiceView.ts | notifications":{"message":"Notifications"},"panels/application/BackgroundServiceView.ts | origin":{"message":"Origin"},"panels/application/BackgroundServiceView.ts | paymentHandler":{"message":"Payment handler"},"panels/application/BackgroundServiceView.ts | periodicBackgroundSync":{"message":"Periodic background sync"},"panels/application/BackgroundServiceView.ts | pushMessaging":{"message":"Push messaging"},"panels/application/BackgroundServiceView.ts | recordingSActivity":{"message":"Recording {PH1} activity..."},"panels/application/BackgroundServiceView.ts | saveEvents":{"message":"Save events"},"panels/application/BackgroundServiceView.ts | selectAnEntryToViewMetadata":{"message":"Select an entry to view metadata"},"panels/application/BackgroundServiceView.ts | showEventsForOtherStorageKeys":{"message":"Show events from other storage partitions"},"panels/application/BackgroundServiceView.ts | showEventsFromOtherDomains":{"message":"Show events from other domains"},"panels/application/BackgroundServiceView.ts | startRecordingEvents":{"message":"Start recording events"},"panels/application/BackgroundServiceView.ts | stopRecordingEvents":{"message":"Stop recording events"},"panels/application/BackgroundServiceView.ts | storageKey":{"message":"Storage Key"},"panels/application/BackgroundServiceView.ts | swScope":{"message":"Service Worker Scope"},"panels/application/BackgroundServiceView.ts | timestamp":{"message":"Timestamp"},"panels/application/BounceTrackingMitigationsTreeElement.ts | bounceTrackingMitigations":{"message":"Bounce tracking mitigations"},"panels/application/components/BackForwardCacheStrings.ts | appBanner":{"message":"Pages that requested an AppBanner are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | backForwardCacheDisabled":{"message":"Back/forward cache is disabled by flags. Visit chrome://flags/#back-forward-cache to enable it locally on this device."},"panels/application/components/BackForwardCacheStrings.ts | backForwardCacheDisabledByCommandLine":{"message":"Back/forward cache is disabled by the command line."},"panels/application/components/BackForwardCacheStrings.ts | backForwardCacheDisabledByLowMemory":{"message":"Back/forward cache is disabled due to insufficient memory."},"panels/application/components/BackForwardCacheStrings.ts | backForwardCacheDisabledForDelegate":{"message":"Back/forward cache is not supported by delegate."},"panels/application/components/BackForwardCacheStrings.ts | backForwardCacheDisabledForPrerender":{"message":"Back/forward cache is disabled for prerenderer."},"panels/application/components/BackForwardCacheStrings.ts | broadcastChannel":{"message":"The page cannot be cached because it has a BroadcastChannel instance with registered listeners."},"panels/application/components/BackForwardCacheStrings.ts | cacheControlNoStore":{"message":"Pages with cache-control:no-store header cannot enter back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | cacheFlushed":{"message":"The cache was intentionally cleared."},"panels/application/components/BackForwardCacheStrings.ts | cacheLimit":{"message":"The page was evicted from the cache to allow another page to be cached."},"panels/application/components/BackForwardCacheStrings.ts | containsPlugins":{"message":"Pages containing plugins are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | contentFileChooser":{"message":"Pages that use FileChooser API are not eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | contentFileSystemAccess":{"message":"Pages that use File System Access API are not eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | contentMediaDevicesDispatcherHost":{"message":"Pages that use Media Device Dispatcher are not eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | contentMediaPlay":{"message":"A media player was playing upon navigating away."},"panels/application/components/BackForwardCacheStrings.ts | contentMediaSession":{"message":"Pages that use MediaSession API and set a playback state are not eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | contentMediaSessionService":{"message":"Pages that use MediaSession API and set action handlers are not eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | contentScreenReader":{"message":"Back/forward cache is disabled due to screen reader."},"panels/application/components/BackForwardCacheStrings.ts | contentSecurityHandler":{"message":"Pages that use SecurityHandler are not eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | contentSerial":{"message":"Pages that use Serial API are not eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | contentWebAuthenticationAPI":{"message":"Pages that use WebAuthetication API are not eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | contentWebBluetooth":{"message":"Pages that use WebBluetooth API are not eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | contentWebUSB":{"message":"Pages that use WebUSB API are not eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | cookieDisabled":{"message":"Back/forward cache is disabled because cookies are disabled on a page that uses Cache-Control: no-store."},"panels/application/components/BackForwardCacheStrings.ts | CookieFlushed":{"message":"Undefined"},"panels/application/components/BackForwardCacheStrings.ts | dedicatedWorkerOrWorklet":{"message":"Pages that use a dedicated worker or worklet are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | documentLoaded":{"message":"The document did not finish loading before navigating away."},"panels/application/components/BackForwardCacheStrings.ts | embedderAppBannerManager":{"message":"App Banner was present upon navigating away."},"panels/application/components/BackForwardCacheStrings.ts | embedderChromePasswordManagerClientBindCredentialManager":{"message":"Chrome Password Manager was present upon navigating away."},"panels/application/components/BackForwardCacheStrings.ts | embedderDomDistillerSelfDeletingRequestDelegate":{"message":"DOM distillation was in progress upon navigating away."},"panels/application/components/BackForwardCacheStrings.ts | embedderDomDistillerViewerSource":{"message":"DOM Distiller Viewer was present upon navigating away."},"panels/application/components/BackForwardCacheStrings.ts | embedderExtensionMessaging":{"message":"Back/forward cache is disabled due to extensions using messaging API."},"panels/application/components/BackForwardCacheStrings.ts | embedderExtensionMessagingForOpenPort":{"message":"Extensions with long-lived connection should close the connection before entering back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | embedderExtensions":{"message":"Back/forward cache is disabled due to extensions."},"panels/application/components/BackForwardCacheStrings.ts | embedderExtensionSentMessageToCachedFrame":{"message":"Extensions with long-lived connection attempted to send messages to frames in back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | embedderModalDialog":{"message":"Modal dialog such as form resubmission or http password dialog was shown for the page upon navigating away."},"panels/application/components/BackForwardCacheStrings.ts | embedderOfflinePage":{"message":"The offline page was shown upon navigating away."},"panels/application/components/BackForwardCacheStrings.ts | embedderOomInterventionTabHelper":{"message":"Out-Of-Memory Intervention bar was present upon navigating away."},"panels/application/components/BackForwardCacheStrings.ts | embedderPermissionRequestManager":{"message":"There were permission requests upon navigating away."},"panels/application/components/BackForwardCacheStrings.ts | embedderPopupBlockerTabHelper":{"message":"Popup blocker was present upon navigating away."},"panels/application/components/BackForwardCacheStrings.ts | embedderSafeBrowsingThreatDetails":{"message":"Safe Browsing details were shown upon navigating away."},"panels/application/components/BackForwardCacheStrings.ts | embedderSafeBrowsingTriggeredPopupBlocker":{"message":"Safe Browsing considered this page to be abusive and blocked popup."},"panels/application/components/BackForwardCacheStrings.ts | enteredBackForwardCacheBeforeServiceWorkerHostAdded":{"message":"A service worker was activated while the page was in back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | errorDocument":{"message":"Back/forward cache is disabled due to a document error."},"panels/application/components/BackForwardCacheStrings.ts | fencedFramesEmbedder":{"message":"Pages using FencedFrames cannot be stored in bfcache."},"panels/application/components/BackForwardCacheStrings.ts | foregroundCacheLimit":{"message":"The page was evicted from the cache to allow another page to be cached."},"panels/application/components/BackForwardCacheStrings.ts | grantedMediaStreamAccess":{"message":"Pages that have granted media stream access are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | haveInnerContents":{"message":"Pages that use portals are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | HTTPAuthRequired":{"message":"Undefined"},"panels/application/components/BackForwardCacheStrings.ts | HTTPMethodNotGET":{"message":"Only pages loaded via a GET request are eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | HTTPStatusNotOK":{"message":"Only pages with a status code of 2XX can be cached."},"panels/application/components/BackForwardCacheStrings.ts | idleManager":{"message":"Pages that use IdleManager are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | indexedDBConnection":{"message":"Pages that have an open IndexedDB connection are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | indexedDBEvent":{"message":"Back/forward cache is disabled due to an IndexedDB event."},"panels/application/components/BackForwardCacheStrings.ts | ineligibleAPI":{"message":"Ineligible APIs were used."},"panels/application/components/BackForwardCacheStrings.ts | injectedJavascript":{"message":"Pages that JavaScript is injected into by extensions are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | injectedStyleSheet":{"message":"Pages that a StyleSheet is injected into by extensions are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | internalError":{"message":"Internal error."},"panels/application/components/BackForwardCacheStrings.ts | JavaScriptExecution":{"message":"Chrome detected an attempt to execute JavaScript while in the cache."},"panels/application/components/BackForwardCacheStrings.ts | jsNetworkRequestReceivedCacheControlNoStoreResource":{"message":"Back/forward cache is disabled because some JavaScript network request received resource with Cache-Control: no-store header."},"panels/application/components/BackForwardCacheStrings.ts | keepaliveRequest":{"message":"Back/forward cache is disabled due to a keepalive request."},"panels/application/components/BackForwardCacheStrings.ts | keyboardLock":{"message":"Pages that use Keyboard lock are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | LiveMediaStreamTrack":{"message":"Undefined"},"panels/application/components/BackForwardCacheStrings.ts | loading":{"message":"The page did not finish loading before navigating away."},"panels/application/components/BackForwardCacheStrings.ts | mainResourceHasCacheControlNoCache":{"message":"Pages whose main resource has cache-control:no-cache cannot enter back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | mainResourceHasCacheControlNoStore":{"message":"Pages whose main resource has cache-control:no-store cannot enter back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | navigationCancelledWhileRestoring":{"message":"Navigation was cancelled before the page could be restored from back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | networkExceedsBufferLimit":{"message":"The page was evicted from the cache because an active network connection received too much data. Chrome limits the amount of data that a page may receive while cached."},"panels/application/components/BackForwardCacheStrings.ts | networkRequestDatapipeDrainedAsBytesConsumer":{"message":"Pages that have inflight fetch() or XHR are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | networkRequestRedirected":{"message":"The page was evicted from back/forward cache because an active network request involved a redirect."},"panels/application/components/BackForwardCacheStrings.ts | networkRequestTimeout":{"message":"The page was evicted from the cache because a network connection was open too long. Chrome limits the amount of time that a page may receive data while cached."},"panels/application/components/BackForwardCacheStrings.ts | noResponseHead":{"message":"Pages that do not have a valid response head cannot enter back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | notMainFrame":{"message":"Navigation happened in a frame other than the main frame."},"panels/application/components/BackForwardCacheStrings.ts | outstandingIndexedDBTransaction":{"message":"Page with ongoing indexed DB transactions are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | outstandingNetworkRequestDirectSocket":{"message":"Pages with an in-flight network request are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | outstandingNetworkRequestFetch":{"message":"Pages with an in-flight fetch network request are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | outstandingNetworkRequestOthers":{"message":"Pages with an in-flight network request are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | outstandingNetworkRequestXHR":{"message":"Pages with an in-flight XHR network request are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | ParserAborted":{"message":"Undefined"},"panels/application/components/BackForwardCacheStrings.ts | paymentManager":{"message":"Pages that use PaymentManager are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | pictureInPicture":{"message":"Pages that use Picture-in-Picture are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | portal":{"message":"Pages that use portals are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | printing":{"message":"Pages that show Printing UI are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | relatedActiveContentsExist":{"message":"The page was opened using 'window.open()' and another tab has a reference to it, or the page opened a window."},"panels/application/components/BackForwardCacheStrings.ts | rendererProcessCrashed":{"message":"The renderer process for the page in back/forward cache crashed."},"panels/application/components/BackForwardCacheStrings.ts | rendererProcessKilled":{"message":"The renderer process for the page in back/forward cache was killed."},"panels/application/components/BackForwardCacheStrings.ts | requestedAudioCapturePermission":{"message":"Pages that have requested audio capture permissions are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | requestedBackForwardCacheBlockedSensors":{"message":"Pages that have requested sensor permissions are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | requestedBackgroundWorkPermission":{"message":"Pages that have requested background sync or fetch permissions are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | requestedMIDIPermission":{"message":"Pages that have requested MIDI permissions are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | requestedNotificationsPermission":{"message":"Pages that have requested notifications permissions are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | requestedStorageAccessGrant":{"message":"Pages that have requested storage access are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | requestedVideoCapturePermission":{"message":"Pages that have requested video capture permissions are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | schemeNotHTTPOrHTTPS":{"message":"Only pages whose URL scheme is HTTP / HTTPS can be cached."},"panels/application/components/BackForwardCacheStrings.ts | serviceWorkerClaim":{"message":"The page was claimed by a service worker while it is in back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | serviceWorkerPostMessage":{"message":"A service worker attempted to send the page in back/forward cache a MessageEvent."},"panels/application/components/BackForwardCacheStrings.ts | serviceWorkerUnregistration":{"message":"ServiceWorker was unregistered while a page was in back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | serviceWorkerVersionActivation":{"message":"The page was evicted from back/forward cache due to a service worker activation."},"panels/application/components/BackForwardCacheStrings.ts | sessionRestored":{"message":"Chrome restarted and cleared the back/forward cache entries."},"panels/application/components/BackForwardCacheStrings.ts | sharedWorker":{"message":"Pages that use SharedWorker are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | SmartCard":{"message":"Undefined"},"panels/application/components/BackForwardCacheStrings.ts | speechRecognizer":{"message":"Pages that use SpeechRecognizer are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | speechSynthesis":{"message":"Pages that use SpeechSynthesis are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | subframeIsNavigating":{"message":"An iframe on the page started a navigation that did not complete."},"panels/application/components/BackForwardCacheStrings.ts | subresourceHasCacheControlNoCache":{"message":"Pages whose subresource has cache-control:no-cache cannot enter back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | subresourceHasCacheControlNoStore":{"message":"Pages whose subresource has cache-control:no-store cannot enter back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | timeout":{"message":"The page exceeded the maximum time in back/forward cache and was expired."},"panels/application/components/BackForwardCacheStrings.ts | timeoutPuttingInCache":{"message":"The page timed out entering back/forward cache (likely due to long-running pagehide handlers)."},"panels/application/components/BackForwardCacheStrings.ts | UnloadHandler":{"message":"Undefined"},"panels/application/components/BackForwardCacheStrings.ts | unloadHandlerExistsInMainFrame":{"message":"The page has an unload handler in the main frame."},"panels/application/components/BackForwardCacheStrings.ts | unloadHandlerExistsInSubFrame":{"message":"The page has an unload handler in a sub frame."},"panels/application/components/BackForwardCacheStrings.ts | userAgentOverrideDiffers":{"message":"Browser has changed the user agent override header."},"panels/application/components/BackForwardCacheStrings.ts | wasGrantedMediaAccess":{"message":"Pages that have granted access to record video or audio are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | webDatabase":{"message":"Pages that use WebDatabase are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | webHID":{"message":"Pages that use WebHID are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | webLocks":{"message":"Pages that use WebLocks are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | webNfc":{"message":"Pages that use WebNfc are not currently eligible for back/forwad cache."},"panels/application/components/BackForwardCacheStrings.ts | webOTPService":{"message":"Pages that use WebOTPService are not currently eligible for bfcache."},"panels/application/components/BackForwardCacheStrings.ts | webRTC":{"message":"Pages with WebRTC cannot enter back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | webRTCSticky":{"message":"Back/forward cache is disabled because WebRTC has been used."},"panels/application/components/BackForwardCacheStrings.ts | webShare":{"message":"Pages that use WebShare are not currently eligible for back/forwad cache."},"panels/application/components/BackForwardCacheStrings.ts | webSocket":{"message":"Pages with WebSocket cannot enter back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | webSocketSticky":{"message":"Back/forward cache is disabled because WebSocket has been used."},"panels/application/components/BackForwardCacheStrings.ts | webTransport":{"message":"Pages with WebTransport cannot enter back/forward cache."},"panels/application/components/BackForwardCacheStrings.ts | webTransportSticky":{"message":"Back/forward cache is disabled because WebTransport has been used."},"panels/application/components/BackForwardCacheStrings.ts | webXR":{"message":"Pages that use WebXR are not currently eligible for back/forward cache."},"panels/application/components/BackForwardCacheView.ts | backForwardCacheTitle":{"message":"Back/forward cache"},"panels/application/components/BackForwardCacheView.ts | blankURLTitle":{"message":"Blank URL [{PH1}]"},"panels/application/components/BackForwardCacheView.ts | blockingExtensionId":{"message":"Extension id: "},"panels/application/components/BackForwardCacheView.ts | circumstantial":{"message":"Not Actionable"},"panels/application/components/BackForwardCacheView.ts | circumstantialExplanation":{"message":"These reasons are not actionable i.e. caching was prevented by something outside of the direct control of the page."},"panels/application/components/BackForwardCacheView.ts | filesPerIssue":{"message":"{n, plural, =1 {# file} other {# files}}"},"panels/application/components/BackForwardCacheView.ts | framesPerIssue":{"message":"{n, plural, =1 {# frame} other {# frames}}"},"panels/application/components/BackForwardCacheView.ts | framesTitle":{"message":"Frames"},"panels/application/components/BackForwardCacheView.ts | issuesInMultipleFrames":{"message":"{n, plural, =1 {# issue found in {m} frames.} other {# issues found in {m} frames.}}"},"panels/application/components/BackForwardCacheView.ts | issuesInSingleFrame":{"message":"{n, plural, =1 {# issue found in 1 frame.} other {# issues found in 1 frame.}}"},"panels/application/components/BackForwardCacheView.ts | learnMore":{"message":"Learn more: back/forward cache eligibility"},"panels/application/components/BackForwardCacheView.ts | mainFrame":{"message":"Main Frame"},"panels/application/components/BackForwardCacheView.ts | neverUseUnload":{"message":"Learn more: Never use unload handler"},"panels/application/components/BackForwardCacheView.ts | normalNavigation":{"message":"Not served from back/forward cache: to trigger back/forward cache, use Chrome's back/forward buttons, or use the test button below to automatically navigate away and back."},"panels/application/components/BackForwardCacheView.ts | pageSupportNeeded":{"message":"Actionable"},"panels/application/components/BackForwardCacheView.ts | pageSupportNeededExplanation":{"message":"These reasons are actionable i.e. they can be cleaned up to make the page eligible for back/forward cache."},"panels/application/components/BackForwardCacheView.ts | restoredFromBFCache":{"message":"Successfully served from back/forward cache."},"panels/application/components/BackForwardCacheView.ts | runningTest":{"message":"Running test"},"panels/application/components/BackForwardCacheView.ts | runTest":{"message":"Test back/forward cache"},"panels/application/components/BackForwardCacheView.ts | supportPending":{"message":"Pending Support"},"panels/application/components/BackForwardCacheView.ts | supportPendingExplanation":{"message":"Chrome support for these reasons is pending i.e. they will not prevent the page from being eligible for back/forward cache in a future version of Chrome."},"panels/application/components/BackForwardCacheView.ts | unavailable":{"message":"unavailable"},"panels/application/components/BackForwardCacheView.ts | unknown":{"message":"Unknown Status"},"panels/application/components/BackForwardCacheView.ts | url":{"message":"URL:"},"panels/application/components/BounceTrackingMitigationsView.ts | bounceTrackingMitigationsTitle":{"message":"Bounce tracking mitigations"},"panels/application/components/BounceTrackingMitigationsView.ts | checkingPotentialTrackers":{"message":"Checking for potential bounce tracking sites."},"panels/application/components/BounceTrackingMitigationsView.ts | featureDisabled":{"message":"Bounce tracking mitigations are disabled. To enable them, set the flag at {PH1} to \"Enabled With Deletion\"."},"panels/application/components/BounceTrackingMitigationsView.ts | featureFlag":{"message":"Bounce Tracking Mitigations Feature Flag"},"panels/application/components/BounceTrackingMitigationsView.ts | forceRun":{"message":"Force run"},"panels/application/components/BounceTrackingMitigationsView.ts | learnMore":{"message":"Learn more: Bounce Tracking Mitigations"},"panels/application/components/BounceTrackingMitigationsView.ts | noPotentialBounceTrackersIdentified":{"message":"State was not cleared for any potential bounce tracking sites. Either none were identified or third-party cookies are not blocked."},"panels/application/components/BounceTrackingMitigationsView.ts | runningMitigations":{"message":"Running"},"panels/application/components/BounceTrackingMitigationsView.ts | stateDeletedFor":{"message":"State was deleted for the following sites:"},"panels/application/components/EndpointsGrid.ts | noEndpointsToDisplay":{"message":"No endpoints to display"},"panels/application/components/FrameDetailsView.ts | additionalInformation":{"message":"Additional Information"},"panels/application/components/FrameDetailsView.ts | adStatus":{"message":"Ad Status"},"panels/application/components/FrameDetailsView.ts | aFrameAncestorIsAnInsecure":{"message":"A frame ancestor is an insecure context"},"panels/application/components/FrameDetailsView.ts | apiAvailability":{"message":"API availability"},"panels/application/components/FrameDetailsView.ts | availabilityOfCertainApisDepends":{"message":"Availability of certain APIs depends on the document being cross-origin isolated."},"panels/application/components/FrameDetailsView.ts | available":{"message":"available"},"panels/application/components/FrameDetailsView.ts | availableNotTransferable":{"message":"available, not transferable"},"panels/application/components/FrameDetailsView.ts | availableTransferable":{"message":"available, transferable"},"panels/application/components/FrameDetailsView.ts | child":{"message":"child"},"panels/application/components/FrameDetailsView.ts | childDescription":{"message":"This frame has been identified as a child frame of an ad"},"panels/application/components/FrameDetailsView.ts | clickToRevealInElementsPanel":{"message":"Click to reveal in Elements panel"},"panels/application/components/FrameDetailsView.ts | clickToRevealInNetworkPanel":{"message":"Click to reveal in Network panel"},"panels/application/components/FrameDetailsView.ts | clickToRevealInNetworkPanelMight":{"message":"Click to reveal in Network panel (might require page reload)"},"panels/application/components/FrameDetailsView.ts | clickToRevealInSourcesPanel":{"message":"Click to reveal in Sources panel"},"panels/application/components/FrameDetailsView.ts | contentSecurityPolicy":{"message":"Content Security Policy (CSP)"},"panels/application/components/FrameDetailsView.ts | createdByAdScriptExplanation":{"message":"There was an ad script in the (async) stack when this frame was created. Examining the creation stack trace of this frame might provide more insight."},"panels/application/components/FrameDetailsView.ts | creationStackTrace":{"message":"Frame Creation Stack Trace"},"panels/application/components/FrameDetailsView.ts | creationStackTraceExplanation":{"message":"This frame was created programmatically. The stack trace shows where this happened."},"panels/application/components/FrameDetailsView.ts | creatorAdScript":{"message":"Creator Ad Script"},"panels/application/components/FrameDetailsView.ts | crossoriginIsolated":{"message":"Cross-Origin Isolated"},"panels/application/components/FrameDetailsView.ts | document":{"message":"Document"},"panels/application/components/FrameDetailsView.ts | frameId":{"message":"Frame ID"},"panels/application/components/FrameDetailsView.ts | learnMore":{"message":"Learn more"},"panels/application/components/FrameDetailsView.ts | localhostIsAlwaysASecureContext":{"message":"Localhost is always a secure context"},"panels/application/components/FrameDetailsView.ts | matchedBlockingRuleExplanation":{"message":"This frame is considered an ad frame because its current (or previous) main document is an ad resource."},"panels/application/components/FrameDetailsView.ts | measureMemory":{"message":"Measure Memory"},"panels/application/components/FrameDetailsView.ts | no":{"message":"No"},"panels/application/components/FrameDetailsView.ts | none":{"message":"None"},"panels/application/components/FrameDetailsView.ts | origin":{"message":"Origin"},"panels/application/components/FrameDetailsView.ts | originTrialsExplanation":{"message":"Origin trials give you access to a new or experimental feature."},"panels/application/components/FrameDetailsView.ts | ownerElement":{"message":"Owner Element"},"panels/application/components/FrameDetailsView.ts | parentIsAdExplanation":{"message":"This frame is considered an ad frame because its parent frame is an ad frame."},"panels/application/components/FrameDetailsView.ts | reportingTo":{"message":"reporting to"},"panels/application/components/FrameDetailsView.ts | requiresCrossoriginIsolated":{"message":"requires cross-origin isolated context"},"panels/application/components/FrameDetailsView.ts | root":{"message":"root"},"panels/application/components/FrameDetailsView.ts | rootDescription":{"message":"This frame has been identified as the root frame of an ad"},"panels/application/components/FrameDetailsView.ts | secureContext":{"message":"Secure Context"},"panels/application/components/FrameDetailsView.ts | securityIsolation":{"message":"Security & Isolation"},"panels/application/components/FrameDetailsView.ts | sharedarraybufferConstructorIs":{"message":"SharedArrayBuffer constructor is available and SABs can be transferred via postMessage"},"panels/application/components/FrameDetailsView.ts | sharedarraybufferConstructorIsAvailable":{"message":"SharedArrayBuffer constructor is available but SABs cannot be transferred via postMessage"},"panels/application/components/FrameDetailsView.ts | theFramesSchemeIsInsecure":{"message":"The frame's scheme is insecure"},"panels/application/components/FrameDetailsView.ts | thePerformanceAPI":{"message":"The performance.measureUserAgentSpecificMemory() API is available"},"panels/application/components/FrameDetailsView.ts | thePerformancemeasureuseragentspecificmemory":{"message":"The performance.measureUserAgentSpecificMemory() API is not available"},"panels/application/components/FrameDetailsView.ts | thisAdditionalDebugging":{"message":"This additional (debugging) information is shown because the 'Protocol Monitor' experiment is enabled."},"panels/application/components/FrameDetailsView.ts | transferRequiresCrossoriginIsolatedPermission":{"message":"SharedArrayBuffer transfer requires enabling the permission policy:"},"panels/application/components/FrameDetailsView.ts | unavailable":{"message":"unavailable"},"panels/application/components/FrameDetailsView.ts | unreachableUrl":{"message":"Unreachable URL"},"panels/application/components/FrameDetailsView.ts | url":{"message":"URL"},"panels/application/components/FrameDetailsView.ts | willRequireCrossoriginIsolated":{"message":"⚠️ will require cross-origin isolated context in the future"},"panels/application/components/FrameDetailsView.ts | yes":{"message":"Yes"},"panels/application/components/InterestGroupAccessGrid.ts | allInterestGroupStorageEvents":{"message":"All interest group storage events."},"panels/application/components/InterestGroupAccessGrid.ts | eventTime":{"message":"Event Time"},"panels/application/components/InterestGroupAccessGrid.ts | eventType":{"message":"Access Type"},"panels/application/components/InterestGroupAccessGrid.ts | groupName":{"message":"Name"},"panels/application/components/InterestGroupAccessGrid.ts | groupOwner":{"message":"Owner"},"panels/application/components/InterestGroupAccessGrid.ts | noEvents":{"message":"No interest group events recorded."},"panels/application/components/OriginTrialTreeView.ts | expiryTime":{"message":"Expiry Time"},"panels/application/components/OriginTrialTreeView.ts | isThirdParty":{"message":"Third Party"},"panels/application/components/OriginTrialTreeView.ts | matchSubDomains":{"message":"Subdomain Matching"},"panels/application/components/OriginTrialTreeView.ts | noTrialTokens":{"message":"No trial tokens"},"panels/application/components/OriginTrialTreeView.ts | origin":{"message":"Origin"},"panels/application/components/OriginTrialTreeView.ts | rawTokenText":{"message":"Raw Token"},"panels/application/components/OriginTrialTreeView.ts | status":{"message":"Token Status"},"panels/application/components/OriginTrialTreeView.ts | token":{"message":"Token"},"panels/application/components/OriginTrialTreeView.ts | tokens":{"message":"{PH1} tokens"},"panels/application/components/OriginTrialTreeView.ts | trialName":{"message":"Trial Name"},"panels/application/components/OriginTrialTreeView.ts | usageRestriction":{"message":"Usage Restriction"},"panels/application/components/PermissionsPolicySection.ts | allowedFeatures":{"message":"Allowed Features"},"panels/application/components/PermissionsPolicySection.ts | clickToShowHeader":{"message":"Click to reveal the request whose \"Permissions-Policy\" HTTP header disables this feature."},"panels/application/components/PermissionsPolicySection.ts | clickToShowIframe":{"message":"Click to reveal the top-most iframe which does not allow this feature in the elements panel."},"panels/application/components/PermissionsPolicySection.ts | disabledByFencedFrame":{"message":"disabled inside a fencedframe"},"panels/application/components/PermissionsPolicySection.ts | disabledByHeader":{"message":"disabled by \"Permissions-Policy\" header"},"panels/application/components/PermissionsPolicySection.ts | disabledByIframe":{"message":"missing in iframe \"allow\" attribute"},"panels/application/components/PermissionsPolicySection.ts | disabledFeatures":{"message":"Disabled Features"},"panels/application/components/PermissionsPolicySection.ts | hideDetails":{"message":"Hide details"},"panels/application/components/PermissionsPolicySection.ts | showDetails":{"message":"Show details"},"panels/application/components/ProtocolHandlersView.ts | dropdownLabel":{"message":"Select protocol handler"},"panels/application/components/ProtocolHandlersView.ts | manifest":{"message":"manifest"},"panels/application/components/ProtocolHandlersView.ts | needHelpReadOur":{"message":"Need help? Read {PH1}."},"panels/application/components/ProtocolHandlersView.ts | protocolDetected":{"message":"Found valid protocol handler registration in the {PH1}. With the app installed, test the registered protocols."},"panels/application/components/ProtocolHandlersView.ts | protocolHandlerRegistrations":{"message":"URL protocol handler registration for PWAs"},"panels/application/components/ProtocolHandlersView.ts | protocolNotDetected":{"message":"Define protocol handlers in the {PH1} to register your app as a handler for custom protocols when your app is installed."},"panels/application/components/ProtocolHandlersView.ts | testProtocol":{"message":"Test protocol"},"panels/application/components/ProtocolHandlersView.ts | textboxLabel":{"message":"Query parameter or endpoint for protocol handler"},"panels/application/components/ProtocolHandlersView.ts | textboxPlaceholder":{"message":"Enter URL"},"panels/application/components/ReportsGrid.ts | destination":{"message":"Destination"},"panels/application/components/ReportsGrid.ts | generatedAt":{"message":"Generated at"},"panels/application/components/ReportsGrid.ts | noReportsToDisplay":{"message":"No reports to display"},"panels/application/components/ReportsGrid.ts | status":{"message":"Status"},"panels/application/components/SharedStorageAccessGrid.ts | allSharedStorageEvents":{"message":"All shared storage events for this page."},"panels/application/components/SharedStorageAccessGrid.ts | eventParams":{"message":"Optional Event Params"},"panels/application/components/SharedStorageAccessGrid.ts | eventTime":{"message":"Event Time"},"panels/application/components/SharedStorageAccessGrid.ts | eventType":{"message":"Access Type"},"panels/application/components/SharedStorageAccessGrid.ts | mainFrameId":{"message":"Main Frame ID"},"panels/application/components/SharedStorageAccessGrid.ts | noEvents":{"message":"No shared storage events recorded."},"panels/application/components/SharedStorageAccessGrid.ts | ownerOrigin":{"message":"Owner Origin"},"panels/application/components/SharedStorageAccessGrid.ts | sharedStorage":{"message":"Shared storage"},"panels/application/components/SharedStorageMetadataView.ts | budgetExplanation":{"message":"Remaining data leakage allowed within a 24-hour period for this origin in bits of entropy"},"panels/application/components/SharedStorageMetadataView.ts | creation":{"message":"Creation Time"},"panels/application/components/SharedStorageMetadataView.ts | entropyBudget":{"message":"Entropy Budget for Fenced Frames"},"panels/application/components/SharedStorageMetadataView.ts | notYetCreated":{"message":"Not yet created"},"panels/application/components/SharedStorageMetadataView.ts | numBytesUsed":{"message":"Number of Bytes Used"},"panels/application/components/SharedStorageMetadataView.ts | numEntries":{"message":"Number of Entries"},"panels/application/components/SharedStorageMetadataView.ts | resetBudget":{"message":"Reset Budget"},"panels/application/components/SharedStorageMetadataView.ts | sharedStorage":{"message":"Shared storage"},"panels/application/components/StackTrace.ts | cannotRenderStackTrace":{"message":"Cannot render stack trace"},"panels/application/components/StackTrace.ts | creationStackTrace":{"message":"Frame Creation Stack Trace"},"panels/application/components/StackTrace.ts | showLess":{"message":"Show less"},"panels/application/components/StackTrace.ts | showSMoreFrames":{"message":"{n, plural, =1 {Show # more frame} other {Show # more frames}}"},"panels/application/components/StorageMetadataView.ts | bucketName":{"message":"Bucket name"},"panels/application/components/StorageMetadataView.ts | confirmBucketDeletion":{"message":"Delete the \"{PH1}\" bucket?"},"panels/application/components/StorageMetadataView.ts | defaultBucket":{"message":"Default bucket"},"panels/application/components/StorageMetadataView.ts | deleteBucket":{"message":"Delete bucket"},"panels/application/components/StorageMetadataView.ts | durability":{"message":"Durability"},"panels/application/components/StorageMetadataView.ts | expiration":{"message":"Expiration"},"panels/application/components/StorageMetadataView.ts | isOpaque":{"message":"Is opaque"},"panels/application/components/StorageMetadataView.ts | isThirdParty":{"message":"Is third-party"},"panels/application/components/StorageMetadataView.ts | loading":{"message":"Loading…"},"panels/application/components/StorageMetadataView.ts | no":{"message":"No"},"panels/application/components/StorageMetadataView.ts | none":{"message":"None"},"panels/application/components/StorageMetadataView.ts | opaque":{"message":"(opaque)"},"panels/application/components/StorageMetadataView.ts | origin":{"message":"Origin"},"panels/application/components/StorageMetadataView.ts | persistent":{"message":"Is persistent"},"panels/application/components/StorageMetadataView.ts | quota":{"message":"Quota"},"panels/application/components/StorageMetadataView.ts | topLevelSite":{"message":"Top-level site"},"panels/application/components/StorageMetadataView.ts | yes":{"message":"Yes"},"panels/application/components/StorageMetadataView.ts | yesBecauseAncestorChainHasCrossSite":{"message":"Yes, because the ancestry chain contains a third-party origin"},"panels/application/components/StorageMetadataView.ts | yesBecauseKeyIsOpaque":{"message":"Yes, because the storage key is opaque"},"panels/application/components/StorageMetadataView.ts | yesBecauseOriginNotInTopLevelSite":{"message":"Yes, because the origin is outside of the top-level site"},"panels/application/components/StorageMetadataView.ts | yesBecauseTopLevelIsOpaque":{"message":"Yes, because the top-level site is opaque"},"panels/application/components/TrustTokensView.ts | allStoredTrustTokensAvailableIn":{"message":"All stored private state tokens available in this browser instance."},"panels/application/components/TrustTokensView.ts | deleteTrustTokens":{"message":"Delete all stored private state tokens issued by {PH1}."},"panels/application/components/TrustTokensView.ts | issuer":{"message":"Issuer"},"panels/application/components/TrustTokensView.ts | noTrustTokensStored":{"message":"No private state tokens are currently stored."},"panels/application/components/TrustTokensView.ts | storedTokenCount":{"message":"Stored token count"},"panels/application/components/TrustTokensView.ts | trustTokens":{"message":"Private state tokens"},"panels/application/CookieItemsView.ts | clearAllCookies":{"message":"Clear all cookies"},"panels/application/CookieItemsView.ts | clearFilteredCookies":{"message":"Clear filtered cookies"},"panels/application/CookieItemsView.ts | cookies":{"message":"Cookies"},"panels/application/CookieItemsView.ts | numberOfCookiesShownInTableS":{"message":"Number of cookies shown in table: {PH1}"},"panels/application/CookieItemsView.ts | onlyShowCookiesWhichHaveAn":{"message":"Only show cookies that have an associated issue"},"panels/application/CookieItemsView.ts | onlyShowCookiesWithAnIssue":{"message":"Only show cookies with an issue"},"panels/application/CookieItemsView.ts | selectACookieToPreviewItsValue":{"message":"Select a cookie to preview its value"},"panels/application/CookieItemsView.ts | showUrlDecoded":{"message":"Show URL-decoded"},"panels/application/DOMStorageItemsView.ts | domStorage":{"message":"DOM Storage"},"panels/application/DOMStorageItemsView.ts | domStorageItemDeleted":{"message":"The storage item was deleted."},"panels/application/DOMStorageItemsView.ts | domStorageItems":{"message":"DOM Storage Items"},"panels/application/DOMStorageItemsView.ts | domStorageItemsCleared":{"message":"DOM Storage Items cleared"},"panels/application/DOMStorageItemsView.ts | domStorageNumberEntries":{"message":"Number of entries shown in table: {PH1}"},"panels/application/DOMStorageItemsView.ts | key":{"message":"Key"},"panels/application/DOMStorageItemsView.ts | selectAValueToPreview":{"message":"Select a value to preview"},"panels/application/DOMStorageItemsView.ts | value":{"message":"Value"},"panels/application/IndexedDBViews.ts | clearObjectStore":{"message":"Clear object store"},"panels/application/IndexedDBViews.ts | collapse":{"message":"Collapse"},"panels/application/IndexedDBViews.ts | dataMayBeStale":{"message":"Data may be stale"},"panels/application/IndexedDBViews.ts | deleteDatabase":{"message":"Delete database"},"panels/application/IndexedDBViews.ts | deleteSelected":{"message":"Delete selected"},"panels/application/IndexedDBViews.ts | expandRecursively":{"message":"Expand Recursively"},"panels/application/IndexedDBViews.ts | idb":{"message":"IDB"},"panels/application/IndexedDBViews.ts | indexedDb":{"message":"Indexed DB"},"panels/application/IndexedDBViews.ts | keyGeneratorValueS":{"message":"Key generator value: {PH1}"},"panels/application/IndexedDBViews.ts | keyPath":{"message":"Key path: "},"panels/application/IndexedDBViews.ts | keyString":{"message":"Key"},"panels/application/IndexedDBViews.ts | objectStores":{"message":"Object stores"},"panels/application/IndexedDBViews.ts | pleaseConfirmDeleteOfSDatabase":{"message":"Please confirm delete of \"{PH1}\" database."},"panels/application/IndexedDBViews.ts | primaryKey":{"message":"Primary key"},"panels/application/IndexedDBViews.ts | refresh":{"message":"Refresh"},"panels/application/IndexedDBViews.ts | refreshDatabase":{"message":"Refresh database"},"panels/application/IndexedDBViews.ts | showNextPage":{"message":"Show next page"},"panels/application/IndexedDBViews.ts | showPreviousPage":{"message":"Show previous page"},"panels/application/IndexedDBViews.ts | someEntriesMayHaveBeenModified":{"message":"Some entries may have been modified"},"panels/application/IndexedDBViews.ts | startFromKey":{"message":"Start from key"},"panels/application/IndexedDBViews.ts | totalEntriesS":{"message":"Total entries: {PH1}"},"panels/application/IndexedDBViews.ts | valueString":{"message":"Value"},"panels/application/IndexedDBViews.ts | version":{"message":"Version"},"panels/application/InterestGroupStorageView.ts | clickToDisplayBody":{"message":"Click on any interest group event to display the group's current state"},"panels/application/InterestGroupStorageView.ts | noDataAvailable":{"message":"No details available for the selected interest group. The browser may have left the group."},"panels/application/InterestGroupTreeElement.ts | interestGroups":{"message":"Interest groups"},"panels/application/OpenedWindowDetailsView.ts | accessToOpener":{"message":"Access to opener"},"panels/application/OpenedWindowDetailsView.ts | clickToRevealInElementsPanel":{"message":"Click to reveal in Elements panel"},"panels/application/OpenedWindowDetailsView.ts | closed":{"message":"closed"},"panels/application/OpenedWindowDetailsView.ts | crossoriginEmbedderPolicy":{"message":"Cross-Origin Embedder Policy"},"panels/application/OpenedWindowDetailsView.ts | document":{"message":"Document"},"panels/application/OpenedWindowDetailsView.ts | no":{"message":"No"},"panels/application/OpenedWindowDetailsView.ts | openerFrame":{"message":"Opener Frame"},"panels/application/OpenedWindowDetailsView.ts | reportingTo":{"message":"reporting to"},"panels/application/OpenedWindowDetailsView.ts | security":{"message":"Security"},"panels/application/OpenedWindowDetailsView.ts | securityIsolation":{"message":"Security & Isolation"},"panels/application/OpenedWindowDetailsView.ts | showsWhetherTheOpenedWindowIs":{"message":"Shows whether the opened window is able to access its opener and vice versa"},"panels/application/OpenedWindowDetailsView.ts | type":{"message":"Type"},"panels/application/OpenedWindowDetailsView.ts | unknown":{"message":"Unknown"},"panels/application/OpenedWindowDetailsView.ts | url":{"message":"URL"},"panels/application/OpenedWindowDetailsView.ts | webWorker":{"message":"Web Worker"},"panels/application/OpenedWindowDetailsView.ts | windowWithoutTitle":{"message":"Window without title"},"panels/application/OpenedWindowDetailsView.ts | worker":{"message":"worker"},"panels/application/OpenedWindowDetailsView.ts | yes":{"message":"Yes"},"panels/application/preloading/components/MismatchedPreloadingGrid.ts | action":{"message":"Action"},"panels/application/preloading/components/MismatchedPreloadingGrid.ts | status":{"message":"Status"},"panels/application/preloading/components/MismatchedPreloadingGrid.ts | statusFailure":{"message":"Failure"},"panels/application/preloading/components/MismatchedPreloadingGrid.ts | statusNotTriggered":{"message":"Not triggered"},"panels/application/preloading/components/MismatchedPreloadingGrid.ts | statusPending":{"message":"Pending"},"panels/application/preloading/components/MismatchedPreloadingGrid.ts | statusReady":{"message":"Ready"},"panels/application/preloading/components/MismatchedPreloadingGrid.ts | statusRunning":{"message":"Running"},"panels/application/preloading/components/MismatchedPreloadingGrid.ts | statusSuccess":{"message":"Success"},"panels/application/preloading/components/MismatchedPreloadingGrid.ts | url":{"message":"URL"},"panels/application/preloading/components/PreloadingDetailsReportView.ts | buttonClickToInspect":{"message":"Click to inspect prerendered page"},"panels/application/preloading/components/PreloadingDetailsReportView.ts | buttonClickToRevealRuleSet":{"message":"Click to reveal rule set"},"panels/application/preloading/components/PreloadingDetailsReportView.ts | buttonInspect":{"message":"Inspect"},"panels/application/preloading/components/PreloadingDetailsReportView.ts | detailedStatusFailure":{"message":"Speculative load failed."},"panels/application/preloading/components/PreloadingDetailsReportView.ts | detailedStatusNotTriggered":{"message":"Speculative load attempt is not yet triggered."},"panels/application/preloading/components/PreloadingDetailsReportView.ts | detailedStatusPending":{"message":"Speculative load attempt is eligible but pending."},"panels/application/preloading/components/PreloadingDetailsReportView.ts | detailedStatusReady":{"message":"Speculative load finished and the result is ready for the next navigation."},"panels/application/preloading/components/PreloadingDetailsReportView.ts | detailedStatusRunning":{"message":"Speculative load is running."},"panels/application/preloading/components/PreloadingDetailsReportView.ts | detailedStatusSuccess":{"message":"Speculative load finished and used for a navigation."},"panels/application/preloading/components/PreloadingDetailsReportView.ts | detailsAction":{"message":"Action"},"panels/application/preloading/components/PreloadingDetailsReportView.ts | detailsDetailedInformation":{"message":"Detailed information"},"panels/application/preloading/components/PreloadingDetailsReportView.ts | detailsFailureReason":{"message":"Failure reason"},"panels/application/preloading/components/PreloadingDetailsReportView.ts | detailsRuleSet":{"message":"Rule set"},"panels/application/preloading/components/PreloadingDetailsReportView.ts | detailsStatus":{"message":"Status"},"panels/application/preloading/components/PreloadingDetailsReportView.ts | selectAnElementForMoreDetails":{"message":"Select an element for more details"},"panels/application/preloading/components/PreloadingDisabledInfobar.ts | descriptionDisabledByBatterySaver":{"message":"Speculative loading is disabled because of the operating system's Battery Saver mode."},"panels/application/preloading/components/PreloadingDisabledInfobar.ts | descriptionDisabledByDataSaver":{"message":"Speculative loading is disabled because of the operating system's Data Saver mode."},"panels/application/preloading/components/PreloadingDisabledInfobar.ts | descriptionDisabledByHoldbackPrefetchSpeculationRules":{"message":"Prefetch is forced-enabled because DevTools is open. When DevTools is closed, prefetch will be disabled because this browser session is part of a holdback group used for performance comparisons."},"panels/application/preloading/components/PreloadingDisabledInfobar.ts | descriptionDisabledByHoldbackPrerenderSpeculationRules":{"message":"Prerendering is forced-enabled because DevTools is open. When DevTools is closed, prerendering will be disabled because this browser session is part of a holdback group used for performance comparisons."},"panels/application/preloading/components/PreloadingDisabledInfobar.ts | descriptionDisabledByPreference":{"message":"Speculative loading is disabled because of user settings or an extension. Go to {PH1} to update your preference. Go to {PH2} to disable any extension that blocks speculative loading."},"panels/application/preloading/components/PreloadingDisabledInfobar.ts | extensionsSettings":{"message":"Extensions settings"},"panels/application/preloading/components/PreloadingDisabledInfobar.ts | footerLearnMore":{"message":"Learn more"},"panels/application/preloading/components/PreloadingDisabledInfobar.ts | headerDisabledByBatterySaver":{"message":"Battery Saver"},"panels/application/preloading/components/PreloadingDisabledInfobar.ts | headerDisabledByDataSaver":{"message":"Data Saver"},"panels/application/preloading/components/PreloadingDisabledInfobar.ts | headerDisabledByHoldbackPrefetchSpeculationRules":{"message":"Prefetch was disabled, but is force-enabled now"},"panels/application/preloading/components/PreloadingDisabledInfobar.ts | headerDisabledByHoldbackPrerenderSpeculationRules":{"message":"Prerendering was disabled, but is force-enabled now"},"panels/application/preloading/components/PreloadingDisabledInfobar.ts | headerDisabledByPreference":{"message":"User settings or extensions"},"panels/application/preloading/components/PreloadingDisabledInfobar.ts | infobarPreloadingIsDisabled":{"message":"Speculative loading is disabled"},"panels/application/preloading/components/PreloadingDisabledInfobar.ts | infobarPreloadingIsForceEnabled":{"message":"Speculative loading is force-enabled"},"panels/application/preloading/components/PreloadingDisabledInfobar.ts | preloadingPagesSettings":{"message":"Preload pages settings"},"panels/application/preloading/components/PreloadingDisabledInfobar.ts | titleReasonsPreventingPreloading":{"message":"Reasons preventing speculative loading"},"panels/application/preloading/components/PreloadingGrid.ts | action":{"message":"Action"},"panels/application/preloading/components/PreloadingGrid.ts | ruleSet":{"message":"Rule set"},"panels/application/preloading/components/PreloadingGrid.ts | status":{"message":"Status"},"panels/application/preloading/components/PreloadingMismatchedHeadersGrid.ts | activationNavigationValue":{"message":"Value in activation navigation"},"panels/application/preloading/components/PreloadingMismatchedHeadersGrid.ts | headerName":{"message":"Header name"},"panels/application/preloading/components/PreloadingMismatchedHeadersGrid.ts | initialNavigationValue":{"message":"Value in initial navigation"},"panels/application/preloading/components/PreloadingMismatchedHeadersGrid.ts | missing":{"message":"(missing)"},"panels/application/preloading/components/PreloadingString.ts | PrefetchEvictedAfterCandidateRemoved":{"message":"The prefetch was discarded because no speculation rule in the initating page triggers a prefetch for this URL anymore."},"panels/application/preloading/components/PreloadingString.ts | PrefetchEvictedForNewerPrefetch":{"message":"The prefetch was discarded because the initiating page has too many prefetches ongoing, and this was one of the oldest."},"panels/application/preloading/components/PreloadingString.ts | PrefetchFailedIneligibleRedirect":{"message":"The prefetch was redirected, but the redirect URL is not eligible for prefetch."},"panels/application/preloading/components/PreloadingString.ts | PrefetchFailedInvalidRedirect":{"message":"The prefetch was redirected, but there was a problem with the redirect."},"panels/application/preloading/components/PreloadingString.ts | PrefetchFailedMIMENotSupported":{"message":"The prefetch failed because the response's Content-Type header was not supported."},"panels/application/preloading/components/PreloadingString.ts | PrefetchFailedNetError":{"message":"The prefetch failed because of a network error."},"panels/application/preloading/components/PreloadingString.ts | PrefetchFailedNon2XX":{"message":"The prefetch failed because of a non-2xx HTTP response status code."},"panels/application/preloading/components/PreloadingString.ts | PrefetchFailedPerPageLimitExceeded":{"message":"The prefetch was not performed because the initiating page already has too many prefetches ongoing."},"panels/application/preloading/components/PreloadingString.ts | PrefetchIneligibleRetryAfter":{"message":"A previous prefetch to the origin got a HTTP 503 response with an Retry-After header that has not elapsed yet."},"panels/application/preloading/components/PreloadingString.ts | PrefetchIsPrivacyDecoy":{"message":"The URL was not eligible to be prefetched because there was a registered service worker or cross-site cookies for that origin, but the prefetch was put on the network anyways and not used, to disguise that the user had some kind of previous relationship with the origin."},"panels/application/preloading/components/PreloadingString.ts | PrefetchIsStale":{"message":"Too much time elapsed between the prefetch and usage, so the prefetch was discarded."},"panels/application/preloading/components/PreloadingString.ts | PrefetchNotEligibleBatterySaverEnabled":{"message":"The prefetch was not performed because the Battery Saver setting was enabled."},"panels/application/preloading/components/PreloadingString.ts | PrefetchNotEligibleBrowserContextOffTheRecord":{"message":"The prefetch was not performed because the browser is in Incognito or Guest mode."},"panels/application/preloading/components/PreloadingString.ts | PrefetchNotEligibleDataSaverEnabled":{"message":"The prefetch was not performed because the operating system is in Data Saver mode."},"panels/application/preloading/components/PreloadingString.ts | PrefetchNotEligibleExistingProxy":{"message":"The URL is not eligible to be prefetched, because in the default network context it is configured to use a proxy server."},"panels/application/preloading/components/PreloadingString.ts | PrefetchNotEligibleHostIsNonUnique":{"message":"The URL was not eligible to be prefetched because its host was not unique (e.g., a non publicly routable IP address or a hostname which is not registry-controlled), but the prefetch was required to be proxied."},"panels/application/preloading/components/PreloadingString.ts | PrefetchNotEligibleNonDefaultStoragePartition":{"message":"The URL was not eligible to be prefetched because it uses a non-default storage partition."},"panels/application/preloading/components/PreloadingString.ts | PrefetchNotEligiblePreloadingDisabled":{"message":"The prefetch was not performed because speculative loading was disabled."},"panels/application/preloading/components/PreloadingString.ts | PrefetchNotEligibleSameSiteCrossOriginPrefetchRequiredProxy":{"message":"The URL was not eligible to be prefetched because the default network context cannot be configured to use the prefetch proxy for a same-site cross-origin prefetch request."},"panels/application/preloading/components/PreloadingString.ts | PrefetchNotEligibleSchemeIsNotHttps":{"message":"The URL was not eligible to be prefetched because its scheme was not https:."},"panels/application/preloading/components/PreloadingString.ts | PrefetchNotEligibleUserHasCookies":{"message":"The URL was not eligible to be prefetched because it was cross-site, but the user had cookies for that origin."},"panels/application/preloading/components/PreloadingString.ts | PrefetchNotEligibleUserHasServiceWorker":{"message":"The URL was not eligible to be prefetched because there was a registered service worker for that origin, which is currently not supported."},"panels/application/preloading/components/PreloadingString.ts | PrefetchNotUsedCookiesChanged":{"message":"The prefetch was not used because it was a cross-site prefetch, and cookies were added for that URL while the prefetch was ongoing, so the prefetched response is now out-of-date."},"panels/application/preloading/components/PreloadingString.ts | PrefetchNotUsedProbeFailed":{"message":"The prefetch was blocked by your Internet Service Provider or network administrator."},"panels/application/preloading/components/PreloadingString.ts | PrefetchProxyNotAvailable":{"message":"A network error was encountered when trying to set up a connection to the prefetching proxy."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusActivatedDuringMainFrameNavigation":{"message":"Prerendered page activated during initiating page's main frame navigation."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusActivatedWithAuxiliaryBrowsingContexts":{"message":"The prerender was not used because during activation time, there were other windows with an active opener reference to the initiating page, which is currently not supported."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusActivationFramePolicyNotCompatible":{"message":"The prerender was not used because the sandboxing flags or permissions policy of the initiating page was not compatible with those of the prerendering page."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusActivationNavigationParameterMismatch":{"message":"The prerender was not used because during activation time, different navigation parameters (e.g., HTTP headers) were calculated than during the original prerendering navigation request."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusActivationUrlHasEffectiveUrl":{"message":"The prerender was not used because during activation time, navigation has an effective URL that is different from its normal URL. (For example, the New Tab Page, or hosted apps.)"},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusAudioOutputDeviceRequested":{"message":"The prerendered page requested audio output, which is currently not supported."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusBatterySaverEnabled":{"message":"The prerender was not performed because the user requested that the browser use less battery."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusBlockedByClient":{"message":"Some resource load was blocked."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusClientCertRequested":{"message":"The prerendering navigation required a HTTP client certificate."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusCrossSiteNavigationInInitialNavigation":{"message":"The prerendering navigation failed because it targeted a cross-site URL."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusCrossSiteNavigationInMainFrameNavigation":{"message":"The prerendered page navigated to a cross-site URL."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusCrossSiteRedirectInInitialNavigation":{"message":"The prerendering navigation failed because the prerendered URL redirected to a cross-site URL."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusCrossSiteRedirectInMainFrameNavigation":{"message":"The prerendered page navigated to a URL which redirected to a cross-site URL."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusDataSaverEnabled":{"message":"The prerender was not performed because the user requested that the browser use less data."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusDownload":{"message":"The prerendered page attempted to initiate a download, which is currently not supported."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusHasEffectiveUrl":{"message":"The initiating page cannot perform prerendering, because it has an effective URL that is different from its normal URL. (For example, the New Tab Page, or hosted apps.)"},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusInvalidSchemeNavigation":{"message":"The URL was not eligible to be prerendered because its scheme was not http: or https:."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusInvalidSchemeRedirect":{"message":"The prerendering navigation failed because it redirected to a URL whose scheme was not http: or https:."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusLoginAuthRequested":{"message":"The prerendering navigation required HTTP authentication, which is currently not supported."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusLowEndDevice":{"message":"The prerender was not performed because this device does not have enough total system memory to support prerendering."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusMainFrameNavigation":{"message":"The prerendered page navigated itself to another URL, which is currently not supported."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusMaxNumOfRunningEagerPrerendersExceeded":{"message":"The prerender whose eagerness is \"eager\" was not performed because the initiating page already has too many prerenders ongoing. Remove other speculation rules with \"eager\" to enable further prerendering."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusMaxNumOfRunningEmbedderPrerendersExceeded":{"message":"The browser-triggered prerender was not performed because the initiating page already has too many prerenders ongoing."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusMaxNumOfRunningNonEagerPrerendersExceeded":{"message":"The old non-eager prerender (with a \"moderate\" or \"conservative\" eagerness and triggered by hovering or clicking links) was automatically canceled due to starting a new non-eager prerender. It can be retriggered by interacting with the link again."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusMemoryLimitExceeded":{"message":"The prerender was not performed because the browser exceeded the prerendering memory limit."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusMemoryPressureAfterTriggered":{"message":"The prerendered page was unloaded because the browser came under critical memory pressure."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusMemoryPressureOnTrigger":{"message":"The prerender was not performed because the browser was under critical memory pressure."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusMixedContent":{"message":"The prerendered page contained mixed content."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusMojoBinderPolicy":{"message":"The prerendered page used a forbidden JavaScript API that is currently not supported. (Internal Mojo interface: {PH1})"},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusNavigationBadHttpStatus":{"message":"The prerendering navigation failed because of a non-2xx HTTP response status code."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusNavigationRequestBlockedByCsp":{"message":"The prerendering navigation was blocked by a Content Security Policy."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusNavigationRequestNetworkError":{"message":"The prerendering navigation encountered a network error."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusPreloadingDisabled":{"message":"The prerender was not performed because the user disabled preloading in their browser settings."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusPrerenderingDisabledByDevTools":{"message":"The prerender was not performed because DevTools has been used to disable prerendering."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusPrerenderingUrlHasEffectiveUrl":{"message":"The prerendering navigation failed because it has an effective URL that is different from its normal URL. (For example, the New Tab Page, or hosted apps.)"},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusPrimaryMainFrameRendererProcessCrashed":{"message":"The initiating page crashed."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusPrimaryMainFrameRendererProcessKilled":{"message":"The initiating page was killed."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusRedirectedPrerenderingUrlHasEffectiveUrl":{"message":"The prerendering navigation failed because it redirected to an effective URL that is different from its normal URL. (For example, the New Tab Page, or hosted apps.)"},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusRendererProcessCrashed":{"message":"The prerendered page crashed."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusRendererProcessKilled":{"message":"The prerendered page was killed."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusSameSiteCrossOriginNavigationNotOptInInInitialNavigation":{"message":"The prerendering navigation failed because it was to a cross-origin same-site URL, but the destination response did not include the appropriate Supports-Loading-Mode header."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusSameSiteCrossOriginNavigationNotOptInInMainFrameNavigation":{"message":"The prerendered page navigated to a cross-origin same-site URL, but the destination response did not include the appropriate Supports-Loading-Mode header."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusSameSiteCrossOriginRedirectNotOptInInInitialNavigation":{"message":"The prerendering navigation failed because the prerendered URL redirected to a cross-origin same-site URL, but the destination response did not include the appropriate Supports-Loading-Mode header."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusSameSiteCrossOriginRedirectNotOptInInMainFrameNavigation":{"message":"The prerendered page navigated to a URL which redirected to a cross-origin same-site URL, but the destination response did not include the appropriate Supports-Loading-Mode header."},"panels/application/preloading/components/PreloadingString.ts | prerenderFinalStatusSpeculationRuleRemoved":{"message":"The prerendered page was unloaded because the initiating page removed the corresponding prerender rule from