Codegen for parcelable/dataclass boilerplate
This is the initial implementation of the `codegen` cli utility
for in-place java boilerplate generation
See DataClass and SampleDataClass for documentation/guide/examples.
See tools/codegen/ for implementation and tests/Codegen/ for tests.
Bug: 64221737
Test: . frameworks/base/tests/Codegen/runTest.sh
Change-Id: I75177cb770f1beabc87dbae9e77ce4b93ca08e7f
diff --git a/core/java/com/android/internal/util/AnnotationValidations.java b/core/java/com/android/internal/util/AnnotationValidations.java
new file mode 100644
index 0000000..c8afdd4
--- /dev/null
+++ b/core/java/com/android/internal/util/AnnotationValidations.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.util;
+
+import static com.android.internal.util.BitUtils.flagsUpTo;
+
+import android.annotation.AppIdInt;
+import android.annotation.ColorInt;
+import android.annotation.FloatRange;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Size;
+import android.annotation.UserIdInt;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.PackageInfoFlags;
+import android.content.pm.PackageManager.PermissionResult;
+import android.os.UserHandle;
+
+import java.lang.annotation.Annotation;
+
+/**
+ * Validations for common annotations, e.g. {@link IntRange}, {@link UserIdInt}, etc.
+ *
+ * For usability from generated {@link DataClass} code, all validations are overloads of
+ * {@link #validate} with the following shape:
+ * {@code
+ * <A extends Annotation> void validate(
+ * Class<A> cls, A ignored, Object value[, (String, Object)... annotationParams])
+ * }
+ * The ignored {@link Annotation} parameter is used to differentiate between overloads that would
+ * otherwise have the same jvm signature. It's usually null at runtime.
+ */
+public class AnnotationValidations {
+ private AnnotationValidations() {}
+
+ public static void validate(Class<UserIdInt> annotation, UserIdInt ignored, int value) {
+ if ((value != UserHandle.USER_NULL && value < -3)
+ || value > Integer.MAX_VALUE / UserHandle.PER_USER_RANGE) {
+ invalid(annotation, value);
+ }
+ }
+
+ public static void validate(Class<AppIdInt> annotation, AppIdInt ignored, int value) {
+ if (value / UserHandle.PER_USER_RANGE != 0 || value < 0) {
+ invalid(annotation, value);
+ }
+ }
+
+ public static void validate(Class<IntRange> annotation, IntRange ignored, int value,
+ String paramName1, int param1, String paramName2, int param2) {
+ validate(annotation, ignored, value, paramName1, param1);
+ validate(annotation, ignored, value, paramName2, param2);
+ }
+
+ public static void validate(Class<IntRange> annotation, IntRange ignored, int value,
+ String paramName, int param) {
+ switch (paramName) {
+ case "from": if (value < param) invalid(annotation, value, paramName, param); break;
+ case "to": if (value > param) invalid(annotation, value, paramName, param); break;
+ }
+ }
+
+ public static void validate(Class<FloatRange> annotation, FloatRange ignored, float value,
+ String paramName1, float param1, String paramName2, float param2) {
+ validate(annotation, ignored, value, paramName1, param1);
+ validate(annotation, ignored, value, paramName2, param2);
+ }
+
+ public static void validate(Class<FloatRange> annotation, FloatRange ignored, float value,
+ String paramName, float param) {
+ switch (paramName) {
+ case "from": if (value < param) invalid(annotation, value, paramName, param); break;
+ case "to": if (value > param) invalid(annotation, value, paramName, param); break;
+ }
+ }
+
+ public static void validate(Class<NonNull> annotation, NonNull ignored, Object value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ }
+
+ public static void validate(Class<Size> annotation, Size ignored, int value,
+ String paramName1, int param1, String paramName2, int param2) {
+ validate(annotation, ignored, value, paramName1, param1);
+ validate(annotation, ignored, value, paramName2, param2);
+ }
+
+ public static void validate(Class<Size> annotation, Size ignored, int value,
+ String paramName, int param) {
+ switch (paramName) {
+ case "value": {
+ if (param != -1 && value != param) invalid(annotation, value, paramName, param);
+ } break;
+ case "min": {
+ if (value < param) invalid(annotation, value, paramName, param);
+ } break;
+ case "max": {
+ if (value > param) invalid(annotation, value, paramName, param);
+ } break;
+ case "multiple": {
+ if (value % param != 0) invalid(annotation, value, paramName, param);
+ } break;
+ }
+ }
+
+ public static void validate(
+ Class<PermissionResult> annotation, PermissionResult ignored, int value) {
+ validateIntEnum(annotation, value, PackageManager.PERMISSION_GRANTED);
+ }
+
+ public static void validate(
+ Class<PackageInfoFlags> annotation, PackageInfoFlags ignored, int value) {
+ validateIntFlags(annotation, value,
+ flagsUpTo(PackageManager.MATCH_HIDDEN_UNTIL_INSTALLED_COMPONENTS));
+ }
+
+ public static void validate(
+ Class<Intent.Flags> annotation, Intent.Flags ignored, int value) {
+ validateIntFlags(annotation, value, flagsUpTo(Intent.FLAG_RECEIVER_OFFLOAD));
+ }
+
+
+ @Deprecated
+ public static void validate(Class<? extends Annotation> annotation,
+ Annotation ignored, Object value, Object... params) {}
+ @Deprecated
+ public static void validate(Class<? extends Annotation> annotation,
+ Annotation ignored, Object value) {}
+ @Deprecated
+ public static void validate(Class<? extends Annotation> annotation,
+ Annotation ignored, int value, Object... params) {}
+ public static void validate(Class<? extends Annotation> annotation,
+ Annotation ignored, int value) {
+ if (("android.annotation".equals(annotation.getPackageName$())
+ && annotation.getSimpleName().endsWith("Res"))
+ || ColorInt.class.equals(annotation)) {
+ if (value < 0) {
+ invalid(annotation, value);
+ }
+ }
+ }
+ public static void validate(Class<? extends Annotation> annotation,
+ Annotation ignored, long value) {
+ if ("android.annotation".equals(annotation.getPackageName$())
+ && annotation.getSimpleName().endsWith("Long")) {
+ if (value < 0L) {
+ invalid(annotation, value);
+ }
+ }
+ }
+
+ private static void validateIntEnum(
+ Class<? extends Annotation> annotation, int value, int lastValid) {
+ if (value > lastValid) {
+ invalid(annotation, value);
+ }
+ }
+ private static void validateIntFlags(
+ Class<? extends Annotation> annotation, int value, int validBits) {
+ if ((validBits & value) != validBits) {
+ invalid(annotation, "0x" + Integer.toHexString(value));
+ }
+ }
+
+ private static void invalid(Class<? extends Annotation> annotation, Object value) {
+ invalid("@" + annotation.getSimpleName(), value);
+ }
+
+ private static void invalid(Class<? extends Annotation> annotation, Object value,
+ String paramName, Object param) {
+ String paramPrefix = "value".equals(paramName) ? "" : paramName + " = ";
+ invalid("@" + annotation.getSimpleName() + "(" + paramPrefix + param + ")", value);
+ }
+
+ private static void invalid(String valueKind, Object value) {
+ throw new IllegalStateException("Invalid " + valueKind + ": " + value);
+ }
+}
diff --git a/core/java/com/android/internal/util/BitUtils.java b/core/java/com/android/internal/util/BitUtils.java
index 6158145..b4bab80 100644
--- a/core/java/com/android/internal/util/BitUtils.java
+++ b/core/java/com/android/internal/util/BitUtils.java
@@ -158,4 +158,18 @@
public static byte[] toBytes(long l) {
return ByteBuffer.allocate(8).putLong(l).array();
}
+
+ /**
+ * 0b01000 -> 0b01111
+ */
+ public static int flagsUpTo(int lastFlag) {
+ return lastFlag <= 0 ? 0 : lastFlag | flagsUpTo(lastFlag >> 1);
+ }
+
+ /**
+ * 0b00010, 0b01000 -> 0b01110
+ */
+ public static int flagsWithin(int firstFlag, int lastFlag) {
+ return (flagsUpTo(lastFlag) & ~flagsUpTo(firstFlag)) | firstFlag;
+ }
}
diff --git a/core/java/com/android/internal/util/DataClass.java b/core/java/com/android/internal/util/DataClass.java
new file mode 100644
index 0000000..146f546
--- /dev/null
+++ b/core/java/com/android/internal/util/DataClass.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.util;
+
+import static java.lang.annotation.ElementType.*;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.annotation.StringDef;
+import android.os.Parcelable;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+
+@Retention(RetentionPolicy.SOURCE)
+@Target(ElementType.TYPE)
+public @interface DataClass {
+
+ /**
+ * Generates {@link Parcelable#writeToParcel}, {@link Parcelable#describeContents} and a
+ * {@link Parcelable.Creator}.
+ *
+ * Can be implicitly requested by adding "implements Parcelable" to class signature
+ *
+ * You can provide custom parceling logic by using a {@link ParcelWith} annotation with a
+ * custom {@link Parcelling} subclass.
+ *
+ * Alternatively, for one-off customizations you can declare methods like:
+ * {@code void parcelFieldName(Parcel dest, int flags)}
+ * {@code static FieldType unparcelFieldName(Parcel in)}
+ */
+ boolean genParcelable() default false;
+
+ /**
+ * Generates a simple "parcelable" .aidl file alongside the original .java file
+ *
+ * If not explicitly requested/suppressed, is on iff {@link #genParcelable} is on
+ */
+ boolean genAidl() default false;
+
+ /**
+ * Generates getters for each field.
+ *
+ * You can request for getter to lazily initialize your field by declaring a method like:
+ * {@code FieldType lazyInitFieldName()}
+ *
+ * You can request for the lazy initialization to be thread safe my marking the field volatile.
+ */
+ boolean genGetters() default true;
+
+ /**
+ * Generates setters for each field.
+ */
+ boolean genSetters() default false;
+
+ /**
+ * Generates a public constructor with each field initialized from a parameter and optionally
+ * some user-defined state validation at the end.
+ *
+ * Uses field {@link Nullable nullability}/default value presence to determine optional
+ * parameters.
+ *
+ * Requesting a {@link #genBuilder} suppresses public constructor generation by default.
+ *
+ * You receive a callback at the end of constructor call by declaring the method:
+ * {@code void onConstructed()}
+ * This is the place to put any custom validation logic.
+ */
+ boolean genConstructor() default true;
+
+ /**
+ * Generates a Builder for your class.
+ *
+ * Uses a package-private constructor under the hood, so same rules hold as for
+ * {@link #genConstructor()}
+ */
+ boolean genBuilder() default false;
+
+ /**
+ * Generates a structural {@link Object#equals} + {@link Object#hashCode}.
+ *
+ * You can customize individual fields' logic by declaring methods like:
+ * {@link boolean fieldNameEquals(ClassName otherInstance)}
+ * {@link boolean fieldNameEquals(FieldType otherValue)}
+ * {@link int fieldNameHashCode()}
+ */
+ boolean genEqualsHashCode() default false;
+
+ /**
+ * Generates a structural {@link Object#toString}.
+ *
+ * You can customize individual fields' logic by declaring methods like:
+ * {@link String fieldNameToString()}
+ */
+ boolean genToString() default false;
+
+ /**
+ * Generates a utility method that takes a {@link PerObjectFieldAction per-field callback}
+ * and calls it once for each field with its name and value.
+ *
+ * If some fields are of primitive types, and additional overload is generated that takes
+ * multiple callbacks, specialized for used primitive types to avoid auto-boxing, e.g.
+ * {@link PerIntFieldAction}.
+ */
+ boolean genForEachField() default false;
+
+ /**
+ * Generates a constructor that copies the given instance of the same class.
+ */
+ boolean genCopyConstructor() default false;
+
+ /**
+ * Generates constant annotations({@link IntDef}/{@link StringDef}) for any constant groups
+ * with common prefix.
+ * The annotation names are based on the common prefix.
+ *
+ * For int constants this additionally generates the corresponding static *ToString method and
+ * uses it in {@link Object#toString}.
+ *
+ * Additionally, any fields you annotate with the generated constants will be automatically
+ * validated in constructor.
+ *
+ * Int constants specified as hex(0x..) are considered to be flags, which is taken into account
+ * for in their *ToString and validation.
+ *
+ * You can optionally override the name of the generated annotation by annotating each constant
+ * with the desired annotation name.
+ *
+ * Unless suppressed, is implied by presence of constants with common prefix.
+ */
+ boolean genConstDefs() default true;
+
+
+ /**
+ * Allows specifying custom parcelling logic based on reusable
+ * {@link Parcelling} implementations
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @Target(FIELD)
+ @interface ParcelWith {
+ Class<? extends Parcelling> value();
+ }
+
+ /**
+ * Allows specifying a singular name for a builder's plural field name e.g. 'name' for 'mNames'
+ * Used for Builder's {@code addName(String name)} methods
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @Target(FIELD)
+ @interface PluralOf {
+ String value();
+ }
+
+ /**
+ * Marks that any annotations following it are applicable to each element of the
+ * collection/array, as opposed to itself.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE})
+ @interface Each {}
+
+ /**
+ * @deprecated to be used by code generator exclusively
+ * @hide
+ */
+ @Deprecated
+ @Retention(RetentionPolicy.SOURCE)
+ @Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, ANNOTATION_TYPE, CONSTRUCTOR, TYPE})
+ @interface Generated {
+ long time();
+ String codegenVersion();
+ String sourceFile();
+ String inputSignatures() default "";
+
+ /**
+ * @deprecated to be used by code generator exclusively
+ * @hide
+ */
+ @Deprecated
+ @Retention(RetentionPolicy.SOURCE)
+ @Target({FIELD, METHOD, ANNOTATION_TYPE, CONSTRUCTOR, TYPE})
+ @interface Member {}
+ }
+
+ /**
+ * Callback used by {@link #genForEachField}.
+ *
+ * @param <THIS> The enclosing data class instance.
+ * Can be used to try and avoid capturing values from outside of the lambda,
+ * minimizing allocations.
+ */
+ interface PerObjectFieldAction<THIS> {
+ void acceptObject(THIS self, String fieldName, Object fieldValue);
+ }
+
+ /**
+ * A specialization of {@link PerObjectFieldAction} called exclusively for int fields to avoid
+ * boxing.
+ */
+ interface PerIntFieldAction<THIS> {
+ void acceptInt(THIS self, String fieldName, int fieldValue);
+ }
+}
diff --git a/core/java/com/android/internal/util/Parcelling.java b/core/java/com/android/internal/util/Parcelling.java
new file mode 100644
index 0000000..63530dc
--- /dev/null
+++ b/core/java/com/android/internal/util/Parcelling.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.util;
+
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.util.ArrayMap;
+
+import java.util.regex.Pattern;
+
+/**
+ * Describes a 2-way parcelling contract of type {@code T} into/out of a {@link Parcel}
+ *
+ * @param <T> the type being [un]parcelled
+ */
+public interface Parcelling<T> {
+
+ /**
+ * Write an item into parcel.
+ */
+ void parcel(T item, Parcel dest, int parcelFlags);
+
+ /**
+ * Read an item from parcel.
+ */
+ T unparcel(Parcel source);
+
+
+ /**
+ * A registry of {@link Parcelling} singletons.
+ */
+ class Cache {
+ private Cache() {}
+
+ private static ArrayMap<Class, Parcelling> sCache = new ArrayMap<>();
+
+ /**
+ * Retrieves an instance of a given {@link Parcelling} class if present.
+ */
+ public static @Nullable <P extends Parcelling<?>> P get(Class<P> clazz) {
+ return (P) sCache.get(clazz);
+ }
+
+ /**
+ * Stores an instance of a given {@link Parcelling}.
+ *
+ * @return the provided parcelling for convenience.
+ */
+ public static <P extends Parcelling<?>> P put(P parcelling) {
+ sCache.put(parcelling.getClass(), parcelling);
+ return parcelling;
+ }
+
+ /**
+ * Produces an instance of a given {@link Parcelling} class, by either retrieving a cached
+ * instance or reflectively creating one.
+ */
+ public static <P extends Parcelling<?>> P getOrCreate(Class<P> clazz) {
+ P cached = get(clazz);
+ if (cached != null) {
+ return cached;
+ } else {
+ try {
+ return put(clazz.newInstance());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Common {@link Parcelling} implementations.
+ */
+ interface BuiltIn {
+
+ class ForPattern implements Parcelling<Pattern> {
+
+ @Override
+ public void parcel(Pattern item, Parcel dest, int parcelFlags) {
+ dest.writeString(item == null ? null : item.pattern());
+ }
+
+ @Override
+ public Pattern unparcel(Parcel source) {
+ String s = source.readString();
+ return s == null ? null : Pattern.compile(s);
+ }
+ }
+ }
+}
diff --git a/tests/Codegen/Android.bp b/tests/Codegen/Android.bp
new file mode 100644
index 0000000..966c560
--- /dev/null
+++ b/tests/Codegen/Android.bp
@@ -0,0 +1,25 @@
+android_test {
+ name: "CodegenTests",
+ srcs: [
+ "**/*.java",
+ ],
+
+ platform_apis: true,
+ test_suites: ["device-tests"],
+ certificate: "platform",
+
+ optimize: {
+ enabled: false,
+ },
+
+ plugins: [
+ "staledataclass-annotation-processor",
+ ],
+ static_libs: [
+ "junit",
+ "hamcrest",
+ "hamcrest-library",
+ "androidx.test.runner",
+ "androidx.test.rules",
+ ],
+}
diff --git a/tests/Codegen/AndroidManifest.xml b/tests/Codegen/AndroidManifest.xml
new file mode 100644
index 0000000..2f18550
--- /dev/null
+++ b/tests/Codegen/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ Copyright (C) 2015 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.codegentest">
+
+ <application/>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.codegentest"
+ android:label="Codegen test" />
+</manifest>
diff --git a/tests/Codegen/AndroidTest.xml b/tests/Codegen/AndroidTest.xml
new file mode 100644
index 0000000..4dbbc55
--- /dev/null
+++ b/tests/Codegen/AndroidTest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration description="Runs Codegen Tests.">
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.codegentest" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <option name="hidden-api-checks" value="false"/>
+ </test>
+</configuration>
diff --git a/tests/Codegen/OWNERS b/tests/Codegen/OWNERS
new file mode 100644
index 0000000..da723b3
--- /dev/null
+++ b/tests/Codegen/OWNERS
@@ -0,0 +1 @@
[email protected]
\ No newline at end of file
diff --git a/tests/Codegen/runTest.sh b/tests/Codegen/runTest.sh
new file mode 100755
index 0000000..fe3adf9
--- /dev/null
+++ b/tests/Codegen/runTest.sh
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+
+if [[ "$0" = *"/Codegen/runTest.sh" ]]; then
+ #running in subshell - print code to eval and exit
+ echo "source $0"
+else
+ function header_and_eval() {
+ printf "\n[ $* ]\n" 1>&2
+ eval "$@"
+ return $?
+ }
+
+ header_and_eval m -j16 codegen && \
+ header_and_eval codegen $ANDROID_BUILD_TOP/frameworks/base/tests/Codegen/src/com/android/codegentest/SampleDataClass.java && \
+ cd $ANDROID_BUILD_TOP &&
+ header_and_eval mmma -j16 frameworks/base/tests/Codegen && \
+ header_and_eval adb install -r -t $ANDROID_PRODUCT_OUT/testcases/CodegenTests/arm64/CodegenTests.apk && \
+ # header_and_eval adb shell am set-debug-app -w com.android.codegentest && \
+ header_and_eval adb shell am instrument -w -e package com.android.codegentest com.android.codegentest/androidx.test.runner.AndroidJUnitRunner
+
+ exitCode=$?
+
+ # header_and_eval adb shell am clear-debug-app
+
+ return $exitCode
+fi
\ No newline at end of file
diff --git a/tests/Codegen/src/com/android/codegentest/DateParcelling.java b/tests/Codegen/src/com/android/codegentest/DateParcelling.java
new file mode 100644
index 0000000..b0b00d0
--- /dev/null
+++ b/tests/Codegen/src/com/android/codegentest/DateParcelling.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.codegentest;
+
+import android.os.Parcel;
+
+import com.android.internal.util.Parcelling;
+
+import java.util.Date;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Sample {@link Parcelling} implementation for {@link Date}.
+ *
+ * See {@link SampleDataClass#mDate} for usage.
+ * See {@link SampleDataClass#writeToParcel} + {@link SampleDataClass#sParcellingForDate}
+ * for resulting generated code.
+ *
+ * Ignore {@link #sInstanceCount} - used for testing.
+ */
+public class DateParcelling implements Parcelling<Date> {
+
+ static AtomicInteger sInstanceCount = new AtomicInteger(0);
+
+ public DateParcelling() {
+ sInstanceCount.getAndIncrement();
+ }
+
+ @Override
+ public void parcel(Date item, Parcel dest, int parcelFlags) {
+ dest.writeLong(item.getTime());
+ }
+
+ @Override
+ public Date unparcel(Parcel source) {
+ return new Date(source.readLong());
+ }
+}
diff --git a/tests/Codegen/src/com/android/codegentest/SampleDataClass.aidl b/tests/Codegen/src/com/android/codegentest/SampleDataClass.aidl
new file mode 100644
index 0000000..f14d47c
--- /dev/null
+++ b/tests/Codegen/src/com/android/codegentest/SampleDataClass.aidl
@@ -0,0 +1,18 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.codegentest;
+
+parcelable SampleDataClass;
diff --git a/tests/Codegen/src/com/android/codegentest/SampleDataClass.java b/tests/Codegen/src/com/android/codegentest/SampleDataClass.java
new file mode 100644
index 0000000..03127ec
--- /dev/null
+++ b/tests/Codegen/src/com/android/codegentest/SampleDataClass.java
@@ -0,0 +1,1542 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.codegentest;
+
+import android.annotation.FloatRange;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.Size;
+import android.annotation.StringDef;
+import android.annotation.StringRes;
+import android.annotation.UserIdInt;
+import android.net.LinkAddress;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.android.internal.util.AnnotationValidations;
+import com.android.internal.util.DataClass;
+import com.android.internal.util.DataClass.Each;
+import com.android.internal.util.Parcelling;
+import com.android.internal.util.Preconditions;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+/**
+ * Sample data class, showing off various code generation features.
+ *
+ * See javadoc on non-generated code for the explanation of the various features.
+ *
+ * See {@link SampleDataClassTest} for various invariants the generated code is expected to hold.
+ */
+@DataClass(
+// genParcelable = true, // implied by `implements Parcelable`
+// genAidl = true, // implied by `implements Parcelable`
+// genGetters = true, // on by default
+// genConstDefs = true, // implied by presence of constants with common prefix
+ genEqualsHashCode = true,
+ genBuilder = true,
+ genToString = true,
+ genForEachField = true,
+ genConstructor = true // on by default but normally suppressed by genBuilder
+)
+public final class SampleDataClass implements Parcelable {
+
+ /**
+ * For any group of {@link int} or {@link String} constants like these, a corresponding
+ * {@link IntDef}/{@link StringDef} will get generated, with name based on common prefix
+ * by default.
+ *
+ * When {@link #SampleDataClass constructing} an instance, fields annotated with these
+ * annotations get automatically validated, with only provided constants being a valid value.
+ *
+ * @see StateName, the generated {@link StringDef}
+ * @see #mStateName annotated with {@link StateName}
+ */
+ public static final String STATE_NAME_UNDEFINED = "?";
+ public static final String STATE_NAME_ON = "on";
+ public static final String STATE_NAME_OFF = "off";
+
+ /**
+ * Additionally, for any generated {@link IntDef} a corresponding static
+ * *ToString method will be also generated, and used in {@link #toString()}.
+ *
+ * @see #stateToString(int)
+ * @see #toString()
+ * @see State
+ */
+ public static final int STATE_UNDEFINED = -1;
+ public static final int STATE_ON = 1;
+ public static final int STATE_OFF = 0;
+
+ /**
+ * {@link IntDef}s with values specified in hex("0x...") are considered to be
+ * {@link IntDef#flag flags}, while ones specified with regular int literals are considered
+ * not to be flags.
+ *
+ * This affects their string representation, e.g. see the difference in
+ * {@link #requestFlagsToString} vs {@link #stateToString}.
+ *
+ * This also affects the validation logic when {@link #SampleDataClass constructing}
+ * an instance, with any flag combination("|") being valid.
+ *
+ * You can customize the name of the generated {@link IntDef}/{@link StringDef} annotation
+ * by annotating each constant with the desired name before running the generation.
+ *
+ * Here the annotation is named {@link RequestFlags} instead of the default {@code Flags}.
+ */
+ public static final @RequestFlags int FLAG_MANUAL_REQUEST = 0x1;
+ public static final @RequestFlags int FLAG_COMPATIBILITY_MODE_REQUEST = 0x2;
+ public static final @RequestFlags int FLAG_AUGMENTED_REQUEST = 0x80000000;
+
+
+ /**
+ * Any property javadoc should go onto the field, and will be copied where appropriate,
+ * including getters, constructor parameters, builder setters, etc.
+ *
+ * <p>
+ * This allows to avoid the burden of maintaining copies of the same documentation
+ * pieces in multiple places for each field.
+ */
+ private int mNum;
+ /**
+ * Various javadoc features should work as expected when copied, e.g {@code code},
+ * {@link #mName links}, <a href="https://google.com">html links</a>, etc.
+ *
+ * @see #mNum2 ..and so should blocks at the bottom, e.g. {@code @see} blocks.
+ */
+ private int mNum2;
+ /**
+ * {@code @hide} javadoc annotation is also propagated, which can be used to adjust the
+ * desired public API surface.
+ *
+ * @see #getNum4() is hidden
+ * @see Builder#setNum4(int) also hidden
+ * @hide
+ */
+ private int mNum4;
+
+ /**
+ * {@link Nullable} fields are considered optional and will not throw an exception if omitted
+ * (or set to null) when creating an instance either via a {@link Builder} or constructor.
+ */
+ private @Nullable String mName;
+ /**
+ * Fields with default value expressions ("mFoo = ...") are also optional, and are automatically
+ * initialized to the provided default expression, unless explicitly set.
+ */
+ private String mName2 = "Bob";
+ /**
+ * Fields without {@link Nullable} annotation or default value are considered required.
+ *
+ * {@link NonNull} annotation is recommended on such non-primitive fields for documentation.
+ */
+ private @NonNull String mName4;
+
+ /**
+ * For parcelling, any field type supported by {@link Parcel} is supported out of the box.
+ * E.g. {@link Parcelable} subclasses, {@link String}, {@link int}, {@link boolean}, etc.
+ */
+ private AccessibilityNodeInfo mOtherParcelable = null;
+ /**
+ * Additionally, support for parcelling other types can be added by implementing a
+ * {@link Parcelling}, and referencing it in the {@link DataClass.ParcelWith} field annotation.
+ *
+ * @see DateParcelling an example {@link Parcelling} implementation
+ */
+ @DataClass.ParcelWith(DateParcelling.class)
+ private Date mDate = new Date(42 * 42);
+ /**
+ * If a {@link Parcelling} is fairly common, consider putting it in {@link Parcelling.BuiltIn}
+ * to encourage its reuse.
+ */
+ @DataClass.ParcelWith(Parcelling.BuiltIn.ForPattern.class)
+ private Pattern mPattern = Pattern.compile("");
+
+ /**
+ * For lists, when using a {@link Builder}, other than a regular
+ * {@link Builder#setLinkAddresses2(List) setter}, and additional
+ * {@link Builder#addLinkAddresses2(LinkAddress) add} method is generated for convenience.
+ */
+ private List<LinkAddress> mLinkAddresses2 = new ArrayList<>();
+ /**
+ * For aesthetics, you may want to consider providing a singular version of the plural field
+ * name, which would be used for the {@link #mLinkAddresses2 above mentioned} "add" method.
+ *
+ * @see Builder#addLinkAddress(LinkAddress)
+ */
+ @DataClass.PluralOf("linkAddress")
+ private ArrayList<LinkAddress> mLinkAddresses = new ArrayList<>();
+ /**
+ * For array fields, when using a {@link Builder}, vararg argument format is used for
+ * convenience.
+ *
+ * @see Builder#setLinkAddresses4(LinkAddress...)
+ */
+ private @Nullable LinkAddress[] mLinkAddresses4 = null;
+ /**
+ * For boolean fields, when using a {@link Builder}, in addition to a regular setter, methods
+ * like {@link Builder#markActive()} and {@link Builder#markNotActive()} are generated.
+ */
+ private boolean mActive = true;
+
+ /**
+ * {@link IntDef}/{@link StringDef}-annotated fields propagate the annotation to
+ * getter/constructor/setter/builder parameters, making for a nicer api.
+ *
+ * @see #getStateName
+ * @see Builder#setStateName
+ */
+ private @StateName String mStateName = STATE_NAME_UNDEFINED;
+ /**
+ * Fields annotated with {@link IntDef} annotations also get a proper {@link #toString()} value.
+ */
+ private @RequestFlags int mFlags;
+ /**
+ * Above is true for both {@link IntDef#flag flags} and enum-like {@link IntDef}s
+ */
+ private @State int mState = STATE_UNDEFINED;
+
+
+ /**
+ * Making a field public will suppress getter generation in favor of accessing it directly.
+ */
+ public CharSequence charSeq = "";
+ /**
+ * Final fields suppress generating a setter (when setters are requested).
+ */
+ private final LinkAddress[] mLinkAddresses5;
+ /**
+ * Transient fields are completely ignored and can be used for caching.
+ */
+ private transient LinkAddress[] mLinkAddresses6;
+ /**
+ * When using transient fields for caching it's often also a good idea to initialize them
+ * lazily.
+ *
+ * You can declare a special method like {@link #lazyInitTmpStorage()}, to let the
+ * {@link #getTmpStorage getter} lazily-initialize the value on demand.
+ */
+ transient int[] mTmpStorage;
+ private int[] lazyInitTmpStorage() {
+ return new int[100];
+ }
+
+ /**
+ * Fields with certain annotations are automatically validated in constructor
+ *
+ * You can see overloads in {@link AnnotationValidations} for a list of currently
+ * supported ones.
+ *
+ * You can also extend support to your custom annotations by creating another corresponding
+ * overloads like
+ * {@link AnnotationValidations#validate(Class, UserIdInt, int)}.
+ *
+ * @see #SampleDataClass
+ */
+ private @StringRes int mStringRes = 0;
+ /**
+ * Validation annotations may also have parameters.
+ *
+ * Parameter values will be supplied to validation method as name-value pairs.
+ *
+ * @see AnnotationValidations#validate(Class, Size, int, String, int, String, int)
+ */
+ private @android.annotation.IntRange(from = 0, to = 4) int mLimited = 3;
+ /**
+ * Unnamed validation annotation parameter gets supplied to the validating method named as
+ * "value".
+ *
+ * Validation annotations following {@link Each} annotation, will be applied for each
+ * array/collection element instead.
+ *
+ * @see AnnotationValidations#validate(Class, Size, int, String, int)
+ */
+ @Size(2)
+ @Each @FloatRange(from = 0f)
+ private float[] mCoords = new float[] {0f, 0f};
+
+
+ /**
+ * Manually declaring any method that would otherwise be generated suppresses its generation,
+ * allowing for fine-grained overrides of the generated behavior.
+ */
+ public LinkAddress[] getLinkAddresses4() {
+ //Suppress autogen
+ return null;
+ }
+
+ /**
+ * Additionally, some methods like {@link #equals}, {@link #hashCode}, {@link #toString},
+ * {@link #writeToParcel}, {@link Parcelable.Creator#createFromParcel} allow you to define
+ * special methods to override their behavior on a per-field basis.
+ *
+ * See the generateted methods' descriptions for the detailed instructions of what the method
+ * signatures for such methods are expected to be.
+ *
+ * Here we use this to "fix" {@link Pattern} not implementing equals/hashCode.
+ *
+ * @see #equals
+ * @see #hashCode
+ */
+ private boolean patternEquals(Pattern other) {
+ return Objects.equals(mPattern.pattern(), other.pattern());
+ }
+ private int patternHashCode() {
+ return Objects.hashCode(mPattern.pattern());
+ }
+
+ /**
+ * Similarly, {@link #onConstructed()}, if defined, gets called at the end of constructing an
+ * instance.
+ *
+ * At this point all fields should be in place, so this is the right place to put any custom
+ * validation logic.
+ */
+ private void onConstructed() {
+ Preconditions.checkState(mNum2 == mNum4);
+ }
+
+ /**
+ * {@link DataClass#genForEachField} can be used to generate a generic {@link #forEachField}
+ * utility, which can be used for various use-cases not covered out of the box.
+ * Callback passed to {@link #forEachField} will be called once per each property with its name
+ * and value.
+ *
+ * Here for example it's used to implement a typical dump method.
+ *
+ * Note that there are 2 {@link #forEachField} versions provided, one that treats each field
+ * value as an {@link Object}, thus boxing primitives if any, and one that additionally takes
+ * specialized callbacks for particular primitive field types used in given class.
+ *
+ * Some primitives like {@link Boolean}s and {@link Integer}s within [-128, 127] don't allocate
+ * when boxed, so it's up to you to decide which one to use for a given use-case.
+ */
+ public void dump(PrintWriter pw) {
+ forEachField((self, name, value) -> {
+ pw.append(" ").append(name).append(": ").append(String.valueOf(value)).append('\n');
+ });
+ }
+
+
+
+ // Code below generated by codegen v0.0.1.
+ // on Jul 17, 2019, 5:10:26 PM PDT
+ //
+ // DO NOT MODIFY!
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/tests/Codegen/src/com/android/codegentest/SampleDataClass.java
+ //
+ // CHECKSTYLE:OFF Generated code
+
+ @IntDef(prefix = "STATE_", value = {
+ STATE_UNDEFINED,
+ STATE_ON,
+ STATE_OFF
+ })
+ @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE)
+ @DataClass.Generated.Member
+ public @interface State {}
+
+ @DataClass.Generated.Member
+ public static String stateToString(@State int value) {
+ switch (value) {
+ case STATE_UNDEFINED:
+ return "STATE_UNDEFINED";
+ case STATE_ON:
+ return "STATE_ON";
+ case STATE_OFF:
+ return "STATE_OFF";
+ default: return Integer.toHexString(value);
+ }
+ }
+
+ @IntDef(flag = true, prefix = "FLAG_", value = {
+ FLAG_MANUAL_REQUEST,
+ FLAG_COMPATIBILITY_MODE_REQUEST,
+ FLAG_AUGMENTED_REQUEST
+ })
+ @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE)
+ @DataClass.Generated.Member
+ public @interface RequestFlags {}
+
+ @DataClass.Generated.Member
+ public static String requestFlagsToString(@RequestFlags int value) {
+ return com.android.internal.util.BitUtils.flagsToString(
+ value, SampleDataClass::singleRequestFlagsToString);
+ }
+
+ @DataClass.Generated.Member
+ static String singleRequestFlagsToString(@RequestFlags int value) {
+ switch (value) {
+ case FLAG_MANUAL_REQUEST:
+ return "FLAG_MANUAL_REQUEST";
+ case FLAG_COMPATIBILITY_MODE_REQUEST:
+ return "FLAG_COMPATIBILITY_MODE_REQUEST";
+ case FLAG_AUGMENTED_REQUEST:
+ return "FLAG_AUGMENTED_REQUEST";
+ default: return Integer.toHexString(value);
+ }
+ }
+
+ @StringDef(prefix = "STATE_NAME_", value = {
+ STATE_NAME_UNDEFINED,
+ STATE_NAME_ON,
+ STATE_NAME_OFF
+ })
+ @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE)
+ @DataClass.Generated.Member
+ public @interface StateName {}
+
+ @DataClass.Generated(
+ time = 1563408627046L,
+ codegenVersion = "0.0.1",
+ sourceFile = "frameworks/base/tests/Codegen/src/com/android/codegentest/SampleDataClass.java",
+ inputSignatures = "public static final java.lang.String STATE_NAME_UNDEFINED\npublic static final java.lang.String STATE_NAME_ON\npublic static final java.lang.String STATE_NAME_OFF\npublic static final int STATE_UNDEFINED\npublic static final int STATE_ON\npublic static final int STATE_OFF\npublic static final @com.android.codegentest.SampleDataClass.RequestFlags int FLAG_MANUAL_REQUEST\npublic static final @com.android.codegentest.SampleDataClass.RequestFlags int FLAG_COMPATIBILITY_MODE_REQUEST\npublic static final @com.android.codegentest.SampleDataClass.RequestFlags int FLAG_AUGMENTED_REQUEST\nprivate int mNum\nprivate int mNum2\nprivate int mNum4\nprivate @android.annotation.Nullable java.lang.String mName\nprivate java.lang.String mName2\nprivate @android.annotation.NonNull java.lang.String mName4\nprivate android.view.accessibility.AccessibilityNodeInfo mOtherParcelable\nprivate @com.android.internal.util.DataClass.ParcelWith(com.android.codegentest.DateParcelling.class) java.util.Date mDate\nprivate @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForPattern.class) java.util.regex.Pattern mPattern\nprivate java.util.List<android.net.LinkAddress> mLinkAddresses2\nprivate @com.android.internal.util.DataClass.PluralOf(\"linkAddress\") java.util.ArrayList<android.net.LinkAddress> mLinkAddresses\nprivate @android.annotation.Nullable android.net.LinkAddress[] mLinkAddresses4\nprivate boolean mActive\nprivate @com.android.codegentest.SampleDataClass.StateName java.lang.String mStateName\nprivate @com.android.codegentest.SampleDataClass.RequestFlags int mFlags\nprivate @com.android.codegentest.SampleDataClass.State int mState\npublic java.lang.CharSequence charSeq\nprivate final android.net.LinkAddress[] mLinkAddresses5\nprivate transient android.net.LinkAddress[] mLinkAddresses6\ntransient int[] mTmpStorage\nprivate @android.annotation.StringRes int mStringRes\nprivate @android.annotation.IntRange(from=0L, to=4L) int mLimited\nprivate @android.annotation.Size(2L) @com.android.internal.util.DataClass.Each @android.annotation.FloatRange(from=0.0) float[] mCoords\nprivate int[] lazyInitTmpStorage()\npublic android.net.LinkAddress[] getLinkAddresses4()\nprivate boolean patternEquals(java.util.regex.Pattern)\nprivate int patternHashCode()\nprivate void onConstructed()\npublic void dump(java.io.PrintWriter)")
+
+/**
+ * @param num
+ * Any property javadoc should go onto the field, and will be copied where appropriate,
+ * including getters, constructor parameters, builder setters, etc.
+ *
+ * <p>
+ * This allows to avoid the burden of maintaining copies of the same documentation
+ * pieces in multiple places for each field.
+ * @param num2
+ * Various javadoc features should work as expected when copied, e.g {@code code},
+ * {@link #mName links}, <a href="https://google.com">html links</a>, etc.
+ * @param num4
+ * {@code @hide} javadoc annotation is also propagated, which can be used to adjust the
+ * desired public API surface.
+ * @param name
+ * {@link Nullable} fields are considered optional and will not throw an exception if omitted
+ * (or set to null) when creating an instance either via a {@link Builder} or constructor.
+ * @param name2
+ * Fields with default value expressions ("mFoo = ...") are also optional, and are automatically
+ * initialized to the provided default expression, unless explicitly set.
+ * @param name4
+ * Fields without {@link Nullable} annotation or default value are considered required.
+ *
+ * {@link NonNull} annotation is recommended on such non-primitive fields for documentation.
+ * @param otherParcelable
+ * For parcelling, any field type supported by {@link Parcel} is supported out of the box.
+ * E.g. {@link Parcelable} subclasses, {@link String}, {@link int}, {@link boolean}, etc.
+ * @param date
+ * Additionally, support for parcelling other types can be added by implementing a
+ * {@link Parcelling}, and referencing it in the {@link DataClass.ParcelWith} field annotation.
+ * @param pattern
+ * If a {@link Parcelling} is fairly common, consider putting it in {@link Parcelling.BuiltIn}
+ * to encourage its reuse.
+ * @param linkAddresses2
+ * For lists, when using a {@link Builder}, other than a regular
+ * {@link Builder#setLinkAddresses2(List) setter}, and additional
+ * {@link Builder#addLinkAddresses2(LinkAddress) add} method is generated for convenience.
+ * @param linkAddresses
+ * For aesthetics, you may want to consider providing a singular version of the plural field
+ * name, which would be used for the {@link #mLinkAddresses2 above mentioned} "add" method.
+ * @param linkAddresses4
+ * For array fields, when using a {@link Builder}, vararg argument format is used for
+ * convenience.
+ * @param active
+ * For boolean fields, when using a {@link Builder}, in addition to a regular setter, methods
+ * like {@link Builder#markActive()} and {@link Builder#markNotActive()} are generated.
+ * @param stateName
+ * {@link IntDef}/{@link StringDef}-annotated fields propagate the annotation to
+ * getter/constructor/setter/builder parameters, making for a nicer api.
+ * @param flags
+ * Fields annotated with {@link IntDef} annotations also get a proper {@link #toString()} value.
+ * @param state
+ * Above is true for both {@link IntDef#flag flags} and enum-like {@link IntDef}s
+ * @param charSeq
+ * Making a field public will suppress getter generation in favor of accessing it directly.
+ * @param linkAddresses5
+ * Final fields suppress generating a setter (when setters are requested).
+ * @param stringRes
+ * Fields with certain annotations are automatically validated in constructor
+ *
+ * You can see overloads in {@link AnnotationValidations} for a list of currently
+ * supported ones.
+ *
+ * You can also extend support to your custom annotations by creating another corresponding
+ * overloads like
+ * {@link AnnotationValidations#validate(Class, UserIdInt, int)}.
+ * @param limited
+ * Validation annotations may also have parameters.
+ *
+ * Parameter values will be supplied to validation method as name-value pairs.
+ * @param coords
+ * Unnamed validation annotation parameter gets supplied to the validating method named as
+ * "value".
+ *
+ * Validation annotations following {@link Each} annotation, will be applied for each
+ * array/collection element instead.
+ */
+ @DataClass.Generated.Member
+ public SampleDataClass(
+ int num,
+ int num2,
+ int num4,
+ @Nullable String name,
+ String name2,
+ @NonNull String name4,
+ AccessibilityNodeInfo otherParcelable,
+ Date date,
+ Pattern pattern,
+ List<LinkAddress> linkAddresses2,
+ ArrayList<LinkAddress> linkAddresses,
+ @Nullable LinkAddress[] linkAddresses4,
+ boolean active,
+ @StateName String stateName,
+ @RequestFlags int flags,
+ @State int state,
+ CharSequence charSeq,
+ LinkAddress[] linkAddresses5,
+ @StringRes int stringRes,
+ @android.annotation.IntRange(from = 0, to = 4) int limited,
+ @Size(2) @FloatRange(from = 0f) float[] coords) {
+ this.mNum = num;
+ this.mNum2 = num2;
+ this.mNum4 = num4;
+ this.mName = name;
+ this.mName2 = name2;
+ this.mName4 = Preconditions.checkNotNull(name4);
+ this.mOtherParcelable = otherParcelable;
+ this.mDate = date;
+ this.mPattern = pattern;
+ this.mLinkAddresses2 = linkAddresses2;
+ this.mLinkAddresses = linkAddresses;
+ this.mLinkAddresses4 = linkAddresses4;
+ this.mActive = active;
+ this.mStateName = stateName;
+ this.mFlags = flags;
+ this.mState = state;
+ this.charSeq = charSeq;
+ this.mLinkAddresses5 = linkAddresses5;
+ this.mStringRes = stringRes;
+ this.mLimited = limited;
+ this.mCoords = coords;
+ AnnotationValidations.validate(
+ NonNull.class, null, mName4);
+
+ //noinspection PointlessBooleanExpression
+ if (true
+ && !(Objects.equals(mStateName, STATE_NAME_UNDEFINED))
+ && !(Objects.equals(mStateName, STATE_NAME_ON))
+ && !(Objects.equals(mStateName, STATE_NAME_OFF))) {
+ throw new java.lang.IllegalArgumentException(
+ "stateName was " + mStateName + " but must be one of: "
+ + "STATE_NAME_UNDEFINED(" + STATE_NAME_UNDEFINED + "), "
+ + "STATE_NAME_ON(" + STATE_NAME_ON + "), "
+ + "STATE_NAME_OFF(" + STATE_NAME_OFF + ")");
+ }
+
+
+ //noinspection PointlessBitwiseExpression
+ Preconditions.checkFlagsArgument(
+ mFlags, 0
+ | FLAG_MANUAL_REQUEST
+ | FLAG_COMPATIBILITY_MODE_REQUEST
+ | FLAG_AUGMENTED_REQUEST);
+
+ //noinspection PointlessBooleanExpression
+ if (true
+ && !(mState == STATE_UNDEFINED)
+ && !(mState == STATE_ON)
+ && !(mState == STATE_OFF)) {
+ throw new java.lang.IllegalArgumentException(
+ "state was " + mState + " but must be one of: "
+ + "STATE_UNDEFINED(" + STATE_UNDEFINED + "), "
+ + "STATE_ON(" + STATE_ON + "), "
+ + "STATE_OFF(" + STATE_OFF + ")");
+ }
+
+ AnnotationValidations.validate(
+ StringRes.class, null, mStringRes);
+ AnnotationValidations.validate(
+ android.annotation.IntRange.class, null, mLimited,
+ "from", 0,
+ "to", 4);
+ AnnotationValidations.validate(
+ Size.class, null, mCoords.length,
+ "value", 2);
+ int coordsSize = mCoords.length;
+ for (int i = 0; i < coordsSize; i++) {
+ AnnotationValidations.validate(
+ FloatRange.class, null, mCoords[i],
+ "from", 0f);
+ }
+
+
+ onConstructed();
+ }
+
+ /**
+ * Any property javadoc should go onto the field, and will be copied where appropriate,
+ * including getters, constructor parameters, builder setters, etc.
+ *
+ * <p>
+ * This allows to avoid the burden of maintaining copies of the same documentation
+ * pieces in multiple places for each field.
+ */
+ @DataClass.Generated.Member
+ public int getNum() {
+ return mNum;
+ }
+
+ /**
+ * Various javadoc features should work as expected when copied, e.g {@code code},
+ * {@link #mName links}, <a href="https://google.com">html links</a>, etc.
+ *
+ * @see #mNum2 ..and so should blocks at the bottom, e.g. {@code @see} blocks.
+ */
+ @DataClass.Generated.Member
+ public int getNum2() {
+ return mNum2;
+ }
+
+ /**
+ * {@code @hide} javadoc annotation is also propagated, which can be used to adjust the
+ * desired public API surface.
+ *
+ * @see #getNum4() is hidden
+ * @see Builder#setNum4(int) also hidden
+ * @hide
+ */
+ @DataClass.Generated.Member
+ public int getNum4() {
+ return mNum4;
+ }
+
+ /**
+ * {@link Nullable} fields are considered optional and will not throw an exception if omitted
+ * (or set to null) when creating an instance either via a {@link Builder} or constructor.
+ */
+ @DataClass.Generated.Member
+ public @Nullable String getName() {
+ return mName;
+ }
+
+ /**
+ * Fields with default value expressions ("mFoo = ...") are also optional, and are automatically
+ * initialized to the provided default expression, unless explicitly set.
+ */
+ @DataClass.Generated.Member
+ public String getName2() {
+ return mName2;
+ }
+
+ /**
+ * Fields without {@link Nullable} annotation or default value are considered required.
+ *
+ * {@link NonNull} annotation is recommended on such non-primitive fields for documentation.
+ */
+ @DataClass.Generated.Member
+ public @NonNull String getName4() {
+ return mName4;
+ }
+
+ /**
+ * For parcelling, any field type supported by {@link Parcel} is supported out of the box.
+ * E.g. {@link Parcelable} subclasses, {@link String}, {@link int}, {@link boolean}, etc.
+ */
+ @DataClass.Generated.Member
+ public AccessibilityNodeInfo getOtherParcelable() {
+ return mOtherParcelable;
+ }
+
+ /**
+ * Additionally, support for parcelling other types can be added by implementing a
+ * {@link Parcelling}, and referencing it in the {@link DataClass.ParcelWith} field annotation.
+ *
+ * @see DateParcelling an example {@link Parcelling} implementation
+ */
+ @DataClass.Generated.Member
+ public Date getDate() {
+ return mDate;
+ }
+
+ /**
+ * If a {@link Parcelling} is fairly common, consider putting it in {@link Parcelling.BuiltIn}
+ * to encourage its reuse.
+ */
+ @DataClass.Generated.Member
+ public Pattern getPattern() {
+ return mPattern;
+ }
+
+ /**
+ * For lists, when using a {@link Builder}, other than a regular
+ * {@link Builder#setLinkAddresses2(List) setter}, and additional
+ * {@link Builder#addLinkAddresses2(LinkAddress) add} method is generated for convenience.
+ */
+ @DataClass.Generated.Member
+ public List<LinkAddress> getLinkAddresses2() {
+ return mLinkAddresses2;
+ }
+
+ /**
+ * For aesthetics, you may want to consider providing a singular version of the plural field
+ * name, which would be used for the {@link #mLinkAddresses2 above mentioned} "add" method.
+ *
+ * @see Builder#addLinkAddress(LinkAddress)
+ */
+ @DataClass.Generated.Member
+ public ArrayList<LinkAddress> getLinkAddresses() {
+ return mLinkAddresses;
+ }
+
+ /**
+ * For boolean fields, when using a {@link Builder}, in addition to a regular setter, methods
+ * like {@link Builder#markActive()} and {@link Builder#markNotActive()} are generated.
+ */
+ @DataClass.Generated.Member
+ public boolean isActive() {
+ return mActive;
+ }
+
+ /**
+ * {@link IntDef}/{@link StringDef}-annotated fields propagate the annotation to
+ * getter/constructor/setter/builder parameters, making for a nicer api.
+ *
+ * @see #getStateName
+ * @see Builder#setStateName
+ */
+ @DataClass.Generated.Member
+ public @StateName String getStateName() {
+ return mStateName;
+ }
+
+ /**
+ * Fields annotated with {@link IntDef} annotations also get a proper {@link #toString()} value.
+ */
+ @DataClass.Generated.Member
+ public @RequestFlags int getFlags() {
+ return mFlags;
+ }
+
+ /**
+ * Above is true for both {@link IntDef#flag flags} and enum-like {@link IntDef}s
+ */
+ @DataClass.Generated.Member
+ public @State int getState() {
+ return mState;
+ }
+
+ /**
+ * Final fields suppress generating a setter (when setters are requested).
+ */
+ @DataClass.Generated.Member
+ public LinkAddress[] getLinkAddresses5() {
+ return mLinkAddresses5;
+ }
+
+ /**
+ * Fields with certain annotations are automatically validated in constructor
+ *
+ * You can see overloads in {@link AnnotationValidations} for a list of currently
+ * supported ones.
+ *
+ * You can also extend support to your custom annotations by creating another corresponding
+ * overloads like
+ * {@link AnnotationValidations#validate(Class, UserIdInt, int)}.
+ *
+ * @see #SampleDataClass
+ */
+ @DataClass.Generated.Member
+ public @StringRes int getStringRes() {
+ return mStringRes;
+ }
+
+ /**
+ * Validation annotations may also have parameters.
+ *
+ * Parameter values will be supplied to validation method as name-value pairs.
+ *
+ * @see AnnotationValidations#validate(Class, Size, int, String, int, String, int)
+ */
+ @DataClass.Generated.Member
+ public @android.annotation.IntRange(from = 0, to = 4) int getLimited() {
+ return mLimited;
+ }
+
+ /**
+ * Unnamed validation annotation parameter gets supplied to the validating method named as
+ * "value".
+ *
+ * Validation annotations following {@link Each} annotation, will be applied for each
+ * array/collection element instead.
+ *
+ * @see AnnotationValidations#validate(Class, Size, int, String, int)
+ */
+ @DataClass.Generated.Member
+ public @Size(2) @FloatRange(from = 0f) float[] getCoords() {
+ return mCoords;
+ }
+
+ /**
+ * When using transient fields for caching it's often also a good idea to initialize them
+ * lazily.
+ *
+ * You can declare a special method like {@link #lazyInitTmpStorage()}, to let the
+ * {@link #getTmpStorage getter} lazily-initialize the value on demand.
+ */
+ @DataClass.Generated.Member
+ public int[] getTmpStorage() {
+ int[] tmpStorage = mTmpStorage;
+ if (tmpStorage == null) {
+ // You can mark field as volatile for thread-safe double-check init
+ tmpStorage = mTmpStorage = lazyInitTmpStorage();
+ }
+ return tmpStorage;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public String toString() {
+ // You can override field toString logic by defining methods like:
+ // String fieldNameToString() { ... }
+
+ return "SampleDataClass { " +
+ "num = " + mNum + ", " +
+ "num2 = " + mNum2 + ", " +
+ "num4 = " + mNum4 + ", " +
+ "name = " + mName + ", " +
+ "name2 = " + mName2 + ", " +
+ "name4 = " + mName4 + ", " +
+ "otherParcelable = " + mOtherParcelable + ", " +
+ "date = " + mDate + ", " +
+ "pattern = " + mPattern + ", " +
+ "linkAddresses2 = " + mLinkAddresses2 + ", " +
+ "linkAddresses = " + mLinkAddresses + ", " +
+ "linkAddresses4 = " + java.util.Arrays.toString(mLinkAddresses4) + ", " +
+ "active = " + mActive + ", " +
+ "stateName = " + mStateName + ", " +
+ "flags = " + requestFlagsToString(mFlags) + ", " +
+ "state = " + stateToString(mState) + ", " +
+ "charSeq = " + charSeq + ", " +
+ "linkAddresses5 = " + java.util.Arrays.toString(mLinkAddresses5) + ", " +
+ "stringRes = " + mStringRes + ", " +
+ "limited = " + mLimited + ", " +
+ "coords = " + java.util.Arrays.toString(mCoords) +
+ " }";
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public boolean equals(Object o) {
+ // You can override field equality logic by defining either of the methods like:
+ // boolean fieldNameEquals(SampleDataClass other) { ... }
+ // boolean fieldNameEquals(FieldType otherValue) { ... }
+
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ @SuppressWarnings("unchecked")
+ SampleDataClass that = (SampleDataClass) o;
+ //noinspection PointlessBooleanExpression
+ return true
+ && mNum == that.mNum
+ && mNum2 == that.mNum2
+ && mNum4 == that.mNum4
+ && Objects.equals(mName, that.mName)
+ && Objects.equals(mName2, that.mName2)
+ && Objects.equals(mName4, that.mName4)
+ && Objects.equals(mOtherParcelable, that.mOtherParcelable)
+ && Objects.equals(mDate, that.mDate)
+ && patternEquals(that.mPattern)
+ && Objects.equals(mLinkAddresses2, that.mLinkAddresses2)
+ && Objects.equals(mLinkAddresses, that.mLinkAddresses)
+ && java.util.Arrays.equals(mLinkAddresses4, that.mLinkAddresses4)
+ && mActive == that.mActive
+ && Objects.equals(mStateName, that.mStateName)
+ && mFlags == that.mFlags
+ && mState == that.mState
+ && Objects.equals(charSeq, that.charSeq)
+ && java.util.Arrays.equals(mLinkAddresses5, that.mLinkAddresses5)
+ && mStringRes == that.mStringRes
+ && mLimited == that.mLimited
+ && java.util.Arrays.equals(mCoords, that.mCoords);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int hashCode() {
+ // You can override field hashCode logic by defining methods like:
+ // int fieldNameHashCode() { ... }
+
+ int _hash = 1;
+ _hash = 31 * _hash + mNum;
+ _hash = 31 * _hash + mNum2;
+ _hash = 31 * _hash + mNum4;
+ _hash = 31 * _hash + Objects.hashCode(mName);
+ _hash = 31 * _hash + Objects.hashCode(mName2);
+ _hash = 31 * _hash + Objects.hashCode(mName4);
+ _hash = 31 * _hash + Objects.hashCode(mOtherParcelable);
+ _hash = 31 * _hash + Objects.hashCode(mDate);
+ _hash = 31 * _hash + patternHashCode();
+ _hash = 31 * _hash + Objects.hashCode(mLinkAddresses2);
+ _hash = 31 * _hash + Objects.hashCode(mLinkAddresses);
+ _hash = 31 * _hash + java.util.Arrays.hashCode(mLinkAddresses4);
+ _hash = 31 * _hash + Boolean.hashCode(mActive);
+ _hash = 31 * _hash + Objects.hashCode(mStateName);
+ _hash = 31 * _hash + mFlags;
+ _hash = 31 * _hash + mState;
+ _hash = 31 * _hash + Objects.hashCode(charSeq);
+ _hash = 31 * _hash + java.util.Arrays.hashCode(mLinkAddresses5);
+ _hash = 31 * _hash + mStringRes;
+ _hash = 31 * _hash + mLimited;
+ _hash = 31 * _hash + java.util.Arrays.hashCode(mCoords);
+ return _hash;
+ }
+
+ @DataClass.Generated.Member
+ void forEachField(
+ DataClass.PerIntFieldAction<SampleDataClass> actionInt,
+ DataClass.PerObjectFieldAction<SampleDataClass> actionObject) {
+ actionInt.acceptInt(this, "num", mNum);
+ actionInt.acceptInt(this, "num2", mNum2);
+ actionInt.acceptInt(this, "num4", mNum4);
+ actionObject.acceptObject(this, "name", mName);
+ actionObject.acceptObject(this, "name2", mName2);
+ actionObject.acceptObject(this, "name4", mName4);
+ actionObject.acceptObject(this, "otherParcelable", mOtherParcelable);
+ actionObject.acceptObject(this, "date", mDate);
+ actionObject.acceptObject(this, "pattern", mPattern);
+ actionObject.acceptObject(this, "linkAddresses2", mLinkAddresses2);
+ actionObject.acceptObject(this, "linkAddresses", mLinkAddresses);
+ actionObject.acceptObject(this, "linkAddresses4", mLinkAddresses4);
+ actionObject.acceptObject(this, "active", mActive);
+ actionObject.acceptObject(this, "stateName", mStateName);
+ actionInt.acceptInt(this, "flags", mFlags);
+ actionInt.acceptInt(this, "state", mState);
+ actionObject.acceptObject(this, "charSeq", charSeq);
+ actionObject.acceptObject(this, "linkAddresses5", mLinkAddresses5);
+ actionInt.acceptInt(this, "stringRes", mStringRes);
+ actionInt.acceptInt(this, "limited", mLimited);
+ actionObject.acceptObject(this, "coords", mCoords);
+ }
+
+ /** @deprecated May cause boxing allocations - use with caution! */
+ @Deprecated
+ @DataClass.Generated.Member
+ void forEachField(DataClass.PerObjectFieldAction<SampleDataClass> action) {
+ action.acceptObject(this, "num", mNum);
+ action.acceptObject(this, "num2", mNum2);
+ action.acceptObject(this, "num4", mNum4);
+ action.acceptObject(this, "name", mName);
+ action.acceptObject(this, "name2", mName2);
+ action.acceptObject(this, "name4", mName4);
+ action.acceptObject(this, "otherParcelable", mOtherParcelable);
+ action.acceptObject(this, "date", mDate);
+ action.acceptObject(this, "pattern", mPattern);
+ action.acceptObject(this, "linkAddresses2", mLinkAddresses2);
+ action.acceptObject(this, "linkAddresses", mLinkAddresses);
+ action.acceptObject(this, "linkAddresses4", mLinkAddresses4);
+ action.acceptObject(this, "active", mActive);
+ action.acceptObject(this, "stateName", mStateName);
+ action.acceptObject(this, "flags", mFlags);
+ action.acceptObject(this, "state", mState);
+ action.acceptObject(this, "charSeq", charSeq);
+ action.acceptObject(this, "linkAddresses5", mLinkAddresses5);
+ action.acceptObject(this, "stringRes", mStringRes);
+ action.acceptObject(this, "limited", mLimited);
+ action.acceptObject(this, "coords", mCoords);
+ }
+
+ @DataClass.Generated.Member
+ static Parcelling<Date> sParcellingForDate =
+ Parcelling.Cache.get(
+ DateParcelling.class);
+ static {
+ if (sParcellingForDate == null) {
+ sParcellingForDate = Parcelling.Cache.put(
+ new DateParcelling());
+ }
+ }
+
+ @DataClass.Generated.Member
+ static Parcelling<Pattern> sParcellingForPattern =
+ Parcelling.Cache.get(
+ Parcelling.BuiltIn.ForPattern.class);
+ static {
+ if (sParcellingForPattern == null) {
+ sParcellingForPattern = Parcelling.Cache.put(
+ new Parcelling.BuiltIn.ForPattern());
+ }
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+
+ long flg = 0;
+ if (mActive) flg |= 0x1000;
+ if (mName != null) flg |= 0x8;
+ if (mName2 != null) flg |= 0x10;
+ if (mOtherParcelable != null) flg |= 0x40;
+ if (mDate != null) flg |= 0x80;
+ if (mPattern != null) flg |= 0x100;
+ if (mLinkAddresses2 != null) flg |= 0x200;
+ if (mLinkAddresses != null) flg |= 0x400;
+ if (mLinkAddresses4 != null) flg |= 0x800;
+ if (mStateName != null) flg |= 0x2000;
+ if (charSeq != null) flg |= 0x10000;
+ if (mLinkAddresses5 != null) flg |= 0x20000;
+ if (mCoords != null) flg |= 0x100000;
+ dest.writeLong(flg);
+ dest.writeInt(mNum);
+ dest.writeInt(mNum2);
+ dest.writeInt(mNum4);
+ if (mName != null) dest.writeString(mName);
+ if (mName2 != null) dest.writeString(mName2);
+ dest.writeString(mName4);
+ if (mOtherParcelable != null) dest.writeTypedObject(mOtherParcelable, flags);
+ sParcellingForDate.parcel(mDate, dest, flags);
+ sParcellingForPattern.parcel(mPattern, dest, flags);
+ if (mLinkAddresses2 != null) dest.writeParcelableList(mLinkAddresses2, flags);
+ if (mLinkAddresses != null) dest.writeParcelableList(mLinkAddresses, flags);
+ if (mLinkAddresses4 != null) dest.writeTypedArray(mLinkAddresses4, flags);
+ if (mStateName != null) dest.writeString(mStateName);
+ dest.writeInt(mFlags);
+ dest.writeInt(mState);
+ if (charSeq != null) dest.writeCharSequence(charSeq);
+ if (mLinkAddresses5 != null) dest.writeTypedArray(mLinkAddresses5, flags);
+ dest.writeInt(mStringRes);
+ dest.writeInt(mLimited);
+ if (mCoords != null) dest.writeFloatArray(mCoords);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ @DataClass.Generated.Member
+ public static final @NonNull Parcelable.Creator<SampleDataClass> CREATOR
+ = new Parcelable.Creator<SampleDataClass>() {
+ @Override
+ public SampleDataClass[] newArray(int size) {
+ return new SampleDataClass[size];
+ }
+
+ @Override
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ public SampleDataClass createFromParcel(Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+
+ long flg = in.readLong();
+ boolean active = (flg & 0x1000) != 0;
+ int num = in.readInt();
+ int num2 = in.readInt();
+ int num4 = in.readInt();
+ String name = (flg & 0x8) == 0 ? null : in.readString();
+ String name2 = (flg & 0x10) == 0 ? null : in.readString();
+ String name4 = in.readString();
+ AccessibilityNodeInfo otherParcelable = (flg & 0x40) == 0 ? null : (AccessibilityNodeInfo) in.readTypedObject(AccessibilityNodeInfo.CREATOR);
+ Date date = sParcellingForDate.unparcel(in);
+ Pattern pattern = sParcellingForPattern.unparcel(in);
+ List<LinkAddress> linkAddresses2 = null;
+ if ((flg & 0x200) != 0) {
+ linkAddresses2 = new ArrayList<>();
+ in.readParcelableList(linkAddresses2, LinkAddress.class.getClassLoader());
+ }
+ ArrayList<LinkAddress> linkAddresses = null;
+ if ((flg & 0x400) != 0) {
+ linkAddresses = new ArrayList<>();
+ in.readParcelableList(linkAddresses, LinkAddress.class.getClassLoader());
+ }
+ LinkAddress[] linkAddresses4 = (flg & 0x800) == 0 ? null : (LinkAddress[]) in.createTypedArray(LinkAddress.CREATOR);
+ String stateName = (flg & 0x2000) == 0 ? null : in.readString();
+ int flags = in.readInt();
+ int state = in.readInt();
+ CharSequence _charSeq = (flg & 0x10000) == 0 ? null : (CharSequence) in.readCharSequence();
+ LinkAddress[] linkAddresses5 = (flg & 0x20000) == 0 ? null : (LinkAddress[]) in.createTypedArray(LinkAddress.CREATOR);
+ int stringRes = in.readInt();
+ int limited = in.readInt();
+ float[] coords = (flg & 0x100000) == 0 ? null : in.createFloatArray();
+ return new SampleDataClass(
+ num,
+ num2,
+ num4,
+ name,
+ name2,
+ name4,
+ otherParcelable,
+ date,
+ pattern,
+ linkAddresses2,
+ linkAddresses,
+ linkAddresses4,
+ active,
+ stateName,
+ flags,
+ state,
+ _charSeq,
+ linkAddresses5,
+ stringRes,
+ limited,
+ coords);
+ }
+ };
+
+ /**
+ * A builder for {@link SampleDataClass}
+ */
+ @SuppressWarnings("WeakerAccess")
+ @DataClass.Generated.Member
+ public static class Builder
+ extends android.provider.OneTimeUseBuilder<SampleDataClass> {
+
+ protected int mNum;
+ protected int mNum2;
+ protected int mNum4;
+ protected @Nullable String mName;
+ protected String mName2;
+ protected @NonNull String mName4;
+ protected AccessibilityNodeInfo mOtherParcelable;
+ protected Date mDate;
+ protected Pattern mPattern;
+ protected List<LinkAddress> mLinkAddresses2;
+ protected ArrayList<LinkAddress> mLinkAddresses;
+ protected @Nullable LinkAddress[] mLinkAddresses4;
+ protected boolean mActive;
+ protected @StateName String mStateName;
+ protected @RequestFlags int mFlags;
+ protected @State int mState;
+ protected CharSequence charSeq;
+ protected LinkAddress[] mLinkAddresses5;
+ protected @StringRes int mStringRes;
+ protected @android.annotation.IntRange(from = 0, to = 4) int mLimited;
+ protected @Size(2) @FloatRange(from = 0f) float[] mCoords;
+
+ protected long mBuilderFieldsSet = 0L;
+
+ public Builder() {};
+
+ /**
+ * Any property javadoc should go onto the field, and will be copied where appropriate,
+ * including getters, constructor parameters, builder setters, etc.
+ *
+ * <p>
+ * This allows to avoid the burden of maintaining copies of the same documentation
+ * pieces in multiple places for each field.
+ */
+ @DataClass.Generated.Member
+ public Builder setNum(int value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x1;
+ mNum = value;
+ return this;
+ }
+
+ /**
+ * Various javadoc features should work as expected when copied, e.g {@code code},
+ * {@link #mName links}, <a href="https://google.com">html links</a>, etc.
+ *
+ * @see #mNum2 ..and so should blocks at the bottom, e.g. {@code @see} blocks.
+ */
+ @DataClass.Generated.Member
+ public Builder setNum2(int value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x2;
+ mNum2 = value;
+ return this;
+ }
+
+ /**
+ * {@code @hide} javadoc annotation is also propagated, which can be used to adjust the
+ * desired public API surface.
+ *
+ * @see #getNum4() is hidden
+ * @see Builder#setNum4(int) also hidden
+ * @hide
+ */
+ @DataClass.Generated.Member
+ public Builder setNum4(int value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x4;
+ mNum4 = value;
+ return this;
+ }
+
+ /**
+ * {@link Nullable} fields are considered optional and will not throw an exception if omitted
+ * (or set to null) when creating an instance either via a {@link Builder} or constructor.
+ */
+ @DataClass.Generated.Member
+ public Builder setName(@Nullable String value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x8;
+ mName = value;
+ return this;
+ }
+
+ /**
+ * Fields with default value expressions ("mFoo = ...") are also optional, and are automatically
+ * initialized to the provided default expression, unless explicitly set.
+ */
+ @DataClass.Generated.Member
+ public Builder setName2(String value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x10;
+ mName2 = value;
+ return this;
+ }
+
+ /**
+ * Fields without {@link Nullable} annotation or default value are considered required.
+ *
+ * {@link NonNull} annotation is recommended on such non-primitive fields for documentation.
+ */
+ @DataClass.Generated.Member
+ public Builder setName4(@NonNull String value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x20;
+ mName4 = value;
+ return this;
+ }
+
+ /**
+ * For parcelling, any field type supported by {@link Parcel} is supported out of the box.
+ * E.g. {@link Parcelable} subclasses, {@link String}, {@link int}, {@link boolean}, etc.
+ */
+ @DataClass.Generated.Member
+ public Builder setOtherParcelable(AccessibilityNodeInfo value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x40;
+ mOtherParcelable = value;
+ return this;
+ }
+
+ /**
+ * Additionally, support for parcelling other types can be added by implementing a
+ * {@link Parcelling}, and referencing it in the {@link DataClass.ParcelWith} field annotation.
+ *
+ * @see DateParcelling an example {@link Parcelling} implementation
+ */
+ @DataClass.Generated.Member
+ public Builder setDate(Date value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x80;
+ mDate = value;
+ return this;
+ }
+
+ /**
+ * If a {@link Parcelling} is fairly common, consider putting it in {@link Parcelling.BuiltIn}
+ * to encourage its reuse.
+ */
+ @DataClass.Generated.Member
+ public Builder setPattern(Pattern value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x100;
+ mPattern = value;
+ return this;
+ }
+
+ /**
+ * For lists, when using a {@link Builder}, other than a regular
+ * {@link Builder#setLinkAddresses2(List) setter}, and additional
+ * {@link Builder#addLinkAddresses2(LinkAddress) add} method is generated for convenience.
+ */
+ @DataClass.Generated.Member
+ public Builder setLinkAddresses2(List<LinkAddress> value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x200;
+ mLinkAddresses2 = value;
+ return this;
+ }
+
+ /** @see #setLinkAddresses2 */
+ @DataClass.Generated.Member
+ public Builder addLinkAddresses2(@NonNull LinkAddress value) {
+ // You can refine this method's name by providing item's singular name, e.g.:
+ // @DataClass.PluralOf("item")) mItems = ...
+
+ if (mLinkAddresses2 == null) setLinkAddresses2(new ArrayList<>());
+ mLinkAddresses2.add(value);
+ return this;
+ }
+
+ /**
+ * For aesthetics, you may want to consider providing a singular version of the plural field
+ * name, which would be used for the {@link #mLinkAddresses2 above mentioned} "add" method.
+ *
+ * @see Builder#addLinkAddress(LinkAddress)
+ */
+ @DataClass.Generated.Member
+ public Builder setLinkAddresses(ArrayList<LinkAddress> value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x400;
+ mLinkAddresses = value;
+ return this;
+ }
+
+ /** @see #setLinkAddresses */
+ @DataClass.Generated.Member
+ public Builder addLinkAddress(@NonNull LinkAddress value) {
+ if (mLinkAddresses == null) setLinkAddresses(new ArrayList<>());
+ mLinkAddresses.add(value);
+ return this;
+ }
+
+ /**
+ * For array fields, when using a {@link Builder}, vararg argument format is used for
+ * convenience.
+ *
+ * @see Builder#setLinkAddresses4(LinkAddress...)
+ */
+ @DataClass.Generated.Member
+ public Builder setLinkAddresses4(@Nullable LinkAddress... value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x800;
+ mLinkAddresses4 = value;
+ return this;
+ }
+
+ /**
+ * For boolean fields, when using a {@link Builder}, in addition to a regular setter, methods
+ * like {@link Builder#markActive()} and {@link Builder#markNotActive()} are generated.
+ */
+ @DataClass.Generated.Member
+ public Builder setActive(boolean value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x1000;
+ mActive = value;
+ return this;
+ }
+
+ /** @see #setActive */
+ @DataClass.Generated.Member
+ public Builder markActive() {
+ return setActive(true);
+ }
+
+ /** @see #setActive */
+ @DataClass.Generated.Member
+ public Builder markNotActive() {
+ return setActive(false);
+ }
+
+ /**
+ * {@link IntDef}/{@link StringDef}-annotated fields propagate the annotation to
+ * getter/constructor/setter/builder parameters, making for a nicer api.
+ *
+ * @see #getStateName
+ * @see Builder#setStateName
+ */
+ @DataClass.Generated.Member
+ public Builder setStateName(@StateName String value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x2000;
+ mStateName = value;
+ return this;
+ }
+
+ /**
+ * Fields annotated with {@link IntDef} annotations also get a proper {@link #toString()} value.
+ */
+ @DataClass.Generated.Member
+ public Builder setFlags(@RequestFlags int value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x4000;
+ mFlags = value;
+ return this;
+ }
+
+ /**
+ * Above is true for both {@link IntDef#flag flags} and enum-like {@link IntDef}s
+ */
+ @DataClass.Generated.Member
+ public Builder setState(@State int value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x8000;
+ mState = value;
+ return this;
+ }
+
+ /**
+ * Making a field public will suppress getter generation in favor of accessing it directly.
+ */
+ @DataClass.Generated.Member
+ public Builder setCharSeq(CharSequence value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x10000;
+ charSeq = value;
+ return this;
+ }
+
+ /**
+ * Final fields suppress generating a setter (when setters are requested).
+ */
+ @DataClass.Generated.Member
+ public Builder setLinkAddresses5(LinkAddress... value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x20000;
+ mLinkAddresses5 = value;
+ return this;
+ }
+
+ /**
+ * Fields with certain annotations are automatically validated in constructor
+ *
+ * You can see overloads in {@link AnnotationValidations} for a list of currently
+ * supported ones.
+ *
+ * You can also extend support to your custom annotations by creating another corresponding
+ * overloads like
+ * {@link AnnotationValidations#validate(Class, UserIdInt, int)}.
+ *
+ * @see #SampleDataClass
+ */
+ @DataClass.Generated.Member
+ public Builder setStringRes(@StringRes int value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x40000;
+ mStringRes = value;
+ return this;
+ }
+
+ /**
+ * Validation annotations may also have parameters.
+ *
+ * Parameter values will be supplied to validation method as name-value pairs.
+ *
+ * @see AnnotationValidations#validate(Class, Size, int, String, int, String, int)
+ */
+ @DataClass.Generated.Member
+ public Builder setLimited(@android.annotation.IntRange(from = 0, to = 4) int value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x80000;
+ mLimited = value;
+ return this;
+ }
+
+ /**
+ * Unnamed validation annotation parameter gets supplied to the validating method named as
+ * "value".
+ *
+ * Validation annotations following {@link Each} annotation, will be applied for each
+ * array/collection element instead.
+ *
+ * @see AnnotationValidations#validate(Class, Size, int, String, int)
+ */
+ @DataClass.Generated.Member
+ public Builder setCoords(@Size(2) @FloatRange(from = 0f) float... value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x100000;
+ mCoords = value;
+ return this;
+ }
+
+ /** Builds the instance. This builder should not be touched after calling this! */
+ public SampleDataClass build() {
+ markUsed();
+ if ((mBuilderFieldsSet & 0x1) == 0) {
+ throw new IllegalStateException("Required field not set: num");
+ }
+ if ((mBuilderFieldsSet & 0x2) == 0) {
+ throw new IllegalStateException("Required field not set: num2");
+ }
+ if ((mBuilderFieldsSet & 0x4) == 0) {
+ throw new IllegalStateException("Required field not set: num4");
+ }
+ if ((mBuilderFieldsSet & 0x10) == 0) {
+ mName2 = "Bob";
+ }
+ if ((mBuilderFieldsSet & 0x20) == 0) {
+ throw new IllegalStateException("Required field not set: name4");
+ }
+ if ((mBuilderFieldsSet & 0x40) == 0) {
+ mOtherParcelable = null;
+ }
+ if ((mBuilderFieldsSet & 0x80) == 0) {
+ mDate = new Date(42 * 42);
+ }
+ if ((mBuilderFieldsSet & 0x100) == 0) {
+ mPattern = Pattern.compile("");
+ }
+ if ((mBuilderFieldsSet & 0x200) == 0) {
+ mLinkAddresses2 = new ArrayList<>();
+ }
+ if ((mBuilderFieldsSet & 0x400) == 0) {
+ mLinkAddresses = new ArrayList<>();
+ }
+ if ((mBuilderFieldsSet & 0x800) == 0) {
+ mLinkAddresses4 = null;
+ }
+ if ((mBuilderFieldsSet & 0x1000) == 0) {
+ mActive = true;
+ }
+ if ((mBuilderFieldsSet & 0x2000) == 0) {
+ mStateName = STATE_NAME_UNDEFINED;
+ }
+ if ((mBuilderFieldsSet & 0x4000) == 0) {
+ throw new IllegalStateException("Required field not set: flags");
+ }
+ if ((mBuilderFieldsSet & 0x8000) == 0) {
+ mState = STATE_UNDEFINED;
+ }
+ if ((mBuilderFieldsSet & 0x10000) == 0) {
+ charSeq = "";
+ }
+ if ((mBuilderFieldsSet & 0x20000) == 0) {
+ throw new IllegalStateException("Required field not set: linkAddresses5");
+ }
+ if ((mBuilderFieldsSet & 0x40000) == 0) {
+ mStringRes = 0;
+ }
+ if ((mBuilderFieldsSet & 0x80000) == 0) {
+ mLimited = 3;
+ }
+ if ((mBuilderFieldsSet & 0x100000) == 0) {
+ mCoords = new float[] { 0f, 0f };
+ }
+ SampleDataClass o = new SampleDataClass(
+ mNum,
+ mNum2,
+ mNum4,
+ mName,
+ mName2,
+ mName4,
+ mOtherParcelable,
+ mDate,
+ mPattern,
+ mLinkAddresses2,
+ mLinkAddresses,
+ mLinkAddresses4,
+ mActive,
+ mStateName,
+ mFlags,
+ mState,
+ charSeq,
+ mLinkAddresses5,
+ mStringRes,
+ mLimited,
+ mCoords);
+ return o;
+ }
+ }
+
+}
diff --git a/tests/Codegen/src/com/android/codegentest/SampleDataClassTest.java b/tests/Codegen/src/com/android/codegentest/SampleDataClassTest.java
new file mode 100644
index 0000000..71e85ab
--- /dev/null
+++ b/tests/Codegen/src/com/android/codegentest/SampleDataClassTest.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.codegentest;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThat;
+
+import android.net.LinkAddress;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Tests {@link SampleDataClass} after it's augmented with dataclass codegen.
+ *
+ * Use {@code $ . runTest.sh} to run.
+ */
+@RunWith(AndroidJUnit4.class)
+public class SampleDataClassTest {
+
+ private SampleDataClass mSpecimen = newBuilder().build();
+
+ private static SampleDataClass.Builder newBuilder() {
+ return newIncompleteBuilder()
+ .setNum(42)
+ .setNum2(42)
+ .setNum4(42)
+ .setName4("foobar")
+ .setLinkAddresses5();
+ }
+
+ private static SampleDataClass.Builder newIncompleteBuilder() {
+ return new SampleDataClass.Builder()
+ .markActive()
+ .setName("some parcelable")
+ .setFlags(SampleDataClass.FLAG_MANUAL_REQUEST);
+ }
+
+ @Test
+ public void testParcelling_producesEqualInstance() {
+ SampleDataClass copy = parcelAndUnparcel(mSpecimen, SampleDataClass.CREATOR);
+ assertEquals(mSpecimen, copy);
+ assertEquals(mSpecimen.hashCode(), copy.hashCode());
+ }
+
+ @Test
+ public void testParcelling_producesInstanceWithEqualFields() {
+ SampleDataClass copy = parcelAndUnparcel(mSpecimen, SampleDataClass.CREATOR);
+ copy.forEachField((self, copyFieldName, copyFieldValue) -> {
+ mSpecimen.forEachField((self2, specimenFieldName, specimenFieldValue) -> {
+ if (copyFieldName.equals(specimenFieldName)
+ && !copyFieldName.equals("pattern")
+ && (specimenFieldValue == null
+ || !specimenFieldValue.getClass().isArray())) {
+ assertEquals("Mismatched field values for " + copyFieldName,
+ specimenFieldValue, copyFieldValue);
+ }
+ });
+ });
+ }
+
+ @Test
+ public void testCustomParcelling_instanceIsCached() {
+ parcelAndUnparcel(mSpecimen, SampleDataClass.CREATOR);
+ parcelAndUnparcel(mSpecimen, SampleDataClass.CREATOR);
+ assertEquals(1, DateParcelling.sInstanceCount.get());
+ }
+
+ @Test
+ public void testDefaultFieldValue_isPropagated() {
+ assertEquals(new Date(42 * 42), mSpecimen.getDate());
+ }
+
+ @Test
+ public void testForEachField_avoidsBoxing() {
+ AtomicInteger intFieldCount = new AtomicInteger(0);
+ mSpecimen.forEachField(
+ (self, name, intValue) -> intFieldCount.getAndIncrement(),
+ (self, name, objectValue) -> {
+ if (objectValue != null) {
+ assertThat("Boxed field " + name,
+ objectValue, not(instanceOf(Integer.class)));
+ }
+ });
+ assertThat(intFieldCount.get(), greaterThanOrEqualTo(1));
+ }
+
+ @Test
+ public void testToString_containsEachField() {
+ String toString = mSpecimen.toString();
+
+ mSpecimen.forEachField((self, name, value) -> {
+ assertThat(toString, containsString(name));
+ if (value instanceof Integer) {
+ // Could be flags, their special toString tested separately
+ } else if (value instanceof Object[]) {
+ assertThat(toString, containsString(Arrays.toString((Object[]) value)));
+ } else if (value != null && value.getClass().isArray()) {
+ // Primitive array, uses multiple specialized Arrays.toString overloads
+ } else {
+ assertThat(toString, containsString("" + value));
+ }
+ });
+ }
+
+ @Test
+ public void testBuilder_propagatesValuesToInstance() {
+ assertEquals(43, newBuilder().setNum(43).build().getNum());
+ }
+
+ @Test
+ public void testPluralFields_canHaveCustomSingularBuilderName() {
+ newBuilder().addLinkAddress(new LinkAddress("127.0.0.1/24"));
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testBuilder_usableOnlyOnce() {
+ SampleDataClass.Builder builder = newBuilder();
+ builder.build();
+ builder.build();
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testBuilder_throwsWhenRequiredFieldMissing() {
+ newIncompleteBuilder().build();
+ }
+
+ @Test
+ public void testIntDefs_haveCorrectToString() {
+ int flagsAsInt = SampleDataClass.FLAG_MANUAL_REQUEST
+ | SampleDataClass.FLAG_COMPATIBILITY_MODE_REQUEST;
+ String flagsAsString = SampleDataClass.requestFlagsToString(flagsAsInt);
+
+ assertThat(flagsAsString, containsString("MANUAL_REQUEST"));
+ assertThat(flagsAsString, containsString("COMPATIBILITY_MODE_REQUEST"));
+ assertThat(flagsAsString, not(containsString("1")));
+ assertThat(flagsAsString, not(containsString("" + flagsAsInt)));
+
+ String dataclassToString = newBuilder()
+ .setFlags(flagsAsInt)
+ .setState(SampleDataClass.STATE_UNDEFINED)
+ .build()
+ .toString();
+ assertThat(dataclassToString, containsString(flagsAsString));
+ assertThat(dataclassToString, containsString("UNDEFINED"));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testFlags_getValidated() {
+ newBuilder().setFlags(12345).build();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testIntEnums_getValidated() {
+ newBuilder().setState(12345).build();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testStringEnums_getValidated() {
+ newBuilder().setStateName("foo").build();
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testCustomValidation_isTriggered() {
+ newBuilder().setNum2(-1).setNum4(1).build();
+ }
+
+ @Test
+ public void testLazyInit_isLazilyCalledOnce() {
+ assertNull(mSpecimen.mTmpStorage);
+
+ int[] tmpStorage = mSpecimen.getTmpStorage();
+ assertNotNull(tmpStorage);
+ assertSame(tmpStorage, mSpecimen.mTmpStorage);
+
+ int[] tmpStorageAgain = mSpecimen.getTmpStorage();
+ assertSame(tmpStorage, tmpStorageAgain);
+ }
+
+ private static <T extends Parcelable> T parcelAndUnparcel(
+ T original, Parcelable.Creator<T> creator) {
+ Parcel p = Parcel.obtain();
+ try {
+ original.writeToParcel(p, 0);
+ p.setDataPosition(0);
+ return creator.createFromParcel(p);
+ } finally {
+ p.recycle();
+ }
+ }
+}
diff --git a/tools/codegen/.gitignore b/tools/codegen/.gitignore
new file mode 100755
index 0000000..9fb18b4
--- /dev/null
+++ b/tools/codegen/.gitignore
@@ -0,0 +1,2 @@
+.idea
+out
diff --git a/tools/codegen/Android.bp b/tools/codegen/Android.bp
new file mode 100644
index 0000000..805b296
--- /dev/null
+++ b/tools/codegen/Android.bp
@@ -0,0 +1,18 @@
+java_binary_host {
+ name: "codegen",
+ manifest: "manifest.txt",
+ srcs: [
+ "src/**/*.kt",
+ ],
+ static_libs: [
+ "javaparser",
+ ],
+}
+
+java_library_host {
+ name: "codegen-version-info",
+
+ srcs: [
+ "src/**/SharedConstants.kt",
+ ],
+}
diff --git a/tools/codegen/OWNERS b/tools/codegen/OWNERS
new file mode 100644
index 0000000..da723b3
--- /dev/null
+++ b/tools/codegen/OWNERS
@@ -0,0 +1 @@
[email protected]
\ No newline at end of file
diff --git a/tools/codegen/manifest.txt b/tools/codegen/manifest.txt
new file mode 100644
index 0000000..6e1018b
--- /dev/null
+++ b/tools/codegen/manifest.txt
@@ -0,0 +1 @@
+Main-class: com.android.codegen.MainKt
diff --git a/tools/codegen/src/com/android/codegen/ClassInfo.kt b/tools/codegen/src/com/android/codegen/ClassInfo.kt
new file mode 100644
index 0000000..7ee79f6
--- /dev/null
+++ b/tools/codegen/src/com/android/codegen/ClassInfo.kt
@@ -0,0 +1,49 @@
+package com.android.codegen
+
+import com.github.javaparser.JavaParser
+import com.github.javaparser.ParseProblemException
+import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration
+
+open class ClassInfo(val sourceLines: List<String>) {
+
+ private val userSourceCode = (sourceLines + "}").joinToString("\n")
+ val fileAst = try {
+ JavaParser.parse(userSourceCode)!!
+ } catch (e: ParseProblemException) {
+ throw RuntimeException("Failed to parse code:\n" +
+ userSourceCode
+ .lines()
+ .mapIndexed { lnNum, ln -> "/*$lnNum*/$ln" }
+ .joinToString("\n"),
+ e)
+ }
+ val classAst = fileAst.types[0] as ClassOrInterfaceDeclaration
+
+ fun hasMethod(name: String, vararg argTypes: String): Boolean {
+ return classAst.methods.any {
+ it.name.asString() == name &&
+ it.parameters.map { it.type.asString() } == argTypes.toList()
+ }
+ }
+
+ val superInterfaces = (fileAst.types[0] as ClassOrInterfaceDeclaration)
+ .implementedTypes.map { it.asString() }
+
+ val ClassName = classAst.nameAsString
+ private val genericArgsAst = classAst.typeParameters
+ val genericArgs = if (genericArgsAst.isEmpty()) "" else {
+ genericArgsAst.map { it.nameAsString }.joinToString(", ").let { "<$it>" }
+ }
+ val ClassType = ClassName + genericArgs
+
+ val constDefs = mutableListOf<ConstDef>()
+
+ val fields = classAst.fields
+ .filterNot { it.isTransient || it.isStatic }
+ .mapIndexed { i, node -> FieldInfo(index = i, fieldAst = node, classInfo = this) }
+ .apply { lastOrNull()?.isLast = true }
+ val lazyTransientFields = classAst.fields
+ .filter { it.isTransient && !it.isStatic }
+ .mapIndexed { i, node -> FieldInfo(index = i, fieldAst = node, classInfo = this) }
+ .filter { hasMethod("lazyInit${it.NameUpperCamel}") }
+}
\ No newline at end of file
diff --git a/tools/codegen/src/com/android/codegen/ClassPrinter.kt b/tools/codegen/src/com/android/codegen/ClassPrinter.kt
new file mode 100644
index 0000000..33256b7
--- /dev/null
+++ b/tools/codegen/src/com/android/codegen/ClassPrinter.kt
@@ -0,0 +1,311 @@
+package com.android.codegen
+
+import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration
+import com.github.javaparser.ast.body.TypeDeclaration
+import com.github.javaparser.ast.expr.BooleanLiteralExpr
+import com.github.javaparser.ast.expr.NormalAnnotationExpr
+import com.github.javaparser.ast.type.ClassOrInterfaceType
+
+/**
+ * [ClassInfo] + utilities for printing out new class code with proper indentation and imports
+ */
+class ClassPrinter(
+ source: List<String>,
+ private val stringBuilder: StringBuilder,
+ var cliArgs: Array<String>
+) : ClassInfo(source) {
+
+ val GENERATED_MEMBER_HEADER by lazy { "@$GeneratedMember" }
+
+ // Imports
+ val NonNull by lazy { classRef("android.annotation.NonNull") }
+ val NonEmpty by lazy { classRef("android.annotation.NonEmpty") }
+ val Nullable by lazy { classRef("android.annotation.Nullable") }
+ val TextUtils by lazy { classRef("android.text.TextUtils") }
+ val LinkedHashMap by lazy { classRef("java.util.LinkedHashMap") }
+ val Collections by lazy { classRef("java.util.Collections") }
+ val Preconditions by lazy { classRef("com.android.internal.util.Preconditions") }
+ val ArrayList by lazy { classRef("java.util.ArrayList") }
+ val DataClass by lazy { classRef("com.android.internal.util.DataClass") }
+ val DataClassEnum by lazy { classRef("com.android.internal.util.DataClass.Enum") }
+ val ParcelWith by lazy { classRef("com.android.internal.util.DataClass.ParcelWith") }
+ val PluralOf by lazy { classRef("com.android.internal.util.DataClass.PluralOf") }
+ val Each by lazy { classRef("com.android.internal.util.DataClass.Each") }
+ val DataClassGenerated by lazy { classRef("com.android.internal.util.DataClass.Generated") }
+ val GeneratedMember by lazy { classRef("com.android.internal.util.DataClass.Generated.Member") }
+ val Parcelling by lazy { classRef("com.android.internal.util.Parcelling") }
+ val UnsupportedAppUsage by lazy { classRef("android.annotation.UnsupportedAppUsage") }
+
+
+ /**
+ * Optionally shortens a class reference if there's a corresponding import present
+ */
+ fun classRef(fullName: String): String {
+ if (cliArgs.contains(FLAG_NO_FULL_QUALIFIERS)) {
+ return fullName.split(".").dropWhile { it[0].isLowerCase() }.joinToString(".")
+ }
+
+ val pkg = fullName.substringBeforeLast(".")
+ val simpleName = fullName.substringAfterLast(".")
+ if (fileAst.imports.any { imprt ->
+ imprt.nameAsString == fullName
+ || (imprt.isAsterisk && imprt.nameAsString == pkg)
+ }) {
+ return simpleName
+ } else {
+ val outerClass = pkg.substringAfterLast(".", "")
+ if (outerClass.firstOrNull()?.isUpperCase() ?: false) {
+ return classRef(pkg) + "." + simpleName
+ }
+ }
+ return fullName
+ }
+
+ /** @see classRef */
+ inline fun <reified T : Any> classRef(): String {
+ return classRef(T::class.java.name)
+ }
+
+ /** @see classRef */
+ fun memberRef(fullName: String): String {
+ val className = fullName.substringBeforeLast(".")
+ val methodName = fullName.substringAfterLast(".")
+ return if (fileAst.imports.any {
+ it.isStatic
+ && (it.nameAsString == fullName
+ || (it.isAsterisk && it.nameAsString == className))
+ }) {
+ className.substringAfterLast(".") + "." + methodName
+ } else {
+ classRef(className) + "." + methodName
+ }
+ }
+
+ val dataClassAnnotationFeatures = classAst.annotations
+ .find { it.nameAsString == DataClass }
+ ?.let { it as? NormalAnnotationExpr }
+ ?.pairs
+ ?.map { pair -> pair.nameAsString to (pair.value as BooleanLiteralExpr).value }
+ ?.toMap()
+ ?: emptyMap()
+
+ val internalAnnotations = setOf(ParcelWith, DataClassEnum, PluralOf, Each, UnsupportedAppUsage)
+
+ /**
+ * @return whether the given feature is enabled
+ */
+ operator fun FeatureFlag.invoke(): Boolean {
+ if (cliArgs.contains("--no-$kebabCase")) return false
+ if (cliArgs.contains("--$kebabCase")) return true
+
+ val annotationKey = "gen$upperCamelCase"
+ if (dataClassAnnotationFeatures.containsKey(annotationKey)) {
+ return dataClassAnnotationFeatures[annotationKey]!!
+ }
+
+ if (cliArgs.contains("--all")) return true
+ if (hidden) return true
+
+ return when (this) {
+ FeatureFlag.SETTERS ->
+ !FeatureFlag.CONSTRUCTOR() && !FeatureFlag.BUILDER() && fields.any { !it.isFinal }
+ FeatureFlag.BUILDER -> cliArgs.contains(FLAG_BUILDER_PROTECTED_SETTERS) || onByDefault
+ FeatureFlag.CONSTRUCTOR -> !FeatureFlag.BUILDER()
+ FeatureFlag.PARCELABLE -> "Parcelable" in superInterfaces
+ FeatureFlag.AIDL -> FeatureFlag.PARCELABLE()
+ FeatureFlag.IMPLICIT_NONNULL -> fields.any { it.isNullable }
+ && fields.none { "@$NonNull" in it.annotations }
+ else -> onByDefault
+ }
+ }
+
+ val FeatureFlag.hidden
+ get(): Boolean = when {
+ cliArgs.contains("--hidden-$kebabCase") -> true
+ this == FeatureFlag.BUILD_UPON -> FeatureFlag.BUILDER.hidden
+ else -> false
+ }
+
+ var currentIndent = INDENT_SINGLE
+ private set
+
+ fun pushIndent() {
+ currentIndent += INDENT_SINGLE
+ }
+
+ fun popIndent() {
+ currentIndent = currentIndent.substring(0, currentIndent.length - INDENT_SINGLE.length)
+ }
+
+ fun backspace() = stringBuilder.setLength(stringBuilder.length - 1)
+ val lastChar get() = stringBuilder[stringBuilder.length - 1]
+
+ private fun appendRaw(s: String) {
+ stringBuilder.append(s)
+ }
+
+ fun append(s: String) {
+ if (s.isBlank() && s != "\n") {
+ appendRaw(s)
+ } else {
+ appendRaw(s.lines().map { line ->
+ if (line.startsWith(" *")) line else line.trimStart()
+ }.joinToString("\n$currentIndent"))
+ }
+ }
+
+ fun appendSameLine(s: String) {
+ while (lastChar.isWhitespace() || lastChar.isNewline()) {
+ backspace()
+ }
+ appendRaw(s)
+ }
+
+ fun rmEmptyLine() {
+ while (lastChar.isWhitespaceNonNewline()) backspace()
+ if (lastChar.isNewline()) backspace()
+ }
+
+ /**
+ * Syntactic sugar for:
+ * ```
+ * +"code()";
+ * ```
+ * to append the given string plus a newline
+ */
+ operator fun String.unaryPlus() = append("$this\n")
+
+ /**
+ * Syntactic sugar for:
+ * ```
+ * !"code()";
+ * ```
+ * to append the given string without a newline
+ */
+ operator fun String.not() = append(this)
+
+ /**
+ * Syntactic sugar for:
+ * ```
+ * ... {
+ * ...
+ * }+";"
+ * ```
+ * to append a ';' on same line after a block, and a newline afterwards
+ */
+ operator fun Unit.plus(s: String) {
+ appendSameLine(s)
+ +""
+ }
+
+ /**
+ * A multi-purpose syntactic sugar for appending the given string plus anything generated in
+ * the given [block], the latter with the appropriate deeper indent,
+ * and resetting the indent back to original at the end
+ *
+ * Usage examples:
+ *
+ * ```
+ * "if (...)" {
+ * ...
+ * }
+ * ```
+ * to append a corresponding if block appropriate indentation
+ *
+ * ```
+ * "void foo(...)" {
+ * ...
+ * }
+ * ```
+ * similar to the previous one, plus an extra empty line after the function body
+ *
+ * ```
+ * "void foo(" {
+ * <args code>
+ * }
+ * ```
+ * to use proper indentation for args code and close the bracket on same line at end
+ *
+ * ```
+ * "..." {
+ * ...
+ * }
+ * to use the correct indentation for inner code, resetting it at the end
+ */
+ inline operator fun String.invoke(block: ClassPrinter.() -> Unit) {
+ if (this == " {") {
+ appendSameLine(this)
+ } else {
+ append(this)
+ }
+ when {
+ endsWith("(") -> {
+ indentedBy(2, block)
+ appendSameLine(")")
+ }
+ endsWith("{") || endsWith(")") -> {
+ if (!endsWith("{")) appendSameLine(" {")
+ indentedBy(1, block)
+ +"}"
+ if ((endsWith(") {") || endsWith(")") || this == " {")
+ && !startsWith("synchronized")
+ && !startsWith("switch")
+ && !startsWith("if ")
+ && !contains(" else ")
+ && !contains("new ")
+ && !contains("return ")) {
+ +"" // extra line after function definitions
+ }
+ }
+ else -> indentedBy(2, block)
+ }
+ }
+
+ inline fun indentedBy(level: Int, block: ClassPrinter.() -> Unit) {
+ append("\n")
+ level times {
+ append(INDENT_SINGLE)
+ pushIndent()
+ }
+ block()
+ level times { popIndent() }
+ rmEmptyLine()
+ +""
+ }
+
+ inline fun Iterable<FieldInfo>.forEachTrimmingTrailingComma(b: FieldInfo.() -> Unit) {
+ forEachApply {
+ b()
+ if (isLast) {
+ while (lastChar == ' ' || lastChar == '\n') backspace()
+ if (lastChar == ',') backspace()
+ }
+ }
+ }
+
+ inline operator fun <R> invoke(f: ClassPrinter.() -> R): R = run(f)
+
+ var BuilderClass = CANONICAL_BUILDER_CLASS
+ var BuilderType = BuilderClass + genericArgs
+
+ init {
+ val builderFactoryOverride = classAst.methods.find {
+ it.isStatic && it.nameAsString == "builder"
+ }
+ if (builderFactoryOverride != null) {
+ BuilderClass = (builderFactoryOverride.type as ClassOrInterfaceType).nameAsString
+ BuilderType = builderFactoryOverride.type.asString()
+ } else {
+ val builderExtension = (fileAst.types
+ + classAst.childNodes.filterIsInstance(TypeDeclaration::class.java)).find {
+ it.nameAsString == CANONICAL_BUILDER_CLASS
+ }
+ if (builderExtension != null) {
+ BuilderClass = GENERATED_BUILDER_CLASS
+ val tp = (builderExtension as ClassOrInterfaceDeclaration).typeParameters
+ BuilderType = if (tp.isEmpty()) BuilderClass
+ else "$BuilderClass<${tp.map { it.nameAsString }.joinToString(", ")}>"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tools/codegen/src/com/android/codegen/ConstDef.kt b/tools/codegen/src/com/android/codegen/ConstDef.kt
new file mode 100644
index 0000000..f559d6f
--- /dev/null
+++ b/tools/codegen/src/com/android/codegen/ConstDef.kt
@@ -0,0 +1,17 @@
+package com.android.codegen
+
+import com.github.javaparser.ast.body.FieldDeclaration
+
+/**
+ * `@IntDef` or `@StringDef`
+ */
+data class ConstDef(val type: Type, val AnnotationName: String, val values: List<FieldDeclaration>) {
+
+ enum class Type {
+ INT, INT_FLAGS, STRING;
+
+ val isInt get() = this == INT || this == INT_FLAGS
+ }
+
+ val CONST_NAMES get() = values.flatMap { it.variables }.map { it.nameAsString }
+}
\ No newline at end of file
diff --git a/tools/codegen/src/com/android/codegen/FeatureFlag.kt b/tools/codegen/src/com/android/codegen/FeatureFlag.kt
new file mode 100644
index 0000000..24150d6
--- /dev/null
+++ b/tools/codegen/src/com/android/codegen/FeatureFlag.kt
@@ -0,0 +1,27 @@
+package com.android.codegen
+
+
+/**
+ * See also [ClassPrinter.invoke] for more default flag values resolution rules
+ */
+enum class FeatureFlag(val onByDefault: Boolean, val desc: String = "") {
+ PARCELABLE(false, "implement Parcelable contract"),
+ AIDL(false, "generate a 'parcelable declaration' .aidl file alongside"),
+ CONSTRUCTOR(true, "an all-argument constructor"),
+ BUILDER(false, "e.g. MyClass.builder().setFoo(..).build();"),
+ GETTERS(true, "getters, e.g. getFoo()"),
+ SETTERS(false, "chainable/fluent setters, e.g. setFoo(..).setBar(..)"),
+ WITHERS(false, "'immutable setters' returning a new instance, " +
+ "e.g. newFoo = foo.withBar(barValue)"),
+ EQUALS_HASH_CODE(false, "equals + hashCode based on fields"),
+ TO_STRING(false, "toString based on fields"),
+ BUILD_UPON(false, "builder factory from existing instance, " +
+ "e.g. instance.buildUpon().setFoo(..).build()"),
+ IMPLICIT_NONNULL(true, "treat lack of @Nullable as @NonNull for Object fields"),
+ COPY_CONSTRUCTOR(false, "a constructor for an instance identical to the given one"),
+ CONST_DEFS(true, "@Int/StringDef's based on declared static constants"),
+ FOR_EACH_FIELD(false, "forEachField((name, value) -> ...)");
+
+ val kebabCase = name.toLowerCase().replace("_", "-")
+ val upperCamelCase = name.split("_").map { it.toLowerCase().capitalize() }.joinToString("")
+}
diff --git a/tools/codegen/src/com/android/codegen/FieldInfo.kt b/tools/codegen/src/com/android/codegen/FieldInfo.kt
new file mode 100644
index 0000000..f326fd5
--- /dev/null
+++ b/tools/codegen/src/com/android/codegen/FieldInfo.kt
@@ -0,0 +1,216 @@
+package com.android.codegen
+
+import com.github.javaparser.JavaParser
+import com.github.javaparser.ast.body.FieldDeclaration
+import com.github.javaparser.ast.expr.ClassExpr
+import com.github.javaparser.ast.expr.Name
+import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr
+import com.github.javaparser.ast.expr.StringLiteralExpr
+import com.github.javaparser.ast.type.ArrayType
+import com.github.javaparser.ast.type.ClassOrInterfaceType
+import com.github.javaparser.javadoc.Javadoc
+import java.lang.Long
+
+data class FieldInfo(
+ val index: Int,
+ val fieldAst: FieldDeclaration,
+ private val classInfo: ClassInfo
+) {
+
+ val classPrinter = classInfo as ClassPrinter
+
+ // AST
+ internal val variableAst = fieldAst.variables[0]
+ val typeAst = variableAst.type
+
+ // Field type
+ val Type = typeAst.asString()
+ val FieldClass = Type.takeWhile { it != '<' }
+ val isPrimitive = Type in PRIMITIVE_TYPES
+
+ // Javadoc
+ val javadoc: Javadoc? = fieldAst.javadoc.orElse(null)
+ private val javadocText = javadoc?.toText()?.let {
+ // Workaround for a bug in Javaparser for javadocs starting with {
+ if (it.hasUnbalancedCurlyBrace()) "{$it" else it
+ }
+ val javadocTextNoAnnotationLines = javadocText
+ ?.lines()
+ ?.dropLastWhile { it.startsWith("@") || it.isBlank() }
+ ?.let { if (it.isEmpty()) null else it }
+ val javadocFull = javadocText
+ ?.trimBlankLines()
+ ?.mapLines { " * $this" }
+ ?.let { "/**\n$it\n */" }
+
+
+ // Field name
+ val name = variableAst.name.asString()!!
+ private val isNameHungarian = name[0] == 'm' && name[1].isUpperCase()
+ val NameUpperCamel = if (isNameHungarian) name.substring(1) else name.capitalize()
+ val nameLowerCamel = if (isNameHungarian) NameUpperCamel.decapitalize() else name
+ val _name = if (name != nameLowerCamel) nameLowerCamel else "_$nameLowerCamel"
+ val SingularNameOrNull by lazy {
+ classPrinter {
+ fieldAst.annotations
+ .find { it.nameAsString == PluralOf }
+ ?.let { it as? SingleMemberAnnotationExpr }
+ ?.memberValue
+ ?.let { it as? StringLiteralExpr }
+ ?.value
+ ?.toLowerCamel()
+ ?.capitalize()
+ }
+ }
+ val SingularName by lazy { SingularNameOrNull ?: NameUpperCamel }
+
+
+ // Field value
+ val mayBeNull: Boolean
+ get() = when {
+ isPrimitive -> false
+ "@${classPrinter.NonNull}" in annotations -> false
+ "@${classPrinter.NonEmpty}" in annotations -> false
+ isNullable -> true
+ lazyInitializer != null -> true
+ else -> classPrinter { !FeatureFlag.IMPLICIT_NONNULL() }
+ }
+ val lazyInitializer
+ get() = classInfo.classAst.methods.find { method ->
+ method.nameAsString == "lazyInit$NameUpperCamel" && method.parameters.isEmpty()
+ }?.nameAsString
+ val internalGetter get() = if (lazyInitializer != null) "get$NameUpperCamel()" else name
+ val defaultExpr: Any?
+ get() {
+ variableAst.initializer.orElse(null)?.let { return it }
+ classInfo.classAst.methods.find {
+ it.nameAsString == "default$NameUpperCamel" && it.parameters.isEmpty()
+ }?.run { "$nameAsString()" }?.let { return it }
+ if (FieldClass == "List") return "${classPrinter.memberRef("java.util.Collections.emptyList")}()"
+ return null
+ }
+ val hasDefault get() = defaultExpr != null
+
+
+ // Generic args
+ val isArray = Type.endsWith("[]")
+ val isList = FieldClass == "List" || FieldClass == "ArrayList"
+ val fieldBit = "0x${Long.toHexString(1L shl index)}"
+ var isLast = false
+ val isFinal = fieldAst.isFinal
+ val fieldTypeGenegicArgs = when (typeAst) {
+ is ArrayType -> listOf(fieldAst.elementType.asString())
+ is ClassOrInterfaceType -> {
+ typeAst.typeArguments.orElse(null)?.map { it.asString() } ?: emptyList()
+ }
+ else -> emptyList()
+ }
+ val FieldInnerType = fieldTypeGenegicArgs.firstOrNull()
+ val FieldInnerClass = FieldInnerType?.takeWhile { it != '<' }
+
+
+ // Annotations
+ var intOrStringDef = null as ConstDef?
+ val annotations by lazy {
+ if (FieldClass in BUILTIN_SPECIAL_PARCELLINGS) {
+ classPrinter {
+ fieldAst.addAnnotation(SingleMemberAnnotationExpr(
+ Name(ParcelWith),
+ ClassExpr(JavaParser.parseClassOrInterfaceType(
+ "$Parcelling.BuiltIn.For$FieldClass"))))
+ }
+ }
+ fieldAst.annotations.map { it.removeComment().toString() }
+ }
+ val annotationsNoInternal by lazy {
+ annotations.filterNot { ann ->
+ classPrinter {
+ internalAnnotations.any {
+ it in ann
+ }
+ }
+ }
+ }
+
+ fun hasAnnotation(a: String) = annotations.any { it.startsWith(a) }
+ val isNullable by lazy { hasAnnotation("@Nullable") }
+ val isNonEmpty by lazy { hasAnnotation("@${classPrinter.NonEmpty}") }
+ val customParcellingClass by lazy {
+ fieldAst.annotations.find { it.nameAsString == classPrinter.ParcelWith }
+ ?.singleArgAs<ClassExpr>()
+ ?.type
+ ?.asString()
+ }
+ val annotationsAndType by lazy { (annotationsNoInternal + Type).joinToString(" ") }
+ val sParcelling by lazy { customParcellingClass?.let { "sParcellingFor$NameUpperCamel" } }
+ val annotatedTypeForSetterParam by lazy {
+ (annotationsNoInternal + if (isArray) "$FieldInnerType..." else Type).joinToString(" ")
+ }
+
+ // Utilities
+
+ /**
+ * `mFoo.size()`
+ */
+ val ClassPrinter.sizeExpr get() = when {
+ isArray && FieldInnerClass !in PRIMITIVE_TYPES ->
+ memberRef("com.android.internal.util.ArrayUtils.size") + "($name)"
+ isArray -> "$name.length"
+ listOf("List", "Set", "Map").any { FieldClass.endsWith(it) } ->
+ memberRef("com.android.internal.util.CollectionUtils.size") + "($name)"
+ Type == "String" -> memberRef("android.text.TextUtils.length") + "($name)"
+ Type == "CharSequence" -> "$name.length()"
+ else -> "$name.size()"
+ }
+ /**
+ * `mFoo.get(0)`
+ */
+ fun elemAtIndexExpr(indexExpr: String) = when {
+ isArray -> "$name[$indexExpr]"
+ FieldClass == "ArraySet" -> "$name.valueAt($indexExpr)"
+ else -> "$name.get($indexExpr)"
+ }
+ /**
+ * `mFoo.isEmpty()`
+ */
+ val ClassPrinter.isEmptyExpr get() = when {
+ isArray || Type == "CharSequence" -> "$sizeExpr == 0"
+ else -> "$name.isEmpty()"
+ }
+
+ /**
+ * `mFoo == that` or `Objects.equals(mFoo, that)`, etc.
+ */
+ fun ClassPrinter.isEqualToExpr(that: String) = when {
+ Type in PRIMITIVE_TYPES -> "$internalGetter == $that"
+ isArray -> "${memberRef("java.util.Arrays.equals")}($internalGetter, $that)"
+ else -> "${memberRef("java.util.Objects.equals")}($internalGetter, $that)"
+ }
+
+ /**
+ * Parcel.write* and Parcel.read* method name wildcard values
+ */
+ val ParcelMethodsSuffix = when {
+ FieldClass in PRIMITIVE_TYPES - "char" - "boolean" +
+ listOf("String", "CharSequence", "Exception", "Size", "SizeF", "Bundle",
+ "FileDescriptor", "SparseBooleanArray", "SparseIntArray", "SparseArray") ->
+ FieldClass
+ FieldClass == "Map" && fieldTypeGenegicArgs[0] == "String" -> "Map"
+ isArray -> when {
+ FieldInnerType!! in (PRIMITIVE_TYPES + "String") -> FieldInnerType + "Array"
+ isBinder(FieldInnerType) -> "BinderArray"
+ else -> "TypedArray"
+ }
+ isList -> when {
+ FieldInnerType == "String" -> "StringList"
+ isBinder(FieldInnerType!!) -> "BinderList"
+ else -> "ParcelableList"
+ }
+ isIInterface(Type) -> "StrongInterface"
+ isBinder(Type) -> "StrongBinder"
+ else -> "TypedObject"
+ }.capitalize()
+
+ private fun isBinder(type: String) = type == "Binder" || type == "IBinder" || isIInterface(type)
+ private fun isIInterface(type: String) = type.length >= 2 && type[0] == 'I' && type[1].isUpperCase()
+}
\ No newline at end of file
diff --git a/tools/codegen/src/com/android/codegen/Generators.kt b/tools/codegen/src/com/android/codegen/Generators.kt
new file mode 100644
index 0000000..ab64f4e
--- /dev/null
+++ b/tools/codegen/src/com/android/codegen/Generators.kt
@@ -0,0 +1,847 @@
+package com.android.codegen
+
+import com.github.javaparser.ast.body.FieldDeclaration
+import com.github.javaparser.ast.body.VariableDeclarator
+import com.github.javaparser.ast.expr.*
+import java.io.File
+
+
+/**
+ * IntDefs and StringDefs based on constants
+ */
+fun ClassPrinter.generateConstDefs() {
+ val consts = classAst.fields.filter {
+ it.isStatic && it.isFinal && it.variables.all { variable ->
+ val initializer = variable.initializer.orElse(null)
+ val isLiteral = initializer is LiteralExpr
+ || (initializer is UnaryExpr && initializer.expression is LiteralExpr)
+ isLiteral && variable.type.asString() in listOf("int", "String")
+ }
+ }.flatMap { field -> field.variables.map { it to field } }
+ val intConsts = consts.filter { it.first.type.asString() == "int" }
+ val strConsts = consts.filter { it.first.type.asString() == "String" }
+ val intGroups = intConsts.groupBy { it.first.nameAsString.split("_")[0] }.values
+ val strGroups = strConsts.groupBy { it.first.nameAsString.split("_")[0] }.values
+ intGroups.forEach {
+ generateConstDef(it)
+ }
+ strGroups.forEach {
+ generateConstDef(it)
+ }
+}
+
+fun ClassPrinter.generateConstDef(consts: List<Pair<VariableDeclarator, FieldDeclaration>>) {
+ if (consts.size <= 1) return
+
+ val names = consts.map { it.first.nameAsString!! }
+ val prefix = names
+ .reduce { a, b -> a.commonPrefixWith(b) }
+ .dropLastWhile { it != '_' }
+ .dropLast(1)
+ if (prefix.isEmpty()) {
+ println("Failed to generate const def for $names")
+ return
+ }
+ var AnnotationName = prefix.split("_")
+ .filterNot { it.isBlank() }
+ .map { it.toLowerCase().capitalize() }
+ .joinToString("")
+ val annotatedConst = consts.find { it.second.annotations.isNonEmpty }
+ if (annotatedConst != null) {
+ AnnotationName = annotatedConst.second.annotations.first().nameAsString
+ }
+ val type = consts[0].first.type.asString()
+ val flag = type == "int" && consts.all { it.first.initializer.get().toString().startsWith("0x") }
+ val constDef = ConstDef(type = when {
+ type == "String" -> ConstDef.Type.STRING
+ flag -> ConstDef.Type.INT_FLAGS
+ else -> ConstDef.Type.INT
+ },
+ AnnotationName = AnnotationName,
+ values = consts.map { it.second }
+ )
+ constDefs += constDef
+ fields.forEachApply {
+ if (fieldAst.annotations.any { it.nameAsString == AnnotationName }) {
+ this.intOrStringDef = constDef
+ }
+ }
+
+ val visibility = if (consts[0].second.isPublic) "public" else "/* package-*/"
+
+ val Retention = classRef("java.lang.annotation.Retention")
+ val RetentionPolicySource = memberRef("java.lang.annotation.RetentionPolicy.SOURCE")
+ val ConstDef = classRef("android.annotation.${type.capitalize()}Def")
+
+ "@$ConstDef(${if_(flag, "flag = true, ")}prefix = \"${prefix}_\", value = {" {
+ names.forEachLastAware { name, isLast ->
+ +"$name${if_(!isLast, ",")}"
+ }
+ } + ")"
+ +"@$Retention($RetentionPolicySource)"
+ +GENERATED_MEMBER_HEADER
+ +"$visibility @interface $AnnotationName {}"
+ +""
+
+ if (type == "int") {
+ +GENERATED_MEMBER_HEADER
+ val methodDefLine = "$visibility static String ${AnnotationName.decapitalize()}ToString(" +
+ "@$AnnotationName int value)"
+ if (flag) {
+ val flg2str = memberRef("com.android.internal.util.BitUtils.flagsToString")
+ methodDefLine {
+ "return $flg2str(" {
+ +"value, $ClassName::single${AnnotationName}ToString"
+ } + ";"
+ }
+ +GENERATED_MEMBER_HEADER
+ !"static String single${AnnotationName}ToString(@$AnnotationName int value)"
+ } else {
+ !methodDefLine
+ }
+ " {" {
+ "switch (value) {" {
+ names.forEach { name ->
+ "case $name:" {
+ +"return \"$name\";"
+ }
+ }
+ +"default: return Integer.toHexString(value);"
+ }
+ }
+ }
+}
+
+fun ClassPrinter.generateAidl(javaFile: File) {
+ val aidl = File(javaFile.path.substringBeforeLast(".java") + ".aidl")
+ if (aidl.exists()) return
+ aidl.writeText(buildString {
+ sourceLines.dropLastWhile { !it.startsWith("package ") }.forEach {
+ appendln(it)
+ }
+ append("\nparcelable $ClassName;\n")
+ })
+}
+
+/**
+ * ```
+ * Foo newFoo = oldFoo.withBar(newBar);
+ * ```
+ */
+fun ClassPrinter.generateWithers() {
+ fields.forEachApply {
+ val metodName = "with$NameUpperCamel"
+ if (!hasMethod(metodName, Type)) {
+ generateFieldJavadoc(forceHide = FeatureFlag.WITHERS.hidden)
+ """@$NonNull
+ $GENERATED_MEMBER_HEADER
+ public $ClassType $metodName($annotatedTypeForSetterParam value)""" {
+ val changedFieldName = name
+
+ "return new $ClassType(" {
+ fields.forEachTrimmingTrailingComma {
+ if (name == changedFieldName) +"value," else +"$name,"
+ }
+ } + ";"
+ }
+ }
+ }
+}
+
+fun ClassPrinter.generateCopyConstructor() {
+ if (classAst.constructors.any {
+ it.parameters.size == 1 &&
+ it.parameters[0].type.asString() == ClassType
+ }) {
+ return
+ }
+
+ +"/** Copy constructor */"
+ +GENERATED_MEMBER_HEADER
+ "public $ClassName(@$NonNull $ClassName orig)" {
+ fields.forEachApply {
+ +"$name = orig.$name;"
+ }
+ }
+}
+
+/**
+ * ```
+ * Foo newFoo = oldFoo.buildUpon().setBar(newBar).build();
+ * ```
+ */
+fun ClassPrinter.generateBuildUpon() {
+ if (hasMethod("buildUpon")) return
+
+ +"/**"
+ +" * Provides an instance of {@link $BuilderClass} with state corresponding to this instance."
+ if (FeatureFlag.BUILD_UPON.hidden) {
+ +" * @hide"
+ }
+ +" */"
+ +GENERATED_MEMBER_HEADER
+ "public $BuilderType buildUpon()" {
+ "return new $BuilderType()" {
+ fields.forEachApply {
+ +".set$NameUpperCamel($internalGetter)"
+ } + ";"
+ }
+ }
+}
+
+fun ClassPrinter.generateBuilder() {
+ val setterVisibility = if (cliArgs.contains(FLAG_BUILDER_PROTECTED_SETTERS))
+ "protected" else "public"
+ val constructorVisibility = if (BuilderClass == CANONICAL_BUILDER_CLASS)
+ "public" else "/* package-*/"
+
+ val OneTimeUseBuilder = classRef("android.provider.OneTimeUseBuilder")
+
+ +"/**"
+ +" * A builder for {@link $ClassName}"
+ if (FeatureFlag.BUILDER.hidden) +" * @hide"
+ +" */"
+ +"@SuppressWarnings(\"WeakerAccess\")"
+ +GENERATED_MEMBER_HEADER
+ "public static class $BuilderClass$genericArgs" {
+ +"extends $OneTimeUseBuilder<$ClassType>"
+ }
+ " {" {
+
+ +""
+ fields.forEachApply {
+ +"protected $annotationsAndType $name;"
+ }
+ +""
+ +"protected long mBuilderFieldsSet = 0L;"
+ +""
+ +"$constructorVisibility $BuilderClass() {};"
+ +""
+
+ generateBuilderSetters(setterVisibility)
+
+ generateBuilderBuild()
+
+ rmEmptyLine()
+ }
+}
+
+private fun ClassPrinter.generateBuilderSetters(visibility: String) {
+
+ fields.forEachApply {
+ val maybeCast =
+ if_(BuilderClass != CANONICAL_BUILDER_CLASS, " ($CANONICAL_BUILDER_CLASS)")
+
+ generateFieldJavadoc()
+ +GENERATED_MEMBER_HEADER
+ "$visibility $CANONICAL_BUILDER_CLASS set$NameUpperCamel($annotatedTypeForSetterParam value)" {
+ +"checkNotUsed();"
+ +"mBuilderFieldsSet |= $fieldBit;"
+ +"$name = value;"
+ +"return$maybeCast this;"
+ }
+
+
+ val javadocSeeSetter = "/** @see #set$NameUpperCamel */"
+ val singularNameCustomizationHint = if (SingularNameOrNull == null) {
+ "// You can refine this method's name by providing item's singular name, e.g.:\n" +
+ "// @DataClass.PluralOf(\"item\")) mItems = ...\n\n"
+ } else ""
+
+ if (isList && FieldInnerType != null) {
+
+ +javadocSeeSetter
+ +GENERATED_MEMBER_HEADER
+ "$visibility $CANONICAL_BUILDER_CLASS add$SingularName(@$NonNull $FieldInnerType value)" {
+ !singularNameCustomizationHint
+ +"if ($name == null) set$NameUpperCamel(new $ArrayList<>());"
+ +"$name.add(value);"
+ +"return$maybeCast this;"
+ }
+ }
+
+ if (Type.contains("Map<")) {
+ val (Key, Value) = fieldTypeGenegicArgs
+
+ +javadocSeeSetter
+ +GENERATED_MEMBER_HEADER
+ "$visibility $CANONICAL_BUILDER_CLASS add$SingularName($Key key, $Value value)" {
+ !singularNameCustomizationHint
+ +"if ($name == null) set$NameUpperCamel(new $LinkedHashMap());"
+ +"$name.put(key, value);"
+ +"return$maybeCast this;"
+ }
+ }
+
+ if (Type == "boolean") {
+ +javadocSeeSetter
+ +GENERATED_MEMBER_HEADER
+ "$visibility $CANONICAL_BUILDER_CLASS mark$NameUpperCamel()" {
+ +"return set$NameUpperCamel(true);"
+ }
+
+ +javadocSeeSetter
+ +GENERATED_MEMBER_HEADER
+ "$visibility $CANONICAL_BUILDER_CLASS markNot$NameUpperCamel()" {
+ +"return set$NameUpperCamel(false);"
+ }
+ }
+ }
+}
+
+private fun ClassPrinter.generateBuilderBuild() {
+ +"/** Builds the instance. This builder should not be touched after calling this! */"
+ "public $ClassType build()" {
+ +"markUsed();"
+ fields.forEachApply {
+ if (!isNullable || hasDefault) {
+ "if ((mBuilderFieldsSet & $fieldBit) == 0)" {
+ if (!isNullable && !hasDefault) {
+ +"throw new IllegalStateException(\"Required field not set: $nameLowerCamel\");"
+ } else {
+ +"$name = $defaultExpr;"
+ }
+ }
+ }
+ }
+ "$ClassType o = new $ClassType(" {
+ fields.forEachTrimmingTrailingComma {
+ +"$name,"
+ }
+ } + ";"
+ +"return o;"
+ }
+}
+
+fun ClassPrinter.generateParcelable() {
+ val booleanFields = fields.filter { it.Type == "boolean" }
+ val objectFields = fields.filter { it.Type !in PRIMITIVE_TYPES }
+ val nullableFields = objectFields.filter { it.mayBeNull }
+ val nonBooleanFields = fields - booleanFields
+
+
+ val flagStorageType = when (fields.size) {
+ in 0..7 -> "byte"
+ in 8..15 -> "int"
+ in 16..31 -> "long"
+ else -> throw NotImplementedError("32+ field classes not yet supported")
+ }
+ val FlagStorageType = flagStorageType.capitalize()
+
+ fields.forEachApply {
+ if (sParcelling != null) {
+ +GENERATED_MEMBER_HEADER
+ "static $Parcelling<$Type> $sParcelling =" {
+ "$Parcelling.Cache.get(" {
+ +"$customParcellingClass.class"
+ } + ";"
+ }
+ "static {" {
+ "if ($sParcelling == null)" {
+ "$sParcelling = $Parcelling.Cache.put(" {
+ +"new $customParcellingClass()"
+ } + ";"
+ }
+ }
+ +""
+ }
+ }
+
+ val Parcel = classRef("android.os.Parcel")
+ if (!hasMethod("writeToParcel", Parcel, "int")) {
+ +"@Override"
+ +GENERATED_MEMBER_HEADER
+ "public void writeToParcel($Parcel dest, int flags)" {
+ +"// You can override field parcelling by defining methods like:"
+ +"// void parcelFieldName(Parcel dest, int flags) { ... }"
+ +""
+
+ if (booleanFields.isNotEmpty() || nullableFields.isNotEmpty()) {
+ +"$flagStorageType flg = 0;"
+ booleanFields.forEachApply {
+ +"if ($internalGetter) flg |= $fieldBit;"
+ }
+ nullableFields.forEachApply {
+ +"if ($internalGetter != null) flg |= $fieldBit;"
+ }
+ +"dest.write$FlagStorageType(flg);"
+ }
+
+ nonBooleanFields.forEachApply {
+ val customParcellingMethod = "parcel$NameUpperCamel"
+ when {
+ hasMethod(customParcellingMethod, Parcel, "int") ->
+ +"$customParcellingMethod(dest, flags);"
+ customParcellingClass != null -> +"$sParcelling.parcel($name, dest, flags);"
+ hasAnnotation("@$DataClassEnum") ->
+ +"dest.writeInt($internalGetter == null ? -1 : $internalGetter.ordinal());"
+ else -> {
+ if (mayBeNull) !"if ($internalGetter != null) "
+ var args = internalGetter
+ if (ParcelMethodsSuffix.startsWith("Parcelable")
+ || ParcelMethodsSuffix.startsWith("TypedObject")
+ || ParcelMethodsSuffix == "TypedArray") {
+ args += ", flags"
+ }
+ +"dest.write$ParcelMethodsSuffix($args);"
+ }
+ }
+ }
+ }
+ }
+
+ if (!hasMethod("describeContents")) {
+ +"@Override"
+ +GENERATED_MEMBER_HEADER
+ +"public int describeContents() { return 0; }"
+ +""
+ }
+
+ if (classAst.fields.none { it.variables[0].nameAsString == "CREATOR" }) {
+ val Creator = classRef("android.os.Parcelable.Creator")
+
+ +GENERATED_MEMBER_HEADER
+ "public static final @$NonNull $Creator<$ClassName> CREATOR" {
+ +"= new $Creator<$ClassName>()"
+ }; " {" {
+
+ +"@Override"
+ "public $ClassName[] newArray(int size)" {
+ +"return new $ClassName[size];"
+ }
+
+ +"@Override"
+ +"@SuppressWarnings({\"unchecked\", \"RedundantCast\"})"
+ "public $ClassName createFromParcel($Parcel in)" {
+ +"// You can override field unparcelling by defining methods like:"
+ +"// static FieldType unparcelFieldName(Parcel in) { ... }"
+ +""
+ if (booleanFields.isNotEmpty() || nullableFields.isNotEmpty()) {
+ +"$flagStorageType flg = in.read$FlagStorageType();"
+ }
+ booleanFields.forEachApply {
+ +"$Type $_name = (flg & $fieldBit) != 0;"
+ }
+ nonBooleanFields.forEachApply {
+
+ // Handle customized parceling
+ val customParcellingMethod = "unparcel$NameUpperCamel"
+ if (hasMethod(customParcellingMethod, Parcel)) {
+ +"$Type $_name = $customParcellingMethod(in);"
+ } else if (customParcellingClass != null) {
+ +"$Type $_name = $sParcelling.unparcel(in);"
+ } else if (hasAnnotation("@$DataClassEnum")) {
+ val ordinal = "${_name}Ordinal"
+ +"int $ordinal = in.readInt();"
+ +"$Type $_name = $ordinal < 0 ? null : $FieldClass.values()[$ordinal];"
+ } else {
+ val methodArgs = mutableListOf<String>()
+
+ // Create container if any
+ val containerInitExpr = when {
+ FieldClass.endsWith("Map") -> "new $LinkedHashMap<>()"
+ FieldClass == "List" || FieldClass == "ArrayList" ->
+ "new ${classRef("java.util.ArrayList")}<>()"
+// isArray && FieldInnerType in (PRIMITIVE_TYPES + "String") ->
+// "new $FieldInnerType[in.readInt()]"
+ else -> ""
+ }
+ val passContainer = containerInitExpr.isNotEmpty()
+
+ // nullcheck +
+ // "FieldType fieldName = (FieldType)"
+ if (passContainer) {
+ methodArgs.add(_name)
+ !"$Type $_name = "
+ if (mayBeNull) {
+ +"null;"
+ !"if ((flg & $fieldBit) != 0) {"
+ pushIndent()
+ +""
+ !"$_name = "
+ }
+ +"$containerInitExpr;"
+ } else {
+ !"$Type $_name = "
+ if (mayBeNull) !"(flg & $fieldBit) == 0 ? null : "
+ if (ParcelMethodsSuffix == "StrongInterface") {
+ !"$FieldClass.Stub.asInterface("
+ } else if (Type !in PRIMITIVE_TYPES + "String" + "Bundle" &&
+ (!isArray || FieldInnerType !in PRIMITIVE_TYPES + "String") &&
+ ParcelMethodsSuffix != "Parcelable") {
+ !"($Type) "
+ }
+ }
+
+ // Determine method args
+ when {
+ ParcelMethodsSuffix == "Parcelable" ->
+ methodArgs += "$FieldClass.class.getClassLoader()"
+ ParcelMethodsSuffix == "TypedObject" ->
+ methodArgs += "$FieldClass.CREATOR"
+ ParcelMethodsSuffix == "TypedArray" ->
+ methodArgs += "$FieldInnerClass.CREATOR"
+ ParcelMethodsSuffix.startsWith("Parcelable")
+ || FieldClass == "Map"
+ || (isList || isArray)
+ && FieldInnerType !in PRIMITIVE_TYPES + "String" ->
+ methodArgs += "$FieldInnerClass.class.getClassLoader()"
+ }
+
+ // ...in.readFieldType(args...);
+ when {
+ ParcelMethodsSuffix == "StrongInterface" -> !"in.readStrongBinder"
+ isArray -> !"in.create$ParcelMethodsSuffix"
+ else -> !"in.read$ParcelMethodsSuffix"
+ }
+ !"(${methodArgs.joinToString(", ")})"
+ if (ParcelMethodsSuffix == "StrongInterface") !")"
+ +";"
+
+ // Cleanup if passContainer
+ if (passContainer && mayBeNull) {
+ popIndent()
+ rmEmptyLine()
+ +"\n}"
+ }
+ }
+ }
+ "return new $ClassType(" {
+ fields.forEachTrimmingTrailingComma {
+ +"$_name,"
+ }
+ } + ";"
+ }
+ rmEmptyLine()
+ } + ";"
+ +""
+ }
+}
+
+fun ClassPrinter.generateEqualsHashcode() {
+ if (!hasMethod("equals", "Object")) {
+ +"@Override"
+ +GENERATED_MEMBER_HEADER
+ "public boolean equals(Object o)" {
+ +"// You can override field equality logic by defining either of the methods like:"
+ +"// boolean fieldNameEquals($ClassName other) { ... }"
+ +"// boolean fieldNameEquals(FieldType otherValue) { ... }"
+ +""
+ """if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ @SuppressWarnings("unchecked")
+ $ClassType that = ($ClassType) o;
+ //noinspection PointlessBooleanExpression
+ return true""" {
+ fields.forEachApply {
+ val sfx = if (isLast) ";" else ""
+ val customEquals = "${nameLowerCamel}Equals"
+ when {
+ hasMethod(customEquals, Type) -> +"&& $customEquals(that.$internalGetter)$sfx"
+ hasMethod(customEquals, ClassType) -> +"&& $customEquals(that)$sfx"
+ else -> +"&& ${isEqualToExpr("that.$internalGetter")}$sfx"
+ }
+ }
+ }
+ }
+ }
+
+ if (!hasMethod("hashCode")) {
+ +"@Override"
+ +GENERATED_MEMBER_HEADER
+ "public int hashCode()" {
+ +"// You can override field hashCode logic by defining methods like:"
+ +"// int fieldNameHashCode() { ... }"
+ +""
+ +"int _hash = 1;"
+ fields.forEachApply {
+ !"_hash = 31 * _hash + "
+ val customHashCode = "${nameLowerCamel}HashCode"
+ when {
+ hasMethod(customHashCode) -> +"$customHashCode();"
+ Type == "int" || Type == "byte" -> +"$internalGetter;"
+ Type in PRIMITIVE_TYPES -> +"${Type.capitalize()}.hashCode($internalGetter);"
+ isArray -> +"${memberRef("java.util.Arrays.hashCode")}($internalGetter);"
+ else -> +"${memberRef("java.util.Objects.hashCode")}($internalGetter);"
+ }
+ }
+ +"return _hash;"
+ }
+ }
+}
+
+//TODO support IntDef flags?
+fun ClassPrinter.generateToString() {
+ if (!hasMethod("toString")) {
+ +"@Override"
+ +GENERATED_MEMBER_HEADER
+ "public String toString()" {
+ +"// You can override field toString logic by defining methods like:"
+ +"// String fieldNameToString() { ... }"
+ +""
+ "return \"$ClassName { \" +" {
+ fields.forEachApply {
+ val customToString = "${nameLowerCamel}ToString"
+ val expr = when {
+ hasMethod(customToString) -> "$customToString()"
+ isArray -> "${memberRef("java.util.Arrays.toString")}($internalGetter)"
+ intOrStringDef?.type?.isInt == true ->
+ "${intOrStringDef!!.AnnotationName.decapitalize()}ToString($name)"
+ else -> internalGetter
+ }
+ +"\"$nameLowerCamel = \" + $expr${if_(!isLast, " + \", \"")} +"
+ }
+ }
+ +"\" }\";"
+ }
+ }
+}
+
+fun ClassPrinter.generateSetters() {
+ fields.forEachApply {
+ if (!hasMethod("set$NameUpperCamel", Type)
+ && !fieldAst.isPublic
+ && !isFinal) {
+
+ generateFieldJavadoc(forceHide = FeatureFlag.SETTERS.hidden)
+ +GENERATED_MEMBER_HEADER
+ "public $ClassType set$NameUpperCamel($annotatedTypeForSetterParam value)" {
+ generateSetFrom("value")
+ +"return this;"
+ }
+ }
+ }
+}
+
+fun ClassPrinter.generateGetters() {
+ (fields + lazyTransientFields).forEachApply {
+ val methodPrefix = if (Type == "boolean") "is" else "get"
+ val methodName = methodPrefix + NameUpperCamel
+
+ if (!hasMethod(methodName) && !fieldAst.isPublic) {
+
+ generateFieldJavadoc(forceHide = FeatureFlag.GETTERS.hidden)
+ +GENERATED_MEMBER_HEADER
+ "public $annotationsAndType $methodName()" {
+ if (lazyInitializer == null) {
+ +"return $name;"
+ } else {
+ +"$Type $_name = $name;"
+ "if ($_name == null)" {
+ if (fieldAst.isVolatile) {
+ "synchronized(this)" {
+ +"$_name = $name;"
+ "if ($_name == null)" {
+ +"$_name = $name = $lazyInitializer();"
+ }
+ }
+ } else {
+ +"// You can mark field as volatile for thread-safe double-check init"
+ +"$_name = $name = $lazyInitializer();"
+ }
+ }
+ +"return $_name;"
+ }
+ }
+ }
+ }
+}
+
+fun FieldInfo.generateFieldJavadoc(forceHide: Boolean = false) = classPrinter {
+ if (javadocFull != null || forceHide) {
+ var hidden = false
+ (javadocFull ?: "/**\n */").lines().forEach {
+ if (it.contains("@hide")) hidden = true
+ if (it.contains("*/") && forceHide && !hidden) {
+ if (javadocFull != null) +" *"
+ +" * @hide"
+ }
+ +it
+ }
+ }
+}
+
+fun FieldInfo.generateSetFrom(source: String) = classPrinter {
+ !"$name = "
+ if (Type in PRIMITIVE_TYPES || mayBeNull) {
+ +"$source;"
+ } else if (defaultExpr != null) {
+ "$source != null" {
+ +"? $source"
+ +": $defaultExpr;"
+ }
+ } else {
+ val checkNotNull = memberRef("com.android.internal.util.Preconditions.checkNotNull")
+ +"$checkNotNull($source);"
+ }
+ if (isNonEmpty) {
+ "if ($isEmptyExpr)" {
+ +"throw new IllegalArgumentException(\"$nameLowerCamel cannot be empty\");"
+ }
+ }
+}
+
+fun ClassPrinter.generateConstructor(visibility: String = "public") {
+ if (visibility == "public") {
+ generateConstructorJavadoc()
+ }
+ +GENERATED_MEMBER_HEADER
+ "$visibility $ClassName(" {
+ fields.forEachApply {
+ +"$annotationsAndType $nameLowerCamel${if_(!isLast, ",")}"
+ }
+ }
+ " {" {
+ fields.forEachApply {
+ !"this."
+ generateSetFrom(nameLowerCamel)
+ }
+
+ generateStateValidation()
+
+ generateOnConstructedCallback()
+ }
+}
+
+private fun ClassPrinter.generateConstructorJavadoc() {
+ if (fields.all { it.javadoc == null } && !FeatureFlag.CONSTRUCTOR.hidden) return
+ +"/**"
+ fields.filter { it.javadoc != null }.forEachApply {
+ javadocTextNoAnnotationLines?.apply {
+ +" * @param $nameLowerCamel"
+ forEach {
+ +" * $it"
+ }
+ }
+ }
+ if (FeatureFlag.CONSTRUCTOR.hidden) +" * @hide"
+ +" */"
+}
+
+private fun ClassPrinter.generateStateValidation() {
+ val Size = classRef("android.annotation.Size")
+ val knownNonValidationAnnotations = internalAnnotations + Nullable
+
+ val validate = memberRef("com.android.internal.util.AnnotationValidations.validate")
+ fun appendValidateCall(annotation: AnnotationExpr, valueToValidate: String) {
+ "$validate(" {
+ !"${annotation.nameAsString}.class, null, $valueToValidate"
+ val params = when (annotation) {
+ is MarkerAnnotationExpr -> emptyMap()
+ is SingleMemberAnnotationExpr -> mapOf("value" to annotation.memberValue)
+ is NormalAnnotationExpr ->
+ annotation.pairs.map { it.name.asString() to it.value }.toMap()
+ else -> throw IllegalStateException()
+ }
+ params.forEach { name, value ->
+ !",\n\"$name\", $value"
+ }
+ }
+ +";"
+ }
+
+ fields.forEachApply {
+ if (intOrStringDef != null) {
+ if (intOrStringDef!!.type == ConstDef.Type.INT_FLAGS) {
+ +""
+ +"//noinspection PointlessBitwiseExpression"
+ "$Preconditions.checkFlagsArgument(" {
+ "$name, 0" {
+ intOrStringDef!!.CONST_NAMES.forEach {
+ +"| $it"
+ }
+ }
+ }
+ +";"
+ } else {
+ +""
+ +"//noinspection PointlessBooleanExpression"
+ "if (true" {
+ intOrStringDef!!.CONST_NAMES.forEach { CONST_NAME ->
+ +"&& !(${isEqualToExpr(CONST_NAME)})"
+ }
+ }; rmEmptyLine(); ") {" {
+ "throw new ${classRef<IllegalArgumentException>()}(" {
+ "\"$nameLowerCamel was \" + $internalGetter + \" but must be one of: \"" {
+
+ intOrStringDef!!.CONST_NAMES.forEachLastAware { CONST_NAME, isLast ->
+ +"""+ "$CONST_NAME(" + $CONST_NAME + ")${if_(!isLast, ", ")}""""
+ }
+ }
+ }
+ +";"
+ }
+ }
+ }
+
+ val eachLine = fieldAst.annotations.find { it.nameAsString == Each }?.range?.orElse(null)?.end?.line
+ val perElementValidations = if (eachLine == null) emptyList() else fieldAst.annotations.filter {
+ it.nameAsString != Each &&
+ it.range.orElse(null)?.begin?.line?.let { it >= eachLine } ?: false
+ }
+
+ fieldAst.annotations.filterNot {
+ it.nameAsString == intOrStringDef?.AnnotationName
+ || it.nameAsString in knownNonValidationAnnotations
+ || it in perElementValidations
+ }.forEach { annotation ->
+ appendValidateCall(annotation,
+ valueToValidate = if (annotation.nameAsString == Size) sizeExpr else name)
+ }
+
+ if (perElementValidations.isNotEmpty()) {
+ +"int ${nameLowerCamel}Size = $sizeExpr;"
+ "for (int i = 0; i < ${nameLowerCamel}Size; i++) {" {
+ perElementValidations.forEach { annotation ->
+ appendValidateCall(annotation,
+ valueToValidate = elemAtIndexExpr("i"))
+ }
+ }
+ }
+ }
+}
+
+private fun ClassPrinter.generateOnConstructedCallback(prefix: String = "") {
+ +""
+ val call = "${prefix}onConstructed();"
+ if (hasMethod("onConstructed")) {
+ +call
+ } else {
+ +"// $call // You can define this method to get a callback"
+ }
+}
+
+fun ClassPrinter.generateForEachField() {
+ val specializations = listOf("Object", "int")
+ val usedSpecializations = fields.map { if (it.Type in specializations) it.Type else "Object" }
+ val usedSpecializationsSet = usedSpecializations.toSet()
+
+ val PerObjectFieldAction = classRef("com.android.internal.util.DataClass.PerObjectFieldAction")
+
+ +GENERATED_MEMBER_HEADER
+ "void forEachField(" {
+ usedSpecializationsSet.toList().forEachLastAware { specType, isLast ->
+ val SpecType = specType.capitalize()
+ val ActionClass = classRef("com.android.internal.util.DataClass.Per${SpecType}FieldAction")
+ +"$ActionClass<$ClassType> action$SpecType${if_(!isLast, ",")}"
+ }
+ }; " {" {
+ usedSpecializations.forEachIndexed { i, specType ->
+ val SpecType = specType.capitalize()
+ fields[i].apply {
+ +"action$SpecType.accept$SpecType(this, \"$nameLowerCamel\", $name);"
+ }
+ }
+ }
+
+ if (usedSpecializationsSet.size > 1) {
+ +"/** @deprecated May cause boxing allocations - use with caution! */"
+ +"@Deprecated"
+ +GENERATED_MEMBER_HEADER
+ "void forEachField($PerObjectFieldAction<$ClassType> action)" {
+ fields.forEachApply {
+ +"action.acceptObject(this, \"$nameLowerCamel\", $name);"
+ }
+ }
+ }
+}
diff --git a/tools/codegen/src/com/android/codegen/InputSignaturesComputation.kt b/tools/codegen/src/com/android/codegen/InputSignaturesComputation.kt
new file mode 100644
index 0000000..d1dc88f
--- /dev/null
+++ b/tools/codegen/src/com/android/codegen/InputSignaturesComputation.kt
@@ -0,0 +1,122 @@
+package com.android.codegen
+
+import com.github.javaparser.ast.body.TypeDeclaration
+import com.github.javaparser.ast.expr.*
+import com.github.javaparser.ast.nodeTypes.NodeWithAnnotations
+import com.github.javaparser.ast.type.ClassOrInterfaceType
+import com.github.javaparser.ast.type.Type
+
+
+fun ClassPrinter.getInputSignatures(): List<String> {
+ return classAst.fields.map { fieldAst ->
+ buildString {
+ append(fieldAst.modifiers.joinToString(" ") {it.asString()})
+ append(" ")
+ append(annotationsToString(fieldAst))
+ append(" ")
+ append(getFullClassName(fieldAst.commonType))
+ append(" ")
+ append(fieldAst.variables.joinToString(", ") { it.nameAsString })
+ }
+ } + classAst.methods.map { methodAst ->
+ buildString {
+ append(methodAst.modifiers.joinToString(" ") {it.asString()})
+ append(" ")
+ append(annotationsToString(methodAst))
+ append(" ")
+ append(getFullClassName(methodAst.type))
+ append(" ")
+ append(methodAst.nameAsString)
+ append("(")
+ append(methodAst.parameters.joinToString(",") {getFullClassName(it.type)})
+ append(")")
+ }
+ }
+}
+
+private fun ClassPrinter.annotationsToString(annotatedAst: NodeWithAnnotations<*>): String {
+ return annotatedAst.annotations.joinToString(" ") {
+ annotationToString(it)
+ }
+}
+
+private fun ClassPrinter.annotationToString(ann: AnnotationExpr): String {
+ return buildString {
+ append("@")
+ append(getFullClassName(ann.nameAsString))
+ if (ann is MarkerAnnotationExpr) return@buildString
+
+ append("(")
+
+ when (ann) {
+ is SingleMemberAnnotationExpr -> {
+ appendExpr(this, ann.memberValue)
+ }
+ is NormalAnnotationExpr -> {
+ ann.pairs.forEachLastAware { pair, isLast ->
+ append(pair.nameAsString)
+ append("=")
+ appendExpr(this, pair.value)
+ if (!isLast) append(", ")
+ }
+ }
+ }
+
+ append(")")
+ }.replace("\"", "\\\"")
+}
+
+private fun ClassPrinter.appendExpr(sb: StringBuilder, ex: Expression?) {
+ when (ex) {
+ is ClassExpr -> sb.append(getFullClassName(ex.typeAsString)).append(".class")
+ is IntegerLiteralExpr -> sb.append(ex.asInt()).append("L")
+ is LongLiteralExpr -> sb.append(ex.asLong()).append("L")
+ is DoubleLiteralExpr -> sb.append(ex.asDouble())
+ else -> sb.append(ex)
+ }
+}
+
+private fun ClassPrinter.getFullClassName(type: Type): String {
+ return if (type is ClassOrInterfaceType) {
+ getFullClassName(buildString {
+ type.scope.ifPresent { append(it).append(".") }
+ type.isArrayType
+ append(type.nameAsString)
+ }) + (type.typeArguments.orElse(null)?.let { args -> args.joinToString(", ") {getFullClassName(it)}}?.let { "<$it>" } ?: "")
+ } else getFullClassName(type.asString())
+}
+
+private fun ClassPrinter.getFullClassName(className: String): String {
+ if (className.endsWith("[]")) return getFullClassName(className.removeSuffix("[]")) + "[]"
+
+ if (className.matches("\\.[a-z]".toRegex())) return className //qualified name
+
+ if ("." in className) return getFullClassName(className.substringBeforeLast(".")) + "." + className.substringAfterLast(".")
+
+ fileAst.imports.find { imp ->
+ imp.nameAsString.endsWith(".$className")
+ }?.nameAsString?.let { return it }
+
+ val thisPackagePrefix = fileAst.packageDeclaration.map { it.nameAsString + "." }.orElse("")
+ val thisClassPrefix = thisPackagePrefix + classAst.nameAsString + "."
+
+ classAst.childNodes.filterIsInstance<TypeDeclaration<*>>().find {
+ it.nameAsString == className
+ }?.let { return thisClassPrefix + it.nameAsString }
+
+ constDefs.find { it.AnnotationName == className }?.let { return thisClassPrefix + className }
+
+ if (tryOrNull { Class.forName("java.lang.$className") } != null) {
+ return "java.lang.$className"
+ }
+
+ if (className[0].isLowerCase()) return className //primitive
+
+ return thisPackagePrefix + className
+}
+
+private inline fun <T> tryOrNull(f: () -> T?) = try {
+ f()
+} catch (e: Exception) {
+ null
+}
diff --git a/tools/codegen/src/com/android/codegen/Main.kt b/tools/codegen/src/com/android/codegen/Main.kt
new file mode 100755
index 0000000..8fafa7c
--- /dev/null
+++ b/tools/codegen/src/com/android/codegen/Main.kt
@@ -0,0 +1,199 @@
+package com.android.codegen
+
+import java.io.File
+
+
+const val THIS_SCRIPT_LOCATION = ""
+const val GENERATED_WARNING_PREFIX = "Code below generated by $CODEGEN_NAME"
+const val INDENT_SINGLE = " "
+
+val PRIMITIVE_TYPES = listOf("byte", "short", "int", "long", "char", "float", "double", "boolean")
+
+const val CANONICAL_BUILDER_CLASS = "Builder"
+const val GENERATED_BUILDER_CLASS = "GeneratedBuilder"
+
+val BUILTIN_SPECIAL_PARCELLINGS = listOf("Pattern")
+
+const val FLAG_BUILDER_PROTECTED_SETTERS = "--builder-protected-setters"
+const val FLAG_NO_FULL_QUALIFIERS = "--no-full-qualifiers"
+
+
+/** @see [FeatureFlag] */
+val USAGE = """
+Usage: $CODEGEN_NAME [--[PREFIX-]FEATURE...] JAVAFILE
+
+Generates boilerplade parcelable/data class code at the bottom of JAVAFILE, based o fields' declaration in the given JAVAFILE's top-level class
+
+FEATURE represents some generatable code, and can be among:
+${FeatureFlag.values().map { feature ->
+ " ${feature.kebabCase}" to feature.desc
+}.columnize(" - ")}
+
+And PREFIX can be:
+ <empty> - request to generate the feature
+ no - suppress generation of the feature
+ hidden - request to generate the feature with @hide
+
+Extra options:
+ --help - view this help
+ --update-only - auto-detect flags from the previously auto-generated comment within the file
+ $FLAG_NO_FULL_QUALIFIERS
+ - when referring to classes don't use package name prefix; handy with IDE auto-import
+ $FLAG_BUILDER_PROTECTED_SETTERS
+ - make builder's setters protected to expose them as public in a subclass on a whitelist basis
+
+
+Special field modifiers and annotations:
+ transient - ignore the field completely
+ @Nullable - support null value when parcelling, and never throw on null input
+ @NonNull - throw on null input and don't parcel the nullness bit for the field
+ @DataClass.Enum - parcel field as an enum value by ordinal
+ @DataClass.PluralOf(..) - provide a singular version of a collection field name to be used in the builder's 'addFoo(..)'
+ @DataClass.ParcelWith(..) - provide a custom Parcelling class, specifying the custom (un)parcelling logic for this field
+ = <initializer>; - provide default value and never throw if this field was not provided e.g. when using builder
+ /** ... */ - copy given javadoc on field's getters/setters/constructor params/builder setters etc.
+ @hide (in javadoc) - force field's getters/setters/withers/builder setters to be @hide-den if generated
+
+
+Special methods/etc. you can define:
+
+ <any auto-generatable method>
+ For any method to be generated, if a method with same name and argument types is already
+ defined, than that method will not be generated.
+ This allows you to override certain details on granular basis.
+
+ void onConstructed()
+ Will be called in constructor, after all the fields have been initialized.
+ This is a good place to put any custom validation logic that you may have
+
+ static class $CANONICAL_BUILDER_CLASS extends $GENERATED_BUILDER_CLASS
+ If a class extending $GENERATED_BUILDER_CLASS is specified, generated builder's setters will
+ return the provided $CANONICAL_BUILDER_CLASS type.
+ $GENERATED_BUILDER_CLASS's constructor(s) will be package-private to encourage using $CANONICAL_BUILDER_CLASS instead
+ This allows you to extend the generated builder, adding or overriding any methods you may want
+
+
+In addition, for any field mMyField(or myField) of type FieldType you can define the following methods:
+
+ void parcelMyField(Parcel dest, int flags)
+ Allows you to provide custom logic for storing mMyField into a Parcel
+
+ static FieldType unparcelMyField(Parcel in)
+ Allows you to provide custom logic to deserialize the value of mMyField from a Parcel
+
+ String myFieldToString()
+ Allows you to provide a custom toString representation of mMyField's value
+
+ FieldType lazyInitMyField()
+ Requests a lazy initialization in getMyField(), with the provided method being the constructor
+ You may additionally mark the fields as volatile to cause this to generate a thread-safe
+ double-check locking lazy initialization
+
+ FieldType defaultMyField()
+ Allows you to provide a default value to initialize the field to, in case an explicit one
+ was not provided.
+ This is an alternative to providing a field initializer that, unlike the initializer,
+ you can use with final fields.
+
+Version: $CODEGEN_VERSION
+Questions? Feedback? Contact: eugenesusla@
+"""
+
+fun main(args: Array<String>) {
+ if (args.contains("--help")) {
+ println(USAGE)
+ System.exit(0)
+ }
+ if (args.contains("--version")) {
+ println(CODEGEN_VERSION)
+ System.exit(0)
+ }
+ val file = File(args.last())
+ val sourceLinesNoClosingBrace = file.readLines().dropLastWhile {
+ it.startsWith("}") || it.all(Char::isWhitespace)
+ }
+ val cliArgs = handleUpdateFlag(args, sourceLinesNoClosingBrace)
+ val sourceLinesAsIs = discardGeneratedCode(sourceLinesNoClosingBrace)
+ val sourceLines = sourceLinesAsIs
+ .filterNot { it.trim().startsWith("//") }
+ .map { it.trimEnd().dropWhile { it == '\n' } }
+
+ val stringBuilder = StringBuilder(sourceLinesAsIs.joinToString("\n"))
+ ClassPrinter(sourceLines, stringBuilder, cliArgs).run {
+
+ val cliExecutable = "$THIS_SCRIPT_LOCATION$CODEGEN_NAME"
+ val fileEscaped = file.absolutePath.replace(
+ System.getenv("ANDROID_BUILD_TOP"), "\$ANDROID_BUILD_TOP")
+
+
+ +"""
+
+
+
+ // $GENERATED_WARNING_PREFIX v$CODEGEN_VERSION.
+ // on ${currentTimestamp()}
+ //
+ // DO NOT MODIFY!
+ //
+ // To regenerate run:
+ // $ $cliExecutable ${cliArgs.dropLast(1).joinToString("") { "$it " }}$fileEscaped
+ //
+ // CHECKSTYLE:OFF Generated code
+ """
+
+ if (FeatureFlag.CONST_DEFS()) generateConstDefs()
+
+ "@$DataClassGenerated(" {
+ +"time = ${System.currentTimeMillis()}L,"
+ +"codegenVersion = \"$CODEGEN_VERSION\","
+ +"sourceFile = \"${file.relativeTo(File(System.getenv("ANDROID_BUILD_TOP")))}\","
+ +"inputSignatures = \"${getInputSignatures().joinToString("\\n")}\""
+ }
+ +"\n"
+
+
+ if (FeatureFlag.CONSTRUCTOR()) {
+ generateConstructor("public")
+ } else if (FeatureFlag.BUILDER()
+ || FeatureFlag.COPY_CONSTRUCTOR()
+ || FeatureFlag.WITHERS()
+ || FeatureFlag.PARCELABLE()) {
+ generateConstructor("/* package-private */")
+ }
+
+ if (FeatureFlag.GETTERS()) generateGetters()
+ if (FeatureFlag.SETTERS()) generateSetters()
+ if (FeatureFlag.TO_STRING()) generateToString()
+ if (FeatureFlag.EQUALS_HASH_CODE()) generateEqualsHashcode()
+
+ if (FeatureFlag.FOR_EACH_FIELD()) generateForEachField()
+
+ if (FeatureFlag.COPY_CONSTRUCTOR()) generateCopyConstructor()
+ if (FeatureFlag.WITHERS()) generateWithers()
+
+ if (FeatureFlag.PARCELABLE()) generateParcelable()
+
+ if (FeatureFlag.BUILDER() && FeatureFlag.BUILD_UPON()) generateBuildUpon()
+ if (FeatureFlag.BUILDER()) generateBuilder()
+
+ if (FeatureFlag.AIDL()) generateAidl(file)
+
+ rmEmptyLine()
+ }
+ stringBuilder.append("\n}\n")
+ file.writeText(stringBuilder.toString().mapLines { trimEnd() })
+}
+
+internal fun discardGeneratedCode(sourceLinesNoClosingBrace: List<String>): List<String> {
+ return sourceLinesNoClosingBrace
+ .takeWhile { GENERATED_WARNING_PREFIX !in it }
+ .dropLastWhile(String::isBlank)
+}
+
+private fun handleUpdateFlag(cliArgs: Array<String>, sourceLines: List<String>): Array<String> {
+ if ("--update-only" in cliArgs
+ && sourceLines.none { GENERATED_WARNING_PREFIX in it || it.startsWith("@DataClass") }) {
+ System.exit(0)
+ }
+ return cliArgs - "--update-only"
+}
\ No newline at end of file
diff --git a/tools/codegen/src/com/android/codegen/SharedConstants.kt b/tools/codegen/src/com/android/codegen/SharedConstants.kt
new file mode 100644
index 0000000..41641f6
--- /dev/null
+++ b/tools/codegen/src/com/android/codegen/SharedConstants.kt
@@ -0,0 +1,4 @@
+package com.android.codegen
+
+const val CODEGEN_NAME = "codegen"
+const val CODEGEN_VERSION = "0.0.1"
\ No newline at end of file
diff --git a/tools/codegen/src/com/android/codegen/Utils.kt b/tools/codegen/src/com/android/codegen/Utils.kt
new file mode 100644
index 0000000..95c9909
--- /dev/null
+++ b/tools/codegen/src/com/android/codegen/Utils.kt
@@ -0,0 +1,76 @@
+package com.android.codegen
+
+import com.github.javaparser.ast.expr.AnnotationExpr
+import com.github.javaparser.ast.expr.Expression
+import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr
+import java.time.Instant
+import java.time.ZoneId
+import java.time.format.DateTimeFormatter
+import java.time.format.FormatStyle
+
+/**
+ * [Iterable.forEach] + [Any.apply]
+ */
+inline fun <T> Iterable<T>.forEachApply(block: T.() -> Unit) = forEach(block)
+
+inline fun String.mapLines(f: String.() -> String?) = lines().mapNotNull(f).joinToString("\n")
+inline fun <T> Iterable<T>.trim(f: T.() -> Boolean) = dropWhile(f).dropLastWhile(f)
+fun String.trimBlankLines() = lines().trim { isBlank() }.joinToString("\n")
+
+fun Char.isNewline() = this == '\n' || this == '\r'
+fun Char.isWhitespaceNonNewline() = isWhitespace() && !isNewline()
+
+fun if_(cond: Boolean, then: String) = if (cond) then else ""
+
+inline infix fun Int.times(action: () -> Unit) {
+ for (i in 1..this) action()
+}
+
+/**
+ * a bbb
+ * cccc dd
+ *
+ * ->
+ *
+ * a bbb
+ * cccc dd
+ */
+fun Iterable<Pair<String, String>>.columnize(separator: String = " | "): String {
+ val col1w = map { (a, _) -> a.length }.max()!!
+ val col2w = map { (_, b) -> b.length }.max()!!
+ return map { it.first.padEnd(col1w) + separator + it.second.padEnd(col2w) }.joinToString("\n")
+}
+
+fun String.hasUnbalancedCurlyBrace(): Boolean {
+ var braces = 0
+ forEach {
+ if (it == '{') braces++
+ if (it == '}') braces--
+ if (braces < 0) return true
+ }
+ return false
+}
+
+fun String.toLowerCamel(): String {
+ if (length >= 2 && this[0] == 'm' && this[1].isUpperCase()) return substring(1).capitalize()
+ if (all { it.isLetterOrDigit() }) return decapitalize()
+ return split("[^a-zA-Z0-9]".toRegex())
+ .map { it.toLowerCase().capitalize() }
+ .joinToString("")
+ .decapitalize()
+}
+
+inline fun <T> List<T>.forEachLastAware(f: (T, Boolean) -> Unit) {
+ forEachIndexed { index, t -> f(t, index == size - 1) }
+}
+
+@Suppress("UNCHECKED_CAST")
+fun <T : Expression> AnnotationExpr.singleArgAs()
+ = ((this as SingleMemberAnnotationExpr).memberValue as T)
+
+inline operator fun <reified T> Array<T>.minus(item: T) = toList().minus(item).toTypedArray()
+
+fun currentTimestamp() = DateTimeFormatter
+ .ofLocalizedDateTime(/* date */ FormatStyle.MEDIUM, /* time */ FormatStyle.LONG)
+ .withZone(ZoneId.systemDefault())
+ .format(Instant.now())
\ No newline at end of file
diff --git a/tools/processors/staledataclass/Android.bp b/tools/processors/staledataclass/Android.bp
new file mode 100644
index 0000000..c81d410
--- /dev/null
+++ b/tools/processors/staledataclass/Android.bp
@@ -0,0 +1,27 @@
+
+java_plugin {
+ name: "staledataclass-annotation-processor",
+ processor_class: "android.processor.staledataclass.StaleDataclassProcessor",
+
+ java_resources: [
+ "META-INF/**/*",
+ ],
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+ static_libs: [
+ "codegen-version-info",
+ ],
+ openjdk9: {
+ javacflags: [
+ "--add-modules=jdk.compiler",
+ "--add-exports jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
+ "--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED",
+ "--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
+ "--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
+ ],
+ },
+
+ use_tools_jar: true,
+}
diff --git a/tools/processors/staledataclass/META-INF/services/javax.annotation.processing.Processor b/tools/processors/staledataclass/META-INF/services/javax.annotation.processing.Processor
new file mode 100644
index 0000000..15ee623
--- /dev/null
+++ b/tools/processors/staledataclass/META-INF/services/javax.annotation.processing.Processor
@@ -0,0 +1 @@
+android.processor.staledataclass.StaleDataclassProcessorOld
diff --git a/tools/processors/staledataclass/src/android/processor/staledataclass/StaleDataclassProcessor.kt b/tools/processors/staledataclass/src/android/processor/staledataclass/StaleDataclassProcessor.kt
new file mode 100644
index 0000000..9e51180
--- /dev/null
+++ b/tools/processors/staledataclass/src/android/processor/staledataclass/StaleDataclassProcessor.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package android.processor.staledataclass
+
+import com.android.codegen.CODEGEN_NAME
+import com.android.codegen.CODEGEN_VERSION
+import com.sun.tools.javac.code.Symbol
+import com.sun.tools.javac.code.Type
+import java.io.File
+import java.io.FileNotFoundException
+import javax.annotation.processing.AbstractProcessor
+import javax.annotation.processing.RoundEnvironment
+import javax.annotation.processing.SupportedAnnotationTypes
+import javax.lang.model.SourceVersion
+import javax.lang.model.element.AnnotationMirror
+import javax.lang.model.element.Element
+import javax.lang.model.element.TypeElement
+import javax.tools.Diagnostic
+
+private const val STALE_FILE_THRESHOLD_MS = 1000
+private val WORKING_DIR = File(".").absoluteFile
+
+private const val DATACLASS_ANNOTATION_NAME = "com.android.internal.util.DataClass"
+private const val GENERATED_ANNOTATION_NAME = "com.android.internal.util.DataClass.Generated"
+private const val GENERATED_MEMBER_ANNOTATION_NAME
+ = "com.android.internal.util.DataClass.Generated.Member"
+
+
+@SupportedAnnotationTypes(DATACLASS_ANNOTATION_NAME, GENERATED_ANNOTATION_NAME)
+class StaleDataclassProcessor: AbstractProcessor() {
+
+ private var dataClassAnnotation: TypeElement? = null
+ private var generatedAnnotation: TypeElement? = null
+ private var repoRoot: File? = null
+
+ private val stale = mutableListOf<Stale>()
+
+ /**
+ * This is the main entry point in the processor, called by the compiler.
+ */
+ override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean {
+
+ if (generatedAnnotation == null) {
+ generatedAnnotation = annotations.find {
+ it.qualifiedName.toString() == GENERATED_ANNOTATION_NAME
+ }
+ }
+ if (dataClassAnnotation == null) {
+ dataClassAnnotation = annotations.find {
+ it.qualifiedName.toString() == DATACLASS_ANNOTATION_NAME
+ }
+ }
+
+ val generatedAnnotatedElements = roundEnv.getElementsAnnotatedWith(generatedAnnotation)
+ generatedAnnotatedElements.forEach {
+ processSingleFile(it)
+ }
+
+
+ val dataClassesWithoutGeneratedPart =
+ roundEnv.getElementsAnnotatedWith(dataClassAnnotation) -
+ generatedAnnotatedElements.map { it.enclosingElement }
+
+ dataClassesWithoutGeneratedPart.forEach { dataClass ->
+ stale += Stale(dataClass.toString(), file = null, lastGenerated = 0L)
+ }
+
+
+ if (!stale.isEmpty()) {
+ error("Stale generated dataclass(es) detected. " +
+ "Run the following command(s) to update them:" +
+ stale.joinToString("") { "\n" + it.refreshCmd })
+ }
+ return true
+ }
+
+ private fun elemToString(elem: Element): String {
+ return buildString {
+ append(elem.modifiers.joinToString(" ") { it.name.toLowerCase() }).append(" ")
+ append(elem.annotationMirrors.joinToString(" ")).append(" ")
+ if (elem is Symbol) {
+ if (elem.type is Type.MethodType) {
+ append((elem.type as Type.MethodType).returnType)
+ } else {
+ append(elem.type)
+ }
+ append(" ")
+ }
+ append(elem)
+ }
+ }
+
+ private fun processSingleFile(elementAnnotatedWithGenerated: Element) {
+
+ val inputSignatures = elementAnnotatedWithGenerated
+ .enclosingElement
+ .enclosedElements
+ .filterNot {
+ it.annotationMirrors.any { "Generated" in it.annotationType.toString() }
+ }.map {
+ elemToString(it)
+ }.toSet()
+
+ val annotationParams = elementAnnotatedWithGenerated
+ .annotationMirrors
+ .find { ann -> isGeneratedAnnotation(ann) }!!
+ .elementValues
+ .map { (k, v) -> k.getSimpleName().toString() to v.getValue() }
+ .toMap()
+
+ val lastGenerated = annotationParams["time"] as Long
+ val codegenVersion = annotationParams["codegenVersion"] as String
+ val sourceRelative = File(annotationParams["sourceFile"] as String)
+
+ val lastGenInputSignatures = (annotationParams["inputSignatures"] as String).lines().toSet()
+
+ if (repoRoot == null) {
+ repoRoot = generateSequence(WORKING_DIR) { it.parentFile }
+ .find { it.resolve(sourceRelative).isFile }
+ ?.canonicalFile
+ ?: throw FileNotFoundException(
+ "Failed to detect repository root: " +
+ "no parent of $WORKING_DIR contains $sourceRelative")
+ }
+
+ val source = repoRoot!!.resolve(sourceRelative)
+ val clazz = elementAnnotatedWithGenerated.enclosingElement.toString()
+
+ if (inputSignatures != lastGenInputSignatures) {
+ error(buildString {
+ append(sourceRelative).append(":\n")
+ append(" Added:\n").append((inputSignatures-lastGenInputSignatures).joinToString("\n"))
+ append("\n")
+ append(" Removed:\n").append((lastGenInputSignatures-inputSignatures).joinToString("\n"))
+ })
+ stale += Stale(clazz, source, lastGenerated)
+ }
+
+ if (codegenVersion != CODEGEN_VERSION) {
+ stale += Stale(clazz, source, lastGenerated)
+ }
+ }
+
+ private fun error(msg: String) {
+ processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, msg)
+ }
+
+ private fun isGeneratedAnnotation(ann: AnnotationMirror): Boolean {
+ return generatedAnnotation!!.qualifiedName.toString() == ann.annotationType.toString()
+ }
+
+ data class Stale(val clazz: String, val file: File?, val lastGenerated: Long) {
+ val refreshCmd = if (file != null) {
+ "$CODEGEN_NAME $file"
+ } else {
+ "find \$ANDROID_BUILD_TOP -path */${clazz.replace('.', '/')}.java -exec $CODEGEN_NAME {} \\;"
+ }
+ }
+
+ override fun getSupportedSourceVersion(): SourceVersion {
+ return SourceVersion.latest()
+ }
+}
\ No newline at end of file