1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.bedstead.remoteframeworkclasses.processor;
18 
19 import com.android.bedstead.remoteframeworkclasses.processor.annotations.RemoteFrameworkClasses;
20 
21 import com.google.android.enterprise.connectedapps.annotations.CrossUser;
22 import com.google.auto.service.AutoService;
23 import com.google.common.collect.ImmutableSet;
24 import com.google.common.io.Resources;
25 import com.squareup.javapoet.AnnotationSpec;
26 import com.squareup.javapoet.ClassName;
27 import com.squareup.javapoet.JavaFile;
28 import com.squareup.javapoet.MethodSpec;
29 import com.squareup.javapoet.ParameterSpec;
30 import com.squareup.javapoet.TypeSpec;
31 
32 import java.io.IOException;
33 import java.io.PrintWriter;
34 import java.net.URL;
35 import java.nio.charset.StandardCharsets;
36 import java.util.ArrayList;
37 import java.util.HashMap;
38 import java.util.HashSet;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.Objects;
42 import java.util.Optional;
43 import java.util.Set;
44 import java.util.stream.Collectors;
45 
46 import javax.annotation.processing.AbstractProcessor;
47 import javax.annotation.processing.RoundEnvironment;
48 import javax.annotation.processing.SupportedAnnotationTypes;
49 import javax.lang.model.SourceVersion;
50 import javax.lang.model.element.ExecutableElement;
51 import javax.lang.model.element.Modifier;
52 import javax.lang.model.element.TypeElement;
53 import javax.lang.model.element.VariableElement;
54 import javax.lang.model.type.DeclaredType;
55 import javax.lang.model.type.TypeKind;
56 import javax.lang.model.type.TypeMirror;
57 import javax.lang.model.util.Elements;
58 import javax.tools.JavaFileObject;
59 
60 /**
61  * Processor for generating {@code RemoteSystemService} classes.
62  *
63  * <p>This is started by including the {@link RemoteFrameworkClasses} annotation.
64  *
65  * <p>For each entry in {@code FRAMEWORK_CLASSES} this will generate an interface including all
66  * public
67  * and test APIs with the {@code CrossUser} annotation. This interface will be named the same as
68  * the
69  * framework class except with a prefix of "Remote", and will be in the same package.
70  *
71  * <p>This will also generate an implementation of the interface which takes an instance of the
72  * framework class in the constructor, and each method proxying calls to the framework class.
73  */
74 @SupportedAnnotationTypes({
75         "com.android.bedstead.remoteframeworkclasses.processor.annotations.RemoteFrameworkClasses",
76 })
77 @AutoService(javax.annotation.processing.Processor.class)
78 public final class Processor extends AbstractProcessor {
79 
80     private static final ImmutableSet<String> FRAMEWORK_CLASSES =
81             loadList("/apis/framework-classes.txt");
82 
83     private static final String PARENT_PROFILE_INSTANCE =
84             "public android.app.admin.DevicePolicyManager getParentProfileInstance(android"
85                     + ".content.ComponentName)";
86     private static final String GET_CONTENT_RESOLVER =
87             "public android.content.ContentResolver getContentResolver()";
88     private static final String GET_ADAPTER =
89             "public android.bluetooth.BluetoothAdapter getAdapter()";
90     private static final String GET_DEFAULT_ADAPTER =
91             "public static android.bluetooth.BluetoothAdapter getDefaultAdapter()";
92 
93     private static final ImmutableSet<String> BLOCKLISTED_TYPES =
94             loadList("/apis/type-blocklist.txt");
95     private static final ImmutableSet<String> ALLOWLISTED_METHODS =
96             loadList("/apis/allowlisted-methods.txt");
97 
98     // TODO(b/332847045): get resource from TestApisReflection target
99     static final ImmutableSet<String> ALLOWLISTED_TEST_CLASSES =
100             loadList("/apis/allowlisted-test-classes.txt");
loadList(String filename)101     private static ImmutableSet<String> loadList(String filename) {
102         try {
103             return ImmutableSet.copyOf(Resources.toString(
104                     Processor.class.getResource(filename),
105                     StandardCharsets.UTF_8).split("\n"));
106         } catch (IOException e) {
107             throw new IllegalStateException("Could not read file", e);
108         }
109     }
110 
111     /**
112      * The TestApisReflection module generates proxy classes used to access TestApi classes and
113      * methods through reflection. These proxy classes are then processed like other framework
114      * classes in this processor.
115      */
116     static final String TEST_APIS_REFLECTION_PACKAGE = "android.cts.testapisreflection";
117     private static final String TEST_APIS_REFLECTION_FILE =
118             TEST_APIS_REFLECTION_PACKAGE + ".TestApisReflectionKt";
119 
120     private static final ClassName NULL_PARCELABLE_REMOTE_DEVICE_POLICY_MANAGER_CLASSNAME =
121             ClassName.get("com.android.bedstead.remoteframeworkclasses",
122                     "NullParcelableRemoteDevicePolicyManager");
123     private static final ClassName NULL_PARCELABLE_REMOTE_CONTENT_RESOLVER_CLASSNAME =
124             ClassName.get("com.android.bedstead.remoteframeworkclasses",
125                     "NullParcelableRemoteContentResolver");
126     private static final ClassName NULL_PARCELABLE_REMOTE_BLUETOOTH_ADAPTER_CLASSNAME =
127             ClassName.get("com.android.bedstead.remoteframeworkclasses",
128                     "NullParcelableRemoteBluetoothAdapter");
129 
130     // TODO(b/205562849): These only support passing null, which is fine for existing tests but
131     //  will be misleading
132     private static final ClassName NULL_PARCELABLE_ACTIVITY_CLASSNAME =
133             ClassName.get("com.android.bedstead.remoteframeworkclasses",
134                     "NullParcelableActivity");
135     private static final ClassName NULL_PARCELABLE_ACCOUNT_MANAGER_CALLBACK_CLASSNAME =
136             ClassName.get("com.android.bedstead.remoteframeworkclasses",
137                     "NullParcelableAccountManagerCallback");
138     private static final ClassName NULL_HANDLER_CALLBACK_CLASSNAME =
139             ClassName.get("com.android.bedstead.remoteframeworkclasses",
140                     "NullParcelableHandler");
141 
142     private static final ClassName COMPONENT_NAME_CLASSNAME =
143             ClassName.get("android.content", "ComponentName");
144 
145     private static final ClassName ACCOUNT_MANAGE_FUTURE_WRAPPER_CLASSNAME =
146             ClassName.get(
147                     "com.android.bedstead.remoteframeworkclasses", "AccountManagerFutureWrapper");
148 
149     @Override
getSupportedSourceVersion()150     public SourceVersion getSupportedSourceVersion() {
151         return SourceVersion.latest();
152     }
153 
154     @Override
process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)155     public boolean process(Set<? extends TypeElement> annotations,
156             RoundEnvironment roundEnv) {
157         if (!roundEnv.getElementsAnnotatedWith(RemoteFrameworkClasses.class).isEmpty()) {
158             Set<MethodSignature> allowListedMethods = ALLOWLISTED_METHODS.stream()
159                     .map(i -> MethodSignature.forApiString(i, processingEnv.getTypeUtils(),
160                             processingEnv.getElementUtils()))
161                     .collect(Collectors.toUnmodifiableSet());
162 
163             for (String systemService : FRAMEWORK_CLASSES) {
164                 TypeElement typeElement =
165                         processingEnv.getElementUtils().getTypeElement(systemService);
166                 generateRemoteSystemService(
167                         typeElement, allowListedMethods, processingEnv.getElementUtils());
168             }
169 
170             generateWrappers();
171         }
172 
173         return true;
174     }
175 
generateWrappers()176     private void generateWrappers() {
177         generateWrapper(NULL_PARCELABLE_REMOTE_DEVICE_POLICY_MANAGER_CLASSNAME);
178         generateWrapper(NULL_PARCELABLE_REMOTE_CONTENT_RESOLVER_CLASSNAME);
179         generateWrapper(NULL_PARCELABLE_REMOTE_BLUETOOTH_ADAPTER_CLASSNAME);
180         generateWrapper(NULL_PARCELABLE_ACTIVITY_CLASSNAME);
181         generateWrapper(NULL_PARCELABLE_ACCOUNT_MANAGER_CALLBACK_CLASSNAME);
182         generateWrapper(NULL_HANDLER_CALLBACK_CLASSNAME);
183     }
184 
generateWrapper(ClassName className)185     private void generateWrapper(ClassName className) {
186         String contents = null;
187         try {
188             URL url = Processor.class.getResource(
189                     "/parcelablewrappers/" + className.simpleName() + ".java.txt");
190             contents = Resources.toString(url, StandardCharsets.UTF_8);
191         } catch (IOException e) {
192             throw new IllegalStateException("Could not parse wrapper " + className, e);
193         }
194 
195         JavaFileObject builderFile;
196         try {
197             builderFile = processingEnv.getFiler()
198                     .createSourceFile(className.packageName() + "." + className.simpleName());
199         } catch (IOException e) {
200             throw new IllegalStateException(
201                     "Could not write parcelablewrapper for " + className, e);
202         }
203 
204         try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {
205             out.write(contents);
206         } catch (IOException e) {
207             throw new IllegalStateException(
208                     "Could not write parcelablewrapper for " + className, e);
209         }
210     }
211 
generateRemoteSystemService( TypeElement frameworkClass, Set<MethodSignature> allowListedMethods, Elements elements)212     private void generateRemoteSystemService(
213             TypeElement frameworkClass,
214             Set<MethodSignature> allowListedMethods,
215             Elements elements) {
216         Set<Api> apis = filterMethods(frameworkClass,
217                 getMethods(frameworkClass, processingEnv.getElementUtils()),
218                 Apis.forClass(frameworkClass.getQualifiedName().toString(),
219                         processingEnv.getTypeUtils(), processingEnv.getElementUtils()), elements)
220                 .stream()
221                 .filter(t -> t.isTestApi ||
222                         !usesBlocklistedType(t.method, allowListedMethods, elements))
223                 .collect(Collectors.toSet());
224 
225         generateFrameworkInterface(frameworkClass, apis);
226         generateFrameworkImpl(frameworkClass, apis);
227 
228         if (frameworkClass.getSimpleName().contentEquals("DevicePolicyManager")) {
229             // Special case, we need to support the .getParentProfileInstance method
230             generateDpmParent(frameworkClass, apis);
231         }
232     }
233 
removeTypeArguments(TypeMirror type)234     private static String removeTypeArguments(TypeMirror type) {
235         if (type instanceof DeclaredType) {
236             return ((DeclaredType) type).asElement().asType().toString().split("<", 2)[0];
237         }
238         return type.toString();
239     }
240 
extractTypeArguments(TypeMirror type)241     public static List<TypeMirror> extractTypeArguments(TypeMirror type) {
242         if (!(type instanceof DeclaredType)) {
243             return new ArrayList<>();
244         }
245 
246         return new ArrayList<>(((DeclaredType) type).getTypeArguments());
247     }
248 
isBlocklistedType(TypeMirror typeMirror)249     private boolean isBlocklistedType(TypeMirror typeMirror) {
250         if (BLOCKLISTED_TYPES.contains(removeTypeArguments(typeMirror))) {
251             return true;
252         }
253 
254         for (TypeMirror t : extractTypeArguments(typeMirror)) {
255             if (isBlocklistedType(t)) {
256                 return true;
257             }
258         }
259 
260         return false;
261     }
262 
usesBlocklistedType(ExecutableElement method, Set<MethodSignature> allowListedMethods, Elements elements)263     private boolean usesBlocklistedType(ExecutableElement method, Set<MethodSignature> allowListedMethods, Elements elements) {
264         if (allowListedMethods.contains(MethodSignature.forMethod(method, elements))) {
265             return false; // Special case hacked in methods
266         }
267 
268         if (isBlocklistedType(method.getReturnType())) {
269             return true;
270         }
271 
272         for (VariableElement parameter : method.getParameters()) {
273             if (isBlocklistedType(parameter.asType())) {
274                 return true;
275             }
276         }
277 
278         for (TypeMirror exception : method.getThrownTypes()) {
279             if (isBlocklistedType(exception)) {
280                 return true;
281             }
282         }
283 
284         return false;
285     }
286 
generateFrameworkInterface(TypeElement frameworkClass, Set<Api> apis)287     private void generateFrameworkInterface(TypeElement frameworkClass, Set<Api> apis) {
288         MethodSignature parentProfileInstanceSignature =
289                 MethodSignature.forApiString(PARENT_PROFILE_INSTANCE, processingEnv.getTypeUtils(),
290                         processingEnv.getElementUtils());
291         MethodSignature getContentResolverSignature =
292                 MethodSignature.forApiString(GET_CONTENT_RESOLVER, processingEnv.getTypeUtils(),
293                         processingEnv.getElementUtils());
294         MethodSignature getAdapterSignature =
295                 MethodSignature.forApiString(GET_ADAPTER, processingEnv.getTypeUtils(),
296                         processingEnv.getElementUtils());
297         MethodSignature getDefaultAdapterSignature =
298                 MethodSignature.forApiString(GET_DEFAULT_ADAPTER, processingEnv.getTypeUtils(),
299                         processingEnv.getElementUtils());
300 
301         Map<MethodSignature, ClassName> signatureReturnOverrides = new HashMap<>();
302         signatureReturnOverrides.put(parentProfileInstanceSignature,
303                 ClassName.get("android.app.admin", "RemoteDevicePolicyManager"));
304         signatureReturnOverrides.put(getContentResolverSignature,
305                 ClassName.get("android.content", "RemoteContentResolver"));
306         signatureReturnOverrides.put(getAdapterSignature,
307                 ClassName.get("android.bluetooth", "RemoteBluetoothAdapter"));
308         signatureReturnOverrides.put(getDefaultAdapterSignature,
309                 ClassName.get("android.bluetooth", "RemoteBluetoothAdapter"));
310 
311         String packageName = frameworkClass.getEnclosingElement().toString();
312         ClassName className = ClassName.get(packageName,
313                 "Remote" + frameworkClass.getSimpleName().toString());
314         ClassName implClassName = ClassName.get(packageName,
315                 "Remote" + frameworkClass.getSimpleName().toString() + "Impl");
316         TypeSpec.Builder classBuilder =
317                 TypeSpec.interfaceBuilder(className)
318                         .addModifiers(Modifier.PUBLIC);
319 
320         classBuilder.addJavadoc("Public, test, and system interface for {@link $T}.\n\n",
321                 frameworkClass);
322         classBuilder.addJavadoc("<p>All methods are annotated {@link $T} for compatibility with the"
323                 + " Connected Apps SDK.\n\n", CrossUser.class);
324         classBuilder.addJavadoc("<p>For implementation see {@link $T}.\n", implClassName);
325 
326 
327         classBuilder.addAnnotation(AnnotationSpec.builder(CrossUser.class)
328                 .addMember("parcelableWrappers",
329                         "{$T.class, $T.class, $T.class, $T.class, $T.class, $T.class}",
330                         NULL_PARCELABLE_REMOTE_DEVICE_POLICY_MANAGER_CLASSNAME,
331                         NULL_PARCELABLE_REMOTE_CONTENT_RESOLVER_CLASSNAME,
332                         NULL_PARCELABLE_REMOTE_BLUETOOTH_ADAPTER_CLASSNAME,
333                         NULL_PARCELABLE_ACTIVITY_CLASSNAME,
334                         NULL_PARCELABLE_ACCOUNT_MANAGER_CALLBACK_CLASSNAME,
335                         NULL_HANDLER_CALLBACK_CLASSNAME)
336                 .addMember("futureWrappers", "$T.class",
337                         ACCOUNT_MANAGE_FUTURE_WRAPPER_CLASSNAME)
338                 .build());
339 
340         for (Api api : apis) {
341             ExecutableElement method = api.method;
342 
343             MethodSpec.Builder methodBuilder =
344                     MethodSpec.methodBuilder(method.getSimpleName().toString())
345                             .returns(ClassName.get(method.getReturnType()))
346                             .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
347                             .addAnnotation(CrossUser.class);
348 
349             MethodSignature signature = MethodSignature.forMethod(method,
350                     processingEnv.getElementUtils());
351 
352             if (signatureReturnOverrides.containsKey(signature)) {
353                 methodBuilder.returns(signatureReturnOverrides.get(signature));
354             }
355 
356             methodBuilder.addJavadoc("See {@link $T#$L}.",
357                     ClassName.get(frameworkClass.asType()), method.getSimpleName());
358 
359             for (TypeMirror thrownType : method.getThrownTypes()) {
360                 methodBuilder.addException(ClassName.get(thrownType));
361             }
362 
363             List<? extends VariableElement> parameters;
364             if (api.isTestApi) {
365                 // This is a kotlin extension method. Kotlin extension methods when converted to
366                 // java code have the receiver as the first argument. We need to drop this argument.
367                 parameters = method.getParameters().subList(1, method.getParameters().size());
368             } else {
369                 parameters = method.getParameters();
370             }
371 
372             for (VariableElement param : parameters) {
373                 ParameterSpec parameterSpec =
374                         ParameterSpec.builder(ClassName.get(param.asType()),
375                                 param.getSimpleName().toString()).build();
376 
377                 methodBuilder.addParameter(parameterSpec);
378             }
379 
380             classBuilder.addMethod(methodBuilder.build());
381         }
382 
383         writeClassToFile(packageName, classBuilder.build());
384     }
385 
generateDpmParent(TypeElement frameworkClass, Set<Api> apis)386     private void generateDpmParent(TypeElement frameworkClass, Set<Api> apis) {
387         MethodSignature parentProfileInstanceSignature = MethodSignature.forApiString(
388                 PARENT_PROFILE_INSTANCE, processingEnv.getTypeUtils(),
389                 processingEnv.getElementUtils());
390         String packageName = frameworkClass.getEnclosingElement().toString();
391         ClassName className =
392                 ClassName.get(packageName, "Remote" + frameworkClass.getSimpleName() + "Parent");
393         TypeSpec.Builder classBuilder =
394                 TypeSpec.classBuilder(className).addModifiers(Modifier.FINAL, Modifier.PUBLIC);
395 
396         classBuilder.addAnnotation(AnnotationSpec.builder(CrossUser.class)
397                 .addMember("parcelableWrappers",
398                         "{$T.class, $T.class, $T.class, $T.class, $T.class, $T.class}",
399                         NULL_PARCELABLE_REMOTE_DEVICE_POLICY_MANAGER_CLASSNAME,
400                         NULL_PARCELABLE_REMOTE_CONTENT_RESOLVER_CLASSNAME,
401                         NULL_PARCELABLE_REMOTE_BLUETOOTH_ADAPTER_CLASSNAME,
402                         NULL_PARCELABLE_ACTIVITY_CLASSNAME,
403                         NULL_PARCELABLE_ACCOUNT_MANAGER_CALLBACK_CLASSNAME,
404                         NULL_HANDLER_CALLBACK_CLASSNAME)
405                 .build());
406 
407         classBuilder.addField(ClassName.get(frameworkClass),
408                 "mFrameworkClass", Modifier.PRIVATE, Modifier.FINAL);
409 
410         classBuilder.addMethod(
411                 MethodSpec.constructorBuilder()
412                         .addModifiers(Modifier.PUBLIC)
413                         .addParameter(ClassName.get(frameworkClass), "frameworkClass")
414                         .addCode("mFrameworkClass = frameworkClass;")
415                         .build()
416         );
417 
418         for (Api api : apis) {
419             ExecutableElement method = api.method;
420 
421             MethodSpec.Builder methodBuilder =
422                     MethodSpec.methodBuilder(method.getSimpleName().toString())
423                             .returns(ClassName.get(method.getReturnType()))
424                             .addModifiers(Modifier.PUBLIC)
425                             .addAnnotation(CrossUser.class);
426 
427             MethodSignature signature = MethodSignature.forMethod(method,
428                     processingEnv.getElementUtils());
429 
430             for (TypeMirror thrownType : method.getThrownTypes()) {
431                 methodBuilder.addException(ClassName.get(thrownType));
432             }
433 
434             methodBuilder.addParameter(COMPONENT_NAME_CLASSNAME, "profileOwnerComponentName");
435 
436             List<? extends VariableElement> parameters;
437             if (api.isTestApi) {
438                 // This is a kotlin extension method. Kotlin extension methods when converted to
439                 // java code have the receiver as the first argument. We need to drop this argument.
440                 parameters = method.getParameters().subList(1, method.getParameters().size());
441             } else {
442                 parameters = method.getParameters();
443             }
444 
445             List<String> paramNames = new ArrayList<>(parameters.size());
446             for (VariableElement param : parameters) {
447                 String paramName = param.getSimpleName().toString();
448                 ParameterSpec parameterSpec =
449                         ParameterSpec.builder(ClassName.get(param.asType()), paramName).build();
450 
451                 paramNames.add(paramName);
452 
453                 methodBuilder.addParameter(parameterSpec);
454             }
455 
456             if (signature.equals(parentProfileInstanceSignature)) {
457                 // Special case, we want to return a RemoteDevicePolicyManager instead
458                 methodBuilder.returns(ClassName.get(
459                         "android.app.admin", "RemoteDevicePolicyManager"));
460                 methodBuilder.addStatement(
461                         "mFrameworkClass.getParentProfileInstance(profileOwnerComponentName).$L"
462                                 + "($L)",
463                         method.getSimpleName(), String.join(", ", paramNames));
464                 methodBuilder.addStatement("throw new $T($S)", UnsupportedOperationException.class,
465                         "TestApp does not support calling .getParentProfileInstance() on a parent"
466                                 + ".");
467             } else if (method.getReturnType().getKind().equals(TypeKind.VOID)) {
468                 if (api.isTestApi) {
469                     if (paramNames.isEmpty()) {
470                         methodBuilder.addStatement(
471                                 "$L.$L(mFrameworkClass.getParentProfileInstance(profileOwnerComponentName))",
472                                 TEST_APIS_REFLECTION_FILE, method.getSimpleName());
473                     } else {
474                         methodBuilder.addStatement(
475                                 "$L.$L(mFrameworkClass.getParentProfileInstance(profileOwnerComponentName), $L)",
476                                 TEST_APIS_REFLECTION_FILE, method.getSimpleName(),
477                                 String.join(", ", paramNames));
478                     }
479                 } else {
480                     methodBuilder.addStatement(
481                             "mFrameworkClass.getParentProfileInstance(profileOwnerComponentName).$L($L)",
482                             method.getSimpleName(), String.join(", ", paramNames));
483                 }
484             } else {
485                 if (api.isTestApi) {
486                     if (paramNames.isEmpty()) {
487                         methodBuilder.addStatement(
488                                 "return $L.$L(mFrameworkClass.getParentProfileInstance(profileOwnerComponentName))",
489                                 TEST_APIS_REFLECTION_FILE, method.getSimpleName());
490                     } else {
491                         methodBuilder.addStatement(
492                                 "return $L.$L(mFrameworkClass.getParentProfileInstance(profileOwnerComponentName), $L)",
493                                 TEST_APIS_REFLECTION_FILE, method.getSimpleName(),
494                                 String.join(", ", paramNames));
495                     }
496                 } else {
497                     methodBuilder.addStatement(
498                             "return mFrameworkClass.getParentProfileInstance"
499                                     + "(profileOwnerComponentName).$L($L)",
500                             method.getSimpleName(), String.join(", ", paramNames));
501                 }
502             }
503 
504             classBuilder.addMethod(methodBuilder.build());
505         }
506 
507         writeClassToFile(packageName, classBuilder.build());
508     }
509 
generateFrameworkImpl(TypeElement frameworkClass, Set<Api> apis)510     private void generateFrameworkImpl(TypeElement frameworkClass, Set<Api> apis) {
511         MethodSignature parentProfileInstanceSignature =
512                 MethodSignature.forApiString(PARENT_PROFILE_INSTANCE, processingEnv.getTypeUtils(),
513                         processingEnv.getElementUtils());
514         MethodSignature getContentResolverSignature =
515                 MethodSignature.forApiString(GET_CONTENT_RESOLVER, processingEnv.getTypeUtils(),
516                         processingEnv.getElementUtils());
517         MethodSignature getAdapterSignature =
518                 MethodSignature.forApiString(GET_ADAPTER, processingEnv.getTypeUtils(),
519                         processingEnv.getElementUtils());
520         MethodSignature getDefaultAdapterSignature =
521                 MethodSignature.forApiString(GET_DEFAULT_ADAPTER, processingEnv.getTypeUtils(),
522                         processingEnv.getElementUtils());
523 
524         Map<MethodSignature, ClassName> signatureReturnOverrides = new HashMap<>();
525         signatureReturnOverrides.put(parentProfileInstanceSignature,
526                 ClassName.get("android.app.admin", "RemoteDevicePolicyManager"));
527         signatureReturnOverrides.put(getContentResolverSignature,
528                 ClassName.get("android.content", "RemoteContentResolver"));
529         signatureReturnOverrides.put(getAdapterSignature,
530                 ClassName.get("android.bluetooth", "RemoteBluetoothAdapter"));
531         signatureReturnOverrides.put(getDefaultAdapterSignature,
532                 ClassName.get("android.bluetooth", "RemoteBluetoothAdapter"));
533 
534         String packageName = frameworkClass.getEnclosingElement().toString();
535         ClassName interfaceClassName = ClassName.get(packageName,
536                 "Remote" + frameworkClass.getSimpleName().toString());
537         ClassName className = ClassName.get(packageName,
538                 "Remote" + frameworkClass.getSimpleName().toString() + "Impl");
539         TypeSpec.Builder classBuilder =
540                 TypeSpec.classBuilder(
541                                 className)
542                         .addSuperinterface(interfaceClassName)
543                         .addModifiers(Modifier.PUBLIC);
544 
545         classBuilder.addField(ClassName.get(frameworkClass),
546                 "mFrameworkClass", Modifier.PRIVATE, Modifier.FINAL);
547 
548         classBuilder.addMethod(
549                 MethodSpec.constructorBuilder()
550                         .addModifiers(Modifier.PUBLIC)
551                         .addParameter(ClassName.get(frameworkClass), "frameworkClass")
552                         .addCode("mFrameworkClass = frameworkClass;")
553                         .build()
554         );
555 
556         for (Api api : apis) {
557             ExecutableElement method = api.method;
558 
559             MethodSpec.Builder methodBuilder =
560                     MethodSpec.methodBuilder(method.getSimpleName().toString())
561                             .returns(ClassName.get(method.getReturnType()))
562                             .addModifiers(Modifier.PUBLIC)
563                             .addAnnotation(Override.class);
564 
565             MethodSignature signature = MethodSignature.forMethod(method,
566                     processingEnv.getElementUtils());
567 
568             for (TypeMirror thrownType : method.getThrownTypes()) {
569                 methodBuilder.addException(ClassName.get(thrownType));
570             }
571 
572             List<? extends VariableElement> parameters;
573             if (api.isTestApi) {
574                 // This is a kotlin extension method. Kotlin extension methods when converted to
575                 // java code have the receiver as the first argument. We need to drop this argument.
576                 parameters = method.getParameters().subList(1, method.getParameters().size());
577             } else {
578                 parameters = method.getParameters();
579             }
580 
581             List<String> paramNames = new ArrayList<>(parameters.size());
582             for (VariableElement param : parameters) {
583                 String paramName = param.getSimpleName().toString();
584 
585                 ParameterSpec parameterSpec =
586                         ParameterSpec.builder(ClassName.get(param.asType()), paramName).build();
587 
588                 paramNames.add(paramName);
589 
590                 methodBuilder.addParameter(parameterSpec);
591             }
592 
593             String frameworkClassName = "mFrameworkClass";
594 
595             if (method.getModifiers().contains(Modifier.STATIC)) {
596                 frameworkClassName = frameworkClass.getQualifiedName().toString();
597             }
598 
599             if (signatureReturnOverrides.containsKey(signature)) {
600                 methodBuilder.returns(signatureReturnOverrides.get(signature));
601 
602                 ClassName iClassName = signatureReturnOverrides.get(signature);
603                 ClassName implClassName = ClassName.get(iClassName.packageName(), iClassName.simpleName() + "Impl");
604 
605                 methodBuilder.addStatement(
606                         "$1T ret = new $1T($2L.$3L($4L))",
607                         implClassName, frameworkClassName,
608                         method.getSimpleName(), String.join(", ", paramNames));
609                 // We assume all replacements are null-only
610                 methodBuilder.addStatement("return null");
611             } else if (method.getReturnType().getKind().equals(TypeKind.VOID)) {
612                 if (api.isTestApi) {
613                     if (paramNames.isEmpty()) {
614                         methodBuilder.addStatement(
615                                 "$L.$L($L)",
616                                 TEST_APIS_REFLECTION_FILE, method.getSimpleName(),
617                                 "mFrameworkClass");
618                     } else {
619                         methodBuilder.addStatement(
620                                 "$L.$L($L, $L)",
621                                 TEST_APIS_REFLECTION_FILE, method.getSimpleName(),
622                                 "mFrameworkClass",
623                                 String.join(", ", paramNames));
624                     }
625                 } else {
626                     methodBuilder.addStatement(
627                             "$L.$L($L)",
628                             frameworkClassName, method.getSimpleName(),
629                             String.join(", ", paramNames));
630                 }
631             } else {
632                 if (api.isTestApi) {
633                     if (paramNames.isEmpty()) {
634                         methodBuilder.addStatement(
635                                 "return $L.$L($L)",
636                                 TEST_APIS_REFLECTION_FILE, method.getSimpleName(),
637                                 "mFrameworkClass");
638                     } else {
639                         methodBuilder.addStatement(
640                                 "return $L.$L($L, $L)",
641                                 TEST_APIS_REFLECTION_FILE, method.getSimpleName(),
642                                 "mFrameworkClass",
643                                 String.join(", ", paramNames));
644                     }
645                 } else {
646                     methodBuilder.addStatement(
647                             "return $L.$L($L)",
648                             frameworkClassName, method.getSimpleName(),
649                             String.join(", ", paramNames));
650                 }
651             }
652 
653             classBuilder.addMethod(methodBuilder.build());
654         }
655 
656         writeClassToFile(packageName, classBuilder.build());
657     }
658 
filterMethods(TypeElement frameworkClass, Set<ExecutableElement> allMethods, Apis validApis, Elements elements)659     private Set<Api> filterMethods(TypeElement frameworkClass,
660             Set<ExecutableElement> allMethods, Apis validApis, Elements elements) {
661         Set<Api> filteredMethods = new HashSet<>();
662 
663         for (ExecutableElement method : allMethods) {
664             MethodSignature methodSignature = MethodSignature.forMethod(method, elements);
665             if (validApis.methods().contains(methodSignature)) {
666                 if (method.getModifiers().contains(Modifier.PROTECTED)) {
667                     System.out.println(methodSignature + " is protected. Dropping");
668                 } else {
669                     filteredMethods.add(new Api(method, /* isTestApi= */ false));
670                 }
671             } else {
672                 System.out.println("No matching public API for " + methodSignature);
673             }
674         }
675 
676         filterValidTestApis(filteredMethods, frameworkClass, validApis, elements);
677 
678         return filteredMethods;
679     }
680 
filterValidTestApis(Set<Api> filteredMethods, TypeElement frameworkClass, Apis validApis, Elements elements)681     private void filterValidTestApis(Set<Api> filteredMethods,
682             TypeElement frameworkClass, Apis validApis, Elements elements) {
683         Set<ExecutableElement> testMethods = new HashSet<>();
684         TypeElement testApisReflectionTypeElement =
685                 processingEnv.getElementUtils().getTypeElement(TEST_APIS_REFLECTION_FILE);
686 
687         testApisReflectionTypeElement.getEnclosedElements().stream()
688                 .filter(e -> e instanceof ExecutableElement)
689                 .map(e -> (ExecutableElement) e)
690                 .filter(e -> e.getModifiers().contains(Modifier.PUBLIC))
691                 .forEach(e -> testMethods.add(e));
692 
693         for (ExecutableElement method : testMethods) {
694             MethodSignature methodSignature = MethodSignature.forMethod(method, elements);
695 
696             if (!methodSignature.mParameterTypes.get(0).equals(
697                     frameworkClass.getQualifiedName().toString())) {
698                 continue;
699             }
700 
701             Optional<MethodSignature> validTestApi = validApis.methods().stream().filter(a ->
702                     methodSignature.getName().equals(a.getName()) &&
703                             methodSignature.getReturnType().equals(a.getReturnType()) &&
704                             methodSignature.mExceptions.equals(a.mExceptions) &&
705                             methodSignature.mParameterTypes.containsAll(a.mParameterTypes))
706                     .findFirst();
707 
708             if (validTestApi.isPresent()) {
709                 if (method.getModifiers().contains(Modifier.PROTECTED)) {
710                     System.out.println(methodSignature + " is protected. Dropping");
711                 } else {
712                     filteredMethods.add(new Api(method, /* isTestApi= */ true));
713                 }
714             } else {
715                 System.out.println("No matching public API for TestApi " + methodSignature);
716             }
717         }
718     }
719 
writeClassToFile(String packageName, TypeSpec clazz)720     private void writeClassToFile(String packageName, TypeSpec clazz) {
721         String qualifiedClassName =
722                 packageName.isEmpty() ? clazz.name : packageName + "." + clazz.name;
723 
724         JavaFile javaFile = JavaFile.builder(packageName, clazz).build();
725         try {
726             JavaFileObject builderFile =
727                     processingEnv.getFiler().createSourceFile(qualifiedClassName);
728             try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {
729                 javaFile.writeTo(out);
730             }
731         } catch (IOException e) {
732             throw new IllegalStateException("Error writing " + qualifiedClassName + " to file", e);
733         }
734     }
735 
getMethods(TypeElement interfaceClass, Elements elements)736     private Set<ExecutableElement> getMethods(TypeElement interfaceClass, Elements elements) {
737         Map<String, ExecutableElement> methods = new HashMap<>();
738         getMethods(methods, interfaceClass, elements);
739         return new HashSet<>(methods.values());
740     }
741 
getMethods(Map<String, ExecutableElement> methods, TypeElement interfaceClass, Elements elements)742     private void getMethods(Map<String, ExecutableElement> methods, TypeElement interfaceClass,
743             Elements elements) {
744 
745         interfaceClass.getEnclosedElements().stream()
746                 .filter(e -> e instanceof ExecutableElement)
747                 .map(e -> (ExecutableElement) e)
748                 .filter(e -> !methods.containsKey(e.getSimpleName().toString()))
749                 .filter(e -> e.getModifiers().contains(Modifier.PUBLIC))
750                 .forEach(e -> methods.put(methodHash(e), e));
751 
752         interfaceClass.getInterfaces().stream()
753                 .map(m -> elements.getTypeElement(m.toString()))
754                 .forEach(m -> getMethods(methods, m, elements));
755 
756 
757         TypeElement superclassElement = (TypeElement) processingEnv.getTypeUtils()
758                 .asElement(interfaceClass.getSuperclass());
759 
760         if (superclassElement != null) {
761             getMethods(methods, superclassElement, elements);
762         }
763     }
764 
methodHash(ExecutableElement method)765     private String methodHash(ExecutableElement method) {
766         return method.getSimpleName() + "(" + method.getParameters().stream()
767                 .map(p -> p.asType().toString()).collect(
768                         Collectors.joining(",")) + ")";
769     }
770     private static class Api {
771         private final ExecutableElement method;
772         private final boolean isTestApi;
773 
Api(ExecutableElement method, boolean isTestApi)774         private Api(ExecutableElement method, boolean isTestApi) {
775             this.method = method;
776             this.isTestApi = isTestApi;
777         }
778 
779         @Override
equals(Object o)780         public boolean equals(Object o) {
781             if (this == o) return true;
782             if (!(o instanceof Api)) return false;
783             Api api = (Api) o;
784             return isTestApi == api.isTestApi && Objects.equals(method, api.method);
785         }
786 
787         @Override
hashCode()788         public int hashCode() {
789             return Objects.hash(method, isTestApi);
790         }
791     }
792 }
793