blob: ce486b9e815b80a1514a7bcea517ec6902148940 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.bedstead.testapp.processor;
import com.android.bedstead.testapp.processor.annotations.TestAppReceiver;
import com.android.bedstead.testapp.processor.annotations.TestAppSender;
import com.google.android.enterprise.connectedapps.annotations.CrossProfile;
import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration;
import com.google.android.enterprise.connectedapps.annotations.CrossProfileProvider;
import com.google.auto.service.AutoService;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.TypeSpec;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.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.tools.JavaFileObject;
/** Processor for generating TestApp API for remote execution. */
@SupportedAnnotationTypes({
"com.android.bedstead.testapp.processor.annotations.TestAppSender",
"com.android.bedstead.testapp.processor.annotations.TestAppReceiver",
})
@AutoService(javax.annotation.processing.Processor.class)
public final class Processor extends AbstractProcessor {
// TODO(scottjonathan): Add more verification before generating - and add processor tests
private static final ClassName CONTEXT_CLASSNAME =
ClassName.get("android.content", "Context");
private static final ClassName NENE_ACTIVITY_CLASSNAME =
ClassName.get(
"com.android.bedstead.nene.activities",
"NeneActivity");
private static final ClassName TEST_APP_ACTIVITY_CLASSNAME =
ClassName.get(
"com.android.bedstead.testapp",
"TestAppActivity");
private static final ClassName TEST_APP_ACTIVITY_IMPL_CLASSNAME =
ClassName.get(
"com.android.bedstead.testapp",
"TestAppActivityImpl");
private static final ClassName PROFILE_TARGETED_REMOTE_ACTIVITY_CLASSNAME =
ClassName.get(
"com.android.bedstead.testapp",
"ProfileTargetedRemoteActivity");
private static final ClassName TARGETED_REMOTE_ACTIVITY_CLASSNAME =
ClassName.get(
"com.android.bedstead.testapp",
"TargetedRemoteActivity");
private static final ClassName TARGETED_REMOTE_ACTIVITY_IMPL_CLASSNAME =
ClassName.get(
"com.android.bedstead.testapp",
"TargetedRemoteActivityImpl");
private static final ClassName TEST_APP_CONTROLLER_CLASSNAME =
ClassName.get(
"com.android.bedstead.testapp",
"TestAppController");
private static final ClassName TARGETED_REMOTE_ACTIVITY_WRAPPER_CLASSNAME =
ClassName.get(
"com.android.bedstead.testapp",
"TargetedRemoteActivityWrapper");
private static final ClassName CROSS_PROFILE_CONNECTOR_CLASSNAME =
ClassName.get("com.google.android.enterprise.connectedapps",
"CrossProfileConnector");
private static final ClassName UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME =
ClassName.get(
"com.google.android.enterprise.connectedapps.exceptions",
"UnavailableProfileException");
private static final ClassName PROFILE_RUNTIME_EXCEPTION_CLASSNAME =
ClassName.get(
"com.google.android.enterprise.connectedapps.exceptions",
"ProfileRuntimeException");
private static final ClassName NENE_EXCEPTION_CLASSNAME =
ClassName.get(
"com.android.bedstead.nene.exceptions", "NeneException");
private static final ClassName TEST_APP_INSTANCE_REFERENCE_CLASSNAME =
ClassName.get("com.android.bedstead.testapp", "TestAppInstanceReference");
private static final ClassName COMPONENT_REFERENCE_CLASSNAME =
ClassName.get("com.android.bedstead.nene.packages", "ComponentReference");
public static final String PACKAGE_NAME = "com.android.bedstead.testapp";
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latest();
}
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
TypeElement neneActivityInterface =
processingEnv.getElementUtils().getTypeElement(
NENE_ACTIVITY_CLASSNAME.canonicalName());
if (!roundEnv.getElementsAnnotatedWith(TestAppReceiver.class).isEmpty()
|| !roundEnv.getElementsAnnotatedWith(TestAppSender.class).isEmpty()) {
generateTargetedRemoteActivityInterface(neneActivityInterface);
generateTargetedRemoteActivityImpl(neneActivityInterface);
generateTargetedRemoteActivityWrapper(neneActivityInterface);
generateProvider();
generateConfiguration();
}
if (!roundEnv.getElementsAnnotatedWith(TestAppSender.class).isEmpty()) {
generateRemoteActivityImpl(neneActivityInterface);
}
return true;
}
private void generateTargetedRemoteActivityImpl(TypeElement neneActivityInterface) {
TypeSpec.Builder classBuilder =
TypeSpec.classBuilder(
TARGETED_REMOTE_ACTIVITY_IMPL_CLASSNAME)
.addSuperinterface(TARGETED_REMOTE_ACTIVITY_CLASSNAME)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL);
for (ExecutableElement method : getMethods(neneActivityInterface)) {
MethodSpec.Builder methodBuilder =
MethodSpec.methodBuilder(method.getSimpleName().toString())
.returns(ClassName.get(method.getReturnType()))
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class);
methodBuilder.addParameter(
ParameterSpec.builder(String.class, "activityClassName").build());
List<String> paramNames = new ArrayList<>();
for (VariableElement param : method.getParameters()) {
ParameterSpec parameterSpec =
ParameterSpec.builder(ClassName.get(param.asType()),
param.getSimpleName().toString()).build();
paramNames.add(param.getSimpleName().toString());
methodBuilder.addParameter(parameterSpec);
}
if (method.getReturnType().getKind().equals(TypeKind.VOID)) {
methodBuilder.addStatement(
"BaseTestAppActivity.findActivity(activityClassName).$L($L)",
method.getSimpleName(), String.join(", ", paramNames));
} else {
methodBuilder.addStatement(
"return BaseTestAppActivity.findActivity(activityClassName).$L($L)",
method.getSimpleName(), String.join(", ", paramNames));
}
classBuilder.addMethod(methodBuilder.build());
}
writeClassToFile(PACKAGE_NAME, classBuilder.build());
}
private void generateTargetedRemoteActivityWrapper(TypeElement neneActivityInterface) {
TypeSpec.Builder classBuilder =
TypeSpec.classBuilder(
TARGETED_REMOTE_ACTIVITY_WRAPPER_CLASSNAME)
.addSuperinterface(TARGETED_REMOTE_ACTIVITY_CLASSNAME)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL);
classBuilder.addField(
FieldSpec.builder(PROFILE_TARGETED_REMOTE_ACTIVITY_CLASSNAME,
"mProfileTargetedRemoteActivity")
.addModifiers(Modifier.PRIVATE, Modifier.FINAL)
.build());
classBuilder.addField(
FieldSpec.builder(CROSS_PROFILE_CONNECTOR_CLASSNAME, "mConnector")
.addModifiers(Modifier.PRIVATE, Modifier.FINAL)
.build());
classBuilder.addMethod(MethodSpec.constructorBuilder()
.addParameter(CROSS_PROFILE_CONNECTOR_CLASSNAME, "connector")
.addStatement("mConnector = connector")
.addStatement(
"mProfileTargetedRemoteActivity = $T.create(connector)",
PROFILE_TARGETED_REMOTE_ACTIVITY_CLASSNAME)
.build());
for (ExecutableElement method : getMethods(neneActivityInterface)) {
MethodSpec.Builder methodBuilder =
MethodSpec.methodBuilder(method.getSimpleName().toString())
.returns(ClassName.get(method.getReturnType()))
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class);
methodBuilder.addParameter(
ParameterSpec.builder(String.class, "activityClassName").build());
String params = "activityClassName";
for (VariableElement param : method.getParameters()) {
ParameterSpec parameterSpec =
ParameterSpec.builder(ClassName.get(param.asType()),
param.getSimpleName().toString()).build();
params += ", " + param.getSimpleName().toString();
methodBuilder.addParameter(parameterSpec);
}
methodBuilder.beginControlFlow("try")
.addStatement("mConnector.connect()");
if (method.getReturnType().getKind().equals(TypeKind.VOID)) {
methodBuilder.addStatement(
"mProfileTargetedRemoteActivity.other().$L($L)",
method.getSimpleName(), params);
} else {
methodBuilder.addStatement(
"return mProfileTargetedRemoteActivity.other().$L($L)",
method.getSimpleName(), params);
}
methodBuilder.nextControlFlow(
"catch ($T e)", UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME)
.addStatement(
"throw new $T($S, e)",
NENE_EXCEPTION_CLASSNAME, "Error connecting to test app")
.nextControlFlow("catch ($T e)", PROFILE_RUNTIME_EXCEPTION_CLASSNAME)
.addStatement("throw ($T) e.getCause()", RuntimeException.class)
.nextControlFlow("finally")
.addStatement("mConnector.stopManualConnectionManagement()")
.endControlFlow();
classBuilder.addMethod(methodBuilder.build());
}
writeClassToFile(PACKAGE_NAME, classBuilder.build());
}
private void generateRemoteActivityImpl(TypeElement neneActivityInterface) {
TypeSpec.Builder classBuilder =
TypeSpec.classBuilder(
TEST_APP_ACTIVITY_IMPL_CLASSNAME)
.superclass(TEST_APP_ACTIVITY_CLASSNAME)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL);
classBuilder.addField(FieldSpec.builder(String.class, "mActivityClassName")
.addModifiers(Modifier.PRIVATE, Modifier.FINAL).build());
classBuilder.addField(FieldSpec.builder(
TARGETED_REMOTE_ACTIVITY_CLASSNAME, "mTargetedRemoteActivity")
.addModifiers(Modifier.PRIVATE, Modifier.FINAL).build());
classBuilder.addMethod(
MethodSpec.constructorBuilder()
.addParameter(TEST_APP_INSTANCE_REFERENCE_CLASSNAME, "instance")
.addParameter(
COMPONENT_REFERENCE_CLASSNAME, "component")
.addStatement("super(instance, component)")
.addStatement("mActivityClassName = component.className()")
.addStatement("mTargetedRemoteActivity = new $T(mInstance.connector())",
TARGETED_REMOTE_ACTIVITY_WRAPPER_CLASSNAME)
.build());
for (ExecutableElement method : getMethods(neneActivityInterface)) {
MethodSpec.Builder methodBuilder =
MethodSpec.methodBuilder(method.getSimpleName().toString())
.returns(ClassName.get(method.getReturnType()))
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class);
String params = "mActivityClassName";
for (VariableElement param : method.getParameters()) {
ParameterSpec parameterSpec =
ParameterSpec.builder(ClassName.get(param.asType()),
param.getSimpleName().toString()).build();
params += ", " + param.getSimpleName().toString();
methodBuilder.addParameter(parameterSpec);
}
if (method.getReturnType().getKind().equals(TypeKind.VOID)) {
methodBuilder.addStatement(
"mTargetedRemoteActivity.$L($L)", method.getSimpleName(), params);
} else {
methodBuilder.addStatement(
"return mTargetedRemoteActivity.$L($L)", method.getSimpleName(), params);
}
classBuilder.addMethod(methodBuilder.build());
}
writeClassToFile(PACKAGE_NAME, classBuilder.build());
}
private void generateTargetedRemoteActivityInterface(TypeElement neneActivityInterface) {
TypeSpec.Builder classBuilder =
TypeSpec.interfaceBuilder(
TARGETED_REMOTE_ACTIVITY_CLASSNAME)
.addModifiers(Modifier.PUBLIC);
for (ExecutableElement method : getMethods(neneActivityInterface)) {
MethodSpec.Builder methodBuilder =
MethodSpec.methodBuilder(method.getSimpleName().toString())
.returns(ClassName.get(method.getReturnType()))
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
.addAnnotation(CrossProfile.class);
methodBuilder.addParameter(
ParameterSpec.builder(String.class, "activityClassName").build());
for (VariableElement param : method.getParameters()) {
ParameterSpec parameterSpec =
ParameterSpec.builder(ClassName.get(param.asType()),
param.getSimpleName().toString()).build();
methodBuilder.addParameter(parameterSpec);
}
classBuilder.addMethod(methodBuilder.build());
}
writeClassToFile(PACKAGE_NAME, classBuilder.build());
}
private void generateProvider() {
TypeSpec.Builder classBuilder =
TypeSpec.classBuilder(
"Provider")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL);
classBuilder.addMethod(MethodSpec.methodBuilder("provideTargetedRemoteActivity")
.returns(TARGETED_REMOTE_ACTIVITY_CLASSNAME)
.addModifiers(Modifier.PUBLIC)
.addAnnotation(CrossProfileProvider.class)
.addCode("return new $T();", TARGETED_REMOTE_ACTIVITY_IMPL_CLASSNAME)
.build());
classBuilder.addMethod(MethodSpec.methodBuilder("provideTestAppController")
.returns(TEST_APP_CONTROLLER_CLASSNAME)
.addModifiers(Modifier.PUBLIC)
.addAnnotation(CrossProfileProvider.class)
.addCode("return new $T();", TEST_APP_CONTROLLER_CLASSNAME)
.build());
writeClassToFile(PACKAGE_NAME, classBuilder.build());
}
private void generateConfiguration() {
TypeSpec.Builder classBuilder =
TypeSpec.classBuilder(
"Configuration")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addAnnotation(AnnotationSpec.builder(CrossProfileConfiguration.class)
.addMember("providers", "Provider.class")
.build());
writeClassToFile(PACKAGE_NAME, classBuilder.build());
}
private void writeClassToFile(String packageName, TypeSpec clazz) {
String qualifiedClassName =
packageName.isEmpty() ? clazz.name : packageName + "." + clazz.name;
JavaFile javaFile = JavaFile.builder(packageName, clazz).build();
try {
JavaFileObject builderFile =
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);
}
}
private Set<ExecutableElement> getMethods(TypeElement interfaceClass) {
return interfaceClass.getEnclosedElements().stream()
.filter(e -> e instanceof ExecutableElement)
.map(e -> (ExecutableElement) e)
.collect(Collectors.toSet());
}
}