Add code generation for RemoteDPC.

This removes a load of boilerplate.

Test: atest RemoteDPCTest
Bug: 177663307
Change-Id: I9d0b87a32997ca08beb3fceffebee5e814daa576
diff --git a/common/device-side/bedstead/remotedpc/Android.bp b/common/device-side/bedstead/remotedpc/Android.bp
index e80fcf7..b4c6259 100644
--- a/common/device-side/bedstead/remotedpc/Android.bp
+++ b/common/device-side/bedstead/remotedpc/Android.bp
@@ -12,11 +12,12 @@
         "Nene",
         "ConnectedAppsSDK",
         "ConnectedAppsSDK_Annotations",
-        "androidx.annotation_annotation"
+        "androidx.annotation_annotation",
+        "RemoteDPC_Annotations"
     ],
     manifest: "src/communication/main/AndroidManifest.xml",
     min_sdk_version: "27",
-    plugins: ["ConnectedAppsSDK_Processor"],
+    plugins: ["ConnectedAppsSDK_Processor", "RemoteDPC_Processor"],
 }
 
 android_library {
@@ -75,4 +76,28 @@
     ],
     manifest: "src/library/test/AndroidManifest.xml",
     min_sdk_version: "27"
+}
+
+java_library {
+    name: "RemoteDPC_Annotations",
+    srcs: [
+        "src/processor/main/java/com/android/bedstead/remotedpc/processor/annotations/*.java"
+    ],
+    host_supported: true
+}
+
+java_plugin {
+    name: "RemoteDPC_Processor",
+    processor_class: "com.android.bedstead.remotedpc.processor.Processor",
+    static_libs: [
+        "javapoet",
+        "auto_service_annotations",
+        "RemoteDPC_Annotations",
+        "ConnectedAppsSDK_Annotations"
+    ],
+    srcs: [
+        "src/processor/main/java/com/android/bedstead/remotedpc/processor/Processor.java"
+    ],
+    plugins: ["auto_service_plugin"],
+    generates_api: true,
 }
\ No newline at end of file
diff --git a/common/device-side/bedstead/remotedpc/src/communication/main/java/com/android/bedstead/remotedpc/Configuration.java b/common/device-side/bedstead/remotedpc/src/communication/main/java/com/android/bedstead/remotedpc/Configuration.java
new file mode 100644
index 0000000..28007c9
--- /dev/null
+++ b/common/device-side/bedstead/remotedpc/src/communication/main/java/com/android/bedstead/remotedpc/Configuration.java
@@ -0,0 +1,34 @@
+/*
+ * 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.remotedpc;
+
+import android.content.ComponentName;
+
+/**
+ * Utilities and configuration for RemoteDPC Communication.
+ */
+public final class Configuration {
+
+    private Configuration() {
+
+    }
+
+    public static final ComponentName DPC_COMPONENT = new ComponentName(
+            "com.android.bedstead.remotedpc.dpc",
+            "com.android.eventlib.premade.EventLibDeviceAdminReceiver"
+    );
+}
diff --git a/common/device-side/bedstead/remotedpc/src/communication/main/java/com/android/bedstead/remotedpc/connected/Configuration.java b/common/device-side/bedstead/remotedpc/src/communication/main/java/com/android/bedstead/remotedpc/connected/Configuration.java
deleted file mode 100644
index 930c596..0000000
--- a/common/device-side/bedstead/remotedpc/src/communication/main/java/com/android/bedstead/remotedpc/connected/Configuration.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * 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.remotedpc.connected;
-
-import com.android.bedstead.remotedpc.managers.Provider;
-
-import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration;
-
-/** Connected Apps SDK Configuration for RemoteDPC. */
-@CrossProfileConfiguration(providers = Provider.class)
-public class Configuration {
-}
diff --git a/common/device-side/bedstead/remotedpc/src/communication/main/java/com/android/bedstead/remotedpc/managers/Provider.java b/common/device-side/bedstead/remotedpc/src/communication/main/java/com/android/bedstead/remotedpc/managers/Provider.java
deleted file mode 100644
index ba08fee..0000000
--- a/common/device-side/bedstead/remotedpc/src/communication/main/java/com/android/bedstead/remotedpc/managers/Provider.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * 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.remotedpc.managers;
-
-import android.content.Context;
-
-import com.google.android.enterprise.connectedapps.annotations.CrossProfileProvider;
-
-/** Connected Apps SDK Provider for RemoteDPC. */
-public class Provider {
-
-    /** Provide instance of {@link RemoteDevicePolicyManager} to Connected Apps SDK. */
-    @CrossProfileProvider
-    public RemoteDevicePolicyManager provideRemoteDevicePolicyManager(Context context) {
-        return new RemoteDevicePolicyManagerImpl(context);
-    }
-}
diff --git a/common/device-side/bedstead/remotedpc/src/communication/main/java/com/android/bedstead/remotedpc/managers/RemoteDevicePolicyManager.java b/common/device-side/bedstead/remotedpc/src/communication/main/java/com/android/bedstead/remotedpc/managers/RemoteDevicePolicyManager.java
index f33209e..f1b79bb 100644
--- a/common/device-side/bedstead/remotedpc/src/communication/main/java/com/android/bedstead/remotedpc/managers/RemoteDevicePolicyManager.java
+++ b/common/device-side/bedstead/remotedpc/src/communication/main/java/com/android/bedstead/remotedpc/managers/RemoteDevicePolicyManager.java
@@ -21,20 +21,33 @@
 
 import androidx.annotation.NonNull;
 
-import com.google.android.enterprise.connectedapps.annotations.CrossProfile;
+import com.android.bedstead.remotedpc.processor.annotations.RemoteDpcAutomaticAdmin;
+import com.android.bedstead.remotedpc.processor.annotations.RemoteDpcManager;
 
-/** See {@link DevicePolicyManager}. */
+/**
+ * Wrapper of {@link DevicePolicyManager} methods for use with Remote DPC
+ *
+ * <p>Methods called on this interface will behave as if they were called directly by the
+ * RemoteDPC instance. Return values and exceptions will behave as expected.
+ *
+ * <p>Methods on this interface must match exactly the methods declared by
+ * {@link DevicePolicyManager}, or else must be identical to a method declared by
+ * {@link DevicePolicyManager} except that it excludes a {@code ComponentName admin} first argument
+ * and must be annotated {@link RemoteDpcAutomaticAdmin}.
+ *
+ * <p>When using {@link RemoteDpcAutomaticAdmin}, there must also exist an identical method on the
+ * interface which includes the {@code ComponentName admin} argument. The RemoteDPC component name
+ * will be automatically provided when the {@link RemoteDpcAutomaticAdmin} annotated method is
+ * called.
+ */
+@RemoteDpcManager(managerClass = DevicePolicyManager.class)
 public interface RemoteDevicePolicyManager {
 
-    /** See {@link DevicePolicyManager#isUsingUnifiedPassword(android.content.ComponentName)}. */
-    @CrossProfile
+    /** See {@link DevicePolicyManager#isUsingUnifiedPassword(ComponentName)}. */
     boolean isUsingUnifiedPassword(@NonNull ComponentName admin);
-
-    /** See {@link DevicePolicyManager#isUsingUnifiedPassword(android.content.ComponentName)}. */
-    @CrossProfile
-    boolean isUsingUnifiedPassword();
+    /** See {@link DevicePolicyManager#isUsingUnifiedPassword(ComponentName)}. */
+    @RemoteDpcAutomaticAdmin boolean isUsingUnifiedPassword();
 
     /** See {@link DevicePolicyManager#getCurrentFailedPasswordAttempts()}. */
-    @CrossProfile
     int getCurrentFailedPasswordAttempts();
 }
diff --git a/common/device-side/bedstead/remotedpc/src/communication/main/java/com/android/bedstead/remotedpc/managers/RemoteDevicePolicyManagerImpl.java b/common/device-side/bedstead/remotedpc/src/communication/main/java/com/android/bedstead/remotedpc/managers/RemoteDevicePolicyManagerImpl.java
deleted file mode 100644
index 5bd0979..0000000
--- a/common/device-side/bedstead/remotedpc/src/communication/main/java/com/android/bedstead/remotedpc/managers/RemoteDevicePolicyManagerImpl.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * 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.remotedpc.managers;
-
-import android.app.admin.DevicePolicyManager;
-import android.content.ComponentName;
-import android.content.Context;
-
-import androidx.annotation.NonNull;
-
-/** Implementation of {@link com.android.bedstead.remotedpc.managers.RemoteDevicePolicyManager}. */
-@SuppressWarnings("NewApi")
-public class RemoteDevicePolicyManagerImpl implements RemoteDevicePolicyManager {
-
-    private static final ComponentName DPC_COMPONENT = new ComponentName(
-            "com.android.bedstead.remotedpc.dpc",
-            "com.android.eventlib.premade.EventLibDeviceAdminReceiver"
-    );
-
-    private final DevicePolicyManager mDevicePolicyManager;
-
-    public RemoteDevicePolicyManagerImpl(Context context) {
-        mDevicePolicyManager = context.getSystemService(DevicePolicyManager.class);
-    }
-
-    @Override
-    public boolean isUsingUnifiedPassword(@NonNull ComponentName admin) {
-        return mDevicePolicyManager.isUsingUnifiedPassword(admin);
-    }
-
-    @Override
-    public boolean isUsingUnifiedPassword() {
-        return isUsingUnifiedPassword(DPC_COMPONENT);
-    }
-
-    @Override
-    public int getCurrentFailedPasswordAttempts() {
-        return mDevicePolicyManager.getCurrentFailedPasswordAttempts();
-    }
-}
diff --git a/common/device-side/bedstead/remotedpc/src/communication/main/java/com/android/bedstead/remotedpc/managers/RemoteDevicePolicyManagerWrapper.java b/common/device-side/bedstead/remotedpc/src/communication/main/java/com/android/bedstead/remotedpc/managers/RemoteDevicePolicyManagerWrapper.java
deleted file mode 100644
index 775a8d4..0000000
--- a/common/device-side/bedstead/remotedpc/src/communication/main/java/com/android/bedstead/remotedpc/managers/RemoteDevicePolicyManagerWrapper.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * 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.remotedpc.managers;
-
-import android.content.ComponentName;
-
-import androidx.annotation.NonNull;
-
-import com.android.bedstead.nene.exceptions.NeneException;
-
-import com.google.android.enterprise.connectedapps.CrossProfileConnector;
-import com.google.android.enterprise.connectedapps.exceptions.ProfileRuntimeException;
-import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException;
-
-
-/**
- * Implementation of {@link RemoteDevicePolicyManager} which uses the Connected Apps SDK internally.
- */
-public class RemoteDevicePolicyManagerWrapper implements RemoteDevicePolicyManager {
-
-    private final ProfileRemoteDevicePolicyManager mProfileRemoteDevicePolicyManager;
-    private final CrossProfileConnector mConnector;
-
-    public RemoteDevicePolicyManagerWrapper(CrossProfileConnector connector) {
-        mConnector = connector;
-        mProfileRemoteDevicePolicyManager = ProfileRemoteDevicePolicyManager.create(connector);
-    }
-
-    @Override
-    public boolean isUsingUnifiedPassword(@NonNull ComponentName admin) {
-        try {
-            mConnector.connect();
-            return mProfileRemoteDevicePolicyManager.other().isUsingUnifiedPassword(admin);
-        } catch (UnavailableProfileException e) {
-            e.printStackTrace();
-            throw new NeneException("Error connecting to RemoteDPC", e);
-        } catch (ProfileRuntimeException e) {
-            throw (RuntimeException) e.getCause();
-        }
-    }
-
-    @Override
-    public boolean isUsingUnifiedPassword() {
-        try {
-            mConnector.connect();
-            return mProfileRemoteDevicePolicyManager.other().isUsingUnifiedPassword();
-        } catch (UnavailableProfileException e) {
-            e.printStackTrace();
-            throw new NeneException("Error connecting to RemoteDPC", e);
-        } catch (ProfileRuntimeException e) {
-            throw (RuntimeException) e.getCause();
-        }
-    }
-
-    @Override
-    public int getCurrentFailedPasswordAttempts() {
-        try {
-            mConnector.connect();
-            return mProfileRemoteDevicePolicyManager.other().getCurrentFailedPasswordAttempts();
-        } catch (UnavailableProfileException e) {
-            e.printStackTrace();
-            throw new NeneException("Error connecting to RemoteDPC", e);
-        } catch (ProfileRuntimeException e) {
-            throw (RuntimeException) e.getCause();
-        }
-    }
-}
diff --git a/common/device-side/bedstead/remotedpc/src/library/main/java/com/android/bedstead/remotedpc/RemoteDpc.java b/common/device-side/bedstead/remotedpc/src/library/main/java/com/android/bedstead/remotedpc/RemoteDpc.java
index 2212ff4..106a35d 100644
--- a/common/device-side/bedstead/remotedpc/src/library/main/java/com/android/bedstead/remotedpc/RemoteDpc.java
+++ b/common/device-side/bedstead/remotedpc/src/library/main/java/com/android/bedstead/remotedpc/RemoteDpc.java
@@ -16,10 +16,10 @@
 
 package com.android.bedstead.remotedpc;
 
+import static com.android.bedstead.remotedpc.Configuration.DPC_COMPONENT;
 import static com.android.compatibility.common.util.FileUtils.readInputStreamFully;
 
 import android.app.admin.DevicePolicyManager;
-import android.content.ComponentName;
 import android.content.Context;
 import android.os.UserHandle;
 
@@ -33,7 +33,7 @@
 import com.android.bedstead.nene.users.UserReference;
 import com.android.bedstead.remotedpc.connected.RemoteDPCBinder;
 import com.android.bedstead.remotedpc.managers.RemoteDevicePolicyManager;
-import com.android.bedstead.remotedpc.managers.RemoteDevicePolicyManagerWrapper;
+import com.android.bedstead.remotedpc.managers.RemoteDevicePolicyManager_Wrapper;
 
 import com.google.android.enterprise.connectedapps.CrossProfileConnector;
 
@@ -45,10 +45,6 @@
 
     private static final TestApis sTestApis = new TestApis();
     private static final Context sContext = sTestApis.context().instrumentedContext();
-    private static final ComponentName DPC_COMPONENT = new ComponentName(
-            "com.android.bedstead.remotedpc.dpc",
-            "com.android.eventlib.premade.EventLibDeviceAdminReceiver"
-    );
 
     /**
      * Get the {@link RemoteDpc} instance for the Device Owner.
@@ -273,6 +269,6 @@
      * this RemoteDPC.
      */
     public RemoteDevicePolicyManager devicePolicyManager() {
-        return new RemoteDevicePolicyManagerWrapper(mConnector);
+        return new RemoteDevicePolicyManager_Wrapper(mConnector);
     }
 }
diff --git a/common/device-side/bedstead/remotedpc/src/library/test/java/com/android/bedstead/remotedpc/RemoteDpcTest.java b/common/device-side/bedstead/remotedpc/src/library/test/java/com/android/bedstead/remotedpc/RemoteDpcTest.java
index e59e4733..7e3e889 100644
--- a/common/device-side/bedstead/remotedpc/src/library/test/java/com/android/bedstead/remotedpc/RemoteDpcTest.java
+++ b/common/device-side/bedstead/remotedpc/src/library/test/java/com/android/bedstead/remotedpc/RemoteDpcTest.java
@@ -703,7 +703,7 @@
     }
 
     @Test
-    public void frameworkCall_notProfileOwner_throwsSecurityException() {
+    public void frameworkCallRequiresProfileOwner_notProfileOwner_throwsSecurityException() {
         RemoteDpc remoteDPC = RemoteDpc.setAsDeviceOwner(sUser);
 
         try {
diff --git a/common/device-side/bedstead/remotedpc/src/processor/main/java/com/android/bedstead/remotedpc/processor/Processor.java b/common/device-side/bedstead/remotedpc/src/processor/main/java/com/android/bedstead/remotedpc/processor/Processor.java
new file mode 100644
index 0000000..dd61bce
--- /dev/null
+++ b/common/device-side/bedstead/remotedpc/src/processor/main/java/com/android/bedstead/remotedpc/processor/Processor.java
@@ -0,0 +1,382 @@
+/*
+ * 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.remotedpc.processor;
+
+import com.android.bedstead.remotedpc.processor.annotations.RemoteDpcAutomaticAdmin;
+import com.android.bedstead.remotedpc.processor.annotations.RemoteDpcManager;
+
+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.CodeBlock;
+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.HashSet;
+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.Element;
+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.MirroredTypeException;
+import javax.lang.model.type.TypeKind;
+import javax.tools.Diagnostic;
+import javax.tools.JavaFileObject;
+
+/** Processor for generating RemoteDPC API for framework manager classes. */
+@SupportedAnnotationTypes({
+        "com.android.bedstead.remotedpc.processor.annotations.RemoteDpcManager",
+})
+@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 CONFIGURATION_CLASSNAME =
+            ClassName.get("com.android.bedstead.remotedpc", "Configuration");
+    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");
+    public static final String MANAGER_PACKAGE_NAME = "com.android.bedstead.remotedpc.managers";
+
+    @Override
+    public SourceVersion getSupportedSourceVersion() {
+        return SourceVersion.latest();
+    }
+
+    @Override
+    public boolean process(Set<? extends TypeElement> annotations,
+            RoundEnvironment roundEnv) {
+
+        Set<TypeElement> interfaces = new HashSet<>();
+
+        for (Element e : roundEnv.getElementsAnnotatedWith(RemoteDpcManager.class)) {
+            TypeElement interfaceClass = (TypeElement) e;
+            interfaces.add(interfaceClass);
+            processRemoteDpcManagerInterface(interfaceClass);
+        }
+
+        if (interfaces.isEmpty()) {
+            // We only want to generate the provider and configuration once, not on every iteration
+            return true;
+        }
+
+        generateProvider(interfaces);
+        generateConfiguration();
+
+        return true;
+    }
+
+    private void processRemoteDpcManagerInterface(TypeElement interfaceClass) {
+        RemoteDpcManager r = interfaceClass.getAnnotation(RemoteDpcManager.class);
+        TypeElement managerClass = extractClassFromAnnotation(r::managerClass);
+
+        if (!interfaceClass.getKind().isInterface()) {
+            showError("@RemoteDpcManager can only be applied to interfaces", interfaceClass);
+        }
+
+        generateCrossProfileInterface(interfaceClass);
+        generateImplClass(interfaceClass, managerClass);
+        generateWrapperClass(interfaceClass);
+    }
+
+    /** Generate Impl which wraps the manager class. */
+    private void generateImplClass(TypeElement interfaceClass, TypeElement managerClass) {
+        ClassName managerClassName = ClassName.get(managerClass);
+
+        TypeSpec.Builder classBuilder = TypeSpec.classBuilder(implName(interfaceClass))
+                .addSuperinterface(crossProfileInterfaceName(interfaceClass))
+                .addModifiers(Modifier.PUBLIC, Modifier.FINAL);
+
+        classBuilder.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)
+                .addMember("value", "{\"NewApi\", \"OldTargetApi\"}")
+                .build());
+
+        classBuilder.addField(managerClassName,
+                "mManager", Modifier.PRIVATE, Modifier.FINAL);
+
+        classBuilder.addMethod(
+                MethodSpec.constructorBuilder()
+                        .addParameter(CONTEXT_CLASSNAME, "context")
+                        .addCode("mManager = context.getSystemService($T.class);",
+                                managerClassName)
+                .build()
+        );
+
+        for (ExecutableElement method : getMethods(interfaceClass)) {
+            MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(
+                    method.getSimpleName().toString())
+                    .returns(ClassName.get(method.getReturnType()))
+                    .addModifiers(Modifier.PUBLIC)
+                    .addAnnotation(Override.class);
+
+            for (VariableElement param : method.getParameters()) {
+                ParameterSpec parameterSpec =
+                        ParameterSpec.builder(ClassName.get(param.asType()),
+                                param.getSimpleName().toString()).build();
+
+                methodBuilder.addParameter(parameterSpec);
+            }
+
+            String parametersString = method.getParameters().stream()
+                    .map(VariableElement::getSimpleName)
+                    .collect(Collectors.joining(", "));
+            CodeBlock methodCall;
+
+            if (method.getAnnotation(RemoteDpcAutomaticAdmin.class) != null) {
+                // We just redirect to the other method, adding in the component
+                if (parametersString.isEmpty()) {
+                    methodCall = CodeBlock.of("$L($T.DPC_COMPONENT);", method.getSimpleName(),
+                            CONFIGURATION_CLASSNAME);
+                } else {
+                    methodCall = CodeBlock.of("$L($T.DPC_COMPONENT, $L);", method.getSimpleName(),
+                            CONFIGURATION_CLASSNAME, parametersString);
+                }
+            } else {
+                // We call through to the wrapped manager class
+                methodCall = CodeBlock.of("mManager.$L($L);",
+                        method.getSimpleName(), parametersString);
+            }
+
+            if (!method.getReturnType().getKind().equals(TypeKind.VOID)) {
+                methodCall = CodeBlock.of("return $L", methodCall);
+            }
+            methodBuilder.addCode(methodCall);
+
+            classBuilder.addMethod(methodBuilder.build());
+        }
+
+        PackageElement packageElement = (PackageElement) interfaceClass.getEnclosingElement();
+
+        writeClassToFile(packageElement.getQualifiedName().toString(), classBuilder.build());
+    }
+
+    /** Generate wrapper which wraps the cross-profile class. */
+    private void generateWrapperClass(TypeElement interfaceClass) {
+        TypeSpec.Builder classBuilder =
+                TypeSpec.classBuilder(
+                        wrapperName(interfaceClass))
+                        .addSuperinterface(crossProfileInterfaceName(interfaceClass))
+                        .addModifiers(Modifier.PUBLIC, Modifier.FINAL);
+
+
+        classBuilder.addField(CROSS_PROFILE_CONNECTOR_CLASSNAME,
+                "mConnector", Modifier.PRIVATE, Modifier.FINAL);
+        classBuilder.addField(profileTypeName(interfaceClass),
+                "mProfileType", Modifier.PRIVATE, Modifier.FINAL);
+
+        classBuilder.addMethod(
+                MethodSpec.constructorBuilder()
+                        .addModifiers(Modifier.PUBLIC)
+                        .addParameter(CROSS_PROFILE_CONNECTOR_CLASSNAME, "connector")
+                        .addCode("mConnector = connector;")
+                        .addCode("mProfileType = $T.create(connector);",
+                                profileTypeName(interfaceClass))
+                        .build()
+        );
+
+        for (ExecutableElement method : getMethods(interfaceClass)) {
+            MethodSpec.Builder methodBuilder =
+                    MethodSpec.methodBuilder(method.getSimpleName().toString())
+                    .returns(ClassName.get(method.getReturnType()))
+                    .addModifiers(Modifier.PUBLIC)
+                    .addAnnotation(Override.class);
+
+            for (VariableElement param : method.getParameters()) {
+                ParameterSpec parameterSpec =
+                        ParameterSpec.builder(ClassName.get(param.asType()),
+                                param.getSimpleName().toString()).build();
+
+                methodBuilder.addParameter(parameterSpec);
+            }
+
+            String parametersString = method.getParameters().stream()
+                    .map(VariableElement::getSimpleName)
+                    .collect(Collectors.joining(", "));
+
+            CodeBlock methodCall = CodeBlock.of("mProfileType.other().$L($L);",
+                    method.getSimpleName().toString(), parametersString);
+            if (!method.getReturnType().getKind().equals(TypeKind.VOID)) {
+                methodCall = CodeBlock.of("return $L", methodCall);
+            }
+
+            methodBuilder.beginControlFlow("try")
+                    .addCode("mConnector.connect();")
+                    .addCode(methodCall)
+                    .nextControlFlow("catch ($T e)",
+                            UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME)
+                    .addCode("throw new $T(\"Error connecting\", e);", NENE_EXCEPTION_CLASSNAME)
+                    .nextControlFlow("catch ($T e)",
+                            PROFILE_RUNTIME_EXCEPTION_CLASSNAME)
+                    .addCode("throw ($T) e.getCause();", RuntimeException.class)
+                    .endControlFlow();
+
+            classBuilder.addMethod(methodBuilder.build());
+        }
+
+        PackageElement packageElement = (PackageElement) interfaceClass.getEnclosingElement();
+        writeClassToFile(packageElement.getQualifiedName().toString(), classBuilder.build());
+    }
+
+    /** Generate sub-interface which is annotated @CrossProfile. */
+    private void generateCrossProfileInterface(TypeElement interfaceClass) {
+        TypeSpec.Builder classBuilder =
+                TypeSpec.interfaceBuilder(
+                        crossProfileInterfaceName(interfaceClass))
+                        .addSuperinterface(ClassName.get(interfaceClass))
+                        .addModifiers(Modifier.PUBLIC);
+
+        for (ExecutableElement method : getMethods(interfaceClass)) {
+            MethodSpec.Builder methodBuilder =
+                    MethodSpec.methodBuilder(method.getSimpleName().toString())
+                    .returns(ClassName.get(method.getReturnType()))
+                    .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
+                    .addAnnotation(CrossProfile.class)
+                    .addAnnotation(Override.class);
+
+            for (VariableElement param : method.getParameters()) {
+                ParameterSpec parameterSpec =
+                        ParameterSpec.builder(ClassName.get(param.asType()),
+                                param.getSimpleName().toString()).build();
+
+                methodBuilder.addParameter(parameterSpec);
+            }
+
+            classBuilder.addMethod(methodBuilder.build());
+        }
+
+        PackageElement packageElement = (PackageElement) interfaceClass.getEnclosingElement();
+        writeClassToFile(packageElement.getQualifiedName().toString(), classBuilder.build());
+    }
+
+    private void generateProvider(Set<TypeElement> interfaces) {
+        TypeSpec.Builder classBuilder =
+                TypeSpec.classBuilder(
+                        "Provider")
+                        .addModifiers(Modifier.PUBLIC, Modifier.FINAL);
+
+        for (TypeElement i : interfaces) {
+            MethodSpec.Builder methodBuilder =
+                    MethodSpec.methodBuilder("provide_" + i.getSimpleName())
+                    .returns(crossProfileInterfaceName(i))
+                    .addModifiers(Modifier.PUBLIC)
+                    .addParameter(CONTEXT_CLASSNAME, "context")
+                    .addAnnotation(CrossProfileProvider.class)
+                    .addCode("return new $T(context);", implName(i));
+
+            classBuilder.addMethod(methodBuilder.build());
+        }
+
+        writeClassToFile(MANAGER_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(MANAGER_PACKAGE_NAME, classBuilder.build());
+    }
+
+    private TypeElement extractClassFromAnnotation(Runnable runnable) {
+        try {
+            runnable.run();
+        } catch (MirroredTypeException e) {
+            return e.getTypeMirrors().stream()
+                    .map(t -> (TypeElement) processingEnv.getTypeUtils().asElement(t))
+                    .findFirst()
+                    .get();
+        }
+        throw new AssertionError("Could not extract class from annotation");
+    }
+
+    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 ClassName crossProfileInterfaceName(TypeElement interfaceClass) {
+        return ClassName.bestGuess(interfaceClass.getQualifiedName().toString() + "_Internal");
+    }
+
+    private ClassName implName(TypeElement interfaceClass) {
+        return ClassName.bestGuess(interfaceClass.getQualifiedName().toString() + "_Impl");
+    }
+
+    private ClassName wrapperName(TypeElement interfaceClass) {
+        return ClassName.bestGuess(interfaceClass.getQualifiedName().toString() + "_Wrapper");
+    }
+
+    private ClassName profileTypeName(TypeElement interfaceClass) {
+        ClassName crossProfileInterfaceName = crossProfileInterfaceName(interfaceClass);
+        return ClassName.get(crossProfileInterfaceName.packageName(),
+                "Profile" + crossProfileInterfaceName.simpleName());
+    }
+
+    private Set<ExecutableElement> getMethods(TypeElement interfaceClass) {
+        return interfaceClass.getEnclosedElements().stream()
+                .filter(e -> e instanceof ExecutableElement)
+                .map(e -> (ExecutableElement) e)
+                .collect(Collectors.toSet());
+    }
+
+    private void showError(String errorText, Element errorElement) {
+        processingEnv
+                .getMessager()
+                .printMessage(Diagnostic.Kind.ERROR, errorText, errorElement);
+    }
+}
diff --git a/common/device-side/bedstead/remotedpc/src/processor/main/java/com/android/bedstead/remotedpc/processor/annotations/RemoteDpcAutomaticAdmin.java b/common/device-side/bedstead/remotedpc/src/processor/main/java/com/android/bedstead/remotedpc/processor/annotations/RemoteDpcAutomaticAdmin.java
new file mode 100644
index 0000000..b609a0e
--- /dev/null
+++ b/common/device-side/bedstead/remotedpc/src/processor/main/java/com/android/bedstead/remotedpc/processor/annotations/RemoteDpcAutomaticAdmin.java
@@ -0,0 +1,40 @@
+/*
+ * 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.remotedpc.processor.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Applied to methods which map to an identically named method on the same interface but needs the
+ * admin {@link android.content.ComponentName} added as the first argument.
+ *
+ * <p>This should always be paired with another method which accepts the
+ * {@link android.content.ComponentName}.
+ *
+ * <p>For example:
+ * <pre><code>
+ *   boolean isUsingUnifiedPassword(@NonNull ComponentName admin);
+ *   @RemoteDpcAutomaticAdmin boolean isUsingUnifiedPassword();
+ * <code></pre>
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.CLASS)
+public @interface RemoteDpcAutomaticAdmin {
+}
diff --git a/common/device-side/bedstead/remotedpc/src/processor/main/java/com/android/bedstead/remotedpc/processor/annotations/RemoteDpcManager.java b/common/device-side/bedstead/remotedpc/src/processor/main/java/com/android/bedstead/remotedpc/processor/annotations/RemoteDpcManager.java
new file mode 100644
index 0000000..8d767a33
--- /dev/null
+++ b/common/device-side/bedstead/remotedpc/src/processor/main/java/com/android/bedstead/remotedpc/processor/annotations/RemoteDpcManager.java
@@ -0,0 +1,39 @@
+/*
+ * 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.remotedpc.processor.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Mark an interface as wrapping a framework manager class for RemoteDPC.
+ *
+ * <p>Every method in the interface must have a matching method on the {@code managerClass} or else
+ * be annotated {@link RemoteDpcAutomaticAdmin}.
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.CLASS)
+public @interface RemoteDpcManager {
+
+    /**
+     * The manager class being wrapped.
+     *
+     * <p>For example, {@link android.app.admin.DevicePolicyManager}. */
+    Class<?> managerClass();
+}