diff --git a/build.gradle b/build.gradle index d8a464b02b..11069a81f5 100644 --- a/build.gradle +++ b/build.gradle @@ -3,6 +3,15 @@ // See gradle/common.gradle for more or less global build logic applied to the subprojects +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'ru.vyarus:gradle-animalsniffer-plugin:2.0.1' + } +} + plugins { id 'idea' } diff --git a/gradle/common.gradle b/gradle/common.gradle index 747b9f1bf3..4bf1233ddd 100644 --- a/gradle/common.gradle +++ b/gradle/common.gradle @@ -6,6 +6,25 @@ java { targetCompatibility = JavaVersion.VERSION_17 } +// Same Android-suitability check as gestalt's gestalt-library-common.gradle.kts: verifies this +// module's API surface only uses classes/methods that also exist on Android, so NUI stays usable +// from an eventual Android-facing module without discovering incompatible JDK-only API usage late. +apply plugin: 'ru.vyarus.animalsniffer' + +dependencies { + signature 'com.toasttab.android:gummy-bears-api-24:0.15.0:coreLib2@signature' +} + +animalsniffer { + // java.nio.* APIs can be desugared by D8. java.io.File.toPath() also needs to be excluded. + // + // java.awt.Toolkit/datatransfer.* genuinely don't exist on Android at all - UIText.AwtClipboard + // isolates that usage behind a LinkageError-catching caller so it degrades gracefully instead + // of crashing, but the check itself can't be satisfied for a real platform absence like this + // one, so it's excluded here rather than by leaving the whole check non-fatal. + ignore = ['java.nio.file.*', 'java.io.File', 'java.awt.Toolkit', 'java.awt.datatransfer.*'] +} + // We use both Maven Central and our own Artifactory instance, which contains module builds, extra libs, and so on repositories { // For development so you can publish binaries locally and have them grabbed from there diff --git a/nui-reflect/src/main/java/org/terasology/reflection/ReflectionUtil.java b/nui-reflect/src/main/java/org/terasology/reflection/ReflectionUtil.java index c8d189d9dc..46c516a956 100644 --- a/nui-reflect/src/main/java/org/terasology/reflection/ReflectionUtil.java +++ b/nui-reflect/src/main/java/org/terasology/reflection/ReflectionUtil.java @@ -8,6 +8,7 @@ import org.reflections.ReflectionUtils; import java.lang.reflect.Array; +import java.lang.reflect.Constructor; import java.lang.reflect.Executable; import java.lang.reflect.Field; import java.lang.reflect.GenericArrayType; @@ -516,7 +517,27 @@ public static String resolvedMethodToString(Type declaringType, try { StringBuilder stringBuilder = new StringBuilder(); - TypeVariable[] typeParameters = method.getTypeParameters(); + // Executable itself isn't available before Android API 26. Method and Constructor each + // declare getTypeParameters()/getGenericParameterTypes()/isVarArgs() directly (and did + // so long before Executable existed), so resolve them through the concrete type instead + // of calling through the Executable-typed parameter - a call resolved against Executable + // trips AnimalSniffer even though the underlying Method/Constructor method is fine. + TypeVariable[] typeParameters; + Type[] unresolvedParameterTypes; + boolean varArgs; + + if (method instanceof Method) { + Method asMethod = (Method) method; + typeParameters = asMethod.getTypeParameters(); + unresolvedParameterTypes = asMethod.getGenericParameterTypes(); + varArgs = asMethod.isVarArgs(); + } else { + Constructor asConstructor = (Constructor) method; + typeParameters = asConstructor.getTypeParameters(); + unresolvedParameterTypes = asConstructor.getGenericParameterTypes(); + varArgs = asConstructor.isVarArgs(); + } + if (typeParameters.length > 0) { boolean first = true; stringBuilder.append('<'); @@ -534,24 +555,24 @@ public static String resolvedMethodToString(Type declaringType, } if (method instanceof Method) { - Type returnType = resolveType(declaringType, ((Method) method).getGenericReturnType()); + Method asMethod = (Method) method; + Type returnType = resolveType(declaringType, asMethod.getGenericReturnType()); stringBuilder.append(typeToString(returnType, useSimpleName)) .append(' '); - stringBuilder.append(method.getName()); + stringBuilder.append(asMethod.getName()); } else { - final Class declaringClass = method.getDeclaringClass(); + final Class declaringClass = ((Constructor) method).getDeclaringClass(); stringBuilder.append(typeToString(declaringClass, useSimpleName)); } stringBuilder.append('('); - Type[] unresolvedParameterTypes = method.getGenericParameterTypes(); for (int i = 0; i < unresolvedParameterTypes.length; ++i) { final Type parameterType = resolveType(declaringType, unresolvedParameterTypes[i]); String parameterName = typeToString(parameterType, useSimpleName); - if (method.isVarArgs() && i == unresolvedParameterTypes.length - 1) { + if (varArgs && i == unresolvedParameterTypes.length - 1) { parameterName = parameterName.replaceFirst("\\[\\]$", "..."); } @@ -581,7 +602,7 @@ public static String typeToString(Type type, boolean useSimpleName) { return clazz.getSimpleName(); } - return clazz.getTypeName(); + return classTypeName(clazz); } if (type instanceof WildcardType) { @@ -599,6 +620,30 @@ public static String typeToString(Type type, boolean useSimpleName) { return null; } + /** + * Equivalent to {@link Class#getTypeName()}, which isn't available before Android API 26. + * Unlike {@link Class#getName()}, array types are rendered as e.g. {@code java.lang.String[]} + * rather than the JVM-internal {@code [Ljava.lang.String;}. + */ + private static String classTypeName(Class clazz) { + if (!clazz.isArray()) { + return clazz.getName(); + } + + Class componentType = clazz; + int dimensions = 0; + while (componentType.isArray()) { + dimensions++; + componentType = componentType.getComponentType(); + } + + StringBuilder stringBuilder = new StringBuilder(componentType.getName()); + for (int i = 0; i < dimensions; i++) { + stringBuilder.append("[]"); + } + return stringBuilder.toString(); + } + /** * Tries to resolve a java type name to a Class diff --git a/nui/src/main/java/org/terasology/nui/widgets/UIText.java b/nui/src/main/java/org/terasology/nui/widgets/UIText.java index 8570975e75..4d20e7248b 100644 --- a/nui/src/main/java/org/terasology/nui/widgets/UIText.java +++ b/nui/src/main/java/org/terasology/nui/widgets/UIText.java @@ -600,29 +600,59 @@ protected void paste() { /** * Get the current clipboard contents. * - * @return The string currently in the clipboard + * @return The string currently in the clipboard, or an empty string if the system clipboard + * isn't available on this platform (e.g. Android, which has no java.awt). */ protected String getClipboardContents() { - Transferable t = Toolkit.getDefaultToolkit().getSystemClipboard().getContents(null); - try { - if (t != null && t.isDataFlavorSupported(DataFlavor.stringFlavor)) { - return (String) t.getTransferData(DataFlavor.stringFlavor); - } - } catch (UnsupportedFlavorException | IOException e) { - logger.warn("Failed to get data from clipboard", e); + return AwtClipboard.getContents(); + } catch (LinkageError e) { + logger.warn("System clipboard is not available on this platform", e); + return ""; } - - return ""; } /** - * Set the contents of the clipboard to a given value. + * Set the contents of the clipboard to a given value. Does nothing if the system clipboard + * isn't available on this platform (e.g. Android, which has no java.awt). * * @param str The new value of the clipboard contents */ protected void setClipboardContents(String str) { - Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(str), null); + try { + AwtClipboard.setContents(str); + } catch (LinkageError e) { + logger.warn("System clipboard is not available on this platform", e); + } + } + + /** + * Isolates the java.awt.datatransfer clipboard access in its own class, rather than referencing + * it directly in getClipboardContents()/setClipboardContents(), so that loading UIText itself + * doesn't require java.awt to be resolvable - it genuinely doesn't exist on Android, unlike the + * other API-level gaps elsewhere in this codebase that have a same-behavior workaround. + * AwtClipboard is only classloaded the first time one of those two methods actually runs, and + * the LinkageError (a NoClassDefFoundError, on a platform without java.awt) is caught by the + * caller rather than here, since the failure happens on this class's own initialization. + */ + private static final class AwtClipboard { + static String getContents() { + Transferable t = Toolkit.getDefaultToolkit().getSystemClipboard().getContents(null); + + try { + if (t != null && t.isDataFlavorSupported(DataFlavor.stringFlavor)) { + return (String) t.getTransferData(DataFlavor.stringFlavor); + } + } catch (UnsupportedFlavorException | IOException e) { + logger.warn("Failed to get data from clipboard", e); + } + + return ""; + } + + static void setContents(String str) { + Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(str), null); + } } /** diff --git a/nui/src/main/java/org/terasology/nui/widgets/types/builtin/object/ObjectLayoutBuilder.java b/nui/src/main/java/org/terasology/nui/widgets/types/builtin/object/ObjectLayoutBuilder.java index 4d5bab7ee2..df5f91b615 100644 --- a/nui/src/main/java/org/terasology/nui/widgets/types/builtin/object/ObjectLayoutBuilder.java +++ b/nui/src/main/java/org/terasology/nui/widgets/types/builtin/object/ObjectLayoutBuilder.java @@ -24,7 +24,6 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Parameter; import java.util.Arrays; import java.util.List; import java.util.Optional; @@ -145,11 +144,10 @@ private void populateConstructorParameters(Binding binding, Binding> selectedConstructor) { parameterLayout.removeAllWidgets(); - Parameter[] parameters = selectedConstructor.get().getParameters(); - + // java.lang.reflect.Parameter isn't available before Android API 26 - Constructor's older + // getGenericParameterTypes() gets us the same Type[] without going through it. List> parameterTypes = - Arrays.stream(parameters) - .map(Parameter::getParameterizedType) + Arrays.stream(selectedConstructor.get().getGenericParameterTypes()) .map(parameterType -> ReflectionUtil.resolveType(type.getType(), parameterType)) .map(TypeInfo::of) .collect(Collectors.toList()); @@ -186,14 +184,13 @@ private void populateConstructorParameters(Binding binding, for (int i = 0; i < parameterTypes.size(); i++) { TypeInfo parameterType = parameterTypes.get(i); Binding argumentBinding = argumentBindings.get(i); - Parameter parameter = parameters[i]; Optional optionalWidget = library.getBaseTypeWidget((Binding) argumentBinding, parameterType); if (!optionalWidget.isPresent()) { LOGGER.warn("Could not create widget for parameter of type {} of constructor {}", - parameter, selectedConstructor.get()); + parameterType, selectedConstructor.get()); continue; }