From 2534aeaddb0490b69dfaba6b8d316616c7e10a9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateo=20Guzm=C3=A1n?= Date: Sat, 19 Jul 2025 06:22:25 -0700 Subject: [PATCH] Migrate `Arguments` to Kotlin (#52457) Summary: Migrate com.facebook.react.bridge.Arguments to Kotlin. ## Changelog: [Android][Changed] - Migrated com.facebook.react.bridge.Arguments to Kotlin. Pull Request resolved: https://github.com/facebook/react-native/pull/52457 Test Plan: ```bash yarn test-android yarn android ``` Reviewed By: javache Differential Revision: D78353290 Pulled By: cortinico fbshipit-source-id: 3d42b44c00a60d34264cb1093991315f5e3c444e --- .../ReactAndroid/api/ReactAndroid.api | 29 +- .../com/facebook/react/bridge/Arguments.java | 423 ------------------ .../com/facebook/react/bridge/Arguments.kt | 356 +++++++++++++++ .../com/facebook/react/bridge/CallbackImpl.kt | 3 +- .../facebook/react/bridge/CxxCallbackImpl.kt | 2 +- .../react/runtime/BridgelessReactContext.kt | 6 +- .../virtual/view/ReactVirtualViewTest.kt | 9 +- .../testutils/shadows/ShadowArguments.kt | 21 +- 8 files changed, 391 insertions(+), 458 deletions(-) delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/Arguments.java create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/Arguments.kt diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index e6b3b5c3e98..12c6f291ff3 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -531,20 +531,21 @@ public abstract interface class com/facebook/react/bridge/ActivityEventListener public fun onUserLeaveHint (Landroid/app/Activity;)V } -public class com/facebook/react/bridge/Arguments { - public fun ()V - public static fun createArray ()Lcom/facebook/react/bridge/WritableArray; - public static fun createMap ()Lcom/facebook/react/bridge/WritableMap; - public static fun fromArray (Ljava/lang/Object;)Lcom/facebook/react/bridge/WritableArray; - public static fun fromBundle (Landroid/os/Bundle;)Lcom/facebook/react/bridge/WritableMap; - public static fun fromJavaArgs ([Ljava/lang/Object;)Lcom/facebook/react/bridge/WritableNativeArray; - public static fun fromList (Ljava/util/List;)Lcom/facebook/react/bridge/WritableArray; - public static fun makeNativeArray (Ljava/lang/Object;)Lcom/facebook/react/bridge/WritableNativeArray; - public static fun makeNativeArray (Ljava/util/List;)Lcom/facebook/react/bridge/WritableNativeArray; - public static fun makeNativeMap (Landroid/os/Bundle;)Lcom/facebook/react/bridge/WritableNativeMap; - public static fun makeNativeMap (Ljava/util/Map;)Lcom/facebook/react/bridge/WritableNativeMap; - public static fun toBundle (Lcom/facebook/react/bridge/ReadableMap;)Landroid/os/Bundle; - public static fun toList (Lcom/facebook/react/bridge/ReadableArray;)Ljava/util/ArrayList; +public final class com/facebook/react/bridge/Arguments { + public static final field INSTANCE Lcom/facebook/react/bridge/Arguments; + public static final fun createArray ()Lcom/facebook/react/bridge/WritableArray; + public static final fun createMap ()Lcom/facebook/react/bridge/WritableMap; + public static final fun fromArray (Ljava/lang/Object;)Lcom/facebook/react/bridge/WritableArray; + public static final fun fromBundle (Landroid/os/Bundle;)Lcom/facebook/react/bridge/WritableMap; + public static final fun fromJavaArgs (Ljava/lang/Object;)Lcom/facebook/react/bridge/WritableNativeArray; + public static final fun fromJavaArgs ([Ljava/lang/Object;)Lcom/facebook/react/bridge/WritableNativeArray; + public static final fun fromList (Ljava/util/List;)Lcom/facebook/react/bridge/WritableArray; + public static final fun makeNativeArray (Ljava/lang/Object;)Lcom/facebook/react/bridge/WritableNativeArray; + public static final fun makeNativeArray (Ljava/util/List;)Lcom/facebook/react/bridge/WritableNativeArray; + public static final fun makeNativeMap (Landroid/os/Bundle;)Lcom/facebook/react/bridge/WritableNativeMap; + public static final fun makeNativeMap (Ljava/util/Map;)Lcom/facebook/react/bridge/WritableNativeMap; + public static final fun toBundle (Lcom/facebook/react/bridge/ReadableMap;)Landroid/os/Bundle; + public static final fun toList (Lcom/facebook/react/bridge/ReadableArray;)Ljava/util/ArrayList; } public final class com/facebook/react/bridge/AssertionException : java/lang/RuntimeException { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/Arguments.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/Arguments.java deleted file mode 100644 index 58ee1ed91c8..00000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/Arguments.java +++ /dev/null @@ -1,423 +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.bridge; - -import android.os.Bundle; -import android.os.Parcelable; -import androidx.annotation.Nullable; -import com.facebook.infer.annotation.Nullsafe; -import com.facebook.proguard.annotations.DoNotStrip; -import java.lang.reflect.Array; -import java.util.AbstractList; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -@Nullsafe(Nullsafe.Mode.LOCAL) -@DoNotStrip -public class Arguments { - private static @Nullable Object makeNativeObject(@Nullable Object object) { - if (object == null) { - return null; - } else if (object instanceof Float - || object instanceof Long - || object instanceof Byte - || object instanceof Short) { - return ((Number) object).doubleValue(); - } else if (object.getClass().isArray()) { - return makeNativeArray(object); - } else if (object instanceof List) { - return makeNativeArray((List) object); - } else if (object instanceof Map) { - return makeNativeMap((Map) object); - } else if (object instanceof Bundle) { - return makeNativeMap((Bundle) object); - } else if (object instanceof JavaOnlyMap) { - return makeNativeMap(((JavaOnlyMap) object).toHashMap()); - } else if (object instanceof JavaOnlyArray) { - return makeNativeArray(((JavaOnlyArray) object).toArrayList()); - } else { - // Boolean, Integer, Double, String, WritableNativeArray, WritableNativeMap - return object; - } - } - - /** - * This method converts a List into a NativeArray. The data types supported are boolean, int, - * float, double, and String. List, Map, and Bundle objects, as well as arrays, containing values - * of the above types and/or null, or any recursive arrangement of these, are also supported. The - * best way to think of this is a way to generate a Java representation of a json list, from Java - * types which have a natural representation in json. - */ - public static WritableNativeArray makeNativeArray(@Nullable List objects) { - WritableNativeArray nativeArray = new WritableNativeArray(); - if (objects == null) { - return nativeArray; - } - for (Object elem : objects) { - elem = makeNativeObject(elem); - if (elem == null) { - nativeArray.pushNull(); - } else if (elem instanceof Boolean) { - nativeArray.pushBoolean((Boolean) elem); - } else if (elem instanceof Integer) { - nativeArray.pushInt((Integer) elem); - } else if (elem instanceof Double) { - nativeArray.pushDouble((Double) elem); - } else if (elem instanceof String) { - nativeArray.pushString((String) elem); - } else if (elem instanceof WritableNativeArray) { - nativeArray.pushArray((WritableNativeArray) elem); - } else if (elem instanceof WritableNativeMap) { - nativeArray.pushMap((WritableNativeMap) elem); - } else { - throw new IllegalArgumentException("Could not convert " + elem.getClass()); - } - } - return nativeArray; - } - - /** - * This overload is like the above, but uses reflection to operate on any primitive or object - * type. - */ - public static WritableNativeArray makeNativeArray(final @Nullable Object objects) { - if (objects == null) { - return new WritableNativeArray(); - } - // No explicit check for objects's type here. If it's not an array, the - // Array methods will throw IllegalArgumentException. - return makeNativeArray( - new AbstractList() { - public int size() { - return Array.getLength(objects); - } - - public @Nullable Object get(int index) { - return Array.get(objects, index); - } - }); - } - - private static void addEntry(WritableNativeMap nativeMap, String key, @Nullable Object value) { - value = makeNativeObject(value); - if (value == null) { - nativeMap.putNull(key); - } else if (value instanceof Boolean) { - nativeMap.putBoolean(key, (Boolean) value); - } else if (value instanceof Integer) { - nativeMap.putInt(key, (Integer) value); - } else if (value instanceof Number) { - nativeMap.putDouble(key, ((Number) value).doubleValue()); - } else if (value instanceof String) { - nativeMap.putString(key, (String) value); - } else if (value instanceof WritableNativeArray) { - nativeMap.putArray(key, (WritableNativeArray) value); - } else if (value instanceof WritableNativeMap) { - nativeMap.putMap(key, (WritableNativeMap) value); - } else { - throw new IllegalArgumentException("Could not convert " + value.getClass()); - } - } - - /** - * This method converts a Map into a NativeMap. Value types are supported as with makeNativeArray. - * The best way to think of this is a way to generate a Java representation of a json object, from - * Java types which have a natural representation in json. - */ - @DoNotStrip - public static WritableNativeMap makeNativeMap(@Nullable Map objects) { - WritableNativeMap nativeMap = new WritableNativeMap(); - if (objects == null) { - return nativeMap; - } - for (Map.Entry entry : objects.entrySet()) { - addEntry(nativeMap, entry.getKey(), entry.getValue()); - } - return nativeMap; - } - - /** Like the above, but takes a Bundle instead of a Map. */ - @DoNotStrip - public static WritableNativeMap makeNativeMap(@Nullable Bundle bundle) { - WritableNativeMap nativeMap = new WritableNativeMap(); - if (bundle == null) { - return nativeMap; - } - for (String key : bundle.keySet()) { - addEntry(nativeMap, key, bundle.get(key)); - } - return nativeMap; - } - - /** This method should be used when you need to stub out creating NativeArrays in unit tests. */ - public static WritableArray createArray() { - return new WritableNativeArray(); - } - - /** This method should be used when you need to stub out creating NativeMaps in unit tests. */ - public static WritableMap createMap() { - return new WritableNativeMap(); - } - - public static WritableNativeArray fromJavaArgs(Object[] args) { - WritableNativeArray arguments = new WritableNativeArray(); - for (int i = 0; i < args.length; i++) { - Object argument = args[i]; - if (argument == null) { - arguments.pushNull(); - continue; - } - - Class argumentClass = argument.getClass(); - if (argumentClass == Boolean.class) { - arguments.pushBoolean(((Boolean) argument).booleanValue()); - } else if (argumentClass == Integer.class) { - arguments.pushDouble(((Integer) argument).doubleValue()); - } else if (argumentClass == Double.class) { - arguments.pushDouble(((Double) argument).doubleValue()); - } else if (argumentClass == Float.class) { - arguments.pushDouble(((Float) argument).doubleValue()); - } else if (argumentClass == String.class) { - arguments.pushString(argument.toString()); - } else if (argumentClass == WritableNativeMap.class) { - arguments.pushMap((WritableNativeMap) argument); - } else if (argumentClass == WritableNativeArray.class) { - arguments.pushArray((WritableNativeArray) argument); - } else { - throw new RuntimeException("Cannot convert argument of type " + argumentClass); - } - } - return arguments; - } - - /** - * Convert an array to a {@link WritableArray}. - * - * @param array the array to convert. Supported types are: {@code String[]}, {@code Bundle[]}, - * {@code int[]}, {@code float[]}, {@code double[]}, {@code boolean[]}. - * @return the converted {@link WritableArray} - * @throws IllegalArgumentException if the passed object is none of the above types - */ - public static WritableArray fromArray(Object array) { - WritableArray catalystArray = createArray(); - if (array instanceof String[]) { - for (String v : (String[]) array) { - catalystArray.pushString(v); - } - } else if (array instanceof Bundle[]) { - for (Bundle v : (Bundle[]) array) { - catalystArray.pushMap(fromBundle(v)); - } - } else if (array instanceof int[]) { - for (int v : (int[]) array) { - catalystArray.pushInt(v); - } - } else if (array instanceof float[]) { - for (float v : (float[]) array) { - catalystArray.pushDouble(v); - } - } else if (array instanceof double[]) { - for (double v : (double[]) array) { - catalystArray.pushDouble(v); - } - } else if (array instanceof boolean[]) { - for (boolean v : (boolean[]) array) { - catalystArray.pushBoolean(v); - } - } else if (array instanceof Parcelable[]) { - for (Parcelable v : (Parcelable[]) array) { - if (v instanceof Bundle) { - catalystArray.pushMap(fromBundle((Bundle) v)); - } else { - throw new IllegalArgumentException("Unexpected array member type " + v.getClass()); - } - } - } else { - throw new IllegalArgumentException("Unknown array type " + array.getClass()); - } - return catalystArray; - } - - /** - * Convert a {@link List} to a {@link WritableArray}. - * - * @param list the list to convert. Supported value types are: {@code null}, {@code String}, - * {@code Bundle}, {@code List}, {@code Number}, {@code Boolean}, and all array types - * supported in {@link #fromArray(Object)}. - * @return the converted {@link WritableArray} - * @throws IllegalArgumentException if one of the values from the passed list is none of the above - * types - */ - public static WritableArray fromList(List list) { - WritableArray catalystArray = createArray(); - for (Object obj : list) { - if (obj == null) { - catalystArray.pushNull(); - } else if (obj.getClass().isArray()) { - catalystArray.pushArray(fromArray(obj)); - } else if (obj instanceof Bundle) { - catalystArray.pushMap(fromBundle((Bundle) obj)); - } else if (obj instanceof List) { - catalystArray.pushArray(fromList((List) obj)); - } else if (obj instanceof String) { - catalystArray.pushString((String) obj); - } else if (obj instanceof Integer) { - catalystArray.pushInt((Integer) obj); - } else if (obj instanceof Number) { - catalystArray.pushDouble(((Number) obj).doubleValue()); - } else if (obj instanceof Boolean) { - catalystArray.pushBoolean((Boolean) obj); - } else { - throw new IllegalArgumentException("Unknown value type " + obj.getClass()); - } - } - return catalystArray; - } - - /** - * Convert a {@link Bundle} to a {@link WritableMap}. Supported key types in the bundle are: - * - *

- * - *

    - *
  • primitive types: int, float, double, boolean - *
  • arrays supported by {@link #fromArray(Object)} - *
  • lists supported by {@link #fromList(List)} - *
  • {@link Bundle} objects that are recursively converted to maps - *
- * - * @param bundle the {@link Bundle} to convert - * @return the converted {@link WritableMap} - * @throws IllegalArgumentException if there are keys of unsupported types - */ - public static WritableMap fromBundle(Bundle bundle) { - WritableMap map = createMap(); - for (String key : bundle.keySet()) { - Object value = bundle.get(key); - if (value == null) { - map.putNull(key); - } else if (value.getClass().isArray()) { - map.putArray(key, fromArray(value)); - } else if (value instanceof String) { - map.putString(key, (String) value); - } else if (value instanceof Number) { - if (value instanceof Integer) { - map.putInt(key, (Integer) value); - } else { - map.putDouble(key, ((Number) value).doubleValue()); - } - } else if (value instanceof Boolean) { - map.putBoolean(key, (Boolean) value); - } else if (value instanceof Bundle) { - map.putMap(key, fromBundle((Bundle) value)); - } else if (value instanceof List) { - map.putArray(key, fromList((List) value)); - } else { - throw new IllegalArgumentException("Could not convert " + value.getClass()); - } - } - return map; - } - - /** - * Convert a {@link WritableArray} to a {@link ArrayList}. - * - * @param readableArray the {@link WritableArray} to convert. - * @return the converted {@link ArrayList}. - */ - @Nullable - public static ArrayList toList(@Nullable ReadableArray readableArray) { - if (readableArray == null) { - return null; - } - - ArrayList list = new ArrayList(); - - for (int i = 0; i < readableArray.size(); i++) { - switch (readableArray.getType(i)) { - case Null: - list.add(null); - break; - case Boolean: - list.add(readableArray.getBoolean(i)); - break; - case Number: - double number = readableArray.getDouble(i); - if (number == Math.rint(number)) { - // Add as an integer - list.add((int) number); - } else { - // Add as a double - list.add(number); - } - break; - case String: - list.add(readableArray.getString(i)); - break; - case Map: - list.add(toBundle(readableArray.getMap(i))); - break; - case Array: - list.add(toList(readableArray.getArray(i))); - break; - default: - throw new IllegalArgumentException("Could not convert object in array."); - } - } - - return list; - } - - /** - * Convert a {@link WritableMap} to a {@link Bundle}. Note: Each array is converted to an {@link - * ArrayList}. - * - * @param readableMap the {@link WritableMap} to convert. - * @return the converted {@link Bundle}. - */ - @Nullable - public static Bundle toBundle(@Nullable ReadableMap readableMap) { - if (readableMap == null) { - return null; - } - - ReadableMapKeySetIterator iterator = readableMap.keySetIterator(); - - Bundle bundle = new Bundle(); - while (iterator.hasNextKey()) { - String key = iterator.nextKey(); - ReadableType readableType = readableMap.getType(key); - switch (readableType) { - case Null: - bundle.putString(key, null); - break; - case Boolean: - bundle.putBoolean(key, readableMap.getBoolean(key)); - break; - case Number: - // Can be int or double. - bundle.putDouble(key, readableMap.getDouble(key)); - break; - case String: - bundle.putString(key, readableMap.getString(key)); - break; - case Map: - bundle.putBundle(key, toBundle(readableMap.getMap(key))); - break; - case Array: - bundle.putSerializable(key, toList(readableMap.getArray(key))); - break; - default: - throw new IllegalArgumentException("Could not convert object with key: " + key + "."); - } - } - - return bundle; - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/Arguments.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/Arguments.kt new file mode 100644 index 00000000000..9dfdc6eac8e --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/Arguments.kt @@ -0,0 +1,356 @@ +/* + * 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.bridge + +import android.os.Bundle +import android.os.Parcelable +import com.facebook.proguard.annotations.DoNotStrip +import java.util.AbstractList +import kotlin.math.round + +@DoNotStrip +public object Arguments { + @Suppress("UNCHECKED_CAST") + private fun makeNativeObject(value: Any?): Any? = + when { + value == null -> null + value is Float || value is Long || value is Byte || value is Short -> + (value as Number).toDouble() + value.javaClass.isArray -> makeNativeArray(value) + value is List<*> -> makeNativeArray(value) + value is Map<*, *> -> makeNativeMap(value as Map) + value is Bundle -> makeNativeMap(value) + value is JavaOnlyMap -> makeNativeMap(value.toHashMap()) + value is JavaOnlyArray -> makeNativeArray(value.toArrayList()) + else -> value // Boolean, Integer, Double, String, WritableNativeArray, WritableNativeMap + } + + /** + * This method converts a List into a NativeArray. The data types supported are boolean, int, + * float, double, and String. List, Map, and Bundle objects, as well as arrays, containing values + * of the above types and/or null, or any recursive arrangement of these, are also supported. The + * best way to think of this is a way to generate a Java representation of a json list, from Java + * types which have a natural representation in json. + */ + @JvmStatic + public fun makeNativeArray(objects: List<*>?): WritableNativeArray { + val nativeArray = WritableNativeArray() + if (objects == null) { + return nativeArray + } + for (elem in objects) { + when (val value = makeNativeObject(elem)) { + null -> nativeArray.pushNull() + is Boolean -> nativeArray.pushBoolean(value) + is Int -> nativeArray.pushInt(value) + is Double -> nativeArray.pushDouble(value) + is String -> nativeArray.pushString(value) + is WritableNativeArray -> nativeArray.pushArray(value) + is WritableNativeMap -> nativeArray.pushMap(value) + else -> throw IllegalArgumentException("Could not convert ${value.javaClass}") + } + } + return nativeArray + } + + /** + * This overload is like the above, but uses reflection to operate on any primitive or object + * type. + */ + @JvmStatic + public fun makeNativeArray(objects: Any?): WritableNativeArray { + if (objects == null) { + return WritableNativeArray() + } + // No explicit check for objects's type here. If it's not an array, the + // Array methods will throw IllegalArgumentException. + return makeNativeArray( + object : AbstractList() { + override val size: Int + get() = java.lang.reflect.Array.getLength(objects) + + override fun get(index: Int): Any? = java.lang.reflect.Array.get(objects, index) + }) + } + + private fun addEntry(nativeMap: WritableNativeMap, key: String, value: Any?) { + when (val nativeObjectValue = makeNativeObject(value)) { + null -> nativeMap.putNull(key) + is Boolean -> nativeMap.putBoolean(key, nativeObjectValue) + is Int -> nativeMap.putInt(key, nativeObjectValue) + is Number -> nativeMap.putDouble(key, nativeObjectValue.toDouble()) + is String -> nativeMap.putString(key, nativeObjectValue) + is WritableNativeArray -> nativeMap.putArray(key, nativeObjectValue) + is WritableNativeMap -> nativeMap.putMap(key, nativeObjectValue) + else -> throw IllegalArgumentException("Could not convert ${nativeObjectValue.javaClass}") + } + } + + /** + * This method converts a Map into a NativeMap. Value types are supported as with makeNativeArray. + * The best way to think of this is a way to generate a Java representation of a json object, from + * Java types which have a natural representation in json. + */ + @DoNotStrip + @JvmStatic + public fun makeNativeMap(objects: Map?): WritableNativeMap { + val nativeMap = WritableNativeMap() + if (objects == null) { + return nativeMap + } + for ((key, value) in objects) { + addEntry(nativeMap, key, value) + } + return nativeMap + } + + /** Like the above, but takes a Bundle instead of a Map. */ + @DoNotStrip + @JvmStatic + @Suppress("DEPRECATION") + public fun makeNativeMap(bundle: Bundle?): WritableNativeMap { + val nativeMap = WritableNativeMap() + if (bundle == null) { + return nativeMap + } + for (key in bundle.keySet()) { + addEntry(nativeMap, key, bundle[key]) + } + return nativeMap + } + + /** This method should be used when you need to stub out creating NativeArrays in unit tests. */ + @JvmStatic public fun createArray(): WritableArray = WritableNativeArray() + + /** This method should be used when you need to stub out creating NativeMaps in unit tests. */ + @JvmStatic public fun createMap(): WritableMap = WritableNativeMap() + + @Suppress("UNCHECKED_CAST") + @JvmStatic + @Deprecated( + "Use fromJavaArgs(Array) instead. This method is added only to retain compatibility with Java consumers.") + public fun fromJavaArgs(args: Any?): WritableNativeArray = fromJavaArgs(args as Array) + + @JvmStatic + public fun fromJavaArgs(args: Array): WritableNativeArray { + val arguments = WritableNativeArray() + for (i in args.indices) { + val argument = args[i] + when (val argumentClass = argument?.javaClass) { + null -> arguments.pushNull() + Boolean::class.java, + java.lang.Boolean::class.java -> arguments.pushBoolean(argument as Boolean) + Int::class.java, + java.lang.Integer::class.java -> arguments.pushDouble((argument as Number).toDouble()) + Double::class.java, + java.lang.Double::class.java -> arguments.pushDouble((argument as Double)) + Float::class.java -> arguments.pushDouble((argument as Float).toDouble()) + java.lang.Float::class.java -> arguments.pushDouble((argument as Float).toDouble()) + String::class.java -> arguments.pushString(argument.toString()) + WritableNativeMap::class.java -> arguments.pushMap(argument as WritableNativeMap) + WritableNativeArray::class.java -> arguments.pushArray(argument as WritableNativeArray) + else -> throw RuntimeException("Cannot convert argument of type $argumentClass") + } + } + return arguments + } + + /** + * Convert an array to a [WritableArray]. + * + * @param array the array to convert. Supported types are: `String[]`, `Bundle[]`, `int[]`, + * `float[]`, `double[]`, `boolean[]`. + * @return the converted [WritableArray] + * @throws IllegalArgumentException if the passed object is none of the above types + */ + @JvmStatic + @Suppress("UNCHECKED_CAST") + public fun fromArray(array: Any): WritableArray { + val catalystArray = createArray() + when { + array is Array<*> && array.isArrayOf() -> { + for (v in array as Array) { + catalystArray.pushString(v) + } + } + array is Array<*> && array.isArrayOf() -> { + for (v in array as Array) { + catalystArray.pushMap(fromBundle(v)) + } + } + array is IntArray -> { + for (v in array) { + catalystArray.pushInt(v) + } + } + array is FloatArray -> { + for (v in array) { + catalystArray.pushDouble(v.toDouble()) + } + } + array is DoubleArray -> { + for (v in array) { + catalystArray.pushDouble(v) + } + } + array is BooleanArray -> { + for (v in array) { + catalystArray.pushBoolean(v) + } + } + array is Array<*> && array.isArrayOf() -> { + for (v in array as Array) { + if (v is Bundle) { + catalystArray.pushMap(fromBundle(v)) + } else { + throw IllegalArgumentException("Unexpected array member type ${v.javaClass}") + } + } + } + else -> throw IllegalArgumentException("Unknown array type ${array.javaClass}") + } + return catalystArray + } + + /** + * Convert a [List] to a [WritableArray]. + * + * @param list the list to convert. Supported value types are: `null`, `String`, `Bundle`, `List`, + * `Number`, `Boolean`, and all array types supported in [.fromArray]. + * @return the converted [WritableArray] + * @throws IllegalArgumentException if one of the values from the passed list is none of the above + * types + */ + @JvmStatic + public fun fromList(list: List<*>): WritableArray { + val catalystArray = createArray() + for (obj in list) { + when { + obj == null -> catalystArray.pushNull() + obj.javaClass.isArray -> catalystArray.pushArray(fromArray(obj)) + obj is Bundle -> catalystArray.pushMap(fromBundle(obj)) + obj is List<*> -> catalystArray.pushArray(fromList(obj)) + obj is String -> catalystArray.pushString(obj) + obj is Int -> catalystArray.pushInt(obj) + obj is Number -> catalystArray.pushDouble(obj.toDouble()) + obj is Boolean -> catalystArray.pushBoolean(obj) + else -> throw IllegalArgumentException("Unknown value type ${obj.javaClass}") + } + } + return catalystArray + } + + /** + * Convert a [Bundle] to a [WritableMap]. Supported key types in the bundle are: + * * primitive types: int, float, double, boolean + * * arrays supported by [.fromArray] + * * lists supported by [.fromList] + * * [Bundle] objects that are recursively converted to maps + * + * @param bundle the [Bundle] to convert + * @return the converted [WritableMap] + * @throws IllegalArgumentException if there are keys of unsupported types + */ + @JvmStatic + @Suppress("DEPRECATION") + public fun fromBundle(bundle: Bundle): WritableMap { + val map = createMap() + for (key in bundle.keySet()) { + val value = bundle[key] + when { + value == null -> map.putNull(key) + value.javaClass.isArray -> map.putArray(key, fromArray(value)) + value is String -> map.putString(key, value) + value is Number -> { + if (value is Int) { + map.putInt(key, value) + } else { + map.putDouble(key, value.toDouble()) + } + } + value is Boolean -> map.putBoolean(key, value) + value is Bundle -> map.putMap(key, fromBundle(value)) + value is List<*> -> map.putArray(key, fromList(value)) + else -> throw IllegalArgumentException("Could not convert ${value.javaClass}") + } + } + return map + } + + /** + * Convert a [WritableArray] to a [ArrayList]. + * + * @param readableArray the [WritableArray] to convert. + * @return the converted [ArrayList]. + */ + @JvmStatic + @Suppress("REDUNDANT_ELSE_IN_WHEN") + public fun toList(readableArray: ReadableArray?): ArrayList? { + if (readableArray == null) { + return null + } + + val list: ArrayList = ArrayList() + + for (i in 0.. list.add(null) + ReadableType.Boolean -> list.add(readableArray.getBoolean(i)) + ReadableType.Number -> { + val number = readableArray.getDouble(i) + if (number == round(number)) { + // Add as an integer + list.add(number.toInt()) + } else { + // Add as a double + list.add(number) + } + } + ReadableType.String -> list.add(readableArray.getString(i)) + ReadableType.Map -> list.add(toBundle(readableArray.getMap(i))) + ReadableType.Array -> list.add(toList(readableArray.getArray(i))) + else -> throw IllegalArgumentException("Could not convert object in array.") + } + } + + return list + } + + /** + * Convert a [WritableMap] to a [Bundle]. Note: Each array is converted to an [ArrayList]. + * + * @param readableMap the [WritableMap] to convert. + * @return the converted [Bundle]. + */ + @JvmStatic + @Suppress("REDUNDANT_ELSE_IN_WHEN") + public fun toBundle(readableMap: ReadableMap?): Bundle? { + if (readableMap == null) { + return null + } + + val iterator = readableMap.keySetIterator() + val bundle = Bundle() + + while (iterator.hasNextKey()) { + val key = iterator.nextKey() + when (readableMap.getType(key)) { + ReadableType.Null -> bundle.putString(key, null) + ReadableType.Boolean -> bundle.putBoolean(key, readableMap.getBoolean(key)) + ReadableType.Number -> + bundle.putDouble(key, readableMap.getDouble(key)) // Can be int or double. + ReadableType.String -> bundle.putString(key, readableMap.getString(key)) + ReadableType.Map -> bundle.putBundle(key, toBundle(readableMap.getMap(key))) + ReadableType.Array -> bundle.putSerializable(key, toList(readableMap.getArray(key))) + else -> throw IllegalArgumentException("Could not convert object with key: $key.") + } + } + + return bundle + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/CallbackImpl.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/CallbackImpl.kt index e0641596d16..ef1b67592d6 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/CallbackImpl.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/CallbackImpl.kt @@ -22,7 +22,8 @@ internal class CallbackImpl(private val jsInstance: JSInstance, private val call throw RuntimeException( "Illegal callback invocation from native module. This callback type only permits a single invocation from native code.") } - jsInstance.invokeCallback(callbackId, Arguments.fromJavaArgs(args)) + @Suppress("UNCHECKED_CAST") + jsInstance.invokeCallback(callbackId, Arguments.fromJavaArgs(args as Array)) invoked = true } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/CxxCallbackImpl.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/CxxCallbackImpl.kt index dbd8c004bc5..8554580f1cb 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/CxxCallbackImpl.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/CxxCallbackImpl.kt @@ -15,7 +15,7 @@ import com.facebook.proguard.annotations.DoNotStrip public class CxxCallbackImpl @DoNotStrip private constructor() : HybridClassBase(), Callback { override fun invoke(vararg args: Any?) { - nativeInvoke(Arguments.fromJavaArgs(args)) + @Suppress("UNCHECKED_CAST") nativeInvoke(Arguments.fromJavaArgs(args as Array)) } private external fun nativeInvoke(arguments: NativeArray) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/BridgelessReactContext.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/BridgelessReactContext.kt index 70dc70a1563..ad2caa13642 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/BridgelessReactContext.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/BridgelessReactContext.kt @@ -21,7 +21,6 @@ import com.facebook.react.bridge.NativeArray import com.facebook.react.bridge.NativeModule import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.UIManager -import com.facebook.react.bridge.WritableNativeArray import com.facebook.react.common.annotations.FrameworkAPI import com.facebook.react.common.annotations.UnstableReactNativeAPI import com.facebook.react.common.build.ReactBuildConfig @@ -105,9 +104,8 @@ internal class BridgelessReactContext(context: Context, private val reactHost: R private val reactHost: ReactHostImpl, private val jsModuleInterface: Class ) : InvocationHandler { - override fun invoke(proxy: Any, method: Method, args: Array?): Any? { - val jsArgs: NativeArray = - if (args != null) Arguments.fromJavaArgs(args) else WritableNativeArray() + override fun invoke(proxy: Any, method: Method, args: Array): Any? { + val jsArgs: NativeArray = Arguments.fromJavaArgs(args) reactHost.callFunctionOnModule( JavaScriptModuleRegistry.getJSModuleName(jsModuleInterface), method.name, jsArgs) return null diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/virtual/view/ReactVirtualViewTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/virtual/view/ReactVirtualViewTest.kt index 34f39ee251a..75c6821e3ae 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/virtual/view/ReactVirtualViewTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/virtual/view/ReactVirtualViewTest.kt @@ -11,9 +11,6 @@ import android.app.Activity import android.content.Context import android.graphics.Rect import android.util.DisplayMetrics -import com.facebook.react.bridge.Arguments -import com.facebook.react.bridge.JavaOnlyMap -import com.facebook.react.bridge.WritableMap import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests import com.facebook.react.uimanager.DisplayMetricsHolder import com.facebook.react.uimanager.events.Event @@ -21,6 +18,7 @@ import com.facebook.react.uimanager.events.EventDispatcher import com.facebook.react.views.scroll.ReactScrollView import com.facebook.react.views.virtual.VirtualViewMode import com.facebook.react.views.virtual.VirtualViewModeChangeEvent +import com.facebook.testutils.shadows.ShadowArguments import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test @@ -32,8 +30,10 @@ import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config /** Tests [ReactVirtualView] */ +@Config(shadows = [ShadowArguments::class]) @RunWith(RobolectricTestRunner::class) class ReactVirtualViewTest { @@ -45,9 +45,6 @@ class ReactVirtualViewTest { context = Robolectric.buildActivity(Activity::class.java).create().get() - val arguments = mockStatic(Arguments::class.java) - arguments.`when` { Arguments.createMap() }.thenAnswer { JavaOnlyMap() } - val displayMetricsHolder = mockStatic(DisplayMetricsHolder::class.java) displayMetricsHolder .`when` { DisplayMetricsHolder.getWindowDisplayMetrics() } diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/testutils/shadows/ShadowArguments.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/testutils/shadows/ShadowArguments.kt index 7987af9ac76..6f6d1070651 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/testutils/shadows/ShadowArguments.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/testutils/shadows/ShadowArguments.kt @@ -18,15 +18,18 @@ import org.robolectric.annotation.Implements import org.robolectric.shadow.api.Shadow @Implements(Arguments::class) -object ShadowArguments { - @JvmStatic @Implementation fun createArray(): WritableArray = JavaOnlyArray() +class ShadowArguments { - @JvmStatic @Implementation fun createMap(): WritableMap = JavaOnlyMap() + companion object { + @JvmStatic @Implementation fun createArray(): WritableArray = JavaOnlyArray() - @JvmStatic - @Implementation - fun fromJavaArgs(args: Array): WritableNativeArray = - WritableNativeArray().apply { - (Shadow.extract(this) as ShadowNativeArray).backingArray = JavaOnlyArray.of(*args) - } + @JvmStatic @Implementation fun createMap(): WritableMap = JavaOnlyMap() + + @JvmStatic + @Implementation + fun fromJavaArgs(args: Array): WritableNativeArray = + WritableNativeArray().apply { + (Shadow.extract(this) as ShadowNativeArray).backingArray = JavaOnlyArray.of(*args) + } + } }