aboutsummaryrefslogtreecommitdiff
path: root/processor/src/main
diff options
context:
space:
mode:
authorJonathan Scott <scottjonathan@google.com>2021-04-15 23:52:57 +0100
committerJonathan Scott <scottjonathan@google.com>2021-04-15 23:53:22 +0100
commit7451f6c15e755236d0e1aef2e1ae40f01c2ea105 (patch)
treedd553a86ad2ab309954ebdc46e5592979c734942 /processor/src/main
parent98aadee05251dfff3611cd31000da37d00d943f2 (diff)
downloadconnectedappssdk-7451f6c15e755236d0e1aef2e1ae40f01c2ea105.tar.gz
Import platform/external/connectedappssdk
Test: N/A importing code - BUILD to follow Bug: 179354604 Change-Id: I6b9833d1b148f526643e6ba34bd09ac4a17f37cd
Diffstat (limited to 'processor/src/main')
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/AlwaysThrowsGenerator.java221
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/BundlerGenerator.java270
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CodeGenerator.java80
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CommonClassNames.java124
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ConfigurationCodeGenerator.java59
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileCallbackCodeGenerator.java501
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileTypeCodeGenerator.java73
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CurrentProfileGenerator.java219
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DefaultProfileClassGenerator.java330
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DispatcherGenerator.java326
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/EarlyValidator.java1297
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeCrossProfileTypeGenerator.java477
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeOtherGenerator.java344
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeProfileConnectorGenerator.java100
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FutureWrappersGenerator.java126
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/GeneratorUtilities.java299
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/IfAvailableGenerator.java232
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InterfaceGenerator.java690
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalCrossProfileClassGenerator.java421
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalProviderClassGenerator.java186
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/LateValidator.java213
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/MultipleProfilesGenerator.java392
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/OtherProfileGenerator.java379
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ParcelableWrappersGenerator.java154
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/Processor.java415
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProcessorConfiguration.java30
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProfileConnectorCodeGenerator.java189
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProtoParcelableWrapperGenerator.java171
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProviderClassCodeGenerator.java53
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ServiceGenerator.java178
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/SupportedTypes.java844
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/TestCodeGenerator.java97
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/TypeUtils.java121
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/UserConnectorCodeGenerator.java178
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ValidationMessageFormatter.java49
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationClasses.java42
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationFinder.java271
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationInfoExtractor.java92
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationInvocationHandler.java49
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationNames.java37
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationPrinter.java52
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationStrings.java188
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileAnnotationInfoExtractor.java78
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileCallbackAnnotationInfoExtractor.java48
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileConfigurationAnnotationInfoExtractor.java69
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileConfigurationsAnnotationInfoExtractor.java69
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileProviderAnnotationInfoExtractor.java52
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileTestAnnotationInfoExtractor.java51
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileAnnotation.java36
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileCallbackAnnotation.java22
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileConfigurationAnnotation.java28
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileConfigurationsAnnotation.java24
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileProviderAnnotation.java22
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileTestAnnotation.java22
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/Context.java29
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileAnnotationInfo.java72
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileCallbackAnnotationInfo.java38
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileCallbackInterfaceInfo.java72
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationAnnotationInfo.java54
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationInfo.java119
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationsAnnotationInfo.java34
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileMethodInfo.java231
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileProviderAnnotationInfo.java33
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTestAnnotationInfo.java39
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTestInfo.java47
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTypeInfo.java171
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/FutureWrapper.java139
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/FutureWrapperAnnotationInfo.java45
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/GeneratorContext.java142
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ParcelableWrapper.java274
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ParcelableWrapperAnnotationInfo.java42
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ProfileConnectorInfo.java160
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ProviderClassInfo.java130
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/Type.java136
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/UserConnectorInfo.java150
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorContext.java120
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileConfigurationInfo.java125
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileTestInfo.java40
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileTypeInfo.java115
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorProviderClassInfo.java51
-rw-r--r--processor/src/main/resources/futurewrappers/ListenableFutureWrapper.java122
-rw-r--r--processor/src/main/resources/parcelablewrappers/ParcelableArray.java118
-rw-r--r--processor/src/main/resources/parcelablewrappers/ParcelableBitmap.java101
-rw-r--r--processor/src/main/resources/parcelablewrappers/ParcelableCollection.java117
-rw-r--r--processor/src/main/resources/parcelablewrappers/ParcelableGuavaOptional.java120
-rw-r--r--processor/src/main/resources/parcelablewrappers/ParcelableImmutableMap.java129
-rw-r--r--processor/src/main/resources/parcelablewrappers/ParcelableList.java117
-rw-r--r--processor/src/main/resources/parcelablewrappers/ParcelableMap.java121
-rw-r--r--processor/src/main/resources/parcelablewrappers/ParcelableOptional.java120
-rw-r--r--processor/src/main/resources/parcelablewrappers/ParcelablePair.java115
-rw-r--r--processor/src/main/resources/parcelablewrappers/ParcelableSet.java116
91 files changed, 14994 insertions, 0 deletions
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/AlwaysThrowsGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/AlwaysThrowsGenerator.java
new file mode 100644
index 0000000..05a506e
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/AlwaysThrowsGenerator.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.EXCEPTION_CALLBACK_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.FutureWrapper;
+import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext;
+import com.squareup.javapoet.AnnotationSpec;
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.MethodSpec;
+import com.squareup.javapoet.ParameterizedTypeName;
+import com.squareup.javapoet.TypeSpec;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.type.TypeMirror;
+
+/**
+ * Generate the {@code Profile_*_AlwaysThrows} class for a single cross-profile type.
+ *
+ * <p>This class is used when running on Pre-O devices to shortcut any cross-profile code and just
+ * throw an {@code UnavailableProfileException}.
+ *
+ * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to
+ * validate that the annotated code is correct.
+ */
+final class AlwaysThrowsGenerator {
+
+ private boolean generated = false;
+ private final GeneratorContext generatorContext;
+ private final GeneratorUtilities generatorUtilities;
+ private final CrossProfileTypeInfo crossProfileType;
+
+ AlwaysThrowsGenerator(GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) {
+ this.generatorContext = checkNotNull(generatorContext);
+ this.generatorUtilities = new GeneratorUtilities(generatorContext);
+ this.crossProfileType = checkNotNull(crossProfileType);
+ }
+
+ void generate() {
+ if (generated) {
+ throw new IllegalStateException("AlwaysThrowsGenerator#generate can only be called once");
+ }
+ generated = true;
+
+ generateAlwaysThrowsClass();
+ }
+
+ private void generateAlwaysThrowsClass() {
+ ClassName className = getAlwaysThrowsClassName(generatorContext, crossProfileType);
+
+ ClassName singleSenderCanThrowInterface =
+ InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName(
+ generatorContext, crossProfileType);
+
+ TypeSpec.Builder classBuilder =
+ TypeSpec.classBuilder(className)
+ .addJavadoc(
+ "Implementation of {@link $T} which throws an {@link $T} for every call.\n",
+ singleSenderCanThrowInterface,
+ UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME)
+ .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
+ .addSuperinterface(singleSenderCanThrowInterface);
+
+ classBuilder.addField(String.class, "errorMessage", Modifier.PRIVATE, Modifier.FINAL);
+
+ classBuilder.addMethod(
+ MethodSpec.constructorBuilder()
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(String.class, "errorMessage")
+ .beginControlFlow("if (errorMessage == null)")
+ .addStatement("throw new $T()", NullPointerException.class)
+ .endControlFlow()
+ .addStatement("this.errorMessage = errorMessage")
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("timeout")
+ .addAnnotation(Override.class)
+ .addAnnotation(
+ AnnotationSpec.builder(SuppressWarnings.class)
+ .addMember("value", "$S", "GoodTime")
+ .build())
+ .addModifiers(Modifier.PUBLIC)
+ .returns(className)
+ .addParameter(long.class, "timeout")
+ .addStatement("return this")
+ .build());
+
+ ClassName ifAvailableClass =
+ IfAvailableGenerator.getIfAvailableClassName(generatorContext, crossProfileType);
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("ifAvailable")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(ifAvailableClass)
+ .addStatement("return new $T(this)", ifAvailableClass)
+ .build());
+
+ for (CrossProfileMethodInfo method : crossProfileType.crossProfileMethods()) {
+ if (method.isBlocking(generatorContext, crossProfileType)) {
+ generateBlockingMethodOnAlwaysThrowsClass(classBuilder, method, crossProfileType);
+ } else if (method.isCrossProfileCallback(generatorContext)) {
+ generateCrossProfileCallbackMethodOnAlwaysThrowsClass(
+ classBuilder, method, crossProfileType);
+ } else if (method.isFuture(crossProfileType)) {
+ generateFutureMethodOnAlwaysThrowsClass(classBuilder, method, crossProfileType);
+ } else {
+ throw new IllegalStateException("Unknown method type: " + method);
+ }
+ }
+
+ generatorUtilities.writeClassToFile(className.packageName(), classBuilder);
+ }
+
+ private void generateBlockingMethodOnAlwaysThrowsClass(
+ TypeSpec.Builder classBuilder,
+ CrossProfileMethodInfo method,
+ CrossProfileTypeInfo crossProfileType) {
+
+ MethodSpec.Builder methodBuilder =
+ MethodSpec.methodBuilder(method.simpleName())
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .addException(UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME)
+ .returns(method.returnTypeTypeName())
+ .addParameters(
+ GeneratorUtilities.extractParametersFromMethod(
+ crossProfileType.supportedTypes(),
+ method.methodElement(),
+ REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS))
+ .addStatement("throw new $T(errorMessage)", UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME);
+
+ classBuilder.addMethod(methodBuilder.build());
+ }
+
+ private static void generateCrossProfileCallbackMethodOnAlwaysThrowsClass(
+ TypeSpec.Builder classBuilder,
+ CrossProfileMethodInfo method,
+ CrossProfileTypeInfo crossProfileType) {
+ MethodSpec.Builder methodBuilder =
+ MethodSpec.methodBuilder(method.simpleName())
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(method.returnTypeTypeName())
+ .addParameters(
+ GeneratorUtilities.extractParametersFromMethod(
+ crossProfileType.supportedTypes(),
+ method.methodElement(),
+ REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS))
+ .addParameter(EXCEPTION_CALLBACK_CLASSNAME, "exceptionCallback")
+ .addStatement(
+ "exceptionCallback.onException(new $T(errorMessage))",
+ UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME);
+
+ classBuilder.addMethod(methodBuilder.build());
+ }
+
+ private void generateFutureMethodOnAlwaysThrowsClass(
+ TypeSpec.Builder classBuilder,
+ CrossProfileMethodInfo method,
+ CrossProfileTypeInfo crossProfileType) {
+
+ TypeMirror rawFutureType = TypeUtils.removeTypeArguments(method.returnType());
+
+ FutureWrapper futureWrapper =
+ crossProfileType.supportedTypes().getType(rawFutureType).getFutureWrapper().get();
+
+ // This assumes futures are only generic on one argument, which is enforced
+ TypeMirror wrappedType = TypeUtils.extractTypeArguments(method.returnType()).get(0);
+ ParameterizedTypeName futureWrapperType =
+ ParameterizedTypeName.get(futureWrapper.wrapperClassName(), ClassName.get(wrappedType));
+
+ MethodSpec.Builder methodBuilder =
+ MethodSpec.methodBuilder(method.simpleName())
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(method.returnTypeTypeName())
+ .addParameters(
+ GeneratorUtilities.extractParametersFromMethod(
+ crossProfileType.supportedTypes(),
+ method.methodElement(),
+ REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS))
+ .addStatement(
+ "$1T failedFuture = $2T.create(new $3T(), $4L)",
+ futureWrapperType,
+ futureWrapper.wrapperClassName(),
+ BundlerGenerator.getBundlerClassName(generatorContext, crossProfileType),
+ TypeUtils.generateBundlerType(wrappedType))
+ .addStatement(
+ "failedFuture.onException(new $T(errorMessage))",
+ UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME)
+ .addStatement("return failedFuture.getFuture()");
+
+ classBuilder.addMethod(methodBuilder.build());
+ }
+
+ static ClassName getAlwaysThrowsClassName(
+ GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) {
+ return GeneratorUtilities.appendToClassName(
+ crossProfileType.profileClassName(), "_AlwaysThrows");
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/BundlerGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/BundlerGenerator.java
new file mode 100644
index 0000000..8d421a3
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/BundlerGenerator.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.BUNDLER_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.BUNDLER_TYPE_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_CLASSNAME;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.stream.Collectors.toList;
+
+import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext;
+import com.google.android.enterprise.connectedapps.processor.containers.Type;
+import com.squareup.javapoet.AnnotationSpec;
+import com.squareup.javapoet.ArrayTypeName;
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.CodeBlock;
+import com.squareup.javapoet.MethodSpec;
+import com.squareup.javapoet.TypeSpec;
+import java.util.List;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.type.PrimitiveType;
+import javax.lang.model.type.TypeMirror;
+
+/**
+ * Generate the {@code *_Bundler} class for a single {@link CrossProfileConfiguration} annotated
+ * method.
+ *
+ * <p>This class is responsible for reading and writing {@code Bundle} and {@code Parcel} instances.
+ *
+ * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to
+ * validate that the annotated code is correct.
+ */
+final class BundlerGenerator {
+
+ private boolean generated = false;
+ private final GeneratorContext generatorContext;
+ private final GeneratorUtilities generatorUtilities;
+ private final CrossProfileTypeInfo crossProfileType;
+
+ BundlerGenerator(GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) {
+ this.generatorContext = checkNotNull(generatorContext);
+ this.generatorUtilities = new GeneratorUtilities(generatorContext);
+ this.crossProfileType = checkNotNull(crossProfileType);
+ }
+
+ void generate() {
+ if (generated) {
+ throw new IllegalStateException("BundlerGenerator#generate can only be called once");
+ }
+ generated = true;
+
+ generateBundlerClass();
+ }
+
+ private void generateBundlerClass() {
+ ClassName className = getBundlerClassName(generatorContext, crossProfileType);
+
+ TypeSpec.Builder classBuilder =
+ TypeSpec.classBuilder(className)
+ .addJavadoc(
+ "Implementation of {@link $T} for use with {@link $T}.\n",
+ BUNDLER_CLASSNAME,
+ crossProfileType.className())
+ .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
+ .addSuperinterface(BUNDLER_CLASSNAME);
+
+ classBuilder.addMethod(MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC).build());
+ classBuilder.addMethod(
+ MethodSpec.constructorBuilder()
+ .addModifiers(Modifier.PRIVATE)
+ .addParameter(PARCEL_CLASSNAME, "in")
+ .build());
+
+ makeParcelable(classBuilder, className);
+ addWriteToParcelMethod(classBuilder);
+ addReadFromParcelMethod(classBuilder);
+ addCreateArrayMethod(classBuilder);
+
+ generatorUtilities.writeClassToFile(className.packageName(), classBuilder);
+ }
+
+ private void makeParcelable(TypeSpec.Builder classBuilder, ClassName bundlerClassName) {
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("writeToParcel")
+ .addModifiers(Modifier.PUBLIC)
+ .addAnnotation(Override.class)
+ .addParameter(PARCEL_CLASSNAME, "dest")
+ .addParameter(int.class, "flags")
+ .build());
+
+ generatorUtilities.addDefaultParcelableMethods(classBuilder, bundlerClassName);
+ }
+
+
+ private void addWriteToParcelMethod(TypeSpec.Builder classBuilder) {
+ CodeBlock.Builder methodCode = CodeBlock.builder();
+
+ List<Type> types =
+ crossProfileType.supportedTypes().usableTypes().stream()
+ .filter(Type::canBeBundled)
+ .filter(t -> !t.isPrimitive())
+ .collect(toList());
+ addWriteToParcelTypes(methodCode, types);
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("writeToParcel")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ // This is for passing rawtypes into the Parcelable*.of() methods
+ .addAnnotation(
+ AnnotationSpec.builder(SuppressWarnings.class)
+ .addMember("value", "\"unchecked\"")
+ .build())
+ .addParameter(PARCEL_CLASSNAME, "parcel")
+ .addParameter(Object.class, "value")
+ .addParameter(BUNDLER_TYPE_CLASSNAME, "valueType")
+ .addParameter(int.class, "flags")
+ .addCode(methodCode.build())
+ .build());
+ }
+
+ private void addWriteToParcelTypes(CodeBlock.Builder codeBuilder, List<Type> types) {
+ codeBuilder.beginControlFlow(
+ "if ($S.equals(valueType.rawTypeQualifiedName()))", "java.lang.Void");
+ codeBuilder.addStatement("return");
+ for (Type type : types) {
+ codeBuilder.nextControlFlow(
+ "else if ($S.equals(valueType.rawTypeQualifiedName()))",
+ TypeUtils.getRawTypeQualifiedName(type.getTypeMirror()));
+ addWriteToParcelType(codeBuilder, type);
+ }
+ codeBuilder.endControlFlow();
+
+ codeBuilder.addStatement(
+ "throw new $T(\"Type \" + valueType.rawTypeQualifiedName() + \" cannot be written to"
+ + " Parcel\")",
+ IllegalArgumentException.class);
+ }
+
+ private void addWriteToParcelType(CodeBlock.Builder codeBuilder, Type type) {
+ CodeBlock convertedValue =
+ CodeBlock.of("($L) value", TypeUtils.getRawTypeQualifiedName(type.getTypeMirror()));
+ codeBuilder.addStatement(
+ crossProfileType
+ .supportedTypes()
+ .generateWriteToParcelCode("parcel", type, convertedValue.toString()));
+ codeBuilder.addStatement("return");
+ }
+
+ private void addReadFromParcelMethod(TypeSpec.Builder classBuilder) {
+ CodeBlock.Builder methodCode = CodeBlock.builder();
+
+ List<Type> types =
+ crossProfileType.supportedTypes().usableTypes().stream()
+ .filter(Type::canBeBundled)
+ .collect(toList());
+ addReadFromParcelTypes(methodCode, types);
+
+ methodCode.addStatement(
+ "throw new $T(\"Type \" + valueType.rawTypeQualifiedName() + \" cannot be read from"
+ + " Parcel\")",
+ IllegalArgumentException.class);
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("readFromParcel")
+ .addAnnotation(
+ AnnotationSpec.builder(SuppressWarnings.class)
+ .addMember("value", "\"unchecked\"")
+ .build())
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(Object.class)
+ .addParameter(PARCEL_CLASSNAME, "parcel")
+ .addParameter(BUNDLER_TYPE_CLASSNAME, "valueType")
+ .addCode(methodCode.build())
+ .build());
+ }
+
+ private void addReadFromParcelTypes(CodeBlock.Builder codeBuilder, List<Type> types) {
+ codeBuilder.beginControlFlow(
+ "if ($S.equals(valueType.rawTypeQualifiedName()))", "java.lang.Void");
+ codeBuilder.addStatement("return null");
+ for (Type type : types) {
+ codeBuilder.nextControlFlow(
+ "else if ($S.equals(valueType.rawTypeQualifiedName()))",
+ TypeUtils.getRawTypeQualifiedName(type.getTypeMirror()));
+ addReadFromParcelType(codeBuilder, type);
+ }
+ codeBuilder.endControlFlow();
+ }
+
+ private void addReadFromParcelType(CodeBlock.Builder codeBuilder, Type type) {
+ TypeMirror objectType = type.getTypeMirror();
+ if (objectType.getKind().isPrimitive()) {
+ PrimitiveType primitiveType = (PrimitiveType) objectType;
+ objectType = generatorContext.types().boxedClass(primitiveType).asType();
+ }
+
+ codeBuilder.addStatement(
+ "return ($L) $L",
+ TypeUtils.getRawTypeQualifiedName(objectType),
+ crossProfileType.supportedTypes().generateReadFromParcelCode("parcel", type));
+ }
+
+ private void addCreateArrayMethod(TypeSpec.Builder classBuilder) {
+ CodeBlock.Builder methodCode = CodeBlock.builder();
+
+ List<Type> types =
+ crossProfileType.supportedTypes().usableTypes().stream()
+ .filter(Type::canBeBundled)
+ .filter(t -> !t.isGeneric())
+ .filter(
+ t ->
+ !t.isPrimitive()) // We can't return a primitive array with return type Object[]
+ .filter(t -> !t.isArray()) // We don't support multidimensional arrays
+ .collect(toList());
+ addCreateArrayTypes(methodCode, types);
+
+ methodCode.addStatement(
+ "throw new $T(\"Cannot create array of type \" + valueType.rawTypeQualifiedName())",
+ IllegalArgumentException.class);
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("createArray")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(ArrayTypeName.of(Object.class))
+ .addParameter(BUNDLER_TYPE_CLASSNAME, "valueType")
+ .addParameter(int.class, "size")
+ .addCode(methodCode.build())
+ .build());
+ }
+
+ private void addCreateArrayTypes(CodeBlock.Builder codeBuilder, List<Type> types) {
+ codeBuilder.beginControlFlow(
+ "if ($S.equals(valueType.rawTypeQualifiedName()))", "java.lang.Void");
+ codeBuilder.addStatement("return new Void[size]");
+ for (Type type : types) {
+ codeBuilder.nextControlFlow(
+ "else if ($S.equals(valueType.rawTypeQualifiedName()))",
+ TypeUtils.getRawTypeQualifiedName(type.getTypeMirror()));
+ addCreateArrayType(codeBuilder, type);
+ }
+ codeBuilder.endControlFlow();
+ }
+
+ private void addCreateArrayType(CodeBlock.Builder codeBuilder, Type type) {
+ codeBuilder.addStatement("return new $T[size]", type.getTypeMirror());
+ }
+
+ static ClassName getBundlerClassName(
+ GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) {
+ return GeneratorUtilities.appendToClassName(crossProfileType.profileClassName(), "_Bundler");
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CodeGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CodeGenerator.java
new file mode 100644
index 0000000..7fcf28a
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CodeGenerator.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackInterfaceInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext;
+import com.google.android.enterprise.connectedapps.processor.containers.ProfileConnectorInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.ProviderClassInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.UserConnectorInfo;
+
+/**
+ * Generator of code for connected apps.
+ *
+ * <p>This is intended to be initialised and used once, which will generate all needed code.
+ *
+ * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to
+ * validate that the annotated code is correct.
+ */
+final class CodeGenerator {
+ private boolean generated = false;
+ private final GeneratorContext generatorContext;
+ private final ParcelableWrappersGenerator parcelableWrappersGenerator;
+ private final FutureWrappersGenerator futureWrappersGenerator;
+ private final TestCodeGenerator testCodeGenerator;
+
+ CodeGenerator(GeneratorContext generatorContext) {
+ this.generatorContext = checkNotNull(generatorContext);
+ this.parcelableWrappersGenerator = new ParcelableWrappersGenerator(generatorContext);
+ this.futureWrappersGenerator = new FutureWrappersGenerator(generatorContext);
+ this.testCodeGenerator = new TestCodeGenerator(generatorContext);
+ }
+
+ void generate() {
+ if (generated) {
+ throw new IllegalStateException("CodeGenerator#generate can only be called once");
+ }
+ generated = true;
+
+ parcelableWrappersGenerator.generate();
+ futureWrappersGenerator.generate();
+ testCodeGenerator.generate();
+
+ for (ProfileConnectorInfo connector : generatorContext.generatedProfileConnectors()) {
+ new ProfileConnectorCodeGenerator(generatorContext, connector).generate();
+ }
+
+ for (UserConnectorInfo connector : generatorContext.generatedUserConnectors()) {
+ new UserConnectorCodeGenerator(generatorContext, connector).generate();
+ }
+
+ for (CrossProfileConfigurationInfo configuration : generatorContext.configurations()) {
+ new ConfigurationCodeGenerator(generatorContext, configuration).generate();
+ }
+
+ for (ProviderClassInfo providerClass : generatorContext.providers()) {
+ new ProviderClassCodeGenerator(generatorContext, providerClass).generate();
+ }
+
+ for (CrossProfileCallbackInterfaceInfo callbackInterface :
+ generatorContext.crossProfileCallbackInterfaces()) {
+ new CrossProfileCallbackCodeGenerator(generatorContext, callbackInterface).generate();
+ }
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CommonClassNames.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CommonClassNames.java
new file mode 100644
index 0000000..cebeebc
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CommonClassNames.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import com.squareup.javapoet.ClassName;
+
+/**
+ * {@link ClassName} instances shared across the processor.
+ *
+ * <p>This is required as most classes are not available to the processor so need to be referenced
+ * through {@link ClassName}
+ */
+public class CommonClassNames {
+ static final ClassName CONTEXT_CLASSNAME = ClassName.get("android.content", "Context");
+ static final ClassName PARCEL_CLASSNAME = ClassName.get("android.os", "Parcel");
+ static final ClassName PARCELABLE_CLASSNAME = ClassName.get("android.os", "Parcelable");
+ static final ClassName CROSS_PROFILE_FUTURE_RESULT_WRITER =
+ ClassName.get(
+ "com.google.android.enterprise.connectedapps.internal", "CrossProfileFutureResultWriter");
+ static final ClassName IF_AVAILABLE_FUTURE_RESULT_WRITER =
+ ClassName.get(
+ "com.google.android.enterprise.connectedapps.internal", "IfAvailableFutureResultWriter");
+ static final ClassName PARCELABLE_CREATOR_CLASSNAME =
+ ClassName.get("android.os.Parcelable", "Creator");
+ static final ClassName UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME =
+ ClassName.get(
+ "com.google.android.enterprise.connectedapps.exceptions", "UnavailableProfileException");
+ static final ClassName AVAILABILITY_RESTRICTIONS_CLASSNAME =
+ ClassName.get(
+ "com.google.android.enterprise.connectedapps.annotations", "AvailabilityRestrictions");
+ static final ClassName PROFILE_RUNTIME_EXCEPTION_CLASSNAME =
+ ClassName.get(
+ "com.google.android.enterprise.connectedapps.exceptions", "ProfileRuntimeException");
+ static final ClassName PROFILE_AWARE_UTILS_CLASSNAME =
+ ClassName.get("com.google.android.enterprise.connectedapps", "ConnectedAppsUtils");
+ static final ClassName BACKGROUND_EXCEPTION_THROWER_CLASSNAME =
+ ClassName.get(
+ "com.google.android.enterprise.connectedapps.internal", "BackgroundExceptionThrower");
+ static final ClassName PARCEL_UTILITIES_CLASSNAME =
+ ClassName.get("com.google.android.enterprise.connectedapps.internal", "ParcelUtilities");
+ static final ClassName METHOD_RUNNER_CLASSNAME =
+ ClassName.get("com.google.android.enterprise.connectedapps.internal", "MethodRunner");
+ static final ClassName BUNDLER_CLASSNAME =
+ ClassName.get("com.google.android.enterprise.connectedapps.internal", "Bundler");
+ static final ClassName BUNDLER_TYPE_CLASSNAME =
+ ClassName.get("com.google.android.enterprise.connectedapps.internal", "BundlerType");
+ static final ClassName PARCEL_CALL_RECEIVER_CLASSNAME =
+ ClassName.get("com.google.android.enterprise.connectedapps.internal", "ParcelCallReceiver");
+ public static final ClassName BINDER_CLASSNAME = ClassName.get("android.os", "Binder");
+ public static final ClassName INTENT_CLASSNAME = ClassName.get("android.content", "Intent");
+ static final ClassName CROSS_PROFILE_SENDER_CLASSNAME =
+ ClassName.get("com.google.android.enterprise.connectedapps", "CrossProfileSender");
+ public static final ClassName CROSSPROFILESERVICE_STUB_CLASSNAME =
+ ClassName.get("com.google.android.enterprise.connectedapps.ICrossProfileService", "Stub");
+ static final ClassName INVALID_PROTOCOL_BUFFER_EXCEPTION_CLASSNAME =
+ ClassName.get("com.google.protobuf", "InvalidProtocolBufferException");
+ static final ClassName PROFILE_CLASSNAME =
+ ClassName.get("com.google.android.enterprise.connectedapps", "Profile");
+ static final ClassName LOCAL_CALLBACK_CLASSNAME =
+ ClassName.get("com.google.android.enterprise.connectedapps", "LocalCallback");
+ public static final ClassName CROSS_PROFILE_CALLBACK_CLASSNAME =
+ ClassName.get("com.google.android.enterprise.connectedapps", "ICrossProfileCallback");
+ static final ClassName ASYNC_CALLBACK_PARAM_MULTIMERGER_CLASSNAME =
+ ClassName.get(
+ "com.google.android.enterprise.connectedapps.internal",
+ "CrossProfileCallbackMultiMerger");
+ static final ClassName CROSS_PROFILE_CALLBACK_PARCEL_CALL_SENDER_CLASSNAME =
+ ClassName.get(
+ "com.google.android.enterprise.connectedapps.internal",
+ "CrossProfileCallbackParcelCallSender");
+ static final ClassName CROSS_PROFILE_CALLBACK_EXCEPTION_PARCEL_CALL_SENDER_CLASSNAME =
+ ClassName.get(
+ "com.google.android.enterprise.connectedapps.internal",
+ "CrossProfileCallbackExceptionParcelCallSender");
+ static final ClassName ASYNC_CALLBACK_PARAM_MULTIMERGER_COMPLETE_LISTENER_CLASSNAME =
+ ClassName.get(
+ "com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger",
+ "CrossProfileCallbackMultiMergerCompleteListener");
+
+ public static final ClassName SERVICE_CLASSNAME = ClassName.get("android.app", "Service");
+ public static final ClassName EXCEPTION_CALLBACK_CLASSNAME =
+ ClassName.get("com.google.android.enterprise.connectedapps", "ExceptionCallback");
+ public static final ClassName CALLBACK_MERGER_EXCEPTION_CALLBACK_CLASSNAME =
+ ClassName.get(
+ "com.google.android.enterprise.connectedapps.internal",
+ "CallbackMergerExceptionCallback");
+ public static final ClassName PROFILE_CONNECTOR_CLASSNAME =
+ ClassName.get("com.google.android.enterprise.connectedapps", "ProfileConnector");
+ public static final ClassName ABSTRACT_PROFILE_CONNECTOR_CLASSNAME =
+ ClassName.get("com.google.android.enterprise.connectedapps", "AbstractProfileConnector");
+ public static final ClassName ABSTRACT_USER_CONNECTOR_CLASSNAME =
+ ClassName.get("com.google.android.enterprise.connectedapps", "AbstractUserConnector");
+ public static final ClassName ABSTRACT_PROFILE_CONNECTOR_BUILDER_CLASSNAME =
+ ClassName.get(
+ "com.google.android.enterprise.connectedapps.AbstractProfileConnector", "Builder");
+ public static final ClassName ABSTRACT_USER_CONNECTOR_BUILDER_CLASSNAME =
+ ClassName.get("com.google.android.enterprise.connectedapps.AbstractUserConnector", "Builder");
+ public static final ClassName CONNECTION_BINDER_CLASSNAME =
+ ClassName.get("com.google.android.enterprise.connectedapps", "ConnectionBinder");
+ public static final ClassName SCHEDULED_EXECUTOR_SERVICE_CLASSNAME =
+ ClassName.get("java.util.concurrent", "ScheduledExecutorService");
+ public static final ClassName ABSTRACT_FAKE_PROFILE_CONNECTOR_CLASSNAME =
+ ClassName.get(
+ "com.google.android.enterprise.connectedapps.testing", "AbstractFakeProfileConnector");
+
+ public static final ClassName VERSION_CLASSNAME = ClassName.get("android.os.Build", "VERSION");
+ public static final ClassName VERSION_CODES_CLASSNAME =
+ ClassName.get("android.os.Build", "VERSION_CODES");
+
+ private CommonClassNames() {}
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ConfigurationCodeGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ConfigurationCodeGenerator.java
new file mode 100644
index 0000000..a4647dd
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ConfigurationCodeGenerator.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.android.enterprise.connectedapps.annotations.CrossProfile;
+import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext;
+
+/**
+ * Generator of code for a single {@link CrossProfileConfiguration}.
+ *
+ * <p>The {@code Service} will only be generated if the configuration contains at least one provider
+ * class which has at least one {@link CrossProfile} type.
+ */
+class ConfigurationCodeGenerator {
+ private boolean generated = false;
+ private final CrossProfileConfigurationInfo configuration;
+ private final ServiceGenerator serviceGenerator;
+ private final DispatcherGenerator dispatcherGenerator;
+
+ ConfigurationCodeGenerator(
+ GeneratorContext generatorContext, CrossProfileConfigurationInfo configuration) {
+ this.configuration = checkNotNull(configuration);
+ this.serviceGenerator = new ServiceGenerator(checkNotNull(generatorContext), configuration);
+ this.dispatcherGenerator = new DispatcherGenerator(generatorContext, configuration);
+ }
+
+ void generate() {
+ if (generated) {
+ throw new IllegalStateException(
+ "ConfigurationCodeGenerator#generate can only be called once");
+ }
+ generated = true;
+
+ if (configuration.profileConnector() == null) {
+ // Without a connector we can't line things up so don't generate
+ return;
+ }
+
+ serviceGenerator.generate();
+ dispatcherGenerator.generate();
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileCallbackCodeGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileCallbackCodeGenerator.java
new file mode 100644
index 0000000..2822bf2
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileCallbackCodeGenerator.java
@@ -0,0 +1,501 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.ASYNC_CALLBACK_PARAM_MULTIMERGER_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.ASYNC_CALLBACK_PARAM_MULTIMERGER_COMPLETE_LISTENER_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.BUNDLER_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CROSS_PROFILE_CALLBACK_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CROSS_PROFILE_CALLBACK_EXCEPTION_PARCEL_CALL_SENDER_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CROSS_PROFILE_CALLBACK_PARCEL_CALL_SENDER_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.EXCEPTION_CALLBACK_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.LOCAL_CALLBACK_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_UTILITIES_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.stream.Collectors.joining;
+
+import com.google.android.enterprise.connectedapps.annotations.CrossProfileCallback;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackInterfaceInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext;
+import com.squareup.javapoet.AnnotationSpec;
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.FieldSpec;
+import com.squareup.javapoet.MethodSpec;
+import com.squareup.javapoet.ParameterizedTypeName;
+import com.squareup.javapoet.TypeName;
+import com.squareup.javapoet.TypeSpec;
+import java.util.Map;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.PackageElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.TypeMirror;
+
+/** Generator of code for a single {@link CrossProfileCallback}. */
+public class CrossProfileCallbackCodeGenerator {
+ private boolean generated = false;
+ private final GeneratorContext generatorContext;
+ private final GeneratorUtilities generatorUtilities;
+ private final CrossProfileCallbackInterfaceInfo callbackInterface;
+
+ private final TypeMirror voidTypeMirror;
+
+ CrossProfileCallbackCodeGenerator(
+ GeneratorContext generatorContext, CrossProfileCallbackInterfaceInfo callbackInterface) {
+ this.generatorContext = checkNotNull(generatorContext);
+ this.callbackInterface = checkNotNull(callbackInterface);
+ this.generatorUtilities = new GeneratorUtilities(generatorContext);
+
+ voidTypeMirror = generatorContext.elements().getTypeElement("java.lang.Void").asType();
+ }
+
+ void generate() {
+ if (generated) {
+ throw new IllegalStateException(
+ "CrossProfileCallbackCodeGenerator#generate can only be called once");
+ }
+ generated = true;
+
+ generateReceiverClass();
+ generateSenderClass();
+
+ if (callbackInterface.isSimple()) {
+ // There can be only one method
+ ExecutableElement method = callbackInterface.methods().get(0);
+ generateMultiInterface(method);
+ generateMultiMergerResultClass(method);
+ generateMultiMergerInputClass(method);
+ }
+ }
+
+ private void generateMultiInterface(ExecutableElement method) {
+ ClassName interfaceName =
+ getCrossProfileCallbackMultiInterfaceClassName(generatorContext, callbackInterface);
+
+ TypeSpec.Builder interfaceBuilder =
+ TypeSpec.interfaceBuilder(interfaceName)
+ .addJavadoc(
+ "Callback interface used when using a {@link $T} with multiple profiles.\n",
+ callbackInterface.interfaceElement())
+ .addModifiers(Modifier.PUBLIC);
+
+ addMultiMethod(interfaceBuilder, method);
+
+ generatorUtilities.writeClassToFile(interfaceName.packageName(), interfaceBuilder);
+ }
+
+ private void generateMultiMergerResultClass(ExecutableElement method) {
+ ClassName className =
+ getCrossProfileCallbackMultiMergerResultClassName(generatorContext, callbackInterface);
+
+ TypeMirror paramType =
+ method.getParameters().isEmpty() ? voidTypeMirror : method.getParameters().get(0).asType();
+
+ TypeName mergerInterface =
+ ParameterizedTypeName.get(
+ ASYNC_CALLBACK_PARAM_MULTIMERGER_COMPLETE_LISTENER_CLASSNAME,
+ ClassName.get(generatorUtilities.boxIfNecessary(paramType)));
+
+ ParameterizedTypeName multiParameterType = getMultiParameterType(paramType);
+
+ TypeSpec.Builder classBuilder =
+ TypeSpec.classBuilder(className)
+ .addJavadoc(
+ "Implementation of {@link $T} which forwards completed results to an instance of"
+ + " {@link $T}.\n",
+ ASYNC_CALLBACK_PARAM_MULTIMERGER_COMPLETE_LISTENER_CLASSNAME,
+ callbackInterface.interfaceElement())
+ .addSuperinterface(mergerInterface)
+ .addModifiers(Modifier.PUBLIC);
+
+ classBuilder.addField(
+ FieldSpec.builder(
+ getCrossProfileCallbackMultiInterfaceClassName(generatorContext, callbackInterface),
+ "callback")
+ .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.constructorBuilder()
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(
+ getCrossProfileCallbackMultiInterfaceClassName(generatorContext, callbackInterface),
+ "callback")
+ .addStatement("this.callback = callback")
+ .build());
+
+ String resultToPass = method.getParameters().isEmpty() ? "" : "results";
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("onResult")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(multiParameterType, "results")
+ .addStatement("callback.$L($L)", method.getSimpleName(), resultToPass)
+ .build());
+
+ generatorUtilities.writeClassToFile(className.packageName(), classBuilder);
+ }
+
+ private void generateMultiMergerInputClass(ExecutableElement method) {
+ ClassName className =
+ getCrossProfileCallbackMultiMergerInputClassName(generatorContext, callbackInterface);
+
+ TypeMirror paramType =
+ method.getParameters().isEmpty() ? voidTypeMirror : method.getParameters().get(0).asType();
+
+ TypeSpec.Builder classBuilder =
+ TypeSpec.classBuilder(className)
+ .addJavadoc(
+ "Implementation of {@link $T} which passes results into an instance of {@link"
+ + " $T}.\n",
+ callbackInterface.interfaceElement(),
+ ASYNC_CALLBACK_PARAM_MULTIMERGER_CLASSNAME)
+ .addSuperinterface(ClassName.get(callbackInterface.interfaceElement()))
+ .addModifiers(Modifier.PUBLIC);
+
+ classBuilder.addField(
+ FieldSpec.builder(PROFILE_CLASSNAME, "profileId")
+ .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
+ .build());
+
+ classBuilder.addField(
+ FieldSpec.builder(
+ ParameterizedTypeName.get(
+ ASYNC_CALLBACK_PARAM_MULTIMERGER_CLASSNAME,
+ ClassName.get(generatorUtilities.boxIfNecessary(paramType))),
+ "callback")
+ .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.constructorBuilder()
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(PROFILE_CLASSNAME, "profileId")
+ .addParameter(
+ ParameterizedTypeName.get(
+ ASYNC_CALLBACK_PARAM_MULTIMERGER_CLASSNAME,
+ ClassName.get(generatorUtilities.boxIfNecessary(paramType))),
+ "callback")
+ .addStatement("this.profileId = profileId")
+ .addStatement("this.callback = callback")
+ .build());
+
+ MethodSpec.Builder methodBuilder =
+ MethodSpec.methodBuilder(method.getSimpleName().toString())
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC);
+
+ if (!method.getParameters().isEmpty()) {
+ String paramName = method.getParameters().get(0).getSimpleName().toString();
+ methodBuilder.addParameter(ClassName.get(paramType), paramName);
+ methodBuilder.addStatement("callback.onResult(profileId, $L)", paramName);
+ } else {
+ methodBuilder.addStatement("callback.onResult(profileId, null)");
+ }
+
+ classBuilder.addMethod(methodBuilder.build());
+
+ generatorUtilities.writeClassToFile(className.packageName(), classBuilder);
+ }
+
+ private void addMultiMethod(TypeSpec.Builder interfaceBuilder, ExecutableElement method) {
+ if (method.getParameters().isEmpty()) {
+ interfaceBuilder.addMethod(
+ MethodSpec.methodBuilder(method.getSimpleName().toString())
+ .addModifiers(method.getModifiers())
+ .build());
+
+ return;
+ }
+
+ // There can be only one parameter
+ VariableElement param = method.getParameters().get(0);
+ ParameterizedTypeName paramType = getMultiParameterType(param.asType());
+
+ interfaceBuilder.addMethod(
+ MethodSpec.methodBuilder(method.getSimpleName().toString())
+ .addModifiers(method.getModifiers())
+ .addParameter(paramType, param.getSimpleName().toString())
+ .build());
+ }
+
+ private ParameterizedTypeName getMultiParameterType(TypeMirror paramType) {
+ TypeName boxedParamType = TypeName.get(generatorUtilities.boxIfNecessary(paramType));
+ return ParameterizedTypeName.get(ClassName.get(Map.class), PROFILE_CLASSNAME, boxedParamType);
+ }
+
+ private void generateReceiverClass() {
+ ClassName className =
+ getCrossProfileCallbackReceiverClassName(generatorContext, callbackInterface);
+
+ TypeSpec.Builder classBuilder =
+ TypeSpec.classBuilder(className)
+ .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
+ .addSuperinterface(ClassName.get(callbackInterface.interfaceElement()))
+ .addJavadoc(
+ "Implementation of {@link $1T} which wraps an {@link $2T},\n"
+ + "writing the callback value to a {@link $3T} and passing it to the {@link"
+ + " $2T}.\n",
+ callbackInterface.interfaceElement(),
+ CROSS_PROFILE_CALLBACK_CLASSNAME,
+ PARCEL_CLASSNAME);
+
+ classBuilder.addField(
+ FieldSpec.builder(CROSS_PROFILE_CALLBACK_CLASSNAME, "callback")
+ .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
+ .build());
+
+ classBuilder.addField(
+ FieldSpec.builder(BUNDLER_CLASSNAME, "bundler")
+ .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.constructorBuilder()
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(CROSS_PROFILE_CALLBACK_CLASSNAME, "callback")
+ .addParameter(BUNDLER_CLASSNAME, "bundler")
+ .beginControlFlow("if (callback == null || bundler == null)")
+ .addStatement("throw new $T()", NullPointerException.class)
+ .endControlFlow()
+ .addStatement("this.callback = callback")
+ .addStatement("this.bundler = bundler")
+ .build());
+
+ for (ExecutableElement method : callbackInterface.methods()) {
+ addReceiverMethod(classBuilder, callbackInterface, method);
+ }
+
+ generatorUtilities.writeClassToFile(className.packageName(), classBuilder);
+ }
+
+ private static void addReceiverMethod(
+ TypeSpec.Builder classBuilder,
+ CrossProfileCallbackInterfaceInfo callbackInterface,
+ ExecutableElement method) {
+ MethodSpec.Builder methodBuilder =
+ MethodSpec.methodBuilder(method.getSimpleName().toString())
+ .addModifiers(Modifier.PUBLIC)
+ .addAnnotation(Override.class)
+ .addAnnotation(
+ AnnotationSpec.builder(SuppressWarnings.class)
+ // Allow catching of Exception
+ .addMember("value", "\"CatchSpecificExceptionsChecker\"")
+ .build())
+ .addParameters(GeneratorUtilities.extractParametersFromMethod(method));
+
+ methodBuilder.beginControlFlow("try");
+
+ methodBuilder.addStatement(
+ "$1T callSender = new $1T(callback, /* methodIdentifier= */ $2L)",
+ CROSS_PROFILE_CALLBACK_PARCEL_CALL_SENDER_CLASSNAME,
+ callbackInterface.getIdentifier(method));
+
+ // parcel is recycled in this method
+ methodBuilder.addStatement("$1T parcel = $1T.obtain()", PARCEL_CLASSNAME);
+
+ for (VariableElement param : method.getParameters()) {
+ methodBuilder.addStatement(
+ "bundler.writeToParcel(parcel, $1L, $2L, /* flags= */ 0)",
+ param.getSimpleName(),
+ TypeUtils.generateBundlerType(param.asType()));
+ }
+
+ methodBuilder.addStatement("callSender.makeParcelCall(parcel)", PARCEL_CLASSNAME);
+
+ methodBuilder.addStatement("parcel.recycle()");
+
+ methodBuilder
+ .nextControlFlow("catch ($T e)", Exception.class)
+ .beginControlFlow("try")
+ .addStatement(
+ "$1T unavailableProfileException = new $1T(\"Error when writing callback result\", e)",
+ UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME)
+ // parcel is recycled in this method
+ .addStatement("$1T parcel = $1T.obtain()", PARCEL_CLASSNAME)
+ .addStatement(
+ "$T.writeThrowableToParcel(parcel, unavailableProfileException)",
+ PARCEL_UTILITIES_CLASSNAME)
+ .addStatement(
+ "$1T callSender = new $1T(callback)",
+ CROSS_PROFILE_CALLBACK_EXCEPTION_PARCEL_CALL_SENDER_CLASSNAME)
+ .addStatement("callSender.makeParcelCall(parcel)", PARCEL_CLASSNAME)
+ .addStatement("parcel.recycle()")
+ .nextControlFlow("catch ($T r)", UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME)
+ .addComment(
+ "TODO: Decide what should happen if the connection is dropped between the call and"
+ + " response")
+ .endControlFlow()
+ .endControlFlow();
+
+ classBuilder.addMethod(methodBuilder.build());
+ }
+
+ private void generateSenderClass() {
+ ClassName className =
+ getCrossProfileCallbackSenderClassName(generatorContext, callbackInterface);
+
+ TypeSpec.Builder classBuilder =
+ TypeSpec.classBuilder(className)
+ .addJavadoc(
+ "Implementation of {@link $1T} which wraps an instance of {@link $2T},\n"
+ + "extracting results and exceptions in callbacks and passing them on to the"
+ + " {@link $2T}.\n",
+ LOCAL_CALLBACK_CLASSNAME,
+ callbackInterface.interfaceElement())
+ .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
+ .addSuperinterface(LOCAL_CALLBACK_CLASSNAME);
+
+ classBuilder.addField(
+ FieldSpec.builder(ClassName.get(callbackInterface.interfaceElement()), "callback")
+ .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
+ .build());
+
+ classBuilder.addField(
+ FieldSpec.builder(EXCEPTION_CALLBACK_CLASSNAME, "exceptionCallback")
+ .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
+ .build());
+
+ classBuilder.addField(
+ FieldSpec.builder(BUNDLER_CLASSNAME, "bundler")
+ .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.constructorBuilder()
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(ClassName.get(callbackInterface.interfaceElement()), "callback")
+ .addParameter(EXCEPTION_CALLBACK_CLASSNAME, "exceptionCallback")
+ .addParameter(BUNDLER_CLASSNAME, "bundler")
+ .beginControlFlow("if (callback == null || bundler == null)")
+ .addStatement("throw new $T()", NullPointerException.class)
+ .endControlFlow()
+ .addStatement("this.callback = callback")
+ .addStatement("this.exceptionCallback = exceptionCallback")
+ .addStatement("this.bundler = bundler")
+ .build());
+
+ addSenderCallbackMethod(classBuilder);
+ addSenderExceptionMethod(classBuilder);
+
+ generatorUtilities.writeClassToFile(className.packageName(), classBuilder);
+ }
+
+ private void addSenderCallbackMethod(TypeSpec.Builder classBuilder) {
+ MethodSpec.Builder methodBuilder =
+ MethodSpec.methodBuilder("onResult")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(int.class, "methodIdentifier")
+ .addParameter(PARCEL_CLASSNAME, "params");
+ methodBuilder.beginControlFlow("switch (methodIdentifier)$>");
+
+ for (ExecutableElement method : callbackInterface.methods()) {
+ // $> means increase indentation, $< means decrease
+ methodBuilder.addCode("$<case $L:\n$>", callbackInterface.getIdentifier(method));
+ addDispatchCode(methodBuilder, method);
+ methodBuilder.addStatement("return");
+ }
+
+ methodBuilder.endControlFlow();
+
+ classBuilder.addMethod(methodBuilder.build());
+ }
+
+ private void addSenderExceptionMethod(TypeSpec.Builder classBuilder) {
+ MethodSpec.Builder methodBuilder =
+ MethodSpec.methodBuilder("onException")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(PARCEL_CLASSNAME, "exception");
+ methodBuilder.addStatement(
+ "$1T throwable = $2T.readThrowableFromParcel(exception)",
+ Throwable.class,
+ PARCEL_UTILITIES_CLASSNAME);
+
+ methodBuilder.addStatement("exceptionCallback.onException(throwable)");
+
+ classBuilder.addMethod(methodBuilder.build());
+ }
+
+ private void addDispatchCode(MethodSpec.Builder methodBuilder, ExecutableElement method) {
+ for (VariableElement parameter : method.getParameters()) {
+ methodBuilder.addStatement(
+ "@SuppressWarnings(\"unchecked\") $1T $2L = ($1T) bundler.readFromParcel(params, $3L)",
+ parameter.asType(),
+ parameter.getSimpleName().toString(),
+ TypeUtils.generateBundlerType(parameter.asType()));
+ }
+
+ String commaSeparatedParams =
+ method.getParameters().stream()
+ .map(p -> p.getSimpleName().toString())
+ .collect(joining(","));
+
+ methodBuilder.addStatement("callback.$L($L)", method.getSimpleName(), commaSeparatedParams);
+ }
+
+ static ClassName getCrossProfileCallbackMultiInterfaceClassName(
+ GeneratorContext generatorContext, CrossProfileCallbackInterfaceInfo callbackInterface) {
+ PackageElement originalPackage =
+ generatorContext.elements().getPackageOf(callbackInterface.interfaceElement());
+ String interfaceName = String.format("%s_Multi", callbackInterface.simpleName());
+
+ return ClassName.get(originalPackage.getQualifiedName().toString(), interfaceName);
+ }
+
+ static ClassName getCrossProfileCallbackMultiMergerResultClassName(
+ GeneratorContext generatorContext, CrossProfileCallbackInterfaceInfo callbackInterface) {
+ PackageElement originalPackage =
+ generatorContext.elements().getPackageOf(callbackInterface.interfaceElement());
+ String interfaceName =
+ String.format("Profile_%s_MultiMergerResult", callbackInterface.simpleName());
+
+ return ClassName.get(originalPackage.getQualifiedName().toString(), interfaceName);
+ }
+
+ static ClassName getCrossProfileCallbackMultiMergerInputClassName(
+ GeneratorContext generatorContext, CrossProfileCallbackInterfaceInfo callbackInterface) {
+ PackageElement originalPackage =
+ generatorContext.elements().getPackageOf(callbackInterface.interfaceElement());
+ String interfaceName =
+ String.format("Profile_%s_MultiMergerInput", callbackInterface.simpleName());
+
+ return ClassName.get(originalPackage.getQualifiedName().toString(), interfaceName);
+ }
+
+ static ClassName getCrossProfileCallbackReceiverClassName(
+ GeneratorContext generatorContext, CrossProfileCallbackInterfaceInfo callbackInterface) {
+ PackageElement originalPackage =
+ generatorContext.elements().getPackageOf(callbackInterface.interfaceElement());
+ String interfaceName = String.format("Profile_%s_Receiver", callbackInterface.simpleName());
+
+ return ClassName.get(originalPackage.getQualifiedName().toString(), interfaceName);
+ }
+
+ static ClassName getCrossProfileCallbackSenderClassName(
+ GeneratorContext generatorContext, CrossProfileCallbackInterfaceInfo callbackInterface) {
+ PackageElement originalPackage =
+ generatorContext.elements().getPackageOf(callbackInterface.interfaceElement());
+ String interfaceName = String.format("Profile_%s_Sender", callbackInterface.simpleName());
+
+ return ClassName.get(originalPackage.getQualifiedName().toString(), interfaceName);
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileTypeCodeGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileTypeCodeGenerator.java
new file mode 100644
index 0000000..075ac6c
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileTypeCodeGenerator.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext;
+import com.google.android.enterprise.connectedapps.processor.containers.ProviderClassInfo;
+
+class CrossProfileTypeCodeGenerator {
+ private boolean generated = false;
+ private final InterfaceGenerator interfaceGenerator;
+ private final CurrentProfileGenerator currentProfileGenerator;
+ private final OtherProfileGenerator otherProfileGenerator;
+ private final IfAvailableGenerator ifAvailableGenerator;
+ private final AlwaysThrowsGenerator alwaysThrowsGenerator;
+ private final MultipleProfilesGenerator multipleProfilesGenerator;
+ private final DefaultProfileClassGenerator defaultProfileClassGenerator;
+ private final InternalCrossProfileClassGenerator internalCrossProfileClassGenerator;
+ private final BundlerGenerator bundlerGenerator;
+
+ public CrossProfileTypeCodeGenerator(
+ GeneratorContext generatorContext,
+ ProviderClassInfo providerClass,
+ CrossProfileTypeInfo crossProfileType) {
+ checkNotNull(generatorContext);
+ checkNotNull(crossProfileType);
+ this.interfaceGenerator = new InterfaceGenerator(generatorContext, crossProfileType);
+ this.currentProfileGenerator = new CurrentProfileGenerator(generatorContext, crossProfileType);
+ this.otherProfileGenerator = new OtherProfileGenerator(generatorContext, crossProfileType);
+ this.ifAvailableGenerator = new IfAvailableGenerator(generatorContext, crossProfileType);
+ this.alwaysThrowsGenerator = new AlwaysThrowsGenerator(generatorContext, crossProfileType);
+ this.multipleProfilesGenerator =
+ new MultipleProfilesGenerator(generatorContext, crossProfileType);
+ this.defaultProfileClassGenerator =
+ new DefaultProfileClassGenerator(generatorContext, crossProfileType);
+ this.internalCrossProfileClassGenerator =
+ new InternalCrossProfileClassGenerator(generatorContext, providerClass, crossProfileType);
+ this.bundlerGenerator = new BundlerGenerator(generatorContext, crossProfileType);
+ }
+
+ void generate() {
+ if (generated) {
+ throw new IllegalStateException(
+ "CrossProfileTypeCodeGenerator#generate can only be called once");
+ }
+ generated = true;
+
+ interfaceGenerator.generate();
+ currentProfileGenerator.generate();
+ otherProfileGenerator.generate();
+ ifAvailableGenerator.generate();
+ alwaysThrowsGenerator.generate();
+ multipleProfilesGenerator.generate();
+ defaultProfileClassGenerator.generate();
+ internalCrossProfileClassGenerator.generate();
+ bundlerGenerator.generate();
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CurrentProfileGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CurrentProfileGenerator.java
new file mode 100644
index 0000000..103b965
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CurrentProfileGenerator.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CONTEXT_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.EXCEPTION_CALLBACK_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS;
+import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext;
+import com.squareup.javapoet.AnnotationSpec;
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.CodeBlock;
+import com.squareup.javapoet.FieldSpec;
+import com.squareup.javapoet.MethodSpec;
+import com.squareup.javapoet.TypeSpec;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.type.TypeKind;
+
+/**
+ * Generate the {@code Profile_*_CurrentProfile} class for a single crossProfileType class.
+ *
+ * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to
+ * validate that the annotated code is correct.
+ */
+final class CurrentProfileGenerator {
+
+ private boolean generated = false;
+ private final GeneratorContext generatorContext;
+ private final GeneratorUtilities generatorUtilities;
+ private final CrossProfileTypeInfo crossProfileType;
+
+ CurrentProfileGenerator(
+ GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) {
+ this.generatorContext = checkNotNull(generatorContext);
+ this.generatorUtilities = new GeneratorUtilities(generatorContext);
+ this.crossProfileType = checkNotNull(crossProfileType);
+ }
+
+ void generate() {
+ if (generated) {
+ throw new IllegalStateException("CurrentProfileGenerator#generate can only be called once");
+ }
+ generated = true;
+
+ generateCurrentProfileClass();
+ }
+
+ private void generateCurrentProfileClass() {
+ ClassName className = getCurrentProfileClassName(generatorContext, crossProfileType);
+
+ ClassName singleSenderInterface =
+ InterfaceGenerator.getSingleSenderInterfaceClassName(generatorContext, crossProfileType);
+ ClassName singleSenderCanThrowInterface =
+ InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName(
+ generatorContext, crossProfileType);
+
+ TypeSpec.Builder classBuilder =
+ TypeSpec.classBuilder(className)
+ .addJavadoc(
+ "Implementation of {@link $T} and {@link $T} which makes calls to the current"
+ + " profile.\n\n"
+ + "<p>{@link $T} will not be thrown by calls to methods in this class.\n",
+ singleSenderInterface,
+ singleSenderCanThrowInterface,
+ UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME)
+ .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
+ .addSuperinterface(singleSenderInterface)
+ .addSuperinterface(singleSenderCanThrowInterface);
+
+ addCrossProfileConstructor(classBuilder);
+
+ for (CrossProfileMethodInfo method : crossProfileType.crossProfileMethods()) {
+ generateMethodOnCurrentProfileClass(classBuilder, method, crossProfileType);
+
+ if (method.isCrossProfileCallback(generatorContext)) {
+ // To meet the interface for canThrow we need a version with exceptionCallback.
+ // However we never use it.
+ generateCrossProfileCallbackWithExceptionMethodOnCurrentProfileClass(
+ classBuilder, method, crossProfileType);
+ }
+ }
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("timeout")
+ .addAnnotation(Override.class)
+ .addAnnotation(
+ AnnotationSpec.builder(SuppressWarnings.class)
+ .addMember("value", "$S", "GoodTime")
+ .build())
+ .addModifiers(Modifier.PUBLIC)
+ .returns(className)
+ .addParameter(long.class, "timeout")
+ .addStatement("return this")
+ .build());
+
+ ClassName ifAvailableClass =
+ IfAvailableGenerator.getIfAvailableClassName(generatorContext, crossProfileType);
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("ifAvailable")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(ifAvailableClass)
+ .addStatement("return new $T(this)", ifAvailableClass)
+ .build());
+
+ generatorUtilities.writeClassToFile(className.packageName(), classBuilder);
+ }
+
+ private void addCrossProfileConstructor(TypeSpec.Builder classBuilder) {
+ classBuilder.addField(
+ FieldSpec.builder(CONTEXT_CLASSNAME, "context")
+ .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
+ .build());
+
+ MethodSpec.Builder constructorBuilder =
+ MethodSpec.constructorBuilder()
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(CONTEXT_CLASSNAME, "context")
+ .addStatement("this.context = context");
+
+ if (!crossProfileType.isStatic()) {
+ classBuilder.addField(
+ FieldSpec.builder(crossProfileType.className(), "crossProfileType")
+ .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
+ .build());
+
+ constructorBuilder
+ .addParameter(crossProfileType.className(), "crossProfileType")
+ .addStatement("this.crossProfileType = crossProfileType");
+ }
+
+ classBuilder.addMethod(constructorBuilder.build());
+ }
+
+ private void generateMethodOnCurrentProfileClass(
+ TypeSpec.Builder classBuilder,
+ CrossProfileMethodInfo method,
+ CrossProfileTypeInfo crossProfileType) {
+
+ CodeBlock crossProfileTypeReference =
+ method.isStatic()
+ ? CodeBlock.of("$1T", crossProfileType.className())
+ : CodeBlock.of("crossProfileType");
+
+ CodeBlock methodCall =
+ CodeBlock.of(
+ "$L.$L($L)",
+ crossProfileTypeReference,
+ method.simpleName(),
+ method.commaSeparatedParameters(
+ crossProfileType.supportedTypes(), REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS));
+
+ if (method.returnType().getKind() != TypeKind.VOID) {
+ methodCall = CodeBlock.of("return $L", methodCall);
+ }
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder(method.simpleName())
+ .addAnnotation(Override.class)
+ .addExceptions(method.thrownExceptions())
+ .addModifiers(Modifier.PUBLIC)
+ .returns(method.returnTypeTypeName())
+ .addParameters(
+ GeneratorUtilities.extractParametersFromMethod(
+ crossProfileType.supportedTypes(),
+ method.methodElement(),
+ REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS))
+ .addStatement(methodCall)
+ .build());
+ }
+
+ private static void generateCrossProfileCallbackWithExceptionMethodOnCurrentProfileClass(
+ TypeSpec.Builder classBuilder,
+ CrossProfileMethodInfo method,
+ CrossProfileTypeInfo crossProfileType) {
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder(method.simpleName())
+ .addModifiers(Modifier.PUBLIC)
+ .returns(method.returnTypeTypeName())
+ .addParameters(
+ GeneratorUtilities.extractParametersFromMethod(
+ crossProfileType.supportedTypes(),
+ method.methodElement(),
+ REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS))
+ .addParameter(EXCEPTION_CALLBACK_CLASSNAME, "exceptionCallback")
+ .addStatement(
+ "$L($L)",
+ method.simpleName(),
+ method.commaSeparatedParameters(
+ crossProfileType.supportedTypes(), REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS))
+ .build());
+ }
+
+ static ClassName getCurrentProfileClassName(
+ GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) {
+ return GeneratorUtilities.appendToClassName(
+ crossProfileType.profileClassName(), "_CurrentProfile");
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DefaultProfileClassGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DefaultProfileClassGenerator.java
new file mode 100644
index 0000000..114a19c
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DefaultProfileClassGenerator.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import static com.google.android.enterprise.connectedapps.processor.AlwaysThrowsGenerator.getAlwaysThrowsClassName;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_AWARE_UTILS_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_CONNECTOR_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.VERSION_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.VERSION_CODES_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CurrentProfileGenerator.getCurrentProfileClassName;
+import static com.google.android.enterprise.connectedapps.processor.InterfaceGenerator.getMultipleSenderInterfaceClassName;
+import static com.google.android.enterprise.connectedapps.processor.InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName;
+import static com.google.android.enterprise.connectedapps.processor.InterfaceGenerator.getSingleSenderInterfaceClassName;
+import static com.google.android.enterprise.connectedapps.processor.MultipleProfilesGenerator.getMultipleProfilesClassName;
+import static com.google.android.enterprise.connectedapps.processor.OtherProfileGenerator.getOtherProfileClassName;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector.ProfileType;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext;
+import com.squareup.javapoet.ArrayTypeName;
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.FieldSpec;
+import com.squareup.javapoet.MethodSpec;
+import com.squareup.javapoet.ParameterizedTypeName;
+import com.squareup.javapoet.TypeSpec;
+import java.util.HashMap;
+import java.util.Map;
+import javax.lang.model.element.Modifier;
+
+/**
+ * Generate the {@code DefaultProfile*} class for each cross-profile type.
+ *
+ * <p>This is intended to be initialised and used once, which will generate all needed code.
+ *
+ * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to
+ * validate that the annotated code is correct.
+ */
+final class DefaultProfileClassGenerator {
+
+ private boolean generated = false;
+ private final GeneratorContext generatorContext;
+ private final GeneratorUtilities generatorUtilities;
+ private final CrossProfileTypeInfo crossProfileType;
+
+ DefaultProfileClassGenerator(
+ GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) {
+ this.generatorContext = checkNotNull(generatorContext);
+ this.generatorUtilities = new GeneratorUtilities(generatorContext);
+ this.crossProfileType = checkNotNull(crossProfileType);
+ }
+
+ void generate() {
+ if (generated) {
+ throw new IllegalStateException(
+ "DefaultProfileClassGenerator#generate can only be called once");
+ }
+ generated = true;
+
+ generateDefaultProfileClass();
+ }
+
+ private void generateDefaultProfileClass() {
+ ClassName className = getDefaultProfileClassName(generatorContext, crossProfileType);
+
+ ClassName connectorClassName =
+ crossProfileType.profileConnector().isPresent()
+ ? crossProfileType.profileConnector().get().connectorClassName()
+ : PROFILE_CONNECTOR_CLASSNAME;
+
+ ClassName crossProfileTypeInterfaceClassName =
+ InterfaceGenerator.getCrossProfileTypeInterfaceClassName(
+ generatorContext, crossProfileType);
+
+ TypeSpec.Builder classBuilder =
+ TypeSpec.classBuilder(className)
+ .addJavadoc(
+ "Default implementation of {@link $T} to be used in production.\n",
+ crossProfileTypeInterfaceClassName)
+ .addModifiers(Modifier.FINAL);
+
+ classBuilder.addSuperinterface(crossProfileTypeInterfaceClassName);
+
+ classBuilder.addField(
+ FieldSpec.builder(connectorClassName, "connector")
+ .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.constructorBuilder()
+ .addParameter(connectorClassName, "connector")
+ .addModifiers(Modifier.PUBLIC)
+ .addStatement("this.connector = connector")
+ .build());
+
+ addCurrentMethod(classBuilder);
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("other")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType))
+ .beginControlFlow("if ($T.SDK_INT < $T.O)", VERSION_CLASSNAME, VERSION_CODES_CLASSNAME)
+ .addStatement(
+ "return new $T($S)",
+ getAlwaysThrowsClassName(generatorContext, crossProfileType),
+ "Cross-profile calls are not supported on this version of Android")
+ .endControlFlow()
+ .addStatement(
+ "return new $T(connector)",
+ getOtherProfileClassName(generatorContext, crossProfileType))
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("personal")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType))
+ .beginControlFlow("if ($T.SDK_INT < $T.O)", VERSION_CLASSNAME, VERSION_CODES_CLASSNAME)
+ .addStatement(
+ "return new $T($S)",
+ getAlwaysThrowsClassName(generatorContext, crossProfileType),
+ "Cross-profile calls are not supported on this version of Android")
+ .endControlFlow()
+ .addStatement("return profile(connector.utils().getPersonalProfile())")
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("work")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType))
+ .beginControlFlow("if ($T.SDK_INT < $T.O)", VERSION_CLASSNAME, VERSION_CODES_CLASSNAME)
+ .addStatement(
+ "return new $T($S)",
+ getAlwaysThrowsClassName(generatorContext, crossProfileType),
+ "Cross-profile calls are not supported on this version of Android")
+ .endControlFlow()
+ .addStatement("return profile(connector.utils().getWorkProfile())")
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("profile")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(PROFILE_CLASSNAME, "profile")
+ .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType))
+ .beginControlFlow("if ($T.SDK_INT < $T.O)", VERSION_CLASSNAME, VERSION_CODES_CLASSNAME)
+ .addStatement(
+ "return new $T($S)",
+ getAlwaysThrowsClassName(generatorContext, crossProfileType),
+ "Cross-profile calls are not supported on this version of Android")
+ .endControlFlow()
+ .beginControlFlow("if (profile.isCurrent())")
+ .addStatement(
+ "return ($T) current()",
+ getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType))
+ .nextControlFlow("else")
+ .addComment("must be other profile")
+ .addStatement("return other()")
+ .endControlFlow()
+ .build());
+
+ ParameterizedTypeName senderMapType =
+ ParameterizedTypeName.get(
+ ClassName.get(Map.class),
+ PROFILE_CLASSNAME,
+ InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName(
+ generatorContext, crossProfileType));
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("profiles")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(ArrayTypeName.of(PROFILE_CLASSNAME), "profiles")
+ .varargs(true)
+ .returns(getMultipleSenderInterfaceClassName(generatorContext, crossProfileType))
+ .addStatement("$T senders = new $T<>()", senderMapType, HashMap.class)
+ .beginControlFlow("for ($1T profileIdentifier : profiles)", PROFILE_CLASSNAME)
+ .addStatement("senders.put(profileIdentifier, profile(profileIdentifier))")
+ .endControlFlow()
+ .addStatement(
+ "return new $1T(senders)",
+ getMultipleProfilesClassName(generatorContext, crossProfileType))
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("both")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(getMultipleSenderInterfaceClassName(generatorContext, crossProfileType))
+ .addStatement("$1T utils = connector.utils()", PROFILE_AWARE_UTILS_CLASSNAME)
+ .addStatement(
+ "$1T currentProfileIdentifier = utils.getCurrentProfile()", PROFILE_CLASSNAME)
+ .beginControlFlow("if ($T.SDK_INT < $T.O)", VERSION_CLASSNAME, VERSION_CODES_CLASSNAME)
+ .addStatement("$T senders = new $T<>()", senderMapType, HashMap.class)
+ .addStatement(
+ "senders.put(currentProfileIdentifier, ($T) current())",
+ InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName(
+ generatorContext, crossProfileType))
+ .addStatement(
+ "return new $1T(senders)",
+ getMultipleProfilesClassName(generatorContext, crossProfileType))
+ .endControlFlow()
+ .addStatement("$1T otherProfileIdentifier = utils.getOtherProfile()", PROFILE_CLASSNAME)
+ .addStatement("return profiles(currentProfileIdentifier, otherProfileIdentifier)")
+ .build());
+
+ if (!crossProfileType.profileConnector().isPresent()
+ || crossProfileType.profileConnector().get().primaryProfile() != ProfileType.NONE) {
+ generatePrimarySecondaryMethods(classBuilder);
+ }
+
+ generatorUtilities.writeClassToFile(className.packageName(), classBuilder);
+ }
+
+ private void addCurrentMethod(TypeSpec.Builder classBuilder) {
+ MethodSpec.Builder currentMethodBuilder =
+ MethodSpec.methodBuilder("current")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(getSingleSenderInterfaceClassName(generatorContext, crossProfileType));
+
+ if (crossProfileType.isStatic()) {
+ currentMethodBuilder.addStatement(
+ "return new $1T(connector.applicationContext())",
+ getCurrentProfileClassName(generatorContext, crossProfileType));
+ } else {
+ currentMethodBuilder.addStatement(
+ "return new $1T(connector.applicationContext(),"
+ + " $2T.instance().crossProfileType(connector.applicationContext()))",
+ getCurrentProfileClassName(generatorContext, crossProfileType),
+ InternalCrossProfileClassGenerator.getInternalCrossProfileClassName(
+ generatorContext, crossProfileType));
+ }
+
+ classBuilder.addMethod(currentMethodBuilder.build());
+ }
+
+ private void generatePrimarySecondaryMethods(TypeSpec.Builder classBuilder) {
+ generatePrimaryMethod(classBuilder);
+ generateSecondaryMethod(classBuilder);
+ generateSuppliersMethod(classBuilder);
+ }
+
+ private void generatePrimaryMethod(TypeSpec.Builder classBuilder) {
+ MethodSpec.Builder methodBuilder =
+ MethodSpec.methodBuilder("primary")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType))
+ .beginControlFlow("if ($T.SDK_INT < $T.O)", VERSION_CLASSNAME, VERSION_CODES_CLASSNAME)
+ .addStatement(
+ "return new $T($S)",
+ getAlwaysThrowsClassName(generatorContext, crossProfileType),
+ "Cross-profile calls are not supported on this version of Android")
+ .endControlFlow()
+ .addStatement(
+ "$T primaryProfile = connector.utils().getPrimaryProfile()", PROFILE_CLASSNAME)
+ .beginControlFlow("if (primaryProfile == null)")
+ .addStatement("throw new $T(\"No primary profile set\")", IllegalStateException.class)
+ .endControlFlow()
+ .addStatement("return profile(primaryProfile)");
+
+ classBuilder.addMethod(methodBuilder.build());
+ }
+
+ private void generateSecondaryMethod(TypeSpec.Builder classBuilder) {
+ MethodSpec.Builder methodBuilder =
+ MethodSpec.methodBuilder("secondary")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType))
+ .beginControlFlow("if ($T.SDK_INT < $T.O)", VERSION_CLASSNAME, VERSION_CODES_CLASSNAME)
+ .addStatement(
+ "return new $T($S)",
+ getAlwaysThrowsClassName(generatorContext, crossProfileType),
+ "Cross-profile calls are not supported on this version of Android")
+ .endControlFlow()
+ .addStatement(
+ "$T secondaryProfile = connector.utils().getSecondaryProfile()", PROFILE_CLASSNAME)
+ .beginControlFlow("if (secondaryProfile == null)")
+ .addStatement("throw new $T(\"No primary profile set\")", IllegalStateException.class)
+ .endControlFlow()
+ .addStatement("return profile(secondaryProfile)");
+
+ classBuilder.addMethod(methodBuilder.build());
+ }
+
+ private void generateSuppliersMethod(TypeSpec.Builder classBuilder) {
+ MethodSpec.Builder methodBuilder =
+ MethodSpec.methodBuilder("suppliers")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(getMultipleSenderInterfaceClassName(generatorContext, crossProfileType))
+ .beginControlFlow("if ($T.SDK_INT < $T.O)", VERSION_CLASSNAME, VERSION_CODES_CLASSNAME)
+ .addStatement("return both()")
+ .endControlFlow()
+ .addStatement("$1T utils = connector.utils()", PROFILE_AWARE_UTILS_CLASSNAME)
+ .addStatement("$1T currentProfile = utils.getCurrentProfile()", PROFILE_CLASSNAME)
+ .addStatement("$1T secondaryProfile = utils.getSecondaryProfile()", PROFILE_CLASSNAME)
+ .beginControlFlow("if (secondaryProfile == null)")
+ .addStatement("throw new $T(\"No primary profile set\")", IllegalStateException.class)
+ .endControlFlow()
+ .addStatement("return profiles(currentProfile, secondaryProfile)");
+
+ classBuilder.addMethod(methodBuilder.build());
+ }
+
+ static ClassName getDefaultProfileClassName(
+ GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) {
+ return ClassName.get(
+ crossProfileType.profileClassName().packageName(),
+ "Default" + crossProfileType.profileClassName().simpleName());
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DispatcherGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DispatcherGenerator.java
new file mode 100644
index 0000000..ff45251
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DispatcherGenerator.java
@@ -0,0 +1,326 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.BACKGROUND_EXCEPTION_THROWER_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.BINDER_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CONTEXT_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CROSS_PROFILE_CALLBACK_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CROSS_PROFILE_SENDER_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_CALL_RECEIVER_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_UTILITIES_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.ServiceGenerator.getConnectedAppsServiceClassName;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.stream.Collectors.joining;
+
+import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext;
+import com.google.android.enterprise.connectedapps.processor.containers.ProviderClassInfo;
+import com.squareup.javapoet.AnnotationSpec;
+import com.squareup.javapoet.ArrayTypeName;
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.CodeBlock;
+import com.squareup.javapoet.FieldSpec;
+import com.squareup.javapoet.MethodSpec;
+import com.squareup.javapoet.TypeSpec;
+import java.util.List;
+import javax.lang.model.element.Modifier;
+
+/**
+ * Generate the {@code *_Dispatcher} class for a single {@link CrossProfileConfiguration} annotated
+ * class.
+ *
+ * <p>This class includes the dispatch of calls to providers.
+ *
+ * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to
+ * validate that the annotated code is correct.
+ */
+final class DispatcherGenerator {
+
+ private boolean generated = false;
+ private final GeneratorContext generatorContext;
+ private final GeneratorUtilities generatorUtilities;
+ private final CrossProfileConfigurationInfo configuration;
+
+ DispatcherGenerator(
+ GeneratorContext generatorContext, CrossProfileConfigurationInfo configuration) {
+ this.generatorContext = checkNotNull(generatorContext);
+ this.generatorUtilities = new GeneratorUtilities(generatorContext);
+ this.configuration = checkNotNull(configuration);
+ }
+
+ void generate() {
+ if (generated) {
+ throw new IllegalStateException("DispatcherGenerator#generate can only be called once");
+ }
+ generated = true;
+
+ generateDispatcherClass();
+ }
+
+ private void generateDispatcherClass() {
+ ClassName className = getDispatcherClassName(generatorContext, configuration);
+
+ TypeSpec.Builder classBuilder =
+ TypeSpec.classBuilder(className)
+ .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
+ .addJavadoc(
+ "Class for dispatching calls to appropriate providers.\n\n"
+ + "<p>This uses a {@link $T} to construct calls before passing the completed"
+ + " call\n"
+ + "to a provider.\n",
+ PARCEL_CALL_RECEIVER_CLASSNAME);
+
+ classBuilder.addField(
+ FieldSpec.builder(PARCEL_CALL_RECEIVER_CLASSNAME, "parcelCallReceiver")
+ .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
+ .initializer("new $T()", PARCEL_CALL_RECEIVER_CLASSNAME)
+ .build());
+
+ addEnsureValidCallerMethod(classBuilder);
+ addCallMethod(classBuilder);
+ addPrepareCallMethod(classBuilder);
+ addFetchResponseMethod(classBuilder);
+
+ generatorUtilities.writeClassToFile(className.packageName(), classBuilder);
+ }
+
+ private static void addPrepareCallMethod(TypeSpec.Builder classBuilder) {
+ MethodSpec prepareCallMethod =
+ MethodSpec.methodBuilder("prepareCall")
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(CONTEXT_CLASSNAME, "context")
+ .addParameter(long.class, "callId")
+ .addParameter(int.class, "blockId")
+ .addParameter(int.class, "numBytes")
+ .addParameter(ArrayTypeName.of(byte.class), "paramBytes")
+ .addStatement("ensureValidCaller(context)")
+ .addStatement("parcelCallReceiver.prepareCall(callId, blockId, numBytes, paramBytes)")
+ .addJavadoc(
+ "Store a block of bytes to be part of a future call to\n"
+ + "{@link #call(Context, long, int, long, int, byte[], ICrossProfileCallback)}."
+ + "\n\n"
+ + "@param callId Arbitrary identifier used to link together\n"
+ + " {@link #prepareCall(Context, long, int, int, byte[])} and\n "
+ + "{@link #call(Context, long, int, long, int, byte[], ICrossProfileCallback)}"
+ + " calls.\n"
+ + "@param blockId The (zero indexed) number of this block. Each block should"
+ + " be\n {@link $1T#MAX_BYTES_PER_BLOCK} bytes so the total number of blocks"
+ + " is\n {@code numBytes / $1T#MAX_BYTES_PER_BLOCK}.\n"
+ + "@param numBytes The total number of bytes being transferred (across all"
+ + " blocks for this call,\n including the final {@link"
+ + " #call(Context, long, int, long, int, byte[], ICrossProfileCallback)}.\n"
+ + "@param paramBytes The bytes for this block. Should contain\n {@link"
+ + " $1T#MAX_BYTES_PER_BLOCK} bytes.\n\n"
+ + "@see $2T#prepareCall(long, int, int, byte[])",
+ CROSS_PROFILE_SENDER_CLASSNAME,
+ PARCEL_CALL_RECEIVER_CLASSNAME)
+ .build();
+ classBuilder.addMethod(prepareCallMethod);
+ }
+
+ private static void addFetchResponseMethod(TypeSpec.Builder classBuilder) {
+ MethodSpec prepareCallMethod =
+ MethodSpec.methodBuilder("fetchResponse")
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(CONTEXT_CLASSNAME, "context")
+ .addParameter(long.class, "callId")
+ .addParameter(int.class, "blockId")
+ .returns(ArrayTypeName.of(byte.class))
+ .addStatement("ensureValidCaller(context)")
+ .addStatement("return parcelCallReceiver.getPreparedResponse(callId, blockId)")
+ .addJavadoc(
+ "Fetch a response block if a previous call to\n {@link #call(Context, long, int,"
+ + " long, int, byte[], ICrossProfileCallback)} returned a\n byte array with"
+ + " 1 as the first byte.\n\n"
+ + "@param callId should be the same callId used with\n {@link #call(Context,"
+ + " long, int, long, int, byte[], ICrossProfileCallback)}\n"
+ + "@param blockId The (zero indexed) number of the block to fetch.\n\n"
+ + "@see $1T#getPreparedResponse(long, int)\n",
+ PARCEL_CALL_RECEIVER_CLASSNAME)
+ .build();
+ classBuilder.addMethod(prepareCallMethod);
+ }
+
+ private static void addEnsureValidCallerMethod(TypeSpec.Builder classBuilder) {
+ CodeBlock.Builder methodCode = CodeBlock.builder();
+
+ methodCode.addStatement(
+ "$T[] callingPackageNames ="
+ + " context.getPackageManager().getPackagesForUid($T.getCallingUid())",
+ String.class,
+ BINDER_CLASSNAME);
+ methodCode.beginControlFlow("for (String callingPackageName : callingPackageNames)");
+ methodCode.beginControlFlow("if (context.getPackageName().equals(callingPackageName))");
+ methodCode.addStatement("return");
+ methodCode.endControlFlow();
+ methodCode.endControlFlow();
+
+ methodCode.addStatement(
+ "throw new $T(\"Cross-profile functionality is only available within the same package\")",
+ IllegalStateException.class);
+
+ MethodSpec ensureValidCallerMethod =
+ MethodSpec.methodBuilder("ensureValidCaller")
+ .addModifiers(Modifier.PRIVATE, Modifier.STATIC)
+ .addParameter(CONTEXT_CLASSNAME, "context")
+ .addCode(methodCode.build())
+ .build();
+
+ classBuilder.addMethod(ensureValidCallerMethod);
+ }
+
+ private void addCallMethod(TypeSpec.Builder classBuilder) {
+ CodeBlock.Builder methodCode = CodeBlock.builder();
+
+ methodCode.beginControlFlow("try");
+
+ methodCode.addStatement("ensureValidCaller(context)");
+
+ methodCode.addStatement(
+ "$1T parcel = parcelCallReceiver.getPreparedCall(callId, blockId, paramBytes)",
+ PARCEL_CLASSNAME);
+
+ List<ProviderClassInfo> providers = configuration.providers().asList();
+
+ if (!providers.isEmpty()) {
+ addProviderDispatch(methodCode, providers);
+ }
+
+ methodCode.addStatement(
+ "throw new $T(\"Unknown type identifier \" + crossProfileTypeIdentifier)",
+ IllegalArgumentException.class);
+
+ methodCode.nextControlFlow("catch ($T e)", RuntimeException.class);
+ // parcel is recycled in this method
+ methodCode.addStatement("$1T throwableParcel = $1T.obtain()", PARCEL_CLASSNAME);
+ methodCode.add("throwableParcel.writeInt(1); //errors\n");
+ methodCode.addStatement(
+ "$T.writeThrowableToParcel(throwableParcel, e)", PARCEL_UTILITIES_CLASSNAME);
+ methodCode.addStatement(
+ "$1T throwableBytes = parcelCallReceiver.prepareResponse(callId, throwableParcel)",
+ ArrayTypeName.of(byte.class));
+ methodCode.addStatement("throwableParcel.recycle()");
+
+ methodCode.addStatement("$T.throwInBackground(e)", BACKGROUND_EXCEPTION_THROWER_CLASSNAME);
+
+ methodCode.addStatement("return throwableBytes");
+ methodCode.endControlFlow();
+
+ MethodSpec callMethod =
+ MethodSpec.methodBuilder("call")
+ .addModifiers(Modifier.PUBLIC)
+ .returns(ArrayTypeName.of(byte.class))
+ .addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)
+ // Allow catching of RuntimeException
+ .addMember("value", "\"CatchSpecificExceptionsChecker\"")
+ .build())
+ .addParameter(CONTEXT_CLASSNAME, "context")
+ .addParameter(long.class, "callId")
+ .addParameter(int.class, "blockId")
+ .addParameter(long.class, "crossProfileTypeIdentifier")
+ .addParameter(int.class, "methodIdentifier")
+ .addParameter(ArrayTypeName.of(byte.class), "paramBytes")
+ .addParameter(CROSS_PROFILE_CALLBACK_CLASSNAME, "callback")
+ .addCode(methodCode.build())
+ .addJavadoc(
+ "Make a call, which will execute some annotated method and return a response.\n\n"
+ + "<p>The parameters to the call should be contained in a {@link $1T}"
+ + " marshalled into\n"
+ + "a byte array. If the byte array is larger than {@link"
+ + " $2T#MAX_BYTES_PER_BLOCK},\n"
+ + "then it should be separated into blocks of {@link"
+ + " $2T#MAX_BYTES_PER_BLOCK}\n"
+ + "bytes, and {@link #prepareCall(Context, long, int, int, byte[])} used to"
+ + " set all but the final\n"
+ + "block, before calling this method with the final block.\n\n"
+ + "<p>The response will be an array of bytes. If the response is complete (it"
+ + " fits into a single\n"
+ + "block), then the first byte will be 0, otherwise the first byte will be 1"
+ + " and the next 4 bytes\n"
+ + "will be an int representing the total size of the return value. The rest of"
+ + " the bytes are the\n"
+ + "first block of the return value. {@link #fetchResponse(Context, long, int)"
+ + " should be used to\n"
+ + "fetch further blocks.\n\n"
+ + "@param callId Arbitrary identifier used to link together\n"
+ + " {@link #prepareCall(Context, long, int, int, byte[])} and\n"
+ + " {@link #call(Context, long, int, long, int, byte[],"
+ + " ICrossProfileCallback)} calls.\n"
+ + "@param blockId The (zero indexed) number of this block. Each block should"
+ + " be\n {@link CrossProfileSender#MAX_BYTES_PER_BLOCK} bytes so the total"
+ + " number of blocks is\n {@code numBytes /"
+ + " CrossProfileSender#MAX_BYTES_PER_BLOCK}.\n"
+ + "@param crossProfileTypeIdentifier The generated identifier for the type"
+ + " which contains the\n method being called.\n"
+ + "@param methodIdentifier The index of the method being called on the cross"
+ + " profile type.\n"
+ + "@param paramBytes The bytes for the final block, this will be merged with"
+ + " any blocks\n previously set by a call to"
+ + " {@link #prepareCall(Context, long, int, int, byte[])}.\n"
+ + "@param callback A callback to be used if this is an asynchronous call."
+ + " Otherwise this should be\n {@code null}.\n\n"
+ + "@see $3T#getPreparedCall(long, int, byte[])\n",
+ PARCEL_CLASSNAME,
+ CROSS_PROFILE_SENDER_CLASSNAME,
+ PARCEL_CALL_RECEIVER_CLASSNAME)
+ .build();
+
+ classBuilder.addMethod(callMethod);
+ }
+
+ private void addProviderDispatch(
+ CodeBlock.Builder methodCode, List<ProviderClassInfo> providers) {
+ for (ProviderClassInfo provider : providers) {
+ addProviderDispatchInner(methodCode, provider);
+ }
+ }
+
+ private void addProviderDispatchInner(CodeBlock.Builder methodCode, ProviderClassInfo provider) {
+ String condition =
+ provider.allCrossProfileTypes().stream()
+ .map(
+ h ->
+ "crossProfileTypeIdentifier == "
+ + h.identifier()
+ + "L // "
+ + h.crossProfileTypeElement().getQualifiedName()
+ + "\n")
+ .collect(joining(" || "));
+
+ methodCode.beginControlFlow("if ($L)", condition);
+ methodCode.addStatement(
+ "$1T returnParcel = $2T.instance().call(context.getApplicationContext(),"
+ + " crossProfileTypeIdentifier, methodIdentifier, parcel, callback)",
+ PARCEL_CLASSNAME,
+ InternalProviderClassGenerator.getInternalProviderClassName(generatorContext, provider));
+ methodCode.addStatement(
+ "$1T returnBytes = parcelCallReceiver.prepareResponse(callId, returnParcel)",
+ ArrayTypeName.of(byte.class));
+ methodCode.addStatement("parcel.recycle()");
+ methodCode.addStatement("returnParcel.recycle()");
+ methodCode.addStatement("return returnBytes");
+ methodCode.endControlFlow();
+ }
+
+ static ClassName getDispatcherClassName(
+ GeneratorContext generatorContext, CrossProfileConfigurationInfo configuration) {
+ ClassName serviceName = getConnectedAppsServiceClassName(generatorContext, configuration);
+ return ClassName.get(serviceName.packageName(), serviceName.simpleName() + "_Dispatcher");
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/EarlyValidator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/EarlyValidator.java
new file mode 100644
index 0000000..f1f566d
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/EarlyValidator.java
@@ -0,0 +1,1297 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCELABLE_CREATOR_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.GeneratorUtilities.findCrossProfileMethodsInClass;
+import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.hasCrossProfileAnnotation;
+import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.hasCrossProfileConfigurationAnnotation;
+import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.hasCrossProfileConfigurationsAnnotation;
+import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.hasCrossProfileProviderAnnotation;
+import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.validationMessageFormatterFor;
+import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.validationMessageFormatterForClass;
+import static java.util.stream.Collectors.toMap;
+import static java.util.stream.Collectors.toSet;
+
+import com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper;
+import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper;
+import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector;
+import com.google.android.enterprise.connectedapps.annotations.CustomUserConnector;
+import com.google.android.enterprise.connectedapps.processor.SupportedTypes.TypeCheckContext;
+import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileAnnotationInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackAnnotationInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileProviderAnnotationInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.FutureWrapperAnnotationInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.ParcelableWrapperAnnotationInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.ProfileConnectorInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.UserConnectorInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.ValidatorContext;
+import com.google.android.enterprise.connectedapps.processor.containers.ValidatorCrossProfileConfigurationInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.ValidatorCrossProfileTestInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.ValidatorCrossProfileTypeInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.ValidatorProviderClassInfo;
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.ParameterizedTypeName;
+import com.squareup.javapoet.TypeName;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.PackageElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.util.ElementFilter;
+import javax.tools.Diagnostic.Kind;
+
+/** Validator to check that annotations have been used correctly before generating code. */
+public final class EarlyValidator {
+
+ private static final String MULTIPLE_PROVIDERS_ERROR =
+ "The @CROSS_PROFILE_ANNOTATION annotated type %s has been provided more than once";
+ private static final String PROVIDING_NON_CROSS_PROFILE_TYPE_ERROR =
+ "Methods annotated @CROSS_PROFILE_PROVIDER_ANNOTATION must only return"
+ + " @CROSS_PROFILE_ANNOTATION annotated types";
+ private static final String INVALID_CONSTRUCTORS_ERROR =
+ "Provider classes must have a single public constructor which takes either a single Context"
+ + " argument or no arguments";
+ private static final String PROVIDER_INCORRECT_ARGS_ERROR =
+ "Methods annotated @CROSS_PROFILE_PROVIDER_ANNOTATION can only take a single Context"
+ + " argument, or no-args";
+ private static final String STATIC_PROVIDER_ERROR =
+ "Methods annotated @CROSS_PROFILE_PROVIDER_ANNOTATION can not be static";
+ private static final String UNSUPPORTED_RETURN_TYPE_ERROR =
+ "The type %s cannot be returned by methods annotated @CROSS_PROFILE_ANNOTATION";
+ private static final String UNSUPPORTED_PARAMETER_TYPE_CROSS_PROFILE_METHOD =
+ "The type %s cannot be used by parameters of methods annotated @CROSS_PROFILE_ANNOTATION";
+ private static final String UNSUPPORTED_PARAMETER_TYPE_CROSS_ASYNC_CALLBACK =
+ "The type %s cannot be used by parameters of methods on interfaces annotated"
+ + " @CROSS_PROFILE_CALLBACK_ANNOTATION";
+ private static final String CROSS_PROFILE_TYPE_DEFAULT_PACKAGE_ERROR =
+ "@CROSS_PROFILE_ANNOTATION types must not be in the default package";
+ private static final String NON_PUBLIC_CROSS_PROFILE_TYPE_ERROR =
+ "@CROSS_PROFILE_ANNOTATION types must be public";
+ private static final String NOT_A_PROVIDER_CLASS_ERROR =
+ "All classes specified in 'providers' must be provider classes";
+ private static final String CONNECTOR_MUST_BE_INTERFACE = "Connectors must be interfaces";
+ private static final String CONNECTOR_MUST_EXTEND_CONNECTOR =
+ "Interfaces specified as a connector must extend ProfileConnector";
+ private static final String CUSTOM_PROFILE_CONNECTOR_MUST_BE_INTERFACE =
+ "@CustomProfileConnector must only be applied to interfaces";
+ private static final String CUSTOM_USER_CONNECTOR_MUST_BE_INTERFACE =
+ "@CustomUserConnector must only be applied to interfaces";
+ private static final String GENERATED_PROFILE_CONNECTOR_MUST_BE_INTERFACE =
+ "@GeneratedProfileConnector must only be applied to interfaces";
+ private static final String GENERATED_USER_CONNECTOR_MUST_BE_INTERFACE =
+ "@GeneratedUserConnector must only be applied to interfaces";
+ private static final String CUSTOM_PROFILE_CONNECTOR_MUST_EXTEND_CONNECTOR =
+ "Interfaces annotated with @CustomProfileConnector must extend ProfileConnector";
+ private static final String CUSTOM_USER_CONNECTOR_MUST_EXTEND_CONNECTOR =
+ "Interfaces annotated with @CustomUserConnector must extend UserConnector";
+ private static final String GENERATED_PROFILE_CONNECTOR_MUST_EXTEND_PROFILE_CONNECTOR =
+ "Interfaces annotated with @GeneratedProfileConnector must extend ProfileConnector";
+ private static final String GENERATED_USER_CONNECTOR_MUST_EXTEND_USER_CONNECTOR =
+ "Interfaces annotated with @GeneratedUserConnector must extend UserConnector";
+ private static final String CALLBACK_INTERFACE_DEFAULT_PACKAGE_ERROR =
+ "Interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION must not be in the default package";
+ private static final String NOT_INTERFACE_ERROR =
+ "Only interfaces may be annotated @CROSS_PROFILE_CALLBACK_ANNOTATION";
+ private static final String NOT_ONE_METHOD_ERROR =
+ "Interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION(simple=true) must have exactly one"
+ + " method";
+ private static final String NO_METHODS_ERROR =
+ "Interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION must have at least one method";
+ private static final String DEFAULT_METHOD_ERROR =
+ "Interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION must have no default methods";
+ private static final String STATIC_METHOD_ERROR =
+ "Interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION must have no static methods";
+ private static final String NOT_VOID_ERROR =
+ "Methods on interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION must return void";
+ private static final String GENERIC_CALLBACK_INTERFACE_ERROR =
+ "Interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION can not be generic";
+ private static final String MORE_THAN_ONE_PARAMETER_ERROR =
+ "Methods on interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION(simple=true) can only"
+ + " take a single parameter";
+ private static final String MULTIPLE_ASYNC_CALLBACK_PARAMETERS_ERROR =
+ "Methods annotated @CROSS_PROFILE_ANNOTATION can have a maximum of one parameter of a type"
+ + " annotated @CROSS_PROFILE_CALLBACK_ANNOTATION";
+ private static final String NON_VOID_CALLBACK_ERROR =
+ "Methods annotated @CROSS_PROFILE_ANNOTATION which take a parameter type annotated"
+ + " @CROSS_PROFILE_CALLBACK_ANNOTATION must return void";
+ private static final String METHOD_ISSTATIC_ERROR =
+ "@CROSS_PROFILE_ANNOTATION annotations on methods can not specify isStatic";
+ private static final String METHOD_CONNECTOR_ERROR =
+ "@CROSS_PROFILE_ANNOTATION annotations on methods can not specify a connector";
+ private static final String METHOD_PARCELABLE_WRAPPERS_ERROR =
+ "@CROSS_PROFILE_ANNOTATION annotations on methods can not specify parcelable wrappers";
+ private static final String METHOD_CLASSNAME_ERROR =
+ "@CROSS_PROFILE_ANNOTATION annotations on methods can not specify a profile class name";
+ private static final String INVALID_TIMEOUT_MILLIS = "timeoutMillis must be positive";
+ private static final String ADDITIONAL_PROFILE_CONNECTOR_METHODS_ERROR =
+ "Interfaces annotated with @GeneratedProfileConnector can not declare non-static methods";
+ private static final String ADDITIONAL_USER_CONNECTOR_METHODS_ERROR =
+ "Interfaces annotated with @GeneratedUserConnector can not declare non-static methods";
+ private static final String NOT_A_CONFIGURATION_ERROR =
+ "Configurations referenced in a @CROSS_PROFILE_TEST_ANNOTATION annotation must be annotated"
+ + " @CROSS_PROFILE_CONFIGURATION_ANNOTATION or @CROSS_PROFILE_CONFIGURATIONS_ANNOTATION";
+ private static final String ASYNC_DECLARED_EXCEPTION_ERROR =
+ "Asynchronous methods annotated @CROSS_PROFILE_ANNOTATION cannot declare exceptions";
+ private static final String NOT_PARCELABLE_ERROR =
+ "Classes annotated @CustomParcelableWrapper must implement Parcelable";
+ private static final String INCORRECT_OF_METHOD =
+ "Classes annotated @CustomParcelableWrapper must have a static 'of' method which takes a"
+ + " Bundler, a BundlerType, and an instance of the wrapped type as arguments and returns"
+ + " an instance of the parcelable wrapper";
+ private static final String INCORRECT_GET_METHOD =
+ "Classes annotated @CustomParcelableWrapper must have a static 'get' method which takes no"
+ + " arguments and returns an instance of the wrapped class";
+ private static final String INCORRECT_PARCELABLE_IMPLEMENTATION =
+ "Classes annotated @CustomParcelableWrapper must correctly implement Parcelable";
+ private static final String PARCELABLE_WRAPPER_ANNOTATION_ERROR =
+ "Parcelable Wrappers must be annotated @CustomParcelableWrapper";
+ private static final String DOES_NOT_EXTEND_FUTURE_WRAPPER_ERROR =
+ "Classes annotated @CustomFutureWrapper must extend FutureWrapper";
+ private static final String INCORRECT_CREATE_METHOD_ERROR =
+ "Classes annotated @CustomFutureWrapper must have a create method which returns an instance"
+ + " of the class and takes a Bundler and BundlerType argument";
+ private static final String INCORRECT_GET_FUTURE_METHOD_ERROR =
+ "Classes annotated @CustomFutureWrapper must have a getFuture method which returns an"
+ + " instance of the wrapped future and takes no arguments";
+ private static final String INCORRECT_RESOLVE_CALLBACK_WHEN_FUTURE_IS_SET_METHOD_ERROR =
+ "Classes annotated @CustomFutureWrapper must have a writeFutureResult method"
+ + " which returns void and takes as arguments an instance of the wrapped future and a"
+ + " FutureResultWriter";
+ private static final String INCORRECT_GROUP_RESULTS_METHOD_ERROR =
+ "Classes annotated @CustomFutureWrapper must have a groupResults method which returns an"
+ + " instance of the wrapped future containing a map from Profile to the wrapped future"
+ + " type, and takes as an argument a map from Profile to an instance of the wrapped"
+ + " future";
+ private static final String FUTURE_WRAPPER_ANNOTATION_ERROR =
+ "Future Wrappers must be annotated @CustomFutureWrapper";
+ private static final String IMPORTS_NOT_PROFILE_CONNECTOR_ERROR =
+ "Classes included in includes= must be annotated @CustomProfileConnector";
+ private static final String IMPORTS_NOT_USER_CONNECTOR_ERROR =
+ "Classes included in includes= must be annotated @CustomUserConnector";
+ private static final String MUST_HAVE_ONE_TYPE_PARAMETER_ERROR =
+ "Classes annotated @CustomFutureWrapper must have a single type parameter";
+ private static final String NOT_STATIC_ERROR =
+ "Types annotated @CROSS_PROFILE_ANNOTATION(isStatic=true) must not contain any non-static"
+ + " methods annotated @CROSS_PROFILE_ANNOTATION";
+ private static final String METHOD_STATICTYPES_ERROR =
+ "@CROSS_PROFILE_PROVIDER_ANNOTATION annotations on methods can not specify staticTypes";
+
+ private final ValidatorContext validatorContext;
+ private final TypeMirror contextType;
+ private final TypeMirror profileConnectorType;
+ private final TypeMirror userConnectorType;
+ private final TypeMirror parcelableType;
+ private final TypeMirror bundlerType;
+ private final TypeMirror bundlerTypeType;
+ private final TypeMirror futureResultWriterType;
+ private final TypeMirror profileType;
+
+ EarlyValidator(ValidatorContext validatorContext) {
+ this.validatorContext = validatorContext;
+ contextType = validatorContext.elements().getTypeElement("android.content.Context").asType();
+
+ parcelableType = validatorContext.elements().getTypeElement("android.os.Parcelable").asType();
+
+ profileConnectorType =
+ validatorContext
+ .elements()
+ .getTypeElement("com.google.android.enterprise.connectedapps.ProfileConnector")
+ .asType();
+
+ userConnectorType =
+ validatorContext
+ .elements()
+ .getTypeElement("com.google.android.enterprise.connectedapps.UserConnector")
+ .asType();
+
+ bundlerType =
+ validatorContext
+ .elements()
+ .getTypeElement("com.google.android.enterprise.connectedapps.internal.Bundler")
+ .asType();
+
+ bundlerTypeType =
+ validatorContext
+ .elements()
+ .getTypeElement("com.google.android.enterprise.connectedapps.internal.BundlerType")
+ .asType();
+
+ futureResultWriterType =
+ validatorContext
+ .elements()
+ .getTypeElement(
+ "com.google.android.enterprise.connectedapps.internal.FutureResultWriter")
+ .asType();
+
+ profileType =
+ validatorContext
+ .elements()
+ .getTypeElement("com.google.android.enterprise.connectedapps.Profile")
+ .asType();
+ }
+
+ /**
+ * Validate code.
+ *
+ * <p>This will show errors for all issues found. It will not terminate upon finding the first
+ * error.
+ *
+ * @return True if the code is valid
+ */
+ boolean validate() {
+
+ return Stream.of(
+ validateProfileConnectorInterfaces(validatorContext.newProfileConnectorInterfaces()),
+ validateUserConnectorInterfaces(validatorContext.newUserConnectorInterfaces()),
+ validateGeneratedProfileConnectors(validatorContext.newGeneratedProfileConnectors()),
+ validateGeneratedUserConnectors(validatorContext.newGeneratedUserConnectors()),
+ validateConfigurations(validatorContext.newConfigurations()),
+ validateCrossProfileTypes(validatorContext.newCrossProfileTypes()),
+ validateProviderMethods(validatorContext.newProviderMethods()),
+ validateProviderClasses(validatorContext.newProviderClasses()),
+ validateCrossProfileCallbackInterfaces(
+ validatorContext.newCrossProfileCallbackInterfaces()),
+ validateCrossProfileTests(validatorContext.newCrossProfileTests()),
+ validateCustomParcelableWrappers(validatorContext.newCustomParcelableWrappers()),
+ validateCustomFutureWrappers(validatorContext.newCustomFutureWrappers()))
+ .allMatch(b -> b);
+ }
+
+ private boolean validateProfileConnectorInterfaces(
+ Collection<ProfileConnectorInfo> connectorInterfaces) {
+ boolean isValid = true;
+
+ for (ProfileConnectorInfo connectorInterface : connectorInterfaces) {
+ isValid = validateProfileConnectorInterface(connectorInterface) && isValid;
+ }
+
+ return isValid;
+ }
+
+ private boolean validateProfileConnectorInterface(ProfileConnectorInfo connectorInterface) {
+ boolean isValid = true;
+
+ if (!connectorInterface.connectorElement().getKind().equals(ElementKind.INTERFACE)) {
+ showError(CUSTOM_PROFILE_CONNECTOR_MUST_BE_INTERFACE, connectorInterface.connectorElement());
+ isValid = false;
+ }
+
+ if (!implementsInterface(connectorInterface.connectorElement(), profileConnectorType)) {
+ showError(
+ CUSTOM_PROFILE_CONNECTOR_MUST_EXTEND_CONNECTOR, connectorInterface.connectorElement());
+ isValid = false;
+ }
+
+ for (TypeElement parcelableWrapper : connectorInterface.parcelableWrapperClasses()) {
+ if (parcelableWrapper.getAnnotation(CustomParcelableWrapper.class) == null) {
+ showError(PARCELABLE_WRAPPER_ANNOTATION_ERROR, connectorInterface.connectorElement());
+ }
+ }
+
+ for (TypeElement futureWrapper : connectorInterface.futureWrapperClasses()) {
+ if (futureWrapper.getAnnotation(CustomFutureWrapper.class) == null) {
+ isValid = false;
+ showError(FUTURE_WRAPPER_ANNOTATION_ERROR, connectorInterface.connectorElement());
+ }
+ }
+
+ for (TypeElement importer : connectorInterface.importsClasses()) {
+ if (importer.getAnnotation(CustomProfileConnector.class) == null) {
+ isValid = false;
+ showError(IMPORTS_NOT_PROFILE_CONNECTOR_ERROR, connectorInterface.connectorElement());
+ showError(IMPORTS_NOT_PROFILE_CONNECTOR_ERROR, connectorInterface.connectorElement());
+ }
+ }
+
+ return isValid;
+ }
+
+ private boolean validateUserConnectorInterfaces(
+ Collection<UserConnectorInfo> connectorInterfaces) {
+ boolean isValid = true;
+
+ for (UserConnectorInfo connectorInterface : connectorInterfaces) {
+ isValid = validateUserConnectorInterface(connectorInterface) && isValid;
+ }
+
+ return isValid;
+ }
+
+ private boolean validateUserConnectorInterface(UserConnectorInfo connectorInterface) {
+ boolean isValid = true;
+
+ if (!connectorInterface.connectorElement().getKind().equals(ElementKind.INTERFACE)) {
+ showError(CUSTOM_USER_CONNECTOR_MUST_BE_INTERFACE, connectorInterface.connectorElement());
+ isValid = false;
+ }
+
+ if (!implementsInterface(connectorInterface.connectorElement(), userConnectorType)) {
+ showError(CUSTOM_USER_CONNECTOR_MUST_EXTEND_CONNECTOR, connectorInterface.connectorElement());
+ isValid = false;
+ }
+
+ for (TypeElement parcelableWrapper : connectorInterface.parcelableWrapperClasses()) {
+ if (parcelableWrapper.getAnnotation(CustomParcelableWrapper.class) == null) {
+ showError(PARCELABLE_WRAPPER_ANNOTATION_ERROR, connectorInterface.connectorElement());
+ }
+ }
+
+ for (TypeElement futureWrapper : connectorInterface.futureWrapperClasses()) {
+ if (futureWrapper.getAnnotation(CustomFutureWrapper.class) == null) {
+ isValid = false;
+ showError(FUTURE_WRAPPER_ANNOTATION_ERROR, connectorInterface.connectorElement());
+ }
+ }
+
+ for (TypeElement importer : connectorInterface.importsClasses()) {
+ if (importer.getAnnotation(CustomUserConnector.class) == null) {
+ isValid = false;
+ showError(IMPORTS_NOT_USER_CONNECTOR_ERROR, connectorInterface.connectorElement());
+ }
+ }
+
+ return isValid;
+ }
+
+ private boolean validateGeneratedProfileConnectors(Collection<TypeElement> generatedConnectors) {
+ boolean isValid = true;
+
+ for (TypeElement generatedConnector : generatedConnectors) {
+ isValid = validateGeneratedProfileConnector(generatedConnector) && isValid;
+ }
+
+ return isValid;
+ }
+
+ private boolean validateGeneratedProfileConnector(TypeElement generatedConnector) {
+ boolean isValid = true;
+
+ if (!generatedConnector.getKind().equals(ElementKind.INTERFACE)) {
+ showError(GENERATED_PROFILE_CONNECTOR_MUST_BE_INTERFACE, generatedConnector);
+ isValid = false;
+ }
+
+ if (!implementsInterface(generatedConnector, profileConnectorType)) {
+ showError(GENERATED_PROFILE_CONNECTOR_MUST_EXTEND_PROFILE_CONNECTOR, generatedConnector);
+ isValid = false;
+ }
+
+ if (generatedConnector.getEnclosedElements().stream()
+ .anyMatch(i -> !i.getModifiers().contains(Modifier.STATIC))) {
+ showError(ADDITIONAL_PROFILE_CONNECTOR_METHODS_ERROR, generatedConnector);
+ isValid = false;
+ }
+
+ return isValid;
+ }
+
+ private boolean validateGeneratedUserConnectors(Collection<TypeElement> generatedConnectors) {
+ boolean isValid = true;
+
+ for (TypeElement generatedConnector : generatedConnectors) {
+ isValid = validateGeneratedUserConnector(generatedConnector) && isValid;
+ }
+
+ return isValid;
+ }
+
+ private boolean validateGeneratedUserConnector(TypeElement generatedConnector) {
+ boolean isValid = true;
+
+ if (!generatedConnector.getKind().equals(ElementKind.INTERFACE)) {
+ showError(GENERATED_USER_CONNECTOR_MUST_BE_INTERFACE, generatedConnector);
+ isValid = false;
+ }
+
+ if (!implementsInterface(generatedConnector, userConnectorType)) {
+ showError(GENERATED_USER_CONNECTOR_MUST_EXTEND_USER_CONNECTOR, generatedConnector);
+ isValid = false;
+ }
+
+ if (generatedConnector.getEnclosedElements().stream()
+ .anyMatch(i -> !i.getModifiers().contains(Modifier.STATIC))) {
+ showError(ADDITIONAL_USER_CONNECTOR_METHODS_ERROR, generatedConnector);
+ isValid = false;
+ }
+
+ return isValid;
+ }
+
+ private boolean implementsInterface(TypeElement type, TypeMirror interfaceType) {
+ for (TypeMirror t : type.getInterfaces()) {
+ if (validatorContext.types().isSameType(t, interfaceType)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean validateConfigurations(
+ Collection<ValidatorCrossProfileConfigurationInfo> configurations) {
+ boolean isValid = true;
+
+ for (ValidatorCrossProfileConfigurationInfo configuration : configurations) {
+ isValid = validateConfiguration(configuration) && isValid;
+ }
+
+ return isValid;
+ }
+
+ private boolean validateConfiguration(ValidatorCrossProfileConfigurationInfo configuration) {
+ boolean isValid = true;
+
+ for (TypeElement providerClass : configuration.providerClassElements()) {
+ if (!hasCrossProfileProviderAnnotation(providerClass)
+ && GeneratorUtilities.findCrossProfileProviderMethodsInClass(providerClass).isEmpty()) {
+ showError(NOT_A_PROVIDER_CLASS_ERROR, configuration.configurationElement());
+ isValid = false;
+ }
+ }
+
+ if (configuration.connector().isPresent()
+ && !configuration.connector().get().getKind().equals(ElementKind.INTERFACE)) {
+ showError(CONNECTOR_MUST_BE_INTERFACE, configuration.configurationElement());
+ isValid = false;
+ }
+
+ if (configuration.connector().isPresent()
+ && !implementsInterface(configuration.connector().get(), profileConnectorType)) {
+ showError(CONNECTOR_MUST_EXTEND_CONNECTOR, configuration.configurationElement());
+ isValid = false;
+ }
+
+ return isValid;
+ }
+
+ private boolean validateCrossProfileTypes(
+ Collection<ValidatorCrossProfileTypeInfo> crossProfileTypes) {
+ boolean isValid =
+ validateCrossProfileTypesAreProvided(
+ crossProfileTypes.stream()
+ .map(ValidatorCrossProfileTypeInfo::crossProfileTypeElement)
+ .collect(toSet()),
+ validatorContext.newProviderMethods(),
+ validatorContext.newProviderClasses());
+
+ for (ValidatorCrossProfileTypeInfo crossProfileType : crossProfileTypes) {
+ isValid = validateCrossProfileType(crossProfileType) && isValid;
+ }
+
+ return isValid;
+ }
+
+ private boolean validateCrossProfileType(ValidatorCrossProfileTypeInfo crossProfileType) {
+ boolean isValid = true;
+
+ PackageElement packageElement =
+ (PackageElement) crossProfileType.crossProfileTypeElement().getEnclosingElement();
+ if (packageElement.getQualifiedName().toString().isEmpty()) {
+ showError(
+ CROSS_PROFILE_TYPE_DEFAULT_PACKAGE_ERROR,
+ crossProfileType.crossProfileTypeElement(),
+ validationMessageFormatterForClass(crossProfileType.crossProfileTypeElement()));
+ isValid = false;
+ }
+
+ if (!crossProfileType.crossProfileTypeElement().getModifiers().contains(Modifier.PUBLIC)) {
+ showError(
+ NON_PUBLIC_CROSS_PROFILE_TYPE_ERROR,
+ crossProfileType.crossProfileTypeElement(),
+ validationMessageFormatterForClass(crossProfileType.crossProfileTypeElement()));
+ isValid = false;
+ }
+
+ if (crossProfileType.isStatic()) {
+ for (ExecutableElement crossProfileMethod : crossProfileType.crossProfileMethods()) {
+ if (!crossProfileMethod.getModifiers().contains(Modifier.STATIC)) {
+ showError(NOT_STATIC_ERROR, crossProfileMethod);
+ isValid = false;
+ }
+ }
+ }
+
+ if (crossProfileType.profileConnector().isPresent()
+ && !crossProfileType
+ .profileConnector()
+ .get()
+ .connectorElement()
+ .getKind()
+ .equals(ElementKind.INTERFACE)) {
+ showError(CONNECTOR_MUST_BE_INTERFACE, crossProfileType.crossProfileTypeElement());
+ isValid = false;
+ }
+
+ if (crossProfileType.profileConnector().isPresent()
+ && !implementsInterface(
+ crossProfileType.profileConnector().get().connectorElement(), profileConnectorType)) {
+ showError(CONNECTOR_MUST_EXTEND_CONNECTOR, crossProfileType.crossProfileTypeElement());
+ isValid = false;
+ }
+
+ if (crossProfileType.timeoutMillis() <= 0) {
+ showError(INVALID_TIMEOUT_MILLIS, crossProfileType.crossProfileTypeElement());
+ isValid = false;
+ }
+
+ for (TypeElement parcelableWrapper : crossProfileType.parcelableWrapperClasses()) {
+ if (parcelableWrapper.getAnnotation(CustomParcelableWrapper.class) == null) {
+ showError(PARCELABLE_WRAPPER_ANNOTATION_ERROR, crossProfileType.crossProfileTypeElement());
+ }
+ }
+
+ for (TypeElement futureWrapper : crossProfileType.futureWrapperClasses()) {
+ if (futureWrapper.getAnnotation(CustomFutureWrapper.class) == null) {
+ isValid = false;
+ showError(FUTURE_WRAPPER_ANNOTATION_ERROR, crossProfileType.crossProfileTypeElement());
+ }
+ }
+
+ isValid =
+ crossProfileType.crossProfileMethods().stream()
+ .map(m -> validateCrossProfileMethod(crossProfileType, m))
+ .allMatch(b -> b)
+ && isValid;
+
+ return isValid;
+ }
+
+ private boolean validateCrossProfileTypesAreProvided(
+ Collection<TypeElement> crossProfileTypeElements,
+ Collection<ExecutableElement> providerMethods,
+ Collection<ValidatorProviderClassInfo> providerClasses) {
+ Map<String, Collection<Element>> crossProfileTypeProviders =
+ crossProfileTypeElements.stream()
+ .collect(toMap(element -> element.asType().toString(), element -> new HashSet<>()));
+
+ for (ExecutableElement provider : providerMethods) {
+ String providedTypeName = provider.getReturnType().toString();
+
+ if (crossProfileTypeProviders.containsKey(providedTypeName)) {
+ crossProfileTypeProviders.get(providedTypeName).add(provider);
+ }
+ }
+
+ for (ValidatorProviderClassInfo provider : providerClasses) {
+ for (TypeElement staticType : provider.staticTypes()) {
+ String providedTypeName = staticType.getQualifiedName().toString();
+
+ if (crossProfileTypeProviders.containsKey(providedTypeName)) {
+ crossProfileTypeProviders.get(providedTypeName).add(provider.providerClassElement());
+ }
+ }
+ }
+
+ boolean isValid = true;
+
+ for (String crossProfileType : crossProfileTypeProviders.keySet()) {
+ Collection<Element> providers = crossProfileTypeProviders.get(crossProfileType);
+
+ if (providers.size() > 1) {
+ isValid = false;
+ for (Element providerElement : providers) {
+ showError(String.format(MULTIPLE_PROVIDERS_ERROR, crossProfileType), providerElement);
+ }
+ }
+ }
+
+ return isValid;
+ }
+
+ private boolean validateProviderMethods(Collection<ExecutableElement> providerMethods) {
+ boolean isValid = true;
+
+ for (ExecutableElement providerMethod : providerMethods) {
+ TypeElement crossProfileType =
+ validatorContext.elements().getTypeElement(providerMethod.getReturnType().toString());
+ if (!hasCrossProfileAnnotation(crossProfileType)
+ && findCrossProfileMethodsInClass(crossProfileType).isEmpty()) {
+ showError(PROVIDING_NON_CROSS_PROFILE_TYPE_ERROR, providerMethod);
+ isValid = false;
+ }
+
+ if (providerMethod.getParameters().stream()
+ .anyMatch(v -> !validatorContext.types().isSameType(v.asType(), contextType))
+ || providerMethod.getParameters().size() > 1) {
+ showError(PROVIDER_INCORRECT_ARGS_ERROR, providerMethod);
+ isValid = false;
+ }
+
+ if (providerMethod.getModifiers().contains(Modifier.STATIC)) {
+ showError(STATIC_PROVIDER_ERROR, providerMethod);
+ isValid = false;
+ }
+
+ CrossProfileProviderAnnotationInfo annotationInfo =
+ AnnotationFinder.extractCrossProfileProviderAnnotationInfo(
+ providerMethod, validatorContext.types(), validatorContext.elements());
+
+ if (!annotationInfo.staticTypes().isEmpty()) {
+ showError(METHOD_STATICTYPES_ERROR, providerMethod);
+ isValid = false;
+ }
+ }
+
+ return isValid;
+ }
+
+ private boolean validateProviderClasses(Collection<ValidatorProviderClassInfo> providerClasses) {
+ boolean isValid = true;
+
+ for (ValidatorProviderClassInfo provider : providerClasses) {
+ if (!hasValidProviderClassConstructor(provider.providerClassElement())) {
+ showError(INVALID_CONSTRUCTORS_ERROR, provider.providerClassElement());
+ isValid = false;
+ }
+
+ if (provider.providerClassElement().getEnclosedElements().stream()
+ .filter(e -> e instanceof ExecutableElement)
+ .map(e -> (ExecutableElement) e)
+ .filter(e -> e.getKind().equals(ElementKind.CONSTRUCTOR))
+ .filter(e -> e.getModifiers().contains(Modifier.PUBLIC))
+ .count()
+ > 1) {
+ showError(INVALID_CONSTRUCTORS_ERROR, provider.providerClassElement());
+ isValid = false;
+ }
+ }
+
+ return isValid;
+ }
+
+ private boolean hasValidProviderClassConstructor(TypeElement clazz) {
+ for (ExecutableElement constructor :
+ ElementFilter.constructorsIn(clazz.getEnclosedElements())) {
+ if (constructor.getModifiers().contains(Modifier.PUBLIC)) {
+ if (isValidProviderClassConstructor(constructor)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private boolean isValidProviderClassConstructor(ExecutableElement constructor) {
+ if (constructor.getParameters().size() == 0) {
+ return true;
+ }
+
+ if (constructor.getParameters().size() > 1) {
+ return false;
+ }
+
+ return validatorContext
+ .types()
+ .isSameType(constructor.getParameters().iterator().next().asType(), contextType);
+ }
+
+ private boolean validateCrossProfileMethod(
+ ValidatorCrossProfileTypeInfo crossProfileType, ExecutableElement crossProfileMethod) {
+ boolean isValid = true;
+
+ CrossProfileAnnotationInfo crossProfileAnnotation =
+ AnnotationFinder.extractCrossProfileAnnotationInfo(
+ crossProfileMethod, validatorContext.types(), validatorContext.elements());
+
+ if (!crossProfileAnnotation.connectorIsDefault()) {
+ showError(METHOD_CONNECTOR_ERROR, crossProfileMethod);
+ isValid = false;
+ }
+
+ if (!crossProfileAnnotation.parcelableWrapperClasses().isEmpty()) {
+ showError(METHOD_PARCELABLE_WRAPPERS_ERROR, crossProfileMethod);
+ isValid = false;
+ }
+
+ if (!crossProfileAnnotation.isProfileClassNameDefault()) {
+ showError(METHOD_CLASSNAME_ERROR, crossProfileMethod);
+ isValid = false;
+ }
+
+ if (crossProfileAnnotation.timeoutMillis().isPresent()
+ && crossProfileAnnotation.timeoutMillis().get() <= 0) {
+ showError(INVALID_TIMEOUT_MILLIS, crossProfileMethod);
+ isValid = false;
+ }
+
+ if (!crossProfileMethod.getThrownTypes().isEmpty()) {
+ if (CrossProfileMethodInfo.isFuture(crossProfileType.supportedTypes(), crossProfileMethod)
+ || CrossProfileMethodInfo.getCrossProfileCallbackParam(
+ validatorContext.elements(), crossProfileMethod)
+ .isPresent()) {
+ showError(ASYNC_DECLARED_EXCEPTION_ERROR, crossProfileMethod);
+ isValid = false;
+ }
+ }
+
+ if (crossProfileAnnotation.isStatic()) {
+ showError(METHOD_ISSTATIC_ERROR, crossProfileMethod);
+ isValid = false;
+ }
+
+ isValid =
+ isValid
+ && validateReturnType(crossProfileType, crossProfileMethod)
+ && validateParameterTypesForCrossProfileMethod(crossProfileType, crossProfileMethod);
+ return isValid;
+ }
+
+ private boolean validateReturnType(
+ ValidatorCrossProfileTypeInfo crossProfileType, ExecutableElement crossProfileMethod) {
+ TypeMirror returnType = crossProfileMethod.getReturnType();
+
+ if (crossProfileType.supportedTypes().isValidReturnType(returnType)) {
+ return true;
+ }
+
+ showError(String.format(UNSUPPORTED_RETURN_TYPE_ERROR, returnType), crossProfileMethod);
+ return false;
+ }
+
+ private boolean validateParameterTypesForCrossProfileMethod(
+ ValidatorCrossProfileTypeInfo crossProfileType, ExecutableElement crossProfileMethod) {
+ boolean isValid =
+ crossProfileMethod.getParameters().stream()
+ .allMatch(p -> validateParameterTypeForCrossProfileMethod(crossProfileType, p));
+
+ List<TypeElement> crossProfileCallbackParameters =
+ crossProfileMethod.getParameters().stream()
+ .map(v -> validatorContext.elements().getTypeElement(v.asType().toString()))
+ .filter(Objects::nonNull)
+ .filter(AnnotationFinder::hasCrossProfileCallbackAnnotation)
+ .collect(Collectors.toList());
+
+ if (crossProfileCallbackParameters.size() > 1) {
+ isValid = false;
+ showError(MULTIPLE_ASYNC_CALLBACK_PARAMETERS_ERROR, crossProfileMethod);
+ }
+
+ if (crossProfileCallbackParameters.size() == 1) {
+ if (!crossProfileMethod.getReturnType().getKind().equals(TypeKind.VOID)) {
+ isValid = false;
+ showError(NON_VOID_CALLBACK_ERROR, crossProfileMethod);
+ }
+
+ isValid =
+ validateParameterTypesForCrossProfileCallbackInterface(
+ crossProfileType, crossProfileCallbackParameters.get(0))
+ && isValid;
+ }
+
+ if (!crossProfileCallbackParameters.isEmpty()
+ && !crossProfileMethod.getReturnType().getKind().equals(TypeKind.VOID)) {
+ isValid = false;
+ showError(NON_VOID_CALLBACK_ERROR, crossProfileMethod);
+ }
+
+ return isValid;
+ }
+
+ private boolean validateParameterTypeForCrossProfileCallbackInterface(
+ ValidatorCrossProfileTypeInfo crossProfileType, VariableElement parameter) {
+ TypeMirror parameterType = parameter.asType();
+
+ if (crossProfileType
+ .supportedTypes()
+ .isValidParameterType(
+ parameterType, TypeCheckContext.createForCrossProfileCallbackInterface())) {
+ return true;
+ }
+
+ showError(
+ String.format(UNSUPPORTED_PARAMETER_TYPE_CROSS_ASYNC_CALLBACK, parameterType),
+ parameter,
+ validationMessageFormatterFor(crossProfileType.crossProfileMethods().get(0)));
+ return false;
+ }
+
+ private boolean validateParameterTypesForCrossProfileCallbackInterface(
+ ValidatorCrossProfileTypeInfo crossProfileType, TypeElement crossProfileCallbackInterface) {
+ return crossProfileCallbackInterface.getEnclosedElements().stream()
+ .filter(m -> m instanceof ExecutableElement)
+ .map(m -> (ExecutableElement) m)
+ .map(m -> validateParameterTypesForCrossProfileCallbackInterface(crossProfileType, m))
+ .allMatch(b -> b);
+ }
+
+ private boolean validateParameterTypesForCrossProfileCallbackInterface(
+ ValidatorCrossProfileTypeInfo crossProfileType, ExecutableElement method) {
+ return method.getParameters().stream()
+ .allMatch(m -> validateParameterTypeForCrossProfileCallbackInterface(crossProfileType, m));
+ }
+
+ private boolean validateParameterTypeForCrossProfileMethod(
+ ValidatorCrossProfileTypeInfo crossProfileType, VariableElement parameter) {
+ TypeMirror parameterType = parameter.asType();
+
+ if (crossProfileType.supportedTypes().isValidParameterType(parameterType)) {
+ return true;
+ }
+
+ showError(
+ String.format(UNSUPPORTED_PARAMETER_TYPE_CROSS_PROFILE_METHOD, parameterType),
+ parameter,
+ validationMessageFormatterFor(crossProfileType.crossProfileMethods().get(0)));
+ return false;
+ }
+
+ private boolean validateCrossProfileCallbackInterfaces(
+ Collection<TypeElement> crossProfileCallbackInterfaces) {
+ return crossProfileCallbackInterfaces.stream()
+ .allMatch(this::validateCrossProfileCallbackInterface);
+ }
+
+ private boolean validateCrossProfileCallbackInterface(TypeElement crossProfileCallbackInterface) {
+ boolean isValid = true;
+
+ CrossProfileCallbackAnnotationInfo annotationInfo =
+ AnnotationFinder.extractCrossProfileCallbackAnnotationInfo(
+ crossProfileCallbackInterface, validatorContext.types(), validatorContext.elements());
+
+ PackageElement packageElement =
+ (PackageElement) crossProfileCallbackInterface.getEnclosingElement();
+ if (packageElement.getQualifiedName().toString().isEmpty()) {
+ showError(CALLBACK_INTERFACE_DEFAULT_PACKAGE_ERROR, crossProfileCallbackInterface);
+ isValid = false;
+ }
+
+ if (crossProfileCallbackInterface.getKind() != ElementKind.INTERFACE) {
+ showError(NOT_INTERFACE_ERROR, crossProfileCallbackInterface);
+ isValid = false;
+ }
+
+ if (!crossProfileCallbackInterface.getTypeParameters().isEmpty()) {
+ showError(GENERIC_CALLBACK_INTERFACE_ERROR, crossProfileCallbackInterface);
+ isValid = false;
+ }
+
+ Collection<ExecutableElement> methods = getMethods(crossProfileCallbackInterface);
+
+ if (methods.isEmpty()) {
+ showError(NO_METHODS_ERROR, crossProfileCallbackInterface);
+ isValid = false;
+ }
+
+ if (annotationInfo.simple() && methods.size() > 1) {
+ showError(NOT_ONE_METHOD_ERROR, crossProfileCallbackInterface);
+ isValid = false;
+ }
+
+ isValid =
+ methods.stream()
+ .allMatch(
+ (method) ->
+ validateMethodOnCrossProfileCallbackInterface(
+ annotationInfo, method, crossProfileCallbackInterface))
+ && isValid;
+
+ return isValid;
+ }
+
+ private boolean validateMethodOnCrossProfileCallbackInterface(
+ CrossProfileCallbackAnnotationInfo annotationInfo,
+ ExecutableElement method,
+ TypeElement crossProfileCallbackInterface) {
+ boolean isValid = true;
+
+ if (method.isDefault()) {
+ showError(
+ DEFAULT_METHOD_ERROR,
+ method,
+ validationMessageFormatterFor(crossProfileCallbackInterface));
+ isValid = false;
+ }
+
+ if (method.getModifiers().contains(Modifier.STATIC)) {
+ showError(
+ STATIC_METHOD_ERROR,
+ method,
+ validationMessageFormatterFor(crossProfileCallbackInterface));
+ isValid = false;
+ }
+
+ if (!method.getReturnType().getKind().equals(TypeKind.VOID)) {
+ showError(
+ NOT_VOID_ERROR, method, validationMessageFormatterFor(crossProfileCallbackInterface));
+ isValid = false;
+ }
+
+ if (annotationInfo.simple() && method.getParameters().size() > 1) {
+ showError(
+ MORE_THAN_ONE_PARAMETER_ERROR,
+ method,
+ validationMessageFormatterFor(crossProfileCallbackInterface));
+ isValid = false;
+ }
+
+ return isValid;
+ }
+
+ private boolean validateCrossProfileTests(
+ Collection<ValidatorCrossProfileTestInfo> crossProfileTests) {
+ return crossProfileTests.stream().allMatch(this::validateCrossProfileTest);
+ }
+
+ private boolean validateCrossProfileTest(ValidatorCrossProfileTestInfo crossProfileTest) {
+ boolean isValid = true;
+
+ if (!hasCrossProfileConfigurationAnnotation(crossProfileTest.configurationElement())
+ && !hasCrossProfileConfigurationsAnnotation(crossProfileTest.configurationElement())) {
+ showError(NOT_A_CONFIGURATION_ERROR, crossProfileTest.crossProfileTestElement());
+ isValid = false;
+ }
+ return isValid;
+ }
+
+ private boolean validateCustomParcelableWrappers(
+ Collection<TypeElement> customParcelableWrappers) {
+ return customParcelableWrappers.stream().allMatch(this::validateCustomParcelableWrapper);
+ }
+
+ private boolean validateCustomParcelableWrapper(TypeElement customParcelableWrapper) {
+ boolean isValid = true;
+ if (!validatorContext.types().isAssignable(customParcelableWrapper.asType(), parcelableType)) {
+ showError(NOT_PARCELABLE_ERROR, customParcelableWrapper);
+ isValid = false;
+ }
+
+ ClassName parcelableWrapperRawType =
+ TypeUtils.getRawTypeClassName(customParcelableWrapper.asType());
+ ClassName wrappedParamRawType =
+ TypeUtils.getRawTypeClassName(
+ ParcelableWrapperAnnotationInfo.extractFromParcelableWrapperAnnotation(
+ validatorContext.types(),
+ customParcelableWrapper.getAnnotation(CustomParcelableWrapper.class))
+ .originalType()
+ .asType());
+
+ Optional<ExecutableElement> ofMethod =
+ customParcelableWrapper.getEnclosedElements().stream()
+ .filter(p -> p.getKind().equals(ElementKind.METHOD))
+ .map(p -> (ExecutableElement) p)
+ .filter(p -> p.getSimpleName().contentEquals("of"))
+ // We drop generics as without being overly prescriptive it's impossible to know that
+ // the method is returning the correct generic type
+ .filter(
+ p ->
+ TypeUtils.getRawTypeClassName(p.getReturnType())
+ .equals(TypeUtils.getRawTypeClassName(customParcelableWrapper.asType())))
+ .filter(p -> ofMethodHasExpectedArguments(wrappedParamRawType, p))
+ .findFirst();
+
+ if (!ofMethod.isPresent()) {
+ showError(INCORRECT_OF_METHOD, customParcelableWrapper);
+ isValid = false;
+ }
+
+ Optional<ExecutableElement> getMethod =
+ customParcelableWrapper.getEnclosedElements().stream()
+ .filter(p -> p.getKind().equals(ElementKind.METHOD))
+ .map(p -> (ExecutableElement) p)
+ .filter(p -> p.getSimpleName().contentEquals("get"))
+ // We drop generics as without being overly prescriptive it's impossible to know that
+ // the method is returning the correct generic type
+ .filter(
+ p -> TypeUtils.getRawTypeClassName(p.getReturnType()).equals(wrappedParamRawType))
+ .findFirst();
+
+ if (!getMethod.isPresent()) {
+ showError(INCORRECT_GET_METHOD, customParcelableWrapper);
+ isValid = false;
+ }
+
+ TypeName creatorType =
+ ParameterizedTypeName.get(PARCELABLE_CREATOR_CLASSNAME, parcelableWrapperRawType);
+
+ Optional<VariableElement> creator =
+ customParcelableWrapper.getEnclosedElements().stream()
+ .filter(p -> p.getKind().equals(ElementKind.FIELD))
+ .map(p -> (VariableElement) p)
+ .filter(p -> p.getSimpleName().contentEquals("CREATOR"))
+ .filter(
+ p ->
+ p.getModifiers()
+ .containsAll(
+ Arrays.asList(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC)))
+ .filter(p -> ClassName.get(p.asType()).equals(creatorType))
+ .findFirst();
+
+ if (!creator.isPresent()) {
+ showError(INCORRECT_PARCELABLE_IMPLEMENTATION, customParcelableWrapper);
+ isValid = false;
+ }
+
+ return isValid;
+ }
+
+ private boolean ofMethodHasExpectedArguments(
+ ClassName wrappedParamRawType, ExecutableElement ofMethod) {
+ List<? extends VariableElement> parameters = ofMethod.getParameters();
+ if (parameters.size() != 3) {
+ return false;
+ }
+
+ if (!validatorContext.types().isSameType(parameters.get(0).asType(), bundlerType)) {
+ return false;
+ }
+
+ if (!validatorContext.types().isSameType(parameters.get(1).asType(), bundlerTypeType)) {
+ return false;
+ }
+
+ if (!TypeUtils.getRawTypeClassName(parameters.get(2).asType()).equals(wrappedParamRawType)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private boolean validateCustomFutureWrappers(Collection<TypeElement> futureWrappers) {
+ return futureWrappers.stream().map(this::validateCustomFutureWrapper).allMatch(b -> b);
+ }
+
+ private boolean validateCustomFutureWrapper(TypeElement futureWrapper) {
+ boolean isValid = true;
+
+ ClassName wrappedFutureRawType =
+ TypeUtils.getRawTypeClassName(
+ FutureWrapperAnnotationInfo.extractFromFutureWrapperAnnotation(
+ validatorContext.types(),
+ futureWrapper.getAnnotation(CustomFutureWrapper.class))
+ .originalType()
+ .asType());
+
+ if (!TypeUtils.getRawTypeQualifiedName(futureWrapper.getSuperclass())
+ .equals("com.google.android.enterprise.connectedapps.FutureWrapper")) {
+ showError(DOES_NOT_EXTEND_FUTURE_WRAPPER_ERROR, futureWrapper);
+ isValid = false;
+ }
+
+ if (futureWrapper.getTypeParameters().size() != 1) {
+ showError(MUST_HAVE_ONE_TYPE_PARAMETER_ERROR, futureWrapper);
+ isValid = false;
+ }
+
+ Optional<ExecutableElement> createMethod =
+ futureWrapper.getEnclosedElements().stream()
+ .filter(e -> e instanceof ExecutableElement)
+ .map(e -> (ExecutableElement) e)
+ .filter(e -> e.getSimpleName().contentEquals("create"))
+ .filter(
+ e -> e.getModifiers().containsAll(Arrays.asList(Modifier.PUBLIC, Modifier.STATIC)))
+ // We drop generics as without being overly prescriptive it's impossible to know that
+ // the method is returning the correct generic type
+ .filter(
+ e ->
+ TypeUtils.getRawTypeClassName(e.getReturnType())
+ .equals(TypeUtils.getRawTypeClassName(futureWrapper.asType())))
+ .filter(this::createMethodHasExpectedArguments)
+ .findFirst();
+
+ if (!createMethod.isPresent()) {
+ showError(INCORRECT_CREATE_METHOD_ERROR, futureWrapper);
+ isValid = false;
+ }
+
+ Optional<ExecutableElement> getFutureMethod =
+ futureWrapper.getEnclosedElements().stream()
+ .filter(e -> e instanceof ExecutableElement)
+ .map(e -> (ExecutableElement) e)
+ .filter(e -> e.getSimpleName().contentEquals("getFuture"))
+ .filter(e -> e.getModifiers().contains(Modifier.PUBLIC))
+ .filter(e -> !e.getModifiers().contains(Modifier.STATIC))
+ // We drop generics as without being overly prescriptive it's impossible to know that
+ // the method is returning the correct generic type
+ .filter(
+ e -> TypeUtils.getRawTypeClassName(e.getReturnType()).equals(wrappedFutureRawType))
+ .filter(e -> e.getParameters().isEmpty())
+ .findFirst();
+
+ if (!getFutureMethod.isPresent()) {
+ showError(INCORRECT_GET_FUTURE_METHOD_ERROR, futureWrapper);
+ isValid = false;
+ }
+
+ Optional<ExecutableElement> writeFutureResultMethod =
+ futureWrapper.getEnclosedElements().stream()
+ .filter(e -> e instanceof ExecutableElement)
+ .map(e -> (ExecutableElement) e)
+ .filter(e -> e.getSimpleName().contentEquals("writeFutureResult"))
+ .filter(
+ e -> e.getModifiers().containsAll(Arrays.asList(Modifier.PUBLIC, Modifier.STATIC)))
+ .filter(e -> e.getReturnType().toString().equals("void"))
+ .filter(e -> writeFutureResultMethodHasExpectedArguments(e, wrappedFutureRawType))
+ .findFirst();
+
+ if (!writeFutureResultMethod.isPresent()) {
+ showError(INCORRECT_RESOLVE_CALLBACK_WHEN_FUTURE_IS_SET_METHOD_ERROR, futureWrapper);
+ isValid = false;
+ }
+
+ Optional<ExecutableElement> groupResultsMethod =
+ futureWrapper.getEnclosedElements().stream()
+ .filter(e -> e instanceof ExecutableElement)
+ .map(e -> (ExecutableElement) e)
+ .filter(e -> e.getSimpleName().contentEquals("groupResults"))
+ .filter(
+ e -> e.getModifiers().containsAll(Arrays.asList(Modifier.PUBLIC, Modifier.STATIC)))
+ .filter(e -> groupResultsMethodHasExpectedReturnType(e, wrappedFutureRawType))
+ .filter(e -> groupResultsMethodHasExpectedArguments(e, wrappedFutureRawType))
+ .findFirst();
+
+ if (!groupResultsMethod.isPresent()) {
+ showError(INCORRECT_GROUP_RESULTS_METHOD_ERROR, futureWrapper);
+ isValid = false;
+ }
+
+ return isValid;
+ }
+
+ private boolean groupResultsMethodHasExpectedReturnType(
+ ExecutableElement groupResultsMethod, ClassName wrappedFutureRawType) {
+
+ if (!TypeUtils.getRawTypeClassName(groupResultsMethod.getReturnType())
+ .equals(wrappedFutureRawType)) {
+ return false;
+ }
+
+ TypeMirror wrappedReturnType =
+ TypeUtils.extractTypeArguments(groupResultsMethod.getReturnType()).get(0);
+
+ if (!TypeUtils.getRawTypeClassName(wrappedReturnType).equals(ClassName.get(Map.class))) {
+ return false;
+ }
+
+ TypeMirror wrappedReturnTypeKey = TypeUtils.extractTypeArguments(wrappedReturnType).get(0);
+
+ if (!validatorContext.types().isSameType(wrappedReturnTypeKey, profileType)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private boolean groupResultsMethodHasExpectedArguments(
+ ExecutableElement groupResultsMethod, ClassName wrappedFutureRawType) {
+ if (groupResultsMethod.getParameters().size() != 1) {
+ return false;
+ }
+
+ TypeMirror param = groupResultsMethod.getParameters().get(0).asType();
+
+ if (!TypeUtils.getRawTypeClassName(param).equals(ClassName.get(Map.class))) {
+ return false;
+ }
+
+ List<TypeMirror> params = TypeUtils.extractTypeArguments(param);
+
+ TypeMirror keyParam = params.get(0);
+ TypeMirror valueParam = params.get(1);
+
+ if (!validatorContext.types().isSameType(keyParam, profileType)) {
+ return false;
+ }
+
+ if (!TypeUtils.getRawTypeClassName(valueParam).equals(wrappedFutureRawType)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private boolean createMethodHasExpectedArguments(ExecutableElement createMethod) {
+ if (createMethod.getParameters().size() != 2) {
+ return false;
+ }
+
+ if (!validatorContext
+ .types()
+ .isSameType(createMethod.getParameters().get(0).asType(), bundlerType)) {
+ return false;
+ }
+
+ if (!validatorContext
+ .types()
+ .isSameType(createMethod.getParameters().get(1).asType(), bundlerTypeType)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private boolean writeFutureResultMethodHasExpectedArguments(
+ ExecutableElement method, ClassName wrappedFutureRawType) {
+ if (method.getParameters().size() != 2) {
+ return false;
+ }
+
+ if (!TypeUtils.getRawTypeClassName(method.getParameters().get(0).asType())
+ .equals(wrappedFutureRawType)) {
+ return false;
+ }
+
+ if (!validatorContext
+ .types()
+ .isAssignable(
+ TypeUtils.removeTypeArguments(method.getParameters().get(1).asType()),
+ futureResultWriterType)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private Collection<ExecutableElement> getMethods(TypeElement typeElement) {
+ return typeElement.getEnclosedElements().stream()
+ .filter(e -> e instanceof ExecutableElement)
+ .map(e -> (ExecutableElement) e)
+ .collect(toSet());
+ }
+
+ private void showError(
+ String errorText,
+ Element errorElement,
+ ValidationMessageFormatter validationMessageFormatter) {
+ showErrorPreformatted(validationMessageFormatter.format(errorText), errorElement);
+ }
+
+ private void showError(String errorText, Element errorElement) {
+ showErrorPreformatted(
+ validationMessageFormatterFor(errorElement).format(errorText), errorElement);
+ }
+
+ private void showErrorPreformatted(String errorText, Element errorElement) {
+ validatorContext
+ .processingEnv()
+ .getMessager()
+ .printMessage(Kind.ERROR, errorText, errorElement);
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeCrossProfileTypeGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeCrossProfileTypeGenerator.java
new file mode 100644
index 0000000..0abba95
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeCrossProfileTypeGenerator.java
@@ -0,0 +1,477 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.ABSTRACT_FAKE_PROFILE_CONNECTOR_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_AWARE_UTILS_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CurrentProfileGenerator.getCurrentProfileClassName;
+import static com.google.android.enterprise.connectedapps.processor.InterfaceGenerator.getMultipleSenderInterfaceClassName;
+import static com.google.android.enterprise.connectedapps.processor.InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName;
+import static com.google.android.enterprise.connectedapps.processor.InterfaceGenerator.getSingleSenderInterfaceClassName;
+import static com.google.android.enterprise.connectedapps.processor.MultipleProfilesGenerator.getMultipleProfilesClassName;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector;
+import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector.ProfileType;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext;
+import com.squareup.javapoet.ArrayTypeName;
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.CodeBlock;
+import com.squareup.javapoet.FieldSpec;
+import com.squareup.javapoet.MethodSpec;
+import com.squareup.javapoet.ParameterizedTypeName;
+import com.squareup.javapoet.TypeSpec;
+import java.util.HashMap;
+import java.util.Map;
+import javax.lang.model.element.Modifier;
+
+class FakeCrossProfileTypeGenerator {
+ private boolean generated = false;
+ private final CrossProfileTypeInfo crossProfileType;
+ private final GeneratorContext generatorContext;
+ private final GeneratorUtilities generatorUtilities;
+
+ public FakeCrossProfileTypeGenerator(
+ GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) {
+ this.generatorContext = checkNotNull(generatorContext);
+ this.generatorUtilities = new GeneratorUtilities(generatorContext);
+ this.crossProfileType = checkNotNull(crossProfileType);
+ }
+
+ void generate() {
+ if (generated) {
+ throw new IllegalStateException(
+ "FakeCrossProfileTypeGenerator#generate can only be called once");
+ }
+ generated = true;
+
+ generateFakeCrossProfileType();
+ }
+
+ private void generateFakeCrossProfileType() {
+ ClassName className = getFakeCrossProfileTypeClassName(generatorContext, crossProfileType);
+ ClassName builderClassName =
+ getFakeCrossProfileTypeBuilderClassName(generatorContext, crossProfileType);
+ ClassName crossProfileTypeInterfaceClassName =
+ InterfaceGenerator.getCrossProfileTypeInterfaceClassName(
+ generatorContext, crossProfileType);
+ ClassName fakeProfileConnectorClassName =
+ crossProfileType.profileConnector().isPresent()
+ ? FakeProfileConnectorGenerator.getFakeProfileConnectorClassName(
+ crossProfileType.profileConnector().get())
+ : ABSTRACT_FAKE_PROFILE_CONNECTOR_CLASSNAME;
+
+ TypeSpec.Builder classBuilder =
+ TypeSpec.classBuilder(className)
+ .addJavadoc(
+ "Fake implementation of {@link $T} for use during tests.\n\n"
+ + "<p>This should be injected into your code under test and the {@link $T}\n"
+ + "used to control the fake state. Calls will be routed to the correct {@link"
+ + " $T}.\n",
+ crossProfileTypeInterfaceClassName,
+ fakeProfileConnectorClassName,
+ crossProfileType.className())
+ .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
+ .addSuperinterface(crossProfileTypeInterfaceClassName);
+
+ classBuilder.addField(
+ FieldSpec.builder(fakeProfileConnectorClassName, "connector")
+ .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
+ .build());
+
+ addConstructor(classBuilder);
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("builder")
+ .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
+ .returns(builderClassName)
+ .addStatement("return new $T()", builderClassName)
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("current")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(getSingleSenderInterfaceClassName(generatorContext, crossProfileType))
+ .beginControlFlow("if (connector.utils().runningOnPersonal())")
+ .addStatement(
+ "return ($T) personal()",
+ InterfaceGenerator.getSingleSenderInterfaceClassName(
+ generatorContext, crossProfileType))
+ .nextControlFlow("else")
+ .addStatement(
+ "return ($T) work()",
+ InterfaceGenerator.getSingleSenderInterfaceClassName(
+ generatorContext, crossProfileType))
+ .endControlFlow()
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("other")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType))
+ .beginControlFlow("if (connector.utils().runningOnPersonal())")
+ .addStatement("return work()")
+ .nextControlFlow("else")
+ .addStatement("return personal()")
+ .endControlFlow()
+ .build());
+
+ addPersonalMethod(classBuilder);
+ addWorkMethod(classBuilder);
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("profile")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(PROFILE_CLASSNAME, "profile")
+ .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType))
+ .beginControlFlow("if (profile.isCurrent())")
+ .addStatement(
+ "return ($T) current()",
+ getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType))
+ .nextControlFlow("else")
+ .addComment("must be other profile")
+ .addStatement("return other()")
+ .endControlFlow()
+ .build());
+
+ ParameterizedTypeName senderMapType =
+ ParameterizedTypeName.get(
+ ClassName.get(Map.class),
+ PROFILE_CLASSNAME,
+ InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName(
+ generatorContext, crossProfileType));
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("profiles")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(ArrayTypeName.of(PROFILE_CLASSNAME), "profiles")
+ .varargs(true)
+ .returns(getMultipleSenderInterfaceClassName(generatorContext, crossProfileType))
+ .addStatement("$T senders = new $T<>()", senderMapType, HashMap.class)
+ .beginControlFlow("for ($1T profileIdentifier : profiles)", PROFILE_CLASSNAME)
+ .addStatement("senders.put(profileIdentifier, profile(profileIdentifier))")
+ .endControlFlow()
+ .addStatement(
+ "return new $1T(senders)",
+ getMultipleProfilesClassName(generatorContext, crossProfileType))
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("both")
+ .addAnnotation(Override.class)
+ .addJavadoc("Run a method on both the personal and work profile, if accessible.")
+ .addModifiers(Modifier.PUBLIC)
+ .returns(getMultipleSenderInterfaceClassName(generatorContext, crossProfileType))
+ .addStatement("$1T utils = connector.utils()", PROFILE_AWARE_UTILS_CLASSNAME)
+ .addStatement(
+ "$1T currentProfileIdentifier = utils.getCurrentProfile()", PROFILE_CLASSNAME)
+ .addStatement("$1T otherProfileIdentifier = utils.getOtherProfile()", PROFILE_CLASSNAME)
+ .addStatement("return profiles(currentProfileIdentifier, otherProfileIdentifier)")
+ .build());
+
+ if (!crossProfileType.profileConnector().isPresent()
+ || crossProfileType.profileConnector().get().primaryProfile() != ProfileType.NONE) {
+ generatePrimarySecondaryMethods(classBuilder);
+ }
+
+ generateFakeCrossProfileTypeBuilder(classBuilder);
+
+ generatorUtilities.writeClassToFile(className.packageName(), classBuilder);
+ }
+
+ private void addConstructor(TypeSpec.Builder classBuilder) {
+ ClassName fakeProfileConnectorClassName =
+ crossProfileType.profileConnector().isPresent()
+ ? FakeProfileConnectorGenerator.getFakeProfileConnectorClassName(
+ crossProfileType.profileConnector().get())
+ : ABSTRACT_FAKE_PROFILE_CONNECTOR_CLASSNAME;
+
+ if (crossProfileType.isStatic()) {
+ classBuilder.addMethod(
+ MethodSpec.constructorBuilder()
+ .addModifiers(Modifier.PRIVATE)
+ .addParameter(fakeProfileConnectorClassName, "connector")
+ .addStatement("this.connector = connector")
+ .build());
+ } else {
+ classBuilder.addField(
+ FieldSpec.builder(crossProfileType.className(), "personal")
+ .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
+ .build());
+
+ classBuilder.addField(
+ FieldSpec.builder(crossProfileType.className(), "work")
+ .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.constructorBuilder()
+ .addModifiers(Modifier.PRIVATE)
+ .addParameter(crossProfileType.className(), "personal")
+ .addParameter(crossProfileType.className(), "work")
+ .addParameter(fakeProfileConnectorClassName, "connector")
+ .addStatement("this.personal = personal")
+ .addStatement("this.work = work")
+ .addStatement("this.connector = connector")
+ .build());
+ }
+ }
+
+ private void addPersonalMethod(TypeSpec.Builder classBuilder) {
+ ClassName currentProfileClassName =
+ getCurrentProfileClassName(generatorContext, crossProfileType);
+ CodeBlock currentPersonalFakeConstructor =
+ crossProfileType.isStatic()
+ ? CodeBlock.of("new $T(connector.applicationContext())", currentProfileClassName)
+ : CodeBlock.of(
+ "new $T(connector.applicationContext(), personal)", currentProfileClassName);
+ ClassName fakeOtherClassName =
+ FakeOtherGenerator.getFakeOtherClassName(generatorContext, crossProfileType);
+ CodeBlock otherPersonalFakeConstructor =
+ crossProfileType.isStatic()
+ ? CodeBlock.of("new $T(connector)", fakeOtherClassName)
+ : CodeBlock.of("new $T(connector, personal)", fakeOtherClassName);
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("personal")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType))
+ .beginControlFlow(
+ "if (connector.runningOnProfile() == $T.ProfileType.PERSONAL)",
+ CustomProfileConnector.class)
+ .addStatement("return $L", currentPersonalFakeConstructor)
+ .nextControlFlow("else")
+ .addStatement("return $L", otherPersonalFakeConstructor)
+ .endControlFlow()
+ .build());
+ }
+
+ private void addWorkMethod(TypeSpec.Builder classBuilder) {
+ ClassName currentProfileClassName =
+ getCurrentProfileClassName(generatorContext, crossProfileType);
+ CodeBlock currentWorkFakeConstructor =
+ crossProfileType.isStatic()
+ ? CodeBlock.of("new $T(connector.applicationContext())", currentProfileClassName)
+ : CodeBlock.of("new $T(connector.applicationContext(), work)", currentProfileClassName);
+ ClassName fakeOtherClassName =
+ FakeOtherGenerator.getFakeOtherClassName(generatorContext, crossProfileType);
+ CodeBlock otherWorkFakeConstructor =
+ crossProfileType.isStatic()
+ ? CodeBlock.of("new $T(connector)", fakeOtherClassName)
+ : CodeBlock.of("new $T(connector, work)", fakeOtherClassName);
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("work")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType))
+ .beginControlFlow(
+ "if (connector.runningOnProfile() == $T.ProfileType.WORK)",
+ CustomProfileConnector.class)
+ .addStatement("return $L", currentWorkFakeConstructor)
+ .nextControlFlow("else")
+ .addStatement("return $L", otherWorkFakeConstructor)
+ .endControlFlow()
+ .build());
+ }
+
+ private void generateFakeCrossProfileTypeBuilder(TypeSpec.Builder fakeCrossProfileType) {
+ ClassName fakeCrossProfileTypeClassName =
+ getFakeCrossProfileTypeClassName(generatorContext, crossProfileType);
+ ClassName builderClassName =
+ getFakeCrossProfileTypeBuilderClassName(generatorContext, crossProfileType);
+ ClassName fakeProfileConnectorClassName =
+ crossProfileType.profileConnector().isPresent()
+ ? FakeProfileConnectorGenerator.getFakeProfileConnectorClassName(
+ crossProfileType.profileConnector().get())
+ : ABSTRACT_FAKE_PROFILE_CONNECTOR_CLASSNAME;
+
+ TypeSpec.Builder classBuilder =
+ TypeSpec.classBuilder(builderClassName)
+ .addJavadoc("Builder for {@link $T}.\n", fakeCrossProfileTypeClassName)
+ .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC);
+
+ if (crossProfileType.isStatic()) {
+ setupStaticBuilder(fakeCrossProfileTypeClassName, classBuilder);
+ } else {
+ setupNonStaticBuilder(builderClassName, fakeCrossProfileTypeClassName, classBuilder);
+ }
+
+ classBuilder.addField(
+ FieldSpec.builder(fakeProfileConnectorClassName, "connector")
+ .addModifiers(Modifier.PRIVATE)
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("connector")
+ .addJavadoc(
+ "Set the {@link $T} to be used to manage the state of this fake.\n",
+ fakeProfileConnectorClassName)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(builderClassName)
+ .addParameter(fakeProfileConnectorClassName, "connector")
+ .addStatement("this.connector = connector")
+ .addStatement("return this")
+ .build());
+
+ fakeCrossProfileType.addType(classBuilder.build());
+ }
+
+ private static void setupStaticBuilder(
+ ClassName fakeCrossProfileTypeClassName, TypeSpec.Builder classBuilder) {
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("build")
+ .addJavadoc("Build the {@link $T}.\n", fakeCrossProfileTypeClassName)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(fakeCrossProfileTypeClassName)
+ .beginControlFlow("if (connector == null)")
+ .addStatement(
+ "throw new $T($S)",
+ IllegalStateException.class,
+ "All arguments must be set to build fake")
+ .endControlFlow()
+ .addStatement("return new $1T(connector)", fakeCrossProfileTypeClassName)
+ .build());
+ }
+
+ private void setupNonStaticBuilder(
+ ClassName builderClassName,
+ ClassName fakeCrossProfileTypeClassName,
+ TypeSpec.Builder classBuilder) {
+ classBuilder.addField(
+ FieldSpec.builder(crossProfileType.className(), "personal")
+ .addModifiers(Modifier.PRIVATE)
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("personal")
+ .addJavadoc(
+ "Set the {@link $T} to be used when a call needs to be made to the personal"
+ + " profile.\n",
+ crossProfileType.className())
+ .addModifiers(Modifier.PUBLIC)
+ .returns(builderClassName)
+ .addParameter(crossProfileType.className(), "personal")
+ .addStatement("this.personal = personal")
+ .addStatement("return this")
+ .build());
+
+ classBuilder.addField(
+ FieldSpec.builder(crossProfileType.className(), "work")
+ .addModifiers(Modifier.PRIVATE)
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("work")
+ .addJavadoc(
+ "Set the {@link $T} to be used when a call needs to be made to the work profile.\n",
+ crossProfileType.className())
+ .addModifiers(Modifier.PUBLIC)
+ .returns(builderClassName)
+ .addParameter(crossProfileType.className(), "work")
+ .addStatement("this.work = work")
+ .addStatement("return this")
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("build")
+ .addJavadoc("Build the {@link $T}.\n", fakeCrossProfileTypeClassName)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(fakeCrossProfileTypeClassName)
+ .beginControlFlow("if (personal == null || work == null || connector == null)")
+ .addStatement(
+ "throw new $T($S)",
+ IllegalStateException.class,
+ "All arguments must be set to build fake")
+ .endControlFlow()
+ .addStatement(
+ "return new $1T(personal, work, connector)", fakeCrossProfileTypeClassName)
+ .build());
+ }
+
+ private void generatePrimarySecondaryMethods(TypeSpec.Builder classBuilder) {
+ generatePrimaryMethod(classBuilder);
+ generateSecondaryMethod(classBuilder);
+ generateSuppliersMethod(classBuilder);
+ }
+
+ private void generatePrimaryMethod(TypeSpec.Builder classBuilder) {
+ MethodSpec.Builder methodBuilder =
+ MethodSpec.methodBuilder("primary")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType))
+ .addStatement("return profile(connector.utils().getPrimaryProfile())");
+
+ classBuilder.addMethod(methodBuilder.build());
+ }
+
+ private void generateSecondaryMethod(TypeSpec.Builder classBuilder) {
+ MethodSpec.Builder methodBuilder =
+ MethodSpec.methodBuilder("secondary")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType))
+ .addStatement("return profile(connector.utils().getSecondaryProfile())");
+
+ classBuilder.addMethod(methodBuilder.build());
+ }
+
+ private void generateSuppliersMethod(TypeSpec.Builder classBuilder) {
+ MethodSpec.Builder methodBuilder =
+ MethodSpec.methodBuilder("suppliers")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(getMultipleSenderInterfaceClassName(generatorContext, crossProfileType))
+ .addStatement("$1T utils = connector.utils()", PROFILE_AWARE_UTILS_CLASSNAME)
+ .addStatement(
+ "$1T currentProfileIdentifier = utils.getCurrentProfile()", PROFILE_CLASSNAME)
+ .addStatement(
+ "$1T secondaryProfileIdentifier = utils.getSecondaryProfile()", PROFILE_CLASSNAME)
+ .addStatement("return profiles(currentProfileIdentifier, secondaryProfileIdentifier)");
+
+ classBuilder.addMethod(methodBuilder.build());
+ }
+
+ static ClassName getFakeCrossProfileTypeClassName(
+ GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) {
+ ClassName crossProfileTypeClassName =
+ InterfaceGenerator.getCrossProfileTypeInterfaceClassName(
+ generatorContext, crossProfileType);
+ return ClassName.get(
+ crossProfileTypeClassName.packageName(), "Fake" + crossProfileTypeClassName.simpleName());
+ }
+
+ static ClassName getFakeCrossProfileTypeBuilderClassName(
+ GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) {
+ ClassName crossProfileTypeClassName =
+ InterfaceGenerator.getCrossProfileTypeInterfaceClassName(
+ generatorContext, crossProfileType);
+ return ClassName.get(
+ crossProfileTypeClassName.packageName()
+ + "."
+ + "Fake"
+ + crossProfileTypeClassName.simpleName(),
+ "Builder");
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeOtherGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeOtherGenerator.java
new file mode 100644
index 0000000..20bf0fb
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeOtherGenerator.java
@@ -0,0 +1,344 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.ABSTRACT_FAKE_PROFILE_CONNECTOR_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CONTEXT_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.EXCEPTION_CALLBACK_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_RUNTIME_EXCEPTION_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS;
+import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.FutureWrapper;
+import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext;
+import com.squareup.javapoet.AnnotationSpec;
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.CodeBlock;
+import com.squareup.javapoet.MethodSpec;
+import com.squareup.javapoet.ParameterizedTypeName;
+import com.squareup.javapoet.TypeSpec;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+
+/**
+ * Generate the {@code Profile_*_FakeOther} class for a single cross-profile type.
+ *
+ * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to
+ * validate that the annotated code is correct.
+ */
+final class FakeOtherGenerator {
+
+ private boolean generated = false;
+ private final GeneratorContext generatorContext;
+ private final GeneratorUtilities generatorUtilities;
+ private final CrossProfileTypeInfo crossProfileType;
+
+ FakeOtherGenerator(GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) {
+ this.generatorContext = checkNotNull(generatorContext);
+ this.generatorUtilities = new GeneratorUtilities(generatorContext);
+ this.crossProfileType = checkNotNull(crossProfileType);
+ }
+
+ void generate() {
+ if (generated) {
+ throw new IllegalStateException("FakeSingleSenderGenerator#generate can only be called once");
+ }
+ generated = true;
+
+ generateFakeOther();
+ }
+
+ private void generateFakeOther() {
+ ClassName className = getFakeOtherClassName(generatorContext, crossProfileType);
+
+ ClassName singleSenderCanThrowInterface =
+ InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName(
+ generatorContext, crossProfileType);
+
+ ClassName fakeProfileConnectorClassName =
+ crossProfileType.profileConnector().isPresent()
+ ? FakeProfileConnectorGenerator.getFakeProfileConnectorClassName(
+ crossProfileType.profileConnector().get())
+ : ABSTRACT_FAKE_PROFILE_CONNECTOR_CLASSNAME;
+
+ TypeSpec.Builder classBuilder =
+ TypeSpec.classBuilder(className)
+ .addJavadoc(
+ "Fake implementation of {@link $T} for use during tests.\n\n"
+ + "<p>This acts based on the state of the passed in {@link $T} and acts as if"
+ + " making a call on the other profile.\n",
+ singleSenderCanThrowInterface,
+ fakeProfileConnectorClassName)
+ .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
+ .addSuperinterface(singleSenderCanThrowInterface);
+
+ classBuilder.addField(
+ fakeProfileConnectorClassName, "connector", Modifier.PRIVATE, Modifier.FINAL);
+
+ addConstructor(classBuilder);
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("timeout")
+ .addAnnotation(Override.class)
+ .addAnnotation(
+ AnnotationSpec.builder(SuppressWarnings.class)
+ .addMember("value", "$S", "GoodTime")
+ .build())
+ .addModifiers(Modifier.PUBLIC)
+ .returns(className)
+ .addParameter(long.class, "timeout")
+ .addStatement("return this")
+ .build());
+
+ ClassName ifAvailableClass =
+ IfAvailableGenerator.getIfAvailableClassName(generatorContext, crossProfileType);
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("ifAvailable")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(ifAvailableClass)
+ .addStatement("return new $T(this)", ifAvailableClass)
+ .build());
+
+ for (CrossProfileMethodInfo method : crossProfileType.crossProfileMethods()) {
+ if (method.isBlocking(generatorContext, crossProfileType)) {
+ generateBlockingMethodOnFakeOther(classBuilder, method, crossProfileType);
+ } else if (method.isCrossProfileCallback(generatorContext)) {
+ generateCrossProfileCallbackMethodOnFakeOther(classBuilder, method, crossProfileType);
+ } else if (method.isFuture(crossProfileType)) {
+ generateFutureMethodOnFakeOther(classBuilder, method, crossProfileType);
+ } else {
+ throw new IllegalStateException("Unknown method type: " + method);
+ }
+ }
+
+ generatorUtilities.writeClassToFile(className.packageName(), classBuilder);
+ }
+
+ private void addConstructor(TypeSpec.Builder classBuilder) {
+ ClassName fakeProfileConnectorClassName =
+ crossProfileType.profileConnector().isPresent()
+ ? FakeProfileConnectorGenerator.getFakeProfileConnectorClassName(
+ crossProfileType.profileConnector().get())
+ : ABSTRACT_FAKE_PROFILE_CONNECTOR_CLASSNAME;
+
+ classBuilder.addField(CONTEXT_CLASSNAME, "context", Modifier.PRIVATE, Modifier.FINAL);
+
+ if (crossProfileType.isStatic()) {
+ classBuilder.addMethod(
+ MethodSpec.constructorBuilder()
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(fakeProfileConnectorClassName, "connector")
+ .addStatement("this.context = connector.applicationContext()")
+ .addStatement("this.connector = connector")
+ .build());
+ } else {
+ classBuilder.addField(
+ crossProfileType.className(), "crossProfileType", Modifier.PRIVATE, Modifier.FINAL);
+
+ classBuilder.addMethod(
+ MethodSpec.constructorBuilder()
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(fakeProfileConnectorClassName, "connector")
+ .addParameter(crossProfileType.className(), "crossProfileType")
+ .addStatement("this.context = connector.applicationContext()")
+ .addStatement("this.connector = connector")
+ .addStatement("this.crossProfileType = crossProfileType")
+ .build());
+ }
+ }
+
+ private void generateBlockingMethodOnFakeOther(
+ TypeSpec.Builder classBuilder,
+ CrossProfileMethodInfo method,
+ CrossProfileTypeInfo crossProfileType) {
+
+ MethodSpec.Builder methodBuilder =
+ MethodSpec.methodBuilder(method.simpleName())
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .addExceptions(method.thrownExceptions())
+ .addException(UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME)
+ .returns(method.returnTypeTypeName())
+ .addParameters(
+ GeneratorUtilities.extractParametersFromMethod(
+ crossProfileType.supportedTypes(),
+ method.methodElement(),
+ REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS));
+
+ CodeBlock methodCall =
+ CodeBlock.of(
+ "$L.$L($L)",
+ getCrossProfileTypeReference(method, crossProfileType),
+ method.simpleName(),
+ method.commaSeparatedParameters(
+ crossProfileType.supportedTypes(), REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS));
+
+ if (method.returnType().getKind() != TypeKind.VOID) {
+ methodCall = CodeBlock.of("return $L", methodCall);
+ }
+
+ methodBuilder.beginControlFlow("if (!connector.isConnected())");
+ methodBuilder.addStatement(
+ "throw new $T($S)",
+ UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME,
+ "Could not access other profile");
+ methodBuilder.endControlFlow();
+
+ methodBuilder.beginControlFlow("if (!connector.isManuallyManagingConnection())");
+ methodBuilder.addStatement(
+ "throw new $T($S)",
+ UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME,
+ "Synchronous calls can only be used when manually connected");
+ methodBuilder.endControlFlow();
+
+ methodBuilder.beginControlFlow("try");
+ methodBuilder.addStatement(methodCall);
+ methodBuilder.nextControlFlow("catch ($T e)", RuntimeException.class);
+ methodBuilder.addStatement("throw new $T(e)", PROFILE_RUNTIME_EXCEPTION_CLASSNAME);
+ methodBuilder.endControlFlow();
+ classBuilder.addMethod(methodBuilder.build());
+ }
+
+ private static CodeBlock getCrossProfileTypeReference(
+ CrossProfileMethodInfo method, CrossProfileTypeInfo crossProfileType) {
+ return method.isStatic()
+ ? CodeBlock.of("$1T", crossProfileType.className())
+ : CodeBlock.of("crossProfileType");
+ }
+
+ private void generateCrossProfileCallbackMethodOnFakeOther(
+ TypeSpec.Builder classBuilder,
+ CrossProfileMethodInfo method,
+ CrossProfileTypeInfo crossProfileType) {
+ MethodSpec.Builder methodBuilder =
+ MethodSpec.methodBuilder(method.simpleName())
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(method.returnTypeTypeName())
+ .addParameters(
+ GeneratorUtilities.extractParametersFromMethod(
+ crossProfileType.supportedTypes(),
+ method.methodElement(),
+ REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS))
+ .addParameter(EXCEPTION_CALLBACK_CLASSNAME, "exceptionCallback");
+
+ CodeBlock methodCall =
+ CodeBlock.of(
+ "$L.$L($L)",
+ getCrossProfileTypeReference(method, crossProfileType),
+ method.simpleName(),
+ method.commaSeparatedParameters(
+ crossProfileType.supportedTypes(), REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS));
+
+ if (method.returnType().getKind() != TypeKind.VOID) {
+ methodCall = CodeBlock.of("return $L", methodCall);
+ }
+
+ methodBuilder.beginControlFlow("if (!connector.isAvailable())");
+ methodBuilder.addStatement(
+ "exceptionCallback.onException(new $T($S))",
+ UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME,
+ "Could not access other profile");
+ methodBuilder.addStatement("return");
+ methodBuilder.endControlFlow();
+
+ methodBuilder.addStatement("connector.automaticallyConnect()");
+ methodBuilder.beginControlFlow("try");
+ methodBuilder.addStatement(methodCall);
+ methodBuilder.nextControlFlow("catch ($T e)", RuntimeException.class);
+ methodBuilder.addStatement("throw new $T(e)", PROFILE_RUNTIME_EXCEPTION_CLASSNAME);
+ methodBuilder.endControlFlow();
+
+ classBuilder.addMethod(methodBuilder.build());
+ }
+
+ private void generateFutureMethodOnFakeOther(
+ TypeSpec.Builder classBuilder,
+ CrossProfileMethodInfo method,
+ CrossProfileTypeInfo crossProfileType) {
+
+ MethodSpec.Builder methodBuilder =
+ MethodSpec.methodBuilder(method.simpleName())
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(method.returnTypeTypeName())
+ .addParameters(
+ GeneratorUtilities.extractParametersFromMethod(
+ crossProfileType.supportedTypes(),
+ method.methodElement(),
+ REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS));
+
+ CodeBlock methodCall =
+ CodeBlock.of(
+ "$L.$L($L)",
+ getCrossProfileTypeReference(method, crossProfileType),
+ method.simpleName(),
+ method.commaSeparatedParameters(
+ crossProfileType.supportedTypes(), REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS));
+
+ if (method.returnType().getKind() != TypeKind.VOID) {
+ methodCall = CodeBlock.of("$1T returnValue = $2L", method.returnType(), methodCall);
+ }
+
+ TypeMirror rawFutureType = TypeUtils.removeTypeArguments(method.returnType());
+
+ FutureWrapper futureWrapper =
+ crossProfileType.supportedTypes().getType(rawFutureType).getFutureWrapper().get();
+
+ // This assumes futures are only generic on one argument, which is enforced
+ TypeMirror wrappedType = TypeUtils.extractTypeArguments(method.returnType()).get(0);
+ ParameterizedTypeName futureWrapperType =
+ ParameterizedTypeName.get(futureWrapper.wrapperClassName(), ClassName.get(wrappedType));
+
+ methodBuilder.beginControlFlow("if (!connector.isAvailable())");
+ methodBuilder.addStatement(
+ "$1T failedFuture = $2T.create(new $3T(), $4L)",
+ futureWrapperType,
+ futureWrapper.wrapperClassName(),
+ BundlerGenerator.getBundlerClassName(generatorContext, crossProfileType),
+ TypeUtils.generateBundlerType(wrappedType));
+ methodBuilder.addStatement(
+ "failedFuture.onException(new $1T($2S))",
+ UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME,
+ "Could not access other profile");
+ methodBuilder.addStatement("return failedFuture.getFuture()");
+ methodBuilder.endControlFlow();
+
+ methodBuilder.beginControlFlow("try");
+ methodBuilder.addStatement("connector.automaticallyConnect()");
+ methodBuilder.addStatement(methodCall);
+ if (method.returnType().getKind() != TypeKind.VOID) {
+ methodBuilder.addStatement("return returnValue");
+ }
+ methodBuilder.nextControlFlow("catch ($T e)", RuntimeException.class);
+ methodBuilder.addStatement("throw new $T(e)", PROFILE_RUNTIME_EXCEPTION_CLASSNAME);
+ methodBuilder.endControlFlow();
+
+ classBuilder.addMethod(methodBuilder.build());
+ }
+
+ static ClassName getFakeOtherClassName(
+ GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) {
+ return GeneratorUtilities.appendToClassName(crossProfileType.profileClassName(), "_FakeOther");
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeProfileConnectorGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeProfileConnectorGenerator.java
new file mode 100644
index 0000000..5ff51ea
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeProfileConnectorGenerator.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.ABSTRACT_FAKE_PROFILE_CONNECTOR_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CONTEXT_CLASSNAME;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector.ProfileType;
+import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext;
+import com.google.android.enterprise.connectedapps.processor.containers.ProfileConnectorInfo;
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.MethodSpec;
+import com.squareup.javapoet.TypeSpec;
+import javax.lang.model.element.Modifier;
+
+class FakeProfileConnectorGenerator {
+ private boolean generated = false;
+ private final ProfileConnectorInfo connector;
+ private final GeneratorUtilities generatorUtilities;
+
+ public FakeProfileConnectorGenerator(
+ GeneratorContext generatorContext, ProfileConnectorInfo connector) {
+ this.generatorUtilities = new GeneratorUtilities(checkNotNull(generatorContext));
+ this.connector = checkNotNull(connector);
+ }
+
+ void generate() {
+ if (generated) {
+ throw new IllegalStateException(
+ "FakeProfileConectorGenerator#generate can only be called once");
+ }
+ generated = true;
+
+ generateFakeProfileConnector();
+ }
+
+ private void generateFakeProfileConnector() {
+ ClassName className = getFakeProfileConnectorClassName(connector);
+
+ TypeSpec.Builder classBuilder =
+ TypeSpec.classBuilder(className)
+ .addJavadoc(
+ "Fake Profile Connector for {@link $1T}.\n\n"
+ + "<p>All functionality is implemented by {@link $2T}, this class is just used"
+ + " for compatibility with the {@link $1T} interface.\n",
+ connector.connectorClassName(),
+ ABSTRACT_FAKE_PROFILE_CONNECTOR_CLASSNAME)
+ .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
+ .addSuperinterface(connector.connectorClassName())
+ .superclass(ABSTRACT_FAKE_PROFILE_CONNECTOR_CLASSNAME);
+
+ if (connector.primaryProfile().equals(ProfileType.UNKNOWN)) {
+ // Special case - we need to provide the profile type to the fake.
+ classBuilder.addMethod(
+ MethodSpec.constructorBuilder()
+ .addParameter(CONTEXT_CLASSNAME, "context")
+ .addModifiers(Modifier.PUBLIC)
+ .addStatement("super(context, $T.NONE)", ProfileType.class)
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.constructorBuilder()
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(CONTEXT_CLASSNAME, "context")
+ .addParameter(ProfileType.class, "primaryProfile")
+ .addStatement("super(context, primaryProfile)")
+ .build());
+ } else {
+ classBuilder.addMethod(
+ MethodSpec.constructorBuilder()
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(CONTEXT_CLASSNAME, "context")
+ .addStatement(
+ "super(context, $T.$L)", ProfileType.class, connector.primaryProfile().name())
+ .build());
+ }
+
+ generatorUtilities.writeClassToFile(className.packageName(), classBuilder);
+ }
+
+ static ClassName getFakeProfileConnectorClassName(ProfileConnectorInfo connector) {
+ return ClassName.get(
+ connector.connectorClassName().packageName(),
+ "Fake" + connector.connectorClassName().simpleName());
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FutureWrappersGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FutureWrappersGenerator.java
new file mode 100644
index 0000000..78902c4
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FutureWrappersGenerator.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toSet;
+
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.FutureWrapper;
+import com.google.android.enterprise.connectedapps.processor.containers.FutureWrapper.WrapperType;
+import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext;
+import com.google.android.enterprise.connectedapps.processor.containers.Type;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.nio.charset.Charset;
+import java.util.Collection;
+import java.util.Optional;
+import javax.tools.JavaFileObject;
+
+/**
+ * Generate the wrapper classes for every used future type.
+ *
+ * <p>This is intended to be initialised and used once, which will generate all needed code.
+ *
+ * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to
+ * validate that the annotated code is correct.
+ */
+final class FutureWrappersGenerator {
+
+ private boolean generated = false;
+ private final GeneratorContext generatorContext;
+
+ FutureWrappersGenerator(GeneratorContext generatorContext) {
+ this.generatorContext = checkNotNull(generatorContext);
+ }
+
+ void generate() {
+ if (generated) {
+ throw new IllegalStateException("FutureWrappersGenerator#generate can only be called once");
+ }
+ generated = true;
+
+ generateFutureWrappers();
+ }
+
+ private void generateFutureWrappers() {
+ Collection<FutureWrapper> futureWrappersToGenerate =
+ generatorContext.crossProfileTypes().stream()
+ .map(CrossProfileTypeInfo::supportedTypes)
+ .flatMap(s -> s.usableTypes().stream())
+ .filter(s -> s.getFutureWrapper().isPresent())
+ .map(Type::getFutureWrapper)
+ .map(Optional::get)
+ .filter(w -> w.wrapperType().equals(WrapperType.DEFAULT))
+ .collect(toSet());
+
+ for (FutureWrapper futureWrapper : futureWrappersToGenerate) {
+ generateFutureWrapper(futureWrapper);
+ }
+ }
+
+ private void generateFutureWrapper(FutureWrapper futureWrapper) {
+ String futureWrapperSimpleName = futureWrapper.defaultWrapperClassName().simpleName();
+
+ String contents;
+ InputStream in =
+ ParcelableWrappersGenerator.class.getResourceAsStream(
+ "/futurewrappers/" + futureWrapperSimpleName + ".java");
+
+ try (BufferedReader br =
+ new BufferedReader(new InputStreamReader(in, Charset.defaultCharset()))) {
+ contents = br.lines().collect(joining(System.lineSeparator()));
+ } catch (IOException e) {
+ throw new IllegalStateException(
+ "Could not read futurewrapper file for " + futureWrapperSimpleName, e);
+ }
+
+ contents =
+ contents.replace(
+ futureWrapper.defaultWrapperClassName().packageName(),
+ futureWrapper.wrapperClassName().packageName());
+ contents =
+ contents.replace(
+ futureWrapper.defaultWrapperClassName().simpleName(),
+ futureWrapper.wrapperClassName().simpleName());
+
+ JavaFileObject builderFile;
+ try {
+ builderFile =
+ generatorContext
+ .processingEnv()
+ .getFiler()
+ .createSourceFile(
+ futureWrapper.wrapperClassName().packageName()
+ + "."
+ + futureWrapper.wrapperClassName().simpleName());
+ } catch (IOException e) {
+ throw new IllegalStateException(
+ "Could not write futurewrapper for " + futureWrapperSimpleName, e);
+ }
+
+ try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {
+ out.write(contents);
+ } catch (IOException e) {
+ throw new IllegalStateException(
+ "Could not write futurewrapper for " + futureWrapperSimpleName, e);
+ }
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/GeneratorUtilities.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/GeneratorUtilities.java
new file mode 100644
index 0000000..7d4dbd7
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/GeneratorUtilities.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCELABLE_CREATOR_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.LEAVE_AUTOMATICALLY_RESOLVED_PARAMETERS;
+import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS;
+import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+
+import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder;
+import com.google.android.enterprise.connectedapps.processor.containers.Context;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo;
+import com.google.common.collect.Iterables;
+import com.squareup.javapoet.AnnotationSpec;
+import com.squareup.javapoet.ArrayTypeName;
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.CodeBlock;
+import com.squareup.javapoet.FieldSpec;
+import com.squareup.javapoet.JavaFile;
+import com.squareup.javapoet.MethodSpec;
+import com.squareup.javapoet.ParameterSpec;
+import com.squareup.javapoet.ParameterizedTypeName;
+import com.squareup.javapoet.TypeName;
+import com.squareup.javapoet.TypeSpec;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.List;
+import java.util.Set;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.MirroredTypeException;
+import javax.lang.model.type.MirroredTypesException;
+import javax.lang.model.type.PrimitiveType;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.util.Types;
+import javax.tools.JavaFileObject;
+
+/** Utility methods used for code generation. */
+public final class GeneratorUtilities {
+
+ private final Context context;
+
+ public GeneratorUtilities(Context context) {
+ this.context = checkNotNull(context);
+ }
+
+ /**
+ * Extract a class provided in an annotation.
+ *
+ * <p>The {@code runnable} should call the annotation method that the class is being extracted
+ * for.
+ */
+ public static TypeElement extractClassFromAnnotation(Types types, Runnable runnable) {
+ // From https://docs.oracle.com/javase/8/docs/api/javax/lang/model/AnnotatedConstruct.html
+ // "The annotation returned by this method could contain an element whose value is of type
+ // Class. This value cannot be returned directly: information necessary to locate and load a
+ // class (such as the class loader to use) is not available, and the class might not be loadable
+ // at all. Attempting to read a Class object by invoking the relevant method on the returned
+ // annotation will result in a MirroredTypeException, from which the corresponding TypeMirror
+ // may be extracted."
+ try {
+ runnable.run();
+ } catch (MirroredTypeException e) {
+ return e.getTypeMirrors().stream()
+ .map(t -> (TypeElement) types.asElement(t))
+ .findFirst()
+ .get();
+ }
+ throw new AssertionError("Could not extract class from annotation");
+ }
+
+ /**
+ * Extract classes provided in an annotation.
+ *
+ * <p>The {@code runnable} should call the annotation method that the classes are being extracted
+ * for.
+ */
+ public static List<TypeElement> extractClassesFromAnnotation(Types types, Runnable runnable) {
+ // From https://docs.oracle.com/javase/8/docs/api/javax/lang/model/AnnotatedConstruct.html
+ // "The annotation returned by this method could contain an element whose value is of type
+ // Class. This value cannot be returned directly: information necessary to locate and load a
+ // class (such as the class loader to use) is not available, and the class might not be loadable
+ // at all. Attempting to read a Class object by invoking the relevant method on the returned
+ // annotation will result in a MirroredTypeException, from which the corresponding TypeMirror
+ // may be extracted."
+ try {
+ runnable.run();
+ } catch (MirroredTypesException e) {
+ return e.getTypeMirrors().stream()
+ .map(t -> (TypeElement) types.asElement(t))
+ .collect(toList());
+ }
+ throw new AssertionError("Could not extract classes from annotation");
+ }
+
+ public static Set<ExecutableElement> findCrossProfileMethodsInClass(TypeElement clazz) {
+ return clazz.getEnclosedElements().stream()
+ .filter(e -> e instanceof ExecutableElement)
+ .map(e -> (ExecutableElement) e)
+ .filter(e -> e.getKind() == ElementKind.METHOD)
+ .filter(AnnotationFinder::hasCrossProfileAnnotation)
+ .collect(toSet());
+ }
+
+ public static Set<ExecutableElement> findCrossProfileProviderMethodsInClass(TypeElement clazz) {
+ return clazz.getEnclosedElements().stream()
+ .filter(e -> e instanceof ExecutableElement)
+ .map(e -> (ExecutableElement) e)
+ .filter(e -> e.getKind() == ElementKind.METHOD)
+ .filter(AnnotationFinder::hasCrossProfileProviderAnnotation)
+ .collect(toSet());
+ }
+
+ /** Generate a {@code @link} reference to a given method. */
+ public static CodeBlock methodJavadocReference(ExecutableElement method) {
+ CodeBlock.Builder methodCall = CodeBlock.builder();
+ methodCall.add("{@link $T#", method.getEnclosingElement());
+ methodCall.add("$L(", method.getSimpleName());
+
+ if (!method.getParameters().isEmpty()) {
+ methodCall.add("$T", method.getParameters().iterator().next().asType());
+
+ for (VariableElement param :
+ method.getParameters().subList(1, method.getParameters().size())) {
+ methodCall.add(",$T", param.asType());
+ }
+ }
+
+ methodCall.add(")}");
+ return methodCall.build();
+ }
+
+ public void writeClassToFile(String packageName, TypeSpec.Builder clazzBuilder) {
+ writeClassToFile(packageName, clazzBuilder.build());
+ }
+
+ void writeClassToFile(String packageName, TypeSpec clazz) {
+ final String qualifiedClassName =
+ packageName.isEmpty() ? clazz.name : packageName + "." + clazz.name;
+
+ JavaFile javaFile = JavaFile.builder(packageName, clazz).build();
+ try {
+ JavaFileObject builderFile =
+ context.processingEnv().getFiler().createSourceFile(qualifiedClassName);
+ try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {
+ javaFile.writeTo(out);
+ }
+ } catch (IOException e) {
+ throw new IllegalStateException("Error writing " + qualifiedClassName + " to file", e);
+ }
+ }
+
+ /**
+ * Take the parameters of an {@link ExecutableElement} and return {@link ParameterSpec} instances
+ * ready to be used with a generated method.
+ */
+ static List<ParameterSpec> extractParametersFromMethod(
+ SupportedTypes supportedTypes,
+ ExecutableElement method,
+ AutomaticallyResolvedParameterFilterBehaviour filterBehaviour) {
+ if (filterBehaviour == LEAVE_AUTOMATICALLY_RESOLVED_PARAMETERS) {
+ return extractParametersFromMethod(method);
+ } else if (filterBehaviour == REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS) {
+ return method.getParameters().stream()
+ .filter(param -> !supportedTypes.isAutomaticallyResolved(param.asType()))
+ .map(GeneratorUtilities::convertVariableToParameterSpec)
+ .collect(toList());
+ } else if (filterBehaviour == REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS) {
+ throw new IllegalArgumentException("Can not replace parameters when extracting");
+ }
+ throw new IllegalArgumentException("Unknown filterBehaviour " + filterBehaviour);
+ }
+
+ /**
+ * Take the parameters of an {@link ExecutableElement} and return {@link ParameterSpec} instances
+ * ready to be used with a generated method.
+ *
+ * <p>This will not filter automatically resolved parameters. For that functionality use {@link
+ * #extractParametersFromMethod(SupportedTypes, ExecutableElement,
+ * AutomaticallyResolvedParameterFilterBehaviour)}.
+ */
+ static List<ParameterSpec> extractParametersFromMethod(ExecutableElement method) {
+ return method.getParameters().stream()
+ .map(GeneratorUtilities::convertVariableToParameterSpec)
+ .collect(toList());
+ }
+
+ private static ParameterSpec convertVariableToParameterSpec(VariableElement variable) {
+ ParameterSpec.Builder builder =
+ ParameterSpec.builder(
+ ClassName.get(variable.asType()), variable.getSimpleName().toString());
+ builder.addModifiers(variable.getModifiers());
+ return builder.build();
+ }
+
+ /** If type is primitive, return the boxed version of that type, otherwise return the type. */
+ TypeMirror boxIfNecessary(TypeMirror type) {
+ if (!type.getKind().isPrimitive()) {
+ return type;
+ }
+
+ PrimitiveType primitiveType = (PrimitiveType) type;
+ return context.types().boxedClass(primitiveType).asType();
+ }
+
+ void addDefaultParcelableMethods(TypeSpec.Builder classBuilder, ClassName className) {
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("describeContents")
+ .addModifiers(Modifier.PUBLIC)
+ .addAnnotation(Override.class)
+ .returns(int.class)
+ .addStatement("return 0")
+ .build());
+
+ TypeName creatorType = ParameterizedTypeName.get(PARCELABLE_CREATOR_CLASSNAME, className);
+
+ TypeSpec creator =
+ TypeSpec.anonymousClassBuilder("")
+ .addSuperinterface(creatorType)
+ .addMethod(
+ MethodSpec.methodBuilder("createFromParcel")
+ .addModifiers(Modifier.PUBLIC)
+ .addAnnotation(Override.class)
+ .returns(className)
+ .addParameter(PARCEL_CLASSNAME, "in")
+ .addStatement("return new $T(in)", className)
+ .build())
+ .addMethod(
+ MethodSpec.methodBuilder("newArray")
+ .addModifiers(Modifier.PUBLIC)
+ .addAnnotation(Override.class)
+ .returns(ArrayTypeName.of(className))
+ .addParameter(int.class, "size")
+ .addStatement("return new $T[size]", className)
+ .build())
+ .build();
+
+ classBuilder.addField(
+ FieldSpec.builder(creatorType, "CREATOR")
+ .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
+ .addAnnotation(
+ AnnotationSpec.builder(SuppressWarnings.class)
+ .addMember("value", "$S", "rawtypes")
+ .build())
+ .initializer("$L", creator)
+ .build());
+ }
+
+ /** Generate a reference to a cross-profile method which can be used in javadoc. */
+ public static CodeBlock generateMethodReference(
+ CrossProfileTypeInfo crossProfileType, CrossProfileMethodInfo method) {
+ CodeBlock.Builder reference = CodeBlock.builder();
+
+ reference.add("$T#$L(", crossProfileType.className(), method.simpleName());
+
+ List<TypeMirror> parameterTypes = convertParametersToTypes(method);
+
+ if (!parameterTypes.isEmpty()) {
+ for (int i = 0; i < parameterTypes.size() - 1; i++) {
+ reference.add("$T, ", TypeUtils.getRawTypeClassName(parameterTypes.get(i)));
+ }
+ reference.add("$T", TypeUtils.getRawTypeClassName(Iterables.getLast(parameterTypes)));
+ }
+
+ reference.add(")");
+ return reference.build();
+ }
+
+ private static List<TypeMirror> convertParametersToTypes(CrossProfileMethodInfo method) {
+ return method.methodElement().getParameters().stream().map(Element::asType).collect(toList());
+ }
+
+ static ClassName appendToClassName(ClassName originalClassName, String suffix) {
+ return ClassName.get(originalClassName.packageName(), originalClassName.simpleName() + suffix);
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/IfAvailableGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/IfAvailableGenerator.java
new file mode 100644
index 0000000..68d84f4
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/IfAvailableGenerator.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.IF_AVAILABLE_FUTURE_RESULT_WRITER;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.GeneratorUtilities.generateMethodReference;
+import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackInterfaceInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.FutureWrapper;
+import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext;
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.FieldSpec;
+import com.squareup.javapoet.MethodSpec;
+import com.squareup.javapoet.TypeSpec;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+
+/**
+ * Generate the {@code Profile_*_IfAvailable} class for a single cross-profile type.
+ *
+ * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to
+ * validate that the annotated code is correct.
+ */
+final class IfAvailableGenerator {
+
+ private boolean generated = false;
+ private final GeneratorContext generatorContext;
+ private final GeneratorUtilities generatorUtilities;
+ private final CrossProfileTypeInfo crossProfileType;
+
+ IfAvailableGenerator(GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) {
+ this.generatorContext = checkNotNull(generatorContext);
+ this.generatorUtilities = new GeneratorUtilities(generatorContext);
+ this.crossProfileType = checkNotNull(crossProfileType);
+ }
+
+ void generate() {
+ if (generated) {
+ throw new IllegalStateException("IfAvailableGenerator#generate can only be called once");
+ }
+ generated = true;
+
+ generateIfAvailableClass();
+ }
+
+ private void generateIfAvailableClass() {
+ ClassName className = getIfAvailableClassName(generatorContext, crossProfileType);
+
+ ClassName singleSenderCanThrowInterface =
+ InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName(
+ generatorContext, crossProfileType);
+ TypeSpec.Builder classBuilder =
+ TypeSpec.classBuilder(className)
+ .addJavadoc(
+ "Wrapper of {@link $T} which will replace\n{@link $T} with default values.\n",
+ singleSenderCanThrowInterface,
+ UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME)
+ .addModifiers(Modifier.PUBLIC, Modifier.FINAL);
+
+ classBuilder.addField(
+ FieldSpec.builder(singleSenderCanThrowInterface, "singleSenderCanThrow")
+ .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.constructorBuilder()
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(singleSenderCanThrowInterface, "singleSenderCanThrow")
+ .addStatement("this.singleSenderCanThrow = singleSenderCanThrow")
+ .build());
+
+ for (CrossProfileMethodInfo method : crossProfileType.crossProfileMethods()) {
+ generateMethodOnIfAvailableClass(classBuilder, method, crossProfileType);
+ }
+
+ generatorUtilities.writeClassToFile(className.packageName(), classBuilder);
+ }
+
+ private void generateMethodOnIfAvailableClass(
+ TypeSpec.Builder classBuilder,
+ CrossProfileMethodInfo method,
+ CrossProfileTypeInfo crossProfileType) {
+
+ MethodSpec.Builder methodBuilder =
+ MethodSpec.methodBuilder(method.simpleName())
+ .addModifiers(Modifier.PUBLIC)
+ .addExceptions(method.thrownExceptions())
+ .addParameters(
+ GeneratorUtilities.extractParametersFromMethod(
+ crossProfileType.supportedTypes(),
+ method.methodElement(),
+ REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS))
+ .addJavadoc("Call {@link $L}.", generateMethodReference(crossProfileType, method));
+
+ if (method.isBlocking(generatorContext, crossProfileType)) {
+ if (method.returnType().getKind().equals(TypeKind.VOID)) {
+ methodBuilder
+ .addJavadoc(
+ "\n\n<p>{@link $T} will be ignored.\n", UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME)
+ .beginControlFlow("try")
+ .addStatement(
+ "singleSenderCanThrow.$L($L)",
+ method.simpleName(),
+ method.commaSeparatedParameters(
+ crossProfileType.supportedTypes(), REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS))
+ .nextControlFlow("catch ($T e)", UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME)
+ .addComment("Ignore exception")
+ .endControlFlow();
+ } else {
+ methodBuilder.addParameter(method.returnTypeTypeName(), "defaultValue");
+ methodBuilder
+ .addJavadoc(
+ "\n\n<p>In case of {@link $T}, {@code defaultValue} will be returned.\n",
+ UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME)
+ .returns(method.returnTypeTypeName())
+ .beginControlFlow("try")
+ .addStatement(
+ "return singleSenderCanThrow.$L($L)",
+ method.simpleName(),
+ method.commaSeparatedParameters(
+ crossProfileType.supportedTypes(), REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS))
+ .nextControlFlow("catch ($T e)", UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME)
+ .addStatement("return defaultValue")
+ .endControlFlow();
+ }
+ } else if (method.isCrossProfileCallback(generatorContext)) {
+ if (!method.isSimpleCrossProfileCallback(generatorContext)) {
+ // Non-simple callbacks can't be used with multiple profiles
+ return;
+ }
+
+ CrossProfileCallbackInterfaceInfo callbackInterface =
+ CrossProfileCallbackInterfaceInfo.create(
+ (TypeElement)
+ generatorContext
+ .types()
+ .asElement(
+ method.getCrossProfileCallbackParam(generatorContext).get().asType()));
+ if (callbackInterface.argumentTypes().isEmpty()) {
+ // Void
+ // This assumes a single callback method
+ methodBuilder
+ .addJavadoc(
+ "\n\n<p>If the profile is not available, the callback will be called anyway.\n")
+ .addStatement(
+ "singleSenderCanThrow.$1L(\n $2L,\n (e) -> {$3L.$4L();})",
+ method.simpleName(),
+ method.commaSeparatedParameters(
+ crossProfileType.supportedTypes(), REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS),
+ method.getCrossProfileCallbackParam(generatorContext).get().getSimpleName(),
+ callbackInterface.methods().get(0).getSimpleName());
+ } else {
+ // This assumes a single callback method
+ methodBuilder.addParameter(
+ ClassName.get(callbackInterface.argumentTypes().iterator().next()), "defaultValue");
+ methodBuilder
+ .addJavadoc(
+ "\n\n"
+ + "<p>If the profile is not available, the callback will be called with the"
+ + " {@code defaultValue}.\n")
+ .addStatement(
+ "singleSenderCanThrow.$1L(\n $2L,\n (e) -> {$3L.$4L(defaultValue);})",
+ method.simpleName(),
+ method.commaSeparatedParameters(
+ crossProfileType.supportedTypes(), REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS),
+ method.getCrossProfileCallbackParam(generatorContext).get().getSimpleName(),
+ callbackInterface.methods().get(0).getSimpleName());
+ }
+ } else if (method.isFuture(crossProfileType)) {
+ // This assumes a Future is generic on a single type
+ TypeMirror wrappedReturnType = TypeUtils.extractTypeArguments(method.returnType()).get(0);
+ TypeMirror rawFutureType = TypeUtils.removeTypeArguments(method.returnType());
+ FutureWrapper futureWrapper =
+ crossProfileType.supportedTypes().getType(rawFutureType).getFutureWrapper().get();
+
+ methodBuilder
+ .addParameter(ClassName.get(wrappedReturnType), "defaultValue")
+ .returns(ClassName.get(method.returnType()));
+ methodBuilder
+ .addJavadoc(
+ "\n\n"
+ + "<p>If the profile is not available, the future will be resolved with the"
+ + " {@code defaultValue}.\n")
+ .addStatement(
+ "$1T internalCrossProfileClass = $1T.instance()",
+ InternalCrossProfileClassGenerator.getInternalCrossProfileClassName(
+ generatorContext, crossProfileType))
+ .addStatement(
+ "$1T<$2T> futureWrapper = $1T.create(internalCrossProfileClass.bundler(), $3L)",
+ futureWrapper.wrapperClassName(),
+ wrappedReturnType,
+ TypeUtils.generateBundlerType(wrappedReturnType))
+ .addStatement(
+ "$T.writeFutureResult(singleSenderCanThrow.$L($L), new"
+ + " $T<$T>(futureWrapper, defaultValue))",
+ futureWrapper.wrapperClassName(),
+ method.simpleName(),
+ method.commaSeparatedParameters(
+ crossProfileType.supportedTypes(), REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS),
+ IF_AVAILABLE_FUTURE_RESULT_WRITER,
+ wrappedReturnType)
+ .addStatement("return futureWrapper.getFuture()");
+ }
+ classBuilder.addMethod(methodBuilder.build());
+ }
+
+ static ClassName getIfAvailableClassName(
+ GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) {
+ return GeneratorUtilities.appendToClassName(
+ crossProfileType.profileClassName(), "_IfAvailable");
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InterfaceGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InterfaceGenerator.java
new file mode 100644
index 0000000..a8c76c6
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InterfaceGenerator.java
@@ -0,0 +1,690 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.EXCEPTION_CALLBACK_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_CONNECTOR_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_RUNTIME_EXCEPTION_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.GeneratorUtilities.generateMethodReference;
+import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.stream.Collectors.toList;
+
+import com.google.android.enterprise.connectedapps.annotations.CrossProfile;
+import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector;
+import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector.ProfileType;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackInterfaceInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext;
+import com.google.common.base.Ascii;
+import com.squareup.javapoet.AnnotationSpec;
+import com.squareup.javapoet.ArrayTypeName;
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.CodeBlock;
+import com.squareup.javapoet.MethodSpec;
+import com.squareup.javapoet.ParameterSpec;
+import com.squareup.javapoet.ParameterizedTypeName;
+import com.squareup.javapoet.TypeName;
+import com.squareup.javapoet.TypeSpec;
+import java.util.List;
+import java.util.Map;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+
+/** Generator of cross-profile code for a single {@link CrossProfile} type. */
+final class InterfaceGenerator {
+
+ private boolean generated = false;
+ private final GeneratorContext generatorContext;
+ private final GeneratorUtilities generatorUtilities;
+ private final CrossProfileTypeInfo crossProfileType;
+
+ InterfaceGenerator(GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) {
+ this.generatorContext = checkNotNull(generatorContext);
+ this.generatorUtilities = new GeneratorUtilities(generatorContext);
+ this.crossProfileType = checkNotNull(crossProfileType);
+ }
+
+ void generate() {
+ if (generated) {
+ throw new IllegalStateException("InterfaceGenerator#generate can only be called once");
+ }
+ generated = true;
+
+ generateCrossProfileTypeInterface();
+ generateSingleSenderInterface();
+ generateSingleSenderCanThrowInterface();
+ generateMultipleSenderInterface();
+ }
+
+ private void generateCrossProfileTypeInterface() {
+ ClassName interfaceName =
+ getCrossProfileTypeInterfaceClassName(generatorContext, crossProfileType);
+
+ TypeSpec.Builder interfaceBuilder =
+ TypeSpec.interfaceBuilder(interfaceName)
+ .addJavadoc(
+ "Entry point for cross-profile calls to {@link $T}.\n",
+ crossProfileType.className())
+ .addModifiers(Modifier.PUBLIC);
+
+ ClassName connectorClassName =
+ crossProfileType.profileConnector().isPresent()
+ ? crossProfileType.profileConnector().get().connectorClassName()
+ : PROFILE_CONNECTOR_CLASSNAME;
+
+ interfaceBuilder.addMethod(
+ MethodSpec.methodBuilder("create")
+ .returns(interfaceName)
+ .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
+ .addParameter(connectorClassName, "connector")
+ .addStatement(
+ "return new $T(connector)",
+ DefaultProfileClassGenerator.getDefaultProfileClassName(
+ generatorContext, crossProfileType))
+ .build());
+
+ interfaceBuilder.addMethod(
+ MethodSpec.methodBuilder("current")
+ .addJavadoc("Run a method on the current profile.\n")
+ .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
+ .returns(getSingleSenderInterfaceClassName(generatorContext, crossProfileType))
+ .build());
+
+ interfaceBuilder.addMethod(
+ MethodSpec.methodBuilder("other")
+ .addJavadoc("Run a method on the other profile, if accessible.\n")
+ .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
+ .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType))
+ .build());
+
+ interfaceBuilder.addMethod(
+ MethodSpec.methodBuilder("personal")
+ .addJavadoc("Run a method on the personal profile, if accessible.\n")
+ .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
+ .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType))
+ .build());
+
+ interfaceBuilder.addMethod(
+ MethodSpec.methodBuilder("work")
+ .addJavadoc("Run a method on the work profile, if accessible.\n")
+ .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
+ .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType))
+ .build());
+
+ interfaceBuilder.addMethod(
+ MethodSpec.methodBuilder("profile")
+ .addJavadoc("Run a method on the given profile, if accessible.\n")
+ .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
+ .addParameter(PROFILE_CLASSNAME, "profile")
+ .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType))
+ .build());
+
+ interfaceBuilder.addMethod(
+ MethodSpec.methodBuilder("profiles")
+ .addJavadoc(
+ CodeBlock.builder()
+ .add("Run a method on the given profiles, if accessible.\n\n")
+ .add(
+ "<p>This will deduplicate profiles to ensure that the method is only run"
+ + " at most once on each profile.\n")
+ .build())
+ .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
+ .addParameter(ArrayTypeName.of(PROFILE_CLASSNAME), "profiles")
+ .varargs(true)
+ .returns(getMultipleSenderInterfaceClassName(generatorContext, crossProfileType))
+ .build());
+
+ interfaceBuilder.addMethod(
+ MethodSpec.methodBuilder("both")
+ .addJavadoc("Run a method on both the personal and work profile, if accessible.\n")
+ .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
+ .returns(getMultipleSenderInterfaceClassName(generatorContext, crossProfileType))
+ .build());
+
+ if (!crossProfileType.profileConnector().isPresent()
+ || crossProfileType.profileConnector().get().primaryProfile() != ProfileType.NONE) {
+ generatePrimarySecondaryMethods(interfaceBuilder);
+ }
+
+ generatorUtilities.writeClassToFile(interfaceName.packageName(), interfaceBuilder);
+ }
+
+ private void generatePrimarySecondaryMethods(TypeSpec.Builder interfaceBuilder) {
+ generatePrimaryMethod(interfaceBuilder);
+ generateSecondaryMethod(interfaceBuilder);
+ generateSuppliersMethod(interfaceBuilder);
+ }
+
+ private void generatePrimaryMethod(TypeSpec.Builder interfaceBuilder) {
+ MethodSpec.Builder methodBuilder =
+ MethodSpec.methodBuilder("primary")
+ .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
+ .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType));
+
+ if (crossProfileType.profileConnector().isPresent()) {
+ methodBuilder.addJavadoc(
+ "Run a method on the primary ("
+ + Ascii.toLowerCase(crossProfileType.profileConnector().get().primaryProfile().name())
+ + ") profile, if accessible.\n\n@see $T#primaryProfile()\n",
+ CustomProfileConnector.class);
+ } else {
+ methodBuilder.addJavadoc(
+ "Run a method on the primary profile, if accessible.\n\n"
+ + "@throws $1T if the {@link $2T} does not have a primary profile set\n"
+ + "@see $2T#primaryProfile()\n",
+ IllegalStateException.class,
+ CustomProfileConnector.class);
+ }
+
+ interfaceBuilder.addMethod(methodBuilder.build());
+ }
+
+ private void generateSecondaryMethod(TypeSpec.Builder interfaceBuilder) {
+ MethodSpec.Builder methodBuilder =
+ MethodSpec.methodBuilder("secondary")
+ .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
+ .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType));
+
+ if (crossProfileType.profileConnector().isPresent()) {
+ String secondaryProfileName =
+ crossProfileType.profileConnector().get().primaryProfile().equals(ProfileType.WORK)
+ ? Ascii.toLowerCase(ProfileType.PERSONAL.name())
+ : Ascii.toLowerCase(ProfileType.WORK.name());
+ methodBuilder.addJavadoc(
+ "Run a method on the secondary ("
+ + secondaryProfileName
+ + ") profile, if accessible.\n\n@see $T#primaryProfile()\n",
+ CustomProfileConnector.class);
+ } else {
+ methodBuilder.addJavadoc(
+ "Run a method on the secondary profile, if accessible.\n\n"
+ + "@throws $1T if the {@link $2T} does not have a primary profile set\n"
+ + "@see $2T#primaryProfile()\n",
+ IllegalStateException.class,
+ CustomProfileConnector.class);
+ }
+
+ interfaceBuilder.addMethod(methodBuilder.build());
+ }
+
+ private void generateSuppliersMethod(TypeSpec.Builder interfaceBuilder) {
+ MethodSpec.Builder methodBuilder =
+ MethodSpec.methodBuilder("suppliers")
+ .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
+ .returns(getMultipleSenderInterfaceClassName(generatorContext, crossProfileType));
+
+ if (crossProfileType.profileConnector().isPresent()) {
+ String primaryProfileName =
+ crossProfileType.profileConnector().get().primaryProfile().equals(ProfileType.WORK)
+ ? Ascii.toLowerCase(ProfileType.WORK.name())
+ : Ascii.toLowerCase(ProfileType.PERSONAL.name());
+ String secondaryProfileName =
+ crossProfileType.profileConnector().get().primaryProfile().equals(ProfileType.WORK)
+ ? Ascii.toLowerCase(ProfileType.PERSONAL.name())
+ : Ascii.toLowerCase(ProfileType.WORK.name());
+ methodBuilder
+ .addJavadoc("Run a method on supplier profiles, if accessible.\n\n")
+ .addJavadoc(
+ "<p>When run from the primary ($1L) profile, supplier profiles are the primary ($1L)"
+ + " and secondary ($2L) profiles. When run from the secondary ($2L) profile,"
+ + " supplier profiles includes only the secondary ($2L) profile.\n\n",
+ primaryProfileName,
+ secondaryProfileName)
+ .addJavadoc("@see $T#primaryProfile()\n", CustomProfileConnector.class);
+ } else {
+ methodBuilder
+ .addJavadoc("Run a method on supplier profiles, if accessible.\n\n")
+ .addJavadoc(
+ "<p>When run from the primary profile, supplier profiles are the primary and"
+ + " secondary profiles. When run from the secondary profile, supplier profiles"
+ + " includes only the secondary profile.\n\n")
+ .addJavadoc(
+ "@throws $1T if the {@link $2T} does not have a primary profile set\n",
+ IllegalStateException.class,
+ CustomProfileConnector.class)
+ .addJavadoc("@see $T#primaryProfile()\n", CustomProfileConnector.class);
+ }
+
+ interfaceBuilder.addMethod(methodBuilder.build());
+ }
+
+ private void generateSingleSenderInterface() {
+ ClassName interfaceName = getSingleSenderInterfaceClassName(generatorContext, crossProfileType);
+
+ TypeSpec.Builder interfaceBuilder =
+ TypeSpec.interfaceBuilder(interfaceName)
+ .addModifiers(Modifier.PUBLIC)
+ .addJavadoc(
+ "Interface used for interacting with an instance of {@link $T} on a given"
+ + " profile.\n\n",
+ crossProfileType.className())
+ .addJavadoc(
+ "<p>The profile is guaranteed to be available, so no {@link $T} will be thrown for"
+ + " any call.\n",
+ UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME);
+
+ for (CrossProfileMethodInfo method : crossProfileType.crossProfileMethods()) {
+ generateMethodOnSingleSenderInterface(interfaceBuilder, method, crossProfileType);
+ }
+
+ generatorUtilities.writeClassToFile(interfaceName.packageName(), interfaceBuilder);
+ }
+
+ private void generateMethodOnSingleSenderInterface(
+ TypeSpec.Builder interfaceBuilder,
+ CrossProfileMethodInfo method,
+ CrossProfileTypeInfo crossProfileType) {
+
+ CodeBlock methodReference = generateMethodReference(crossProfileType, method);
+
+ MethodSpec.Builder methodBuilder =
+ MethodSpec.methodBuilder(method.simpleName())
+ .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
+ .returns(method.returnTypeTypeName())
+ .addJavadoc("Make a call to {@link $L} on the given profile.\n\n", methodReference);
+
+ for (TypeMirror automaticallyResolvedType :
+ method.automaticallyResolvedParameterTypes(crossProfileType.supportedTypes())) {
+ methodBuilder.addJavadoc(
+ "<p>A {@link $T} will automatically be passed to {@link $L}.\n\n",
+ automaticallyResolvedType,
+ methodReference);
+ }
+
+ methodBuilder
+ .addJavadoc("@see $L\n", methodReference)
+ .addExceptions(method.thrownExceptions())
+ .addParameters(
+ GeneratorUtilities.extractParametersFromMethod(
+ crossProfileType.supportedTypes(),
+ method.methodElement(),
+ REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS));
+
+ interfaceBuilder.addMethod(methodBuilder.build());
+ }
+
+ private void generateSingleSenderCanThrowInterface() {
+ ClassName interfaceName =
+ getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType);
+
+ TypeSpec.Builder interfaceBuilder =
+ TypeSpec.interfaceBuilder(interfaceName)
+ .addModifiers(Modifier.PUBLIC)
+ .addJavadoc(
+ "Interface used for interacting with a {@link $T} on a given profile.\n",
+ crossProfileType.className());
+
+ for (CrossProfileMethodInfo method : crossProfileType.crossProfileMethods()) {
+ generateMethodOnSingleSenderCanThrowInterface(interfaceBuilder, method, crossProfileType);
+ }
+
+ interfaceBuilder.addMethod(
+ MethodSpec.methodBuilder("ifAvailable")
+ .addJavadoc(
+ "Make a call, returning a default value in case of error rather than throwing"
+ + " {@link $T}.\n",
+ UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME)
+ .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
+ .returns(
+ IfAvailableGenerator.getIfAvailableClassName(generatorContext, crossProfileType))
+ .build());
+
+ interfaceBuilder.addMethod(
+ MethodSpec.methodBuilder("timeout")
+ .addJavadoc(
+ "Set a timeout to be used when making asynchronous calls to other profiles.\n\n"
+ + "<p>This overrides any timeout set on the type or method being called.\n")
+ .addAnnotation(
+ AnnotationSpec.builder(SuppressWarnings.class)
+ .addMember("value", "$S", "GoodTime")
+ .build())
+ .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
+ .returns(interfaceName)
+ .addParameter(long.class, "timeout")
+ .build());
+
+ generatorUtilities.writeClassToFile(interfaceName.packageName(), interfaceBuilder);
+ }
+
+ private void generateMethodOnSingleSenderCanThrowInterface(
+ TypeSpec.Builder interfaceBuilder,
+ CrossProfileMethodInfo method,
+ CrossProfileTypeInfo crossProfileType) {
+
+ CodeBlock methodReference = generateMethodReference(crossProfileType, method);
+
+ MethodSpec.Builder methodBuilder =
+ MethodSpec.methodBuilder(method.simpleName())
+ .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
+ .addExceptions(method.thrownExceptions())
+ .returns(method.returnTypeTypeName())
+ .addJavadoc("Make a call to {@link $L} on the given profile.\n\n", methodReference)
+ .addParameters(
+ GeneratorUtilities.extractParametersFromMethod(
+ crossProfileType.supportedTypes(),
+ method.methodElement(),
+ REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS));
+
+ for (TypeMirror automaticallyResolvedType :
+ method.automaticallyResolvedParameterTypes(crossProfileType.supportedTypes())) {
+ methodBuilder.addJavadoc(
+ "<p>A {@link $T} will automatically be passed to {@link $L}.\n\n",
+ automaticallyResolvedType,
+ methodReference);
+ }
+
+ if (method.isBlocking(generatorContext, crossProfileType)) {
+ methodBuilder.addException(UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME);
+ methodBuilder.addJavadoc(
+ "<p>If an unchecked exception is thrown and this call is made to a profile other than"
+ + " the current one, a {@link $T} will be thrown with the original exception as the"
+ + " cause.\n\n",
+ PROFILE_RUNTIME_EXCEPTION_CLASSNAME);
+ methodBuilder.addJavadoc(
+ "@throws $T if the profile is not connected.\n", UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME);
+ } else if (method.isCrossProfileCallback(generatorContext)) {
+ methodBuilder.addJavadoc(
+ "<p>If an unchecked exception is thrown and this call is made to a profile other than"
+ + " the current one, a {@link $T} will be thrown on another thread with the original"
+ + " exception as the cause.\n\n",
+ PROFILE_RUNTIME_EXCEPTION_CLASSNAME);
+ methodBuilder.addJavadoc(
+ "<p>If the profile does not exist or is not available, an {@link $T} will be passed into"
+ + " the {@code exceptionCallback}.\n\n",
+ UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME);
+ methodBuilder.addParameter(EXCEPTION_CALLBACK_CLASSNAME, "exceptionCallback");
+ } else {
+ // Future
+ methodBuilder.addJavadoc(
+ "<p>If an unchecked exception is thrown and this call is made to a profile other than"
+ + " the current one, a {@link $T} will be thrown on another thread with the original"
+ + " exception as the cause.\n\n",
+ PROFILE_RUNTIME_EXCEPTION_CLASSNAME);
+ methodBuilder.addJavadoc(
+ "<p>If the profile does not exist or is not available, the future will be set with an"
+ + " {@link $T}.\n\n",
+ UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME);
+ }
+
+ methodBuilder.addJavadoc("@see $L\n", methodReference);
+
+ interfaceBuilder.addMethod(methodBuilder.build());
+ }
+
+ private void generateMultipleSenderInterface() {
+ ClassName interfaceName =
+ getMultipleSenderInterfaceClassName(generatorContext, crossProfileType);
+
+ TypeSpec.Builder interfaceBuilder =
+ TypeSpec.interfaceBuilder(interfaceName)
+ .addModifiers(Modifier.PUBLIC)
+ .addJavadoc(
+ "Interface used for interacting with a {@link $T} on multiple profiles.\n\n",
+ crossProfileType.className())
+ .addJavadoc(
+ "<p>If any profiles are unavailable, the profile will not be included in the"
+ + " results. No {@link $T} will be thrown for any call.\n",
+ UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME);
+
+ for (CrossProfileMethodInfo method : crossProfileType.crossProfileMethods()) {
+ if (method.isBlocking(generatorContext, crossProfileType)) {
+ generateBlockingMethodOnMultipleSenderInterface(interfaceBuilder, method, crossProfileType);
+ } else if (method.isCrossProfileCallback(generatorContext)) {
+ generateCrossProfileCallbackMethodOnMultipleSenderInterface(
+ interfaceBuilder, method, crossProfileType);
+ } else if (method.isFuture(crossProfileType)) {
+ generateFutureMethodOnMultipleSenderInterface(interfaceBuilder, method, crossProfileType);
+ } else {
+ throw new IllegalStateException("Unknown method type: " + method);
+ }
+ }
+
+ interfaceBuilder.addMethod(
+ MethodSpec.methodBuilder("timeout")
+ .addJavadoc(
+ "Set a timeout to be used when making asynchronous calls to other profiles.\n\n"
+ + "<p>This overrides any timeout set on the type or method being called.")
+ .addAnnotation(
+ AnnotationSpec.builder(SuppressWarnings.class)
+ .addMember("value", "$S", "GoodTime")
+ .build())
+ .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
+ .returns(interfaceName)
+ .addParameter(long.class, "timeout")
+ .build());
+
+ generatorUtilities.writeClassToFile(interfaceName.packageName(), interfaceBuilder);
+ }
+
+ private void generateBlockingMethodOnMultipleSenderInterface(
+ TypeSpec.Builder interfaceBuilder,
+ CrossProfileMethodInfo method,
+ CrossProfileTypeInfo crossProfileType) {
+
+ TypeName returnType;
+
+ if (!method.thrownExceptions().isEmpty()) {
+ // We don't add methods with exceptions to the multiplesender interface
+ return;
+ }
+
+ if (method.returnType().getKind().equals(TypeKind.VOID)) {
+ // void is a special case so we don't return a map
+ returnType = TypeName.VOID;
+ } else {
+ TypeName boxedMethodReturnType =
+ TypeName.get(generatorUtilities.boxIfNecessary(method.returnType()));
+ returnType =
+ ParameterizedTypeName.get(
+ ClassName.get(Map.class), PROFILE_CLASSNAME, boxedMethodReturnType);
+ }
+
+ CodeBlock methodReference = generateMethodReference(crossProfileType, method);
+
+ MethodSpec.Builder methodBuilder =
+ MethodSpec.methodBuilder(method.simpleName())
+ .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
+ .returns(returnType)
+ .addParameters(
+ GeneratorUtilities.extractParametersFromMethod(
+ crossProfileType.supportedTypes(),
+ method.methodElement(),
+ REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS))
+ .addJavadoc("Make a call to {@link $L} on the given profiles.\n\n", methodReference);
+
+ for (TypeMirror automaticallyResolvedType :
+ method.automaticallyResolvedParameterTypes(crossProfileType.supportedTypes())) {
+ methodBuilder.addJavadoc(
+ "<p>A {@link $T} will automatically be passed to {@link $L}.\n\n",
+ automaticallyResolvedType,
+ methodReference);
+ }
+
+ methodBuilder
+ .addJavadoc(
+ "<p>If any profiles are not connected, they will not be included in the"
+ + " results.\n\n")
+ .addJavadoc(
+ "<p>If an unchecked exception is thrown on any profile other than the current one,"
+ + " a {@link $T} will be thrown with the original exception as the cause.\n\n",
+ PROFILE_RUNTIME_EXCEPTION_CLASSNAME)
+ .addJavadoc("@see $L\n", methodReference);
+
+ interfaceBuilder.addMethod(methodBuilder.build());
+ }
+
+ private void generateCrossProfileCallbackMethodOnMultipleSenderInterface(
+ TypeSpec.Builder interfaceBuilder,
+ CrossProfileMethodInfo method,
+ CrossProfileTypeInfo crossProfileType) {
+
+ if (!method.isSimpleCrossProfileCallback(generatorContext)) {
+ // Non-simple callbacks can't be used with multiple profiles
+ return;
+ }
+
+ VariableElement callbackParameter = method.getCrossProfileCallbackParam(generatorContext).get();
+ TypeElement callbackType =
+ generatorContext.elements().getTypeElement(callbackParameter.asType().toString());
+ CrossProfileCallbackInterfaceInfo callbackInterface =
+ CrossProfileCallbackInterfaceInfo.create(callbackType);
+
+ List<ParameterSpec> parameters =
+ convertCallbackParametersIntoMulti(
+ GeneratorUtilities.extractParametersFromMethod(
+ crossProfileType.supportedTypes(),
+ method.methodElement(),
+ REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS),
+ callbackParameter,
+ callbackInterface);
+
+ CodeBlock methodReference = generateMethodReference(crossProfileType, method);
+
+ MethodSpec.Builder methodBuilder =
+ MethodSpec.methodBuilder(method.simpleName())
+ .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
+ .addParameters(parameters)
+ .addJavadoc("Make a call to {@link $L} on the given profiles.\n\n", methodReference);
+
+ for (TypeMirror automaticallyResolvedType :
+ method.automaticallyResolvedParameterTypes(crossProfileType.supportedTypes())) {
+ methodBuilder.addJavadoc(
+ "<p>A {@link $T} will automatically be passed to {@link $L}.\n\n",
+ automaticallyResolvedType,
+ methodReference);
+ }
+
+ methodBuilder
+ .addJavadoc(
+ "<p>If any profiles are not available, they will not be included in the"
+ + " results.\n\n")
+ .addJavadoc(
+ "<p>If an unchecked exception is thrown on any profile other than the current one,"
+ + " a {@link $T} will be thrown on another thread with the original exception"
+ + " as the cause.\n\n",
+ PROFILE_RUNTIME_EXCEPTION_CLASSNAME)
+ .addJavadoc("@see $L\n", methodReference);
+
+ interfaceBuilder.addMethod(methodBuilder.build());
+ }
+
+ private void generateFutureMethodOnMultipleSenderInterface(
+ TypeSpec.Builder interfaceBuilder,
+ CrossProfileMethodInfo method,
+ CrossProfileTypeInfo crossProfileType) {
+
+ ClassName rawFutureType = TypeUtils.getRawTypeClassName(method.returnType());
+ // We assume all Futures are generic with a single generic type
+ TypeMirror wrappedType = TypeUtils.extractTypeArguments(method.returnType()).iterator().next();
+
+ TypeMirror boxedWrappedType = generatorUtilities.boxIfNecessary(wrappedType);
+
+ TypeName mapType =
+ ParameterizedTypeName.get(
+ ClassName.get(Map.class), PROFILE_CLASSNAME, ClassName.get(boxedWrappedType));
+
+ ParameterizedTypeName returnType = ParameterizedTypeName.get(rawFutureType, mapType);
+
+ CodeBlock methodReference = generateMethodReference(crossProfileType, method);
+
+ MethodSpec.Builder methodBuilder =
+ MethodSpec.methodBuilder(method.simpleName())
+ .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
+ .returns(returnType)
+ .addParameters(
+ GeneratorUtilities.extractParametersFromMethod(
+ crossProfileType.supportedTypes(),
+ method.methodElement(),
+ REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS))
+ .addJavadoc("Make a call to {@link $L} on the given profiles.\n\n", methodReference);
+
+ for (TypeMirror automaticallyResolvedType :
+ method.automaticallyResolvedParameterTypes(crossProfileType.supportedTypes())) {
+ methodBuilder.addJavadoc(
+ "<p>A {@link $T} will automatically be passed to {@link $L}.\n\n",
+ automaticallyResolvedType,
+ methodReference);
+ }
+
+ methodBuilder
+ .addJavadoc(
+ "<p>If any profiles are not available, or if any profiles set the future with an"
+ + " {@link $T}, they will not be included in the results.\n\n",
+ Exception.class)
+ .addJavadoc(
+ "<p>If an unchecked exception is thrown on any profile other than the current one,"
+ + " a {@link $T} will be thrown on another thread with the original exception"
+ + " as the cause.\n\n",
+ PROFILE_RUNTIME_EXCEPTION_CLASSNAME)
+ .addJavadoc("@see $L\n", methodReference);
+
+ interfaceBuilder.addMethod(methodBuilder.build());
+ }
+
+ private List<ParameterSpec> convertCallbackParametersIntoMulti(
+ List<ParameterSpec> parameters,
+ VariableElement callbackParameter,
+ CrossProfileCallbackInterfaceInfo callbackInterface) {
+ return parameters.stream()
+ .map(
+ e ->
+ e.name.equals(callbackParameter.getSimpleName().toString())
+ ? convertCallbackToMulti(e, callbackInterface)
+ : e)
+ .collect(toList());
+ }
+
+ private ParameterSpec convertCallbackToMulti(
+ ParameterSpec parameter, CrossProfileCallbackInterfaceInfo callbackInterface) {
+ return ParameterSpec.builder(
+ CrossProfileCallbackCodeGenerator.getCrossProfileCallbackMultiInterfaceClassName(
+ generatorContext, callbackInterface),
+ parameter.name)
+ .addModifiers(parameter.modifiers)
+ .addAnnotations(parameter.annotations)
+ .build();
+ }
+
+ static ClassName getCrossProfileTypeInterfaceClassName(
+ GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) {
+ return crossProfileType.profileClassName();
+ }
+
+ static ClassName getSingleSenderInterfaceClassName(
+ GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) {
+ return GeneratorUtilities.appendToClassName(
+ crossProfileType.profileClassName(), "_SingleSender");
+ }
+
+ static ClassName getSingleSenderCanThrowInterfaceClassName(
+ GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) {
+ return GeneratorUtilities.appendToClassName(
+ crossProfileType.profileClassName(), "_SingleSenderCanThrow");
+ }
+
+ static ClassName getMultipleSenderInterfaceClassName(
+ GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) {
+ return GeneratorUtilities.appendToClassName(
+ crossProfileType.profileClassName(), "_MultipleSender");
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalCrossProfileClassGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalCrossProfileClassGenerator.java
new file mode 100644
index 0000000..f4aad75
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalCrossProfileClassGenerator.java
@@ -0,0 +1,421 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.BUNDLER_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CONTEXT_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CROSS_PROFILE_CALLBACK_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CROSS_PROFILE_FUTURE_RESULT_WRITER;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.METHOD_RUNNER_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_UTILITIES_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.stream.Collectors.joining;
+
+import com.google.android.enterprise.connectedapps.annotations.CrossProfile;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackInterfaceInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.FutureWrapper;
+import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext;
+import com.google.android.enterprise.connectedapps.processor.containers.ProviderClassInfo;
+import com.squareup.javapoet.ArrayTypeName;
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.CodeBlock;
+import com.squareup.javapoet.FieldSpec;
+import com.squareup.javapoet.MethodSpec;
+import com.squareup.javapoet.TypeName;
+import com.squareup.javapoet.TypeSpec;
+import java.util.Optional;
+import java.util.stream.IntStream;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+
+/**
+ * Generate the {@code Profile_*_Internal} class for a single {@link CrossProfile} type.
+ *
+ * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to
+ * validate that the annotated code is correct.
+ */
+final class InternalCrossProfileClassGenerator {
+
+ private boolean generated = false;
+ private final GeneratorContext generatorContext;
+ private final GeneratorUtilities generatorUtilities;
+ private final ProviderClassInfo providerClass;
+ private final CrossProfileTypeInfo crossProfileType;
+
+ InternalCrossProfileClassGenerator(
+ GeneratorContext generatorContext,
+ ProviderClassInfo providerClass,
+ CrossProfileTypeInfo crossProfileType) {
+ this.generatorContext = checkNotNull(generatorContext);
+ this.generatorUtilities = new GeneratorUtilities(generatorContext);
+ this.providerClass = checkNotNull(providerClass);
+ this.crossProfileType = checkNotNull(crossProfileType);
+ }
+
+ void generate() {
+ if (generated) {
+ throw new IllegalStateException(
+ "InternalCrossProfileClassGenerator#generate can only be called once");
+ }
+ generated = true;
+
+ generateInternalCrossProfileClass();
+ }
+
+ private void generateInternalCrossProfileClass() {
+ ClassName className = getInternalCrossProfileClassName(generatorContext, crossProfileType);
+
+ TypeSpec.Builder classBuilder =
+ TypeSpec.classBuilder(className).addModifiers(Modifier.PUBLIC, Modifier.FINAL);
+
+ classBuilder.addJavadoc(
+ "Internal class for {@link $T}.\n\n"
+ + "<p>This is used by the Connected Apps SDK to dispatch cross-profile calls.\n\n"
+ + "<p>Cross-profile type identifier: $L.\n",
+ crossProfileType.crossProfileTypeElement().asType(),
+ crossProfileType.identifier());
+
+ classBuilder.addField(
+ FieldSpec.builder(className, "instance")
+ .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
+ .initializer("new $T()", className)
+ .build());
+
+ classBuilder.addField(
+ FieldSpec.builder(BUNDLER_CLASSNAME, "bundler")
+ .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
+ .initializer(
+ "new $T()",
+ BundlerGenerator.getBundlerClassName(generatorContext, crossProfileType))
+ .build());
+
+ if (!crossProfileType.isStatic()) {
+ ExecutableElement providerMethod =
+ providerClass.findProviderMethodFor(generatorContext, crossProfileType);
+ String paramsString = providerMethod.getParameters().isEmpty() ? "()" : "(context)";
+ CodeBlock providerMethodCall =
+ CodeBlock.of("$L$L", providerMethod.getSimpleName(), paramsString);
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("crossProfileType")
+ .addParameter(CONTEXT_CLASSNAME, "context")
+ .returns(crossProfileType.className())
+ .addStatement(
+ "return $T.instance().providerClass(context).$L",
+ InternalProviderClassGenerator.getInternalProviderClassName(
+ generatorContext, providerClass),
+ providerMethodCall)
+ .build());
+ }
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("bundler")
+ .returns(BUNDLER_CLASSNAME)
+ .addStatement("return bundler")
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("instance")
+ .addModifiers(Modifier.STATIC)
+ .returns(className)
+ .addStatement("return instance")
+ .build());
+
+ classBuilder.addMethod(MethodSpec.constructorBuilder().addModifiers(Modifier.PRIVATE).build());
+
+ addMethodsField(classBuilder, crossProfileType);
+ addCrossProfileTypeMethods(classBuilder, crossProfileType);
+ addCallMethod(classBuilder);
+
+ generatorUtilities.writeClassToFile(className.packageName(), classBuilder);
+ }
+
+ private void addMethodsField(
+ TypeSpec.Builder classBuilder, CrossProfileTypeInfo crossProfileType) {
+ int totalMethods = crossProfileType.crossProfileMethods().size();
+
+ classBuilder.addField(
+ FieldSpec.builder(ArrayTypeName.of(METHOD_RUNNER_CLASSNAME), "methods")
+ .addModifiers(Modifier.PRIVATE)
+ .initializer(
+ "new $T[]{$L}",
+ METHOD_RUNNER_CLASSNAME,
+ IntStream.range(0, totalMethods)
+ .mapToObj(n -> "this::method" + n)
+ .collect(joining(",")))
+ .build());
+ }
+
+ private void addCrossProfileTypeMethods(
+ TypeSpec.Builder classBuilder, CrossProfileTypeInfo crossProfileType) {
+ for (CrossProfileMethodInfo method : crossProfileType.crossProfileMethods()) {
+ if (method.isBlocking(generatorContext, crossProfileType)) {
+ addBlockingCrossProfileTypeMethod(classBuilder, method);
+ } else if (method.isCrossProfileCallback(generatorContext)) {
+ addCrossProfileCallbackCrossProfileTypeMethod(classBuilder, method);
+ } else if (method.isFuture(crossProfileType)) {
+ addFutureCrossProfileTypeMethod(classBuilder, method);
+ } else {
+ throw new IllegalStateException("Unknown method type: " + method);
+ }
+ }
+ }
+
+ private void addBlockingCrossProfileTypeMethod(
+ TypeSpec.Builder classBuilder, CrossProfileMethodInfo method) {
+ CodeBlock.Builder methodCode = CodeBlock.builder();
+
+ // parcle is recycled by caller
+ methodCode.addStatement("$1T returnParcel = $1T.obtain()", PARCEL_CLASSNAME);
+
+ addExtractParametersCode(methodCode, method);
+
+ CodeBlock methodCall =
+ CodeBlock.of(
+ "$L.$L($L)",
+ getCrossProfileTypeReference(method),
+ method.simpleName(),
+ method.commaSeparatedParameters(
+ crossProfileType.supportedTypes(), REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS));
+
+ if (!method.thrownExceptions().isEmpty()) {
+ methodCode.beginControlFlow("try");
+ }
+
+ if (isPrimitiveOrObjectVoid(method.returnType())) {
+ methodCode.addStatement(methodCall);
+ } else {
+ methodCall = CodeBlock.of("$T returnValue = $L", method.returnType(), methodCall);
+ methodCode.addStatement(methodCall);
+ methodCode.add("returnParcel.writeInt(0); // No errors\n");
+ methodCode.addStatement(
+ "bundler.writeToParcel(returnParcel, returnValue, $L, /* flags= */ 0)",
+ TypeUtils.generateBundlerType(method.returnType()));
+ }
+
+ if (!method.thrownExceptions().isEmpty()) {
+ for (TypeName exceptionType : method.thrownExceptions()) {
+ methodCode.nextControlFlow("catch ($L e)", exceptionType);
+ methodCode.add("returnParcel.writeInt(1); // Errors\n");
+ methodCode.addStatement(
+ "$T.writeThrowableToParcel(returnParcel, e)", PARCEL_UTILITIES_CLASSNAME);
+ }
+ methodCode.endControlFlow();
+ }
+
+ methodCode.addStatement("return returnParcel");
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("method" + method.identifier())
+ .addModifiers(Modifier.PRIVATE)
+ .returns(PARCEL_CLASSNAME)
+ .addParameter(CONTEXT_CLASSNAME, "context")
+ .addParameter(PARCEL_CLASSNAME, "params")
+ .addParameter(CROSS_PROFILE_CALLBACK_CLASSNAME, "callback")
+ .addCode(methodCode.build())
+ .addJavadoc(
+ "Call $1L and return a {@link $2T} containing the return value.\n\n"
+ + "<p>The {@link $2T} must be recycled after use.\n",
+ GeneratorUtilities.methodJavadocReference(method.methodElement()),
+ PARCEL_CLASSNAME)
+ .build());
+ }
+
+ private void addCrossProfileCallbackCrossProfileTypeMethod(
+ TypeSpec.Builder classBuilder, CrossProfileMethodInfo method) {
+ CodeBlock.Builder methodCode = CodeBlock.builder();
+
+ // parcel is recycled by caller
+ methodCode.addStatement("$1T returnParcel = $1T.obtain()", PARCEL_CLASSNAME);
+
+ addExtractParametersCode(methodCode, method);
+
+ createCrossProfileCallbackParameter(methodCode, method);
+
+ CodeBlock methodCall =
+ CodeBlock.of(
+ "$L.$L($L)",
+ getCrossProfileTypeReference(method),
+ method.simpleName(),
+ method.commaSeparatedParameters(
+ crossProfileType.supportedTypes(), REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS));
+
+ if (isPrimitiveOrObjectVoid(method.returnType())) {
+ methodCode.addStatement(methodCall);
+ } else {
+ methodCall = CodeBlock.of("$T returnValue = $L", method.returnType(), methodCall);
+ methodCode.addStatement(methodCall);
+ methodCode.add("returnParcel.writeInt(0); // No errors\n");
+ methodCode.addStatement(
+ "bundler.writeToParcel(returnParcel, returnValue, $L, /* flags= */ 0)",
+ TypeUtils.generateBundlerType(method.returnType()));
+ }
+
+ methodCode.addStatement("return returnParcel");
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("method" + method.identifier())
+ .addModifiers(Modifier.PRIVATE)
+ .returns(PARCEL_CLASSNAME)
+ .addParameter(CONTEXT_CLASSNAME, "context")
+ .addParameter(PARCEL_CLASSNAME, "params")
+ // TODO: This should be renamed to "callback" once we prefix unpacked parameter names
+ // (without doing this, a param named "callback" will cause a compile error)
+ .addParameter(CROSS_PROFILE_CALLBACK_CLASSNAME, "crossProfileCallback")
+ .addCode(methodCode.build())
+ .addJavadoc(
+ "Call $1L, and link the callback to {@code crossProfileCallback}.\n\n"
+ + "@return An empty parcel. This must be recycled after use.\n",
+ GeneratorUtilities.methodJavadocReference(method.methodElement()))
+ .build());
+ }
+
+ private void addFutureCrossProfileTypeMethod(
+ TypeSpec.Builder classBuilder, CrossProfileMethodInfo method) {
+ CodeBlock.Builder methodCode = CodeBlock.builder();
+
+ // parcel is recycled by caller
+ methodCode.addStatement("$1T returnParcel = $1T.obtain()", PARCEL_CLASSNAME);
+
+ addExtractParametersCode(methodCode, method);
+
+ CodeBlock methodCall =
+ CodeBlock.of(
+ "$L.$L($L)",
+ getCrossProfileTypeReference(method),
+ method.simpleName(),
+ method.commaSeparatedParameters(
+ crossProfileType.supportedTypes(), REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS));
+
+ methodCode.addStatement("$T future = $L", method.returnType(), methodCall);
+
+ TypeMirror rawFutureType = TypeUtils.removeTypeArguments(method.returnType());
+
+ FutureWrapper futureWrapper =
+ crossProfileType.supportedTypes().getType(rawFutureType).getFutureWrapper().get();
+ // This assumes every Future is generic with one type argument
+ TypeMirror wrappedReturnType =
+ TypeUtils.extractTypeArguments(method.returnType()).iterator().next();
+ methodCode.addStatement(
+ "$T.writeFutureResult(future, new $T<>(callback, bundler, $L))",
+ futureWrapper.wrapperClassName(),
+ CROSS_PROFILE_FUTURE_RESULT_WRITER,
+ TypeUtils.generateBundlerType(wrappedReturnType));
+
+ // TODO: Can this just return null? where does it go? that'd avoid having to obtain/recycle
+ methodCode.addStatement("return returnParcel");
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("method" + method.identifier())
+ .addModifiers(Modifier.PRIVATE)
+ .returns(PARCEL_CLASSNAME)
+ .addParameter(CONTEXT_CLASSNAME, "context")
+ .addParameter(PARCEL_CLASSNAME, "params")
+ .addParameter(CROSS_PROFILE_CALLBACK_CLASSNAME, "callback")
+ .addCode(methodCode.build())
+ .addJavadoc(
+ "Call $1L, and link the returned future to {@code crossProfileCallback}.\n\n"
+ + "@return An empty parcel. This must be recycled after use.\n",
+ GeneratorUtilities.methodJavadocReference(method.methodElement()))
+ .build());
+ }
+
+ private void createCrossProfileCallbackParameter(
+ CodeBlock.Builder methodCode, CrossProfileMethodInfo method) {
+ VariableElement asyncCallbackParam =
+ method.getCrossProfileCallbackParam(generatorContext).get();
+
+ TypeElement callbackType =
+ generatorContext.elements().getTypeElement(asyncCallbackParam.asType().toString());
+ CrossProfileCallbackInterfaceInfo callbackInterface =
+ CrossProfileCallbackInterfaceInfo.create(callbackType);
+
+ methodCode.addStatement(
+ "$T $L = new $L(crossProfileCallback, bundler)",
+ asyncCallbackParam.asType(),
+ asyncCallbackParam.getSimpleName(),
+ CrossProfileCallbackCodeGenerator.getCrossProfileCallbackReceiverClassName(
+ generatorContext, callbackInterface));
+ }
+
+ private static boolean isPrimitiveOrObjectVoid(TypeMirror typeMirror) {
+ return typeMirror.getKind().equals(TypeKind.VOID)
+ || typeMirror.toString().equals("java.lang.Void");
+ }
+
+ private void addExtractParametersCode(CodeBlock.Builder code, CrossProfileMethodInfo method) {
+ Optional<VariableElement> callbackParameter =
+ method.getCrossProfileCallbackParam(generatorContext);
+ for (VariableElement parameter : method.methodElement().getParameters()) {
+ if (callbackParameter.isPresent()
+ && callbackParameter.get().getSimpleName().equals(parameter.getSimpleName())) {
+ continue; // Don't extract a callback parameter
+ }
+ if (crossProfileType.supportedTypes().isAutomaticallyResolved(parameter.asType())) {
+ continue;
+ }
+ code.addStatement(
+ "@SuppressWarnings(\"unchecked\") $1T $2L = ($1T) bundler.readFromParcel(params, $3L)",
+ parameter.asType(),
+ parameter.getSimpleName().toString(),
+ TypeUtils.generateBundlerType(parameter.asType()));
+ }
+ }
+
+ private static void addCallMethod(TypeSpec.Builder classBuilder) {
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("call")
+ .addModifiers(Modifier.PUBLIC)
+ .returns(PARCEL_CLASSNAME)
+ .addParameter(CONTEXT_CLASSNAME, "context")
+ .addParameter(int.class, "methodIdentifier")
+ .addParameter(PARCEL_CLASSNAME, "params")
+ .addParameter(CROSS_PROFILE_CALLBACK_CLASSNAME, "callback")
+ .beginControlFlow("if (methodIdentifier >= methods.length)")
+ .addStatement(
+ "throw new $T(\"Invalid method identifier\" + methodIdentifier)",
+ IllegalArgumentException.class)
+ .endControlFlow()
+ .addStatement("return methods[methodIdentifier].call(context, params, callback)")
+ .addJavadoc(
+ "Call the method referenced by {@code methodIdentifier}.\n\n"
+ + "<p>If the method is synchronous, this will return a {@link $1T} containing"
+ + " the return value, otherwise it will return an empty {@link $1T}. The"
+ + " {@link $1T} must be recycled after use.\n",
+ PARCEL_CLASSNAME)
+ .build());
+ }
+
+ private CodeBlock getCrossProfileTypeReference(CrossProfileMethodInfo method) {
+ if (method.isStatic()) {
+ return CodeBlock.of("$1T", crossProfileType.className());
+ }
+ return CodeBlock.of("crossProfileType(context)");
+ }
+
+ static ClassName getInternalCrossProfileClassName(
+ GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) {
+ return GeneratorUtilities.appendToClassName(crossProfileType.profileClassName(), "_Internal");
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalProviderClassGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalProviderClassGenerator.java
new file mode 100644
index 0000000..c02bbfb
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalProviderClassGenerator.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CONTEXT_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CROSS_PROFILE_CALLBACK_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_CLASSNAME;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext;
+import com.google.android.enterprise.connectedapps.processor.containers.ProviderClassInfo;
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.CodeBlock;
+import com.squareup.javapoet.FieldSpec;
+import com.squareup.javapoet.MethodSpec;
+import com.squareup.javapoet.TypeSpec;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.PackageElement;
+
+/**
+ * Generate the {@code Profile_*_Internal} class for a single provider class.
+ *
+ * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to
+ * validate that the annotated code is correct.
+ */
+final class InternalProviderClassGenerator {
+
+ private boolean generated = false;
+ private final GeneratorContext generatorContext;
+ private final GeneratorUtilities generatorUtilities;
+ private final ProviderClassInfo providerClass;
+
+ InternalProviderClassGenerator(
+ GeneratorContext generatorContext, ProviderClassInfo providerClass) {
+ this.generatorContext = checkNotNull(generatorContext);
+ this.generatorUtilities = new GeneratorUtilities(generatorContext);
+ this.providerClass = checkNotNull(providerClass);
+ }
+
+ void generate() {
+ if (generated) {
+ throw new IllegalStateException(
+ "InternalProviderClassGenerator#generate can only be called once");
+ }
+ generated = true;
+
+ generateInternalProviderClassClass();
+ }
+
+ private void generateInternalProviderClassClass() {
+ ClassName className = getInternalProviderClassName(generatorContext, providerClass);
+
+ TypeSpec.Builder classBuilder =
+ TypeSpec.classBuilder(className).addModifiers(Modifier.PUBLIC, Modifier.FINAL);
+
+ classBuilder.addJavadoc(
+ "Internal provider class for $L\n",
+ providerClass.providerClassElement().asType().toString());
+
+ classBuilder.addField(
+ FieldSpec.builder(className, "instance")
+ .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
+ .initializer("new $T()", className)
+ .build());
+
+ classBuilder.addField(
+ FieldSpec.builder(providerClass.className(), "providerClass")
+ .addModifiers(Modifier.PRIVATE)
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("instance")
+ .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
+ .returns(className)
+ .addStatement("return instance")
+ .addJavadoc("Get the singleton instance of this provider.\n")
+ .build());
+
+ String providerClassConstructorParameters =
+ providerClass.publicConstructorArgumentTypes().isEmpty()
+ ? ""
+ : "context"; // We only allow a context or no-args
+
+ classBuilder.addMethod(
+ MethodSpec.constructorBuilder()
+ .addModifiers(Modifier.PRIVATE)
+
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("providerClass")
+ .addJavadoc(
+ "Get a singleton instance of the original class which caused generation of this"
+ + " class.\n")
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(CONTEXT_CLASSNAME, "context")
+ .returns(providerClass.className())
+ .addComment(
+ "This can't be constructed with the field declaration as sometimes these classes"
+ + " need arguments")
+ .beginControlFlow("if (providerClass == null)")
+ .beginControlFlow(
+ "synchronized ($T.class)",
+ getInternalProviderClassName(generatorContext, providerClass))
+ .beginControlFlow("if (providerClass == null)")
+ .addStatement(
+ "this.providerClass = new $T($L)",
+ providerClass.className(),
+ providerClassConstructorParameters)
+ .endControlFlow()
+ .endControlFlow()
+ .endControlFlow()
+ .addStatement("return providerClass")
+ .build());
+
+ addCallMethod(classBuilder);
+
+ generatorUtilities.writeClassToFile(className.packageName(), classBuilder);
+ }
+
+ private void addCallMethod(TypeSpec.Builder classBuilder) {
+ CodeBlock.Builder methodCode = CodeBlock.builder();
+
+ for (CrossProfileTypeInfo crossProfileType : providerClass.allCrossProfileTypes()) {
+ addCrossProfileTypeDispatcher(methodCode, crossProfileType);
+ }
+
+ methodCode.addStatement(
+ "throw new $T(\"Unknown type identifier \" + crossProfileTypeIdentifier)",
+ IllegalArgumentException.class);
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("call")
+ .addModifiers(Modifier.PUBLIC)
+ .returns(PARCEL_CLASSNAME)
+ .addParameter(CONTEXT_CLASSNAME, "context")
+ .addParameter(long.class, "crossProfileTypeIdentifier")
+ .addParameter(int.class, "methodIdentifier")
+ .addParameter(PARCEL_CLASSNAME, "params")
+ .addParameter(CROSS_PROFILE_CALLBACK_CLASSNAME, "callback")
+ .addCode(methodCode.build())
+ .addJavadoc(
+ "Call the {@code call} method on the internal type referenced by the {@code"
+ + " crossProfileTypeIdentifier}.\n\n"
+ + "@return A {@link $1T} which contains the return value (if a synchronous"
+ + " call) or is empty\n (if asynchronous). This {@link $1T} must be recycled"
+ + " after use.\n",
+ PARCEL_CLASSNAME)
+ .build());
+ }
+
+ private void addCrossProfileTypeDispatcher(
+ CodeBlock.Builder methodCode, CrossProfileTypeInfo crossProfileType) {
+ methodCode.beginControlFlow(
+ "if (crossProfileTypeIdentifier == $LL)", crossProfileType.identifier());
+ methodCode.addStatement(
+ "return $T.instance().call(context, methodIdentifier, params, callback)",
+ InternalCrossProfileClassGenerator.getInternalCrossProfileClassName(
+ generatorContext, crossProfileType));
+ methodCode.endControlFlow();
+ }
+
+ static ClassName getInternalProviderClassName(
+ GeneratorContext generatorContext, ProviderClassInfo providerClass) {
+ PackageElement originalPackage =
+ generatorContext.elements().getPackageOf(providerClass.providerClassElement());
+ String internalProviderClassName =
+ String.format("Profile_%s_Internal", providerClass.simpleName());
+
+ return ClassName.get(originalPackage.getQualifiedName().toString(), internalProviderClassName);
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/LateValidator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/LateValidator.java
new file mode 100644
index 0000000..4b55483
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/LateValidator.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.validationMessageFormatterFor;
+import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.validationMessageFormatterForClass;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.stream.Collectors.groupingBy;
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext;
+import com.google.android.enterprise.connectedapps.processor.containers.ProfileConnectorInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.ProviderClassInfo;
+import com.google.common.collect.Streams;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Stream;
+import javax.lang.model.element.Element;
+import javax.lang.model.type.TypeMirror;
+import javax.tools.Diagnostic.Kind;
+
+/** Validator to check that annotations have been used correctly before generating code. */
+final class LateValidator {
+
+ private final GeneratorContext generatorContext;
+
+ private static final String PROVIDER_CLASS_DIFFERENT_CONNECTOR_ERROR =
+ "All @CROSS_PROFILE_ANNOTATION types provided by a provider class must use the same"
+ + " ProfileConnector";
+ private static final String CONFIGURATION_DIFFERENT_CONNECTOR_ERROR =
+ "All @CROSS_PROFILE_ANNOTATION types specified in the same configuration must use the same"
+ + " ProfileConnector";
+ private static final String INCORRECT_SERVICE_CLASS =
+ "The class specified by serviceClass must match the serviceClassName given by the"
+ + " ProfileConnector";
+ private static final String STATICTYPES_ERROR =
+ "@CROSS_PROFILE_ANNOTATION classes referenced in @CROSS_PROFILE_PROVIDER_ANNOTATION"
+ + " staticTypes annotations must not have non-static @CROSS_PROFILE_ANNOTATION annotated"
+ + " methods";
+
+ LateValidator(GeneratorContext generatorContext) {
+ this.generatorContext = checkNotNull(generatorContext);
+ }
+
+ /**
+ * Validate code.
+ *
+ * <p>This will show errors for all issues found. It will not terminate upon finding the first
+ * error.
+ *
+ * @return True if the state is valid
+ */
+ boolean validate() {
+ return Stream.of(
+ validateProviderClasses(generatorContext.providers()),
+ validateConfigurations(generatorContext.configurations()))
+ .allMatch(b -> b);
+ }
+
+ private boolean validateProviderClasses(Collection<ProviderClassInfo> providerClasses) {
+ boolean isValid = true;
+
+ for (ProviderClassInfo providerClass : providerClasses) {
+ isValid = validateProviderClass(providerClass) && isValid;
+ }
+
+ return isValid;
+ }
+
+ private boolean validateProviderClass(ProviderClassInfo providerClass) {
+ boolean isValid = true;
+
+ if (getConnectorQualifiedNamesUsedInProviderClass(providerClass).size() > 1) {
+ showError(
+ PROVIDER_CLASS_DIFFERENT_CONNECTOR_ERROR,
+ providerClass.providerClassElement(),
+ validationMessageFormatterForClass(providerClass.providerClassElement()));
+ isValid = false;
+ }
+
+ for (CrossProfileTypeInfo crossProfileType : providerClass.staticTypes()) {
+ if (!crossProfileType.isStatic()) {
+ showError(STATICTYPES_ERROR, providerClass.providerClassElement());
+ isValid = false;
+ }
+ }
+
+ return isValid;
+ }
+
+ private boolean validateConfigurations(Collection<CrossProfileConfigurationInfo> configurations) {
+ boolean isValid = true;
+
+ for (CrossProfileConfigurationInfo configuration : configurations) {
+ isValid = validateConfiguration(configuration) && isValid;
+ }
+
+ return isValid;
+ }
+
+ private boolean validateConfiguration(CrossProfileConfigurationInfo configuration) {
+ boolean isValid = true;
+
+ isValid =
+ validateCrossProfileTypesHaveUniqueIdentifiers(
+ configuration.providers().stream()
+ .flatMap(m -> m.allCrossProfileTypes().stream())
+ .collect(toList()))
+ && isValid;
+
+ if (getConnectorQualifiedNamesUsedInConfiguration(configuration).size() > 1) {
+ showError(CONFIGURATION_DIFFERENT_CONNECTOR_ERROR, configuration.configurationElement());
+ isValid = false;
+ }
+
+ if (configuration.serviceClass().isPresent()
+ && !configuration
+ .serviceClass()
+ .get()
+ .toString()
+ .equals(configuration.profileConnector().serviceName().toString())) {
+ showError(INCORRECT_SERVICE_CLASS, configuration.configurationElement());
+ isValid = false;
+ }
+
+ return isValid;
+ }
+
+ private static Collection<String> getConnectorQualifiedNamesUsedInConfiguration(
+ CrossProfileConfigurationInfo configuration) {
+ Set<String> connectorQualifiedNames =
+ configuration.providers().stream()
+ .flatMap(m -> getConnectorQualifiedNamesUsedInProviderClass(m).stream())
+ .collect(toSet());
+ connectorQualifiedNames.add(
+ configuration.profileConnector().connectorElement().asType().toString());
+ return connectorQualifiedNames;
+ }
+
+ private boolean validateCrossProfileTypesHaveUniqueIdentifiers(
+ Collection<CrossProfileTypeInfo> crossProfileTypes) {
+ boolean isValid = true;
+ Map<Long, List<CrossProfileTypeInfo>> crossProfileTypeByIdentifier =
+ crossProfileTypes.stream().collect(groupingBy(CrossProfileTypeInfo::identifier));
+
+ for (long identifier : crossProfileTypeByIdentifier.keySet()) {
+ if (crossProfileTypeByIdentifier.get(identifier).size() > 1) {
+ isValid = false;
+ String crossProfileTypesString =
+ crossProfileTypeByIdentifier.get(identifier).stream()
+ .map(m -> m.className().toString())
+ .collect(joining(","));
+ showError(
+ "The following cross-profile types all share an identifier("
+ + identifier
+ + "): "
+ + crossProfileTypesString,
+ crossProfileTypeByIdentifier.get(identifier).get(0).crossProfileTypeElement());
+ }
+ }
+
+ return isValid;
+ }
+
+ private static Collection<String> getConnectorQualifiedNamesUsedInProviderClass(
+ ProviderClassInfo providerClass) {
+ return providerClass.allCrossProfileTypes().stream()
+ .map(CrossProfileTypeInfo::profileConnector)
+ .flatMap(Streams::stream)
+ .map(ProfileConnectorInfo::connectorElement)
+ .map(Element::asType)
+ .map(TypeMirror::toString)
+ .collect(toSet());
+ }
+
+ private void showError(
+ String errorText,
+ Element errorElement,
+ ValidationMessageFormatter validationMessageFormatter) {
+ showErrorPreformatted(validationMessageFormatter.format(errorText), errorElement);
+ }
+
+ private void showError(String errorText, Element errorElement) {
+ showErrorPreformatted(
+ validationMessageFormatterFor(errorElement).format(errorText), errorElement);
+ }
+
+ private void showErrorPreformatted(String errorText, Element errorElement) {
+ generatorContext
+ .processingEnv()
+ .getMessager()
+ .printMessage(Kind.ERROR, errorText, errorElement);
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/MultipleProfilesGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/MultipleProfilesGenerator.java
new file mode 100644
index 0000000..38b8038
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/MultipleProfilesGenerator.java
@@ -0,0 +1,392 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.ASYNC_CALLBACK_PARAM_MULTIMERGER_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CALLBACK_MERGER_EXCEPTION_CALLBACK_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.stream.Collectors.toList;
+
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackInterfaceInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.FutureWrapper;
+import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext;
+import com.squareup.javapoet.AnnotationSpec;
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.CodeBlock;
+import com.squareup.javapoet.MethodSpec;
+import com.squareup.javapoet.ParameterSpec;
+import com.squareup.javapoet.ParameterizedTypeName;
+import com.squareup.javapoet.TypeName;
+import com.squareup.javapoet.TypeSpec;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.PrimitiveType;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+
+/**
+ * Generate the {@code Profile_*_MultipleProfiles} class for a single cross-profile type.
+ *
+ * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to
+ * validate that the annotated code is correct.
+ */
+final class MultipleProfilesGenerator {
+
+ private boolean generated = false;
+ private final GeneratorContext generatorContext;
+ private final GeneratorUtilities generatorUtilities;
+ private final CrossProfileTypeInfo crossProfileType;
+
+ MultipleProfilesGenerator(
+ GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) {
+ this.generatorContext = checkNotNull(generatorContext);
+ this.generatorUtilities = new GeneratorUtilities(generatorContext);
+ this.crossProfileType = checkNotNull(crossProfileType);
+ }
+
+ void generate() {
+ if (generated) {
+ throw new IllegalStateException("MultipleProfilesGenerator#generate can only be called once");
+ }
+ generated = true;
+
+ generateMultipleProfilesClass();
+ }
+
+ private void generateMultipleProfilesClass() {
+ ClassName className = getMultipleProfilesClassName(generatorContext, crossProfileType);
+
+ ClassName multipleSenderInterface =
+ InterfaceGenerator.getMultipleSenderInterfaceClassName(generatorContext, crossProfileType);
+ ClassName singleSenderCanThrowInterfaceName =
+ InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName(
+ generatorContext, crossProfileType);
+
+ TypeSpec.Builder classBuilder =
+ TypeSpec.classBuilder(className)
+ .addJavadoc(
+ "Default implementation of {@link $T}.\n\n"
+ + "<p>Wraps a number of {@link $T} instances and merges their return"
+ + " values.\n",
+ multipleSenderInterface,
+ singleSenderCanThrowInterfaceName)
+ .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
+ .addSuperinterface(multipleSenderInterface);
+
+ ParameterizedTypeName senderMapType =
+ ParameterizedTypeName.get(
+ ClassName.get(Map.class), PROFILE_CLASSNAME, singleSenderCanThrowInterfaceName);
+
+ classBuilder.addField(senderMapType, "senders", Modifier.PRIVATE, Modifier.FINAL);
+
+ classBuilder.addMethod(
+ MethodSpec.constructorBuilder()
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(senderMapType, "senders")
+ .addStatement("this.senders = senders")
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("timeout")
+ .addAnnotation(Override.class)
+ .addAnnotation(
+ AnnotationSpec.builder(SuppressWarnings.class)
+ .addMember("value", "$S", "GoodTime")
+ .build())
+ .addModifiers(Modifier.PUBLIC)
+ .returns(className)
+ .addParameter(long.class, "timeout")
+ .beginControlFlow("for ($T senderProfile : senders.keySet())", PROFILE_CLASSNAME)
+ .addStatement("senders.put(senderProfile, senders.get(senderProfile).timeout(timeout))")
+ .endControlFlow()
+ .addStatement("return this")
+ .build());
+
+ for (CrossProfileMethodInfo method : crossProfileType.crossProfileMethods()) {
+ if (method.isBlocking(generatorContext, crossProfileType)) {
+ generateBlockingMethodOnMultipleProfilesClass(classBuilder, method, crossProfileType);
+ } else if (method.isCrossProfileCallback(generatorContext)) {
+ generateCrossProfileCallbackMethodOnMultipleProfilesClass(
+ classBuilder, method, crossProfileType);
+ } else if (method.isFuture(crossProfileType)) {
+ generateFutureMethodOnMultipleProfilesClass(classBuilder, method, crossProfileType);
+ } else {
+ throw new IllegalStateException("Unknown method type: " + method);
+ }
+ }
+
+ generatorUtilities.writeClassToFile(className.packageName(), classBuilder);
+ }
+
+ private void generateBlockingMethodOnMultipleProfilesClass(
+ TypeSpec.Builder classBuilder,
+ CrossProfileMethodInfo method,
+ CrossProfileTypeInfo crossProfileType) {
+
+ if (!method.thrownExceptions().isEmpty()) {
+ // We don't add methods with exceptions to multiple profiles
+ return;
+ }
+
+ TypeName returnType;
+ if (method.returnType().getKind().equals(TypeKind.VOID)) {
+ // void is a special case so we don't return a map
+ returnType = TypeName.VOID;
+ } else {
+ TypeName boxedMethodReturnType =
+ TypeName.get(generatorUtilities.boxIfNecessary(method.returnType()));
+ returnType =
+ ParameterizedTypeName.get(
+ ClassName.get(Map.class), PROFILE_CLASSNAME, boxedMethodReturnType);
+ }
+
+ String methodName = method.simpleName();
+ String params =
+ method.commaSeparatedParameters(
+ crossProfileType.supportedTypes(), REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS);
+
+ CodeBlock methodCall = CodeBlock.of("$L($L)", methodName, params);
+
+ MethodSpec.Builder methodBuilder =
+ MethodSpec.methodBuilder(methodName)
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(returnType)
+ .addParameters(
+ GeneratorUtilities.extractParametersFromMethod(
+ crossProfileType.supportedTypes(),
+ method.methodElement(),
+ REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS));
+
+ if (method.returnType().getKind().equals(TypeKind.VOID)) {
+ methodBuilder.beginControlFlow(
+ "for ($T sender : senders.values())",
+ InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName(
+ generatorContext, crossProfileType));
+ methodBuilder.beginControlFlow("try");
+ methodBuilder.addStatement("sender.$L", methodCall);
+ methodBuilder.nextControlFlow("catch ($T e)", UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME);
+ methodBuilder.addComment(
+ "If the profile is not available we just don't include it in results");
+ methodBuilder.endControlFlow();
+ methodBuilder.endControlFlow();
+ } else {
+ methodBuilder.addStatement("$T returnValue = new $T<>()", returnType, HashMap.class);
+ methodBuilder.beginControlFlow(
+ "for ($T senderIdentifier : senders.keySet())", PROFILE_CLASSNAME);
+ methodBuilder.addStatement(
+ "$T sender = senders.get(senderIdentifier)",
+ InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName(
+ generatorContext, crossProfileType));
+ methodBuilder.beginControlFlow("try");
+ methodBuilder.addStatement("returnValue.put(senderIdentifier, sender.$L)", methodCall);
+ methodBuilder.nextControlFlow("catch ($T e)", UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME);
+ methodBuilder.addComment(
+ "If the profile is not available we just don't include it in results");
+ methodBuilder.endControlFlow();
+ methodBuilder.endControlFlow();
+ methodBuilder.addStatement("return returnValue");
+ }
+
+ classBuilder.addMethod(methodBuilder.build());
+ }
+
+ private void generateCrossProfileCallbackMethodOnMultipleProfilesClass(
+ TypeSpec.Builder classBuilder,
+ CrossProfileMethodInfo method,
+ CrossProfileTypeInfo crossProfileType) {
+
+ if (!method.isSimpleCrossProfileCallback(generatorContext)) {
+ // Non-simple callbacks can't be used with multiple profiles
+ return;
+ }
+
+ String methodName = method.simpleName();
+
+ VariableElement callbackParameter = method.getCrossProfileCallbackParam(generatorContext).get();
+ TypeElement callbackType =
+ generatorContext.elements().getTypeElement(callbackParameter.asType().toString());
+ CrossProfileCallbackInterfaceInfo callbackInterface =
+ CrossProfileCallbackInterfaceInfo.create(callbackType);
+
+ List<ParameterSpec> parameters =
+ convertCallbackParametersIntoMulti(
+ GeneratorUtilities.extractParametersFromMethod(
+ crossProfileType.supportedTypes(),
+ method.methodElement(),
+ REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS),
+ callbackParameter,
+ callbackInterface);
+
+ TypeMirror paramType =
+ callbackInterface.methods().get(0).getParameters().isEmpty()
+ ? generatorContext.elements().getTypeElement("java.lang.Void").asType()
+ : callbackInterface.methods().get(0).getParameters().get(0).asType();
+
+ if (paramType.getKind().isPrimitive()) {
+ PrimitiveType primitiveType = (PrimitiveType) paramType;
+ paramType = generatorContext.types().boxedClass(primitiveType).asType();
+ }
+
+ MethodSpec.Builder methodBuilder =
+ MethodSpec.methodBuilder(methodName)
+ .addModifiers(Modifier.PUBLIC)
+ .addParameters(parameters)
+ .addStatement(
+ "$1T mergedResultListener = new $1T($2L)",
+ CrossProfileCallbackCodeGenerator.getCrossProfileCallbackMultiMergerResultClassName(
+ generatorContext, callbackInterface),
+ callbackParameter.getSimpleName())
+ .addStatement(
+ "$1T<$2T> merger = new $1T<>(senders.size(), mergedResultListener)",
+ ASYNC_CALLBACK_PARAM_MULTIMERGER_CLASSNAME,
+ paramType);
+
+ String params =
+ method.commaSeparatedParameters(
+ crossProfileType.supportedTypes(),
+ REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS,
+ (p) ->
+ callbackParameter.getSimpleName().contentEquals(p)
+ ? generateMergerInputConstructor(callbackInterface)
+ : p);
+
+ methodBuilder.beginControlFlow(
+ "for ($T senderIdentifier : senders.keySet())", PROFILE_CLASSNAME);
+ methodBuilder.addStatement(
+ "$T sender = senders.get(senderIdentifier)",
+ InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName(
+ generatorContext, crossProfileType));
+
+ methodBuilder.addStatement(
+ "sender.$L($L, new $T<$T>(senderIdentifier, merger))",
+ methodName,
+ params,
+ CALLBACK_MERGER_EXCEPTION_CALLBACK_CLASSNAME,
+ paramType);
+ methodBuilder.endControlFlow();
+
+ classBuilder.addMethod(methodBuilder.build());
+ }
+
+ private void generateFutureMethodOnMultipleProfilesClass(
+ TypeSpec.Builder classBuilder,
+ CrossProfileMethodInfo method,
+ CrossProfileTypeInfo crossProfileType) {
+
+ ClassName rawFutureType = TypeUtils.getRawTypeClassName(method.returnType());
+ FutureWrapper futureWrapper =
+ crossProfileType
+ .supportedTypes()
+ .getType(TypeUtils.removeTypeArguments(method.returnType()))
+ .getFutureWrapper()
+ .get();
+ // We assume all Futures are generic with a single generic type
+ TypeMirror wrappedType = TypeUtils.extractTypeArguments(method.returnType()).iterator().next();
+
+ TypeMirror boxedWrappedType = generatorUtilities.boxIfNecessary(wrappedType);
+
+ TypeName mapType =
+ ParameterizedTypeName.get(
+ ClassName.get(Map.class), PROFILE_CLASSNAME, ClassName.get(boxedWrappedType));
+
+ ParameterizedTypeName returnType = ParameterizedTypeName.get(rawFutureType, mapType);
+
+ String methodName = method.simpleName();
+ String params =
+ method.commaSeparatedParameters(
+ crossProfileType.supportedTypes(), REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS);
+
+ CodeBlock methodCall = CodeBlock.of("$L($L)", methodName, params);
+
+ MethodSpec.Builder methodBuilder =
+ MethodSpec.methodBuilder(methodName)
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(returnType)
+ .addParameters(
+ GeneratorUtilities.extractParametersFromMethod(
+ crossProfileType.supportedTypes(),
+ method.methodElement(),
+ REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS));
+
+ methodBuilder.addStatement(
+ "$T<$T, $T> results = new $T<>()",
+ Map.class,
+ PROFILE_CLASSNAME,
+ method.returnType(),
+ HashMap.class);
+ methodBuilder.beginControlFlow(
+ "for ($T senderIdentifier : senders.keySet())", PROFILE_CLASSNAME);
+ methodBuilder.addStatement(
+ "$T sender = senders.get(senderIdentifier)",
+ InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName(
+ generatorContext, crossProfileType));
+ methodBuilder.addStatement("results.put(senderIdentifier, sender.$L)", methodCall);
+ methodBuilder.endControlFlow();
+ methodBuilder.addStatement("return $T.groupResults(results)", futureWrapper.wrapperClassName());
+
+ classBuilder.addMethod(methodBuilder.build());
+ }
+
+ private List<ParameterSpec> convertCallbackParametersIntoMulti(
+ List<ParameterSpec> parameters,
+ VariableElement callbackParameter,
+ CrossProfileCallbackInterfaceInfo callbackInterface) {
+ return parameters.stream()
+ .map(
+ e ->
+ e.name.equals(callbackParameter.getSimpleName().toString())
+ ? convertCallbackToMulti(e, callbackInterface)
+ : e)
+ .collect(toList());
+ }
+
+ private String generateMergerInputConstructor(
+ CrossProfileCallbackInterfaceInfo callbackInterface) {
+ return CodeBlock.of(
+ "new $T(senderIdentifier, merger)",
+ CrossProfileCallbackCodeGenerator.getCrossProfileCallbackMultiMergerInputClassName(
+ generatorContext, callbackInterface))
+ .toString();
+ }
+
+ private ParameterSpec convertCallbackToMulti(
+ ParameterSpec parameter, CrossProfileCallbackInterfaceInfo callbackInterface) {
+ return ParameterSpec.builder(
+ CrossProfileCallbackCodeGenerator.getCrossProfileCallbackMultiInterfaceClassName(
+ generatorContext, callbackInterface),
+ parameter.name)
+ .addModifiers(parameter.modifiers)
+ .addAnnotations(parameter.annotations)
+ .build();
+ }
+
+ static ClassName getMultipleProfilesClassName(
+ GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) {
+ return GeneratorUtilities.appendToClassName(
+ crossProfileType.profileClassName(), "_MultipleProfiles");
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/OtherProfileGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/OtherProfileGenerator.java
new file mode 100644
index 0000000..ace2f9e
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/OtherProfileGenerator.java
@@ -0,0 +1,379 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.EXCEPTION_CALLBACK_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.LOCAL_CALLBACK_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_CONNECTOR_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileAnnotation;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackInterfaceInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.FutureWrapper;
+import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext;
+import com.squareup.javapoet.AnnotationSpec;
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.CodeBlock;
+import com.squareup.javapoet.FieldSpec;
+import com.squareup.javapoet.MethodSpec;
+import com.squareup.javapoet.TypeName;
+import com.squareup.javapoet.TypeSpec;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+
+/**
+ * Generate the {@code Profile_*_OtherProfile} class for a single cross-profile type.
+ *
+ * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to
+ * validate that the annotated code is correct.
+ */
+final class OtherProfileGenerator {
+
+ private boolean generated = false;
+ private final GeneratorContext generatorContext;
+ private final GeneratorUtilities generatorUtilities;
+ private final CrossProfileTypeInfo crossProfileType;
+
+ OtherProfileGenerator(GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) {
+ this.generatorContext = checkNotNull(generatorContext);
+ this.generatorUtilities = new GeneratorUtilities(generatorContext);
+ this.crossProfileType = checkNotNull(crossProfileType);
+ }
+
+ void generate() {
+ if (generated) {
+ throw new IllegalStateException("OtherProfileGenerator#generate can only be called once");
+ }
+ generated = true;
+
+ generateOtherProfileClass();
+ }
+
+ private void generateOtherProfileClass() {
+ ClassName className = getOtherProfileClassName(generatorContext, crossProfileType);
+
+ ClassName singleSenderCanThrowInterface =
+ InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName(
+ generatorContext, crossProfileType);
+
+ TypeSpec.Builder classBuilder =
+ TypeSpec.classBuilder(className)
+ .addJavadoc(
+ "Implementation of {@link $T} used when interacting with the other profile.\n",
+ singleSenderCanThrowInterface)
+ .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
+ .addSuperinterface(singleSenderCanThrowInterface);
+
+ ClassName connectorClassName =
+ crossProfileType.profileConnector().isPresent()
+ ? crossProfileType.profileConnector().get().connectorClassName()
+ : PROFILE_CONNECTOR_CLASSNAME;
+
+ classBuilder.addField(connectorClassName, "connector", Modifier.PRIVATE, Modifier.FINAL);
+
+ classBuilder.addMethod(
+ MethodSpec.constructorBuilder()
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(connectorClassName, "connector")
+ .beginControlFlow("if (connector == null)")
+ .addStatement("throw new $T()", NullPointerException.class)
+ .endControlFlow()
+ .addStatement("this.connector = connector")
+ .build());
+
+ classBuilder.addField(
+ FieldSpec.builder(long.class, "timeout")
+ .addAnnotation(
+ AnnotationSpec.builder(SuppressWarnings.class)
+ .addMember("value", "$S", "GoodTime")
+ .build())
+ .addModifiers(Modifier.PRIVATE)
+ .initializer("$L", CrossProfileAnnotation.TIMEOUT_MILLIS_NOT_SET)
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("timeout")
+ .addAnnotation(Override.class)
+ .addAnnotation(
+ AnnotationSpec.builder(SuppressWarnings.class)
+ .addMember("value", "$S", "GoodTime")
+ .build())
+ .addModifiers(Modifier.PUBLIC)
+ .returns(className)
+ .addParameter(long.class, "timeout")
+ .addStatement("this.timeout = timeout")
+ .addStatement("return this")
+ .build());
+
+ ClassName ifAvailableClass =
+ IfAvailableGenerator.getIfAvailableClassName(generatorContext, crossProfileType);
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("ifAvailable")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(ifAvailableClass)
+ .addStatement("return new $T(this)", ifAvailableClass)
+ .build());
+
+ for (CrossProfileMethodInfo method : crossProfileType.crossProfileMethods()) {
+ if (method.isBlocking(generatorContext, crossProfileType)) {
+ generateBlockingMethodOnOtherProfileClass(classBuilder, method, crossProfileType);
+ } else if (method.isCrossProfileCallback(generatorContext)) {
+ generateCrossProfileCallbackMethodOnOtherProfileClass(
+ classBuilder, method, crossProfileType);
+ } else if (method.isFuture(crossProfileType)) {
+ generateFutureMethodOnOtherProfileClass(classBuilder, method, crossProfileType);
+ } else {
+ throw new IllegalStateException("Unknown method type: " + method);
+ }
+ }
+
+ generatorUtilities.writeClassToFile(className.packageName(), classBuilder);
+ }
+
+ private void generateBlockingMethodOnOtherProfileClass(
+ TypeSpec.Builder classBuilder,
+ CrossProfileMethodInfo method,
+ CrossProfileTypeInfo crossProfileType) {
+
+ MethodSpec.Builder methodBuilder =
+ MethodSpec.methodBuilder(method.simpleName())
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .addExceptions(method.thrownExceptions())
+ .addException(UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME)
+ .returns(method.returnTypeTypeName())
+ .addParameters(
+ GeneratorUtilities.extractParametersFromMethod(
+ crossProfileType.supportedTypes(),
+ method.methodElement(),
+ REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS));
+
+ methodBuilder.addStatement(
+ "$1T internalCrossProfileClass = $1T.instance()",
+ InternalCrossProfileClassGenerator.getInternalCrossProfileClassName(
+ generatorContext, crossProfileType));
+
+ // parcel is recycled in this method
+ methodBuilder.addStatement("$1T params = $1T.obtain()", PARCEL_CLASSNAME);
+ for (VariableElement param : method.methodElement().getParameters()) {
+ if (crossProfileType.supportedTypes().isAutomaticallyResolved(param.asType())) {
+ continue;
+ }
+ methodBuilder.addStatement(
+ "internalCrossProfileClass.bundler().writeToParcel(params, $1L, $2L, /* flags= */ 0)",
+ param.getSimpleName(),
+ TypeUtils.generateBundlerType(param.asType()));
+ }
+
+ if (method.thrownExceptions().isEmpty()) {
+ methodBuilder.addStatement(
+ "$1T returnParcel = connector.crossProfileSender().call($2LL, $3L, params)",
+ PARCEL_CLASSNAME,
+ crossProfileType.identifier(),
+ method.identifier());
+ } else {
+ methodBuilder.addStatement("$1T returnParcel", PARCEL_CLASSNAME);
+ methodBuilder.beginControlFlow("try");
+ methodBuilder.addStatement(
+ "returnParcel = connector.crossProfileSender().callWithExceptions($1LL, $2L, params)",
+ crossProfileType.identifier(),
+ method.identifier());
+ methodBuilder.nextControlFlow("catch ($T e)", UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME);
+ methodBuilder.addStatement("throw e");
+
+ for (TypeName exception : method.thrownExceptions()) {
+ methodBuilder.nextControlFlow("catch ($T e)", exception);
+ methodBuilder.addStatement("throw e");
+ }
+
+ methodBuilder.nextControlFlow("catch ($T e)", Throwable.class);
+ methodBuilder.addStatement(
+ "throw new $T($S)", IllegalStateException.class, "Unexpected exception thrown");
+ methodBuilder.endControlFlow();
+ }
+
+ methodBuilder.addStatement("params.recycle()");
+
+ if (!method.returnType().getKind().equals(TypeKind.VOID)) {
+ methodBuilder.addStatement(
+ CodeBlock.of(
+ "@SuppressWarnings(\"unchecked\") $1T returnValue = ($1T)"
+ + " internalCrossProfileClass.bundler().readFromParcel(returnParcel,"
+ + " $2L)",
+ method.returnType(),
+ TypeUtils.generateBundlerType(method.returnType())));
+ methodBuilder.addStatement("returnParcel.recycle()");
+ methodBuilder.addStatement("return returnValue");
+ } else {
+ methodBuilder.addStatement("returnParcel.recycle()");
+ }
+
+ classBuilder.addMethod(methodBuilder.build());
+ }
+
+ private void generateCrossProfileCallbackMethodOnOtherProfileClass(
+ TypeSpec.Builder classBuilder,
+ CrossProfileMethodInfo method,
+ CrossProfileTypeInfo crossProfileType) {
+ VariableElement callbackParameter = method.getCrossProfileCallbackParam(generatorContext).get();
+ TypeElement callbackType =
+ generatorContext.elements().getTypeElement(callbackParameter.asType().toString());
+ CrossProfileCallbackInterfaceInfo callbackInterface =
+ CrossProfileCallbackInterfaceInfo.create(callbackType);
+
+ MethodSpec.Builder methodBuilder =
+ MethodSpec.methodBuilder(method.simpleName())
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(method.returnTypeTypeName())
+ .addParameters(
+ GeneratorUtilities.extractParametersFromMethod(
+ crossProfileType.supportedTypes(),
+ method.methodElement(),
+ REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS))
+ .addParameter(EXCEPTION_CALLBACK_CLASSNAME, "exceptionCallback");
+
+ methodBuilder.addStatement(
+ "$1T internalCrossProfileClass = $1T.instance()",
+ InternalCrossProfileClassGenerator.getInternalCrossProfileClassName(
+ generatorContext, crossProfileType));
+
+ // parcel is passed into callAsync where it will be cached and recycled afterwards
+ methodBuilder.addStatement("$1T params = $1T.obtain()", PARCEL_CLASSNAME);
+
+ for (VariableElement param : method.methodElement().getParameters()) {
+ if (crossProfileType.supportedTypes().isAutomaticallyResolved(param.asType())) {
+ continue;
+ }
+ if (param.getSimpleName().equals(callbackParameter.getSimpleName())) {
+ continue;
+ }
+ methodBuilder.addStatement(
+ "internalCrossProfileClass.bundler().writeToParcel(params, $1L, $2L, /* flags= */ 0)",
+ param.getSimpleName(),
+ TypeUtils.generateBundlerType(param.asType()));
+ }
+
+ methodBuilder.addStatement(
+ "$1T sender = new $2T($3L, exceptionCallback, internalCrossProfileClass.bundler())",
+ LOCAL_CALLBACK_CLASSNAME,
+ CrossProfileCallbackCodeGenerator.getCrossProfileCallbackSenderClassName(
+ generatorContext, callbackInterface),
+ callbackParameter.getSimpleName());
+
+ // Suppress GoodTime warning for unboxing Duration.
+ methodBuilder.addAnnotation(
+ AnnotationSpec.builder(SuppressWarnings.class)
+ .addMember("value", "$S", "GoodTime")
+ .build());
+ methodBuilder.addStatement(
+ "connector.crossProfileSender().callAsync($1LL, $2L, params, sender, timeout =="
+ + " $3L ? $4L : timeout)",
+ crossProfileType.identifier(),
+ method.identifier(),
+ CrossProfileAnnotation.TIMEOUT_MILLIS_NOT_SET,
+ method.timeoutMillis());
+
+ methodBuilder.addComment(
+ "We don't recycle the params as they will be stored for the async call and recycled"
+ + " afterwards");
+
+ classBuilder.addMethod(methodBuilder.build());
+ }
+
+ private void generateFutureMethodOnOtherProfileClass(
+ TypeSpec.Builder classBuilder,
+ CrossProfileMethodInfo method,
+ CrossProfileTypeInfo crossProfileType) {
+
+ MethodSpec.Builder methodBuilder =
+ MethodSpec.methodBuilder(method.simpleName())
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(method.returnTypeTypeName())
+ .addParameters(
+ GeneratorUtilities.extractParametersFromMethod(
+ crossProfileType.supportedTypes(),
+ method.methodElement(),
+ REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS));
+
+ methodBuilder.addStatement(
+ "$1T internalCrossProfileClass = $1T.instance()",
+ InternalCrossProfileClassGenerator.getInternalCrossProfileClassName(
+ generatorContext, crossProfileType));
+
+ // parcel is passed into callAsync where it will be cached and recycled afterwards
+ methodBuilder.addStatement("$1T params = $1T.obtain()", PARCEL_CLASSNAME);
+ for (VariableElement param : method.methodElement().getParameters()) {
+ if (crossProfileType.supportedTypes().isAutomaticallyResolved(param.asType())) {
+ continue;
+ }
+ methodBuilder.addStatement(
+ "internalCrossProfileClass.bundler().writeToParcel(params, $1L, $2L, /* flags= */ 0)",
+ param.getSimpleName(),
+ TypeUtils.generateBundlerType(param.asType()));
+ }
+
+ TypeMirror rawFutureType = TypeUtils.removeTypeArguments(method.returnType());
+
+ FutureWrapper futureWrapper =
+ crossProfileType.supportedTypes().getType(rawFutureType).getFutureWrapper().get();
+ // This assumes every Future is generic with one type argument
+ TypeMirror wrappedReturnType =
+ TypeUtils.extractTypeArguments(method.returnType()).iterator().next();
+ methodBuilder.addStatement(
+ "$1T<$2T> futureWrapper = $1T.create(internalCrossProfileClass.bundler(), $3L)",
+ futureWrapper.wrapperClassName(),
+ wrappedReturnType,
+ TypeUtils.generateBundlerType(wrappedReturnType));
+
+ methodBuilder.addAnnotation(
+ AnnotationSpec.builder(SuppressWarnings.class)
+ .addMember("value", "$S", "GoodTime")
+ .build());
+ methodBuilder.addStatement(
+ "connector.crossProfileSender().callAsync($1LL, $2L, params, futureWrapper,"
+ + " timeout == $3L ? $4L : timeout)",
+ crossProfileType.identifier(),
+ method.identifier(),
+ CrossProfileAnnotation.TIMEOUT_MILLIS_NOT_SET,
+ method.timeoutMillis());
+
+ methodBuilder.addComment(
+ "We don't recycle the params as they will be stored for the async call and recycled"
+ + " afterwards");
+
+ methodBuilder.addStatement("return futureWrapper.getFuture()");
+
+ classBuilder.addMethod(methodBuilder.build());
+ }
+
+ static ClassName getOtherProfileClassName(
+ GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) {
+ return GeneratorUtilities.appendToClassName(
+ crossProfileType.profileClassName(), "_OtherProfile");
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ParcelableWrappersGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ParcelableWrappersGenerator.java
new file mode 100644
index 0000000..dfcea33
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ParcelableWrappersGenerator.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toSet;
+
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext;
+import com.google.android.enterprise.connectedapps.processor.containers.ParcelableWrapper;
+import com.google.android.enterprise.connectedapps.processor.containers.ParcelableWrapper.WrapperType;
+import com.google.android.enterprise.connectedapps.processor.containers.Type;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.nio.charset.Charset;
+import java.util.Collection;
+import java.util.Optional;
+import javax.tools.JavaFileObject;
+
+/**
+ * Generate the {@code Parcelable*} classes for every used compatible type.
+ *
+ * <p>This is intended to be initialised and used once, which will generate all needed code.
+ *
+ * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to
+ * validate that the annotated code is correct.
+ */
+final class ParcelableWrappersGenerator {
+
+ private boolean generated = false;
+ private final GeneratorContext generatorContext;
+
+ ParcelableWrappersGenerator(GeneratorContext generatorContext) {
+ this.generatorContext = checkNotNull(generatorContext);
+ }
+
+ void generate() {
+ if (generated) {
+ throw new IllegalStateException(
+ "ParcelableWrappersGenerator#generate can only be called once");
+ }
+ generated = true;
+
+ generateParcelableWrappers();
+ }
+
+ private void generateParcelableWrappers() {
+ Collection<ParcelableWrapper> parcelableWrappersToGenerate =
+ generatorContext.crossProfileTypes().stream()
+ .map(CrossProfileTypeInfo::supportedTypes)
+ .flatMap(s -> s.usableTypes().stream())
+ .filter(s -> s.getParcelableWrapper().isPresent())
+ .map(Type::getParcelableWrapper)
+ .map(Optional::get)
+ .collect(toSet());
+
+ generateDefaultParcelableWrappers(parcelableWrappersToGenerate);
+ generateProtoParcelableWrappers(parcelableWrappersToGenerate);
+ }
+
+ private void generateDefaultParcelableWrappers(Collection<ParcelableWrapper> parcelableWrappers) {
+ Collection<ParcelableWrapper> defaultParcelableWrappersToGenerate =
+ parcelableWrappers.stream()
+ .filter(f -> f.wrapperType() == WrapperType.DEFAULT)
+ .collect(toSet());
+
+ for (ParcelableWrapper parcelableWrapper : defaultParcelableWrappersToGenerate) {
+ if (generatorContext
+ .elements()
+ .getTypeElement(parcelableWrapper.wrapperClassName().toString())
+ != null) {
+ // We don't generate things which already exist
+ return;
+ }
+ generateDefaultParcelableWrapper(parcelableWrapper);
+ }
+ }
+
+ private void generateDefaultParcelableWrapper(ParcelableWrapper parcelableWrapper) {
+ String parcelableWrapperSimpleName = parcelableWrapper.defaultWrapperClassName().simpleName();
+
+ String contents;
+ InputStream in =
+ ParcelableWrappersGenerator.class.getResourceAsStream(
+ "/parcelablewrappers/" + parcelableWrapperSimpleName + ".java");
+
+ try (BufferedReader br =
+ new BufferedReader(new InputStreamReader(in, Charset.defaultCharset()))) {
+ contents = br.lines().collect(joining(System.lineSeparator()));
+ } catch (IOException e) {
+ throw new IllegalStateException(
+ "Could not read parcelablewrapper file for " + parcelableWrapperSimpleName, e);
+ }
+
+ contents =
+ contents.replace(
+ parcelableWrapper.defaultWrapperClassName().packageName(),
+ parcelableWrapper.wrapperClassName().packageName());
+ contents =
+ contents.replace(
+ parcelableWrapper.defaultWrapperClassName().simpleName(),
+ parcelableWrapper.wrapperClassName().simpleName());
+
+ JavaFileObject builderFile;
+ try {
+ builderFile =
+ generatorContext
+ .processingEnv()
+ .getFiler()
+ .createSourceFile(
+ parcelableWrapper.wrapperClassName().packageName()
+ + "."
+ + parcelableWrapper.wrapperClassName().simpleName());
+ } catch (IOException e) {
+ throw new IllegalStateException(
+ "Could not write parcelablewrapper for " + parcelableWrapperSimpleName, e);
+ }
+
+ try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {
+ out.write(contents);
+ } catch (IOException e) {
+ throw new IllegalStateException(
+ "Could not write parcelablewrapper for " + parcelableWrapperSimpleName, e);
+ }
+ }
+
+ private void generateProtoParcelableWrappers(Collection<ParcelableWrapper> parcelableWrappers) {
+ Collection<ParcelableWrapper> protoParcelableWrappersToGenerate =
+ parcelableWrappers.stream()
+ .filter(f -> f.wrapperType() == WrapperType.PROTO)
+ .collect(toSet());
+
+ for (ParcelableWrapper parcelableWrapper : protoParcelableWrappersToGenerate) {
+ new ProtoParcelableWrapperGenerator(generatorContext, parcelableWrapper).generate();
+ }
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/Processor.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/Processor.java
new file mode 100644
index 0000000..4948196
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/Processor.java
@@ -0,0 +1,415 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.elementsAnnotatedWithCrossProfile;
+import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.elementsAnnotatedWithCrossProfileCallback;
+import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.elementsAnnotatedWithCrossProfileConfiguration;
+import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.elementsAnnotatedWithCrossProfileConfigurations;
+import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.elementsAnnotatedWithCrossProfileProvider;
+import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.elementsAnnotatedWithCrossProfileTest;
+import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.hasCrossProfileProviderAnnotation;
+import static java.util.stream.Collectors.toSet;
+
+import com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper;
+import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper;
+import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector;
+import com.google.android.enterprise.connectedapps.annotations.CustomUserConnector;
+import com.google.android.enterprise.connectedapps.annotations.GeneratedProfileConnector;
+import com.google.android.enterprise.connectedapps.annotations.GeneratedUserConnector;
+import com.google.android.enterprise.connectedapps.processor.containers.FutureWrapper;
+import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext;
+import com.google.android.enterprise.connectedapps.processor.containers.ParcelableWrapper;
+import com.google.android.enterprise.connectedapps.processor.containers.ProfileConnectorInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.UserConnectorInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.ValidatorContext;
+import com.google.android.enterprise.connectedapps.processor.containers.ValidatorCrossProfileConfigurationInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.ValidatorCrossProfileTestInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.ValidatorCrossProfileTypeInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.ValidatorProviderClassInfo;
+import com.google.auto.service.AutoService;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.annotation.processing.RoundEnvironment;
+import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
+
+/** Processor for generation of cross-profile code. */
+@SupportedAnnotationTypes({
+ "com.google.android.enterprise.connectedapps.annotations.CrossProfile",
+ "com.google.android.enterprise.connectedapps.annotations.CrossProfileCallback",
+ "com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration",
+ "com.google.android.enterprise.connectedapps.annotations.CrossProfileConfigurations",
+ "com.google.android.enterprise.connectedapps.annotations.CrossProfileProvider",
+ "com.google.android.enterprise.connectedapps.testing.annotations.CrossProfileTest",
+ "com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector",
+ "com.google.android.enterprise.connectedapps.annotations.GeneratedProfileConnector",
+ "com.google.android.enterprise.connectedapps.annotations.CrossUser",
+ "com.google.android.enterprise.connectedapps.annotations.CrossUserCallback",
+ "com.google.android.enterprise.connectedapps.annotations.CrossUserConfiguration",
+ "com.google.android.enterprise.connectedapps.annotations.CrossUserConfigurations",
+ "com.google.android.enterprise.connectedapps.annotations.CrossUserProvider",
+ "com.google.android.enterprise.connectedapps.testing.annotations.CrossUserTest",
+ "com.google.android.enterprise.connectedapps.annotations.CustomUserConnector",
+ "com.google.android.enterprise.connectedapps.annotations.GeneratedUserConnector",
+ "com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper",
+ "com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper"
+})
+@AutoService(javax.annotation.processing.Processor.class)
+public final class Processor extends AbstractProcessor {
+
+ private Types types;
+
+ @Override
+ public SourceVersion getSupportedSourceVersion() {
+ return SourceVersion.latest();
+ }
+
+ @Override
+ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
+ Elements elements = processingEnv.getElementUtils();
+ types = processingEnv.getTypeUtils();
+
+ Collection<ValidatorCrossProfileTestInfo> newCrossProfileTests =
+ findNewCrossProfileTests(roundEnv);
+ // Only new configurations need code generating - but we need to support types used by methods
+ // included in configurations under test
+ Collection<ValidatorCrossProfileConfigurationInfo> newConfigurations =
+ findNewConfigurations(roundEnv);
+ Collection<ValidatorCrossProfileConfigurationInfo> allConfigurations =
+ findAllConfigurations(newConfigurations, newCrossProfileTests);
+
+ Collection<ValidatorProviderClassInfo> newProviderClasses = findNewProviderClasses(roundEnv);
+ Collection<ExecutableElement> newProviderMethods = findNewProviderMethods(roundEnv);
+ Collection<TypeElement> newGeneratedConnectors = findNewGeneratedConnectors(roundEnv);
+ Collection<TypeElement> newGeneratedUserConnectors = findNewGeneratedUserConnectors(roundEnv);
+ Collection<ExecutableElement> newCrossProfileMethods = findNewCrossProfileMethods(roundEnv);
+ Collection<ExecutableElement> allCrossProfileMethods =
+ findAllCrossProfileMethods(
+ processingEnv,
+ elements,
+ newCrossProfileMethods,
+ allConfigurations,
+ newProviderMethods,
+ newProviderClasses);
+ Collection<TypeElement> newCrossProfileCallbackInterfaces =
+ findNewCrossProfileCallbackInterfaces(roundEnv);
+
+ Collection<ExecutableElement> methods = new HashSet<>(allCrossProfileMethods);
+ methods.addAll(
+ newCrossProfileCallbackInterfaces.stream()
+ .flatMap(i -> i.getEnclosedElements().stream())
+ .filter(e -> e instanceof ExecutableElement)
+ .map(e -> (ExecutableElement) e)
+ .filter(e -> e.getKind() == ElementKind.METHOD)
+ .collect(toSet()));
+
+ Collection<TypeElement> newCustomParcelableWrappers = findNewParcelableWrappers(roundEnv);
+ Collection<TypeElement> newCustomFutureWrappers = findNewFutureWrappers(roundEnv);
+
+ Collection<FutureWrapper> globalFutureWrappers =
+ FutureWrapper.createGlobalFutureWrappers(elements);
+ Collection<ParcelableWrapper> globalParcelableWrappers =
+ ParcelableWrapper.createGlobalParcelableWrappers(types, elements, methods);
+
+ SupportedTypes globalSupportedTypes =
+ SupportedTypes.createFromMethods(
+ types, elements, globalParcelableWrappers, globalFutureWrappers, methods);
+
+ Collection<ValidatorCrossProfileTypeInfo> newCrossProfileTypes =
+ findNewCrossProfileTypes(roundEnv, globalSupportedTypes);
+ Collection<ProfileConnectorInfo> newProfileConnectorInterfaces =
+ findNewProfileConnectorInterfaces(roundEnv, globalSupportedTypes);
+ Collection<UserConnectorInfo> newUserConnectorInterfaces =
+ findNewUserConnectorInterfaces(roundEnv, globalSupportedTypes);
+
+ ValidatorContext validatorContext =
+ ValidatorContext.builder()
+ .setProcessingEnv(processingEnv)
+ .setElements(elements)
+ .setTypes(types)
+ .setGlobalSupportedTypes(globalSupportedTypes)
+ .setNewProfileConnectorInterfaces(newProfileConnectorInterfaces)
+ .setNewUserConnectorInterfaces(newUserConnectorInterfaces)
+ .setNewGeneratedProfileConnectors(newGeneratedConnectors)
+ .setNewGeneratedUserConnectors(newGeneratedUserConnectors)
+ .setNewConfigurations(newConfigurations)
+ .setNewCrossProfileTypes(newCrossProfileTypes)
+ .setNewCrossProfileMethods(newCrossProfileMethods)
+ .setNewProviderClasses(newProviderClasses)
+ .setNewProviderMethods(newProviderMethods)
+ .setNewCrossProfileCallbackInterfaces(newCrossProfileCallbackInterfaces)
+ .setNewCrossProfileTests(newCrossProfileTests)
+ .setNewCustomParcelableWrappers(newCustomParcelableWrappers)
+ .setNewCustomFutureWrappers(newCustomFutureWrappers)
+ .build();
+
+ boolean isValid = new EarlyValidator(validatorContext).validate();
+
+ if (!isValid) {
+ return false;
+ }
+
+ GeneratorContext generatorContext =
+ GeneratorContext.createFromValidatorContext(validatorContext);
+
+ isValid = new LateValidator(generatorContext).validate();
+
+ if (!isValid) {
+ return false;
+ }
+
+ new CodeGenerator(generatorContext).generate();
+
+ return false;
+ }
+
+ private Collection<ValidatorCrossProfileConfigurationInfo> findNewConfigurations(
+ RoundEnvironment roundEnv) {
+ Set<ValidatorCrossProfileConfigurationInfo> annotations = new HashSet<>();
+
+ elementsAnnotatedWithCrossProfileConfiguration(roundEnv)
+ .map(
+ element ->
+ ValidatorCrossProfileConfigurationInfo.createFromElement(
+ processingEnv, (TypeElement) element))
+ .forEach(annotations::add);
+
+ elementsAnnotatedWithCrossProfileConfigurations(roundEnv)
+ .map(
+ element ->
+ ValidatorCrossProfileConfigurationInfo.createMultipleFromElement(
+ processingEnv, (TypeElement) element))
+ .forEach(annotations::addAll);
+
+ return annotations;
+ }
+
+ private Collection<ValidatorCrossProfileConfigurationInfo> findAllConfigurations(
+ Collection<ValidatorCrossProfileConfigurationInfo> newConfigurations,
+ Collection<ValidatorCrossProfileTestInfo> crossProfileTests) {
+ Set<ValidatorCrossProfileConfigurationInfo> allConfigurations = new HashSet<>();
+ allConfigurations.addAll(newConfigurations);
+ allConfigurations.addAll(
+ crossProfileTests.stream()
+ .flatMap(
+ t ->
+ ValidatorCrossProfileConfigurationInfo.createMultipleFromElement(
+ processingEnv, t.configurationElement())
+ .stream())
+ .collect(toSet()));
+ return allConfigurations;
+ }
+
+ private Collection<ValidatorProviderClassInfo> findNewProviderClasses(RoundEnvironment roundEnv) {
+ Set<ValidatorProviderClassInfo> annotatedClasses =
+ elementsAnnotatedWithCrossProfileProvider(roundEnv)
+ .filter(m -> m instanceof TypeElement)
+ .map(m -> (TypeElement) m)
+ .map(m -> ValidatorProviderClassInfo.create(processingEnv, m))
+ .collect(toSet());
+
+ Set<ValidatorProviderClassInfo> unannotatedClasses =
+ elementsAnnotatedWithCrossProfileProvider(roundEnv)
+ .filter(m -> m instanceof ExecutableElement)
+ .map(m -> (ExecutableElement) m)
+ .map(Element::getEnclosingElement)
+ .map(m -> (TypeElement) m)
+ .filter(m -> !hasCrossProfileProviderAnnotation(m))
+ .map(m -> ValidatorProviderClassInfo.create(processingEnv, m))
+ .collect(toSet());
+
+ Collection<ValidatorProviderClassInfo> allProviders = new HashSet<>();
+ allProviders.addAll(annotatedClasses);
+ allProviders.addAll(unannotatedClasses);
+ return allProviders;
+ }
+
+ private Collection<ExecutableElement> findNewProviderMethods(RoundEnvironment roundEnv) {
+ return elementsAnnotatedWithCrossProfileProvider(roundEnv)
+ .filter(m -> m instanceof ExecutableElement)
+ .map(m -> (ExecutableElement) m)
+ .collect(toSet());
+ }
+
+ private Collection<ValidatorCrossProfileTypeInfo> findNewCrossProfileTypes(
+ RoundEnvironment roundEnv, SupportedTypes globalSupportedTypes) {
+ Collection<ValidatorCrossProfileTypeInfo> annotatedTypes =
+ elementsAnnotatedWithCrossProfile(roundEnv)
+ .filter(m -> m instanceof TypeElement)
+ .map(m -> (TypeElement) m)
+ .map(m -> ValidatorCrossProfileTypeInfo.create(processingEnv, m, globalSupportedTypes))
+ .collect(toSet());
+
+ Collection<ValidatorCrossProfileTypeInfo> unannotatedTypes =
+ elementsAnnotatedWithCrossProfile(roundEnv)
+ .filter(m -> m instanceof ExecutableElement)
+ .map(m -> (ExecutableElement) m)
+ .map(ExecutableElement::getEnclosingElement)
+ .filter(m -> m instanceof TypeElement)
+ .map(m -> (TypeElement) m)
+ .map(m -> ValidatorCrossProfileTypeInfo.create(processingEnv, m, globalSupportedTypes))
+ .collect(toSet());
+
+ Collection<ValidatorCrossProfileTypeInfo> allTypes = new HashSet<>();
+ allTypes.addAll(annotatedTypes);
+ allTypes.addAll(unannotatedTypes);
+ return allTypes;
+ }
+
+ private Collection<ExecutableElement> findNewCrossProfileMethods(RoundEnvironment roundEnv) {
+ return elementsAnnotatedWithCrossProfile(roundEnv)
+ .filter(m -> m instanceof ExecutableElement)
+ .map(m -> (ExecutableElement) m)
+ .collect(toSet());
+ }
+
+ private Collection<ProfileConnectorInfo> findNewProfileConnectorInterfaces(
+ RoundEnvironment roundEnv, SupportedTypes globalSupportedTypes) {
+ Collection<TypeElement> connectorInterfaces =
+ roundEnv.getElementsAnnotatedWith(CustomProfileConnector.class).stream()
+ .map(m -> (TypeElement) m)
+ .collect(toSet());
+
+ // We manually add the SDK-provided CrossProfileConnector as it won't be detected by roundEnv
+ connectorInterfaces.add(
+ processingEnv
+ .getElementUtils()
+ .getTypeElement("com.google.android.enterprise.connectedapps.CrossProfileConnector"));
+
+ return connectorInterfaces.stream()
+ .map(t -> ProfileConnectorInfo.create(processingEnv, t, globalSupportedTypes))
+ .collect(Collectors.toSet());
+ }
+
+ private Collection<UserConnectorInfo> findNewUserConnectorInterfaces(
+ RoundEnvironment roundEnv, SupportedTypes globalSupportedTypes) {
+ Collection<TypeElement> connectorInterfaces =
+ roundEnv.getElementsAnnotatedWith(CustomUserConnector.class).stream()
+ .map(m -> (TypeElement) m)
+ .collect(toSet());
+
+ // We manually add the SDK-provided CrossUserConnector as it won't be detected by roundEnv
+ connectorInterfaces.add(
+ processingEnv
+ .getElementUtils()
+ .getTypeElement("com.google.android.enterprise.connectedapps.CrossUserConnector"));
+
+ return connectorInterfaces.stream()
+ .map(t -> UserConnectorInfo.create(processingEnv, t, globalSupportedTypes))
+ .collect(Collectors.toSet());
+ }
+
+ private Collection<TypeElement> findNewGeneratedConnectors(RoundEnvironment roundEnv) {
+ Collection<TypeElement> connectorInterfaces =
+ roundEnv.getElementsAnnotatedWith(GeneratedProfileConnector.class).stream()
+ .map(m -> (TypeElement) m)
+ .collect(toSet());
+
+ return connectorInterfaces;
+ }
+
+ private Collection<TypeElement> findNewGeneratedUserConnectors(RoundEnvironment roundEnv) {
+ Collection<TypeElement> connectorInterfaces =
+ roundEnv.getElementsAnnotatedWith(GeneratedUserConnector.class).stream()
+ .map(m -> (TypeElement) m)
+ .collect(toSet());
+
+ return connectorInterfaces;
+ }
+
+ private static Collection<ExecutableElement> findAllCrossProfileMethods(
+ ProcessingEnvironment processingEnvironment,
+ Elements elements,
+ Collection<ExecutableElement> newCrossProfileMethods,
+ Collection<ValidatorCrossProfileConfigurationInfo> configurations,
+ Collection<ExecutableElement> newProviderMethods,
+ Collection<ValidatorProviderClassInfo> newProviderClasses) {
+ Collection<ExecutableElement> allCrossProfileMethods = new HashSet<>(newCrossProfileMethods);
+
+ Collection<ValidatorProviderClassInfo> foundProviderClasses =
+ configurations.stream()
+ .flatMap(a -> a.providerClassElements().stream())
+ .map(m -> ValidatorProviderClassInfo.create(processingEnvironment, m))
+ .collect(toSet());
+
+ Collection<ExecutableElement> providerMethods =
+ foundProviderClasses.stream()
+ .flatMap(
+ m ->
+ GeneratorUtilities.findCrossProfileProviderMethodsInClass(
+ m.providerClassElement())
+ .stream())
+ .collect(toSet());
+
+ providerMethods.addAll(newProviderMethods);
+
+ Collection<TypeElement> crossProfileTypes =
+ providerMethods.stream()
+ .map(e -> elements.getTypeElement(e.getReturnType().toString()))
+ .filter(Objects::nonNull)
+ .collect(toSet());
+ crossProfileTypes.addAll(
+ foundProviderClasses.stream().flatMap(m -> m.staticTypes().stream()).collect(toSet()));
+ crossProfileTypes.addAll(
+ newProviderClasses.stream().flatMap(m -> m.staticTypes().stream()).collect(toSet()));
+
+ Collection<ExecutableElement> foundCrossProfileMethods =
+ crossProfileTypes.stream()
+ .flatMap(t -> GeneratorUtilities.findCrossProfileMethodsInClass(t).stream())
+ .collect(toSet());
+
+ allCrossProfileMethods.addAll(foundCrossProfileMethods);
+ return allCrossProfileMethods;
+ }
+
+ private Collection<ValidatorCrossProfileTestInfo> findNewCrossProfileTests(
+ RoundEnvironment roundEnv) {
+ return elementsAnnotatedWithCrossProfileTest(roundEnv)
+ .map(e -> (TypeElement) e)
+ .map(e -> ValidatorCrossProfileTestInfo.create(processingEnv, e))
+ .collect(toSet());
+ }
+
+ private Collection<TypeElement> findNewCrossProfileCallbackInterfaces(RoundEnvironment roundEnv) {
+ return elementsAnnotatedWithCrossProfileCallback(roundEnv)
+ .map(m -> (TypeElement) m)
+ .collect(toSet());
+ }
+
+ private Collection<TypeElement> findNewParcelableWrappers(RoundEnvironment roundEnv) {
+ return roundEnv.getElementsAnnotatedWith(CustomParcelableWrapper.class).stream()
+ .map(m -> (TypeElement) m)
+ .collect(toSet());
+ }
+
+ private Collection<TypeElement> findNewFutureWrappers(RoundEnvironment roundEnv) {
+ return roundEnv.getElementsAnnotatedWith(CustomFutureWrapper.class).stream()
+ .map(m -> (TypeElement) m)
+ .collect(toSet());
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProcessorConfiguration.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProcessorConfiguration.java
new file mode 100644
index 0000000..75d3c12
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProcessorConfiguration.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+/** General configuration. */
+public final class ProcessorConfiguration {
+ private ProcessorConfiguration() {}
+
+ /**
+ * When {@code true}, will generate a copy of each parcelable and future wrapper for each type
+ * which requires it.
+ *
+ * <p>This is required to ensure that there are no conflicts due to duplicate classes being
+ * generated in separate targets.
+ */
+ public static final boolean GENERATE_TYPE_SPECIFIC_WRAPPERS = true;
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProfileConnectorCodeGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProfileConnectorCodeGenerator.java
new file mode 100644
index 0000000..68c14f2
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProfileConnectorCodeGenerator.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.ABSTRACT_PROFILE_CONNECTOR_BUILDER_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.ABSTRACT_PROFILE_CONNECTOR_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.AVAILABILITY_RESTRICTIONS_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CONNECTION_BINDER_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CONTEXT_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.SCHEDULED_EXECUTOR_SERVICE_CLASSNAME;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector.ProfileType;
+import com.google.android.enterprise.connectedapps.annotations.GeneratedProfileConnector;
+import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext;
+import com.google.android.enterprise.connectedapps.processor.containers.ProfileConnectorInfo;
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.CodeBlock;
+import com.squareup.javapoet.FieldSpec;
+import com.squareup.javapoet.MethodSpec;
+import com.squareup.javapoet.TypeSpec;
+import javax.lang.model.element.Modifier;
+
+/**
+ * Generate the {@code Generated*} class for a single {@link GeneratedProfileConnector} annotated
+ * class.
+ *
+ * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to
+ * validate that the annotated code is correct.
+ */
+class ProfileConnectorCodeGenerator {
+ private boolean generated = false;
+
+ private final GeneratorContext generatorContext;
+ private final GeneratorUtilities generatorUtilities;
+ private final ProfileConnectorInfo connector;
+
+ ProfileConnectorCodeGenerator(GeneratorContext generatorContext, ProfileConnectorInfo connector) {
+ this.generatorContext = checkNotNull(generatorContext);
+ this.generatorUtilities = new GeneratorUtilities(generatorContext);
+ this.connector = checkNotNull(connector);
+ }
+
+ void generate() {
+ if (generated) {
+ throw new IllegalStateException(
+ "ProfileConnectorCodeGenerator#generate can only be called once");
+ }
+ generated = true;
+
+ generateProfileConnector();
+ }
+
+ private void generateProfileConnector() {
+ ClassName className = getGeneratedProfileConnectorClassName(generatorContext, connector);
+ ClassName builderClassName =
+ getGeneratedProfileConnectorBuilderClassName(generatorContext, connector);
+
+ TypeSpec.Builder classBuilder =
+ TypeSpec.classBuilder(className)
+ .addJavadoc(
+ "Generated implementation of {@link $T}.\n\n"
+ + "<p>All logic is implemented by {@link $T}.\n",
+ connector.connectorClassName(),
+ ABSTRACT_PROFILE_CONNECTOR_CLASSNAME)
+ .addModifiers(Modifier.FINAL)
+ .addSuperinterface(connector.connectorClassName())
+ .superclass(ABSTRACT_PROFILE_CONNECTOR_CLASSNAME);
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("builder")
+ .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
+ .returns(builderClassName)
+ .addParameter(CONTEXT_CLASSNAME, "context")
+ .addStatement("return new $T(context)", builderClassName)
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.constructorBuilder()
+ .addModifiers(Modifier.PRIVATE)
+ .addParameter(builderClassName, "builder")
+ .addStatement(
+ "super($1T.class, builder.profileConnectorBuilder)", connector.connectorClassName())
+ .build());
+
+ generateProfileConnectorBuilder(classBuilder);
+
+ generatorUtilities.writeClassToFile(className.packageName(), classBuilder);
+ }
+
+ private void generateProfileConnectorBuilder(TypeSpec.Builder profileConnector) {
+ ClassName profileConnectorClassName =
+ getGeneratedProfileConnectorClassName(generatorContext, connector);
+ ClassName builderClassName =
+ getGeneratedProfileConnectorBuilderClassName(generatorContext, connector);
+
+ TypeSpec.Builder classBuilder =
+ TypeSpec.classBuilder(builderClassName)
+ .addJavadoc("Builder for {@link $T}.\n", profileConnectorClassName)
+ .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC);
+
+ CodeBlock initialiser =
+ CodeBlock.of(
+ "new $T().setServiceClassName($S).setAvailabilityRestrictions($T.$L)",
+ ABSTRACT_PROFILE_CONNECTOR_BUILDER_CLASSNAME,
+ connector.serviceName().toString(),
+ AVAILABILITY_RESTRICTIONS_CLASSNAME,
+ connector.availabilityRestrictions().name());
+
+ if (connector.primaryProfile() != ProfileType.NONE) {
+ initialiser =
+ CodeBlock.of(
+ "$L.setPrimaryProfileType($T.$L)",
+ initialiser,
+ ProfileType.class,
+ connector.primaryProfile().name());
+ }
+
+ classBuilder.addMethod(
+ MethodSpec.constructorBuilder()
+ .addParameter(CONTEXT_CLASSNAME, "context")
+ .addStatement("profileConnectorBuilder.setContext(context)")
+ .build());
+
+ classBuilder.addField(
+ FieldSpec.builder(ABSTRACT_PROFILE_CONNECTOR_BUILDER_CLASSNAME, "profileConnectorBuilder")
+ .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
+ .initializer(initialiser)
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("setScheduledExecutorService")
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(SCHEDULED_EXECUTOR_SERVICE_CLASSNAME, "scheduledExecutorService")
+ .returns(builderClassName)
+ .addStatement(
+ "profileConnectorBuilder.setScheduledExecutorService(scheduledExecutorService)")
+ .addStatement("return this")
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("setBinder")
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(CONNECTION_BINDER_CLASSNAME, "binder")
+ .returns(builderClassName)
+ .addStatement("profileConnectorBuilder.setBinder(binder)")
+ .addStatement("return this")
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("build")
+ .addModifiers(Modifier.PUBLIC)
+ .returns(profileConnectorClassName)
+ .addStatement("return new $1T(this)", profileConnectorClassName)
+ .build());
+
+ profileConnector.addType(classBuilder.build());
+ }
+
+ static ClassName getGeneratedProfileConnectorClassName(
+ GeneratorContext generatorContext, ProfileConnectorInfo connector) {
+ return ClassName.get(
+ connector.connectorClassName().packageName(),
+ "Generated" + connector.connectorClassName().simpleName());
+ }
+
+ static ClassName getGeneratedProfileConnectorBuilderClassName(
+ GeneratorContext generatorContext, ProfileConnectorInfo connector) {
+ return ClassName.get(
+ connector.connectorClassName().packageName()
+ + "."
+ + "Generated"
+ + connector.connectorClassName().simpleName(),
+ "Builder");
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProtoParcelableWrapperGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProtoParcelableWrapperGenerator.java
new file mode 100644
index 0000000..302b420
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProtoParcelableWrapperGenerator.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.BUNDLER_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.BUNDLER_TYPE_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.INVALID_PROTOCOL_BUFFER_EXCEPTION_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCELABLE_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.containers.ParcelableWrapper.PARCELABLE_WRAPPER_PACKAGE;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext;
+import com.google.android.enterprise.connectedapps.processor.containers.ParcelableWrapper;
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.FieldSpec;
+import com.squareup.javapoet.MethodSpec;
+import com.squareup.javapoet.TypeSpec;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.type.TypeMirror;
+
+/**
+ * Generate the Parcelable Wrapper for a single Proto.
+ *
+ * <p>This is intended to be initialised and used once, which will generate all needed code.
+ *
+ * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to
+ * validate that the annotated code is correct.
+ */
+public final class ProtoParcelableWrapperGenerator {
+
+ private static final String GENERATED_PARCELABLE_WRAPPER_PACKAGE =
+ PARCELABLE_WRAPPER_PACKAGE + ".generated";
+
+ private boolean generated = false;
+ private final GeneratorContext generatorContext;
+ private final GeneratorUtilities generatorUtilities;
+ private final ParcelableWrapper parcelableWrapper;
+
+ ProtoParcelableWrapperGenerator(
+ GeneratorContext generatorContext, ParcelableWrapper parcelableWrapper) {
+ this.generatorContext = checkNotNull(generatorContext);
+ this.generatorUtilities = new GeneratorUtilities(generatorContext);
+ this.parcelableWrapper = checkNotNull(parcelableWrapper);
+ }
+
+ void generate() {
+ if (generated) {
+ throw new IllegalStateException(
+ "ProtoParcelableWrapperGenerator#generate can only be called once");
+ }
+ generated = true;
+
+ generateProtoParcelableWrapper();
+ }
+
+ private void generateProtoParcelableWrapper() {
+ ClassName wrapperClassName = parcelableWrapper.wrapperClassName();
+
+ if (generatorContext.elements().getTypeElement(wrapperClassName.toString()) != null) {
+ // We don't generate things which already exist
+ return;
+ }
+
+ TypeSpec.Builder classBuilder =
+ TypeSpec.classBuilder(wrapperClassName)
+ .addModifiers(Modifier.PUBLIC)
+ .addSuperinterface(PARCELABLE_CLASSNAME)
+ .addJavadoc(
+ "Wrapper for reading & writing {@link $T} instances to and from {@link $T}"
+ + " instances.",
+ parcelableWrapper.wrappedType(),
+ PARCEL_CLASSNAME);
+
+ classBuilder.addField(
+ FieldSpec.builder(ClassName.get(parcelableWrapper.wrappedType()), "proto", Modifier.PRIVATE)
+ .build());
+
+ classBuilder.addField(
+ FieldSpec.builder(int.class, "NULL_SIZE")
+ .addModifiers(Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC)
+ .initializer("-1")
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("of")
+ .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
+ .addJavadoc(
+ "Create a wrapper for the given {@link $T}.\n", parcelableWrapper.wrappedType())
+ .returns(parcelableWrapper.wrapperClassName())
+ .addParameter(BUNDLER_CLASSNAME, "bundler")
+ .addParameter(BUNDLER_TYPE_CLASSNAME, "type")
+ .addParameter(ClassName.get(parcelableWrapper.wrappedType()), "proto")
+ .addStatement(
+ "return new $T(bundler, type, proto)", parcelableWrapper.wrapperClassName())
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("get")
+ .addModifiers(Modifier.PUBLIC)
+ .returns(ClassName.get(parcelableWrapper.wrappedType()))
+ .addStatement("return proto")
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.constructorBuilder()
+ .addModifiers(Modifier.PRIVATE)
+ .addParameter(BUNDLER_CLASSNAME, "bundler")
+ .addParameter(BUNDLER_TYPE_CLASSNAME, "type")
+ .addParameter(ClassName.get(parcelableWrapper.wrappedType()), "proto")
+ .addStatement("this.proto = proto")
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.constructorBuilder()
+ .addModifiers(Modifier.PRIVATE)
+ .addParameter(PARCEL_CLASSNAME, "in")
+ .addStatement("int size = in.readInt()")
+ .beginControlFlow("if (size == NULL_SIZE)")
+ .addStatement("proto = null")
+ .addStatement("return")
+ .endControlFlow()
+ .addStatement("byte[] protoBytes = new byte[size]")
+ .addStatement("in.readByteArray(protoBytes)")
+ .beginControlFlow("try")
+ .addStatement("proto = $T.parseFrom(protoBytes)", parcelableWrapper.wrappedType())
+ .nextControlFlow("catch ($T e)", INVALID_PROTOCOL_BUFFER_EXCEPTION_CLASSNAME)
+ .addComment("TODO: Deal with exception")
+ .endControlFlow()
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("writeToParcel")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(PARCEL_CLASSNAME, "dest")
+ .addParameter(int.class, "flags")
+ .beginControlFlow("if (proto == null)")
+ .addStatement("dest.writeInt(NULL_SIZE)")
+ .addStatement("return")
+ .endControlFlow()
+ .addStatement("byte[] protoBytes = proto.toByteArray()")
+ .addStatement("dest.writeInt(protoBytes.length)")
+ .addStatement("dest.writeByteArray(protoBytes)")
+ .build());
+
+ generatorUtilities.addDefaultParcelableMethods(
+ classBuilder, parcelableWrapper.wrapperClassName());
+
+ generatorUtilities.writeClassToFile(
+ parcelableWrapper.wrapperClassName().packageName(), classBuilder);
+ }
+
+ public static ClassName getGeneratedProtoWrapperClassName(TypeMirror type) {
+ String simpleName = type.toString().substring(type.toString().lastIndexOf(".") + 1);
+ return ClassName.get(GENERATED_PARCELABLE_WRAPPER_PACKAGE, simpleName + "Wrapper");
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProviderClassCodeGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProviderClassCodeGenerator.java
new file mode 100644
index 0000000..06f6b71
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProviderClassCodeGenerator.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext;
+import com.google.android.enterprise.connectedapps.processor.containers.ProviderClassInfo;
+
+/** Generator of code for a single provider class. */
+class ProviderClassCodeGenerator {
+ private boolean generated = false;
+
+ private final GeneratorContext generatorContext;
+ private final InternalProviderClassGenerator internalProviderClassGenerator;
+ private final ProviderClassInfo providerClass;
+
+ ProviderClassCodeGenerator(GeneratorContext generatorContext, ProviderClassInfo providerClass) {
+ this.generatorContext = checkNotNull(generatorContext);
+ this.providerClass = checkNotNull(providerClass);
+ this.internalProviderClassGenerator =
+ new InternalProviderClassGenerator(generatorContext, providerClass);
+ }
+
+ void generate() {
+ if (generated) {
+ throw new IllegalStateException(
+ "ProviderClassCodeGenerator#generate can only be called once");
+ }
+ generated = true;
+
+ internalProviderClassGenerator.generate();
+
+ for (CrossProfileTypeInfo crossProfileType : providerClass.allCrossProfileTypes()) {
+ new CrossProfileTypeCodeGenerator(generatorContext, providerClass, crossProfileType)
+ .generate();
+ }
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ServiceGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ServiceGenerator.java
new file mode 100644
index 0000000..28fa128
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ServiceGenerator.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.BINDER_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CROSSPROFILESERVICE_STUB_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CROSS_PROFILE_CALLBACK_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.INTENT_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.DispatcherGenerator.getDispatcherClassName;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext;
+import com.squareup.javapoet.ArrayTypeName;
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.FieldSpec;
+import com.squareup.javapoet.MethodSpec;
+import com.squareup.javapoet.TypeSpec;
+import javax.lang.model.element.Modifier;
+
+/**
+ * Generate the {@code *_Service} class for a single {@link CrossProfileConfiguration} annotated
+ * class.
+ *
+ * <p>This class includes the dispatch of calls to providers.
+ *
+ * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to
+ * validate that the annotated code is correct.
+ */
+final class ServiceGenerator {
+
+ private boolean generated = false;
+ private final GeneratorContext generatorContext;
+ private final GeneratorUtilities generatorUtilities;
+ private final CrossProfileConfigurationInfo configuration;
+
+ ServiceGenerator(GeneratorContext generatorContext, CrossProfileConfigurationInfo configuration) {
+ this.generatorContext = checkNotNull(generatorContext);
+ this.generatorUtilities = new GeneratorUtilities(generatorContext);
+ this.configuration = checkNotNull(configuration);
+ }
+
+ void generate() {
+ if (generated) {
+ throw new IllegalStateException("ServiceGenerator#generate can only be called once");
+ }
+ generated = true;
+
+ if (configuration.serviceClass().isPresent()) {
+ // Using a pre-existing service
+ return;
+ }
+
+ generateServiceClass();
+ }
+
+ private void generateServiceClass() {
+ ClassName className = getConnectedAppsServiceClassName(generatorContext, configuration);
+
+ TypeSpec.Builder classBuilder =
+ TypeSpec.classBuilder(className)
+ .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
+ .superclass(configuration.serviceSuperclass())
+ .addJavadoc(
+ "Generated Service for {@link $T}\n\n"
+ + "<p>This is bound to by {@link $T} to make cross-profile calls.\n\n"
+ + "<p>This primarily forwards calls to {@link $T}\n\n"
+ + "<p>This service must be exposed in a <service> tag in your"
+ + " AndroidManifest.xml\n",
+ configuration.configurationElement(),
+ configuration.profileConnector().connectorClassName(),
+ getDispatcherClassName(generatorContext, configuration));
+
+ addBinder(classBuilder);
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("onBind")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(BINDER_CLASSNAME)
+ .addParameter(INTENT_CLASSNAME, "intent")
+ .addStatement("return binder")
+ .build());
+
+ generatorUtilities.writeClassToFile(className.packageName(), classBuilder);
+ }
+
+ private void addBinder(TypeSpec.Builder classBuilder) {
+ TypeSpec.Builder binderBuilder =
+ TypeSpec.anonymousClassBuilder("")
+ .addSuperinterface(CROSSPROFILESERVICE_STUB_CLASSNAME)
+ .addField(
+ FieldSpec.builder(
+ getDispatcherClassName(generatorContext, configuration), "dispatcher")
+ .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
+ .initializer(
+ "new $T()", getDispatcherClassName(generatorContext, configuration))
+ .build());
+
+ addPrepareCallMethod(binderBuilder);
+ addCallMethod(binderBuilder);
+ addFetchResponseMethod(binderBuilder);
+
+ classBuilder.addField(
+ FieldSpec.builder(CROSSPROFILESERVICE_STUB_CLASSNAME, "binder", Modifier.PRIVATE)
+ .initializer("$L", binderBuilder.build())
+ .build());
+ }
+
+ private static void addPrepareCallMethod(TypeSpec.Builder classBuilder) {
+ MethodSpec prepareCallMethod =
+ MethodSpec.methodBuilder("prepareCall")
+ .addModifiers(Modifier.PUBLIC)
+ .addAnnotation(Override.class)
+ .addParameter(long.class, "callId")
+ .addParameter(int.class, "blockId")
+ .addParameter(int.class, "numBytes")
+ .addParameter(ArrayTypeName.of(byte.class), "paramBytes")
+ .addStatement(
+ "dispatcher.prepareCall(getApplicationContext(), callId, blockId, numBytes,"
+ + " paramBytes)")
+ .build();
+ classBuilder.addMethod(prepareCallMethod);
+ }
+
+ private static void addCallMethod(TypeSpec.Builder classBuilder) {
+ MethodSpec callMethod =
+ MethodSpec.methodBuilder("call")
+ .addModifiers(Modifier.PUBLIC)
+ .addAnnotation(Override.class)
+ .returns(ArrayTypeName.of(byte.class))
+ .addParameter(long.class, "callId")
+ .addParameter(int.class, "blockId")
+ .addParameter(long.class, "crossProfileTypeIdentifier")
+ .addParameter(int.class, "methodIdentifier")
+ .addParameter(ArrayTypeName.of(byte.class), "paramBytes")
+ .addParameter(CROSS_PROFILE_CALLBACK_CLASSNAME, "callback")
+ .addStatement(
+ "return dispatcher.call(getApplicationContext(),"
+ + "callId, blockId, crossProfileTypeIdentifier, methodIdentifier, paramBytes,"
+ + " callback)")
+ .build();
+ classBuilder.addMethod(callMethod);
+ }
+
+ private static void addFetchResponseMethod(TypeSpec.Builder classBuilder) {
+ MethodSpec prepareCallMethod =
+ MethodSpec.methodBuilder("fetchResponse")
+ .addModifiers(Modifier.PUBLIC)
+ .addAnnotation(Override.class)
+ .addParameter(long.class, "callId")
+ .addParameter(int.class, "blockId")
+ .returns(ArrayTypeName.of(byte.class))
+ .addStatement(
+ "return dispatcher.fetchResponse(getApplicationContext(), callId, blockId)")
+ .build();
+ classBuilder.addMethod(prepareCallMethod);
+ }
+
+ static ClassName getConnectedAppsServiceClassName(
+ GeneratorContext generatorContext, CrossProfileConfigurationInfo configuration) {
+ return configuration.profileConnector().serviceName();
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/SupportedTypes.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/SupportedTypes.java
new file mode 100644
index 0000000..1238b4b
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/SupportedTypes.java
@@ -0,0 +1,844 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.hasCrossProfileCallbackAnnotation;
+
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackInterfaceInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.FutureWrapper;
+import com.google.android.enterprise.connectedapps.processor.containers.ParcelableWrapper;
+import com.google.android.enterprise.connectedapps.processor.containers.Type;
+import com.google.android.enterprise.connectedapps.processor.containers.ValidatorContext;
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableMap;
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.CodeBlock;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Utility methods for generating code related to valid types for use with the Connected Apps SDK.
+ */
+public final class SupportedTypes {
+
+ @Override
+ public String toString() {
+ return "SupportedTypes{" +
+ "usableTypes=" + usableTypes +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ SupportedTypes that = (SupportedTypes) o;
+ return usableTypes.equals(that.usableTypes);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(usableTypes);
+ }
+
+ /** Record of the current context for type checking. */
+ @AutoValue
+ public abstract static class TypeCheckContext {
+
+ /** True if we are checking inside a generic type or an array. */
+ public abstract boolean isWrapped();
+
+ public abstract boolean isOnCrossProfileCallbackInterface();
+
+ public abstract Builder toBuilder();
+
+ public static TypeCheckContext create() {
+ return new AutoValue_SupportedTypes_TypeCheckContext.Builder()
+ .setWrapped(false)
+ .setOnCrossProfileCallbackInterface(false)
+ .build();
+ }
+
+ public static TypeCheckContext createForCrossProfileCallbackInterface() {
+ return new AutoValue_SupportedTypes_TypeCheckContext.Builder()
+ .setWrapped(false)
+ .setOnCrossProfileCallbackInterface(true)
+ .build();
+ }
+
+ @AutoValue.Builder
+ abstract static class Builder {
+ abstract Builder setWrapped(boolean wrapped);
+
+ abstract Builder setOnCrossProfileCallbackInterface(boolean onCrossProfileCallbackInterface);
+
+ abstract TypeCheckContext build();
+ }
+ }
+
+ private final ImmutableMap<String, Type> usableTypes;
+
+ public boolean isFuture(TypeMirror type) {
+ Type supportedType = get(type);
+ return supportedType != null && supportedType.isFuture();
+ }
+
+ boolean isValidReturnType(TypeMirror type) {
+ return isValidReturnType(type, TypeCheckContext.create());
+ }
+
+ private boolean isValidReturnType(TypeMirror type, TypeCheckContext context) {
+ if (TypeUtils.isArray(type)) {
+ TypeMirror wrappedType = TypeUtils.extractTypeFromArray(type);
+ if (TypeUtils.isGeneric(wrappedType)) {
+ return false; // We don't support generic arrays
+ }
+ if (wrappedType.getKind().isPrimitive()) {
+ return false; // We don't support primitive arrays
+ }
+ if (TypeUtils.isArray(wrappedType)) {
+ return false; // We don't support multidimensional arrays
+ }
+ return isValidReturnType(wrappedType, context);
+ }
+
+ return TypeUtils.isGeneric(type)
+ ? isValidGenericReturnType(type, context)
+ : isValidReturnType(get(type), context);
+ }
+
+ private static boolean isValidReturnType(@Nullable Type supportedType, TypeCheckContext context) {
+ if (supportedType == null) {
+ return false;
+ }
+
+ if (context.isWrapped() && !supportedType.isSupportedInsideWrapper()) {
+ return false;
+ }
+
+ return supportedType.isAcceptableReturnType();
+ }
+
+ private boolean isValidGenericReturnType(TypeMirror type, TypeCheckContext context) {
+ TypeMirror genericType = TypeUtils.removeTypeArguments(type);
+ Type supportedType = get(genericType);
+
+ if (supportedType == null) {
+ return false;
+ }
+
+ if (!supportedType.isSupportedWithAnyGenericType()) {
+ // We need to recursively check all type arguments
+ for (TypeMirror typeArgument : TypeUtils.extractTypeArguments(type)) {
+ if (!isValidReturnType(typeArgument, context.toBuilder().setWrapped(true).build())) {
+ return false;
+ }
+ }
+ }
+
+ return isValidReturnType(supportedType, context);
+ }
+
+ /**
+ * Returns true if this type is automatically resolved.
+ *
+ * <p>An automatically resolved type does not need to have a value provided by the developer at
+ * runtime, and should instead use the value of
+ * {@link #getAutomaticallyResolvedReplacement(TypeMirror)}.
+ */
+ public boolean isAutomaticallyResolved(TypeMirror type) {
+ Type supportedType = get(type);
+ return supportedType != null && supportedType.getAutomaticallyResolvedReplacement().isPresent();
+ }
+
+ public String getAutomaticallyResolvedReplacement(TypeMirror type) {
+ Type supportedType = get(type);
+ return supportedType.getAutomaticallyResolvedReplacement().get();
+ }
+
+ boolean isValidParameterType(TypeMirror type) {
+ return isValidParameterType(type, TypeCheckContext.create());
+ }
+
+ boolean isValidParameterType(TypeMirror type, TypeCheckContext context) {
+ if (TypeUtils.isArray(type)) {
+ TypeMirror wrappedType = TypeUtils.extractTypeFromArray(type);
+ if (TypeUtils.isGeneric(wrappedType)) {
+ return false; // We don't support generic arrays
+ }
+ if (wrappedType.getKind().isPrimitive()) {
+ return false; // We don't support primitive arrays
+ }
+ if (TypeUtils.isArray(wrappedType)) {
+ return false; // We don't support multidimensional arrays
+ }
+ return isValidParameterType(wrappedType, context.toBuilder().setWrapped(true).build());
+ }
+
+ Type supportedType = get(TypeUtils.removeTypeArguments(type));
+ if (context.isOnCrossProfileCallbackInterface()) {
+ if (supportedType != null && !supportedType.isSupportedInsideCrossProfileCallback()) {
+ return false;
+ }
+ }
+
+ if (context.isWrapped()) {
+ if (supportedType == null || !supportedType.isSupportedInsideWrapper()) {
+ return false;
+ }
+ }
+
+ return TypeUtils.isGeneric(type)
+ ? isValidGenericParameterType(type, context)
+ : isValidParameterType(get(type));
+ }
+
+ private static boolean isValidParameterType(Type supportedType) {
+ return supportedType != null && supportedType.isAcceptableParameterType();
+ }
+
+ private boolean isValidGenericParameterType(TypeMirror type, TypeCheckContext context) {
+ TypeMirror genericType = TypeUtils.removeTypeArguments(type);
+ Type supportedType = get(genericType);
+
+ if (supportedType == null) {
+ return false;
+ }
+
+ if (!supportedType.isSupportedWithAnyGenericType()) {
+ // We need to recursively check all type arguments
+ for (TypeMirror typeArgument : TypeUtils.extractTypeArguments(type)) {
+ if (!isValidParameterType(typeArgument, context.toBuilder().setWrapped(true).build())) {
+ return false;
+ }
+ }
+ }
+
+ return isValidParameterType(supportedType);
+ }
+
+ ImmutableCollection<Type> usableTypes() {
+ return usableTypes.values();
+ }
+
+ private Type get(TypeMirror type) {
+ return usableTypes.getOrDefault(type.toString(), null);
+ }
+
+ CodeBlock generateWriteToParcelCode(String parcelName, Type type, String valueCode) {
+ if (type.getWriteToParcelCode().isPresent()) {
+ return CodeBlock.of(type.getWriteToParcelCode().get(), parcelName, valueCode);
+ }
+
+ throw new IllegalArgumentException(
+ String.format("%s can not write to parcel", type.getQualifiedName()));
+ }
+
+ CodeBlock generateReadFromParcelCode(String parcelName, Type type) {
+ if (type.getReadFromParcelCode().isPresent()) {
+ return CodeBlock.of(type.getReadFromParcelCode().get(), parcelName);
+ }
+
+ throw new IllegalArgumentException(
+ String.format("%s can not read from parcel", type.getQualifiedName()));
+ }
+
+ public Type getType(TypeMirror type) {
+ String typeName = type.toString();
+ if (!usableTypes.containsKey(typeName)) {
+ throw new IllegalArgumentException(String.format("%s type not loaded", type));
+ }
+
+ return get(type);
+ }
+
+ private SupportedTypes(Map<String, Type> usableTypes) {
+ this.usableTypes = ImmutableMap.copyOf(usableTypes);
+ }
+
+ public static SupportedTypes createFromMethods(
+ Types types,
+ Elements elements,
+ Collection<ParcelableWrapper> parcelableWrappers,
+ Collection<FutureWrapper> futureWrappers,
+ Collection<ExecutableElement> methods) {
+ Map<String, Type> usableTypes = new HashMap<>();
+
+ addDefaultTypes(types, elements, usableTypes);
+ addParcelableWrapperTypes(usableTypes, parcelableWrappers);
+ addFutureWrapperTypes(usableTypes, futureWrappers);
+ addSupportForUsedTypes(types, elements, usableTypes, methods);
+
+ return new SupportedTypes(usableTypes);
+ }
+
+ private static void addSupportForUsedTypes(
+ Types types,
+ Elements elements,
+ Map<String, Type> usableTypes,
+ Collection<ExecutableElement> methods) {
+ for (ExecutableElement method : methods) {
+ addSupportForUsedType(types, elements, usableTypes, method.getReturnType());
+
+ for (VariableElement parameter : method.getParameters()) {
+ addSupportForUsedType(types, elements, usableTypes, parameter.asType());
+ }
+ }
+ }
+
+ private static void addSupportForUsedType(
+ Types types, Elements elements, Map<String, Type> usableTypes, TypeMirror type) {
+ if (TypeUtils.isArray(type)) {
+ addSupportForUsedType(types, elements, usableTypes, TypeUtils.extractTypeFromArray(type));
+ if (!TypeUtils.extractTypeFromArray(type).getKind().isPrimitive()) {
+ type = types.getArrayType(elements.getTypeElement("java.lang.Object").asType());
+ }
+ }
+
+
+ if (TypeUtils.isGeneric(type)) {
+ addSupportForGenericUsedType(types, elements, usableTypes, type);
+ return;
+ }
+ Optional<Type> optionalSupportedType = getSupportedType(types, elements, usableTypes, type);
+ if (!optionalSupportedType.isPresent()) {
+ // The type isn't supported
+ return;
+ }
+
+ Type supportedType = optionalSupportedType.get();
+
+ // We don't support generic callbacks so any callback interfaces can be picked up here
+ if (supportedType.isCrossProfileCallbackInterface()) {
+ for (TypeMirror typeMirror :
+ supportedType.getCrossProfileCallbackInterface().get().argumentTypes()) {
+ addSupportForUsedType(types, elements, usableTypes, typeMirror);
+ }
+ }
+
+ addUsableType(usableTypes, supportedType);
+ }
+
+ private static void addSupportForGenericUsedType(
+ Types types, Elements elements, Map<String, Type> usableTypes, TypeMirror type) {
+ TypeMirror genericType = TypeUtils.removeTypeArguments(type);
+
+ Optional<Type> optionalSupportedType =
+ getSupportedType(types, elements, usableTypes, genericType);
+ if (!optionalSupportedType.isPresent()) {
+ // The base type isn't supported
+ return;
+ }
+
+ Type supportedType = optionalSupportedType.get();
+
+ addUsableType(usableTypes, supportedType);
+
+ if (!supportedType.isSupportedWithAnyGenericType()) {
+ for (TypeMirror typeArgument : TypeUtils.extractTypeArguments(type)) {
+ addSupportForUsedType(types, elements, usableTypes, typeArgument);
+ }
+ }
+ }
+
+ private static Optional<Type> getSupportedType(
+ Types types, Elements elements, Map<String, Type> usableTypes, TypeMirror type) {
+ if (usableTypes.containsKey(type.toString())) {
+ return Optional.of(usableTypes.get(type.toString()));
+ }
+
+ TypeMirror parcelable = elements.getTypeElement("android.os.Parcelable").asType();
+ if (types.isAssignable(type, parcelable)) {
+ return Optional.of(createParcelableType(type));
+ }
+
+ TypeMirror serializable = elements.getTypeElement("java.io.Serializable").asType();
+ if (types.isAssignable(type, serializable)) {
+ return Optional.of(createSerializableType(type));
+ }
+
+ TypeElement element = elements.getTypeElement(type.toString());
+
+ if (element != null && hasCrossProfileCallbackAnnotation(element)) {
+ return Optional.of(createCrossProfileCallbackType(element));
+ }
+
+ // We don't support this type - it will error in a later stage
+ return Optional.empty();
+ }
+
+ private static Type createCrossProfileCallbackType(TypeElement type) {
+ return Type.builder()
+ .setTypeMirror(type.asType())
+ .setAcceptableReturnType(false)
+ .setAcceptableParameterType(true)
+ .setSupportedInsideWrapper(false)
+ .setSupportedInsideCrossProfileCallback(false)
+ .setCrossProfileCallbackInterface(CrossProfileCallbackInterfaceInfo.create(type))
+ .build();
+ }
+
+ private static Type createParcelableType(TypeMirror typeMirror) {
+ return Type.builder()
+ .setTypeMirror(typeMirror)
+ .setAcceptableReturnType(true)
+ .setAcceptableParameterType(true)
+ .setWriteToParcelCode("$L.writeParcelable($L, flags)")
+ .setReadFromParcelCode("$L.readParcelable(Bundler.class.getClassLoader())")
+ // Parcelables must take care of their own generic types
+ .setSupportedWithAnyGenericType(true)
+ .build();
+ }
+
+ private static Type createSerializableType(TypeMirror typeMirror) {
+ return Type.builder()
+ .setTypeMirror(typeMirror)
+ .setAcceptableReturnType(true)
+ .setAcceptableParameterType(true)
+ .setWriteToParcelCode("$L.writeSerializable($L)")
+ .setReadFromParcelCode("$L.readSerializable()")
+ // Serializables must take care of their own generic types
+ .setSupportedWithAnyGenericType(true)
+ .build();
+ }
+
+ /** Create a {@link Builder} to create a new {@link SupportedTypes} with modified entries. */
+ public Builder asBuilder() {
+ return new Builder(usableTypes);
+ }
+
+ private static void addDefaultTypes(
+ Types types, Elements elements, Map<String, Type> usableTypes) {
+ addUsableType(
+ usableTypes,
+ Type.builder()
+ .setTypeMirror(types.getNoType(TypeKind.VOID))
+ .setAcceptableReturnType(true)
+ .setReadFromParcelCode("null")
+ .build());
+ addUsableType(
+ usableTypes,
+ Type.builder()
+ .setTypeMirror(elements.getTypeElement("java.lang.Void").asType())
+ .setAcceptableReturnType(true)
+ .setReadFromParcelCode("null")
+ .build());
+ addUsableType(
+ usableTypes,
+ Type.builder()
+ .setTypeMirror(elements.getTypeElement("java.lang.String").asType())
+ .setAcceptableReturnType(true)
+ .setAcceptableParameterType(true)
+ .setWriteToParcelCode("$L.writeString($L)")
+ .setReadFromParcelCode("$L.readString()")
+ .build());
+ addUsableType(
+ usableTypes,
+ Type.builder()
+ .setTypeMirror(types.getPrimitiveType(TypeKind.BYTE))
+ .setAcceptableReturnType(true)
+ .setAcceptableParameterType(true)
+ .setWriteToParcelCode("$L.writeByte($L)")
+ .setReadFromParcelCode("$L.readByte()")
+ .build());
+ addUsableType(
+ usableTypes,
+ Type.builder()
+ .setTypeMirror(types.boxedClass(types.getPrimitiveType(TypeKind.BYTE)).asType())
+ .setAcceptableReturnType(true)
+ .setAcceptableParameterType(true)
+ .setWriteToParcelCode("$L.writeByte($L)")
+ .setReadFromParcelCode("$L.readByte()")
+ .build());
+ addUsableType(
+ usableTypes,
+ Type.builder()
+ .setTypeMirror(types.getPrimitiveType(TypeKind.SHORT))
+ .setAcceptableReturnType(true)
+ .setAcceptableParameterType(true)
+ .setWriteToParcelCode("$L.writeInt($L)")
+ .setReadFromParcelCode("(short)$L.readInt()")
+ .build());
+ addUsableType(
+ usableTypes,
+ Type.builder()
+ .setTypeMirror(types.boxedClass(types.getPrimitiveType(TypeKind.SHORT)).asType())
+ .setAcceptableReturnType(true)
+ .setAcceptableParameterType(true)
+ .setWriteToParcelCode("$L.writeInt($L)")
+ .setReadFromParcelCode("(short)$L.readInt()")
+ .build());
+ addUsableType(
+ usableTypes,
+ Type.builder()
+ .setTypeMirror(types.getPrimitiveType(TypeKind.INT))
+ .setAcceptableReturnType(true)
+ .setAcceptableParameterType(true)
+ .setWriteToParcelCode("$L.writeInt($L)")
+ .setReadFromParcelCode("$L.readInt()")
+ .build());
+ addUsableType(
+ usableTypes,
+ Type.builder()
+ .setTypeMirror(types.boxedClass(types.getPrimitiveType(TypeKind.INT)).asType())
+ .setAcceptableReturnType(true)
+ .setAcceptableParameterType(true)
+ .setWriteToParcelCode("$L.writeInt($L)")
+ .setReadFromParcelCode("$L.readInt()")
+ .build());
+ addUsableType(
+ usableTypes,
+ Type.builder()
+ .setTypeMirror(types.getPrimitiveType(TypeKind.LONG))
+ .setAcceptableReturnType(true)
+ .setAcceptableParameterType(true)
+ .setWriteToParcelCode("$L.writeLong($L)")
+ .setReadFromParcelCode("$L.readLong()")
+ .build());
+ addUsableType(
+ usableTypes,
+ Type.builder()
+ .setTypeMirror(types.boxedClass(types.getPrimitiveType(TypeKind.LONG)).asType())
+ .setAcceptableReturnType(true)
+ .setAcceptableParameterType(true)
+ .setWriteToParcelCode("$L.writeLong($L)")
+ .setReadFromParcelCode("$L.readLong()")
+ .build());
+ addUsableType(
+ usableTypes,
+ Type.builder()
+ .setTypeMirror(types.getPrimitiveType(TypeKind.FLOAT))
+ .setAcceptableReturnType(true)
+ .setAcceptableParameterType(true)
+ .setWriteToParcelCode("$L.writeFloat($L)")
+ .setReadFromParcelCode("$L.readFloat()")
+ .build());
+ addUsableType(
+ usableTypes,
+ Type.builder()
+ .setTypeMirror(types.boxedClass(types.getPrimitiveType(TypeKind.FLOAT)).asType())
+ .setAcceptableReturnType(true)
+ .setAcceptableParameterType(true)
+ .setWriteToParcelCode("$L.writeFloat($L)")
+ .setReadFromParcelCode("$L.readFloat()")
+ .build());
+ addUsableType(
+ usableTypes,
+ Type.builder()
+ .setTypeMirror(types.getPrimitiveType(TypeKind.DOUBLE))
+ .setAcceptableReturnType(true)
+ .setAcceptableParameterType(true)
+ .setWriteToParcelCode("$L.writeDouble($L)")
+ .setReadFromParcelCode("$L.readDouble()")
+ .build());
+ addUsableType(
+ usableTypes,
+ Type.builder()
+ .setTypeMirror(types.boxedClass(types.getPrimitiveType(TypeKind.DOUBLE)).asType())
+ .setAcceptableReturnType(true)
+ .setAcceptableParameterType(true)
+ .setWriteToParcelCode("$L.writeDouble($L)")
+ .setReadFromParcelCode("$L.readDouble()")
+ .build());
+ addUsableType(
+ usableTypes,
+ Type.builder()
+ .setTypeMirror(types.getPrimitiveType(TypeKind.CHAR))
+ .setAcceptableReturnType(true)
+ .setAcceptableParameterType(true)
+ .setWriteToParcelCode("$L.writeInt($L)")
+ .setReadFromParcelCode("(char)$L.readInt()")
+ .build());
+ addUsableType(
+ usableTypes,
+ Type.builder()
+ .setTypeMirror(types.boxedClass(types.getPrimitiveType(TypeKind.CHAR)).asType())
+ .setAcceptableReturnType(true)
+ .setAcceptableParameterType(true)
+ .setWriteToParcelCode("$L.writeInt($L)")
+ .setReadFromParcelCode("(char)$L.readInt()")
+ .build());
+ addUsableType(
+ usableTypes,
+ Type.builder()
+ .setTypeMirror(types.getPrimitiveType(TypeKind.BOOLEAN))
+ .setAcceptableReturnType(true)
+ .setAcceptableParameterType(true)
+ .setWriteToParcelCode("$L.writeInt($L ? 1 : 0)")
+ .setReadFromParcelCode("($L.readInt() == 1)")
+ .build());
+ addUsableType(
+ usableTypes,
+ Type.builder()
+ .setTypeMirror(types.boxedClass(types.getPrimitiveType(TypeKind.BOOLEAN)).asType())
+ .setAcceptableReturnType(true)
+ .setAcceptableParameterType(true)
+ .setWriteToParcelCode("$L.writeInt($L ? 1 : 0)")
+ .setReadFromParcelCode("($L.readInt() == 1)")
+ .build());
+ addUsableType(
+ usableTypes,
+ Type.builder()
+ .setTypeMirror(elements.getTypeElement("android.content.Context").asType())
+ .setAcceptableParameterType(true)
+ .setAutomaticallyResolvedReplacement("context")
+ .setAcceptableReturnType(false)
+ .setSupportedInsideWrapper(false)
+ .setSupportedInsideCrossProfileCallback(false)
+ .build());
+ }
+
+ private static void addUsableType(Map<String, Type> usableTypes, Type type) {
+ usableTypes.put(type.getQualifiedName(), type);
+ }
+
+ private static void addParcelableWrapperTypes(
+ Map<String, Type> usableTypes, Collection<ParcelableWrapper> parcelableWrappers) {
+ for (ParcelableWrapper parcelableWrapper : parcelableWrappers) {
+ addParcelableWrapperType(usableTypes, parcelableWrapper);
+ }
+ }
+
+ private static void addParcelableWrapperType(
+ Map<String, Type> usableTypes, ParcelableWrapper parcelableWrapper) {
+ String createParcelableCode = parcelableWrapper.wrapperClassName() + ".of(this, valueType, $L)";
+ // "this" will be a Bundler as this code is only run within a Bundler
+
+ addUsableType(
+ usableTypes,
+ Type.builder()
+ .setTypeMirror(parcelableWrapper.wrappedType())
+ .setAcceptableReturnType(true)
+ .setAcceptableParameterType(true)
+ .setWriteToParcelCode("$L.writeParcelable(" + createParcelableCode + ", flags)")
+ .setReadFromParcelCode(
+ "(("
+ + parcelableWrapper.wrapperClassName()
+ + ") $L.readParcelable(Bundler.class.getClassLoader())).get()")
+ .setParcelableWrapper(parcelableWrapper)
+ .build());
+ }
+
+ private static void addFutureWrapperTypes(
+ Map<String, Type> usableTypes, Collection<FutureWrapper> futureWrappers) {
+ for (FutureWrapper futureWrapper : futureWrappers) {
+ addFutureWrapperType(usableTypes, futureWrapper);
+ }
+ }
+
+ private static void addFutureWrapperType(
+ Map<String, Type> usableTypes, FutureWrapper futureWrapper) {
+ addUsableType(
+ usableTypes,
+ Type.builder()
+ .setTypeMirror(futureWrapper.wrappedType())
+ .setAcceptableReturnType(true)
+ .setSupportedInsideWrapper(false)
+ .setFutureWrapper(futureWrapper)
+ .build());
+ }
+
+ public static final class Builder {
+
+ private Map<String, Type> usableTypes;
+
+ private Builder(Map<String, Type> usableTypes) {
+ this.usableTypes = usableTypes;
+ }
+
+ /** Filtering to only include used types. */
+ public Builder filterUsed(
+ ValidatorContext context, Collection<CrossProfileMethodInfo> methods) {
+
+ Map<String, Type> usedTypes = new HashMap<>();
+
+ for (CrossProfileMethodInfo method : methods) {
+ copySupportedTypesForMethod(context, usedTypes, method);
+ }
+
+ this.usableTypes = usedTypes;
+
+ return this;
+ }
+
+ private void copySupportedTypesForMethod(
+ ValidatorContext context, Map<String, Type> usedTypes, CrossProfileMethodInfo method) {
+ copySupportedType(context, usedTypes, method.returnType());
+ for (TypeMirror argumentType : method.parameterTypes()) {
+ copySupportedType(context, usedTypes, argumentType);
+ }
+ }
+
+ private void copySupportedType(
+ ValidatorContext context, Map<String, Type> usedTypes, TypeMirror type) {
+ if (TypeUtils.isGeneric(type)) {
+ copySupportedGenericType(context, usedTypes, type);
+ return;
+ }
+
+ if (TypeUtils.isArray(type)) {
+ copySupportedType(context, usedTypes, TypeUtils.extractTypeFromArray(type));
+ if (!TypeUtils.extractTypeFromArray(type).getKind().isPrimitive()) {
+ type =
+ context
+ .types()
+ .getArrayType(context.elements().getTypeElement("java.lang.Object").asType());
+ }
+ }
+
+ // The type must have been seen in when constructing the original so this should not
+ // be null
+ Type supportedType = usableTypes.get(type.toString());
+
+ // We don't support generic callbacks so any callback interfaces can be picked up here
+ if (supportedType.isCrossProfileCallbackInterface()) {
+ for (TypeMirror typeMirror :
+ supportedType.getCrossProfileCallbackInterface().get().argumentTypes()) {
+ copySupportedType(context, usedTypes, typeMirror);
+ }
+ }
+
+ copySupportedType(usedTypes, supportedType);
+ }
+
+ private void copySupportedType(Map<String, Type> usedTypes, Type supportedType) {
+ addUsableType(usedTypes, supportedType);
+ }
+
+ private void copySupportedGenericType(
+ ValidatorContext context, Map<String, Type> usedTypes, TypeMirror type) {
+ TypeMirror genericType = TypeUtils.removeTypeArguments(type);
+
+ // The type must have been seen in when constructing the oldSupportedTypes so this should not
+ // be null
+ Type supportedType = usableTypes.get(genericType.toString());
+
+ if (!supportedType.isSupportedWithAnyGenericType()) {
+ // We need to recursively copy all type arguments
+ for (TypeMirror typeArgument : TypeUtils.extractTypeArguments(type)) {
+ copySupportedType(context, usedTypes, typeArgument);
+ }
+ }
+
+ copySupportedType(usedTypes, supportedType);
+ }
+
+ /** Add additianal parcelable wrappers. */
+ public Builder addParcelableWrappers(Collection<ParcelableWrapper> parcelableWrappers) {
+ Map<String, Type> newUsableTypes = new HashMap<>(usableTypes);
+
+ addParcelableWrapperTypes(newUsableTypes, parcelableWrappers);
+
+ usableTypes = newUsableTypes;
+
+ return this;
+ }
+
+ /** Add additianal future wrappers. */
+ public Builder addFutureWrappers(Collection<FutureWrapper> futureWrappers) {
+ Map<String, Type> newUsableTypes = new HashMap<>(usableTypes);
+
+ addFutureWrapperTypes(newUsableTypes, futureWrappers);
+
+ usableTypes = newUsableTypes;
+
+ return this;
+ }
+
+ public Builder replaceWrapperPrefix(ClassName prefix) {
+ Map<String, Type> newUsableTypes = new HashMap<>();
+
+ for (Type usableType : usableTypes.values()) {
+ if (usableType.getParcelableWrapper().isPresent()) {
+ replaceParcelableWrapperPrefix(newUsableTypes, prefix, usableType);
+ } else if (usableType.getFutureWrapper().isPresent()) {
+ replaceFutureWrapperPrefix(newUsableTypes, prefix, usableType);
+ } else {
+ addUsableType(newUsableTypes, usableType);
+ }
+ }
+
+ usableTypes = newUsableTypes;
+
+ return this;
+ }
+
+ private void replaceParcelableWrapperPrefix(
+ Map<String, Type> newUsableTypes, ClassName prefix, Type usableType) {
+ ParcelableWrapper parcelableWrapper = usableType.getParcelableWrapper().get();
+
+ if (parcelableWrapper.wrapperType().equals(ParcelableWrapper.WrapperType.CUSTOM)) {
+ // Custom types never get prefixed
+ addUsableType(newUsableTypes, usableType);
+ return;
+ }
+
+ addParcelableWrapperType(
+ newUsableTypes,
+ ParcelableWrapper.create(
+ parcelableWrapper.wrappedType(),
+ parcelableWrapper.defaultWrapperClassName(),
+ prefix(prefix, parcelableWrapper.wrapperClassName()),
+ parcelableWrapper.wrapperType()));
+ }
+
+ private void replaceFutureWrapperPrefix(
+ Map<String, Type> newUsableTypes, ClassName prefix, Type usableType) {
+ FutureWrapper futureWrapper = usableType.getFutureWrapper().get();
+
+ if (futureWrapper.wrapperType().equals(FutureWrapper.WrapperType.CUSTOM)) {
+ // Custom types never get prefixed
+ addUsableType(newUsableTypes, usableType);
+ return;
+ }
+
+ addFutureWrapperType(
+ newUsableTypes,
+ FutureWrapper.create(
+ futureWrapper.wrappedType(),
+ futureWrapper.defaultWrapperClassName(),
+ prefix(prefix, futureWrapper.wrapperClassName()),
+ futureWrapper.wrapperType()));
+ }
+
+ private ClassName prefix(ClassName prefix, ClassName finalName) {
+ return ClassName.get(
+ prefix.packageName(), prefix.simpleName() + "_" + finalName.simpleName());
+ }
+
+ /** Build a new {@link SupportedTypes}. */
+ public SupportedTypes build() {
+ return new SupportedTypes(usableTypes);
+ }
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/TestCodeGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/TestCodeGenerator.java
new file mode 100644
index 0000000..77483f5
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/TestCodeGenerator.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTestInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext;
+import com.google.android.enterprise.connectedapps.processor.containers.ProfileConnectorInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.ProviderClassInfo;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Generator of cross-profile test code.
+ *
+ * <p>This is intended to be initialised and used once, which will generate all needed code.
+ *
+ * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to
+ * validate that the annotated code is correct.
+ */
+final class TestCodeGenerator {
+ private boolean generated = false;
+ private final GeneratorContext generatorContext;
+ private final Set<CrossProfileTypeInfo> fakedTypes = new HashSet<>();
+ private final Set<ProfileConnectorInfo> fakedConnectors = new HashSet<>();
+
+ TestCodeGenerator(GeneratorContext generatorContext) {
+ this.generatorContext = checkNotNull(generatorContext);
+ }
+
+ void generate() {
+ if (generated) {
+ throw new IllegalStateException("TestCodeGenerator#generate can only be called once");
+ }
+ generated = true;
+
+ collectTestTypes();
+ generateFakes();
+ }
+
+ private void generateFakes() {
+ for (ProfileConnectorInfo connector : fakedConnectors) {
+ new FakeProfileConnectorGenerator(generatorContext, connector).generate();
+ }
+
+ for (CrossProfileTypeInfo type : fakedTypes) {
+ new FakeCrossProfileTypeGenerator(generatorContext, type).generate();
+ new FakeOtherGenerator(generatorContext, type).generate();
+ }
+ }
+
+ private void collectTestTypes() {
+ for (CrossProfileTestInfo crossProfileTest : generatorContext.crossProfileTests()) {
+ collectTestTypes(crossProfileTest);
+ }
+ }
+
+ private void collectTestTypes(CrossProfileTestInfo crossProfileTest) {
+ for (CrossProfileConfigurationInfo configuration : crossProfileTest.configurations()) {
+ collectTestTypes(configuration);
+ }
+ }
+
+ private void collectTestTypes(CrossProfileConfigurationInfo configuration) {
+ for (ProviderClassInfo provider : configuration.providers()) {
+ collectTestTypes(provider);
+ }
+
+ fakedConnectors.add(configuration.profileConnector());
+ }
+
+ private void collectTestTypes(ProviderClassInfo provider) {
+ for (CrossProfileTypeInfo type : provider.allCrossProfileTypes()) {
+ collectTestTypes(type);
+ }
+ }
+
+ private void collectTestTypes(CrossProfileTypeInfo type) {
+ fakedTypes.add(type);
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/TypeUtils.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/TypeUtils.java
new file mode 100644
index 0000000..6d5d073
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/TypeUtils.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.BUNDLER_TYPE_CLASSNAME;
+import static java.util.stream.Collectors.toList;
+
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.CodeBlock;
+import java.util.ArrayList;
+import java.util.List;
+import javax.lang.model.type.ArrayType;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.TypeMirror;
+
+/** Utilities for manipulating {@link TypeMirror} instances. */
+public class TypeUtils {
+
+ public static boolean isArray(TypeMirror type) {
+ return type instanceof ArrayType;
+ }
+
+ /**
+ * Extract a type from an array.
+ *
+ * <p>Assumes that {@code type} represents an array.
+ */
+ public static TypeMirror extractTypeFromArray(TypeMirror type) {
+ return ((ArrayType) type).getComponentType();
+ }
+
+ public static boolean isGeneric(TypeMirror type) {
+ if (type instanceof DeclaredType) {
+ return !((DeclaredType) type).getTypeArguments().isEmpty();
+ }
+ return false;
+ }
+
+ public static TypeMirror removeTypeArguments(TypeMirror type) {
+ if (type instanceof DeclaredType) {
+ return ((DeclaredType) type).asElement().asType();
+ }
+ return type;
+ }
+
+ public static List<TypeMirror> extractTypeArguments(TypeMirror type) {
+ if (!(type instanceof DeclaredType)) {
+ return null;
+ }
+
+ return new ArrayList<>(((DeclaredType) type).getTypeArguments());
+ }
+
+ static ClassName getRawTypeClassName(TypeMirror type) {
+ String rawTypeQualifiedName = getRawTypeQualifiedName(type);
+
+ if (!rawTypeQualifiedName.contains(".")) {
+ return ClassName.get("", rawTypeQualifiedName);
+ }
+
+ String packageName = rawTypeQualifiedName.substring(0, rawTypeQualifiedName.lastIndexOf("."));
+ String simpleName = rawTypeQualifiedName.substring(rawTypeQualifiedName.lastIndexOf(".") + 1);
+
+ return ClassName.get(packageName, simpleName);
+ }
+
+ static String getRawTypeQualifiedName(TypeMirror type) {
+ // This converts e.g. java.util.List<String> into java.util.List
+ return type.toString().split("<", 2)[0];
+ }
+
+ static CodeBlock generateBundlerType(TypeMirror type) {
+ if (isArray(type)) {
+ return generateArrayBundlerType(type);
+ }
+ if (isGeneric(type)) {
+ return generateGenericBundlerType(type);
+ }
+ return CodeBlock.of("$T.of($S)", BUNDLER_TYPE_CLASSNAME, getRawTypeQualifiedName(type));
+ }
+
+ private static CodeBlock generateArrayBundlerType(TypeMirror type) {
+ TypeMirror arrayType = extractTypeFromArray(type);
+
+ return CodeBlock.of(
+ "$T.of($S, $L)",
+ BUNDLER_TYPE_CLASSNAME,
+ "java.lang.Object[]",
+ generateBundlerType(arrayType));
+ }
+
+ private static CodeBlock generateGenericBundlerType(TypeMirror type) {
+ CodeBlock.Builder typeArgs = CodeBlock.builder();
+
+ List<CodeBlock> typeArgBlocks =
+ extractTypeArguments(type).stream().map(TypeUtils::generateBundlerType).collect(toList());
+
+ typeArgs.add(typeArgBlocks.get(0));
+ for (CodeBlock typeArgBlock : typeArgBlocks.subList(1, typeArgBlocks.size())) {
+ typeArgs.add(", $L", typeArgBlock);
+ }
+
+ return CodeBlock.of(
+ "$T.of($S, $L)", BUNDLER_TYPE_CLASSNAME, getRawTypeQualifiedName(type), typeArgs.build());
+ }
+
+ private TypeUtils() {}
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/UserConnectorCodeGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/UserConnectorCodeGenerator.java
new file mode 100644
index 0000000..415765b
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/UserConnectorCodeGenerator.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.ABSTRACT_USER_CONNECTOR_BUILDER_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.ABSTRACT_USER_CONNECTOR_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.AVAILABILITY_RESTRICTIONS_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CONNECTION_BINDER_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CONTEXT_CLASSNAME;
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.SCHEDULED_EXECUTOR_SERVICE_CLASSNAME;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.android.enterprise.connectedapps.annotations.GeneratedUserConnector;
+import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext;
+import com.google.android.enterprise.connectedapps.processor.containers.UserConnectorInfo;
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.CodeBlock;
+import com.squareup.javapoet.FieldSpec;
+import com.squareup.javapoet.MethodSpec;
+import com.squareup.javapoet.TypeSpec;
+import javax.lang.model.element.Modifier;
+
+/**
+ * Generate the {@code Generated*} class for a single {@link GeneratedUserConnector} annotated
+ * class.
+ *
+ * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to
+ * validate that the annotated code is correct.
+ */
+class UserConnectorCodeGenerator {
+ private boolean generated = false;
+
+ private final GeneratorContext generatorContext;
+ private final GeneratorUtilities generatorUtilities;
+ private final UserConnectorInfo connector;
+
+ UserConnectorCodeGenerator(GeneratorContext generatorContext, UserConnectorInfo connector) {
+ this.generatorContext = checkNotNull(generatorContext);
+ this.generatorUtilities = new GeneratorUtilities(generatorContext);
+ this.connector = checkNotNull(connector);
+ }
+
+ void generate() {
+ if (generated) {
+ throw new IllegalStateException(
+ "ProfileConnectorCodeGenerator#generate can only be called once");
+ }
+ generated = true;
+
+ generateUserConnector();
+ }
+
+ private void generateUserConnector() {
+ ClassName className = getGeneratedUserConnectorClassName(generatorContext, connector);
+ ClassName builderClassName =
+ getGeneratedUserConnectorBuilderClassName(generatorContext, connector);
+
+ TypeSpec.Builder classBuilder =
+ TypeSpec.classBuilder(className)
+ .addJavadoc(
+ "Generated implementation of {@link $T}.\n\n"
+ + "<p>All logic is implemented by {@link $T}.\n",
+ connector.connectorClassName(),
+ ABSTRACT_USER_CONNECTOR_CLASSNAME)
+ .addModifiers(Modifier.FINAL)
+ .addSuperinterface(connector.connectorClassName())
+ .superclass(ABSTRACT_USER_CONNECTOR_CLASSNAME);
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("builder")
+ .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
+ .returns(builderClassName)
+ .addParameter(CONTEXT_CLASSNAME, "context")
+ .addStatement("return new $T(context)", builderClassName)
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.constructorBuilder()
+ .addModifiers(Modifier.PRIVATE)
+ .addParameter(builderClassName, "builder")
+ .addStatement(
+ "super($1T.class, builder.profileConnectorBuilder)", connector.connectorClassName())
+ .build());
+
+ generateUserConnectorBuilder(classBuilder);
+
+ generatorUtilities.writeClassToFile(className.packageName(), classBuilder);
+ }
+
+ private void generateUserConnectorBuilder(TypeSpec.Builder profileConnector) {
+ ClassName connectorClassName = getGeneratedUserConnectorClassName(generatorContext, connector);
+ ClassName builderClassName =
+ getGeneratedUserConnectorBuilderClassName(generatorContext, connector);
+
+ TypeSpec.Builder classBuilder =
+ TypeSpec.classBuilder(builderClassName)
+ .addJavadoc("Builder for {@link $T}.\n", connectorClassName)
+ .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC);
+
+ CodeBlock initialiser =
+ CodeBlock.of(
+ "new $T().setServiceClassName($S).setAvailabilityRestrictions($T.$L)",
+ ABSTRACT_USER_CONNECTOR_BUILDER_CLASSNAME,
+ connector.serviceName().toString(),
+ AVAILABILITY_RESTRICTIONS_CLASSNAME,
+ connector.availabilityRestrictions().name());
+
+ classBuilder.addMethod(
+ MethodSpec.constructorBuilder()
+ .addParameter(CONTEXT_CLASSNAME, "context")
+ .addStatement("profileConnectorBuilder.setContext(context)")
+ .build());
+
+ classBuilder.addField(
+ FieldSpec.builder(ABSTRACT_USER_CONNECTOR_BUILDER_CLASSNAME, "profileConnectorBuilder")
+ .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
+ .initializer(initialiser)
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("setScheduledExecutorService")
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(SCHEDULED_EXECUTOR_SERVICE_CLASSNAME, "scheduledExecutorService")
+ .returns(builderClassName)
+ .addStatement(
+ "profileConnectorBuilder.setScheduledExecutorService(scheduledExecutorService)")
+ .addStatement("return this")
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("setBinder")
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(CONNECTION_BINDER_CLASSNAME, "binder")
+ .returns(builderClassName)
+ .addStatement("profileConnectorBuilder.setBinder(binder)")
+ .addStatement("return this")
+ .build());
+
+ classBuilder.addMethod(
+ MethodSpec.methodBuilder("build")
+ .addModifiers(Modifier.PUBLIC)
+ .returns(connectorClassName)
+ .addStatement("return new $1T(this)", connectorClassName)
+ .build());
+
+ profileConnector.addType(classBuilder.build());
+ }
+
+ static ClassName getGeneratedUserConnectorClassName(
+ GeneratorContext generatorContext, UserConnectorInfo connector) {
+ return ClassName.get(
+ connector.connectorClassName().packageName(),
+ "Generated" + connector.connectorClassName().simpleName());
+ }
+
+ static ClassName getGeneratedUserConnectorBuilderClassName(
+ GeneratorContext generatorContext, UserConnectorInfo connector) {
+ return ClassName.get(
+ connector.connectorClassName().packageName()
+ + "."
+ + "Generated"
+ + connector.connectorClassName().simpleName(),
+ "Builder");
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ValidationMessageFormatter.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ValidationMessageFormatter.java
new file mode 100644
index 0000000..e5f4a01
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ValidationMessageFormatter.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor;
+
+import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationNames;
+
+/** Formats annotation validation messages with the provided names of the annotation set. */
+public final class ValidationMessageFormatter {
+
+ private final AnnotationNames annotationNames;
+
+ public static ValidationMessageFormatter forAnnotations(AnnotationNames annotationNames) {
+ return new ValidationMessageFormatter(annotationNames);
+ }
+
+ private ValidationMessageFormatter(AnnotationNames annotationNames) {
+ this.annotationNames = annotationNames;
+ }
+
+ /**
+ * Supports the replacement strings CROSS_PROFILE_ANNOTATION, CROSS_PROFILE_CALLBACK_ANNOTATION,
+ * CROSS_PROFILE_CONFIGURATION_ANNOTATION, CROSS_PROFILE_CONFIGURATIONS_ANNOTATION,
+ * CROSS_PROFILE_PROVIDER_ANNOTATION, and CROSS_PROFILE_TEST_ANNOTATION.
+ */
+ String format(String message) {
+ return message
+ .replace("CROSS_PROFILE_ANNOTATION", annotationNames.crossProfile())
+ .replace("CROSS_PROFILE_CALLBACK_ANNOTATION", annotationNames.crossProfileCallback())
+ .replace(
+ "CROSS_PROFILE_CONFIGURATION_ANNOTATION", annotationNames.crossProfileConfiguration())
+ .replace(
+ "CROSS_PROFILE_CONFIGURATIONS_ANNOTATION", annotationNames.crossProfileConfigurations())
+ .replace("CROSS_PROFILE_PROVIDER_ANNOTATION", annotationNames.crossProfileProvider())
+ .replace("CROSS_PROFILE_TEST_ANNOTATION", annotationNames.crossProfileTest());
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationClasses.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationClasses.java
new file mode 100644
index 0000000..1616838
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationClasses.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.annotationdiscovery;
+
+import com.google.android.enterprise.connectedapps.annotations.CrossUser;
+import com.google.android.enterprise.connectedapps.annotations.CrossUserCallback;
+import com.google.android.enterprise.connectedapps.annotations.CrossUserProvider;
+import java.lang.annotation.Annotation;
+
+/**
+ * A set of parallel annotation classes.
+ *
+ * <p>For example, a valid instance could return {@link CrossUser}, {@link CrossUserCallback},
+ * {@link CrossUserProvider}, etc.
+ */
+public interface AnnotationClasses {
+
+ Class<? extends Annotation> crossProfileAnnotationClass();
+
+ Class<? extends Annotation> crossProfileCallbackAnnotationClass();
+
+ Class<? extends Annotation> crossProfileConfigurationAnnotationClass();
+
+ Class<? extends Annotation> crossProfileConfigurationsAnnotationClass();
+
+ Class<? extends Annotation> crossProfileProviderAnnotationClass();
+
+ Class<? extends Annotation> crossProfileTestAnnotationClass();
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationFinder.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationFinder.java
new file mode 100644
index 0000000..4923567
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationFinder.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.annotationdiscovery;
+
+import static java.util.stream.Collectors.toSet;
+import static javax.lang.model.element.ElementKind.METHOD;
+
+import com.google.android.enterprise.connectedapps.annotations.CrossProfile;
+import com.google.android.enterprise.connectedapps.annotations.CrossProfileCallback;
+import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration;
+import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfigurations;
+import com.google.android.enterprise.connectedapps.annotations.CrossProfileProvider;
+import com.google.android.enterprise.connectedapps.annotations.CrossUser;
+import com.google.android.enterprise.connectedapps.annotations.CrossUserCallback;
+import com.google.android.enterprise.connectedapps.annotations.CrossUserConfiguration;
+import com.google.android.enterprise.connectedapps.annotations.CrossUserConfigurations;
+import com.google.android.enterprise.connectedapps.annotations.CrossUserProvider;
+import com.google.android.enterprise.connectedapps.processor.ValidationMessageFormatter;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileAnnotationInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackAnnotationInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationAnnotationInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationsAnnotationInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileProviderAnnotationInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTestAnnotationInfo;
+import com.google.android.enterprise.connectedapps.testing.annotations.CrossProfileTest;
+import com.google.android.enterprise.connectedapps.testing.annotations.CrossUserTest;
+import com.google.common.collect.ImmutableList;
+import java.lang.annotation.Annotation;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import javax.annotation.processing.RoundEnvironment;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
+
+/** Helper methods to discover all cross-profile annotations of a specific type on elements. */
+public final class AnnotationFinder {
+
+ private static final AnnotationStrings CROSS_PROFILE_ANNOTATION_STRINGS =
+ AnnotationStrings.builder()
+ .setCrossProfileAnnotationClass(CrossProfile.class)
+ .setCrossProfileCallbackAnnotationClass(CrossProfileCallback.class)
+ .setCrossProfileConfigurationAnnotationClass(CrossProfileConfiguration.class)
+ .setCrossProfileConfigurationsAnnotationClass(CrossProfileConfigurations.class)
+ .setCrossProfileProviderAnnotationClass(CrossProfileProvider.class)
+ .setCrossProfileTestAnnotationClass(CrossProfileTest.class)
+ .build();
+
+ private static final AnnotationStrings CROSS_USER_ANNOTATION_STRINGS =
+ AnnotationStrings.builder()
+ .setCrossProfileAnnotationClass(CrossUser.class)
+ .setCrossProfileCallbackAnnotationClass(CrossUserCallback.class)
+ .setCrossProfileProviderAnnotationClass(CrossUserProvider.class)
+ .setCrossProfileConfigurationAnnotationClass(CrossUserConfiguration.class)
+ .setCrossProfileConfigurationsAnnotationClass(CrossUserConfigurations.class)
+ .setCrossProfileTestAnnotationClass(CrossUserTest.class)
+ .build();
+
+ private static final ImmutableList<AnnotationStrings> SUPPORTED_ANNOTATIONS =
+ ImmutableList.of(CROSS_PROFILE_ANNOTATION_STRINGS, CROSS_USER_ANNOTATION_STRINGS);
+
+ private static final AnnotationStrings DEFAULT_ANNOTATIONS = CROSS_PROFILE_ANNOTATION_STRINGS;
+
+ private static final Set<Class<? extends Annotation>> crossProfileAnnotations =
+ annotationsOfType(AnnotationClasses::crossProfileAnnotationClass);
+
+ private static final Set<Class<? extends Annotation>> crossProfileCallbackAnnotations =
+ annotationsOfType(AnnotationClasses::crossProfileCallbackAnnotationClass);
+
+ private static final Set<Class<? extends Annotation>> crossProfileConfigurationAnnotations =
+ annotationsOfType(AnnotationClasses::crossProfileConfigurationAnnotationClass);
+
+ private static final Set<Class<? extends Annotation>> crossProfileConfigurationsAnnotations =
+ annotationsOfType(AnnotationClasses::crossProfileConfigurationsAnnotationClass);
+
+ private static final Set<Class<? extends Annotation>> crossProfileProviderAnnotations =
+ annotationsOfType(AnnotationClasses::crossProfileProviderAnnotationClass);
+
+ private static final Set<Class<? extends Annotation>> crossProfileTestAnnotations =
+ annotationsOfType(AnnotationClasses::crossProfileTestAnnotationClass);
+
+ public static Iterable<AnnotationStrings> annotationStrings() {
+ return SUPPORTED_ANNOTATIONS;
+ }
+
+ public static AnnotationNames crossProfileAnnotationNames() {
+ return CROSS_PROFILE_ANNOTATION_STRINGS;
+ }
+
+ public static AnnotationNames crossUserAnnotationNames() {
+ return CROSS_USER_ANNOTATION_STRINGS;
+ }
+
+ public static ValidationMessageFormatter validationMessageFormatterFor(Element element) {
+ return ValidationMessageFormatter.forAnnotations(annotationNamesFor(element));
+ }
+
+ private static AnnotationNames annotationNamesFor(Element element) {
+ for (AnnotationStrings annotationStrings : SUPPORTED_ANNOTATIONS) {
+ if (hasAnyAnnotationsOfClass(element, annotationStrings)) {
+ return annotationStrings;
+ }
+ }
+
+ return DEFAULT_ANNOTATIONS;
+ }
+
+ public static ValidationMessageFormatter validationMessageFormatterForClass(
+ TypeElement typeElement) {
+ return ValidationMessageFormatter.forAnnotations(annotationNamesForClass(typeElement));
+ }
+
+ public static AnnotationNames annotationNamesForClass(TypeElement typeElement) {
+ for (AnnotationStrings annotationStrings : SUPPORTED_ANNOTATIONS) {
+ if (hasAnyAnnotationsOfClass(typeElement, annotationStrings)) {
+ return annotationStrings;
+ }
+
+ for (ExecutableElement method :
+ typeElement.getEnclosedElements().stream()
+ .filter(element -> element.getKind() == METHOD)
+ .map(element -> (ExecutableElement) element)
+ .collect(toSet())) {
+ if (hasAnyAnnotationsOfClass(method, annotationStrings)) {
+ return annotationStrings;
+ }
+ }
+ }
+
+ return DEFAULT_ANNOTATIONS;
+ }
+
+ private static boolean hasAnyAnnotationsOfClass(
+ Element element, AnnotationClasses annotationClasses) {
+ return hasAnnotationOfClass(element, annotationClasses.crossProfileAnnotationClass())
+ || hasAnnotationOfClass(element, annotationClasses.crossProfileCallbackAnnotationClass())
+ || hasAnnotationOfClass(element, annotationClasses.crossProfileProviderAnnotationClass())
+ || hasAnnotationOfClass(
+ element, annotationClasses.crossProfileConfigurationAnnotationClass())
+ || hasAnnotationOfClass(
+ element, annotationClasses.crossProfileConfigurationsAnnotationClass())
+ || hasAnnotationOfClass(element, annotationClasses.crossProfileTestAnnotationClass());
+ }
+
+ private static boolean hasAnnotationOfClass(
+ Element element, Class<? extends Annotation> annotationClass) {
+ return element.getAnnotation(annotationClass) != null;
+ }
+
+ public static CrossProfileAnnotationInfo extractCrossProfileAnnotationInfo(
+ Element annotatedElement, Types types, Elements elements) {
+ return new CrossProfileAnnotationInfoExtractor()
+ .extractAnnotationInfo(SUPPORTED_ANNOTATIONS, annotatedElement, types, elements);
+ }
+
+ public static CrossProfileCallbackAnnotationInfo extractCrossProfileCallbackAnnotationInfo(
+ Element annotatedElement, Types types, Elements elements) {
+ return new CrossProfileCallbackAnnotationInfoExtractor()
+ .extractAnnotationInfo(SUPPORTED_ANNOTATIONS, annotatedElement, types, elements);
+ }
+
+ public static CrossProfileConfigurationAnnotationInfo
+ extractCrossProfileConfigurationAnnotationInfo(
+ Element annotatedElement, Types types, Elements elements) {
+ return new CrossProfileConfigurationAnnotationInfoExtractor()
+ .extractAnnotationInfo(SUPPORTED_ANNOTATIONS, annotatedElement, types, elements);
+ }
+
+ public static CrossProfileConfigurationsAnnotationInfo
+ extractCrossProfileConfigurationsAnnotationInfo(
+ Element annotatedElement, Types types, Elements elements) {
+ return new CrossProfileConfigurationsAnnotationInfoExtractor()
+ .extractAnnotationInfo(SUPPORTED_ANNOTATIONS, annotatedElement, types, elements);
+ }
+
+ public static CrossProfileProviderAnnotationInfo extractCrossProfileProviderAnnotationInfo(
+ Element annotatedElement, Types types, Elements elements) {
+ return new CrossProfileProviderAnnotationInfoExtractor()
+ .extractAnnotationInfo(SUPPORTED_ANNOTATIONS, annotatedElement, types, elements);
+ }
+
+ public static CrossProfileTestAnnotationInfo extractCrossProfileTestAnnotationInfo(
+ Element annotatedElement, Types types, Elements elements) {
+ return new CrossProfileTestAnnotationInfoExtractor()
+ .extractAnnotationInfo(SUPPORTED_ANNOTATIONS, annotatedElement, types, elements);
+ }
+
+ public static boolean hasCrossProfileAnnotation(Element element) {
+ return hasAnyAnnotations(element, crossProfileAnnotations);
+ }
+
+ public static boolean hasCrossProfileCallbackAnnotation(Element element) {
+ return hasAnyAnnotations(element, crossProfileCallbackAnnotations);
+ }
+
+ public static boolean hasCrossProfileConfigurationAnnotation(Element element) {
+ return hasAnyAnnotations(element, crossProfileConfigurationAnnotations);
+ }
+
+ public static boolean hasCrossProfileConfigurationsAnnotation(Element element) {
+ return hasAnyAnnotations(element, crossProfileConfigurationsAnnotations);
+ }
+
+ public static boolean hasCrossProfileProviderAnnotation(Element element) {
+ return hasAnyAnnotations(element, crossProfileProviderAnnotations);
+ }
+
+ private static boolean hasAnyAnnotations(
+ Element element, Set<Class<? extends Annotation>> annotations) {
+ return annotations.stream().anyMatch(annotation -> element.getAnnotation(annotation) != null);
+ }
+
+ public static Stream<? extends Element> elementsAnnotatedWithCrossProfile(
+ RoundEnvironment roundEnv) {
+ return findElementsContainingAnnotations(roundEnv, crossProfileAnnotations);
+ }
+
+ public static Stream<? extends Element> elementsAnnotatedWithCrossProfileCallback(
+ RoundEnvironment roundEnv) {
+ return findElementsContainingAnnotations(roundEnv, crossProfileCallbackAnnotations);
+ }
+
+ public static Stream<? extends Element> elementsAnnotatedWithCrossProfileConfiguration(
+ RoundEnvironment roundEnv) {
+ return findElementsContainingAnnotations(roundEnv, crossProfileConfigurationAnnotations);
+ }
+
+ public static Stream<? extends Element> elementsAnnotatedWithCrossProfileConfigurations(
+ RoundEnvironment roundEnv) {
+ return findElementsContainingAnnotations(roundEnv, crossProfileConfigurationsAnnotations);
+ }
+
+ public static Stream<? extends Element> elementsAnnotatedWithCrossProfileProvider(
+ RoundEnvironment roundEnv) {
+ return findElementsContainingAnnotations(roundEnv, crossProfileProviderAnnotations);
+ }
+
+ public static Stream<? extends Element> elementsAnnotatedWithCrossProfileTest(
+ RoundEnvironment roundEnv) {
+ return findElementsContainingAnnotations(roundEnv, crossProfileTestAnnotations);
+ }
+
+ private static Stream<? extends Element> findElementsContainingAnnotations(
+ RoundEnvironment roundEnv, Set<Class<? extends Annotation>> annotations) {
+ return annotations.stream()
+ .flatMap(annotation -> roundEnv.getElementsAnnotatedWith(annotation).stream());
+ }
+
+ private static Set<Class<? extends Annotation>> annotationsOfType(
+ Function<AnnotationClasses, Class<? extends Annotation>> annotationClassGetter) {
+ return SUPPORTED_ANNOTATIONS.stream().map(annotationClassGetter).collect(toSet());
+ }
+
+ private AnnotationFinder() {}
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationInfoExtractor.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationInfoExtractor.java
new file mode 100644
index 0000000..08022c4
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationInfoExtractor.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.annotationdiscovery;
+
+import com.google.android.enterprise.connectedapps.annotations.CrossProfileProvider;
+import com.google.android.enterprise.connectedapps.annotations.CrossUserProvider;
+import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileProviderAnnotation;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Proxy;
+import javax.lang.model.element.Element;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
+
+/**
+ * An extractor which generates {@link AnnotationInfoT} for elements annotated with annotations that
+ * conform to {@link AnnotationInterfaceT}.
+ */
+abstract class AnnotationInfoExtractor<AnnotationInfoT, AnnotationInterfaceT> {
+
+ private final Class<AnnotationInterfaceT> annotationInterfaceClass;
+
+ AnnotationInfoExtractor(Class<AnnotationInterfaceT> annotationInterfaceClass) {
+ this.annotationInterfaceClass = annotationInterfaceClass;
+ }
+
+ /**
+ * Returns the {@link AnnotationInfoT} that can be extracted from the first supported annotation
+ * on {@code annotatedElement}, or a default instance otherwise.
+ */
+ AnnotationInfoT extractAnnotationInfo(
+ Iterable<? extends AnnotationClasses> availableAnnotations,
+ Element annotatedElement,
+ Types types,
+ Elements elements) {
+ for (AnnotationClasses annotationClasses : availableAnnotations) {
+ Annotation annotation =
+ annotatedElement.getAnnotation(supportedAnnotationClass(annotationClasses));
+
+ if (annotation != null) {
+ return annotationInfoFromAnnotation(
+ wrapAnnotationWithInterface(annotationInterfaceClass, annotation), types);
+ }
+ }
+
+ return emptyAnnotationInfo(elements);
+ }
+
+ /**
+ * Returns the class of the annotation type that this extractor generates {@link AnnotationInfoT}
+ * for.
+ *
+ * <p>For example, if supporting {@link CrossProfileProvider} and {@link CrossUserProvider}
+ * annotations, return the value of {@link
+ * AnnotationClasses#crossProfileProviderAnnotationClass()}.
+ */
+ protected abstract Class<? extends Annotation> supportedAnnotationClass(
+ AnnotationClasses annotationClasses);
+
+ protected abstract AnnotationInfoT annotationInfoFromAnnotation(
+ AnnotationInterfaceT annotation, Types types);
+
+ protected abstract AnnotationInfoT emptyAnnotationInfo(Elements elements);
+
+ /**
+ * Wraps any annotation of a specific type (e.g. {@link CrossProfileProvider} and {@link
+ * CrossUserProvider}) with its interface (in that case {@link CrossProfileProviderAnnotation}).
+ *
+ * <p>Java does not allow annotation subclassing so we use Java proxies to treat these different
+ * annotations with identical interfaces polymorphically.
+ */
+ protected static <T> T wrapAnnotationWithInterface(
+ Class<T> annotationInterfaceClass, Annotation annotation) {
+ return annotationInterfaceClass.cast(
+ Proxy.newProxyInstance(
+ annotationInterfaceClass.getClassLoader(),
+ new Class<?>[] {annotationInterfaceClass},
+ new AnnotationInvocationHandler(annotation)));
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationInvocationHandler.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationInvocationHandler.java
new file mode 100644
index 0000000..0179dd9
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationInvocationHandler.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.annotationdiscovery;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * Given an annotation, forwards method calls from a mock instance of its interface to its actual
+ * instance.
+ *
+ * <p>This allows us to treat separate annotations with identical interfaces polymorphically.
+ */
+class AnnotationInvocationHandler implements InvocationHandler {
+
+ private final Annotation annotation;
+
+ AnnotationInvocationHandler(Annotation annotation) {
+ this.annotation = annotation;
+ }
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ return invokeAndUnwrapExceptions(annotation.annotationType().getMethod(method.getName()));
+ }
+
+ private Object invokeAndUnwrapExceptions(Method method) throws Throwable {
+ try {
+ return method.invoke(annotation);
+ } catch (InvocationTargetException e) {
+ throw e.getCause();
+ }
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationNames.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationNames.java
new file mode 100644
index 0000000..37c3961
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationNames.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.annotationdiscovery;
+
+/**
+ * A set of parallel annotation names.
+ *
+ * <p>For example, a valid instance could return "CrossUser", "CrossUserCallback", and
+ * "CrossUserProvider", etc.
+ */
+public interface AnnotationNames {
+
+ String crossProfile();
+
+ String crossProfileCallback();
+
+ String crossProfileConfiguration();
+
+ String crossProfileConfigurations();
+
+ String crossProfileProvider();
+
+ String crossProfileTest();
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationPrinter.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationPrinter.java
new file mode 100644
index 0000000..adf1662
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationPrinter.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.annotationdiscovery;
+
+/** Prints annotations as they would appear in source code. */
+public interface AnnotationPrinter {
+
+ String crossProfileAsAnnotation();
+
+ String crossProfileAsAnnotation(String content);
+
+ String crossProfileQualifiedName();
+
+ String crossProfileCallbackAsAnnotation();
+
+ String crossProfileCallbackAsAnnotation(String content);
+
+ String crossProfileCallbackQualifiedName();
+
+ String crossProfileConfigurationAsAnnotation();
+
+ String crossProfileConfigurationAsAnnotation(String content);
+
+ String crossProfileConfigurationQualifiedName();
+
+ String crossProfileConfigurationsAsAnnotation(String content);
+
+ String crossProfileConfigurationsQualifiedName();
+
+ String crossProfileProviderAsAnnotation();
+
+ String crossProfileProviderAsAnnotation(String content);
+
+ String crossProfileProviderQualifiedName();
+
+ String crossProfileTestAsAnnotation(String content);
+
+ String crossProfileTestQualifiedName();
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationStrings.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationStrings.java
new file mode 100644
index 0000000..35ce81a
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationStrings.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.annotationdiscovery;
+
+import com.google.auto.value.AutoValue;
+import java.lang.annotation.Annotation;
+
+/** Provides the raw string names for cross-profile annotations. */
+@AutoValue
+public abstract class AnnotationStrings
+ implements AnnotationNames, AnnotationPrinter, AnnotationClasses {
+
+ @Override
+ public String crossProfile() {
+ return crossProfileAnnotationClass().getSimpleName();
+ }
+
+ @Override
+ public String crossProfileAsAnnotation() {
+ return asAnnotation(crossProfile());
+ }
+
+ @Override
+ public String crossProfileAsAnnotation(String content) {
+ return asAnnotationWithContent(crossProfile(), content);
+ }
+
+ @Override
+ public String crossProfileQualifiedName() {
+ return crossProfileAnnotationClass().getCanonicalName();
+ }
+
+ @Override
+ public String crossProfileCallback() {
+ return crossProfileCallbackAnnotationClass().getSimpleName();
+ }
+
+ @Override
+ public String crossProfileCallbackAsAnnotation() {
+ return asAnnotation(crossProfileCallback());
+ }
+
+ @Override
+ public String crossProfileCallbackAsAnnotation(String content) {
+ return crossProfileCallbackAsAnnotation() + "(" + content + ")";
+ }
+
+ @Override
+ public String crossProfileCallbackQualifiedName() {
+ return crossProfileCallbackAnnotationClass().getCanonicalName();
+ }
+
+ @Override
+ public String crossProfileConfiguration() {
+ return crossProfileConfigurationAnnotationClass().getSimpleName();
+ }
+
+ @Override
+ public String crossProfileConfigurationAsAnnotation() {
+ return asAnnotation(crossProfileConfiguration());
+ }
+
+ @Override
+ public String crossProfileConfigurationAsAnnotation(String content) {
+ return asAnnotationWithContent(crossProfileConfiguration(), content);
+ }
+
+ @Override
+ public String crossProfileConfigurationQualifiedName() {
+ return crossProfileConfigurationAnnotationClass().getCanonicalName();
+ }
+
+ @Override
+ public String crossProfileConfigurations() {
+ return crossProfileConfigurationsAnnotationClass().getSimpleName();
+ }
+
+ @Override
+ public String crossProfileConfigurationsAsAnnotation(String content) {
+ return asAnnotationWithContent(crossProfileProvider(), content);
+ }
+
+ @Override
+ public String crossProfileConfigurationsQualifiedName() {
+ return crossProfileConfigurationsAnnotationClass().getCanonicalName();
+ }
+
+ @Override
+ public String crossProfileProvider() {
+ return crossProfileProviderAnnotationClass().getSimpleName();
+ }
+
+ @Override
+ public String crossProfileProviderAsAnnotation() {
+ return asAnnotation(crossProfileProvider());
+ }
+
+ @Override
+ public String crossProfileProviderAsAnnotation(String content) {
+ return asAnnotationWithContent(crossProfileProvider(), content);
+ }
+
+ @Override
+ public String crossProfileProviderQualifiedName() {
+ return crossProfileProviderAnnotationClass().getCanonicalName();
+ }
+
+ @Override
+ public String crossProfileTest() {
+ return crossProfileTestAnnotationClass().getSimpleName();
+ }
+
+ @Override
+ public String crossProfileTestAsAnnotation(String content) {
+ return asAnnotationWithContent(crossProfileTest(), content);
+ }
+
+ @Override
+ public String crossProfileTestQualifiedName() {
+ return crossProfileTestAnnotationClass().getCanonicalName();
+ }
+
+ @Override
+ public final String toString() {
+ return crossProfile() + " AnnotationStrings";
+ }
+
+ private static String asAnnotation(String annotationName) {
+ return "@" + annotationName;
+ }
+
+ private static String asAnnotationWithContent(String annotationName, String content) {
+ return "@" + annotationName + "(" + content + ")";
+ }
+
+ @Override
+ public abstract Class<? extends Annotation> crossProfileAnnotationClass();
+
+ @Override
+ public abstract Class<? extends Annotation> crossProfileCallbackAnnotationClass();
+
+ @Override
+ public abstract Class<? extends Annotation> crossProfileConfigurationAnnotationClass();
+
+ @Override
+ public abstract Class<? extends Annotation> crossProfileConfigurationsAnnotationClass();
+
+ @Override
+ public abstract Class<? extends Annotation> crossProfileProviderAnnotationClass();
+
+ @Override
+ public abstract Class<? extends Annotation> crossProfileTestAnnotationClass();
+
+ static Builder builder() {
+ return new AutoValue_AnnotationStrings.Builder();
+ }
+
+ @AutoValue.Builder
+ abstract static class Builder {
+ abstract Builder setCrossProfileAnnotationClass(Class<? extends Annotation> value);
+
+ abstract Builder setCrossProfileCallbackAnnotationClass(Class<? extends Annotation> value);
+
+ abstract Builder setCrossProfileConfigurationAnnotationClass(Class<? extends Annotation> value);
+
+ abstract Builder setCrossProfileConfigurationsAnnotationClass(
+ Class<? extends Annotation> value);
+
+ abstract Builder setCrossProfileProviderAnnotationClass(Class<? extends Annotation> value);
+
+ abstract Builder setCrossProfileTestAnnotationClass(Class<? extends Annotation> value);
+
+ abstract AnnotationStrings build();
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileAnnotationInfoExtractor.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileAnnotationInfoExtractor.java
new file mode 100644
index 0000000..49e737d
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileAnnotationInfoExtractor.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.annotationdiscovery;
+
+import com.google.android.enterprise.connectedapps.processor.GeneratorUtilities;
+import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileAnnotation;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileAnnotationInfo;
+import com.google.common.collect.ImmutableSet;
+import java.lang.annotation.Annotation;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
+
+final class CrossProfileAnnotationInfoExtractor
+ extends AnnotationInfoExtractor<CrossProfileAnnotationInfo, CrossProfileAnnotation> {
+
+ CrossProfileAnnotationInfoExtractor() {
+ super(CrossProfileAnnotation.class);
+ }
+
+ @Override
+ protected Class<? extends Annotation> supportedAnnotationClass(
+ AnnotationClasses annotationClasses) {
+ return annotationClasses.crossProfileAnnotationClass();
+ }
+
+ @Override
+ protected CrossProfileAnnotationInfo annotationInfoFromAnnotation(
+ CrossProfileAnnotation annotation, Types types) {
+ CrossProfileAnnotationInfo.Builder builder =
+ CrossProfileAnnotationInfo.builder()
+ .setConnectorClass(
+ GeneratorUtilities.extractClassFromAnnotation(types, annotation::connector))
+ .setProfileClassName(annotation.profileClassName())
+ .setParcelableWrapperClasses(
+ ImmutableSet.copyOf(
+ GeneratorUtilities.extractClassesFromAnnotation(
+ types, annotation::parcelableWrappers)))
+ .setFutureWrapperClasses(
+ ImmutableSet.copyOf(
+ GeneratorUtilities.extractClassesFromAnnotation(
+ types, annotation::futureWrappers)))
+ .setIsStatic(annotation.isStatic());
+
+ long timeoutMillis = annotation.timeoutMillis();
+
+ if (timeoutMillis != CrossProfileAnnotation.TIMEOUT_MILLIS_NOT_SET) {
+ builder.setTimeoutMillis(timeoutMillis);
+ }
+
+ return builder.build();
+ }
+
+ @Override
+ protected CrossProfileAnnotationInfo emptyAnnotationInfo(Elements elements) {
+ return CrossProfileAnnotationInfo.builder()
+ .setConnectorClass(
+ elements.getTypeElement(
+ "com.google.android.enterprise.connectedapps.annotations.CrossProfile"))
+ .setProfileClassName("")
+ .setParcelableWrapperClasses(ImmutableSet.of())
+ .setFutureWrapperClasses(ImmutableSet.of())
+ .setIsStatic(false)
+ .build();
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileCallbackAnnotationInfoExtractor.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileCallbackAnnotationInfoExtractor.java
new file mode 100644
index 0000000..37016e8
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileCallbackAnnotationInfoExtractor.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.annotationdiscovery;
+
+import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileCallbackAnnotation;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackAnnotationInfo;
+import java.lang.annotation.Annotation;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
+
+final class CrossProfileCallbackAnnotationInfoExtractor
+ extends AnnotationInfoExtractor<
+ CrossProfileCallbackAnnotationInfo, CrossProfileCallbackAnnotation> {
+
+ CrossProfileCallbackAnnotationInfoExtractor() {
+ super(CrossProfileCallbackAnnotation.class);
+ }
+
+ @Override
+ protected Class<? extends Annotation> supportedAnnotationClass(
+ AnnotationClasses annotationClasses) {
+ return annotationClasses.crossProfileCallbackAnnotationClass();
+ }
+
+ @Override
+ protected CrossProfileCallbackAnnotationInfo annotationInfoFromAnnotation(
+ CrossProfileCallbackAnnotation annotation, Types types) {
+ return CrossProfileCallbackAnnotationInfo.builder().setSimple(annotation.simple()).build();
+ }
+
+ @Override
+ protected CrossProfileCallbackAnnotationInfo emptyAnnotationInfo(Elements elements) {
+ return CrossProfileCallbackAnnotationInfo.builder().setSimple(false).build();
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileConfigurationAnnotationInfoExtractor.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileConfigurationAnnotationInfoExtractor.java
new file mode 100644
index 0000000..3b76469
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileConfigurationAnnotationInfoExtractor.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.annotationdiscovery;
+
+import com.google.android.enterprise.connectedapps.processor.GeneratorUtilities;
+import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileConfigurationAnnotation;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationAnnotationInfo;
+import com.google.common.collect.ImmutableSet;
+import java.lang.annotation.Annotation;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
+
+final class CrossProfileConfigurationAnnotationInfoExtractor
+ extends AnnotationInfoExtractor<
+ CrossProfileConfigurationAnnotationInfo, CrossProfileConfigurationAnnotation> {
+
+ CrossProfileConfigurationAnnotationInfoExtractor() {
+ super(CrossProfileConfigurationAnnotation.class);
+ }
+
+ @Override
+ protected Class<? extends Annotation> supportedAnnotationClass(
+ AnnotationClasses annotationClasses) {
+ return annotationClasses.crossProfileConfigurationAnnotationClass();
+ }
+
+ @Override
+ protected CrossProfileConfigurationAnnotationInfo annotationInfoFromAnnotation(
+ CrossProfileConfigurationAnnotation annotation, Types types) {
+ return CrossProfileConfigurationAnnotationInfo.builder()
+ .setConnector(GeneratorUtilities.extractClassFromAnnotation(types, annotation::connector))
+ .setProviderClasses(
+ ImmutableSet.copyOf(
+ GeneratorUtilities.extractClassesFromAnnotation(types, annotation::providers)))
+ .setServiceClass(
+ GeneratorUtilities.extractClassFromAnnotation(types, annotation::serviceClass))
+ .setServiceSuperclass(
+ GeneratorUtilities.extractClassFromAnnotation(types, annotation::serviceSuperclass))
+ .build();
+ }
+
+ @Override
+ protected CrossProfileConfigurationAnnotationInfo emptyAnnotationInfo(Elements elements) {
+ TypeElement crossProfileConfiguration =
+ elements.getTypeElement(
+ "com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration");
+
+ return CrossProfileConfigurationAnnotationInfo.builder()
+ .setConnector(crossProfileConfiguration)
+ .setProviderClasses(ImmutableSet.of())
+ .setServiceClass(crossProfileConfiguration)
+ .setServiceSuperclass(crossProfileConfiguration)
+ .build();
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileConfigurationsAnnotationInfoExtractor.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileConfigurationsAnnotationInfoExtractor.java
new file mode 100644
index 0000000..99eb04d
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileConfigurationsAnnotationInfoExtractor.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.annotationdiscovery;
+
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static java.util.Arrays.stream;
+
+import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileConfigurationAnnotation;
+import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileConfigurationsAnnotation;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationAnnotationInfo;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationsAnnotationInfo;
+import com.google.common.collect.ImmutableSet;
+import java.lang.annotation.Annotation;
+import java.util.Set;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
+
+final class CrossProfileConfigurationsAnnotationInfoExtractor
+ extends AnnotationInfoExtractor<
+ CrossProfileConfigurationsAnnotationInfo, CrossProfileConfigurationsAnnotation> {
+
+ CrossProfileConfigurationsAnnotationInfoExtractor() {
+ super(CrossProfileConfigurationsAnnotation.class);
+ }
+
+ @Override
+ protected Class<? extends Annotation> supportedAnnotationClass(
+ AnnotationClasses annotationClasses) {
+ return annotationClasses.crossProfileConfigurationsAnnotationClass();
+ }
+
+ @Override
+ protected CrossProfileConfigurationsAnnotationInfo annotationInfoFromAnnotation(
+ CrossProfileConfigurationsAnnotation annotation, Types types) {
+ CrossProfileConfigurationAnnotationInfoExtractor innerExtractor =
+ new CrossProfileConfigurationAnnotationInfoExtractor();
+
+ Set<CrossProfileConfigurationAnnotationInfo> annotationInfos =
+ stream(annotation.value())
+ .map(
+ configurationAnnotation ->
+ wrapAnnotationWithInterface(
+ CrossProfileConfigurationAnnotation.class, configurationAnnotation))
+ .map(
+ configurationAnnotation ->
+ innerExtractor.annotationInfoFromAnnotation(configurationAnnotation, types))
+ .collect(toImmutableSet());
+
+ return CrossProfileConfigurationsAnnotationInfo.create(ImmutableSet.copyOf(annotationInfos));
+ }
+
+ @Override
+ protected CrossProfileConfigurationsAnnotationInfo emptyAnnotationInfo(Elements elements) {
+ return CrossProfileConfigurationsAnnotationInfo.create(ImmutableSet.of());
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileProviderAnnotationInfoExtractor.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileProviderAnnotationInfoExtractor.java
new file mode 100644
index 0000000..1fb79c4
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileProviderAnnotationInfoExtractor.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.annotationdiscovery;
+
+import com.google.android.enterprise.connectedapps.processor.GeneratorUtilities;
+import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileProviderAnnotation;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileProviderAnnotationInfo;
+import com.google.common.collect.ImmutableSet;
+import java.lang.annotation.Annotation;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
+
+final class CrossProfileProviderAnnotationInfoExtractor
+ extends AnnotationInfoExtractor<
+ CrossProfileProviderAnnotationInfo, CrossProfileProviderAnnotation> {
+
+ CrossProfileProviderAnnotationInfoExtractor() {
+ super(CrossProfileProviderAnnotation.class);
+ }
+
+ @Override
+ protected Class<? extends Annotation> supportedAnnotationClass(
+ AnnotationClasses annotationClasses) {
+ return annotationClasses.crossProfileProviderAnnotationClass();
+ }
+
+ @Override
+ protected CrossProfileProviderAnnotationInfo annotationInfoFromAnnotation(
+ CrossProfileProviderAnnotation annotation, Types types) {
+ return CrossProfileProviderAnnotationInfo.create(
+ ImmutableSet.copyOf(
+ GeneratorUtilities.extractClassesFromAnnotation(types, annotation::staticTypes)));
+ }
+
+ @Override
+ protected CrossProfileProviderAnnotationInfo emptyAnnotationInfo(Elements elements) {
+ return CrossProfileProviderAnnotationInfo.create(ImmutableSet.of());
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileTestAnnotationInfoExtractor.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileTestAnnotationInfoExtractor.java
new file mode 100644
index 0000000..4a119de
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileTestAnnotationInfoExtractor.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.annotationdiscovery;
+
+import com.google.android.enterprise.connectedapps.processor.GeneratorUtilities;
+import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileTestAnnotation;
+import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTestAnnotationInfo;
+import java.lang.annotation.Annotation;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
+
+final class CrossProfileTestAnnotationInfoExtractor
+ extends AnnotationInfoExtractor<CrossProfileTestAnnotationInfo, CrossProfileTestAnnotation> {
+
+ CrossProfileTestAnnotationInfoExtractor() {
+ super(CrossProfileTestAnnotation.class);
+ }
+
+ @Override
+ protected Class<? extends Annotation> supportedAnnotationClass(
+ AnnotationClasses annotationClasses) {
+ return annotationClasses.crossProfileTestAnnotationClass();
+ }
+
+ @Override
+ protected CrossProfileTestAnnotationInfo annotationInfoFromAnnotation(
+ CrossProfileTestAnnotation annotation, Types types) {
+ return CrossProfileTestAnnotationInfo.builder()
+ .setConfiguration(
+ GeneratorUtilities.extractClassFromAnnotation(types, annotation::configuration))
+ .build();
+ }
+
+ @Override
+ protected CrossProfileTestAnnotationInfo emptyAnnotationInfo(Elements elements) {
+ throw new UnsupportedOperationException("Annotations of type CrossProfileTest cannot be empty");
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileAnnotation.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileAnnotation.java
new file mode 100644
index 0000000..8e76d9c
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileAnnotation.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces;
+
+/** Elements that can be populated on annotations of type CrossProfile. */
+public interface CrossProfileAnnotation {
+
+ long DEFAULT_TIMEOUT_MILLIS = 10000;
+
+ long TIMEOUT_MILLIS_NOT_SET = -1;
+
+ String profileClassName();
+
+ Class<?> connector();
+
+ Class<?>[] parcelableWrappers();
+
+ Class<?>[] futureWrappers();
+
+ boolean isStatic();
+
+ long timeoutMillis();
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileCallbackAnnotation.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileCallbackAnnotation.java
new file mode 100644
index 0000000..34e4619
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileCallbackAnnotation.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces;
+
+/* Elements that can be populated on annotations of type CrossProfileCallback. */
+public interface CrossProfileCallbackAnnotation {
+
+ boolean simple();
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileConfigurationAnnotation.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileConfigurationAnnotation.java
new file mode 100644
index 0000000..94b5205
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileConfigurationAnnotation.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces;
+
+/** Elements that can be populated on annotations of type CrossProfileConfiguration. */
+public interface CrossProfileConfigurationAnnotation {
+
+ Class<?>[] providers();
+
+ Class<?> serviceSuperclass();
+
+ Class<?> serviceClass();
+
+ Class<?> connector();
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileConfigurationsAnnotation.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileConfigurationsAnnotation.java
new file mode 100644
index 0000000..17acd78
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileConfigurationsAnnotation.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces;
+
+import java.lang.annotation.Annotation;
+
+/** Elements that can be populated on annotations of type CrossProfileConfigurations. */
+public interface CrossProfileConfigurationsAnnotation {
+
+ Annotation[] value();
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileProviderAnnotation.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileProviderAnnotation.java
new file mode 100644
index 0000000..b478d36
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileProviderAnnotation.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces;
+
+/* Elements that can be populated on annotations of type CrossProfileProvider. */
+public interface CrossProfileProviderAnnotation {
+
+ Class<?>[] staticTypes();
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileTestAnnotation.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileTestAnnotation.java
new file mode 100644
index 0000000..542748b
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileTestAnnotation.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces;
+
+/* Elements that can be populated on annotations of type CrossProfileTest. */
+public interface CrossProfileTestAnnotation {
+
+ Class<?> configuration();
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/Context.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/Context.java
new file mode 100644
index 0000000..e3b17e7
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/Context.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.containers;
+
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
+
+/** A container for a validator or generator context. */
+public abstract class Context {
+ public abstract ProcessingEnvironment processingEnv();
+
+ public abstract Elements elements();
+
+ public abstract Types types();
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileAnnotationInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileAnnotationInfo.java
new file mode 100644
index 0000000..f083a69
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileAnnotationInfo.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.containers;
+
+import com.google.android.enterprise.connectedapps.annotations.CrossProfile;
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableCollection;
+import java.util.Optional;
+import javax.lang.model.element.TypeElement;
+
+/** Wrapper around information contained in an annotation of type {@link CrossProfile}. */
+@AutoValue
+public abstract class CrossProfileAnnotationInfo {
+
+ public static final String DEFAULT_CONNECTOR_NAME =
+ "com.google.android.enterprise.connectedapps.annotations.CrossProfile";
+
+ public abstract TypeElement connectorClass();
+
+ public abstract String profileClassName();
+
+ public abstract Optional<Long> timeoutMillis();
+
+ public abstract ImmutableCollection<TypeElement> parcelableWrapperClasses();
+
+ public abstract ImmutableCollection<TypeElement> futureWrapperClasses();
+
+ public abstract boolean isStatic();
+
+ public boolean connectorIsDefault() {
+ return connectorClass().asType().toString().equals(DEFAULT_CONNECTOR_NAME);
+ }
+
+ public boolean isProfileClassNameDefault() {
+ return profileClassName().isEmpty();
+ }
+
+ public static Builder builder() {
+ return new AutoValue_CrossProfileAnnotationInfo.Builder();
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+
+ public abstract Builder setConnectorClass(TypeElement value);
+
+ public abstract Builder setProfileClassName(String value);
+
+ public abstract Builder setTimeoutMillis(Long value);
+
+ public abstract Builder setParcelableWrapperClasses(ImmutableCollection<TypeElement> value);
+
+ public abstract Builder setFutureWrapperClasses(ImmutableCollection<TypeElement> value);
+
+ public abstract Builder setIsStatic(boolean value);
+
+ public abstract CrossProfileAnnotationInfo build();
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileCallbackAnnotationInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileCallbackAnnotationInfo.java
new file mode 100644
index 0000000..080b726
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileCallbackAnnotationInfo.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.containers;
+
+import com.google.android.enterprise.connectedapps.annotations.CrossProfileCallback;
+import com.google.auto.value.AutoValue;
+
+/** Wrapper around information contained in an annotation of type {@link CrossProfileCallback}. */
+@AutoValue
+public abstract class CrossProfileCallbackAnnotationInfo {
+
+ public abstract boolean simple();
+
+ public static Builder builder() {
+ return new AutoValue_CrossProfileCallbackAnnotationInfo.Builder();
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+
+ public abstract Builder setSimple(boolean value);
+
+ public abstract CrossProfileCallbackAnnotationInfo build();
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileCallbackInterfaceInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileCallbackInterfaceInfo.java
new file mode 100644
index 0000000..ea6f068
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileCallbackInterfaceInfo.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.containers;
+
+import static java.util.stream.Collectors.toList;
+
+import com.google.android.enterprise.connectedapps.annotations.CrossProfileCallback;
+import com.google.auto.value.AutoValue;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.Name;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.TypeMirror;
+
+/** Wrapper of a {@link CrossProfileCallback} annotated interface. */
+@AutoValue
+public abstract class CrossProfileCallbackInterfaceInfo {
+
+ public abstract TypeElement interfaceElement();
+
+ public Name simpleName() {
+ return interfaceElement().getSimpleName();
+ }
+
+ public boolean isSimple() {
+ List<ExecutableElement> methods = methods();
+ return methods.size() == 1 && methods.get(0).getParameters().size() < 2;
+ }
+
+ public List<ExecutableElement> methods() {
+ return interfaceElement().getEnclosedElements().stream()
+ .filter(e -> e instanceof ExecutableElement)
+ .map(e -> (ExecutableElement) e)
+ .filter(e -> e.getKind() == ElementKind.METHOD)
+ .sorted(Comparator.comparing(e -> e.getSimpleName().toString()))
+ .collect(toList());
+ }
+
+ public int getIdentifier(ExecutableElement method) {
+ return methods().indexOf(method);
+ }
+
+ /** Get all types used by methods on this interface. */
+ public Set<TypeMirror> argumentTypes() {
+ return methods().stream()
+ .flatMap(m -> m.getParameters().stream())
+ .map(Element::asType)
+ .collect(Collectors.toSet());
+ }
+
+ public static CrossProfileCallbackInterfaceInfo create(TypeElement interfaceElement) {
+ return new AutoValue_CrossProfileCallbackInterfaceInfo(interfaceElement);
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationAnnotationInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationAnnotationInfo.java
new file mode 100644
index 0000000..a39c2e3
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationAnnotationInfo.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.containers;
+
+import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration;
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableCollection;
+import javax.lang.model.element.TypeElement;
+
+/**
+ * Wrapper around information contained in an annotation of type {@link CrossProfileConfiguration}.
+ */
+@AutoValue
+public abstract class CrossProfileConfigurationAnnotationInfo {
+
+ public abstract ImmutableCollection<TypeElement> providerClasses();
+
+ public abstract TypeElement serviceSuperclass();
+
+ public abstract TypeElement serviceClass();
+
+ public abstract TypeElement connector();
+
+ public static Builder builder() {
+ return new AutoValue_CrossProfileConfigurationAnnotationInfo.Builder();
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+
+ public abstract Builder setProviderClasses(ImmutableCollection<TypeElement> value);
+
+ public abstract Builder setServiceSuperclass(TypeElement value);
+
+ public abstract Builder setServiceClass(TypeElement value);
+
+ public abstract Builder setConnector(TypeElement value);
+
+ public abstract CrossProfileConfigurationAnnotationInfo build();
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationInfo.java
new file mode 100644
index 0000000..2d4ed43
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationInfo.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.containers;
+
+import static java.util.stream.Collectors.toSet;
+
+import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration;
+import com.google.android.enterprise.connectedapps.processor.SupportedTypes;
+import com.google.android.enterprise.connectedapps.processor.TypeUtils;
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Streams;
+import com.squareup.javapoet.ClassName;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Optional;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.TypeMirror;
+
+/** Wrapper of a {@link CrossProfileConfiguration} annotated class. */
+@AutoValue
+public abstract class CrossProfileConfigurationInfo {
+
+ public static final String CROSS_PROFILE_CONNECTOR_QUALIFIED_NAME =
+ "com.google.android.enterprise.connectedapps.CrossProfileConnector";
+
+ public abstract TypeElement configurationElement();
+
+ public abstract ImmutableCollection<ProviderClassInfo> providers();
+
+ public abstract ClassName serviceSuperclass();
+
+ public abstract Optional<TypeElement> serviceClass();
+
+ public String simpleName() {
+ return configurationElement().getSimpleName().toString();
+ }
+
+ public ClassName className() {
+ return ClassName.get(configurationElement());
+ }
+
+ public abstract ProfileConnectorInfo profileConnector();
+
+ public static CrossProfileConfigurationInfo create(
+ ValidatorContext context, ValidatorCrossProfileConfigurationInfo configuration) {
+ Collection<ProviderClassInfo> providerClasses =
+ configuration.providerClassElements().stream()
+ .map(
+ m ->
+ ProviderClassInfo.create(
+ context, ValidatorProviderClassInfo.create(context.processingEnv(), m)))
+ .collect(toSet());
+
+ ProfileConnectorInfo profileConnectorInfo =
+ providerClasses.stream()
+ .flatMap(m -> m.allCrossProfileTypes().stream())
+ .map(CrossProfileTypeInfo::profileConnector)
+ .flatMap(Streams::stream)
+ .findFirst()
+ .orElseGet(
+ () ->
+ ProfileConnectorInfo.create(
+ context.processingEnv(),
+ getConfiguredConnectorOrDefault(context, configuration),
+ context.globalSupportedTypes()));
+
+ return new AutoValue_CrossProfileConfigurationInfo(
+ configuration.configurationElement(),
+ ImmutableSet.copyOf(providerClasses),
+ configuration.serviceSuperclass(),
+ configuration.serviceClass(),
+ profileConnectorInfo);
+ }
+
+ private static TypeElement getConfiguredConnectorOrDefault(
+ ValidatorContext context, ValidatorCrossProfileConfigurationInfo configuration) {
+ return configuration
+ .connector()
+ .orElseGet(() -> context.elements().getTypeElement(CROSS_PROFILE_CONNECTOR_QUALIFIED_NAME));
+ }
+
+ private static Collection<Type> convertTypeMirrorToSupportedTypes(
+ SupportedTypes supportedTypes, TypeMirror typeMirror) {
+ if (TypeUtils.isGeneric(typeMirror)) {
+ return convertGenericTypeMirrorToSupportedTypes(supportedTypes, typeMirror);
+ }
+ return Collections.singleton(supportedTypes.getType(typeMirror));
+ }
+
+ private static Collection<Type> convertGenericTypeMirrorToSupportedTypes(
+ SupportedTypes supportedTypes, TypeMirror typeMirror) {
+ Collection<Type> types = new HashSet<>();
+ TypeMirror genericType = TypeUtils.removeTypeArguments(typeMirror);
+ Type supportedType = supportedTypes.getType(genericType);
+ if (!supportedType.isSupportedWithAnyGenericType()) {
+ for (TypeMirror typeArgument : TypeUtils.extractTypeArguments(typeMirror)) {
+ types.addAll(convertTypeMirrorToSupportedTypes(supportedTypes, typeArgument));
+ }
+ }
+ types.add(supportedTypes.getType(genericType));
+ return types;
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationsAnnotationInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationsAnnotationInfo.java
new file mode 100644
index 0000000..1060d68
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationsAnnotationInfo.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.containers;
+
+import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfigurations;
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableSet;
+
+/**
+ * Wrapper around information contained in an annotation of type {@link CrossProfileConfigurations}.
+ */
+@AutoValue
+public abstract class CrossProfileConfigurationsAnnotationInfo {
+
+ public abstract ImmutableSet<CrossProfileConfigurationAnnotationInfo> configurations();
+
+ public static CrossProfileConfigurationsAnnotationInfo create(
+ ImmutableSet<CrossProfileConfigurationAnnotationInfo> configurations) {
+ return new AutoValue_CrossProfileConfigurationsAnnotationInfo(configurations);
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileMethodInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileMethodInfo.java
new file mode 100644
index 0000000..72831a9
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileMethodInfo.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.containers;
+
+import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.hasCrossProfileAnnotation;
+import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.hasCrossProfileCallbackAnnotation;
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toSet;
+
+import com.google.android.enterprise.connectedapps.annotations.CrossProfile;
+import com.google.android.enterprise.connectedapps.annotations.CrossProfileCallback;
+import com.google.android.enterprise.connectedapps.processor.SupportedTypes;
+import com.google.android.enterprise.connectedapps.processor.TypeUtils;
+import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder;
+import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileAnnotation;
+import com.google.auto.value.AutoValue;
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.TypeName;
+import java.util.Collection;
+import java.util.Optional;
+import java.util.function.Function;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.util.Elements;
+
+/** Wrapper of a {@link CrossProfile} annotated method. */
+@AutoValue
+public abstract class CrossProfileMethodInfo {
+
+ public abstract ExecutableElement methodElement();
+
+ public abstract int identifier();
+
+ public abstract boolean isStatic();
+
+ public String simpleName() {
+ return methodElement().getSimpleName().toString();
+ }
+
+ public TypeMirror returnType() {
+ return methodElement().getReturnType();
+ }
+
+ public TypeName returnTypeTypeName() {
+ return ClassName.get(returnType());
+ }
+
+ public Collection<TypeName> thrownExceptions() {
+ return methodElement().getThrownTypes().stream()
+ .map(ClassName::get)
+ .collect(toSet());
+ }
+
+ public Collection<TypeMirror> automaticallyResolvedParameterTypes(SupportedTypes supportedTypes) {
+ return parameterTypes().stream()
+ .filter(supportedTypes::isAutomaticallyResolved)
+ .collect(toSet());
+ }
+
+ /**
+ * The number of milliseconds to timeout async calls. This is either set on the method, the type,
+ * or defaults to {@link CrossProfileAnnotation#DEFAULT_TIMEOUT_MILLIS}.
+ */
+ public abstract long timeoutMillis();
+
+ /**
+ * Specify behaviour when encountering parameters of a type which is automatically resolved by the
+ * SDK.
+ */
+ public enum AutomaticallyResolvedParameterFilterBehaviour {
+ /** Do not change the parameters. */
+ LEAVE_AUTOMATICALLY_RESOLVED_PARAMETERS,
+ /** Remove the parameters and act as if they are not present. */
+ REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS,
+ /** Replace the parameter with the variable specified in the type configuration. */
+ REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS
+ }
+
+ /**
+ * A string of parameter names separated by commas.
+ *
+ * <p>This is useful when generating a call for this method, using the same parameter names.
+ *
+ * <p>Parameters which are automatically resolved will be removed.
+ */
+ public String commaSeparatedParameters(
+ SupportedTypes supportedTypes,
+ AutomaticallyResolvedParameterFilterBehaviour filterBehaviour) {
+ return commaSeparatedParameters(supportedTypes, filterBehaviour, Function.identity());
+ }
+
+ /**
+ * A string of parameter names separated by commas.
+ *
+ * <p>This is useful when generating a call for this method, using the same parameter names.
+ *
+ * <p>Parameters which are automatically resolved will be removed.
+ */
+ public String commaSeparatedParameters(
+ SupportedTypes supportedTypes,
+ AutomaticallyResolvedParameterFilterBehaviour filterBehaviour,
+ Function<String, String> map) {
+ if (filterBehaviour
+ == AutomaticallyResolvedParameterFilterBehaviour.REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS) {
+ return methodElement().getParameters().stream()
+ .filter(p -> !supportedTypes.isAutomaticallyResolved(p.asType()))
+ .map(p -> p.getSimpleName().toString())
+ .map(map)
+ .collect(joining(", "));
+ } else if (filterBehaviour
+ == AutomaticallyResolvedParameterFilterBehaviour
+ .REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS) {
+ return methodElement().getParameters().stream()
+ .map(
+ p ->
+ supportedTypes.isAutomaticallyResolved(p.asType())
+ ? supportedTypes.getAutomaticallyResolvedReplacement(p.asType())
+ : p.getSimpleName().toString())
+ .map(map)
+ .collect(joining(", "));
+ } else if (filterBehaviour
+ == AutomaticallyResolvedParameterFilterBehaviour.LEAVE_AUTOMATICALLY_RESOLVED_PARAMETERS) {
+ return methodElement().getParameters().stream()
+ .map(p -> p.getSimpleName().toString())
+ .map(map)
+ .collect(joining(", "));
+ }
+ throw new IllegalArgumentException("Invalid filter behaviour: " + filterBehaviour);
+ }
+
+ /** An unordered collection of the types used in the parameters of this method. */
+ public Collection<TypeMirror> parameterTypes() {
+ return methodElement().getParameters().stream().map(Element::asType).collect(toSet());
+ }
+
+ /**
+ * True if both {@link #isCrossProfileCallback(GeneratorContext)} and {@link
+ * #isFuture(CrossProfileTypeInfo)} are {@code False}.
+ */
+ public boolean isBlocking(GeneratorContext context, CrossProfileTypeInfo type) {
+ return !isCrossProfileCallback(context) && !isFuture(type);
+ }
+
+ /** True if any argument is annotated with {@link CrossProfileCallback}. */
+ public boolean isCrossProfileCallback(GeneratorContext generatorContext) {
+ return getCrossProfileCallbackParam(generatorContext).isPresent();
+ }
+
+ /** True if there is only a single {@link CrossProfileCallback} argument and it is simple. */
+ public boolean isSimpleCrossProfileCallback(GeneratorContext generatorContext) {
+ Optional<VariableElement> param = getCrossProfileCallbackParam(generatorContext);
+
+ if (param.isPresent()) {
+ CrossProfileCallbackInterfaceInfo callbackInterface =
+ CrossProfileCallbackInterfaceInfo.create(
+ (TypeElement) generatorContext.types().asElement(param.get().asType()));
+ return callbackInterface.isSimple();
+ }
+
+ return false;
+ }
+
+ /** True if the return type is a supported {@code Future} type. */
+ public boolean isFuture(CrossProfileTypeInfo type) {
+ return isFuture(type.supportedTypes(), methodElement());
+ }
+
+ public static boolean isFuture(SupportedTypes supportedTypes, ExecutableElement method) {
+ return supportedTypes.isFuture(TypeUtils.removeTypeArguments(method.getReturnType()));
+ }
+
+ /** Return the {@link CrossProfileCallback} annotated parameter, if any. */
+ public Optional<VariableElement> getCrossProfileCallbackParam(GeneratorContext generatorContext) {
+ return getCrossProfileCallbackParam(generatorContext.elements(), methodElement());
+ }
+
+ public static Optional<VariableElement> getCrossProfileCallbackParam(
+ Elements elements, ExecutableElement method) {
+ return method.getParameters().stream()
+ .filter(v -> isCrossProfileCallbackInterface(elements, v.asType()))
+ .findFirst()
+ .map(e -> (VariableElement) e);
+ }
+
+ private static boolean isCrossProfileCallbackInterface(Elements elements, TypeMirror type) {
+ TypeElement typeElement = elements.getTypeElement(type.toString());
+ return typeElement != null && hasCrossProfileCallbackAnnotation(typeElement);
+ }
+
+ public static CrossProfileMethodInfo create(
+ int identifier,
+ ValidatorCrossProfileTypeInfo type,
+ ExecutableElement methodElement,
+ Context context) {
+ return new AutoValue_CrossProfileMethodInfo(
+ methodElement,
+ identifier,
+ methodElement.getModifiers().contains(Modifier.STATIC),
+ findTimeoutMillis(type, methodElement, context));
+ }
+
+ private static long findTimeoutMillis(
+ ValidatorCrossProfileTypeInfo type, ExecutableElement methodElement, Context context) {
+ if (hasCrossProfileAnnotation(methodElement)) {
+ return AnnotationFinder.extractCrossProfileAnnotationInfo(
+ methodElement, context.types(), context.elements())
+ .timeoutMillis()
+ .filter(timeout -> timeout > 0)
+ .orElse(type.timeoutMillis());
+ }
+
+ return type.timeoutMillis();
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileProviderAnnotationInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileProviderAnnotationInfo.java
new file mode 100644
index 0000000..ab05fff
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileProviderAnnotationInfo.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.containers;
+
+import com.google.android.enterprise.connectedapps.annotations.CrossProfileProvider;
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableCollection;
+import javax.lang.model.element.TypeElement;
+
+/** Wrapper around information contained in an annotation of type {@link CrossProfileProvider}. */
+@AutoValue
+public abstract class CrossProfileProviderAnnotationInfo {
+
+ public abstract ImmutableCollection<TypeElement> staticTypes();
+
+ public static CrossProfileProviderAnnotationInfo create(
+ ImmutableCollection<TypeElement> staticTypes) {
+ return new AutoValue_CrossProfileProviderAnnotationInfo(staticTypes);
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTestAnnotationInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTestAnnotationInfo.java
new file mode 100644
index 0000000..f96a153
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTestAnnotationInfo.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.containers;
+
+import com.google.android.enterprise.connectedapps.testing.annotations.CrossProfileTest;
+import com.google.auto.value.AutoValue;
+import javax.lang.model.element.TypeElement;
+
+/** Wrapper around information contained in an annotation of type {@link CrossProfileTest}. */
+@AutoValue
+public abstract class CrossProfileTestAnnotationInfo {
+
+ public abstract TypeElement configuration();
+
+ public static Builder builder() {
+ return new AutoValue_CrossProfileTestAnnotationInfo.Builder();
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+
+ public abstract Builder setConfiguration(TypeElement value);
+
+ public abstract CrossProfileTestAnnotationInfo build();
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTestInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTestInfo.java
new file mode 100644
index 0000000..a2219be
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTestInfo.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.containers;
+
+import static java.util.stream.Collectors.toSet;
+
+import com.google.android.enterprise.connectedapps.testing.annotations.CrossProfileTest;
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableSet;
+import java.util.Set;
+import javax.lang.model.element.TypeElement;
+
+/** Wrapper of a {@link CrossProfileTest} annotated class. */
+@AutoValue
+public abstract class CrossProfileTestInfo {
+
+ public abstract TypeElement crossProfileTestElement();
+
+ public abstract ImmutableSet<CrossProfileConfigurationInfo> configurations();
+
+ public static CrossProfileTestInfo create(
+ ValidatorContext context, ValidatorCrossProfileTestInfo validatorCrossProfileTest) {
+
+ Set<CrossProfileConfigurationInfo> configurations =
+ ValidatorCrossProfileConfigurationInfo.createMultipleFromElement(
+ context.processingEnv(), validatorCrossProfileTest.configurationElement())
+ .stream()
+ .map(b -> CrossProfileConfigurationInfo.create(context, b))
+ .collect(toSet());
+
+ return new AutoValue_CrossProfileTestInfo(
+ validatorCrossProfileTest.crossProfileTestElement(), ImmutableSet.copyOf(configurations));
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTypeInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTypeInfo.java
new file mode 100644
index 0000000..fffe4a1
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTypeInfo.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.containers;
+
+import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.hasCrossProfileAnnotation;
+import static java.util.stream.Collectors.toSet;
+
+import com.google.android.enterprise.connectedapps.annotations.CrossProfile;
+import com.google.android.enterprise.connectedapps.processor.ProcessorConfiguration;
+import com.google.android.enterprise.connectedapps.processor.SupportedTypes;
+import com.google.android.enterprise.connectedapps.processor.TypeUtils;
+import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileAnnotation;
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.hash.Hashing;
+import com.squareup.javapoet.ClassName;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.IntStream;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.PackageElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.TypeMirror;
+
+/** Wrapper of a {@link CrossProfile} type. */
+@AutoValue
+public abstract class CrossProfileTypeInfo {
+
+ public abstract TypeElement crossProfileTypeElement();
+
+ public abstract ImmutableCollection<CrossProfileMethodInfo> crossProfileMethods();
+
+ public abstract SupportedTypes supportedTypes();
+
+ public abstract Optional<ProfileConnectorInfo> profileConnector();
+
+ public abstract ClassName profileClassName();
+
+ /**
+ * The specified timeout for async calls, or {@link CrossProfileAnnotation#DEFAULT_TIMEOUT_MILLIS}
+ * if unspecified.
+ */
+ public abstract long timeoutMillis();
+
+ public String simpleName() {
+ return crossProfileTypeElement().getSimpleName().toString();
+ }
+
+ public ClassName className() {
+ return ClassName.get(crossProfileTypeElement());
+ }
+
+ public boolean isStatic() {
+ return crossProfileMethods().stream().allMatch(CrossProfileMethodInfo::isStatic);
+ }
+
+ /**
+ * Get a numeric identifier for the cross-profile type.
+ *
+ * <p>This identifier is based on the type's qualified name, and will not change between runs.
+ */
+ public long identifier() {
+ // Stored in a 64 bit long, with ~200 cross-profile types, chance of collision is 1 in 10^15
+ return Hashing.murmur3_128()
+ .hashString(crossProfileTypeElement().getQualifiedName().toString(), StandardCharsets.UTF_8)
+ .asLong();
+ }
+
+ public static CrossProfileTypeInfo create(
+ ValidatorContext context, ValidatorCrossProfileTypeInfo crossProfileType) {
+ TypeElement crossProfileTypeElement = crossProfileType.crossProfileTypeElement();
+
+ List<ExecutableElement> crossProfileMethodElements = crossProfileType.crossProfileMethods();
+
+ Collection<CrossProfileMethodInfo> crossProfileMethods =
+ IntStream.range(0, crossProfileMethodElements.size())
+ .mapToObj(
+ t ->
+ CrossProfileMethodInfo.create(
+ t, crossProfileType, crossProfileMethodElements.get(t), context))
+ .collect(toSet());
+
+ SupportedTypes.Builder supportedTypesBuilder = crossProfileType.supportedTypes().asBuilder();
+
+ supportedTypesBuilder.filterUsed(context, crossProfileMethods);
+
+ if (ProcessorConfiguration.GENERATE_TYPE_SPECIFIC_WRAPPERS) {
+ supportedTypesBuilder.replaceWrapperPrefix(
+ ClassName.bestGuess(
+ crossProfileType.crossProfileTypeElement().getQualifiedName().toString()));
+ }
+
+ return new AutoValue_CrossProfileTypeInfo(
+ crossProfileTypeElement,
+ ImmutableSet.copyOf(crossProfileMethods),
+ supportedTypesBuilder.build(),
+ crossProfileType.profileConnector(),
+ findProfileClassName(context, crossProfileTypeElement, crossProfileType),
+ crossProfileType.timeoutMillis());
+ }
+
+ private static ClassName findProfileClassName(
+ ValidatorContext context,
+ TypeElement typeElement,
+ ValidatorCrossProfileTypeInfo crossProfileType) {
+ return hasCrossProfileAnnotation(typeElement)
+ ? findAnnotatedProfileClassName(context, typeElement, crossProfileType)
+ : createDefaultProfileClassName(context, typeElement);
+ }
+
+ private static ClassName createDefaultProfileClassName(
+ ValidatorContext context, TypeElement typeElement) {
+ PackageElement originalPackage = context.elements().getPackageOf(typeElement);
+ String profileAwareClassName =
+ String.format("Profile%s", typeElement.getSimpleName().toString());
+
+ return ClassName.get(originalPackage.getQualifiedName().toString(), profileAwareClassName);
+ }
+
+ private static ClassName findAnnotatedProfileClassName(
+ ValidatorContext context,
+ TypeElement typeElement,
+ ValidatorCrossProfileTypeInfo crossProfileType) {
+ String profileClassName = crossProfileType.profileClassName();
+ if (!profileClassName.isEmpty()) {
+ return ClassName.bestGuess(profileClassName);
+ }
+
+ return createDefaultProfileClassName(context, typeElement);
+ }
+
+ private static Collection<Type> convertTypeMirrorToSupportedTypes(
+ SupportedTypes supportedTypes, TypeMirror typeMirror) {
+ if (TypeUtils.isGeneric(typeMirror)) {
+ return convertGenericTypeMirrorToSupportedTypes(supportedTypes, typeMirror);
+ }
+ return Collections.singleton(supportedTypes.getType(typeMirror));
+ }
+
+ private static Collection<Type> convertGenericTypeMirrorToSupportedTypes(
+ SupportedTypes supportedTypes, TypeMirror typeMirror) {
+ Collection<Type> types = new HashSet<>();
+ TypeMirror genericType = TypeUtils.removeTypeArguments(typeMirror);
+ Type supportedType = supportedTypes.getType(genericType);
+ if (!supportedType.isSupportedWithAnyGenericType()) {
+ for (TypeMirror typeArgument : TypeUtils.extractTypeArguments(typeMirror)) {
+ types.addAll(convertTypeMirrorToSupportedTypes(supportedTypes, typeArgument));
+ }
+ }
+ types.add(supportedTypes.getType(genericType));
+ return types;
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/FutureWrapper.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/FutureWrapper.java
new file mode 100644
index 0000000..5208e7c
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/FutureWrapper.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.containers;
+
+import com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper;
+import com.google.auto.value.AutoValue;
+import com.squareup.javapoet.ClassName;
+import java.util.ArrayList;
+import java.util.Collection;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
+
+/** Information about future wrapper. */
+@AutoValue
+public abstract class FutureWrapper {
+
+ /** The type of the Wrapper. This controls how supporting code is generated. */
+ public enum WrapperType {
+ DEFAULT, // Copied from a resource
+ CUSTOM // Included in classpath
+ }
+
+ public static final String FUTURE_WRAPPER_PACKAGE =
+ "com.google.android.enterprise.connectedapps.futurewrappers";
+
+ public abstract TypeMirror wrappedType();
+
+ public abstract ClassName defaultWrapperClassName();
+
+ public abstract ClassName wrapperClassName();
+
+ public abstract WrapperType wrapperType();
+
+ private static FutureWrapper create(
+ TypeMirror wrappedType, ClassName defaultWrapperClassName, WrapperType wrapperType) {
+ return create(wrappedType, defaultWrapperClassName, defaultWrapperClassName, wrapperType);
+ }
+
+ public static FutureWrapper create(
+ TypeMirror wrappedType,
+ ClassName defaultWrapperClassName,
+ ClassName wrapperClassName,
+ WrapperType wrapperType) {
+ return new AutoValue_FutureWrapper(
+ wrappedType, defaultWrapperClassName, wrapperClassName, wrapperType);
+ }
+
+ public static Collection<FutureWrapper> createGlobalFutureWrappers(Elements elements) {
+ Collection<FutureWrapper> wrappers = new ArrayList<>();
+
+ addDefaultFutureWrappers(elements, wrappers);
+
+ return wrappers;
+ }
+
+ private static void addDefaultFutureWrappers(
+ Elements elements, Collection<FutureWrapper> wrappers) {
+ tryAddWrapper(
+ elements,
+ wrappers,
+ "com.google.common.util.concurrent.ListenableFuture",
+ ClassName.get(FUTURE_WRAPPER_PACKAGE, "ListenableFutureWrapper"),
+ WrapperType.DEFAULT);
+ }
+
+ public static Collection<FutureWrapper> createCustomFutureWrappers(
+ Types types, Elements elements, Collection<TypeElement> customFutureWrappers) {
+ Collection<FutureWrapper> wrappers = new ArrayList<>();
+
+ addCustomFutureWrappers(types, elements, wrappers, customFutureWrappers);
+
+ return wrappers;
+ }
+
+ private static void addCustomFutureWrappers(
+ Types types,
+ Elements elements,
+ Collection<FutureWrapper> wrappers,
+ Collection<TypeElement> customFutureWrappers) {
+ for (TypeElement customFutureWrapper : customFutureWrappers) {
+ addCustomFutureWrapper(types, elements, wrappers, customFutureWrapper);
+ }
+ }
+
+ private static void addCustomFutureWrapper(
+ Types types,
+ Elements elements,
+ Collection<FutureWrapper> wrappers,
+ TypeElement customFutureWrapper) {
+ CustomFutureWrapper customFutureWrapperAnnotation =
+ customFutureWrapper.getAnnotation(CustomFutureWrapper.class);
+
+ if (customFutureWrapperAnnotation == null) {
+ // This will be dealt with as part of early validation
+ return;
+ }
+
+ tryAddWrapper(
+ elements,
+ wrappers,
+ FutureWrapperAnnotationInfo.extractFromFutureWrapperAnnotation(
+ types, customFutureWrapperAnnotation)
+ .originalType()
+ .toString(),
+ ClassName.get(customFutureWrapper),
+ WrapperType.CUSTOM);
+ }
+
+ private static void tryAddWrapper(
+ Elements elements,
+ Collection<FutureWrapper> wrappers,
+ String typeQualifiedName,
+ ClassName wrapperClassName,
+ WrapperType wrapperType) {
+ TypeElement typeElement = elements.getTypeElement(typeQualifiedName);
+
+ if (typeElement == null) {
+ // The type isn't supported at compile-time - so won't be included in this app
+ return;
+ }
+
+ wrappers.add(FutureWrapper.create(typeElement.asType(), wrapperClassName, wrapperType));
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/FutureWrapperAnnotationInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/FutureWrapperAnnotationInfo.java
new file mode 100644
index 0000000..18fb34b
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/FutureWrapperAnnotationInfo.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.containers;
+
+import com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper;
+import com.google.android.enterprise.connectedapps.processor.GeneratorUtilities;
+import com.google.auto.value.AutoValue;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.util.Types;
+
+/**
+ * Wrapper around information contained in a {@link
+ * com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper} annotation.
+ */
+@AutoValue
+public abstract class FutureWrapperAnnotationInfo {
+
+ public abstract TypeElement originalType();
+
+ public static FutureWrapperAnnotationInfo extractFromFutureWrapperAnnotation(
+ Types types, CustomFutureWrapper customFutureWrapperAnnotation) {
+ if (customFutureWrapperAnnotation == null) {
+ throw new NullPointerException("customFutureWrapperAnnotation must not be null");
+ }
+
+ TypeElement originalType =
+ GeneratorUtilities.extractClassFromAnnotation(
+ types, customFutureWrapperAnnotation::originalType);
+
+ return new AutoValue_FutureWrapperAnnotationInfo(originalType);
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/GeneratorContext.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/GeneratorContext.java
new file mode 100644
index 0000000..ac47218
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/GeneratorContext.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.containers;
+
+import static java.util.stream.Collectors.toSet;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableSet;
+import java.util.Collection;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
+
+/** Context for connected apps code generators. */
+@AutoValue
+public abstract class GeneratorContext extends Context {
+
+ public static GeneratorContext createFromValidatorContext(ValidatorContext validatorContext) {
+ Collection<CrossProfileConfigurationInfo> configurations =
+ validatorContext.newConfigurations().stream()
+ .map(a -> CrossProfileConfigurationInfo.create(validatorContext, a))
+ .collect(toSet());
+
+ Collection<ProviderClassInfo> providers =
+ validatorContext.newProviderClasses().stream()
+ .map(m -> ProviderClassInfo.create(validatorContext, m))
+ .collect(toSet());
+
+ Collection<CrossProfileTypeInfo> crossProfileTypes =
+ validatorContext.newCrossProfileTypes().stream()
+ .map(m -> CrossProfileTypeInfo.create(validatorContext, m))
+ .collect(toSet());
+
+ Collection<CrossProfileCallbackInterfaceInfo> crossProfileCallbackInterfaces =
+ validatorContext.newCrossProfileCallbackInterfaces().stream()
+ .map(CrossProfileCallbackInterfaceInfo::create)
+ .collect(toSet());
+
+ Collection<ProfileConnectorInfo> generatedProfileConnectors =
+ validatorContext.newGeneratedProfileConnectors().stream()
+ .map(
+ t ->
+ ProfileConnectorInfo.create(
+ validatorContext.processingEnv(),
+ t,
+ validatorContext.globalSupportedTypes()))
+ .collect(toSet());
+
+ Collection<UserConnectorInfo> generatedUserConnectors =
+ validatorContext.newGeneratedUserConnectors().stream()
+ .map(
+ t ->
+ UserConnectorInfo.create(
+ validatorContext.processingEnv(),
+ t,
+ validatorContext.globalSupportedTypes()))
+ .collect(toSet());
+
+ Collection<CrossProfileTestInfo> crossProfileTests =
+ validatorContext.newCrossProfileTests().stream()
+ .map(t -> CrossProfileTestInfo.create(validatorContext, t))
+ .collect(toSet());
+
+ return GeneratorContext.builder()
+ .setProcessingEnv(validatorContext.processingEnv())
+ .setElements(validatorContext.elements())
+ .setTypes(validatorContext.types())
+ .setConfigurations(configurations)
+ .setGeneratedProfileConnectors(generatedProfileConnectors)
+ .setGeneratedUserConnectors(generatedUserConnectors)
+ .setProviders(providers)
+ .setCrossProfileTypes(crossProfileTypes)
+ .setCrossProfileMethods(validatorContext.newCrossProfileMethods())
+ .setCrossProfileCallbackInterfaces(crossProfileCallbackInterfaces)
+ .setCrossProfileTests(crossProfileTests)
+ .build();
+ }
+
+ static Builder builder() {
+ return new AutoValue_GeneratorContext.Builder();
+ }
+
+ public abstract ImmutableSet<CrossProfileConfigurationInfo> configurations();
+
+ public abstract ImmutableSet<ProfileConnectorInfo> generatedProfileConnectors();
+
+ public abstract ImmutableSet<UserConnectorInfo> generatedUserConnectors();
+
+ public abstract ImmutableSet<ProviderClassInfo> providers();
+
+ public abstract ImmutableSet<CrossProfileTypeInfo> crossProfileTypes();
+
+ public abstract ImmutableSet<ExecutableElement> crossProfileMethods();
+
+ public abstract ImmutableSet<CrossProfileCallbackInterfaceInfo> crossProfileCallbackInterfaces();
+
+ public abstract ImmutableSet<CrossProfileTestInfo> crossProfileTests();
+
+ @AutoValue.Builder
+ abstract static class Builder {
+ abstract Builder setProcessingEnv(ProcessingEnvironment processingEnv);
+
+ abstract Builder setElements(Elements elements);
+
+ abstract Builder setTypes(Types types);
+
+ abstract Builder setConfigurations(Collection<CrossProfileConfigurationInfo> configurations);
+
+ abstract Builder setGeneratedProfileConnectors(
+ Collection<ProfileConnectorInfo> generatedProfileConnectors);
+
+ abstract Builder setGeneratedUserConnectors(
+ Collection<UserConnectorInfo> generatedUserConnectors);
+
+ abstract Builder setProviders(Collection<ProviderClassInfo> providers);
+
+ abstract Builder setCrossProfileTypes(Collection<CrossProfileTypeInfo> crossProfileTypes);
+
+ abstract Builder setCrossProfileMethods(Collection<ExecutableElement> crossProfileMethods);
+
+ abstract Builder setCrossProfileCallbackInterfaces(
+ Collection<CrossProfileCallbackInterfaceInfo> crossProfileCallbackInterfaces);
+
+ abstract Builder setCrossProfileTests(Collection<CrossProfileTestInfo> crossProfileTests);
+
+ abstract GeneratorContext build();
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ParcelableWrapper.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ParcelableWrapper.java
new file mode 100644
index 0000000..114abfb
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ParcelableWrapper.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.containers;
+
+import static com.google.android.enterprise.connectedapps.processor.ProtoParcelableWrapperGenerator.getGeneratedProtoWrapperClassName;
+import static java.util.stream.Collectors.toSet;
+
+import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper;
+import com.google.android.enterprise.connectedapps.processor.TypeUtils;
+import com.google.auto.value.AutoValue;
+import com.squareup.javapoet.ClassName;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
+
+/** Information about a Parcelable Wrapper. */
+@AutoValue
+public abstract class ParcelableWrapper {
+
+ /** The type of the Wrapper. This controls how supporting code is generated. */
+ public enum WrapperType {
+ DEFAULT, // Copied from a resource
+ PROTO, // Generated by ProtoParcelableWrapperGenerator
+ CUSTOM // Included in classpath
+ }
+
+ public static final String PARCELABLE_WRAPPER_PACKAGE =
+ "com.google.android.enterprise.connectedapps.parcelablewrappers";
+
+ public abstract TypeMirror wrappedType();
+
+ public abstract ClassName defaultWrapperClassName();
+
+ public abstract ClassName wrapperClassName();
+
+ public abstract WrapperType wrapperType();
+
+ private static ParcelableWrapper create(
+ TypeMirror wrappedType, ClassName defaultWrapperClassName, WrapperType wrapperType) {
+ return create(wrappedType, defaultWrapperClassName, defaultWrapperClassName, wrapperType);
+ }
+
+ public static ParcelableWrapper create(
+ TypeMirror wrappedType,
+ ClassName defaultWrapperClassName,
+ ClassName wrapperClassName,
+ WrapperType wrapperType) {
+ return new AutoValue_ParcelableWrapper(
+ wrappedType, defaultWrapperClassName, wrapperClassName, wrapperType);
+ }
+
+ public static Collection<ParcelableWrapper> createCustomParcelableWrappers(
+ Types types, Elements elements, Collection<TypeElement> customParcelableWrappers) {
+ Collection<ParcelableWrapper> wrappers = new ArrayList<>();
+
+ addCustomParcelableWrappers(types, wrappers, customParcelableWrappers);
+
+ return wrappers;
+ }
+
+ public static Collection<ParcelableWrapper> createGlobalParcelableWrappers(
+ Types types, Elements elements, Collection<ExecutableElement> methods) {
+ Collection<ParcelableWrapper> wrappers = new ArrayList<>();
+
+ addDefaultParcelableWrappers(types, elements, wrappers);
+
+ Collection<TypeMirror> usedTypes = extractTypesFromMethods(methods);
+
+ addGeneratedProtoParcelableWrappers(types, elements, wrappers, usedTypes);
+
+ return wrappers;
+ }
+
+ private static Collection<TypeMirror> extractTypesFromMethods(
+ Collection<ExecutableElement> methods) {
+ return methods.stream()
+ .flatMap(m -> extractReturnTypeAndParameters(m).stream())
+ .flatMap(t -> extractTypeArgumentsIfWrapped(t).stream())
+ .collect(toSet());
+ }
+
+ private static Collection<TypeMirror> extractReturnTypeAndParameters(ExecutableElement method) {
+ Collection<TypeMirror> types = new HashSet<>();
+ types.add(method.getReturnType());
+ types.addAll(method.getParameters().stream().map(Element::asType).collect(toSet()));
+ return types;
+ }
+
+ private static Collection<TypeMirror> extractTypeArgumentsIfWrapped(TypeMirror type) {
+ if (TypeUtils.isGeneric(type)) {
+ return extractTypeArgumentsFromGeneric(type);
+ }
+ if (TypeUtils.isArray(type)) {
+ return extractTypeArgumentsIfWrapped(TypeUtils.extractTypeFromArray(type));
+ }
+
+ return Collections.singleton(type);
+ }
+
+ private static Collection<TypeMirror> extractTypeArgumentsFromGeneric(TypeMirror type) {
+ Collection<TypeMirror> types = new HashSet<>();
+ types.add(TypeUtils.removeTypeArguments(type));
+
+ types.addAll(
+ TypeUtils.extractTypeArguments(type).stream()
+ .flatMap(t -> extractTypeArgumentsIfWrapped(t).stream())
+ .collect(toSet()));
+ return types;
+ }
+
+ private static void addCustomParcelableWrappers(
+ Types types,
+ Collection<ParcelableWrapper> wrappers,
+ Collection<TypeElement> customParcelableWrappers) {
+ for (TypeElement parcelableWrapper : customParcelableWrappers) {
+ addCustomParcelableWrapper(types, wrappers, parcelableWrapper);
+ }
+ }
+
+ private static void addCustomParcelableWrapper(
+ Types types, Collection<ParcelableWrapper> wrappers, TypeElement parcelableWrapper) {
+
+ CustomParcelableWrapper customParcelableWrapperAnnotation =
+ parcelableWrapper.getAnnotation(CustomParcelableWrapper.class);
+
+ if (customParcelableWrapperAnnotation == null) {
+ // This will be dealt with as part of early validation
+ return;
+ }
+
+ ParcelableWrapperAnnotationInfo annotationInfo =
+ ParcelableWrapperAnnotationInfo.extractFromParcelableWrapperAnnotation(
+ types, customParcelableWrapperAnnotation);
+ wrappers.add(
+ ParcelableWrapper.create(
+ annotationInfo.originalType().asType(),
+ ClassName.get(parcelableWrapper),
+ WrapperType.CUSTOM));
+ }
+
+ private static void addDefaultParcelableWrappers(
+ Types types, Elements elements, Collection<ParcelableWrapper> wrappers) {
+ tryAddWrapper(
+ elements,
+ wrappers,
+ "java.util.Collection",
+ ClassName.get(PARCELABLE_WRAPPER_PACKAGE, "ParcelableCollection"));
+
+ tryAddWrapper(
+ elements,
+ wrappers,
+ "java.util.List",
+ ClassName.get(PARCELABLE_WRAPPER_PACKAGE, "ParcelableList"));
+
+ tryAddWrapper(
+ elements,
+ wrappers,
+ "java.util.Map",
+ ClassName.get(PARCELABLE_WRAPPER_PACKAGE, "ParcelableMap"));
+
+ tryAddWrapper(
+ elements,
+ wrappers,
+ "java.util.Set",
+ ClassName.get(PARCELABLE_WRAPPER_PACKAGE, "ParcelableSet"));
+
+ tryAddWrapper(
+ elements,
+ wrappers,
+ "java.util.Optional",
+ ClassName.get(PARCELABLE_WRAPPER_PACKAGE, "ParcelableOptional"));
+
+ tryAddWrapper(
+ elements,
+ wrappers,
+ "com.google.common.base.Optional",
+ ClassName.get(PARCELABLE_WRAPPER_PACKAGE, "ParcelableGuavaOptional"));
+
+ tryAddWrapper(
+ elements,
+ wrappers,
+ "com.google.common.collect.ImmutableMap",
+ ClassName.get(PARCELABLE_WRAPPER_PACKAGE, "ParcelableImmutableMap"));
+
+ tryAddWrapper(
+ elements,
+ wrappers,
+ "android.util.Pair",
+ ClassName.get(PARCELABLE_WRAPPER_PACKAGE, "ParcelablePair"));
+
+ tryAddWrapper(
+ elements,
+ wrappers,
+ "android.graphics.Bitmap",
+ ClassName.get(PARCELABLE_WRAPPER_PACKAGE, "ParcelableBitmap"));
+
+ addArrayWrappers(types, elements, wrappers);
+ }
+
+ private static void addGeneratedProtoParcelableWrappers(
+ Types types,
+ Elements elements,
+ Collection<ParcelableWrapper> wrappers,
+ Collection<TypeMirror> usedTypes) {
+ TypeElement protoElement = elements.getTypeElement("com.google.protobuf.MessageLite");
+ if (protoElement == null) {
+ // Protos are not included at compile-time
+ return;
+ }
+ TypeMirror proto = protoElement.asType();
+
+ Collection<TypeMirror> protoTypes =
+ usedTypes.stream()
+ // <any> is the value when the compiler encounters a type which isn't accessible
+ // or does not exist. This passes the types.isAssignable filter, which makes such
+ // bugs hard to debug. This will already fail because the Java compiler won't allow
+ // it - so this is just to suppress strange test failures
+ .filter(t -> !t.toString().equals("<any>"))
+ .filter(t -> types.isAssignable(t, proto))
+ .collect(toSet());
+
+ for (TypeMirror protoType : protoTypes) {
+ wrappers.add(
+ ParcelableWrapper.create(
+ protoType, getGeneratedProtoWrapperClassName(protoType), WrapperType.PROTO));
+ }
+ }
+
+ private static void addArrayWrappers(
+ Types types, Elements elements, Collection<ParcelableWrapper> wrappers) {
+ TypeElement typeElement = elements.getTypeElement("java.lang.Object");
+ TypeMirror typeMirror = types.getArrayType(typeElement.asType());
+
+ ClassName wrapperClassName = ClassName.get(PARCELABLE_WRAPPER_PACKAGE, "ParcelableArray");
+
+ wrappers.add(ParcelableWrapper.create(typeMirror, wrapperClassName, WrapperType.DEFAULT));
+ }
+
+ private static void tryAddWrapper(
+ Elements elements,
+ Collection<ParcelableWrapper> wrappers,
+ String typeQualifiedName,
+ ClassName wrapperClassName) {
+ TypeElement typeElement = elements.getTypeElement(typeQualifiedName);
+
+ if (typeElement == null) {
+ // The type isn't supported at compile-time - so won't be included in this app
+ return;
+ }
+
+ wrappers.add(
+ ParcelableWrapper.create(typeElement.asType(), wrapperClassName, WrapperType.DEFAULT));
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ParcelableWrapperAnnotationInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ParcelableWrapperAnnotationInfo.java
new file mode 100644
index 0000000..d9e7949
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ParcelableWrapperAnnotationInfo.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.containers;
+
+import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper;
+import com.google.android.enterprise.connectedapps.processor.GeneratorUtilities;
+import com.google.auto.value.AutoValue;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.util.Types;
+
+/** Wrapper around information contained in a {@link CustomParcelableWrapper} annotation. */
+@AutoValue
+public abstract class ParcelableWrapperAnnotationInfo {
+
+ public abstract TypeElement originalType();
+
+ public static ParcelableWrapperAnnotationInfo extractFromParcelableWrapperAnnotation(
+ Types types, CustomParcelableWrapper customParcelableWrapperAnnotation) {
+ if (customParcelableWrapperAnnotation == null) {
+ throw new NullPointerException("parcelableWrapperAnnotation must not be null");
+ }
+
+ TypeElement originalType =
+ GeneratorUtilities.extractClassFromAnnotation(
+ types, customParcelableWrapperAnnotation::originalType);
+
+ return new AutoValue_ParcelableWrapperAnnotationInfo(originalType);
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ProfileConnectorInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ProfileConnectorInfo.java
new file mode 100644
index 0000000..9a68099
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ProfileConnectorInfo.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.containers;
+
+import com.google.android.enterprise.connectedapps.annotations.AvailabilityRestrictions;
+import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector;
+import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector.ProfileType;
+import com.google.android.enterprise.connectedapps.processor.GeneratorUtilities;
+import com.google.android.enterprise.connectedapps.processor.SupportedTypes;
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableSet;
+import com.squareup.javapoet.ClassName;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.element.PackageElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.util.Elements;
+
+/** Wrapper of an interface used as a profile connector. */
+@AutoValue
+public abstract class ProfileConnectorInfo {
+
+ @AutoValue
+ abstract static class CustomProfileConnectorAnnotationInfo {
+ abstract ProfileType primaryProfile();
+
+ abstract ClassName serviceName();
+
+ abstract ImmutableCollection<TypeElement> parcelableWrapperClasses();
+
+ abstract ImmutableCollection<TypeElement> futureWrapperClasses();
+
+ abstract ImmutableCollection<TypeElement> importsClasses();
+
+ abstract AvailabilityRestrictions availabilityRestrictions();
+ }
+
+ public abstract TypeElement connectorElement();
+
+ public ClassName connectorClassName() {
+ return ClassName.get(connectorElement());
+ }
+
+ public abstract ProfileType primaryProfile();
+
+ public abstract ClassName serviceName();
+
+ public abstract SupportedTypes supportedTypes();
+
+ public abstract ImmutableCollection<TypeElement> parcelableWrapperClasses();
+
+ public abstract ImmutableCollection<TypeElement> futureWrapperClasses();
+
+ public abstract ImmutableCollection<TypeElement> importsClasses();
+
+ public abstract AvailabilityRestrictions availabilityRestrictions();
+
+ public static ProfileConnectorInfo create(
+ ProcessingEnvironment processingEnv,
+ TypeElement connectorElement,
+ SupportedTypes globalSupportedTypes) {
+
+ Elements elements = processingEnv.getElementUtils();
+
+ CustomProfileConnectorAnnotationInfo annotationInfo =
+ extractFromCustomProfileConnectorAnnotation(processingEnv, elements, connectorElement);
+
+ Set<TypeElement> parcelableWrappers = new HashSet<>(annotationInfo.parcelableWrapperClasses());
+ Set<TypeElement> futureWrappers = new HashSet<>(annotationInfo.futureWrapperClasses());
+
+ for (TypeElement importConnectorClass : annotationInfo.importsClasses()) {
+ ProfileConnectorInfo importConnector =
+ ProfileConnectorInfo.create(processingEnv, importConnectorClass, globalSupportedTypes);
+ parcelableWrappers.addAll(importConnector.parcelableWrapperClasses());
+ futureWrappers.addAll(importConnector.futureWrapperClasses());
+ }
+
+ return new AutoValue_ProfileConnectorInfo(
+ connectorElement,
+ annotationInfo.primaryProfile(),
+ annotationInfo.serviceName(),
+ globalSupportedTypes
+ .asBuilder()
+ .addParcelableWrappers(
+ ParcelableWrapper.createCustomParcelableWrappers(
+ processingEnv.getTypeUtils(),
+ processingEnv.getElementUtils(),
+ parcelableWrappers))
+ .addFutureWrappers(
+ FutureWrapper.createCustomFutureWrappers(
+ processingEnv.getTypeUtils(), processingEnv.getElementUtils(), futureWrappers))
+ .build(),
+ ImmutableSet.copyOf(parcelableWrappers),
+ ImmutableSet.copyOf(futureWrappers),
+ annotationInfo.importsClasses(),
+ annotationInfo.availabilityRestrictions());
+ }
+
+ private static CustomProfileConnectorAnnotationInfo extractFromCustomProfileConnectorAnnotation(
+ ProcessingEnvironment processingEnv, Elements elements, TypeElement connectorElement) {
+ CustomProfileConnector customProfileConnector =
+ connectorElement.getAnnotation(CustomProfileConnector.class);
+
+ if (customProfileConnector == null) {
+ return new AutoValue_ProfileConnectorInfo_CustomProfileConnectorAnnotationInfo(
+ ProfileType.NONE,
+ getDefaultServiceName(elements, connectorElement),
+ ImmutableSet.of(),
+ ImmutableSet.of(),
+ ImmutableSet.of(),
+ AvailabilityRestrictions.DEFAULT);
+ }
+
+ Collection<TypeElement> parcelableWrappers =
+ GeneratorUtilities.extractClassesFromAnnotation(
+ processingEnv.getTypeUtils(), customProfileConnector::parcelableWrappers);
+ Collection<TypeElement> futureWrappers =
+ GeneratorUtilities.extractClassesFromAnnotation(
+ processingEnv.getTypeUtils(), customProfileConnector::futureWrappers);
+ Collection<TypeElement> imports =
+ GeneratorUtilities.extractClassesFromAnnotation(
+ processingEnv.getTypeUtils(), customProfileConnector::imports);
+
+ String serviceClassName = customProfileConnector.serviceClassName();
+
+ return new AutoValue_ProfileConnectorInfo_CustomProfileConnectorAnnotationInfo(
+ customProfileConnector.primaryProfile(),
+ serviceClassName.isEmpty()
+ ? getDefaultServiceName(elements, connectorElement)
+ : ClassName.bestGuess(serviceClassName),
+ ImmutableSet.copyOf(parcelableWrappers),
+ ImmutableSet.copyOf(futureWrappers),
+ ImmutableSet.copyOf(imports),
+ customProfileConnector.availabilityRestrictions());
+ }
+
+ public static ClassName getDefaultServiceName(Elements elements, TypeElement connectorElement) {
+ PackageElement originalPackage = elements.getPackageOf(connectorElement);
+
+ return ClassName.get(
+ originalPackage.getQualifiedName().toString(),
+ String.format("%s_Service", connectorElement.getSimpleName().toString()));
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ProviderClassInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ProviderClassInfo.java
new file mode 100644
index 0000000..02adf0c
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ProviderClassInfo.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.containers;
+
+import static com.google.android.enterprise.connectedapps.processor.GeneratorUtilities.findCrossProfileProviderMethodsInClass;
+import static java.util.stream.Collectors.toSet;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.squareup.javapoet.ClassName;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.util.Elements;
+
+/** Wrapper of a cross-profile provider class. */
+@AutoValue
+public abstract class ProviderClassInfo {
+
+ public abstract TypeElement providerClassElement();
+
+ public ImmutableCollection<CrossProfileTypeInfo> allCrossProfileTypes() {
+ Set<CrossProfileTypeInfo> types = new HashSet<>();
+ types.addAll(nonStaticTypes());
+ types.addAll(staticTypes());
+ return ImmutableSet.copyOf(types);
+ }
+
+ public abstract ImmutableCollection<CrossProfileTypeInfo> nonStaticTypes();
+
+ public abstract ImmutableCollection<CrossProfileTypeInfo> staticTypes();
+
+ public String simpleName() {
+ return providerClassElement().getSimpleName().toString();
+ }
+
+ public ClassName className() {
+ return ClassName.get(providerClassElement());
+ }
+
+ public ImmutableCollection<VariableElement> publicConstructorArgumentTypes() {
+ return ImmutableList.copyOf(
+ providerClassElement().getEnclosedElements().stream()
+ .filter(e -> e instanceof ExecutableElement)
+ .map(e -> (ExecutableElement) e)
+ .filter(e -> e.getKind().equals(ElementKind.CONSTRUCTOR))
+ .filter(e -> e.getModifiers().contains(Modifier.PUBLIC))
+ .findFirst()
+ .get()
+ .getParameters());
+ }
+
+ public ExecutableElement findProviderMethodFor(
+ GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) {
+ if (!nonStaticTypes().contains(crossProfileType)) {
+ throw new IllegalArgumentException("This provider class does not provide this type");
+ }
+
+ return providerClassElement().getEnclosedElements().stream()
+ .filter(e -> e instanceof ExecutableElement)
+ .map(e -> (ExecutableElement) e)
+ .filter(
+ e ->
+ generatorContext
+ .types()
+ .isSameType(
+ e.getReturnType(), crossProfileType.crossProfileTypeElement().asType()))
+ .findFirst()
+ .get();
+ }
+
+ public static ProviderClassInfo create(
+ ValidatorContext context, ValidatorProviderClassInfo provider) {
+ Set<CrossProfileTypeInfo> nonStaticTypes =
+ extractCrossProfileTypeElementsFromReturnValues(
+ context.elements(), provider.providerClassElement())
+ .stream()
+ .map(
+ crossProfileTypeElement ->
+ ValidatorCrossProfileTypeInfo.create(
+ context.processingEnv(),
+ crossProfileTypeElement,
+ context.globalSupportedTypes()))
+ .map(crossProfileType -> CrossProfileTypeInfo.create(context, crossProfileType))
+ .collect(toSet());
+
+ Set<CrossProfileTypeInfo> staticTypes =
+ provider.staticTypes().stream()
+ .map(
+ crossProfileTypeElement ->
+ ValidatorCrossProfileTypeInfo.create(
+ context.processingEnv(),
+ crossProfileTypeElement,
+ context.globalSupportedTypes()))
+ .map(crossProfileType -> CrossProfileTypeInfo.create(context, crossProfileType))
+ .collect(toSet());
+
+ return new AutoValue_ProviderClassInfo(
+ provider.providerClassElement(),
+ ImmutableSet.copyOf(nonStaticTypes),
+ ImmutableSet.copyOf(staticTypes));
+ }
+
+ public static Collection<TypeElement> extractCrossProfileTypeElementsFromReturnValues(
+ Elements elements, TypeElement providerClassElement) {
+ return findCrossProfileProviderMethodsInClass(providerClassElement).stream()
+ .map(e -> elements.getTypeElement(e.getReturnType().toString()))
+ .collect(toSet());
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/Type.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/Type.java
new file mode 100644
index 0000000..d81f6cd
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/Type.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.containers;
+
+import com.google.android.enterprise.connectedapps.processor.TypeUtils;
+import com.google.auto.value.AutoValue;
+import java.util.Optional;
+import javax.lang.model.type.TypeMirror;
+
+/** A type which may be supported by a given {@code CrossProfileConfiguration}. */
+@AutoValue
+public abstract class Type {
+ public static Builder builder() {
+ return new AutoValue_Type.Builder()
+ .setAcceptableParameterType(false)
+ .setAcceptableReturnType(false)
+ .setSupportedWithAnyGenericType(false)
+ .setSupportedInsideWrapper(true)
+ .setSupportedInsideCrossProfileCallback(true);
+ }
+
+ public abstract Builder toBuilder();
+
+ public abstract TypeMirror getTypeMirror();
+
+ public String getQualifiedName() {
+ return getTypeMirror().toString();
+ }
+
+ public abstract boolean isAcceptableReturnType();
+
+ public abstract boolean isAcceptableParameterType();
+
+ public abstract Optional<String> getAutomaticallyResolvedReplacement();
+
+ public boolean isArray() {
+ return TypeUtils.isArray(getTypeMirror());
+ }
+
+ public boolean canBeBundled() {
+ return getWriteToParcelCode().isPresent() && getReadFromParcelCode().isPresent();
+ }
+
+ public boolean isPrimitive() {
+ return getTypeMirror().getKind().isPrimitive();
+ }
+
+ public boolean isGeneric() {
+ return TypeUtils.isGeneric(getTypeMirror());
+ }
+
+ /**
+ * If this is set, then type arguments will not validated.
+ *
+ * <p>This allows for Parcelables which take responsibility for their own generics and do not use
+ * Bundler.
+ */
+ public abstract boolean isSupportedWithAnyGenericType();
+
+ /**
+ * Can this type be used inside a wrapper type? For example a List or an array.
+ *
+ * <p>This allows for async listeners to only be acceptable as parameter types but not type
+ * arguments
+ */
+ public abstract boolean isSupportedInsideWrapper();
+
+ public abstract boolean isSupportedInsideCrossProfileCallback();
+
+ public abstract Optional<FutureWrapper> getFutureWrapper();
+
+ public boolean isFuture() {
+ return getFutureWrapper().isPresent();
+ }
+
+ public abstract Optional<CrossProfileCallbackInterfaceInfo> getCrossProfileCallbackInterface();
+
+ public boolean isCrossProfileCallbackInterface() {
+ return getCrossProfileCallbackInterface().isPresent();
+ }
+
+ // If this is a generated Parcelable Wrapper then this will be set to the simple name
+ // (e.g. ParcelableList)
+ public abstract Optional<ParcelableWrapper> getParcelableWrapper();
+
+ public abstract Optional<String> getWriteToParcelCode();
+
+ public abstract Optional<String> getReadFromParcelCode();
+
+ /** A builder for {@link Type}. */
+ @AutoValue.Builder
+ public abstract static class Builder {
+
+ public abstract Builder setTypeMirror(TypeMirror typeMirror);
+
+ public abstract Builder setAcceptableReturnType(boolean acceptableReturnType);
+
+ public abstract Builder setAcceptableParameterType(boolean acceptableParameterType);
+
+ public abstract Builder setAutomaticallyResolvedReplacement(
+ String automaticallyResolvedReplacement);
+
+ public abstract Builder setSupportedWithAnyGenericType(boolean supportedWithAnyGenericType);
+
+ public abstract Builder setSupportedInsideWrapper(boolean supportedInsideWrapper);
+
+ public abstract Builder setSupportedInsideCrossProfileCallback(
+ boolean supportedInsideCrossProfileCallback);
+
+ public abstract Builder setFutureWrapper(FutureWrapper futureWrapper);
+
+ public abstract Builder setCrossProfileCallbackInterface(
+ CrossProfileCallbackInterfaceInfo crossProfileCallbackInterface);
+
+ public abstract Builder setWriteToParcelCode(String writeToParcelCode);
+
+ public abstract Builder setReadFromParcelCode(String readFromParcelCode);
+
+ public abstract Builder setParcelableWrapper(ParcelableWrapper parcelableWrapper);
+
+ public abstract Type build();
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/UserConnectorInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/UserConnectorInfo.java
new file mode 100644
index 0000000..33a4d56
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/UserConnectorInfo.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.containers;
+
+import com.google.android.enterprise.connectedapps.annotations.AvailabilityRestrictions;
+import com.google.android.enterprise.connectedapps.annotations.CustomUserConnector;
+import com.google.android.enterprise.connectedapps.processor.GeneratorUtilities;
+import com.google.android.enterprise.connectedapps.processor.SupportedTypes;
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableSet;
+import com.squareup.javapoet.ClassName;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.element.PackageElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.util.Elements;
+
+/** Wrapper of an interface used as a user connector. */
+@AutoValue
+public abstract class UserConnectorInfo {
+
+ @AutoValue
+ abstract static class CustomUserConnectorAnnotationInfo {
+ abstract ClassName serviceName();
+
+ abstract ImmutableCollection<TypeElement> parcelableWrapperClasses();
+
+ abstract ImmutableCollection<TypeElement> futureWrapperClasses();
+
+ abstract ImmutableCollection<TypeElement> importsClasses();
+
+ abstract AvailabilityRestrictions availabilityRestrictions();
+ }
+
+ public abstract TypeElement connectorElement();
+
+ public ClassName connectorClassName() {
+ return ClassName.get(connectorElement());
+ }
+
+ public abstract ClassName serviceName();
+
+ public abstract SupportedTypes supportedTypes();
+
+ public abstract ImmutableCollection<TypeElement> parcelableWrapperClasses();
+
+ public abstract ImmutableCollection<TypeElement> futureWrapperClasses();
+
+ public abstract ImmutableCollection<TypeElement> importsClasses();
+
+ public abstract AvailabilityRestrictions availabilityRestrictions();
+
+ public static UserConnectorInfo create(
+ ProcessingEnvironment processingEnv,
+ TypeElement connectorElement,
+ SupportedTypes globalSupportedTypes) {
+ Elements elements = processingEnv.getElementUtils();
+ CustomUserConnectorAnnotationInfo annotationInfo =
+ extractFromCustomUserConnectorAnnotation(processingEnv, elements, connectorElement);
+
+ Set<TypeElement> parcelableWrappers = new HashSet<>(annotationInfo.parcelableWrapperClasses());
+ Set<TypeElement> futureWrappers = new HashSet<>(annotationInfo.futureWrapperClasses());
+
+ for (TypeElement importConnectorClass : annotationInfo.importsClasses()) {
+ UserConnectorInfo importConnector =
+ UserConnectorInfo.create(processingEnv, importConnectorClass, globalSupportedTypes);
+ parcelableWrappers.addAll(importConnector.parcelableWrapperClasses());
+ futureWrappers.addAll(importConnector.futureWrapperClasses());
+ }
+
+ return new AutoValue_UserConnectorInfo(
+ connectorElement,
+ annotationInfo.serviceName(),
+ globalSupportedTypes
+ .asBuilder()
+ .addParcelableWrappers(
+ ParcelableWrapper.createCustomParcelableWrappers(
+ processingEnv.getTypeUtils(),
+ processingEnv.getElementUtils(),
+ parcelableWrappers))
+ .addFutureWrappers(
+ FutureWrapper.createCustomFutureWrappers(
+ processingEnv.getTypeUtils(), processingEnv.getElementUtils(), futureWrappers))
+ .build(),
+ ImmutableSet.copyOf(parcelableWrappers),
+ ImmutableSet.copyOf(futureWrappers),
+ annotationInfo.importsClasses(),
+ annotationInfo.availabilityRestrictions());
+ }
+
+ private static CustomUserConnectorAnnotationInfo extractFromCustomUserConnectorAnnotation(
+ ProcessingEnvironment processingEnv, Elements elements, TypeElement connectorElement) {
+ CustomUserConnector customUserConnector =
+ connectorElement.getAnnotation(CustomUserConnector.class);
+
+ if (customUserConnector == null) {
+ return new AutoValue_UserConnectorInfo_CustomUserConnectorAnnotationInfo(
+ getDefaultServiceName(elements, connectorElement),
+ ImmutableSet.of(),
+ ImmutableSet.of(),
+ ImmutableSet.of(),
+ AvailabilityRestrictions.DEFAULT);
+ }
+
+ Collection<TypeElement> parcelableWrappers =
+ GeneratorUtilities.extractClassesFromAnnotation(
+ processingEnv.getTypeUtils(), customUserConnector::parcelableWrappers);
+ Collection<TypeElement> futureWrappers =
+ GeneratorUtilities.extractClassesFromAnnotation(
+ processingEnv.getTypeUtils(), customUserConnector::futureWrappers);
+ Collection<TypeElement> imports =
+ GeneratorUtilities.extractClassesFromAnnotation(
+ processingEnv.getTypeUtils(), customUserConnector::imports);
+
+ String serviceClassName = customUserConnector.serviceClassName();
+
+ return new AutoValue_UserConnectorInfo_CustomUserConnectorAnnotationInfo(
+ serviceClassName.isEmpty()
+ ? getDefaultServiceName(elements, connectorElement)
+ : ClassName.bestGuess(serviceClassName),
+ ImmutableSet.copyOf(parcelableWrappers),
+ ImmutableSet.copyOf(futureWrappers),
+ ImmutableSet.copyOf(imports),
+ customUserConnector.availabilityRestrictions());
+ }
+
+ public static ClassName getDefaultServiceName(Elements elements, TypeElement connectorElement) {
+ PackageElement originalPackage = elements.getPackageOf(connectorElement);
+
+ return ClassName.get(
+ originalPackage.getQualifiedName().toString(),
+ String.format("%s_Service", connectorElement.getSimpleName().toString()));
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorContext.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorContext.java
new file mode 100644
index 0000000..e13f0c5
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorContext.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.containers;
+
+import com.google.android.enterprise.connectedapps.processor.SupportedTypes;
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableSet;
+import java.util.Collection;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
+
+/**
+ * Context for connected apps code validators.
+ *
+ * <p>This is used to validate enough that a {@link GeneratorContext} can be created for further
+ * validation and generation.
+ */
+@AutoValue
+public abstract class ValidatorContext extends Context {
+
+ public static Builder builder() {
+ return new AutoValue_ValidatorContext.Builder();
+ }
+
+ public abstract SupportedTypes globalSupportedTypes();
+
+ public abstract ImmutableSet<ProfileConnectorInfo> newProfileConnectorInterfaces();
+
+ public abstract ImmutableSet<UserConnectorInfo> newUserConnectorInterfaces();
+
+ public abstract ImmutableSet<TypeElement> newGeneratedProfileConnectors();
+
+ public abstract ImmutableSet<TypeElement> newGeneratedUserConnectors();
+
+ public abstract ImmutableSet<ValidatorCrossProfileConfigurationInfo> newConfigurations();
+
+ public abstract ImmutableSet<ValidatorCrossProfileTypeInfo> newCrossProfileTypes();
+
+ public abstract ImmutableSet<ExecutableElement> newCrossProfileMethods();
+
+ public abstract ImmutableSet<ValidatorProviderClassInfo> newProviderClasses();
+
+ public abstract ImmutableSet<ExecutableElement> newProviderMethods();
+
+ public abstract ImmutableSet<TypeElement> newCrossProfileCallbackInterfaces();
+
+ public abstract ImmutableSet<ValidatorCrossProfileTestInfo> newCrossProfileTests();
+
+ public abstract ImmutableSet<TypeElement> newCustomParcelableWrappers();
+
+ public abstract ImmutableSet<TypeElement> newCustomFutureWrappers();
+
+ /** A builder for {@link ValidatorContext}. */
+ @AutoValue.Builder
+ public abstract static class Builder {
+ public abstract Builder setProcessingEnv(ProcessingEnvironment processingEnv);
+
+ public abstract Builder setElements(Elements elements);
+
+ public abstract Builder setTypes(Types types);
+
+ public abstract Builder setGlobalSupportedTypes(SupportedTypes globalSupportedTypes);
+
+ public abstract Builder setNewProfileConnectorInterfaces(
+ Collection<ProfileConnectorInfo> newProfileConnectorInterfaces);
+
+ public abstract Builder setNewUserConnectorInterfaces(
+ Collection<UserConnectorInfo> newUserConnectorInterfaces);
+
+ public abstract Builder setNewGeneratedProfileConnectors(
+ Collection<TypeElement> newGeneratedConnectors);
+
+ public abstract Builder setNewGeneratedUserConnectors(
+ Collection<TypeElement> newGeneratedUserConnectors);
+
+ public abstract Builder setNewConfigurations(
+ Collection<ValidatorCrossProfileConfigurationInfo> newConfigurations);
+
+ public abstract Builder setNewCrossProfileTypes(
+ Collection<ValidatorCrossProfileTypeInfo> newCrossProfileTypes);
+
+ public abstract Builder setNewCrossProfileMethods(
+ Collection<ExecutableElement> newCrossProfileMethods);
+
+ public abstract Builder setNewProviderClasses(
+ Collection<ValidatorProviderClassInfo> newProviderClasses);
+
+ public abstract Builder setNewProviderMethods(Collection<ExecutableElement> newProviderMethods);
+
+ public abstract Builder setNewCrossProfileCallbackInterfaces(
+ Collection<TypeElement> newCrossProfileCallbackInterfaces);
+
+ public abstract Builder setNewCrossProfileTests(
+ Collection<ValidatorCrossProfileTestInfo> newCrossProfileTests);
+
+ public abstract Builder setNewCustomParcelableWrappers(
+ Collection<TypeElement> newCustomParcelableWrappers);
+
+ public abstract Builder setNewCustomFutureWrappers(
+ Collection<TypeElement> newCustomFutureWrappers);
+
+ public abstract ValidatorContext build();
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileConfigurationInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileConfigurationInfo.java
new file mode 100644
index 0000000..973714c
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileConfigurationInfo.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.containers;
+
+import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.SERVICE_CLASSNAME;
+
+import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration;
+import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder;
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableSet;
+import com.squareup.javapoet.ClassName;
+import java.util.Optional;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.TypeElement;
+
+/** A wrapper around basic information from a {@link CrossProfileConfiguration} annotation. */
+@AutoValue
+public abstract class ValidatorCrossProfileConfigurationInfo {
+
+ public abstract TypeElement configurationElement();
+
+ public abstract ImmutableCollection<TypeElement> providerClassElements();
+
+ public abstract ClassName serviceSuperclass();
+
+ public abstract Optional<TypeElement> serviceClass();
+
+ public abstract Optional<TypeElement> connector();
+
+ public static ImmutableSet<ValidatorCrossProfileConfigurationInfo> createMultipleFromElement(
+ ProcessingEnvironment processingEnvironment, TypeElement annotatedElement) {
+ ImmutableSet<CrossProfileConfigurationAnnotationInfo> infos =
+ AnnotationFinder.extractCrossProfileConfigurationsAnnotationInfo(
+ annotatedElement,
+ processingEnvironment.getTypeUtils(),
+ processingEnvironment.getElementUtils())
+ .configurations();
+ ImmutableSet.Builder<ValidatorCrossProfileConfigurationInfo> configurations =
+ ImmutableSet.builder();
+
+ if (infos.isEmpty()) {
+ configurations.add(createFromElement(processingEnvironment, annotatedElement));
+ } else {
+ for (CrossProfileConfigurationAnnotationInfo info : infos) {
+ configurations.add(createFromAnnotationInfo(info, annotatedElement));
+ }
+ }
+
+ return configurations.build();
+ }
+
+ public static ValidatorCrossProfileConfigurationInfo createFromElement(
+ ProcessingEnvironment processingEnv, TypeElement annotatedElement) {
+ CrossProfileConfigurationAnnotationInfo annotationInfo =
+ extractFromCrossProfileConfigurationAnnotation(annotatedElement, processingEnv);
+
+ return createFromAnnotationInfo(annotationInfo, annotatedElement);
+ }
+
+ private static ValidatorCrossProfileConfigurationInfo createFromAnnotationInfo(
+ CrossProfileConfigurationAnnotationInfo annotationInfo, TypeElement annotatedElement) {
+ ClassName serviceSuperclass =
+ serviceSuperclassIsDefault(annotationInfo.serviceSuperclass())
+ ? SERVICE_CLASSNAME
+ : ClassName.get(annotationInfo.serviceSuperclass());
+
+ TypeElement serviceClass =
+ serviceClassIsDefault(annotationInfo.serviceClass()) ? null : annotationInfo.serviceClass();
+
+ Optional<TypeElement> connector =
+ connectorIsDefault(annotationInfo.connector())
+ ? Optional.empty()
+ : Optional.of(annotationInfo.connector());
+
+ return new AutoValue_ValidatorCrossProfileConfigurationInfo(
+ annotatedElement,
+ ImmutableSet.copyOf(annotationInfo.providerClasses()),
+ serviceSuperclass,
+ Optional.ofNullable(serviceClass),
+ connector);
+ }
+
+ private static boolean serviceSuperclassIsDefault(TypeElement serviceSuperclass) {
+ // CrossProfileConfiguration.class is the default specified serviceSuperclass
+ return serviceSuperclass
+ .asType()
+ .toString()
+ .equals(CrossProfileConfiguration.class.getCanonicalName());
+ }
+
+ private static boolean serviceClassIsDefault(TypeElement serviceClass) {
+ // CrossProfileConfiguration.class is the default specified serviceClass
+ return serviceClass
+ .asType()
+ .toString()
+ .equals(CrossProfileConfiguration.class.getCanonicalName());
+ }
+
+ private static boolean connectorIsDefault(TypeElement connector) {
+ // CrossProfileConfiguration.class is the default specified connector
+ return connector.asType().toString().equals(CrossProfileConfiguration.class.getCanonicalName());
+ }
+
+ private static CrossProfileConfigurationAnnotationInfo
+ extractFromCrossProfileConfigurationAnnotation(
+ Element annotatedElement, ProcessingEnvironment processingEnv) {
+ return AnnotationFinder.extractCrossProfileConfigurationAnnotationInfo(
+ annotatedElement, processingEnv.getTypeUtils(), processingEnv.getElementUtils());
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileTestInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileTestInfo.java
new file mode 100644
index 0000000..172c8ed
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileTestInfo.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.containers;
+
+import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder;
+import com.google.android.enterprise.connectedapps.testing.annotations.CrossProfileTest;
+import com.google.auto.value.AutoValue;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.element.TypeElement;
+
+/** Wrapper of a {@link CrossProfileTest} annotated class. */
+@AutoValue
+public abstract class ValidatorCrossProfileTestInfo {
+
+ public abstract TypeElement crossProfileTestElement();
+
+ public abstract TypeElement configurationElement();
+
+ public static ValidatorCrossProfileTestInfo create(
+ ProcessingEnvironment processingEnv, TypeElement crossProfileTestElement) {
+ CrossProfileTestAnnotationInfo annotationInfo =
+ AnnotationFinder.extractCrossProfileTestAnnotationInfo(
+ crossProfileTestElement, processingEnv.getTypeUtils(), processingEnv.getElementUtils());
+ return new AutoValue_ValidatorCrossProfileTestInfo(
+ crossProfileTestElement, annotationInfo.configuration());
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileTypeInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileTypeInfo.java
new file mode 100644
index 0000000..c757ba4
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileTypeInfo.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.containers;
+
+import static com.google.android.enterprise.connectedapps.processor.GeneratorUtilities.findCrossProfileMethodsInClass;
+import static java.util.stream.Collectors.toList;
+
+import com.google.android.enterprise.connectedapps.annotations.CrossProfile;
+import com.google.android.enterprise.connectedapps.processor.SupportedTypes;
+import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder;
+import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileAnnotation;
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+
+/** A wrapper around basic information from a {@link CrossProfile} type annotation. */
+@AutoValue
+public abstract class ValidatorCrossProfileTypeInfo {
+
+ public abstract TypeElement crossProfileTypeElement();
+
+ public abstract ImmutableList<ExecutableElement> crossProfileMethods();
+
+ public abstract Optional<ProfileConnectorInfo> profileConnector();
+
+ public abstract SupportedTypes supportedTypes();
+
+ public abstract ImmutableCollection<TypeElement> parcelableWrapperClasses();
+
+ public abstract ImmutableCollection<TypeElement> futureWrapperClasses();
+
+ public abstract String profileClassName();
+
+ public abstract boolean isStatic();
+
+ /**
+ * The specified timeout for async calls, or {@link CrossProfileAnnotation#DEFAULT_TIMEOUT_MILLIS}
+ * if unspecified.
+ */
+ public abstract long timeoutMillis();
+
+ public static ValidatorCrossProfileTypeInfo create(
+ ProcessingEnvironment processingEnv,
+ TypeElement crossProfileTypeElement,
+ SupportedTypes globalSupportedTypes) {
+ CrossProfileAnnotationInfo annotationInfo =
+ AnnotationFinder.extractCrossProfileAnnotationInfo(
+ crossProfileTypeElement, processingEnv.getTypeUtils(), processingEnv.getElementUtils());
+
+ Optional<ProfileConnectorInfo> profileConnectorElement =
+ annotationInfo.connectorIsDefault()
+ ? Optional.empty()
+ : Optional.of(
+ ProfileConnectorInfo.create(
+ processingEnv, annotationInfo.connectorClass(), globalSupportedTypes));
+
+ List<ExecutableElement> crossProfileMethodElements =
+ findCrossProfileMethodsInClass(crossProfileTypeElement).stream()
+ .sorted(Comparator.comparing(i -> i.getSimpleName().toString()))
+ .collect(toList());
+
+ SupportedTypes incomingSupportedTypes =
+ profileConnectorElement.isPresent()
+ ? profileConnectorElement.get().supportedTypes()
+ : globalSupportedTypes;
+
+ SupportedTypes supportedTypes =
+ incomingSupportedTypes
+ .asBuilder()
+ .addParcelableWrappers(
+ ParcelableWrapper.createCustomParcelableWrappers(
+ processingEnv.getTypeUtils(),
+ processingEnv.getElementUtils(),
+ annotationInfo.parcelableWrapperClasses()))
+ .addFutureWrappers(
+ FutureWrapper.createCustomFutureWrappers(
+ processingEnv.getTypeUtils(),
+ processingEnv.getElementUtils(),
+ annotationInfo.futureWrapperClasses()))
+ .build();
+
+ return new AutoValue_ValidatorCrossProfileTypeInfo(
+ crossProfileTypeElement,
+ ImmutableList.copyOf(crossProfileMethodElements),
+ profileConnectorElement,
+ supportedTypes,
+ annotationInfo.parcelableWrapperClasses(),
+ annotationInfo.futureWrapperClasses(),
+ annotationInfo.profileClassName(),
+ annotationInfo.isStatic(),
+ annotationInfo
+ .timeoutMillis()
+ .filter(value -> value != CrossProfileAnnotation.TIMEOUT_MILLIS_NOT_SET)
+ .orElse(CrossProfileAnnotation.DEFAULT_TIMEOUT_MILLIS));
+ }
+}
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorProviderClassInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorProviderClassInfo.java
new file mode 100644
index 0000000..c3ffc28
--- /dev/null
+++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorProviderClassInfo.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.processor.containers;
+
+import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder;
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableSet;
+import com.squareup.javapoet.ClassName;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.element.TypeElement;
+
+/** Wrapper of basic information for a cross-profile provider class. */
+@AutoValue
+public abstract class ValidatorProviderClassInfo {
+
+ public abstract TypeElement providerClassElement();
+
+ public abstract ImmutableCollection<TypeElement> staticTypes();
+
+ public String simpleName() {
+ return providerClassElement().getSimpleName().toString();
+ }
+
+ public ClassName className() {
+ return ClassName.get(providerClassElement());
+ }
+
+ public static ValidatorProviderClassInfo create(
+ ProcessingEnvironment processingEnv, TypeElement providerClassElement) {
+ CrossProfileProviderAnnotationInfo annotationInfo =
+ AnnotationFinder.extractCrossProfileProviderAnnotationInfo(
+ providerClassElement, processingEnv.getTypeUtils(), processingEnv.getElementUtils());
+
+ return new AutoValue_ValidatorProviderClassInfo(
+ providerClassElement, ImmutableSet.copyOf(annotationInfo.staticTypes()));
+ }
+}
diff --git a/processor/src/main/resources/futurewrappers/ListenableFutureWrapper.java b/processor/src/main/resources/futurewrappers/ListenableFutureWrapper.java
new file mode 100644
index 0000000..f9fe728
--- /dev/null
+++ b/processor/src/main/resources/futurewrappers/ListenableFutureWrapper.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.futurewrappers;
+
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+
+import com.google.android.enterprise.connectedapps.FutureWrapper;
+import com.google.android.enterprise.connectedapps.Profile;
+import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException;
+import com.google.android.enterprise.connectedapps.internal.Bundler;
+import com.google.android.enterprise.connectedapps.internal.BundlerType;
+import com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger;
+import com.google.android.enterprise.connectedapps.internal.FutureResultWriter;
+import com.google.common.util.concurrent.FluentFuture;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+import java.util.Map;
+
+/** Wrapper for adding support for {@link ListenableFuture} to the Connected Apps SDK. */
+public final class ListenableFutureWrapper<E> extends FutureWrapper<E> {
+
+ private final SettableFuture<E> future = SettableFuture.create();
+
+ public static <E> ListenableFutureWrapper<E> create(Bundler bundler, BundlerType bundlerType) {
+ return new ListenableFutureWrapper<>(bundler, bundlerType);
+ }
+
+ private ListenableFutureWrapper(Bundler bundler, BundlerType bundlerType) {
+ super(bundler, bundlerType);
+ }
+
+ public ListenableFuture<E> getFuture() {
+ return future;
+ }
+
+ @Override
+ public void onResult(E result) {
+ future.set(result);
+ }
+
+ @Override
+ public void onException(Throwable throwable) {
+ future.setException(throwable);
+ }
+
+ public static <E> void writeFutureResult(
+ ListenableFuture<E> future, FutureResultWriter<E> resultWriter) {
+ FluentFuture.from(future)
+ .addCallback(
+ new FutureCallback<E>() {
+ @Override
+ public void onSuccess(E result) {
+ resultWriter.onSuccess(result);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ resultWriter.onFailure(t);
+ }
+ },
+ directExecutor());
+ }
+
+ private static class MergerFutureCallback<E> implements FutureCallback<E> {
+
+ private final Profile profileId;
+ private final CrossProfileCallbackMultiMerger<E> merger;
+
+ MergerFutureCallback(Profile profileId, CrossProfileCallbackMultiMerger<E> merger) {
+ if (profileId == null || merger == null) {
+ throw new NullPointerException();
+ }
+ this.profileId = profileId;
+ this.merger = merger;
+ }
+
+ @Override
+ public void onSuccess(E result) {
+ merger.onResult(profileId, result);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ // TODO: What should we do with the Throwable?
+ merger.missingResult(profileId);
+ }
+ }
+
+ public static <E> ListenableFuture<Map<Profile, E>> groupResults(
+ Map<Profile, ListenableFuture<E>> results) {
+ SettableFuture<Map<Profile, E>> m = SettableFuture.create();
+ CrossProfileCallbackMultiMerger<E> merger =
+ new CrossProfileCallbackMultiMerger<>(results.size(), m::set);
+ for (Map.Entry<Profile, ListenableFuture<E>> result : results.entrySet()) {
+ FluentFuture.from(result.getValue())
+ .catching(
+ UnavailableProfileException.class,
+ (throwable) -> {
+ merger.missingResult(result.getKey());
+ return null; // This will be passed into the callback but will be rejected by merger
+ // as duplicate
+ },
+ directExecutor())
+ .addCallback(new MergerFutureCallback<>(result.getKey(), merger), directExecutor());
+ }
+ return m;
+ }
+}
diff --git a/processor/src/main/resources/parcelablewrappers/ParcelableArray.java b/processor/src/main/resources/parcelablewrappers/ParcelableArray.java
new file mode 100644
index 0000000..c1cf335
--- /dev/null
+++ b/processor/src/main/resources/parcelablewrappers/ParcelableArray.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.parcelablewrappers;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.google.android.enterprise.connectedapps.internal.Bundler;
+import com.google.android.enterprise.connectedapps.internal.BundlerType;
+
+/** Wrapper for reading & writing arrays from and to {@link Parcel} instances. */
+public class ParcelableArray<E> implements Parcelable {
+
+ private static final int NULL_SIZE = -1;
+
+ private final Bundler bundler;
+ private final BundlerType type;
+ private final E[] array;
+
+ /**
+ * Create a wrapper for a given array.
+ *
+ * <p>The passed in {@link Bundler} must be capable of bundling {@code F}.
+ */
+ public static <F> ParcelableArray<F> of(Bundler bundler, BundlerType type, F[] array) {
+ return new ParcelableArray<>(bundler, type, array);
+ }
+
+ public E[] get() {
+ return array;
+ }
+
+ private ParcelableArray(Bundler bundler, BundlerType type, E[] array) {
+ if (bundler == null || type == null) {
+ throw new NullPointerException();
+ }
+ this.bundler = bundler;
+ this.type = type;
+ this.array = array;
+ }
+
+ private ParcelableArray(Parcel in) {
+ bundler = in.readParcelable(Bundler.class.getClassLoader());
+ int size = in.readInt();
+
+ if (size == NULL_SIZE) {
+ type = null;
+ array = null;
+ return;
+ }
+
+ type = in.readParcelable(Bundler.class.getClassLoader());
+ BundlerType valueType = type.typeArguments().get(0);
+
+ @SuppressWarnings("unchecked")
+ E[] a = (E[]) bundler.createArray(valueType, size);
+ array = a;
+
+ if (size > 0) {
+ for (int i = 0; i < size; i++) {
+ @SuppressWarnings("unchecked")
+ E value = (E) bundler.readFromParcel(in, valueType);
+ array[i] = value;
+ }
+ }
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(bundler, flags);
+
+ if (array == null) {
+ dest.writeInt(NULL_SIZE);
+ return;
+ }
+
+ dest.writeInt(array.length);
+ dest.writeParcelable(type, flags);
+ if (array.length > 0) {
+ BundlerType valueType = type.typeArguments().get(0);
+
+ for (E value : array) {
+ bundler.writeToParcel(dest, value, valueType, flags);
+ }
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @SuppressWarnings("rawtypes")
+ public static final Creator<ParcelableArray> CREATOR =
+ new Creator<ParcelableArray>() {
+ @Override
+ public ParcelableArray createFromParcel(Parcel in) {
+ return new ParcelableArray(in);
+ }
+
+ @Override
+ public ParcelableArray[] newArray(int size) {
+ return new ParcelableArray[size];
+ }
+ };
+}
diff --git a/processor/src/main/resources/parcelablewrappers/ParcelableBitmap.java b/processor/src/main/resources/parcelablewrappers/ParcelableBitmap.java
new file mode 100644
index 0000000..ba27af8
--- /dev/null
+++ b/processor/src/main/resources/parcelablewrappers/ParcelableBitmap.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.parcelablewrappers;
+
+import android.graphics.Bitmap;
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.google.android.enterprise.connectedapps.internal.Bundler;
+import com.google.android.enterprise.connectedapps.internal.BundlerType;
+
+/** Wrapper for reading & writing {@link Bitmap} instances from and to {@link Parcel} instances. */
+// Though Bitmap is itself Parcelable, in some circumstances the Parcelling process can fail (see
+// b/159895007).
+public class ParcelableBitmap implements Parcelable {
+ private final Bitmap bitmap;
+
+ /** Create a wrapper for a given bitmap. */
+ public static ParcelableBitmap of(Bundler bundler, BundlerType type, Bitmap bitmap) {
+ return new ParcelableBitmap(bitmap);
+ }
+
+ private ParcelableBitmap(Bitmap bitmap) {
+ this.bitmap = bitmap;
+ }
+
+ private ParcelableBitmap(Parcel in) {
+ String configKey = in.readString();
+
+ if (configKey == null) {
+ bitmap = null;
+ return;
+ }
+
+ Bitmap.Config config = Bitmap.Config.valueOf(configKey);
+ int width = in.readInt();
+ int height = in.readInt();
+ int[] colors = new int[width * height];
+ in.readIntArray(colors);
+
+ bitmap = Bitmap.createBitmap(colors, width, height, config);
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ if (bitmap == null) {
+ out.writeString(null);
+ return;
+ }
+
+ Bitmap.Config config = bitmap.getConfig();
+ int width = bitmap.getWidth();
+ int height = bitmap.getHeight();
+ int[] colors = bitmapToPixelArray(bitmap);
+
+ out.writeString(config.toString());
+ out.writeInt(width);
+ out.writeInt(height);
+ out.writeIntArray(colors);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public Bitmap get() {
+ return bitmap;
+ }
+
+ public static final Creator<ParcelableBitmap> CREATOR =
+ new Creator<ParcelableBitmap>() {
+ @Override
+ public ParcelableBitmap createFromParcel(Parcel in) {
+ return new ParcelableBitmap(in);
+ }
+
+ @Override
+ public ParcelableBitmap[] newArray(int size) {
+ return new ParcelableBitmap[size];
+ }
+ };
+
+ private static int[] bitmapToPixelArray(Bitmap bitmap) {
+ int[] pixels = new int[bitmap.getHeight() * bitmap.getWidth()];
+ bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
+ return pixels;
+ }
+}
diff --git a/processor/src/main/resources/parcelablewrappers/ParcelableCollection.java b/processor/src/main/resources/parcelablewrappers/ParcelableCollection.java
new file mode 100644
index 0000000..66db136
--- /dev/null
+++ b/processor/src/main/resources/parcelablewrappers/ParcelableCollection.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.parcelablewrappers;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.google.android.enterprise.connectedapps.internal.Bundler;
+import com.google.android.enterprise.connectedapps.internal.BundlerType;
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * Wrapper for reading & writing {@link Collection} instances from and to {@link Parcel} instances.
+ */
+public class ParcelableCollection<E> implements Parcelable {
+
+ private static final int NULL_SIZE = -1;
+
+ private final Bundler bundler;
+ private final BundlerType type;
+ private final Collection<E> collection;
+
+ /**
+ * Create a wrapper for a given collection.
+ *
+ * <p>The passed in {@link Bundler} must be capable of bundling {@code F}.
+ */
+ public static <F> ParcelableCollection<F> of(
+ Bundler bundler, BundlerType type, Collection<F> collection) {
+ return new ParcelableCollection<>(bundler, type, collection);
+ }
+
+ public Collection<E> get() {
+ return collection;
+ }
+
+ private ParcelableCollection(Bundler bundler, BundlerType type, Collection<E> collection) {
+ if (bundler == null || type == null) {
+ throw new NullPointerException();
+ }
+ this.bundler = bundler;
+ this.type = type;
+ this.collection = collection;
+ }
+
+ private ParcelableCollection(Parcel in) {
+ bundler = in.readParcelable(Bundler.class.getClassLoader());
+ int size = in.readInt();
+ if (size == NULL_SIZE) {
+ type = null;
+ collection = null;
+ return;
+ }
+
+ collection = new ArrayList<>();
+ type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader());
+ if (size > 0) {
+ BundlerType valueType = type.typeArguments().get(0);
+ for (int i = 0; i < size; i++) {
+ @SuppressWarnings("unchecked")
+ E value = (E) bundler.readFromParcel(in, valueType);
+ collection.add(value);
+ }
+ }
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(bundler, flags);
+
+ if (collection == null) {
+ dest.writeInt(NULL_SIZE);
+ return;
+ }
+
+ dest.writeInt(collection.size());
+ dest.writeParcelable(type, flags);
+ if (!collection.isEmpty()) {
+ BundlerType valueType = type.typeArguments().get(0);
+ for (E value : collection) {
+ bundler.writeToParcel(dest, value, valueType, flags);
+ }
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @SuppressWarnings("rawtypes")
+ public static final Creator<ParcelableCollection> CREATOR =
+ new Creator<ParcelableCollection>() {
+ @Override
+ public ParcelableCollection createFromParcel(Parcel in) {
+ return new ParcelableCollection(in);
+ }
+
+ @Override
+ public ParcelableCollection[] newArray(int size) {
+ return new ParcelableCollection[size];
+ }
+ };
+}
diff --git a/processor/src/main/resources/parcelablewrappers/ParcelableGuavaOptional.java b/processor/src/main/resources/parcelablewrappers/ParcelableGuavaOptional.java
new file mode 100644
index 0000000..e2a14b9
--- /dev/null
+++ b/processor/src/main/resources/parcelablewrappers/ParcelableGuavaOptional.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.parcelablewrappers;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.google.android.enterprise.connectedapps.internal.Bundler;
+import com.google.android.enterprise.connectedapps.internal.BundlerType;
+import com.google.common.base.Optional;
+
+/**
+ * Wrapper for reading & writing {@link Optional} instances from and to {@link Parcel} instances.
+ */
+public class ParcelableGuavaOptional<E> implements Parcelable {
+
+ private static final int NULL = -1;
+ private static final int ABSENT = 0;
+ private static final int PRESENT = 1;
+
+ private final Bundler bundler;
+ private final BundlerType type;
+ private final Optional<E> optional;
+
+ /**
+ * Create a wrapper for a given optional.
+ *
+ * <p>The passed in {@link Bundler} must be capable of bundling {@code F}.
+ */
+ public static <F> ParcelableGuavaOptional<F> of(
+ Bundler bundler, BundlerType type, Optional<F> optional) {
+ return new ParcelableGuavaOptional<>(bundler, type, optional);
+ }
+
+ public Optional<E> get() {
+ return optional;
+ }
+
+ private ParcelableGuavaOptional(Bundler bundler, BundlerType type, Optional<E> optional) {
+ if (bundler == null || type == null) {
+ throw new NullPointerException();
+ }
+ this.bundler = bundler;
+ this.type = type;
+ this.optional = optional;
+ }
+
+ private ParcelableGuavaOptional(Parcel in) {
+ bundler = in.readParcelable(Bundler.class.getClassLoader());
+
+ int presentValue = in.readInt();
+
+ if (presentValue == NULL) {
+ type = null;
+ optional = null;
+ return;
+ }
+
+ boolean isPresent = presentValue == PRESENT;
+ type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader());
+ if (isPresent) {
+ BundlerType valueType = type.typeArguments().get(0);
+
+ @SuppressWarnings("unchecked")
+ E value = (E) bundler.readFromParcel(in, valueType);
+
+ optional = Optional.of(value);
+ } else {
+ optional = Optional.absent();
+ }
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(bundler, flags);
+
+ if (optional == null) {
+ dest.writeInt(NULL);
+ return;
+ }
+
+ dest.writeInt(optional.isPresent() ? PRESENT : ABSENT);
+ dest.writeParcelable(type, flags);
+ if (optional.isPresent()) {
+ BundlerType valueType = type.typeArguments().get(0);
+ bundler.writeToParcel(dest, optional.get(), valueType, flags);
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @SuppressWarnings("rawtypes")
+ public static final Creator<ParcelableGuavaOptional> CREATOR =
+ new Creator<ParcelableGuavaOptional>() {
+ @Override
+ public ParcelableGuavaOptional createFromParcel(Parcel in) {
+ return new ParcelableGuavaOptional(in);
+ }
+
+ @Override
+ public ParcelableGuavaOptional[] newArray(int size) {
+ return new ParcelableGuavaOptional[size];
+ }
+ };
+}
diff --git a/processor/src/main/resources/parcelablewrappers/ParcelableImmutableMap.java b/processor/src/main/resources/parcelablewrappers/ParcelableImmutableMap.java
new file mode 100644
index 0000000..78b7790
--- /dev/null
+++ b/processor/src/main/resources/parcelablewrappers/ParcelableImmutableMap.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.parcelablewrappers;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.google.android.enterprise.connectedapps.internal.Bundler;
+import com.google.android.enterprise.connectedapps.internal.BundlerType;
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * Wrapper for reading & writing {@link ImmutableMap} instances from and to {@link Parcel}
+ * instances.
+ */
+public class ParcelableImmutableMap<E, F> implements Parcelable {
+
+ private static final int NULL_SIZE = -1;
+ private static final int KEY_TYPE_INDEX = 0;
+ private static final int VALUE_TYPE_INDEX = 1;
+
+ private final Bundler bundler;
+ private final BundlerType type;
+ private final ImmutableMap<E, F> map;
+
+ /**
+ * Create a wrapper for a given immutable map.
+ *
+ * <p>The passed in {@link Bundler} must be capable of bundling {@code E} and {@code F}.
+ */
+ public static <E, F> ParcelableImmutableMap<E, F> of(
+ Bundler bundler, BundlerType type, ImmutableMap<E, F> map) {
+ return new ParcelableImmutableMap<>(bundler, type, map);
+ }
+
+ public ImmutableMap<E, F> get() {
+ return map;
+ }
+
+ private ParcelableImmutableMap(Bundler bundler, BundlerType type, ImmutableMap<E, F> map) {
+ if (bundler == null || type == null) {
+ throw new NullPointerException();
+ }
+ this.bundler = bundler;
+ this.type = type;
+ this.map = map;
+ }
+
+ private ParcelableImmutableMap(Parcel in) {
+ bundler = in.readParcelable(Bundler.class.getClassLoader());
+ int size = in.readInt();
+
+ if (size == NULL_SIZE) {
+ type = null;
+ map = null;
+ return;
+ }
+
+ ImmutableMap.Builder<E, F> mapBuilder = ImmutableMap.builder();
+
+ type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader());
+ if (size > 0) {
+ BundlerType keyType = type.typeArguments().get(KEY_TYPE_INDEX);
+ BundlerType valueType = type.typeArguments().get(VALUE_TYPE_INDEX);
+ for (int i = 0; i < size; i++) {
+ @SuppressWarnings("unchecked")
+ E key = (E) bundler.readFromParcel(in, keyType);
+ @SuppressWarnings("unchecked")
+ F value = (F) bundler.readFromParcel(in, valueType);
+ mapBuilder.put(key, value);
+ }
+ }
+
+ map = mapBuilder.build();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(bundler, flags);
+
+ if (map == null) {
+ dest.writeInt(NULL_SIZE);
+ return;
+ }
+
+ dest.writeInt(map.size());
+ dest.writeParcelable(type, flags);
+ if (!map.isEmpty()) {
+ BundlerType keyType = type.typeArguments().get(0);
+ BundlerType valueType = type.typeArguments().get(1);
+
+ for (ImmutableMap.Entry<E, F> entry : map.entrySet()) {
+ bundler.writeToParcel(dest, entry.getKey(), keyType, flags);
+ bundler.writeToParcel(dest, entry.getValue(), valueType, flags);
+ }
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @SuppressWarnings("rawtypes")
+ public static final Creator<ParcelableImmutableMap> CREATOR =
+ new Creator<ParcelableImmutableMap>() {
+ @Override
+ public ParcelableImmutableMap createFromParcel(Parcel in) {
+ return new ParcelableImmutableMap(in);
+ }
+
+ @Override
+ public ParcelableImmutableMap[] newArray(int size) {
+ return new ParcelableImmutableMap[size];
+ }
+ };
+}
diff --git a/processor/src/main/resources/parcelablewrappers/ParcelableList.java b/processor/src/main/resources/parcelablewrappers/ParcelableList.java
new file mode 100644
index 0000000..b1ff12e
--- /dev/null
+++ b/processor/src/main/resources/parcelablewrappers/ParcelableList.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.parcelablewrappers;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.google.android.enterprise.connectedapps.internal.Bundler;
+import com.google.android.enterprise.connectedapps.internal.BundlerType;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Wrapper for reading & writing {@link List} instances from and to {@link Parcel} instances. */
+
+public class ParcelableList<E> implements Parcelable {
+
+ private static final int NULL_SIZE = -1;
+
+ private final Bundler bundler;
+ private final BundlerType type;
+ private final List<E> list;
+
+ /**
+ * Create a wrapper for a given list.
+ *
+ * <p>The passed in {@link Bundler} must be capable of bundling {@code F}.
+ */
+ public static <F> ParcelableList<F> of(Bundler bundler, BundlerType type, List<F> list) {
+ return new ParcelableList<>(bundler, type, list);
+ }
+
+ public List<E> get() {
+ return list;
+ }
+
+ private ParcelableList(Bundler bundler, BundlerType type, List<E> list) {
+ if (bundler == null || type == null) {
+ throw new NullPointerException();
+ }
+ this.bundler = bundler;
+ this.type = type;
+ this.list = list;
+ }
+
+ private ParcelableList(Parcel in) {
+ bundler = in.readParcelable(Bundler.class.getClassLoader());
+ int size = in.readInt();
+
+ if (size == NULL_SIZE) {
+ type = null;
+ list = null;
+ return;
+ }
+
+ list = new ArrayList<>();
+ type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader());
+ if (size > 0) {
+ BundlerType valueType = type.typeArguments().get(0);
+ for (int i = 0; i < size; i++) {
+ @SuppressWarnings("unchecked")
+ E value = (E) bundler.readFromParcel(in, valueType);
+ list.add(value);
+ }
+ }
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(bundler, flags);
+
+ if (list == null) {
+ dest.writeInt(NULL_SIZE);
+ return;
+ }
+
+ dest.writeInt(list.size());
+ dest.writeParcelable(type, flags);
+ if (!list.isEmpty()) {
+ BundlerType valueType = type.typeArguments().get(0);
+
+ for (E value : list) {
+ bundler.writeToParcel(dest, value, valueType, flags);
+ }
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @SuppressWarnings("rawtypes")
+ public static final Creator<ParcelableList> CREATOR =
+ new Creator<ParcelableList>() {
+ @Override
+ public ParcelableList createFromParcel(Parcel in) {
+ return new ParcelableList(in);
+ }
+
+ @Override
+ public ParcelableList[] newArray(int size) {
+ return new ParcelableList[size];
+ }
+ };
+}
diff --git a/processor/src/main/resources/parcelablewrappers/ParcelableMap.java b/processor/src/main/resources/parcelablewrappers/ParcelableMap.java
new file mode 100644
index 0000000..e90c22b
--- /dev/null
+++ b/processor/src/main/resources/parcelablewrappers/ParcelableMap.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.parcelablewrappers;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.google.android.enterprise.connectedapps.internal.Bundler;
+import com.google.android.enterprise.connectedapps.internal.BundlerType;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Wrapper for reading & writing {@link Map} instances from and to {@link Parcel} instances. */
+public class ParcelableMap<E, F> implements Parcelable {
+
+ private static final int NULL_SIZE = -1;
+
+ private final Bundler bundler;
+ private final BundlerType type;
+ private final Map<E, F> map;
+
+ /**
+ * Create a wrapper for a given map.
+ *
+ * <p>The passed in {@link Bundler} must be capable of bundling {@code E} and {@code F}.
+ */
+ public static <E, F> ParcelableMap<E, F> of(Bundler bundler, BundlerType type, Map<E, F> map) {
+ return new ParcelableMap<>(bundler, type, map);
+ }
+
+ public Map<E, F> get() {
+ return map;
+ }
+
+ private ParcelableMap(Bundler bundler, BundlerType type, Map<E, F> map) {
+ if (bundler == null || type == null) {
+ throw new NullPointerException();
+ }
+ this.bundler = bundler;
+ this.type = type;
+ this.map = map;
+ }
+
+ private ParcelableMap(Parcel in) {
+ bundler = in.readParcelable(Bundler.class.getClassLoader());
+ int size = in.readInt();
+
+ if (size == NULL_SIZE) {
+ type = null;
+ map = null;
+ return;
+ }
+
+ map = new HashMap<>();
+ type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader());
+ if (size > 0) {
+ BundlerType keyType = type.typeArguments().get(0);
+ BundlerType valueType = type.typeArguments().get(1);
+ for (int i = 0; i < size; i++) {
+ @SuppressWarnings("unchecked")
+ E key = (E) bundler.readFromParcel(in, keyType);
+ @SuppressWarnings("unchecked")
+ F value = (F) bundler.readFromParcel(in, valueType);
+ map.put(key, value);
+ }
+ }
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(bundler, flags);
+
+ if (map == null) {
+ dest.writeInt(NULL_SIZE);
+ return;
+ }
+
+ dest.writeInt(map.size());
+ dest.writeParcelable(type, flags);
+ if (!map.isEmpty()) {
+ BundlerType keyType = type.typeArguments().get(0);
+ BundlerType valueType = type.typeArguments().get(1);
+
+ for (Map.Entry<E, F> entry : map.entrySet()) {
+ bundler.writeToParcel(dest, entry.getKey(), keyType, flags);
+ bundler.writeToParcel(dest, entry.getValue(), valueType, flags);
+ }
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @SuppressWarnings("rawtypes")
+ public static final Creator<ParcelableMap> CREATOR =
+ new Creator<ParcelableMap>() {
+ @Override
+ public ParcelableMap createFromParcel(Parcel in) {
+ return new ParcelableMap(in);
+ }
+
+ @Override
+ public ParcelableMap[] newArray(int size) {
+ return new ParcelableMap[size];
+ }
+ };
+}
diff --git a/processor/src/main/resources/parcelablewrappers/ParcelableOptional.java b/processor/src/main/resources/parcelablewrappers/ParcelableOptional.java
new file mode 100644
index 0000000..aa81dc9
--- /dev/null
+++ b/processor/src/main/resources/parcelablewrappers/ParcelableOptional.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.parcelablewrappers;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.google.android.enterprise.connectedapps.internal.Bundler;
+import com.google.android.enterprise.connectedapps.internal.BundlerType;
+import java.util.Optional;
+
+/**
+ * Wrapper for reading & writing {@link Optional} instances from and to {@link Parcel} instances.
+ */
+public class ParcelableOptional<E> implements Parcelable {
+
+ private static final int NULL = -1;
+ private static final int ABSENT = 0;
+ private static final int PRESENT = 1;
+
+ private final Bundler bundler;
+ private final BundlerType type;
+ private final Optional<E> optional;
+
+ /**
+ * Create a wrapper for a given optional.
+ *
+ * <p>The passed in {@link Bundler} must be capable of bundling {@code F}.
+ */
+ public static <F> ParcelableOptional<F> of(
+ Bundler bundler, BundlerType type, Optional<F> optional) {
+ return new ParcelableOptional<>(bundler, type, optional);
+ }
+
+ public Optional<E> get() {
+ return optional;
+ }
+
+ private ParcelableOptional(Bundler bundler, BundlerType type, Optional<E> optional) {
+ if (bundler == null || type == null) {
+ throw new NullPointerException();
+ }
+ this.bundler = bundler;
+ this.type = type;
+ this.optional = optional;
+ }
+
+ private ParcelableOptional(Parcel in) {
+ bundler = in.readParcelable(Bundler.class.getClassLoader());
+
+ int presentValue = in.readInt();
+
+ if (presentValue == NULL) {
+ type = null;
+ optional = null;
+ return;
+ }
+
+ boolean isPresent = presentValue == PRESENT;
+ type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader());
+ if (isPresent) {
+ BundlerType valueType = type.typeArguments().get(0);
+
+ @SuppressWarnings("unchecked")
+ E value = (E) bundler.readFromParcel(in, valueType);
+
+ optional = Optional.of(value);
+ } else {
+ optional = Optional.empty();
+ }
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(bundler, flags);
+
+ if (optional == null) {
+ dest.writeInt(NULL);
+ return;
+ }
+
+ dest.writeInt(optional.isPresent() ? PRESENT : ABSENT);
+ dest.writeParcelable(type, flags);
+ if (optional.isPresent()) {
+ BundlerType valueType = type.typeArguments().get(0);
+ bundler.writeToParcel(dest, optional.get(), valueType, flags);
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @SuppressWarnings("rawtypes")
+ public static final Creator<ParcelableOptional> CREATOR =
+ new Creator<ParcelableOptional>() {
+ @Override
+ public ParcelableOptional createFromParcel(Parcel in) {
+ return new ParcelableOptional(in);
+ }
+
+ @Override
+ public ParcelableOptional[] newArray(int size) {
+ return new ParcelableOptional[size];
+ }
+ };
+}
diff --git a/processor/src/main/resources/parcelablewrappers/ParcelablePair.java b/processor/src/main/resources/parcelablewrappers/ParcelablePair.java
new file mode 100644
index 0000000..41dea47
--- /dev/null
+++ b/processor/src/main/resources/parcelablewrappers/ParcelablePair.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.parcelablewrappers;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Pair;
+import com.google.android.enterprise.connectedapps.internal.Bundler;
+import com.google.android.enterprise.connectedapps.internal.BundlerType;
+
+/** Wrapper for reading & writing {@link Pair} instances from and to {@link Parcel} instances. */
+public class ParcelablePair<F, S> implements Parcelable {
+
+ private static final int NULL = -1;
+ private static final int NOT_NULL = 1;
+
+ private final Bundler bundler;
+ private final BundlerType type;
+ private final Pair<F, S> pair;
+
+ /**
+ * Create a wrapper for a given pair.
+ *
+ * <p>The passed in {@link Bundler} must be capable of bundling {@code E} and {@code F}.
+ */
+ public static <F, S> ParcelablePair<F, S> of(Bundler bundler, BundlerType type, Pair<F, S> pair) {
+ return new ParcelablePair<>(bundler, type, pair);
+ }
+
+ public Pair<F, S> get() {
+ return pair;
+ }
+
+ private ParcelablePair(Bundler bundler, BundlerType type, Pair<F, S> pair) {
+ if (bundler == null || type == null) {
+ throw new NullPointerException();
+ }
+ this.bundler = bundler;
+ this.type = type;
+ this.pair = pair;
+ }
+
+ private ParcelablePair(Parcel in) {
+ bundler = in.readParcelable(Bundler.class.getClassLoader());
+ int present = in.readInt();
+
+ if (present == NULL) {
+ type = null;
+ pair = null;
+ return;
+ }
+
+ type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader());
+ BundlerType fType = type.typeArguments().get(0);
+ BundlerType sType = type.typeArguments().get(1);
+
+ @SuppressWarnings("unchecked")
+ F first = (F) bundler.readFromParcel(in, fType);
+ @SuppressWarnings("unchecked")
+ S second = (S) bundler.readFromParcel(in, sType);
+
+ pair = new Pair<>(first, second);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(bundler, flags);
+
+ if (pair == null) {
+ dest.writeInt(NULL);
+ return;
+ }
+
+ dest.writeInt(NOT_NULL);
+ dest.writeParcelable(type, flags);
+
+ BundlerType fType = type.typeArguments().get(0);
+ BundlerType sType = type.typeArguments().get(1);
+
+ bundler.writeToParcel(dest, pair.first, fType, flags);
+ bundler.writeToParcel(dest, pair.second, sType, flags);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @SuppressWarnings("rawtypes")
+ public static final Creator<ParcelablePair> CREATOR =
+ new Creator<ParcelablePair>() {
+ @Override
+ public ParcelablePair createFromParcel(Parcel in) {
+ return new ParcelablePair(in);
+ }
+
+ @Override
+ public ParcelablePair[] newArray(int size) {
+ return new ParcelablePair[size];
+ }
+ };
+}
diff --git a/processor/src/main/resources/parcelablewrappers/ParcelableSet.java b/processor/src/main/resources/parcelablewrappers/ParcelableSet.java
new file mode 100644
index 0000000..b032f21
--- /dev/null
+++ b/processor/src/main/resources/parcelablewrappers/ParcelableSet.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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
+ *
+ * https://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.google.android.enterprise.connectedapps.parcelablewrappers;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.google.android.enterprise.connectedapps.internal.Bundler;
+import com.google.android.enterprise.connectedapps.internal.BundlerType;
+import java.util.HashSet;
+import java.util.Set;
+
+/** Wrapper for reading & writing {@link Set} instances from and to {@link Parcel} instances. */
+public class ParcelableSet<E> implements Parcelable {
+
+ private static final int NULL_SIZE = -1;
+
+ private final Bundler bundler;
+ private final BundlerType type;
+ private final Set<E> set;
+
+ /**
+ * Create a wrapper for a given set.
+ *
+ * <p>The passed in {@link Bundler} must be capable of bundling {@code F}.
+ */
+ public static <F> ParcelableSet<F> of(Bundler bundler, BundlerType type, Set<F> set) {
+ return new ParcelableSet<>(bundler, type, set);
+ }
+
+ public Set<E> get() {
+ return set;
+ }
+
+ private ParcelableSet(Bundler bundler, BundlerType type, Set<E> set) {
+ if (bundler == null || type == null) {
+ throw new NullPointerException();
+ }
+ this.bundler = bundler;
+ this.type = type;
+ this.set = set;
+ }
+
+ private ParcelableSet(Parcel in) {
+ bundler = in.readParcelable(Bundler.class.getClassLoader());
+ int size = in.readInt();
+
+ if (size == NULL_SIZE) {
+ type = null;
+ set = null;
+ return;
+ }
+
+ set = new HashSet<>();
+ type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader());
+ if (size > 0) {
+ BundlerType valueType = type.typeArguments().get(0);
+ for (int i = 0; i < size; i++) {
+ @SuppressWarnings("unchecked")
+ E value = (E) bundler.readFromParcel(in, valueType);
+ set.add(value);
+ }
+ }
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(bundler, flags);
+
+ if (set == null) {
+ dest.writeInt(NULL_SIZE);
+ return;
+ }
+
+ dest.writeInt(set.size());
+ dest.writeParcelable(type, flags);
+ if (!set.isEmpty()) {
+ BundlerType valueType = type.typeArguments().get(0);
+
+ for (E value : set) {
+ bundler.writeToParcel(dest, value, valueType, flags);
+ }
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @SuppressWarnings("rawtypes")
+ public static final Creator<ParcelableSet> CREATOR =
+ new Creator<ParcelableSet>() {
+ @Override
+ public ParcelableSet createFromParcel(Parcel in) {
+ return new ParcelableSet(in);
+ }
+
+ @Override
+ public ParcelableSet[] newArray(int size) {
+ return new ParcelableSet[size];
+ }
+ };
+}