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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public object ColorPropConverter {
private fun supportWideGamut(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O

private const val JSON_KEY = "resource_paths"
private const val FALLBACK_KEY = "fallback"
private const val PREFIX_RESOURCE = "@"
private const val PREFIX_ATTR = "?"
private const val PACKAGE_DELIMITER = ":"
Expand Down Expand Up @@ -65,15 +66,18 @@ public object ColorPropConverter {
}
}

val fallback = resolveFallback(value)
if (fallback != null) {
return fallback
}

val attemptedPaths = (0 until resourcePaths.size()).map { resourcePaths.getString(it) }
FLog.w(
ReactConstants.TAG,
"ColorValue: Failed to resolve resource paths: ${attemptedPaths.joinToString(", ")}",
)

throw JSApplicationCausedNativeException(
"ColorValue: None of the paths in the `$JSON_KEY` array resolved to a color resource.",
)
return Color.TRANSPARENT
}

throw JSApplicationCausedNativeException("ColorValue: the value must be a number or Object.")
Expand Down Expand Up @@ -121,12 +125,21 @@ public object ColorPropConverter {
}
}

val fallback = resolveFallback(value)
if (supportWideGamut() && fallback != null) {
return Color.valueOf(fallback)
}

val attemptedPaths = (0 until resourcePaths.size()).map { resourcePaths.getString(it) }
FLog.w(
ReactConstants.TAG,
"ColorValue: Failed to resolve resource paths: ${attemptedPaths.joinToString(", ")}",
)

if (supportWideGamut()) {
return Color.valueOf(Color.TRANSPARENT)
}

throw JSApplicationCausedNativeException(
"ColorValue: None of the paths in the `$JSON_KEY` array resolved to a color resource.",
)
Expand Down Expand Up @@ -162,6 +175,14 @@ public object ColorPropConverter {
}
}

private fun resolveFallback(value: ReadableMap): Int? {
if (!value.hasKey(FALLBACK_KEY)) {
return null
}
val fallback = value.getString(FALLBACK_KEY) ?: return null
return RawColorParser.nativeParse(fallback)
}

@JvmStatic
public fun resolveResourcePath(context: Context, resourcePath: String?): Int? {
if (resourcePath.isNullOrEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* 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 com.facebook.proguard.annotations.DoNotStrip

@DoNotStrip
internal object RawColorParser {
init {
ReactNativeJNISoLoader.staticInit()
}

@DoNotStrip @JvmStatic external fun nativeParse(value: String): Int?
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ add_library(
MethodInvoker.cpp
ModuleRegistryBuilder.cpp
OnLoad.cpp
RawColorParser.cpp
SafeReleaseJniRef.cpp
ScrollEventHelper.cpp
TouchesHelper.cpp
Expand All @@ -83,6 +84,7 @@ target_link_libraries(reactnativejni
logger
react_cxxreact
react_cxxstableapi
react_renderer_css
react_renderer_runtimescheduler
reactnativejni_common
rrc_view
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <fbjni/fbjni.h>

#include "InspectorNetworkRequestListener.h"
#include "RawColorParser.h"
#include "ScrollEventHelper.h"
#include "TouchesHelper.h"
#include "TransformHelper.h"
Expand All @@ -36,6 +37,7 @@ extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
FLAGS_minloglevel = 0;
#endif
InspectorNetworkRequestListener::registerNatives();
RawColorParser::registerNatives();
ScrollEventHelper::registerNatives();
TouchesHelper::registerNatives();
TransformHelper::registerNatives();
Expand Down
Original file line number Diff line number Diff line change
@@ -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 "RawColorParser.h"

#include <react/renderer/css/CSSColor.h>
#include <react/renderer/css/CSSValueParser.h>
#include <react/renderer/graphics/Color.h>

namespace facebook::react {

namespace {

jni::local_ref<jni::JInteger> parse(
jni::alias_ref<jclass> /*unused*/,
std::string value) {
auto cssColor = parseCSSProperty<CSSColor>(value);
if (std::holds_alternative<CSSColor>(cssColor)) {
auto c = std::get<CSSColor>(cssColor);
return jni::JInteger::valueOf(
hostPlatformColorFromRGBA(c.r, c.g, c.b, c.a));
}
return nullptr;
}

} // namespace

void RawColorParser::registerNatives() {
javaClassLocal()->registerNatives({
makeNativeMethod("nativeParse", parse),
});
}

} // namespace facebook::react
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* 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 <fbjni/fbjni.h>

namespace facebook::react {

class RawColorParser : public jni::JavaClass<RawColorParser> {
public:
static auto constexpr *kJavaDescriptor = "Lcom/facebook/react/bridge/RawColorParser;";

static void registerNatives();
};

} // namespace facebook::react
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,21 @@
package com.facebook.react.bridge

import android.content.Context
import android.graphics.Color
import android.view.ContextThemeWrapper
import com.facebook.testutils.shadows.ShadowRawColorParser
import com.facebook.testutils.shadows.ShadowSoLoader
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config

/** Tests for [ColorPropConverter] */
@RunWith(RobolectricTestRunner::class)
@Config(shadows = [ShadowSoLoader::class, ShadowRawColorParser::class])
class ColorPropConverterTest {

private lateinit var context: Context
Expand Down Expand Up @@ -45,6 +50,58 @@ class ColorPropConverterTest {
assertThat(result).isEqualTo(themeColor(android.R.attr.textColorPrimary))
}

@Test
fun unresolvedPathUsesFallback() {
val result = ColorPropConverter.getColor(platformColor("@color/missing", "#ff0000"), context)

assertThat(result).isEqualTo(Color.RED)
}

@Test
fun unresolvedPathUsesFallbackOverDefault() {
val result = ColorPropConverter.getColor(platformColor("@color/missing", "#ff0000"), context, 0)

assertThat(result).isEqualTo(Color.RED)
}

@Test
fun unresolvedPathUsesTransparentFallback() {
val result = ColorPropConverter.getColor(platformColor("?attr/missing", "transparent"), context)

assertThat(result).isEqualTo(Color.TRANSPARENT)
}

@Test
fun resolvedPathIgnoresFallback() {
val result =
ColorPropConverter.getColor(platformColor("?android:attr/colorAccent", "#ff0000"), context)

assertThat(result).isEqualTo(themeColor(android.R.attr.colorAccent))
}

@Test
fun unresolvedPathWithoutFallbackIsTransparent() {
val result = ColorPropConverter.getColor(platformColor("@color/missing"), context)

assertThat(result).isEqualTo(Color.TRANSPARENT)
}

@Test
fun unresolvedPathWithInvalidFallbackIsTransparent() {
val result =
ColorPropConverter.getColor(platformColor("@color/missing", "not-a-color"), context)

assertThat(result).isEqualTo(Color.TRANSPARENT)
}

private fun platformColor(path: String, fallback: String? = null): ReadableMap {
val map = JavaOnlyMap.of("resource_paths", JavaOnlyArray.of(path))
if (fallback != null) {
map.putString("fallback", fallback)
}
return map
}

private fun themeColor(attr: Int): Int {
val typedArray = context.obtainStyledAttributes(intArrayOf(attr))
val color = typedArray.getColor(0, 0)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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.testutils.shadows

import android.graphics.Color
import com.facebook.react.bridge.RawColorParser
import org.robolectric.annotation.Implementation
import org.robolectric.annotation.Implements

@Implements(RawColorParser::class)
class ShadowRawColorParser {
companion object {
/** Stands in for the native CSS color parser with a fixed lookup table. */
@JvmStatic
@Implementation
fun nativeParse(value: String): Int? =
when (value) {
"#ff0000" -> Color.RED
"transparent" -> Color.TRANSPARENT
else -> null
}
}
}
5 changes: 5 additions & 0 deletions scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -2128,6 +2128,11 @@ class facebook::react::PullToRefreshViewProps : public facebook::react::HostPlat
public virtual folly::dynamic getDiffProps(const facebook::react::Props* prevProps) const override;
}

class facebook::react::RawColorParser : public facebook::jni::JavaClass<facebook::react::RawColorParser> {
public static constexpr auto * kJavaDescriptor;
public static void registerNatives();
}

class facebook::react::RawProps {
public RawProps();
public RawProps(const facebook::react::RawProps& other) noexcept;
Expand Down
5 changes: 5 additions & 0 deletions scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -3275,6 +3275,11 @@ class facebook::react::PullToRefreshViewProps : public facebook::react::HostPlat
public virtual folly::dynamic getDiffProps(const facebook::react::Props* prevProps) const override;
}

class facebook::react::RawColorParser : public facebook::jni::JavaClass<facebook::react::RawColorParser> {
public static constexpr auto * kJavaDescriptor;
public static void registerNatives();
}

class facebook::react::RawProps {
public RawProps();
public RawProps(const facebook::react::RawProps& other) noexcept;
Expand Down
5 changes: 5 additions & 0 deletions scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -2127,6 +2127,11 @@ class facebook::react::PullToRefreshViewProps : public facebook::react::HostPlat
public virtual folly::dynamic getDiffProps(const facebook::react::Props* prevProps) const override;
}

class facebook::react::RawColorParser : public facebook::jni::JavaClass<facebook::react::RawColorParser> {
public static constexpr auto * kJavaDescriptor;
public static void registerNatives();
}

class facebook::react::RawProps {
public RawProps();
public RawProps(const facebook::react::RawProps& other) noexcept;
Expand Down
5 changes: 5 additions & 0 deletions scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -2127,6 +2127,11 @@ class facebook::react::PullToRefreshViewProps : public facebook::react::HostPlat
public virtual folly::dynamic getDiffProps(const facebook::react::Props* prevProps) const override;
}

class facebook::react::RawColorParser : public facebook::jni::JavaClass<facebook::react::RawColorParser> {
public static constexpr auto * kJavaDescriptor;
public static void registerNatives();
}

class facebook::react::RawProps {
public RawProps();
public RawProps(const facebook::react::RawProps& other) noexcept;
Expand Down
Loading