diff options
author | Jonathan Scott <scottjonathan@google.com> | 2021-04-15 23:52:57 +0100 |
---|---|---|
committer | Jonathan Scott <scottjonathan@google.com> | 2021-04-15 23:53:22 +0100 |
commit | 7451f6c15e755236d0e1aef2e1ae40f01c2ea105 (patch) | |
tree | dd553a86ad2ab309954ebdc46e5592979c734942 | |
parent | 98aadee05251dfff3611cd31000da37d00d943f2 (diff) | |
download | connectedappssdk-7451f6c15e755236d0e1aef2e1ae40f01c2ea105.tar.gz |
Import platform/external/connectedappssdk
Test: N/A importing code - BUILD to follow
Bug: 179354604
Change-Id: I6b9833d1b148f526643e6ba34bd09ac4a17f37cd
357 files changed, 46005 insertions, 0 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d32b285 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,33 @@ +# How to Contribute + +We'd love to accept your patches and contributions to this project. There are +just a few small guidelines you need to follow. + +## Contributor License Agreement + +Contributions to this project must be accompanied by a Contributor License +Agreement (CLA). You (or your employer) retain the copyright to your +contribution; this simply gives us permission to use and redistribute your +contributions as part of the project. Head over to +<https://cla.developers.google.com/> to see your current agreements on file or +to sign a new one. + +You generally only need to submit a CLA once, so if you've already submitted one +(even if it was for a different project), you probably don't need to do it +again. + +## Code Reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more +information on using pull requests. + +It is strongly recommended that you open an issue before beginning work on a +large contribution to get implementation guidance and ensure that the work fits +with the broader project goals. + +## Community Guidelines + +This project follows +[Google's Open Source Community Guidelines](https://opensource.google/conduct/).
\ No newline at end of file @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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.
\ No newline at end of file diff --git a/METADATA b/METADATA new file mode 100644 index 0000000..a31fd47 --- /dev/null +++ b/METADATA @@ -0,0 +1,15 @@ +name: "Connected Apps SDK" + +third_party { + url { + type: HOMEPAGE + value: "https://github.com/google/connectedappssdk" + } + url { + type: GIT + value: "https://github.com/google/connectedappssdk" + } + version: "a78c9c1a60b6c26d896a26dcb19be78feee18c3e" + last_upgrade_date { year: 2021 month: 4 day: 12 } + license_type: NOTICE +}
\ No newline at end of file diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/MODULE_LICENSE_APACHE2 @@ -0,0 +1,2 @@ +scottjonathan@google.com +alexkershaw@google.com
\ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..252b9d6 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +Connected apps is an Android feature that allows your application to utilize +both work and personal data, when given the corresponding permission from the +user. + +The Connected Apps SDK makes it as easy as possible to perform and test +cross-profile behaviour or data access, once you have the appropriate +permission. + +For more information see +https://developers.google.com/android/work/connected-apps
\ No newline at end of file diff --git a/annotations/build.gradle b/annotations/build.gradle new file mode 100644 index 0000000..50b5211 --- /dev/null +++ b/annotations/build.gradle @@ -0,0 +1,29 @@ +plugins { + id 'java-library' + id 'maven-publish' +} + +publishing { + publications { + maven(MavenPublication) { + from components.java + groupId = 'com.google.android.enterprise.connectedapps' + artifactId = 'connectedapps-annotations' + version = project.version + + pom { + licenses { + license { + name = 'Apache 2.0' + url = 'https://opensource.org/licenses/Apache-2.0' + } + } + } + } + } +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} diff --git a/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/AvailabilityRestrictions.java b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/AvailabilityRestrictions.java new file mode 100644 index 0000000..924a6ff --- /dev/null +++ b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/AvailabilityRestrictions.java @@ -0,0 +1,30 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.annotations; + +/** Requirements to connect to a user. */ +public enum AvailabilityRestrictions { + /** Require a user to be unlocked to connect to it. */ + DEFAULT(/* requireUnlocked= */ true), + /** Only require that a user be running and not in quiet mode. */ + DIRECT_BOOT_AWARE(/* requireUnlocked/= */ false); + + public final boolean requireUnlocked; + + AvailabilityRestrictions(boolean requireUnlocked) { + this.requireUnlocked = requireUnlocked; + } +} diff --git a/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossProfile.java b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossProfile.java new file mode 100644 index 0000000..dd88592 --- /dev/null +++ b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossProfile.java @@ -0,0 +1,83 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicate something is accessible from a different profile. + * + * <p>Annotated methods must only return, or take as parameters, types supported by the Profile + * Aware SDK. + * + * <p>Annotated types must be provided by a {@link CrossProfileProvider} class. + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.CLASS) +public @interface CrossProfile { + + /** + * The name of the Profile class generated for this cross-profile type. + * + * <p>This argument can only be passed when annotating types, not methods. + * + * <p>Defaults to this type name prefixed with "Profile". + */ + String profileClassName() default ""; + + /** + * The {@link CustomProfileConnector} used by this type. + * + * <p>Setting this option for a cross-profile type ensures the generated code provides a better + * API surface with more accurate Javadoc and stronger compile-time checking. + * + * <p>This argument can only be passed when annotating types, not methods. + * + * <p>Defaults to undefined, which allows any connector to be used. + */ + Class<?> connector() default CrossProfile.class; + + /** + * Custom parcelable wrappers to be accessible in this cross-profile type. + * + * <p>This argument can only be passed when annotating types, not methods. + */ + Class<?>[] parcelableWrappers() default {}; + + /** + * Custom future wrappers to be accessible in this cross-profile type. + * + * <p>This argument can only be passed when annotating types, not methods. + */ + Class<?>[] futureWrappers() default {}; + + /** + * Can this type contain only static {@link CrossProfile} annotated methods. + * + * <p>This argument can only be passed when annotating types, not methods. + */ + boolean isStatic() default false; + + /** + * The number of milliseconds to wait before timing out asynchronous calls to this method or type. + * + * <p>Defaults to {@link #DEFAULT_TIMEOUT_MILLIS}. + */ + long timeoutMillis() default -1; +} diff --git a/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossProfileCallback.java b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossProfileCallback.java new file mode 100644 index 0000000..38cc9be --- /dev/null +++ b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossProfileCallback.java @@ -0,0 +1,42 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotate an interface as being used for asynchronous cross-profile callbacks. + * + * <p>Interfaces annotated with this must have a single method which returns void, and must only + * take a single parameter of a type supported by the Connected Apps SDK. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.CLASS) +public @interface CrossProfileCallback { + + /** + * If the callback should be enforced to be "simple". + * + * <p>Simple callbacks have a single method which accepts 0 or 1 arguments. + * + * <p>Simple callbacks can be used with calls to multiple profiles and with + * {@code .isAvailable()}, whereas non-simple callbacks can not. + */ + boolean simple() default false; +} diff --git a/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossProfileConfiguration.java b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossProfileConfiguration.java new file mode 100644 index 0000000..d0b9cc8 --- /dev/null +++ b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossProfileConfiguration.java @@ -0,0 +1,60 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Specify configuration for cross-profile calls. + * + * <p>Typically you should have only one {@link CrossProfileConfiguration} annotated class in your + * build. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.CLASS) +public @interface CrossProfileConfiguration { + /** provider classes that should be accessible */ + Class<?>[] providers() default {}; + + /** + * A {@code android.app.Service} subclass which should be used as the superclass of the generated + * service. By default this is {@code android.app.Service}. + */ + Class<?> serviceSuperclass() default CrossProfileConfiguration.class; + // android.app.Service isn't available to the annotations library, so we default to + // CrossProfileConfiguration and swap it to android.app.Service when compiling + + /** + * A {@code android.app.Service} subclass which should be used instead of the generated service. + * + * <p>If not specified, a service will be generated. + * + * <p>The class name must match the {@link CustomProfileConnector#serviceClassName()}. + */ + Class<?> serviceClass() default CrossProfileConfiguration.class; + + /** + * The {@link CustomProfileConnector} used by this configuration. + * + * <p>Defaults to {@code com.google.android.enterprise.connectedapps.CrossProfileConnector}. + */ + Class<?> connector() default CrossProfileConfiguration.class; + // com.google.android.enterprise.connectedapps.CrossProfileConnector isn't available to the + // annotations library, so we default to CrossProfileConfiguration and swap it out when compiling. +} diff --git a/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossProfileConfigurations.java b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossProfileConfigurations.java new file mode 100644 index 0000000..4b28195 --- /dev/null +++ b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossProfileConfigurations.java @@ -0,0 +1,28 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Specify multiple configurations for cross-profile calls. */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.CLASS) +public @interface CrossProfileConfigurations { + CrossProfileConfiguration[] value(); +} diff --git a/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossProfileProvider.java b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossProfileProvider.java new file mode 100644 index 0000000..5139a7a --- /dev/null +++ b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossProfileProvider.java @@ -0,0 +1,38 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotate a method as providing an instance of a {@link CrossProfile} class or (optionally) a + * class which includes such methods. + * + * <p>When applied to a method, the method may optionally take a single {@code Context} parameter. + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.CLASS) +public @interface CrossProfileProvider { + /** + * Cross-profile types which contain only {@code static} cross-profile methods. + * + * <p>This argument can only be passed when annotating types, not methods. + */ + Class<?>[] staticTypes() default {}; +} diff --git a/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossUser.java b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossUser.java new file mode 100644 index 0000000..da95795 --- /dev/null +++ b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossUser.java @@ -0,0 +1,83 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicate something is accessible from a different user. + * + * <p>Annotated methods must only return, or take as parameters, types supported by the Profile + * Aware SDK. + * + * <p>Annotated types must be provided by a {@link CrossUserProvider} class. + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.CLASS) +public @interface CrossUser { + + /** + * The name of the Profile class generated for this cross-profile type. + * + * <p>This argument can only be passed when annotating types, not methods. + * + * <p>Defaults to this type name prefixed with "Profile". + */ + String profileClassName() default ""; + + /** + * The {@link CustomProfileConnector} used by this type. + * + * <p>Setting this option for a cross-profile type ensures the generated code provides a better + * API surface with more accurate Javadoc and stronger compile-time checking. + * + * <p>This argument can only be passed when annotating types, not methods. + * + * <p>Defaults to undefined, which allows any connector to be used. + */ + Class<?> connector() default CrossProfile.class; + + /** + * Custom parcelable wrappers to be accessible in this cross-profile type. + * + * <p>This argument can only be passed when annotating types, not methods. + */ + Class<?>[] parcelableWrappers() default {}; + + /** + * Custom future wrappers to be accessible in this cross-profile type. + * + * <p>This argument can only be passed when annotating types, not methods. + */ + Class<?>[] futureWrappers() default {}; + + /** + * Can this type contain only static {@link CrossUser} annotated methods. + * + * <p>This argument can only be passed when annotating types, not methods. + */ + boolean isStatic() default false; + + /** + * The number of milliseconds to wait before timing out asynchronous calls to this method or type. + * + * <p>Defaults to {@link #DEFAULT_TIMEOUT_MILLIS}. + */ + long timeoutMillis() default -1; +} diff --git a/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossUserCallback.java b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossUserCallback.java new file mode 100644 index 0000000..3ca5bc4 --- /dev/null +++ b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossUserCallback.java @@ -0,0 +1,42 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotate an interface as being used for asynchronous cross-user callbacks. + * + * <p>Interfaces annotated with this must have a single method which returns void, and must only + * take a single parameter of a type supported by the Connected Apps SDK. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.CLASS) +public @interface CrossUserCallback { + + /** + * If the callback should be enforced to be "simple". + * + * <p>Simple callbacks have a single method which accepts 0 or 1 arguments. + * + * <p>Simple callbacks can be used with calls to multiple users and with + * {@code .isAvailable()}, whereas non-simple callbacks can not. + */ + boolean simple() default false; +} diff --git a/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossUserConfiguration.java b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossUserConfiguration.java new file mode 100644 index 0000000..f88b910 --- /dev/null +++ b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossUserConfiguration.java @@ -0,0 +1,61 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Specify configuration for cross-user calls. + * + * <p>Typically you should have only one {@link CrossUserConfiguration} annotated class in your + * build. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.CLASS) +public @interface CrossUserConfiguration { + + /** provider classes that should be accessible */ + Class<?>[] providers() default {}; + + /** + * A {@code android.app.Service} subclass which should be used as the superclass of the generated + * service. By default this is {@code android.app.Service}. + */ + Class<?> serviceSuperclass() default CrossProfileConfiguration.class; + // android.app.Service isn't available to the annotations library, so we default to + // CrossProfileConfiguration and swap it to android.app.Service when compiling + + /** + * A {@code android.app.Service} subclass which should be used instead of the generated service. + * + * <p>If not specified, a service will be generated. + * + * <p>The class name must match the {@link CustomProfileConnector#serviceClassName()}. + */ + Class<?> serviceClass() default CrossProfileConfiguration.class; + + /** + * The {@link CustomProfileConnector} used by this configuration. + * + * <p>Defaults to {@code com.google.android.enterprise.connectedapps.CrossProfileConnector}. + */ + Class<?> connector() default CrossProfileConfiguration.class; + // com.google.android.enterprise.connectedapps.CrossProfileConnector isn't available to the + // annotations library, so we default to CrossProfileConfiguration and swap it out when compiling. +} diff --git a/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossUserConfigurations.java b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossUserConfigurations.java new file mode 100644 index 0000000..5e509ee --- /dev/null +++ b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossUserConfigurations.java @@ -0,0 +1,29 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Specify multiple configurations for cross-user calls. */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.CLASS) +public @interface CrossUserConfigurations { + + CrossUserConfiguration[] value(); +} diff --git a/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossUserProvider.java b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossUserProvider.java new file mode 100644 index 0000000..c31225a --- /dev/null +++ b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossUserProvider.java @@ -0,0 +1,38 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotate a method as providing an instance of a {@link CrossUser} class or (optionally) a class + * which includes such methods. + * + * <p>When applied to a method, the method may optionally take a single {@code Context} parameter. + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.CLASS) +public @interface CrossUserProvider { + /** + * Cross-user types which contain only {@code static} cross-profile methods. + * + * <p>This argument can only be passed when annotating types, not methods. + */ + Class<?>[] staticTypes() default {}; +} diff --git a/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CustomFutureWrapper.java b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CustomFutureWrapper.java new file mode 100644 index 0000000..b0972fc --- /dev/null +++ b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CustomFutureWrapper.java @@ -0,0 +1,32 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A class which follows the future wrapper pattern to allow using a future with the Connected Apps + * SDK + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.CLASS) +public @interface CustomFutureWrapper { + /** The original type which should be wrapped by this wrapper */ + Class<?> originalType(); +} diff --git a/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CustomParcelableWrapper.java b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CustomParcelableWrapper.java new file mode 100644 index 0000000..f733213 --- /dev/null +++ b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CustomParcelableWrapper.java @@ -0,0 +1,32 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A class which follows the parcelable wrapper pattern to allow parcelling an otherwise + * non-parcelable class. + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.CLASS) +public @interface CustomParcelableWrapper { + /** The original type which should be wrapped by this wrapper */ + Class<?> originalType(); +} diff --git a/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CustomProfileConnector.java b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CustomProfileConnector.java new file mode 100644 index 0000000..807bb04 --- /dev/null +++ b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CustomProfileConnector.java @@ -0,0 +1,73 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Annotate the connector which manages connections between processes. */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface CustomProfileConnector { + + /** A type of profile supported by the SDK. */ + enum ProfileType { + UNKNOWN, + NONE, + WORK, + PERSONAL; + } + + /** + * The service that will be generated by the SDK. + * + * <p>If set to empty string, this defaults to the name of the connector suffixed with _Service. + */ + String serviceClassName() default ""; + + /** + * The "primary" profile used by {@code .primary()}, {@code .secondary()}, and {@code + * .suppliers()} calls. + * + * <p>This should typically be the profile which displays a combined experience, if any. + * + * <p>If this is not set, or is set to {@link ProfileType#NONE}, then methods which depend on the + * existence of a primary profile will not be accessible. + */ + ProfileType primaryProfile() default ProfileType.NONE; + + /** Custom parcelable wrappers to be accessible to all users of this connector */ + Class<?>[] parcelableWrappers() default {}; + + /** Custom future wrappers to be accessible to all users of this connector */ + Class<?>[] futureWrappers() default {}; + + /** + * Other {@link CustomProfileConnector} annotated types which we can import configuration from. + * + * <p>This will import {@link #parcelableWrappers()} and {@link #futureWrappers()}. + */ + Class<?>[] imports() default {}; + + /** + * Which set of restrictions should be applied to checking availability. + * + * <p>By default, this will require that a user be running, unlocked, and not in quiet mode. + */ + AvailabilityRestrictions availabilityRestrictions() default AvailabilityRestrictions.DEFAULT; +} diff --git a/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CustomUserConnector.java b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CustomUserConnector.java new file mode 100644 index 0000000..0d01df6 --- /dev/null +++ b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CustomUserConnector.java @@ -0,0 +1,54 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Annotate the connector which manages connections between processes. */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface CustomUserConnector { + + /** + * The service that will be generated by the SDK. + * + * <p>If set to empty string, this defaults to the name of the connector suffixed with _Service. + */ + String serviceClassName() default ""; + + /** Custom parcelable wrappers to be accessible to all users of this connector */ + Class<?>[] parcelableWrappers() default {}; + + /** Custom future wrappers to be accessible to all users of this connector */ + Class<?>[] futureWrappers() default {}; + + /** + * Other {@link CustomUserConnector} annotated types which we can import configuration from. + * + * <p>This will import {@link #parcelableWrappers()} and {@link #futureWrappers()}. + */ + Class<?>[] imports() default {}; + + /** + * Which set of restrictions should be applied to checking availability. + * + * <p>By default, this will require that a user be running, unlocked, and not in quiet mode. + */ + AvailabilityRestrictions availabilityRestrictions() default AvailabilityRestrictions.DEFAULT; +} diff --git a/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/GeneratedProfileConnector.java b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/GeneratedProfileConnector.java new file mode 100644 index 0000000..3995dac --- /dev/null +++ b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/GeneratedProfileConnector.java @@ -0,0 +1,29 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Causes the generation of a class with the same name prefixed with "Generated" which implements + * {@code ProfileConnector}. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface GeneratedProfileConnector {} diff --git a/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/GeneratedUserConnector.java b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/GeneratedUserConnector.java new file mode 100644 index 0000000..c8bfece --- /dev/null +++ b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/GeneratedUserConnector.java @@ -0,0 +1,29 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Causes the generation of a class with the same name prefixed with "Generated" which implements + * {@code UserConnector}. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface GeneratedUserConnector {} diff --git a/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/ParcelableWrapper.java b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/ParcelableWrapper.java new file mode 100644 index 0000000..629efaf --- /dev/null +++ b/annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/ParcelableWrapper.java @@ -0,0 +1,32 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A class which follows the parcelable wrapper pattern to allow parcelling an otherwise + * non-parcelable class. + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.CLASS) +public @interface ParcelableWrapper { + /** The original type which should be wrapped by this wrapper */ + Class<?> originalType(); +} diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..8e106ac --- /dev/null +++ b/build.gradle @@ -0,0 +1,27 @@ +buildscript { + ext.deps = [ + androidGradlePlugin: "com.android.tools.build:gradle:4.1.0", + checkerFramework: "org.checkerframework:checker-qual:3.9.1", + androidxTest: "androidx.test:core:1.3.0", + autovalue: "com.google.auto.value:auto-value:1.6.3", + autovalueAnnotations: "com.google.auto.value:auto-value-annotations:1.6.3", + autoservice: "com.google.auto.service:auto-service:1.0-rc6", + autoserviceAnnotations: "com.google.auto.service:auto-service-annotations:1.0-rc6", + javapoet: "com.squareup:javapoet:1.13.0", + guava: "com.google.guava:guava:29.0-jre" + ] + repositories { + jcenter() + google() + } + dependencies { + classpath deps.androidGradlePlugin + } +} + +allprojects { + repositories { + google() + jcenter() + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..2b34218 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,3 @@ +android.useAndroidX=true +version = 1.0.0-alpha04 +org.gradle.jvmargs=-Xmx4g
\ No newline at end of file diff --git a/processor/build.gradle b/processor/build.gradle new file mode 100644 index 0000000..e911f28 --- /dev/null +++ b/processor/build.gradle @@ -0,0 +1,41 @@ +plugins { + id 'java-library' + id 'maven-publish' +} + +dependencies { + compileOnly deps.autovalueAnnotations + compileOnly deps.autoserviceAnnotations + implementation deps.javapoet + implementation deps.guava + implementation project(path: ':connectedapps-annotations') + implementation project(path: ':connectedapps-testing-annotations') + + annotationProcessor deps.autovalue + annotationProcessor deps.autoservice +} + +publishing { + publications { + maven(MavenPublication) { + from components.java + groupId = 'com.google.android.enterprise.connectedapps' + artifactId = 'connectedapps-processor' + version = project.version + + pom { + licenses { + license { + name = 'Apache 2.0' + url = 'https://opensource.org/licenses/Apache-2.0' + } + } + } + } + } +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/AlwaysThrowsGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/AlwaysThrowsGenerator.java new file mode 100644 index 0000000..05a506e --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/AlwaysThrowsGenerator.java @@ -0,0 +1,221 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.EXCEPTION_CALLBACK_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo; +import com.google.android.enterprise.connectedapps.processor.containers.FutureWrapper; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.squareup.javapoet.AnnotationSpec; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeSpec; +import javax.lang.model.element.Modifier; +import javax.lang.model.type.TypeMirror; + +/** + * Generate the {@code Profile_*_AlwaysThrows} class for a single cross-profile type. + * + * <p>This class is used when running on Pre-O devices to shortcut any cross-profile code and just + * throw an {@code UnavailableProfileException}. + * + * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to + * validate that the annotated code is correct. + */ +final class AlwaysThrowsGenerator { + + private boolean generated = false; + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final CrossProfileTypeInfo crossProfileType; + + AlwaysThrowsGenerator(GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.crossProfileType = checkNotNull(crossProfileType); + } + + void generate() { + if (generated) { + throw new IllegalStateException("AlwaysThrowsGenerator#generate can only be called once"); + } + generated = true; + + generateAlwaysThrowsClass(); + } + + private void generateAlwaysThrowsClass() { + ClassName className = getAlwaysThrowsClassName(generatorContext, crossProfileType); + + ClassName singleSenderCanThrowInterface = + InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName( + generatorContext, crossProfileType); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addJavadoc( + "Implementation of {@link $T} which throws an {@link $T} for every call.\n", + singleSenderCanThrowInterface, + UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addSuperinterface(singleSenderCanThrowInterface); + + classBuilder.addField(String.class, "errorMessage", Modifier.PRIVATE, Modifier.FINAL); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(String.class, "errorMessage") + .beginControlFlow("if (errorMessage == null)") + .addStatement("throw new $T()", NullPointerException.class) + .endControlFlow() + .addStatement("this.errorMessage = errorMessage") + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("timeout") + .addAnnotation(Override.class) + .addAnnotation( + AnnotationSpec.builder(SuppressWarnings.class) + .addMember("value", "$S", "GoodTime") + .build()) + .addModifiers(Modifier.PUBLIC) + .returns(className) + .addParameter(long.class, "timeout") + .addStatement("return this") + .build()); + + ClassName ifAvailableClass = + IfAvailableGenerator.getIfAvailableClassName(generatorContext, crossProfileType); + + classBuilder.addMethod( + MethodSpec.methodBuilder("ifAvailable") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(ifAvailableClass) + .addStatement("return new $T(this)", ifAvailableClass) + .build()); + + for (CrossProfileMethodInfo method : crossProfileType.crossProfileMethods()) { + if (method.isBlocking(generatorContext, crossProfileType)) { + generateBlockingMethodOnAlwaysThrowsClass(classBuilder, method, crossProfileType); + } else if (method.isCrossProfileCallback(generatorContext)) { + generateCrossProfileCallbackMethodOnAlwaysThrowsClass( + classBuilder, method, crossProfileType); + } else if (method.isFuture(crossProfileType)) { + generateFutureMethodOnAlwaysThrowsClass(classBuilder, method, crossProfileType); + } else { + throw new IllegalStateException("Unknown method type: " + method); + } + } + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private void generateBlockingMethodOnAlwaysThrowsClass( + TypeSpec.Builder classBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder(method.simpleName()) + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .addException(UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME) + .returns(method.returnTypeTypeName()) + .addParameters( + GeneratorUtilities.extractParametersFromMethod( + crossProfileType.supportedTypes(), + method.methodElement(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)) + .addStatement("throw new $T(errorMessage)", UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME); + + classBuilder.addMethod(methodBuilder.build()); + } + + private static void generateCrossProfileCallbackMethodOnAlwaysThrowsClass( + TypeSpec.Builder classBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder(method.simpleName()) + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(method.returnTypeTypeName()) + .addParameters( + GeneratorUtilities.extractParametersFromMethod( + crossProfileType.supportedTypes(), + method.methodElement(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)) + .addParameter(EXCEPTION_CALLBACK_CLASSNAME, "exceptionCallback") + .addStatement( + "exceptionCallback.onException(new $T(errorMessage))", + UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME); + + classBuilder.addMethod(methodBuilder.build()); + } + + private void generateFutureMethodOnAlwaysThrowsClass( + TypeSpec.Builder classBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + + TypeMirror rawFutureType = TypeUtils.removeTypeArguments(method.returnType()); + + FutureWrapper futureWrapper = + crossProfileType.supportedTypes().getType(rawFutureType).getFutureWrapper().get(); + + // This assumes futures are only generic on one argument, which is enforced + TypeMirror wrappedType = TypeUtils.extractTypeArguments(method.returnType()).get(0); + ParameterizedTypeName futureWrapperType = + ParameterizedTypeName.get(futureWrapper.wrapperClassName(), ClassName.get(wrappedType)); + + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder(method.simpleName()) + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(method.returnTypeTypeName()) + .addParameters( + GeneratorUtilities.extractParametersFromMethod( + crossProfileType.supportedTypes(), + method.methodElement(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)) + .addStatement( + "$1T failedFuture = $2T.create(new $3T(), $4L)", + futureWrapperType, + futureWrapper.wrapperClassName(), + BundlerGenerator.getBundlerClassName(generatorContext, crossProfileType), + TypeUtils.generateBundlerType(wrappedType)) + .addStatement( + "failedFuture.onException(new $T(errorMessage))", + UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME) + .addStatement("return failedFuture.getFuture()"); + + classBuilder.addMethod(methodBuilder.build()); + } + + static ClassName getAlwaysThrowsClassName( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + return GeneratorUtilities.appendToClassName( + crossProfileType.profileClassName(), "_AlwaysThrows"); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/BundlerGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/BundlerGenerator.java new file mode 100644 index 0000000..8d421a3 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/BundlerGenerator.java @@ -0,0 +1,270 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.BUNDLER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.BUNDLER_TYPE_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_CLASSNAME; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.stream.Collectors.toList; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.google.android.enterprise.connectedapps.processor.containers.Type; +import com.squareup.javapoet.AnnotationSpec; +import com.squareup.javapoet.ArrayTypeName; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeSpec; +import java.util.List; +import javax.lang.model.element.Modifier; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeMirror; + +/** + * Generate the {@code *_Bundler} class for a single {@link CrossProfileConfiguration} annotated + * method. + * + * <p>This class is responsible for reading and writing {@code Bundle} and {@code Parcel} instances. + * + * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to + * validate that the annotated code is correct. + */ +final class BundlerGenerator { + + private boolean generated = false; + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final CrossProfileTypeInfo crossProfileType; + + BundlerGenerator(GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.crossProfileType = checkNotNull(crossProfileType); + } + + void generate() { + if (generated) { + throw new IllegalStateException("BundlerGenerator#generate can only be called once"); + } + generated = true; + + generateBundlerClass(); + } + + private void generateBundlerClass() { + ClassName className = getBundlerClassName(generatorContext, crossProfileType); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addJavadoc( + "Implementation of {@link $T} for use with {@link $T}.\n", + BUNDLER_CLASSNAME, + crossProfileType.className()) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addSuperinterface(BUNDLER_CLASSNAME); + + classBuilder.addMethod(MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC).build()); + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PRIVATE) + .addParameter(PARCEL_CLASSNAME, "in") + .build()); + + makeParcelable(classBuilder, className); + addWriteToParcelMethod(classBuilder); + addReadFromParcelMethod(classBuilder); + addCreateArrayMethod(classBuilder); + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private void makeParcelable(TypeSpec.Builder classBuilder, ClassName bundlerClassName) { + classBuilder.addMethod( + MethodSpec.methodBuilder("writeToParcel") + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .addParameter(PARCEL_CLASSNAME, "dest") + .addParameter(int.class, "flags") + .build()); + + generatorUtilities.addDefaultParcelableMethods(classBuilder, bundlerClassName); + } + + + private void addWriteToParcelMethod(TypeSpec.Builder classBuilder) { + CodeBlock.Builder methodCode = CodeBlock.builder(); + + List<Type> types = + crossProfileType.supportedTypes().usableTypes().stream() + .filter(Type::canBeBundled) + .filter(t -> !t.isPrimitive()) + .collect(toList()); + addWriteToParcelTypes(methodCode, types); + + classBuilder.addMethod( + MethodSpec.methodBuilder("writeToParcel") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + // This is for passing rawtypes into the Parcelable*.of() methods + .addAnnotation( + AnnotationSpec.builder(SuppressWarnings.class) + .addMember("value", "\"unchecked\"") + .build()) + .addParameter(PARCEL_CLASSNAME, "parcel") + .addParameter(Object.class, "value") + .addParameter(BUNDLER_TYPE_CLASSNAME, "valueType") + .addParameter(int.class, "flags") + .addCode(methodCode.build()) + .build()); + } + + private void addWriteToParcelTypes(CodeBlock.Builder codeBuilder, List<Type> types) { + codeBuilder.beginControlFlow( + "if ($S.equals(valueType.rawTypeQualifiedName()))", "java.lang.Void"); + codeBuilder.addStatement("return"); + for (Type type : types) { + codeBuilder.nextControlFlow( + "else if ($S.equals(valueType.rawTypeQualifiedName()))", + TypeUtils.getRawTypeQualifiedName(type.getTypeMirror())); + addWriteToParcelType(codeBuilder, type); + } + codeBuilder.endControlFlow(); + + codeBuilder.addStatement( + "throw new $T(\"Type \" + valueType.rawTypeQualifiedName() + \" cannot be written to" + + " Parcel\")", + IllegalArgumentException.class); + } + + private void addWriteToParcelType(CodeBlock.Builder codeBuilder, Type type) { + CodeBlock convertedValue = + CodeBlock.of("($L) value", TypeUtils.getRawTypeQualifiedName(type.getTypeMirror())); + codeBuilder.addStatement( + crossProfileType + .supportedTypes() + .generateWriteToParcelCode("parcel", type, convertedValue.toString())); + codeBuilder.addStatement("return"); + } + + private void addReadFromParcelMethod(TypeSpec.Builder classBuilder) { + CodeBlock.Builder methodCode = CodeBlock.builder(); + + List<Type> types = + crossProfileType.supportedTypes().usableTypes().stream() + .filter(Type::canBeBundled) + .collect(toList()); + addReadFromParcelTypes(methodCode, types); + + methodCode.addStatement( + "throw new $T(\"Type \" + valueType.rawTypeQualifiedName() + \" cannot be read from" + + " Parcel\")", + IllegalArgumentException.class); + + classBuilder.addMethod( + MethodSpec.methodBuilder("readFromParcel") + .addAnnotation( + AnnotationSpec.builder(SuppressWarnings.class) + .addMember("value", "\"unchecked\"") + .build()) + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(Object.class) + .addParameter(PARCEL_CLASSNAME, "parcel") + .addParameter(BUNDLER_TYPE_CLASSNAME, "valueType") + .addCode(methodCode.build()) + .build()); + } + + private void addReadFromParcelTypes(CodeBlock.Builder codeBuilder, List<Type> types) { + codeBuilder.beginControlFlow( + "if ($S.equals(valueType.rawTypeQualifiedName()))", "java.lang.Void"); + codeBuilder.addStatement("return null"); + for (Type type : types) { + codeBuilder.nextControlFlow( + "else if ($S.equals(valueType.rawTypeQualifiedName()))", + TypeUtils.getRawTypeQualifiedName(type.getTypeMirror())); + addReadFromParcelType(codeBuilder, type); + } + codeBuilder.endControlFlow(); + } + + private void addReadFromParcelType(CodeBlock.Builder codeBuilder, Type type) { + TypeMirror objectType = type.getTypeMirror(); + if (objectType.getKind().isPrimitive()) { + PrimitiveType primitiveType = (PrimitiveType) objectType; + objectType = generatorContext.types().boxedClass(primitiveType).asType(); + } + + codeBuilder.addStatement( + "return ($L) $L", + TypeUtils.getRawTypeQualifiedName(objectType), + crossProfileType.supportedTypes().generateReadFromParcelCode("parcel", type)); + } + + private void addCreateArrayMethod(TypeSpec.Builder classBuilder) { + CodeBlock.Builder methodCode = CodeBlock.builder(); + + List<Type> types = + crossProfileType.supportedTypes().usableTypes().stream() + .filter(Type::canBeBundled) + .filter(t -> !t.isGeneric()) + .filter( + t -> + !t.isPrimitive()) // We can't return a primitive array with return type Object[] + .filter(t -> !t.isArray()) // We don't support multidimensional arrays + .collect(toList()); + addCreateArrayTypes(methodCode, types); + + methodCode.addStatement( + "throw new $T(\"Cannot create array of type \" + valueType.rawTypeQualifiedName())", + IllegalArgumentException.class); + + classBuilder.addMethod( + MethodSpec.methodBuilder("createArray") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(ArrayTypeName.of(Object.class)) + .addParameter(BUNDLER_TYPE_CLASSNAME, "valueType") + .addParameter(int.class, "size") + .addCode(methodCode.build()) + .build()); + } + + private void addCreateArrayTypes(CodeBlock.Builder codeBuilder, List<Type> types) { + codeBuilder.beginControlFlow( + "if ($S.equals(valueType.rawTypeQualifiedName()))", "java.lang.Void"); + codeBuilder.addStatement("return new Void[size]"); + for (Type type : types) { + codeBuilder.nextControlFlow( + "else if ($S.equals(valueType.rawTypeQualifiedName()))", + TypeUtils.getRawTypeQualifiedName(type.getTypeMirror())); + addCreateArrayType(codeBuilder, type); + } + codeBuilder.endControlFlow(); + } + + private void addCreateArrayType(CodeBlock.Builder codeBuilder, Type type) { + codeBuilder.addStatement("return new $T[size]", type.getTypeMirror()); + } + + static ClassName getBundlerClassName( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + return GeneratorUtilities.appendToClassName(crossProfileType.profileClassName(), "_Bundler"); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CodeGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CodeGenerator.java new file mode 100644 index 0000000..7fcf28a --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CodeGenerator.java @@ -0,0 +1,80 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackInterfaceInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.google.android.enterprise.connectedapps.processor.containers.ProfileConnectorInfo; +import com.google.android.enterprise.connectedapps.processor.containers.ProviderClassInfo; +import com.google.android.enterprise.connectedapps.processor.containers.UserConnectorInfo; + +/** + * Generator of code for connected apps. + * + * <p>This is intended to be initialised and used once, which will generate all needed code. + * + * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to + * validate that the annotated code is correct. + */ +final class CodeGenerator { + private boolean generated = false; + private final GeneratorContext generatorContext; + private final ParcelableWrappersGenerator parcelableWrappersGenerator; + private final FutureWrappersGenerator futureWrappersGenerator; + private final TestCodeGenerator testCodeGenerator; + + CodeGenerator(GeneratorContext generatorContext) { + this.generatorContext = checkNotNull(generatorContext); + this.parcelableWrappersGenerator = new ParcelableWrappersGenerator(generatorContext); + this.futureWrappersGenerator = new FutureWrappersGenerator(generatorContext); + this.testCodeGenerator = new TestCodeGenerator(generatorContext); + } + + void generate() { + if (generated) { + throw new IllegalStateException("CodeGenerator#generate can only be called once"); + } + generated = true; + + parcelableWrappersGenerator.generate(); + futureWrappersGenerator.generate(); + testCodeGenerator.generate(); + + for (ProfileConnectorInfo connector : generatorContext.generatedProfileConnectors()) { + new ProfileConnectorCodeGenerator(generatorContext, connector).generate(); + } + + for (UserConnectorInfo connector : generatorContext.generatedUserConnectors()) { + new UserConnectorCodeGenerator(generatorContext, connector).generate(); + } + + for (CrossProfileConfigurationInfo configuration : generatorContext.configurations()) { + new ConfigurationCodeGenerator(generatorContext, configuration).generate(); + } + + for (ProviderClassInfo providerClass : generatorContext.providers()) { + new ProviderClassCodeGenerator(generatorContext, providerClass).generate(); + } + + for (CrossProfileCallbackInterfaceInfo callbackInterface : + generatorContext.crossProfileCallbackInterfaces()) { + new CrossProfileCallbackCodeGenerator(generatorContext, callbackInterface).generate(); + } + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CommonClassNames.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CommonClassNames.java new file mode 100644 index 0000000..cebeebc --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CommonClassNames.java @@ -0,0 +1,124 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import com.squareup.javapoet.ClassName; + +/** + * {@link ClassName} instances shared across the processor. + * + * <p>This is required as most classes are not available to the processor so need to be referenced + * through {@link ClassName} + */ +public class CommonClassNames { + static final ClassName CONTEXT_CLASSNAME = ClassName.get("android.content", "Context"); + static final ClassName PARCEL_CLASSNAME = ClassName.get("android.os", "Parcel"); + static final ClassName PARCELABLE_CLASSNAME = ClassName.get("android.os", "Parcelable"); + static final ClassName CROSS_PROFILE_FUTURE_RESULT_WRITER = + ClassName.get( + "com.google.android.enterprise.connectedapps.internal", "CrossProfileFutureResultWriter"); + static final ClassName IF_AVAILABLE_FUTURE_RESULT_WRITER = + ClassName.get( + "com.google.android.enterprise.connectedapps.internal", "IfAvailableFutureResultWriter"); + static final ClassName PARCELABLE_CREATOR_CLASSNAME = + ClassName.get("android.os.Parcelable", "Creator"); + static final ClassName UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME = + ClassName.get( + "com.google.android.enterprise.connectedapps.exceptions", "UnavailableProfileException"); + static final ClassName AVAILABILITY_RESTRICTIONS_CLASSNAME = + ClassName.get( + "com.google.android.enterprise.connectedapps.annotations", "AvailabilityRestrictions"); + static final ClassName PROFILE_RUNTIME_EXCEPTION_CLASSNAME = + ClassName.get( + "com.google.android.enterprise.connectedapps.exceptions", "ProfileRuntimeException"); + static final ClassName PROFILE_AWARE_UTILS_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps", "ConnectedAppsUtils"); + static final ClassName BACKGROUND_EXCEPTION_THROWER_CLASSNAME = + ClassName.get( + "com.google.android.enterprise.connectedapps.internal", "BackgroundExceptionThrower"); + static final ClassName PARCEL_UTILITIES_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps.internal", "ParcelUtilities"); + static final ClassName METHOD_RUNNER_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps.internal", "MethodRunner"); + static final ClassName BUNDLER_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps.internal", "Bundler"); + static final ClassName BUNDLER_TYPE_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps.internal", "BundlerType"); + static final ClassName PARCEL_CALL_RECEIVER_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps.internal", "ParcelCallReceiver"); + public static final ClassName BINDER_CLASSNAME = ClassName.get("android.os", "Binder"); + public static final ClassName INTENT_CLASSNAME = ClassName.get("android.content", "Intent"); + static final ClassName CROSS_PROFILE_SENDER_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps", "CrossProfileSender"); + public static final ClassName CROSSPROFILESERVICE_STUB_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps.ICrossProfileService", "Stub"); + static final ClassName INVALID_PROTOCOL_BUFFER_EXCEPTION_CLASSNAME = + ClassName.get("com.google.protobuf", "InvalidProtocolBufferException"); + static final ClassName PROFILE_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps", "Profile"); + static final ClassName LOCAL_CALLBACK_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps", "LocalCallback"); + public static final ClassName CROSS_PROFILE_CALLBACK_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps", "ICrossProfileCallback"); + static final ClassName ASYNC_CALLBACK_PARAM_MULTIMERGER_CLASSNAME = + ClassName.get( + "com.google.android.enterprise.connectedapps.internal", + "CrossProfileCallbackMultiMerger"); + static final ClassName CROSS_PROFILE_CALLBACK_PARCEL_CALL_SENDER_CLASSNAME = + ClassName.get( + "com.google.android.enterprise.connectedapps.internal", + "CrossProfileCallbackParcelCallSender"); + static final ClassName CROSS_PROFILE_CALLBACK_EXCEPTION_PARCEL_CALL_SENDER_CLASSNAME = + ClassName.get( + "com.google.android.enterprise.connectedapps.internal", + "CrossProfileCallbackExceptionParcelCallSender"); + static final ClassName ASYNC_CALLBACK_PARAM_MULTIMERGER_COMPLETE_LISTENER_CLASSNAME = + ClassName.get( + "com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger", + "CrossProfileCallbackMultiMergerCompleteListener"); + + public static final ClassName SERVICE_CLASSNAME = ClassName.get("android.app", "Service"); + public static final ClassName EXCEPTION_CALLBACK_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps", "ExceptionCallback"); + public static final ClassName CALLBACK_MERGER_EXCEPTION_CALLBACK_CLASSNAME = + ClassName.get( + "com.google.android.enterprise.connectedapps.internal", + "CallbackMergerExceptionCallback"); + public static final ClassName PROFILE_CONNECTOR_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps", "ProfileConnector"); + public static final ClassName ABSTRACT_PROFILE_CONNECTOR_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps", "AbstractProfileConnector"); + public static final ClassName ABSTRACT_USER_CONNECTOR_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps", "AbstractUserConnector"); + public static final ClassName ABSTRACT_PROFILE_CONNECTOR_BUILDER_CLASSNAME = + ClassName.get( + "com.google.android.enterprise.connectedapps.AbstractProfileConnector", "Builder"); + public static final ClassName ABSTRACT_USER_CONNECTOR_BUILDER_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps.AbstractUserConnector", "Builder"); + public static final ClassName CONNECTION_BINDER_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps", "ConnectionBinder"); + public static final ClassName SCHEDULED_EXECUTOR_SERVICE_CLASSNAME = + ClassName.get("java.util.concurrent", "ScheduledExecutorService"); + public static final ClassName ABSTRACT_FAKE_PROFILE_CONNECTOR_CLASSNAME = + ClassName.get( + "com.google.android.enterprise.connectedapps.testing", "AbstractFakeProfileConnector"); + + public static final ClassName VERSION_CLASSNAME = ClassName.get("android.os.Build", "VERSION"); + public static final ClassName VERSION_CODES_CLASSNAME = + ClassName.get("android.os.Build", "VERSION_CODES"); + + private CommonClassNames() {} +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ConfigurationCodeGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ConfigurationCodeGenerator.java new file mode 100644 index 0000000..a4647dd --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ConfigurationCodeGenerator.java @@ -0,0 +1,59 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfile; +import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; + +/** + * Generator of code for a single {@link CrossProfileConfiguration}. + * + * <p>The {@code Service} will only be generated if the configuration contains at least one provider + * class which has at least one {@link CrossProfile} type. + */ +class ConfigurationCodeGenerator { + private boolean generated = false; + private final CrossProfileConfigurationInfo configuration; + private final ServiceGenerator serviceGenerator; + private final DispatcherGenerator dispatcherGenerator; + + ConfigurationCodeGenerator( + GeneratorContext generatorContext, CrossProfileConfigurationInfo configuration) { + this.configuration = checkNotNull(configuration); + this.serviceGenerator = new ServiceGenerator(checkNotNull(generatorContext), configuration); + this.dispatcherGenerator = new DispatcherGenerator(generatorContext, configuration); + } + + void generate() { + if (generated) { + throw new IllegalStateException( + "ConfigurationCodeGenerator#generate can only be called once"); + } + generated = true; + + if (configuration.profileConnector() == null) { + // Without a connector we can't line things up so don't generate + return; + } + + serviceGenerator.generate(); + dispatcherGenerator.generate(); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileCallbackCodeGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileCallbackCodeGenerator.java new file mode 100644 index 0000000..2822bf2 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileCallbackCodeGenerator.java @@ -0,0 +1,501 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.ASYNC_CALLBACK_PARAM_MULTIMERGER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.ASYNC_CALLBACK_PARAM_MULTIMERGER_COMPLETE_LISTENER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.BUNDLER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CROSS_PROFILE_CALLBACK_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CROSS_PROFILE_CALLBACK_EXCEPTION_PARCEL_CALL_SENDER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CROSS_PROFILE_CALLBACK_PARCEL_CALL_SENDER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.EXCEPTION_CALLBACK_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.LOCAL_CALLBACK_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_UTILITIES_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.stream.Collectors.joining; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileCallback; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackInterfaceInfo; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.squareup.javapoet.AnnotationSpec; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; +import java.util.Map; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; + +/** Generator of code for a single {@link CrossProfileCallback}. */ +public class CrossProfileCallbackCodeGenerator { + private boolean generated = false; + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final CrossProfileCallbackInterfaceInfo callbackInterface; + + private final TypeMirror voidTypeMirror; + + CrossProfileCallbackCodeGenerator( + GeneratorContext generatorContext, CrossProfileCallbackInterfaceInfo callbackInterface) { + this.generatorContext = checkNotNull(generatorContext); + this.callbackInterface = checkNotNull(callbackInterface); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + + voidTypeMirror = generatorContext.elements().getTypeElement("java.lang.Void").asType(); + } + + void generate() { + if (generated) { + throw new IllegalStateException( + "CrossProfileCallbackCodeGenerator#generate can only be called once"); + } + generated = true; + + generateReceiverClass(); + generateSenderClass(); + + if (callbackInterface.isSimple()) { + // There can be only one method + ExecutableElement method = callbackInterface.methods().get(0); + generateMultiInterface(method); + generateMultiMergerResultClass(method); + generateMultiMergerInputClass(method); + } + } + + private void generateMultiInterface(ExecutableElement method) { + ClassName interfaceName = + getCrossProfileCallbackMultiInterfaceClassName(generatorContext, callbackInterface); + + TypeSpec.Builder interfaceBuilder = + TypeSpec.interfaceBuilder(interfaceName) + .addJavadoc( + "Callback interface used when using a {@link $T} with multiple profiles.\n", + callbackInterface.interfaceElement()) + .addModifiers(Modifier.PUBLIC); + + addMultiMethod(interfaceBuilder, method); + + generatorUtilities.writeClassToFile(interfaceName.packageName(), interfaceBuilder); + } + + private void generateMultiMergerResultClass(ExecutableElement method) { + ClassName className = + getCrossProfileCallbackMultiMergerResultClassName(generatorContext, callbackInterface); + + TypeMirror paramType = + method.getParameters().isEmpty() ? voidTypeMirror : method.getParameters().get(0).asType(); + + TypeName mergerInterface = + ParameterizedTypeName.get( + ASYNC_CALLBACK_PARAM_MULTIMERGER_COMPLETE_LISTENER_CLASSNAME, + ClassName.get(generatorUtilities.boxIfNecessary(paramType))); + + ParameterizedTypeName multiParameterType = getMultiParameterType(paramType); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addJavadoc( + "Implementation of {@link $T} which forwards completed results to an instance of" + + " {@link $T}.\n", + ASYNC_CALLBACK_PARAM_MULTIMERGER_COMPLETE_LISTENER_CLASSNAME, + callbackInterface.interfaceElement()) + .addSuperinterface(mergerInterface) + .addModifiers(Modifier.PUBLIC); + + classBuilder.addField( + FieldSpec.builder( + getCrossProfileCallbackMultiInterfaceClassName(generatorContext, callbackInterface), + "callback") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .build()); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter( + getCrossProfileCallbackMultiInterfaceClassName(generatorContext, callbackInterface), + "callback") + .addStatement("this.callback = callback") + .build()); + + String resultToPass = method.getParameters().isEmpty() ? "" : "results"; + + classBuilder.addMethod( + MethodSpec.methodBuilder("onResult") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .addParameter(multiParameterType, "results") + .addStatement("callback.$L($L)", method.getSimpleName(), resultToPass) + .build()); + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private void generateMultiMergerInputClass(ExecutableElement method) { + ClassName className = + getCrossProfileCallbackMultiMergerInputClassName(generatorContext, callbackInterface); + + TypeMirror paramType = + method.getParameters().isEmpty() ? voidTypeMirror : method.getParameters().get(0).asType(); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addJavadoc( + "Implementation of {@link $T} which passes results into an instance of {@link" + + " $T}.\n", + callbackInterface.interfaceElement(), + ASYNC_CALLBACK_PARAM_MULTIMERGER_CLASSNAME) + .addSuperinterface(ClassName.get(callbackInterface.interfaceElement())) + .addModifiers(Modifier.PUBLIC); + + classBuilder.addField( + FieldSpec.builder(PROFILE_CLASSNAME, "profileId") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .build()); + + classBuilder.addField( + FieldSpec.builder( + ParameterizedTypeName.get( + ASYNC_CALLBACK_PARAM_MULTIMERGER_CLASSNAME, + ClassName.get(generatorUtilities.boxIfNecessary(paramType))), + "callback") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .build()); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(PROFILE_CLASSNAME, "profileId") + .addParameter( + ParameterizedTypeName.get( + ASYNC_CALLBACK_PARAM_MULTIMERGER_CLASSNAME, + ClassName.get(generatorUtilities.boxIfNecessary(paramType))), + "callback") + .addStatement("this.profileId = profileId") + .addStatement("this.callback = callback") + .build()); + + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder(method.getSimpleName().toString()) + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC); + + if (!method.getParameters().isEmpty()) { + String paramName = method.getParameters().get(0).getSimpleName().toString(); + methodBuilder.addParameter(ClassName.get(paramType), paramName); + methodBuilder.addStatement("callback.onResult(profileId, $L)", paramName); + } else { + methodBuilder.addStatement("callback.onResult(profileId, null)"); + } + + classBuilder.addMethod(methodBuilder.build()); + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private void addMultiMethod(TypeSpec.Builder interfaceBuilder, ExecutableElement method) { + if (method.getParameters().isEmpty()) { + interfaceBuilder.addMethod( + MethodSpec.methodBuilder(method.getSimpleName().toString()) + .addModifiers(method.getModifiers()) + .build()); + + return; + } + + // There can be only one parameter + VariableElement param = method.getParameters().get(0); + ParameterizedTypeName paramType = getMultiParameterType(param.asType()); + + interfaceBuilder.addMethod( + MethodSpec.methodBuilder(method.getSimpleName().toString()) + .addModifiers(method.getModifiers()) + .addParameter(paramType, param.getSimpleName().toString()) + .build()); + } + + private ParameterizedTypeName getMultiParameterType(TypeMirror paramType) { + TypeName boxedParamType = TypeName.get(generatorUtilities.boxIfNecessary(paramType)); + return ParameterizedTypeName.get(ClassName.get(Map.class), PROFILE_CLASSNAME, boxedParamType); + } + + private void generateReceiverClass() { + ClassName className = + getCrossProfileCallbackReceiverClassName(generatorContext, callbackInterface); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addSuperinterface(ClassName.get(callbackInterface.interfaceElement())) + .addJavadoc( + "Implementation of {@link $1T} which wraps an {@link $2T},\n" + + "writing the callback value to a {@link $3T} and passing it to the {@link" + + " $2T}.\n", + callbackInterface.interfaceElement(), + CROSS_PROFILE_CALLBACK_CLASSNAME, + PARCEL_CLASSNAME); + + classBuilder.addField( + FieldSpec.builder(CROSS_PROFILE_CALLBACK_CLASSNAME, "callback") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .build()); + + classBuilder.addField( + FieldSpec.builder(BUNDLER_CLASSNAME, "bundler") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .build()); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(CROSS_PROFILE_CALLBACK_CLASSNAME, "callback") + .addParameter(BUNDLER_CLASSNAME, "bundler") + .beginControlFlow("if (callback == null || bundler == null)") + .addStatement("throw new $T()", NullPointerException.class) + .endControlFlow() + .addStatement("this.callback = callback") + .addStatement("this.bundler = bundler") + .build()); + + for (ExecutableElement method : callbackInterface.methods()) { + addReceiverMethod(classBuilder, callbackInterface, method); + } + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private static void addReceiverMethod( + TypeSpec.Builder classBuilder, + CrossProfileCallbackInterfaceInfo callbackInterface, + ExecutableElement method) { + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder(method.getSimpleName().toString()) + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .addAnnotation( + AnnotationSpec.builder(SuppressWarnings.class) + // Allow catching of Exception + .addMember("value", "\"CatchSpecificExceptionsChecker\"") + .build()) + .addParameters(GeneratorUtilities.extractParametersFromMethod(method)); + + methodBuilder.beginControlFlow("try"); + + methodBuilder.addStatement( + "$1T callSender = new $1T(callback, /* methodIdentifier= */ $2L)", + CROSS_PROFILE_CALLBACK_PARCEL_CALL_SENDER_CLASSNAME, + callbackInterface.getIdentifier(method)); + + // parcel is recycled in this method + methodBuilder.addStatement("$1T parcel = $1T.obtain()", PARCEL_CLASSNAME); + + for (VariableElement param : method.getParameters()) { + methodBuilder.addStatement( + "bundler.writeToParcel(parcel, $1L, $2L, /* flags= */ 0)", + param.getSimpleName(), + TypeUtils.generateBundlerType(param.asType())); + } + + methodBuilder.addStatement("callSender.makeParcelCall(parcel)", PARCEL_CLASSNAME); + + methodBuilder.addStatement("parcel.recycle()"); + + methodBuilder + .nextControlFlow("catch ($T e)", Exception.class) + .beginControlFlow("try") + .addStatement( + "$1T unavailableProfileException = new $1T(\"Error when writing callback result\", e)", + UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME) + // parcel is recycled in this method + .addStatement("$1T parcel = $1T.obtain()", PARCEL_CLASSNAME) + .addStatement( + "$T.writeThrowableToParcel(parcel, unavailableProfileException)", + PARCEL_UTILITIES_CLASSNAME) + .addStatement( + "$1T callSender = new $1T(callback)", + CROSS_PROFILE_CALLBACK_EXCEPTION_PARCEL_CALL_SENDER_CLASSNAME) + .addStatement("callSender.makeParcelCall(parcel)", PARCEL_CLASSNAME) + .addStatement("parcel.recycle()") + .nextControlFlow("catch ($T r)", UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME) + .addComment( + "TODO: Decide what should happen if the connection is dropped between the call and" + + " response") + .endControlFlow() + .endControlFlow(); + + classBuilder.addMethod(methodBuilder.build()); + } + + private void generateSenderClass() { + ClassName className = + getCrossProfileCallbackSenderClassName(generatorContext, callbackInterface); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addJavadoc( + "Implementation of {@link $1T} which wraps an instance of {@link $2T},\n" + + "extracting results and exceptions in callbacks and passing them on to the" + + " {@link $2T}.\n", + LOCAL_CALLBACK_CLASSNAME, + callbackInterface.interfaceElement()) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addSuperinterface(LOCAL_CALLBACK_CLASSNAME); + + classBuilder.addField( + FieldSpec.builder(ClassName.get(callbackInterface.interfaceElement()), "callback") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .build()); + + classBuilder.addField( + FieldSpec.builder(EXCEPTION_CALLBACK_CLASSNAME, "exceptionCallback") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .build()); + + classBuilder.addField( + FieldSpec.builder(BUNDLER_CLASSNAME, "bundler") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .build()); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(ClassName.get(callbackInterface.interfaceElement()), "callback") + .addParameter(EXCEPTION_CALLBACK_CLASSNAME, "exceptionCallback") + .addParameter(BUNDLER_CLASSNAME, "bundler") + .beginControlFlow("if (callback == null || bundler == null)") + .addStatement("throw new $T()", NullPointerException.class) + .endControlFlow() + .addStatement("this.callback = callback") + .addStatement("this.exceptionCallback = exceptionCallback") + .addStatement("this.bundler = bundler") + .build()); + + addSenderCallbackMethod(classBuilder); + addSenderExceptionMethod(classBuilder); + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private void addSenderCallbackMethod(TypeSpec.Builder classBuilder) { + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder("onResult") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .addParameter(int.class, "methodIdentifier") + .addParameter(PARCEL_CLASSNAME, "params"); + methodBuilder.beginControlFlow("switch (methodIdentifier)$>"); + + for (ExecutableElement method : callbackInterface.methods()) { + // $> means increase indentation, $< means decrease + methodBuilder.addCode("$<case $L:\n$>", callbackInterface.getIdentifier(method)); + addDispatchCode(methodBuilder, method); + methodBuilder.addStatement("return"); + } + + methodBuilder.endControlFlow(); + + classBuilder.addMethod(methodBuilder.build()); + } + + private void addSenderExceptionMethod(TypeSpec.Builder classBuilder) { + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder("onException") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .addParameter(PARCEL_CLASSNAME, "exception"); + methodBuilder.addStatement( + "$1T throwable = $2T.readThrowableFromParcel(exception)", + Throwable.class, + PARCEL_UTILITIES_CLASSNAME); + + methodBuilder.addStatement("exceptionCallback.onException(throwable)"); + + classBuilder.addMethod(methodBuilder.build()); + } + + private void addDispatchCode(MethodSpec.Builder methodBuilder, ExecutableElement method) { + for (VariableElement parameter : method.getParameters()) { + methodBuilder.addStatement( + "@SuppressWarnings(\"unchecked\") $1T $2L = ($1T) bundler.readFromParcel(params, $3L)", + parameter.asType(), + parameter.getSimpleName().toString(), + TypeUtils.generateBundlerType(parameter.asType())); + } + + String commaSeparatedParams = + method.getParameters().stream() + .map(p -> p.getSimpleName().toString()) + .collect(joining(",")); + + methodBuilder.addStatement("callback.$L($L)", method.getSimpleName(), commaSeparatedParams); + } + + static ClassName getCrossProfileCallbackMultiInterfaceClassName( + GeneratorContext generatorContext, CrossProfileCallbackInterfaceInfo callbackInterface) { + PackageElement originalPackage = + generatorContext.elements().getPackageOf(callbackInterface.interfaceElement()); + String interfaceName = String.format("%s_Multi", callbackInterface.simpleName()); + + return ClassName.get(originalPackage.getQualifiedName().toString(), interfaceName); + } + + static ClassName getCrossProfileCallbackMultiMergerResultClassName( + GeneratorContext generatorContext, CrossProfileCallbackInterfaceInfo callbackInterface) { + PackageElement originalPackage = + generatorContext.elements().getPackageOf(callbackInterface.interfaceElement()); + String interfaceName = + String.format("Profile_%s_MultiMergerResult", callbackInterface.simpleName()); + + return ClassName.get(originalPackage.getQualifiedName().toString(), interfaceName); + } + + static ClassName getCrossProfileCallbackMultiMergerInputClassName( + GeneratorContext generatorContext, CrossProfileCallbackInterfaceInfo callbackInterface) { + PackageElement originalPackage = + generatorContext.elements().getPackageOf(callbackInterface.interfaceElement()); + String interfaceName = + String.format("Profile_%s_MultiMergerInput", callbackInterface.simpleName()); + + return ClassName.get(originalPackage.getQualifiedName().toString(), interfaceName); + } + + static ClassName getCrossProfileCallbackReceiverClassName( + GeneratorContext generatorContext, CrossProfileCallbackInterfaceInfo callbackInterface) { + PackageElement originalPackage = + generatorContext.elements().getPackageOf(callbackInterface.interfaceElement()); + String interfaceName = String.format("Profile_%s_Receiver", callbackInterface.simpleName()); + + return ClassName.get(originalPackage.getQualifiedName().toString(), interfaceName); + } + + static ClassName getCrossProfileCallbackSenderClassName( + GeneratorContext generatorContext, CrossProfileCallbackInterfaceInfo callbackInterface) { + PackageElement originalPackage = + generatorContext.elements().getPackageOf(callbackInterface.interfaceElement()); + String interfaceName = String.format("Profile_%s_Sender", callbackInterface.simpleName()); + + return ClassName.get(originalPackage.getQualifiedName().toString(), interfaceName); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileTypeCodeGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileTypeCodeGenerator.java new file mode 100644 index 0000000..075ac6c --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileTypeCodeGenerator.java @@ -0,0 +1,73 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.google.android.enterprise.connectedapps.processor.containers.ProviderClassInfo; + +class CrossProfileTypeCodeGenerator { + private boolean generated = false; + private final InterfaceGenerator interfaceGenerator; + private final CurrentProfileGenerator currentProfileGenerator; + private final OtherProfileGenerator otherProfileGenerator; + private final IfAvailableGenerator ifAvailableGenerator; + private final AlwaysThrowsGenerator alwaysThrowsGenerator; + private final MultipleProfilesGenerator multipleProfilesGenerator; + private final DefaultProfileClassGenerator defaultProfileClassGenerator; + private final InternalCrossProfileClassGenerator internalCrossProfileClassGenerator; + private final BundlerGenerator bundlerGenerator; + + public CrossProfileTypeCodeGenerator( + GeneratorContext generatorContext, + ProviderClassInfo providerClass, + CrossProfileTypeInfo crossProfileType) { + checkNotNull(generatorContext); + checkNotNull(crossProfileType); + this.interfaceGenerator = new InterfaceGenerator(generatorContext, crossProfileType); + this.currentProfileGenerator = new CurrentProfileGenerator(generatorContext, crossProfileType); + this.otherProfileGenerator = new OtherProfileGenerator(generatorContext, crossProfileType); + this.ifAvailableGenerator = new IfAvailableGenerator(generatorContext, crossProfileType); + this.alwaysThrowsGenerator = new AlwaysThrowsGenerator(generatorContext, crossProfileType); + this.multipleProfilesGenerator = + new MultipleProfilesGenerator(generatorContext, crossProfileType); + this.defaultProfileClassGenerator = + new DefaultProfileClassGenerator(generatorContext, crossProfileType); + this.internalCrossProfileClassGenerator = + new InternalCrossProfileClassGenerator(generatorContext, providerClass, crossProfileType); + this.bundlerGenerator = new BundlerGenerator(generatorContext, crossProfileType); + } + + void generate() { + if (generated) { + throw new IllegalStateException( + "CrossProfileTypeCodeGenerator#generate can only be called once"); + } + generated = true; + + interfaceGenerator.generate(); + currentProfileGenerator.generate(); + otherProfileGenerator.generate(); + ifAvailableGenerator.generate(); + alwaysThrowsGenerator.generate(); + multipleProfilesGenerator.generate(); + defaultProfileClassGenerator.generate(); + internalCrossProfileClassGenerator.generate(); + bundlerGenerator.generate(); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CurrentProfileGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CurrentProfileGenerator.java new file mode 100644 index 0000000..103b965 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CurrentProfileGenerator.java @@ -0,0 +1,219 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CONTEXT_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.EXCEPTION_CALLBACK_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS; +import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.squareup.javapoet.AnnotationSpec; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeSpec; +import javax.lang.model.element.Modifier; +import javax.lang.model.type.TypeKind; + +/** + * Generate the {@code Profile_*_CurrentProfile} class for a single crossProfileType class. + * + * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to + * validate that the annotated code is correct. + */ +final class CurrentProfileGenerator { + + private boolean generated = false; + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final CrossProfileTypeInfo crossProfileType; + + CurrentProfileGenerator( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.crossProfileType = checkNotNull(crossProfileType); + } + + void generate() { + if (generated) { + throw new IllegalStateException("CurrentProfileGenerator#generate can only be called once"); + } + generated = true; + + generateCurrentProfileClass(); + } + + private void generateCurrentProfileClass() { + ClassName className = getCurrentProfileClassName(generatorContext, crossProfileType); + + ClassName singleSenderInterface = + InterfaceGenerator.getSingleSenderInterfaceClassName(generatorContext, crossProfileType); + ClassName singleSenderCanThrowInterface = + InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName( + generatorContext, crossProfileType); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addJavadoc( + "Implementation of {@link $T} and {@link $T} which makes calls to the current" + + " profile.\n\n" + + "<p>{@link $T} will not be thrown by calls to methods in this class.\n", + singleSenderInterface, + singleSenderCanThrowInterface, + UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addSuperinterface(singleSenderInterface) + .addSuperinterface(singleSenderCanThrowInterface); + + addCrossProfileConstructor(classBuilder); + + for (CrossProfileMethodInfo method : crossProfileType.crossProfileMethods()) { + generateMethodOnCurrentProfileClass(classBuilder, method, crossProfileType); + + if (method.isCrossProfileCallback(generatorContext)) { + // To meet the interface for canThrow we need a version with exceptionCallback. + // However we never use it. + generateCrossProfileCallbackWithExceptionMethodOnCurrentProfileClass( + classBuilder, method, crossProfileType); + } + } + + classBuilder.addMethod( + MethodSpec.methodBuilder("timeout") + .addAnnotation(Override.class) + .addAnnotation( + AnnotationSpec.builder(SuppressWarnings.class) + .addMember("value", "$S", "GoodTime") + .build()) + .addModifiers(Modifier.PUBLIC) + .returns(className) + .addParameter(long.class, "timeout") + .addStatement("return this") + .build()); + + ClassName ifAvailableClass = + IfAvailableGenerator.getIfAvailableClassName(generatorContext, crossProfileType); + + classBuilder.addMethod( + MethodSpec.methodBuilder("ifAvailable") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(ifAvailableClass) + .addStatement("return new $T(this)", ifAvailableClass) + .build()); + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private void addCrossProfileConstructor(TypeSpec.Builder classBuilder) { + classBuilder.addField( + FieldSpec.builder(CONTEXT_CLASSNAME, "context") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .build()); + + MethodSpec.Builder constructorBuilder = + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(CONTEXT_CLASSNAME, "context") + .addStatement("this.context = context"); + + if (!crossProfileType.isStatic()) { + classBuilder.addField( + FieldSpec.builder(crossProfileType.className(), "crossProfileType") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .build()); + + constructorBuilder + .addParameter(crossProfileType.className(), "crossProfileType") + .addStatement("this.crossProfileType = crossProfileType"); + } + + classBuilder.addMethod(constructorBuilder.build()); + } + + private void generateMethodOnCurrentProfileClass( + TypeSpec.Builder classBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + + CodeBlock crossProfileTypeReference = + method.isStatic() + ? CodeBlock.of("$1T", crossProfileType.className()) + : CodeBlock.of("crossProfileType"); + + CodeBlock methodCall = + CodeBlock.of( + "$L.$L($L)", + crossProfileTypeReference, + method.simpleName(), + method.commaSeparatedParameters( + crossProfileType.supportedTypes(), REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS)); + + if (method.returnType().getKind() != TypeKind.VOID) { + methodCall = CodeBlock.of("return $L", methodCall); + } + + classBuilder.addMethod( + MethodSpec.methodBuilder(method.simpleName()) + .addAnnotation(Override.class) + .addExceptions(method.thrownExceptions()) + .addModifiers(Modifier.PUBLIC) + .returns(method.returnTypeTypeName()) + .addParameters( + GeneratorUtilities.extractParametersFromMethod( + crossProfileType.supportedTypes(), + method.methodElement(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)) + .addStatement(methodCall) + .build()); + } + + private static void generateCrossProfileCallbackWithExceptionMethodOnCurrentProfileClass( + TypeSpec.Builder classBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + + classBuilder.addMethod( + MethodSpec.methodBuilder(method.simpleName()) + .addModifiers(Modifier.PUBLIC) + .returns(method.returnTypeTypeName()) + .addParameters( + GeneratorUtilities.extractParametersFromMethod( + crossProfileType.supportedTypes(), + method.methodElement(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)) + .addParameter(EXCEPTION_CALLBACK_CLASSNAME, "exceptionCallback") + .addStatement( + "$L($L)", + method.simpleName(), + method.commaSeparatedParameters( + crossProfileType.supportedTypes(), REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)) + .build()); + } + + static ClassName getCurrentProfileClassName( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + return GeneratorUtilities.appendToClassName( + crossProfileType.profileClassName(), "_CurrentProfile"); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DefaultProfileClassGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DefaultProfileClassGenerator.java new file mode 100644 index 0000000..114a19c --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DefaultProfileClassGenerator.java @@ -0,0 +1,330 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.AlwaysThrowsGenerator.getAlwaysThrowsClassName; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_AWARE_UTILS_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_CONNECTOR_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.VERSION_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.VERSION_CODES_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CurrentProfileGenerator.getCurrentProfileClassName; +import static com.google.android.enterprise.connectedapps.processor.InterfaceGenerator.getMultipleSenderInterfaceClassName; +import static com.google.android.enterprise.connectedapps.processor.InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName; +import static com.google.android.enterprise.connectedapps.processor.InterfaceGenerator.getSingleSenderInterfaceClassName; +import static com.google.android.enterprise.connectedapps.processor.MultipleProfilesGenerator.getMultipleProfilesClassName; +import static com.google.android.enterprise.connectedapps.processor.OtherProfileGenerator.getOtherProfileClassName; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector.ProfileType; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.squareup.javapoet.ArrayTypeName; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeSpec; +import java.util.HashMap; +import java.util.Map; +import javax.lang.model.element.Modifier; + +/** + * Generate the {@code DefaultProfile*} class for each cross-profile type. + * + * <p>This is intended to be initialised and used once, which will generate all needed code. + * + * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to + * validate that the annotated code is correct. + */ +final class DefaultProfileClassGenerator { + + private boolean generated = false; + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final CrossProfileTypeInfo crossProfileType; + + DefaultProfileClassGenerator( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.crossProfileType = checkNotNull(crossProfileType); + } + + void generate() { + if (generated) { + throw new IllegalStateException( + "DefaultProfileClassGenerator#generate can only be called once"); + } + generated = true; + + generateDefaultProfileClass(); + } + + private void generateDefaultProfileClass() { + ClassName className = getDefaultProfileClassName(generatorContext, crossProfileType); + + ClassName connectorClassName = + crossProfileType.profileConnector().isPresent() + ? crossProfileType.profileConnector().get().connectorClassName() + : PROFILE_CONNECTOR_CLASSNAME; + + ClassName crossProfileTypeInterfaceClassName = + InterfaceGenerator.getCrossProfileTypeInterfaceClassName( + generatorContext, crossProfileType); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addJavadoc( + "Default implementation of {@link $T} to be used in production.\n", + crossProfileTypeInterfaceClassName) + .addModifiers(Modifier.FINAL); + + classBuilder.addSuperinterface(crossProfileTypeInterfaceClassName); + + classBuilder.addField( + FieldSpec.builder(connectorClassName, "connector") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .build()); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addParameter(connectorClassName, "connector") + .addModifiers(Modifier.PUBLIC) + .addStatement("this.connector = connector") + .build()); + + addCurrentMethod(classBuilder); + + classBuilder.addMethod( + MethodSpec.methodBuilder("other") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .beginControlFlow("if ($T.SDK_INT < $T.O)", VERSION_CLASSNAME, VERSION_CODES_CLASSNAME) + .addStatement( + "return new $T($S)", + getAlwaysThrowsClassName(generatorContext, crossProfileType), + "Cross-profile calls are not supported on this version of Android") + .endControlFlow() + .addStatement( + "return new $T(connector)", + getOtherProfileClassName(generatorContext, crossProfileType)) + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("personal") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .beginControlFlow("if ($T.SDK_INT < $T.O)", VERSION_CLASSNAME, VERSION_CODES_CLASSNAME) + .addStatement( + "return new $T($S)", + getAlwaysThrowsClassName(generatorContext, crossProfileType), + "Cross-profile calls are not supported on this version of Android") + .endControlFlow() + .addStatement("return profile(connector.utils().getPersonalProfile())") + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("work") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .beginControlFlow("if ($T.SDK_INT < $T.O)", VERSION_CLASSNAME, VERSION_CODES_CLASSNAME) + .addStatement( + "return new $T($S)", + getAlwaysThrowsClassName(generatorContext, crossProfileType), + "Cross-profile calls are not supported on this version of Android") + .endControlFlow() + .addStatement("return profile(connector.utils().getWorkProfile())") + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("profile") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .addParameter(PROFILE_CLASSNAME, "profile") + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .beginControlFlow("if ($T.SDK_INT < $T.O)", VERSION_CLASSNAME, VERSION_CODES_CLASSNAME) + .addStatement( + "return new $T($S)", + getAlwaysThrowsClassName(generatorContext, crossProfileType), + "Cross-profile calls are not supported on this version of Android") + .endControlFlow() + .beginControlFlow("if (profile.isCurrent())") + .addStatement( + "return ($T) current()", + getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .nextControlFlow("else") + .addComment("must be other profile") + .addStatement("return other()") + .endControlFlow() + .build()); + + ParameterizedTypeName senderMapType = + ParameterizedTypeName.get( + ClassName.get(Map.class), + PROFILE_CLASSNAME, + InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName( + generatorContext, crossProfileType)); + + classBuilder.addMethod( + MethodSpec.methodBuilder("profiles") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .addParameter(ArrayTypeName.of(PROFILE_CLASSNAME), "profiles") + .varargs(true) + .returns(getMultipleSenderInterfaceClassName(generatorContext, crossProfileType)) + .addStatement("$T senders = new $T<>()", senderMapType, HashMap.class) + .beginControlFlow("for ($1T profileIdentifier : profiles)", PROFILE_CLASSNAME) + .addStatement("senders.put(profileIdentifier, profile(profileIdentifier))") + .endControlFlow() + .addStatement( + "return new $1T(senders)", + getMultipleProfilesClassName(generatorContext, crossProfileType)) + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("both") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(getMultipleSenderInterfaceClassName(generatorContext, crossProfileType)) + .addStatement("$1T utils = connector.utils()", PROFILE_AWARE_UTILS_CLASSNAME) + .addStatement( + "$1T currentProfileIdentifier = utils.getCurrentProfile()", PROFILE_CLASSNAME) + .beginControlFlow("if ($T.SDK_INT < $T.O)", VERSION_CLASSNAME, VERSION_CODES_CLASSNAME) + .addStatement("$T senders = new $T<>()", senderMapType, HashMap.class) + .addStatement( + "senders.put(currentProfileIdentifier, ($T) current())", + InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName( + generatorContext, crossProfileType)) + .addStatement( + "return new $1T(senders)", + getMultipleProfilesClassName(generatorContext, crossProfileType)) + .endControlFlow() + .addStatement("$1T otherProfileIdentifier = utils.getOtherProfile()", PROFILE_CLASSNAME) + .addStatement("return profiles(currentProfileIdentifier, otherProfileIdentifier)") + .build()); + + if (!crossProfileType.profileConnector().isPresent() + || crossProfileType.profileConnector().get().primaryProfile() != ProfileType.NONE) { + generatePrimarySecondaryMethods(classBuilder); + } + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private void addCurrentMethod(TypeSpec.Builder classBuilder) { + MethodSpec.Builder currentMethodBuilder = + MethodSpec.methodBuilder("current") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(getSingleSenderInterfaceClassName(generatorContext, crossProfileType)); + + if (crossProfileType.isStatic()) { + currentMethodBuilder.addStatement( + "return new $1T(connector.applicationContext())", + getCurrentProfileClassName(generatorContext, crossProfileType)); + } else { + currentMethodBuilder.addStatement( + "return new $1T(connector.applicationContext()," + + " $2T.instance().crossProfileType(connector.applicationContext()))", + getCurrentProfileClassName(generatorContext, crossProfileType), + InternalCrossProfileClassGenerator.getInternalCrossProfileClassName( + generatorContext, crossProfileType)); + } + + classBuilder.addMethod(currentMethodBuilder.build()); + } + + private void generatePrimarySecondaryMethods(TypeSpec.Builder classBuilder) { + generatePrimaryMethod(classBuilder); + generateSecondaryMethod(classBuilder); + generateSuppliersMethod(classBuilder); + } + + private void generatePrimaryMethod(TypeSpec.Builder classBuilder) { + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder("primary") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .beginControlFlow("if ($T.SDK_INT < $T.O)", VERSION_CLASSNAME, VERSION_CODES_CLASSNAME) + .addStatement( + "return new $T($S)", + getAlwaysThrowsClassName(generatorContext, crossProfileType), + "Cross-profile calls are not supported on this version of Android") + .endControlFlow() + .addStatement( + "$T primaryProfile = connector.utils().getPrimaryProfile()", PROFILE_CLASSNAME) + .beginControlFlow("if (primaryProfile == null)") + .addStatement("throw new $T(\"No primary profile set\")", IllegalStateException.class) + .endControlFlow() + .addStatement("return profile(primaryProfile)"); + + classBuilder.addMethod(methodBuilder.build()); + } + + private void generateSecondaryMethod(TypeSpec.Builder classBuilder) { + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder("secondary") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .beginControlFlow("if ($T.SDK_INT < $T.O)", VERSION_CLASSNAME, VERSION_CODES_CLASSNAME) + .addStatement( + "return new $T($S)", + getAlwaysThrowsClassName(generatorContext, crossProfileType), + "Cross-profile calls are not supported on this version of Android") + .endControlFlow() + .addStatement( + "$T secondaryProfile = connector.utils().getSecondaryProfile()", PROFILE_CLASSNAME) + .beginControlFlow("if (secondaryProfile == null)") + .addStatement("throw new $T(\"No primary profile set\")", IllegalStateException.class) + .endControlFlow() + .addStatement("return profile(secondaryProfile)"); + + classBuilder.addMethod(methodBuilder.build()); + } + + private void generateSuppliersMethod(TypeSpec.Builder classBuilder) { + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder("suppliers") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(getMultipleSenderInterfaceClassName(generatorContext, crossProfileType)) + .beginControlFlow("if ($T.SDK_INT < $T.O)", VERSION_CLASSNAME, VERSION_CODES_CLASSNAME) + .addStatement("return both()") + .endControlFlow() + .addStatement("$1T utils = connector.utils()", PROFILE_AWARE_UTILS_CLASSNAME) + .addStatement("$1T currentProfile = utils.getCurrentProfile()", PROFILE_CLASSNAME) + .addStatement("$1T secondaryProfile = utils.getSecondaryProfile()", PROFILE_CLASSNAME) + .beginControlFlow("if (secondaryProfile == null)") + .addStatement("throw new $T(\"No primary profile set\")", IllegalStateException.class) + .endControlFlow() + .addStatement("return profiles(currentProfile, secondaryProfile)"); + + classBuilder.addMethod(methodBuilder.build()); + } + + static ClassName getDefaultProfileClassName( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + return ClassName.get( + crossProfileType.profileClassName().packageName(), + "Default" + crossProfileType.profileClassName().simpleName()); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DispatcherGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DispatcherGenerator.java new file mode 100644 index 0000000..ff45251 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DispatcherGenerator.java @@ -0,0 +1,326 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.BACKGROUND_EXCEPTION_THROWER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.BINDER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CONTEXT_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CROSS_PROFILE_CALLBACK_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CROSS_PROFILE_SENDER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_CALL_RECEIVER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_UTILITIES_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.ServiceGenerator.getConnectedAppsServiceClassName; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.stream.Collectors.joining; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.google.android.enterprise.connectedapps.processor.containers.ProviderClassInfo; +import com.squareup.javapoet.AnnotationSpec; +import com.squareup.javapoet.ArrayTypeName; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeSpec; +import java.util.List; +import javax.lang.model.element.Modifier; + +/** + * Generate the {@code *_Dispatcher} class for a single {@link CrossProfileConfiguration} annotated + * class. + * + * <p>This class includes the dispatch of calls to providers. + * + * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to + * validate that the annotated code is correct. + */ +final class DispatcherGenerator { + + private boolean generated = false; + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final CrossProfileConfigurationInfo configuration; + + DispatcherGenerator( + GeneratorContext generatorContext, CrossProfileConfigurationInfo configuration) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.configuration = checkNotNull(configuration); + } + + void generate() { + if (generated) { + throw new IllegalStateException("DispatcherGenerator#generate can only be called once"); + } + generated = true; + + generateDispatcherClass(); + } + + private void generateDispatcherClass() { + ClassName className = getDispatcherClassName(generatorContext, configuration); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addJavadoc( + "Class for dispatching calls to appropriate providers.\n\n" + + "<p>This uses a {@link $T} to construct calls before passing the completed" + + " call\n" + + "to a provider.\n", + PARCEL_CALL_RECEIVER_CLASSNAME); + + classBuilder.addField( + FieldSpec.builder(PARCEL_CALL_RECEIVER_CLASSNAME, "parcelCallReceiver") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .initializer("new $T()", PARCEL_CALL_RECEIVER_CLASSNAME) + .build()); + + addEnsureValidCallerMethod(classBuilder); + addCallMethod(classBuilder); + addPrepareCallMethod(classBuilder); + addFetchResponseMethod(classBuilder); + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private static void addPrepareCallMethod(TypeSpec.Builder classBuilder) { + MethodSpec prepareCallMethod = + MethodSpec.methodBuilder("prepareCall") + .addModifiers(Modifier.PUBLIC) + .addParameter(CONTEXT_CLASSNAME, "context") + .addParameter(long.class, "callId") + .addParameter(int.class, "blockId") + .addParameter(int.class, "numBytes") + .addParameter(ArrayTypeName.of(byte.class), "paramBytes") + .addStatement("ensureValidCaller(context)") + .addStatement("parcelCallReceiver.prepareCall(callId, blockId, numBytes, paramBytes)") + .addJavadoc( + "Store a block of bytes to be part of a future call to\n" + + "{@link #call(Context, long, int, long, int, byte[], ICrossProfileCallback)}." + + "\n\n" + + "@param callId Arbitrary identifier used to link together\n" + + " {@link #prepareCall(Context, long, int, int, byte[])} and\n " + + "{@link #call(Context, long, int, long, int, byte[], ICrossProfileCallback)}" + + " calls.\n" + + "@param blockId The (zero indexed) number of this block. Each block should" + + " be\n {@link $1T#MAX_BYTES_PER_BLOCK} bytes so the total number of blocks" + + " is\n {@code numBytes / $1T#MAX_BYTES_PER_BLOCK}.\n" + + "@param numBytes The total number of bytes being transferred (across all" + + " blocks for this call,\n including the final {@link" + + " #call(Context, long, int, long, int, byte[], ICrossProfileCallback)}.\n" + + "@param paramBytes The bytes for this block. Should contain\n {@link" + + " $1T#MAX_BYTES_PER_BLOCK} bytes.\n\n" + + "@see $2T#prepareCall(long, int, int, byte[])", + CROSS_PROFILE_SENDER_CLASSNAME, + PARCEL_CALL_RECEIVER_CLASSNAME) + .build(); + classBuilder.addMethod(prepareCallMethod); + } + + private static void addFetchResponseMethod(TypeSpec.Builder classBuilder) { + MethodSpec prepareCallMethod = + MethodSpec.methodBuilder("fetchResponse") + .addModifiers(Modifier.PUBLIC) + .addParameter(CONTEXT_CLASSNAME, "context") + .addParameter(long.class, "callId") + .addParameter(int.class, "blockId") + .returns(ArrayTypeName.of(byte.class)) + .addStatement("ensureValidCaller(context)") + .addStatement("return parcelCallReceiver.getPreparedResponse(callId, blockId)") + .addJavadoc( + "Fetch a response block if a previous call to\n {@link #call(Context, long, int," + + " long, int, byte[], ICrossProfileCallback)} returned a\n byte array with" + + " 1 as the first byte.\n\n" + + "@param callId should be the same callId used with\n {@link #call(Context," + + " long, int, long, int, byte[], ICrossProfileCallback)}\n" + + "@param blockId The (zero indexed) number of the block to fetch.\n\n" + + "@see $1T#getPreparedResponse(long, int)\n", + PARCEL_CALL_RECEIVER_CLASSNAME) + .build(); + classBuilder.addMethod(prepareCallMethod); + } + + private static void addEnsureValidCallerMethod(TypeSpec.Builder classBuilder) { + CodeBlock.Builder methodCode = CodeBlock.builder(); + + methodCode.addStatement( + "$T[] callingPackageNames =" + + " context.getPackageManager().getPackagesForUid($T.getCallingUid())", + String.class, + BINDER_CLASSNAME); + methodCode.beginControlFlow("for (String callingPackageName : callingPackageNames)"); + methodCode.beginControlFlow("if (context.getPackageName().equals(callingPackageName))"); + methodCode.addStatement("return"); + methodCode.endControlFlow(); + methodCode.endControlFlow(); + + methodCode.addStatement( + "throw new $T(\"Cross-profile functionality is only available within the same package\")", + IllegalStateException.class); + + MethodSpec ensureValidCallerMethod = + MethodSpec.methodBuilder("ensureValidCaller") + .addModifiers(Modifier.PRIVATE, Modifier.STATIC) + .addParameter(CONTEXT_CLASSNAME, "context") + .addCode(methodCode.build()) + .build(); + + classBuilder.addMethod(ensureValidCallerMethod); + } + + private void addCallMethod(TypeSpec.Builder classBuilder) { + CodeBlock.Builder methodCode = CodeBlock.builder(); + + methodCode.beginControlFlow("try"); + + methodCode.addStatement("ensureValidCaller(context)"); + + methodCode.addStatement( + "$1T parcel = parcelCallReceiver.getPreparedCall(callId, blockId, paramBytes)", + PARCEL_CLASSNAME); + + List<ProviderClassInfo> providers = configuration.providers().asList(); + + if (!providers.isEmpty()) { + addProviderDispatch(methodCode, providers); + } + + methodCode.addStatement( + "throw new $T(\"Unknown type identifier \" + crossProfileTypeIdentifier)", + IllegalArgumentException.class); + + methodCode.nextControlFlow("catch ($T e)", RuntimeException.class); + // parcel is recycled in this method + methodCode.addStatement("$1T throwableParcel = $1T.obtain()", PARCEL_CLASSNAME); + methodCode.add("throwableParcel.writeInt(1); //errors\n"); + methodCode.addStatement( + "$T.writeThrowableToParcel(throwableParcel, e)", PARCEL_UTILITIES_CLASSNAME); + methodCode.addStatement( + "$1T throwableBytes = parcelCallReceiver.prepareResponse(callId, throwableParcel)", + ArrayTypeName.of(byte.class)); + methodCode.addStatement("throwableParcel.recycle()"); + + methodCode.addStatement("$T.throwInBackground(e)", BACKGROUND_EXCEPTION_THROWER_CLASSNAME); + + methodCode.addStatement("return throwableBytes"); + methodCode.endControlFlow(); + + MethodSpec callMethod = + MethodSpec.methodBuilder("call") + .addModifiers(Modifier.PUBLIC) + .returns(ArrayTypeName.of(byte.class)) + .addAnnotation(AnnotationSpec.builder(SuppressWarnings.class) + // Allow catching of RuntimeException + .addMember("value", "\"CatchSpecificExceptionsChecker\"") + .build()) + .addParameter(CONTEXT_CLASSNAME, "context") + .addParameter(long.class, "callId") + .addParameter(int.class, "blockId") + .addParameter(long.class, "crossProfileTypeIdentifier") + .addParameter(int.class, "methodIdentifier") + .addParameter(ArrayTypeName.of(byte.class), "paramBytes") + .addParameter(CROSS_PROFILE_CALLBACK_CLASSNAME, "callback") + .addCode(methodCode.build()) + .addJavadoc( + "Make a call, which will execute some annotated method and return a response.\n\n" + + "<p>The parameters to the call should be contained in a {@link $1T}" + + " marshalled into\n" + + "a byte array. If the byte array is larger than {@link" + + " $2T#MAX_BYTES_PER_BLOCK},\n" + + "then it should be separated into blocks of {@link" + + " $2T#MAX_BYTES_PER_BLOCK}\n" + + "bytes, and {@link #prepareCall(Context, long, int, int, byte[])} used to" + + " set all but the final\n" + + "block, before calling this method with the final block.\n\n" + + "<p>The response will be an array of bytes. If the response is complete (it" + + " fits into a single\n" + + "block), then the first byte will be 0, otherwise the first byte will be 1" + + " and the next 4 bytes\n" + + "will be an int representing the total size of the return value. The rest of" + + " the bytes are the\n" + + "first block of the return value. {@link #fetchResponse(Context, long, int)" + + " should be used to\n" + + "fetch further blocks.\n\n" + + "@param callId Arbitrary identifier used to link together\n" + + " {@link #prepareCall(Context, long, int, int, byte[])} and\n" + + " {@link #call(Context, long, int, long, int, byte[]," + + " ICrossProfileCallback)} calls.\n" + + "@param blockId The (zero indexed) number of this block. Each block should" + + " be\n {@link CrossProfileSender#MAX_BYTES_PER_BLOCK} bytes so the total" + + " number of blocks is\n {@code numBytes /" + + " CrossProfileSender#MAX_BYTES_PER_BLOCK}.\n" + + "@param crossProfileTypeIdentifier The generated identifier for the type" + + " which contains the\n method being called.\n" + + "@param methodIdentifier The index of the method being called on the cross" + + " profile type.\n" + + "@param paramBytes The bytes for the final block, this will be merged with" + + " any blocks\n previously set by a call to" + + " {@link #prepareCall(Context, long, int, int, byte[])}.\n" + + "@param callback A callback to be used if this is an asynchronous call." + + " Otherwise this should be\n {@code null}.\n\n" + + "@see $3T#getPreparedCall(long, int, byte[])\n", + PARCEL_CLASSNAME, + CROSS_PROFILE_SENDER_CLASSNAME, + PARCEL_CALL_RECEIVER_CLASSNAME) + .build(); + + classBuilder.addMethod(callMethod); + } + + private void addProviderDispatch( + CodeBlock.Builder methodCode, List<ProviderClassInfo> providers) { + for (ProviderClassInfo provider : providers) { + addProviderDispatchInner(methodCode, provider); + } + } + + private void addProviderDispatchInner(CodeBlock.Builder methodCode, ProviderClassInfo provider) { + String condition = + provider.allCrossProfileTypes().stream() + .map( + h -> + "crossProfileTypeIdentifier == " + + h.identifier() + + "L // " + + h.crossProfileTypeElement().getQualifiedName() + + "\n") + .collect(joining(" || ")); + + methodCode.beginControlFlow("if ($L)", condition); + methodCode.addStatement( + "$1T returnParcel = $2T.instance().call(context.getApplicationContext()," + + " crossProfileTypeIdentifier, methodIdentifier, parcel, callback)", + PARCEL_CLASSNAME, + InternalProviderClassGenerator.getInternalProviderClassName(generatorContext, provider)); + methodCode.addStatement( + "$1T returnBytes = parcelCallReceiver.prepareResponse(callId, returnParcel)", + ArrayTypeName.of(byte.class)); + methodCode.addStatement("parcel.recycle()"); + methodCode.addStatement("returnParcel.recycle()"); + methodCode.addStatement("return returnBytes"); + methodCode.endControlFlow(); + } + + static ClassName getDispatcherClassName( + GeneratorContext generatorContext, CrossProfileConfigurationInfo configuration) { + ClassName serviceName = getConnectedAppsServiceClassName(generatorContext, configuration); + return ClassName.get(serviceName.packageName(), serviceName.simpleName() + "_Dispatcher"); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/EarlyValidator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/EarlyValidator.java new file mode 100644 index 0000000..f1f566d --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/EarlyValidator.java @@ -0,0 +1,1297 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCELABLE_CREATOR_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.GeneratorUtilities.findCrossProfileMethodsInClass; +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.hasCrossProfileAnnotation; +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.hasCrossProfileConfigurationAnnotation; +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.hasCrossProfileConfigurationsAnnotation; +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.hasCrossProfileProviderAnnotation; +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.validationMessageFormatterFor; +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.validationMessageFormatterForClass; +import static java.util.stream.Collectors.toMap; +import static java.util.stream.Collectors.toSet; + +import com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper; +import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector; +import com.google.android.enterprise.connectedapps.annotations.CustomUserConnector; +import com.google.android.enterprise.connectedapps.processor.SupportedTypes.TypeCheckContext; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileAnnotationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackAnnotationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileProviderAnnotationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.FutureWrapperAnnotationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.ParcelableWrapperAnnotationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.ProfileConnectorInfo; +import com.google.android.enterprise.connectedapps.processor.containers.UserConnectorInfo; +import com.google.android.enterprise.connectedapps.processor.containers.ValidatorContext; +import com.google.android.enterprise.connectedapps.processor.containers.ValidatorCrossProfileConfigurationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.ValidatorCrossProfileTestInfo; +import com.google.android.enterprise.connectedapps.processor.containers.ValidatorCrossProfileTypeInfo; +import com.google.android.enterprise.connectedapps.processor.containers.ValidatorProviderClassInfo; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.ElementFilter; +import javax.tools.Diagnostic.Kind; + +/** Validator to check that annotations have been used correctly before generating code. */ +public final class EarlyValidator { + + private static final String MULTIPLE_PROVIDERS_ERROR = + "The @CROSS_PROFILE_ANNOTATION annotated type %s has been provided more than once"; + private static final String PROVIDING_NON_CROSS_PROFILE_TYPE_ERROR = + "Methods annotated @CROSS_PROFILE_PROVIDER_ANNOTATION must only return" + + " @CROSS_PROFILE_ANNOTATION annotated types"; + private static final String INVALID_CONSTRUCTORS_ERROR = + "Provider classes must have a single public constructor which takes either a single Context" + + " argument or no arguments"; + private static final String PROVIDER_INCORRECT_ARGS_ERROR = + "Methods annotated @CROSS_PROFILE_PROVIDER_ANNOTATION can only take a single Context" + + " argument, or no-args"; + private static final String STATIC_PROVIDER_ERROR = + "Methods annotated @CROSS_PROFILE_PROVIDER_ANNOTATION can not be static"; + private static final String UNSUPPORTED_RETURN_TYPE_ERROR = + "The type %s cannot be returned by methods annotated @CROSS_PROFILE_ANNOTATION"; + private static final String UNSUPPORTED_PARAMETER_TYPE_CROSS_PROFILE_METHOD = + "The type %s cannot be used by parameters of methods annotated @CROSS_PROFILE_ANNOTATION"; + private static final String UNSUPPORTED_PARAMETER_TYPE_CROSS_ASYNC_CALLBACK = + "The type %s cannot be used by parameters of methods on interfaces annotated" + + " @CROSS_PROFILE_CALLBACK_ANNOTATION"; + private static final String CROSS_PROFILE_TYPE_DEFAULT_PACKAGE_ERROR = + "@CROSS_PROFILE_ANNOTATION types must not be in the default package"; + private static final String NON_PUBLIC_CROSS_PROFILE_TYPE_ERROR = + "@CROSS_PROFILE_ANNOTATION types must be public"; + private static final String NOT_A_PROVIDER_CLASS_ERROR = + "All classes specified in 'providers' must be provider classes"; + private static final String CONNECTOR_MUST_BE_INTERFACE = "Connectors must be interfaces"; + private static final String CONNECTOR_MUST_EXTEND_CONNECTOR = + "Interfaces specified as a connector must extend ProfileConnector"; + private static final String CUSTOM_PROFILE_CONNECTOR_MUST_BE_INTERFACE = + "@CustomProfileConnector must only be applied to interfaces"; + private static final String CUSTOM_USER_CONNECTOR_MUST_BE_INTERFACE = + "@CustomUserConnector must only be applied to interfaces"; + private static final String GENERATED_PROFILE_CONNECTOR_MUST_BE_INTERFACE = + "@GeneratedProfileConnector must only be applied to interfaces"; + private static final String GENERATED_USER_CONNECTOR_MUST_BE_INTERFACE = + "@GeneratedUserConnector must only be applied to interfaces"; + private static final String CUSTOM_PROFILE_CONNECTOR_MUST_EXTEND_CONNECTOR = + "Interfaces annotated with @CustomProfileConnector must extend ProfileConnector"; + private static final String CUSTOM_USER_CONNECTOR_MUST_EXTEND_CONNECTOR = + "Interfaces annotated with @CustomUserConnector must extend UserConnector"; + private static final String GENERATED_PROFILE_CONNECTOR_MUST_EXTEND_PROFILE_CONNECTOR = + "Interfaces annotated with @GeneratedProfileConnector must extend ProfileConnector"; + private static final String GENERATED_USER_CONNECTOR_MUST_EXTEND_USER_CONNECTOR = + "Interfaces annotated with @GeneratedUserConnector must extend UserConnector"; + private static final String CALLBACK_INTERFACE_DEFAULT_PACKAGE_ERROR = + "Interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION must not be in the default package"; + private static final String NOT_INTERFACE_ERROR = + "Only interfaces may be annotated @CROSS_PROFILE_CALLBACK_ANNOTATION"; + private static final String NOT_ONE_METHOD_ERROR = + "Interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION(simple=true) must have exactly one" + + " method"; + private static final String NO_METHODS_ERROR = + "Interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION must have at least one method"; + private static final String DEFAULT_METHOD_ERROR = + "Interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION must have no default methods"; + private static final String STATIC_METHOD_ERROR = + "Interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION must have no static methods"; + private static final String NOT_VOID_ERROR = + "Methods on interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION must return void"; + private static final String GENERIC_CALLBACK_INTERFACE_ERROR = + "Interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION can not be generic"; + private static final String MORE_THAN_ONE_PARAMETER_ERROR = + "Methods on interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION(simple=true) can only" + + " take a single parameter"; + private static final String MULTIPLE_ASYNC_CALLBACK_PARAMETERS_ERROR = + "Methods annotated @CROSS_PROFILE_ANNOTATION can have a maximum of one parameter of a type" + + " annotated @CROSS_PROFILE_CALLBACK_ANNOTATION"; + private static final String NON_VOID_CALLBACK_ERROR = + "Methods annotated @CROSS_PROFILE_ANNOTATION which take a parameter type annotated" + + " @CROSS_PROFILE_CALLBACK_ANNOTATION must return void"; + private static final String METHOD_ISSTATIC_ERROR = + "@CROSS_PROFILE_ANNOTATION annotations on methods can not specify isStatic"; + private static final String METHOD_CONNECTOR_ERROR = + "@CROSS_PROFILE_ANNOTATION annotations on methods can not specify a connector"; + private static final String METHOD_PARCELABLE_WRAPPERS_ERROR = + "@CROSS_PROFILE_ANNOTATION annotations on methods can not specify parcelable wrappers"; + private static final String METHOD_CLASSNAME_ERROR = + "@CROSS_PROFILE_ANNOTATION annotations on methods can not specify a profile class name"; + private static final String INVALID_TIMEOUT_MILLIS = "timeoutMillis must be positive"; + private static final String ADDITIONAL_PROFILE_CONNECTOR_METHODS_ERROR = + "Interfaces annotated with @GeneratedProfileConnector can not declare non-static methods"; + private static final String ADDITIONAL_USER_CONNECTOR_METHODS_ERROR = + "Interfaces annotated with @GeneratedUserConnector can not declare non-static methods"; + private static final String NOT_A_CONFIGURATION_ERROR = + "Configurations referenced in a @CROSS_PROFILE_TEST_ANNOTATION annotation must be annotated" + + " @CROSS_PROFILE_CONFIGURATION_ANNOTATION or @CROSS_PROFILE_CONFIGURATIONS_ANNOTATION"; + private static final String ASYNC_DECLARED_EXCEPTION_ERROR = + "Asynchronous methods annotated @CROSS_PROFILE_ANNOTATION cannot declare exceptions"; + private static final String NOT_PARCELABLE_ERROR = + "Classes annotated @CustomParcelableWrapper must implement Parcelable"; + private static final String INCORRECT_OF_METHOD = + "Classes annotated @CustomParcelableWrapper must have a static 'of' method which takes a" + + " Bundler, a BundlerType, and an instance of the wrapped type as arguments and returns" + + " an instance of the parcelable wrapper"; + private static final String INCORRECT_GET_METHOD = + "Classes annotated @CustomParcelableWrapper must have a static 'get' method which takes no" + + " arguments and returns an instance of the wrapped class"; + private static final String INCORRECT_PARCELABLE_IMPLEMENTATION = + "Classes annotated @CustomParcelableWrapper must correctly implement Parcelable"; + private static final String PARCELABLE_WRAPPER_ANNOTATION_ERROR = + "Parcelable Wrappers must be annotated @CustomParcelableWrapper"; + private static final String DOES_NOT_EXTEND_FUTURE_WRAPPER_ERROR = + "Classes annotated @CustomFutureWrapper must extend FutureWrapper"; + private static final String INCORRECT_CREATE_METHOD_ERROR = + "Classes annotated @CustomFutureWrapper must have a create method which returns an instance" + + " of the class and takes a Bundler and BundlerType argument"; + private static final String INCORRECT_GET_FUTURE_METHOD_ERROR = + "Classes annotated @CustomFutureWrapper must have a getFuture method which returns an" + + " instance of the wrapped future and takes no arguments"; + private static final String INCORRECT_RESOLVE_CALLBACK_WHEN_FUTURE_IS_SET_METHOD_ERROR = + "Classes annotated @CustomFutureWrapper must have a writeFutureResult method" + + " which returns void and takes as arguments an instance of the wrapped future and a" + + " FutureResultWriter"; + private static final String INCORRECT_GROUP_RESULTS_METHOD_ERROR = + "Classes annotated @CustomFutureWrapper must have a groupResults method which returns an" + + " instance of the wrapped future containing a map from Profile to the wrapped future" + + " type, and takes as an argument a map from Profile to an instance of the wrapped" + + " future"; + private static final String FUTURE_WRAPPER_ANNOTATION_ERROR = + "Future Wrappers must be annotated @CustomFutureWrapper"; + private static final String IMPORTS_NOT_PROFILE_CONNECTOR_ERROR = + "Classes included in includes= must be annotated @CustomProfileConnector"; + private static final String IMPORTS_NOT_USER_CONNECTOR_ERROR = + "Classes included in includes= must be annotated @CustomUserConnector"; + private static final String MUST_HAVE_ONE_TYPE_PARAMETER_ERROR = + "Classes annotated @CustomFutureWrapper must have a single type parameter"; + private static final String NOT_STATIC_ERROR = + "Types annotated @CROSS_PROFILE_ANNOTATION(isStatic=true) must not contain any non-static" + + " methods annotated @CROSS_PROFILE_ANNOTATION"; + private static final String METHOD_STATICTYPES_ERROR = + "@CROSS_PROFILE_PROVIDER_ANNOTATION annotations on methods can not specify staticTypes"; + + private final ValidatorContext validatorContext; + private final TypeMirror contextType; + private final TypeMirror profileConnectorType; + private final TypeMirror userConnectorType; + private final TypeMirror parcelableType; + private final TypeMirror bundlerType; + private final TypeMirror bundlerTypeType; + private final TypeMirror futureResultWriterType; + private final TypeMirror profileType; + + EarlyValidator(ValidatorContext validatorContext) { + this.validatorContext = validatorContext; + contextType = validatorContext.elements().getTypeElement("android.content.Context").asType(); + + parcelableType = validatorContext.elements().getTypeElement("android.os.Parcelable").asType(); + + profileConnectorType = + validatorContext + .elements() + .getTypeElement("com.google.android.enterprise.connectedapps.ProfileConnector") + .asType(); + + userConnectorType = + validatorContext + .elements() + .getTypeElement("com.google.android.enterprise.connectedapps.UserConnector") + .asType(); + + bundlerType = + validatorContext + .elements() + .getTypeElement("com.google.android.enterprise.connectedapps.internal.Bundler") + .asType(); + + bundlerTypeType = + validatorContext + .elements() + .getTypeElement("com.google.android.enterprise.connectedapps.internal.BundlerType") + .asType(); + + futureResultWriterType = + validatorContext + .elements() + .getTypeElement( + "com.google.android.enterprise.connectedapps.internal.FutureResultWriter") + .asType(); + + profileType = + validatorContext + .elements() + .getTypeElement("com.google.android.enterprise.connectedapps.Profile") + .asType(); + } + + /** + * Validate code. + * + * <p>This will show errors for all issues found. It will not terminate upon finding the first + * error. + * + * @return True if the code is valid + */ + boolean validate() { + + return Stream.of( + validateProfileConnectorInterfaces(validatorContext.newProfileConnectorInterfaces()), + validateUserConnectorInterfaces(validatorContext.newUserConnectorInterfaces()), + validateGeneratedProfileConnectors(validatorContext.newGeneratedProfileConnectors()), + validateGeneratedUserConnectors(validatorContext.newGeneratedUserConnectors()), + validateConfigurations(validatorContext.newConfigurations()), + validateCrossProfileTypes(validatorContext.newCrossProfileTypes()), + validateProviderMethods(validatorContext.newProviderMethods()), + validateProviderClasses(validatorContext.newProviderClasses()), + validateCrossProfileCallbackInterfaces( + validatorContext.newCrossProfileCallbackInterfaces()), + validateCrossProfileTests(validatorContext.newCrossProfileTests()), + validateCustomParcelableWrappers(validatorContext.newCustomParcelableWrappers()), + validateCustomFutureWrappers(validatorContext.newCustomFutureWrappers())) + .allMatch(b -> b); + } + + private boolean validateProfileConnectorInterfaces( + Collection<ProfileConnectorInfo> connectorInterfaces) { + boolean isValid = true; + + for (ProfileConnectorInfo connectorInterface : connectorInterfaces) { + isValid = validateProfileConnectorInterface(connectorInterface) && isValid; + } + + return isValid; + } + + private boolean validateProfileConnectorInterface(ProfileConnectorInfo connectorInterface) { + boolean isValid = true; + + if (!connectorInterface.connectorElement().getKind().equals(ElementKind.INTERFACE)) { + showError(CUSTOM_PROFILE_CONNECTOR_MUST_BE_INTERFACE, connectorInterface.connectorElement()); + isValid = false; + } + + if (!implementsInterface(connectorInterface.connectorElement(), profileConnectorType)) { + showError( + CUSTOM_PROFILE_CONNECTOR_MUST_EXTEND_CONNECTOR, connectorInterface.connectorElement()); + isValid = false; + } + + for (TypeElement parcelableWrapper : connectorInterface.parcelableWrapperClasses()) { + if (parcelableWrapper.getAnnotation(CustomParcelableWrapper.class) == null) { + showError(PARCELABLE_WRAPPER_ANNOTATION_ERROR, connectorInterface.connectorElement()); + } + } + + for (TypeElement futureWrapper : connectorInterface.futureWrapperClasses()) { + if (futureWrapper.getAnnotation(CustomFutureWrapper.class) == null) { + isValid = false; + showError(FUTURE_WRAPPER_ANNOTATION_ERROR, connectorInterface.connectorElement()); + } + } + + for (TypeElement importer : connectorInterface.importsClasses()) { + if (importer.getAnnotation(CustomProfileConnector.class) == null) { + isValid = false; + showError(IMPORTS_NOT_PROFILE_CONNECTOR_ERROR, connectorInterface.connectorElement()); + showError(IMPORTS_NOT_PROFILE_CONNECTOR_ERROR, connectorInterface.connectorElement()); + } + } + + return isValid; + } + + private boolean validateUserConnectorInterfaces( + Collection<UserConnectorInfo> connectorInterfaces) { + boolean isValid = true; + + for (UserConnectorInfo connectorInterface : connectorInterfaces) { + isValid = validateUserConnectorInterface(connectorInterface) && isValid; + } + + return isValid; + } + + private boolean validateUserConnectorInterface(UserConnectorInfo connectorInterface) { + boolean isValid = true; + + if (!connectorInterface.connectorElement().getKind().equals(ElementKind.INTERFACE)) { + showError(CUSTOM_USER_CONNECTOR_MUST_BE_INTERFACE, connectorInterface.connectorElement()); + isValid = false; + } + + if (!implementsInterface(connectorInterface.connectorElement(), userConnectorType)) { + showError(CUSTOM_USER_CONNECTOR_MUST_EXTEND_CONNECTOR, connectorInterface.connectorElement()); + isValid = false; + } + + for (TypeElement parcelableWrapper : connectorInterface.parcelableWrapperClasses()) { + if (parcelableWrapper.getAnnotation(CustomParcelableWrapper.class) == null) { + showError(PARCELABLE_WRAPPER_ANNOTATION_ERROR, connectorInterface.connectorElement()); + } + } + + for (TypeElement futureWrapper : connectorInterface.futureWrapperClasses()) { + if (futureWrapper.getAnnotation(CustomFutureWrapper.class) == null) { + isValid = false; + showError(FUTURE_WRAPPER_ANNOTATION_ERROR, connectorInterface.connectorElement()); + } + } + + for (TypeElement importer : connectorInterface.importsClasses()) { + if (importer.getAnnotation(CustomUserConnector.class) == null) { + isValid = false; + showError(IMPORTS_NOT_USER_CONNECTOR_ERROR, connectorInterface.connectorElement()); + } + } + + return isValid; + } + + private boolean validateGeneratedProfileConnectors(Collection<TypeElement> generatedConnectors) { + boolean isValid = true; + + for (TypeElement generatedConnector : generatedConnectors) { + isValid = validateGeneratedProfileConnector(generatedConnector) && isValid; + } + + return isValid; + } + + private boolean validateGeneratedProfileConnector(TypeElement generatedConnector) { + boolean isValid = true; + + if (!generatedConnector.getKind().equals(ElementKind.INTERFACE)) { + showError(GENERATED_PROFILE_CONNECTOR_MUST_BE_INTERFACE, generatedConnector); + isValid = false; + } + + if (!implementsInterface(generatedConnector, profileConnectorType)) { + showError(GENERATED_PROFILE_CONNECTOR_MUST_EXTEND_PROFILE_CONNECTOR, generatedConnector); + isValid = false; + } + + if (generatedConnector.getEnclosedElements().stream() + .anyMatch(i -> !i.getModifiers().contains(Modifier.STATIC))) { + showError(ADDITIONAL_PROFILE_CONNECTOR_METHODS_ERROR, generatedConnector); + isValid = false; + } + + return isValid; + } + + private boolean validateGeneratedUserConnectors(Collection<TypeElement> generatedConnectors) { + boolean isValid = true; + + for (TypeElement generatedConnector : generatedConnectors) { + isValid = validateGeneratedUserConnector(generatedConnector) && isValid; + } + + return isValid; + } + + private boolean validateGeneratedUserConnector(TypeElement generatedConnector) { + boolean isValid = true; + + if (!generatedConnector.getKind().equals(ElementKind.INTERFACE)) { + showError(GENERATED_USER_CONNECTOR_MUST_BE_INTERFACE, generatedConnector); + isValid = false; + } + + if (!implementsInterface(generatedConnector, userConnectorType)) { + showError(GENERATED_USER_CONNECTOR_MUST_EXTEND_USER_CONNECTOR, generatedConnector); + isValid = false; + } + + if (generatedConnector.getEnclosedElements().stream() + .anyMatch(i -> !i.getModifiers().contains(Modifier.STATIC))) { + showError(ADDITIONAL_USER_CONNECTOR_METHODS_ERROR, generatedConnector); + isValid = false; + } + + return isValid; + } + + private boolean implementsInterface(TypeElement type, TypeMirror interfaceType) { + for (TypeMirror t : type.getInterfaces()) { + if (validatorContext.types().isSameType(t, interfaceType)) { + return true; + } + } + return false; + } + + private boolean validateConfigurations( + Collection<ValidatorCrossProfileConfigurationInfo> configurations) { + boolean isValid = true; + + for (ValidatorCrossProfileConfigurationInfo configuration : configurations) { + isValid = validateConfiguration(configuration) && isValid; + } + + return isValid; + } + + private boolean validateConfiguration(ValidatorCrossProfileConfigurationInfo configuration) { + boolean isValid = true; + + for (TypeElement providerClass : configuration.providerClassElements()) { + if (!hasCrossProfileProviderAnnotation(providerClass) + && GeneratorUtilities.findCrossProfileProviderMethodsInClass(providerClass).isEmpty()) { + showError(NOT_A_PROVIDER_CLASS_ERROR, configuration.configurationElement()); + isValid = false; + } + } + + if (configuration.connector().isPresent() + && !configuration.connector().get().getKind().equals(ElementKind.INTERFACE)) { + showError(CONNECTOR_MUST_BE_INTERFACE, configuration.configurationElement()); + isValid = false; + } + + if (configuration.connector().isPresent() + && !implementsInterface(configuration.connector().get(), profileConnectorType)) { + showError(CONNECTOR_MUST_EXTEND_CONNECTOR, configuration.configurationElement()); + isValid = false; + } + + return isValid; + } + + private boolean validateCrossProfileTypes( + Collection<ValidatorCrossProfileTypeInfo> crossProfileTypes) { + boolean isValid = + validateCrossProfileTypesAreProvided( + crossProfileTypes.stream() + .map(ValidatorCrossProfileTypeInfo::crossProfileTypeElement) + .collect(toSet()), + validatorContext.newProviderMethods(), + validatorContext.newProviderClasses()); + + for (ValidatorCrossProfileTypeInfo crossProfileType : crossProfileTypes) { + isValid = validateCrossProfileType(crossProfileType) && isValid; + } + + return isValid; + } + + private boolean validateCrossProfileType(ValidatorCrossProfileTypeInfo crossProfileType) { + boolean isValid = true; + + PackageElement packageElement = + (PackageElement) crossProfileType.crossProfileTypeElement().getEnclosingElement(); + if (packageElement.getQualifiedName().toString().isEmpty()) { + showError( + CROSS_PROFILE_TYPE_DEFAULT_PACKAGE_ERROR, + crossProfileType.crossProfileTypeElement(), + validationMessageFormatterForClass(crossProfileType.crossProfileTypeElement())); + isValid = false; + } + + if (!crossProfileType.crossProfileTypeElement().getModifiers().contains(Modifier.PUBLIC)) { + showError( + NON_PUBLIC_CROSS_PROFILE_TYPE_ERROR, + crossProfileType.crossProfileTypeElement(), + validationMessageFormatterForClass(crossProfileType.crossProfileTypeElement())); + isValid = false; + } + + if (crossProfileType.isStatic()) { + for (ExecutableElement crossProfileMethod : crossProfileType.crossProfileMethods()) { + if (!crossProfileMethod.getModifiers().contains(Modifier.STATIC)) { + showError(NOT_STATIC_ERROR, crossProfileMethod); + isValid = false; + } + } + } + + if (crossProfileType.profileConnector().isPresent() + && !crossProfileType + .profileConnector() + .get() + .connectorElement() + .getKind() + .equals(ElementKind.INTERFACE)) { + showError(CONNECTOR_MUST_BE_INTERFACE, crossProfileType.crossProfileTypeElement()); + isValid = false; + } + + if (crossProfileType.profileConnector().isPresent() + && !implementsInterface( + crossProfileType.profileConnector().get().connectorElement(), profileConnectorType)) { + showError(CONNECTOR_MUST_EXTEND_CONNECTOR, crossProfileType.crossProfileTypeElement()); + isValid = false; + } + + if (crossProfileType.timeoutMillis() <= 0) { + showError(INVALID_TIMEOUT_MILLIS, crossProfileType.crossProfileTypeElement()); + isValid = false; + } + + for (TypeElement parcelableWrapper : crossProfileType.parcelableWrapperClasses()) { + if (parcelableWrapper.getAnnotation(CustomParcelableWrapper.class) == null) { + showError(PARCELABLE_WRAPPER_ANNOTATION_ERROR, crossProfileType.crossProfileTypeElement()); + } + } + + for (TypeElement futureWrapper : crossProfileType.futureWrapperClasses()) { + if (futureWrapper.getAnnotation(CustomFutureWrapper.class) == null) { + isValid = false; + showError(FUTURE_WRAPPER_ANNOTATION_ERROR, crossProfileType.crossProfileTypeElement()); + } + } + + isValid = + crossProfileType.crossProfileMethods().stream() + .map(m -> validateCrossProfileMethod(crossProfileType, m)) + .allMatch(b -> b) + && isValid; + + return isValid; + } + + private boolean validateCrossProfileTypesAreProvided( + Collection<TypeElement> crossProfileTypeElements, + Collection<ExecutableElement> providerMethods, + Collection<ValidatorProviderClassInfo> providerClasses) { + Map<String, Collection<Element>> crossProfileTypeProviders = + crossProfileTypeElements.stream() + .collect(toMap(element -> element.asType().toString(), element -> new HashSet<>())); + + for (ExecutableElement provider : providerMethods) { + String providedTypeName = provider.getReturnType().toString(); + + if (crossProfileTypeProviders.containsKey(providedTypeName)) { + crossProfileTypeProviders.get(providedTypeName).add(provider); + } + } + + for (ValidatorProviderClassInfo provider : providerClasses) { + for (TypeElement staticType : provider.staticTypes()) { + String providedTypeName = staticType.getQualifiedName().toString(); + + if (crossProfileTypeProviders.containsKey(providedTypeName)) { + crossProfileTypeProviders.get(providedTypeName).add(provider.providerClassElement()); + } + } + } + + boolean isValid = true; + + for (String crossProfileType : crossProfileTypeProviders.keySet()) { + Collection<Element> providers = crossProfileTypeProviders.get(crossProfileType); + + if (providers.size() > 1) { + isValid = false; + for (Element providerElement : providers) { + showError(String.format(MULTIPLE_PROVIDERS_ERROR, crossProfileType), providerElement); + } + } + } + + return isValid; + } + + private boolean validateProviderMethods(Collection<ExecutableElement> providerMethods) { + boolean isValid = true; + + for (ExecutableElement providerMethod : providerMethods) { + TypeElement crossProfileType = + validatorContext.elements().getTypeElement(providerMethod.getReturnType().toString()); + if (!hasCrossProfileAnnotation(crossProfileType) + && findCrossProfileMethodsInClass(crossProfileType).isEmpty()) { + showError(PROVIDING_NON_CROSS_PROFILE_TYPE_ERROR, providerMethod); + isValid = false; + } + + if (providerMethod.getParameters().stream() + .anyMatch(v -> !validatorContext.types().isSameType(v.asType(), contextType)) + || providerMethod.getParameters().size() > 1) { + showError(PROVIDER_INCORRECT_ARGS_ERROR, providerMethod); + isValid = false; + } + + if (providerMethod.getModifiers().contains(Modifier.STATIC)) { + showError(STATIC_PROVIDER_ERROR, providerMethod); + isValid = false; + } + + CrossProfileProviderAnnotationInfo annotationInfo = + AnnotationFinder.extractCrossProfileProviderAnnotationInfo( + providerMethod, validatorContext.types(), validatorContext.elements()); + + if (!annotationInfo.staticTypes().isEmpty()) { + showError(METHOD_STATICTYPES_ERROR, providerMethod); + isValid = false; + } + } + + return isValid; + } + + private boolean validateProviderClasses(Collection<ValidatorProviderClassInfo> providerClasses) { + boolean isValid = true; + + for (ValidatorProviderClassInfo provider : providerClasses) { + if (!hasValidProviderClassConstructor(provider.providerClassElement())) { + showError(INVALID_CONSTRUCTORS_ERROR, provider.providerClassElement()); + isValid = false; + } + + if (provider.providerClassElement().getEnclosedElements().stream() + .filter(e -> e instanceof ExecutableElement) + .map(e -> (ExecutableElement) e) + .filter(e -> e.getKind().equals(ElementKind.CONSTRUCTOR)) + .filter(e -> e.getModifiers().contains(Modifier.PUBLIC)) + .count() + > 1) { + showError(INVALID_CONSTRUCTORS_ERROR, provider.providerClassElement()); + isValid = false; + } + } + + return isValid; + } + + private boolean hasValidProviderClassConstructor(TypeElement clazz) { + for (ExecutableElement constructor : + ElementFilter.constructorsIn(clazz.getEnclosedElements())) { + if (constructor.getModifiers().contains(Modifier.PUBLIC)) { + if (isValidProviderClassConstructor(constructor)) { + return true; + } + } + } + return false; + } + + private boolean isValidProviderClassConstructor(ExecutableElement constructor) { + if (constructor.getParameters().size() == 0) { + return true; + } + + if (constructor.getParameters().size() > 1) { + return false; + } + + return validatorContext + .types() + .isSameType(constructor.getParameters().iterator().next().asType(), contextType); + } + + private boolean validateCrossProfileMethod( + ValidatorCrossProfileTypeInfo crossProfileType, ExecutableElement crossProfileMethod) { + boolean isValid = true; + + CrossProfileAnnotationInfo crossProfileAnnotation = + AnnotationFinder.extractCrossProfileAnnotationInfo( + crossProfileMethod, validatorContext.types(), validatorContext.elements()); + + if (!crossProfileAnnotation.connectorIsDefault()) { + showError(METHOD_CONNECTOR_ERROR, crossProfileMethod); + isValid = false; + } + + if (!crossProfileAnnotation.parcelableWrapperClasses().isEmpty()) { + showError(METHOD_PARCELABLE_WRAPPERS_ERROR, crossProfileMethod); + isValid = false; + } + + if (!crossProfileAnnotation.isProfileClassNameDefault()) { + showError(METHOD_CLASSNAME_ERROR, crossProfileMethod); + isValid = false; + } + + if (crossProfileAnnotation.timeoutMillis().isPresent() + && crossProfileAnnotation.timeoutMillis().get() <= 0) { + showError(INVALID_TIMEOUT_MILLIS, crossProfileMethod); + isValid = false; + } + + if (!crossProfileMethod.getThrownTypes().isEmpty()) { + if (CrossProfileMethodInfo.isFuture(crossProfileType.supportedTypes(), crossProfileMethod) + || CrossProfileMethodInfo.getCrossProfileCallbackParam( + validatorContext.elements(), crossProfileMethod) + .isPresent()) { + showError(ASYNC_DECLARED_EXCEPTION_ERROR, crossProfileMethod); + isValid = false; + } + } + + if (crossProfileAnnotation.isStatic()) { + showError(METHOD_ISSTATIC_ERROR, crossProfileMethod); + isValid = false; + } + + isValid = + isValid + && validateReturnType(crossProfileType, crossProfileMethod) + && validateParameterTypesForCrossProfileMethod(crossProfileType, crossProfileMethod); + return isValid; + } + + private boolean validateReturnType( + ValidatorCrossProfileTypeInfo crossProfileType, ExecutableElement crossProfileMethod) { + TypeMirror returnType = crossProfileMethod.getReturnType(); + + if (crossProfileType.supportedTypes().isValidReturnType(returnType)) { + return true; + } + + showError(String.format(UNSUPPORTED_RETURN_TYPE_ERROR, returnType), crossProfileMethod); + return false; + } + + private boolean validateParameterTypesForCrossProfileMethod( + ValidatorCrossProfileTypeInfo crossProfileType, ExecutableElement crossProfileMethod) { + boolean isValid = + crossProfileMethod.getParameters().stream() + .allMatch(p -> validateParameterTypeForCrossProfileMethod(crossProfileType, p)); + + List<TypeElement> crossProfileCallbackParameters = + crossProfileMethod.getParameters().stream() + .map(v -> validatorContext.elements().getTypeElement(v.asType().toString())) + .filter(Objects::nonNull) + .filter(AnnotationFinder::hasCrossProfileCallbackAnnotation) + .collect(Collectors.toList()); + + if (crossProfileCallbackParameters.size() > 1) { + isValid = false; + showError(MULTIPLE_ASYNC_CALLBACK_PARAMETERS_ERROR, crossProfileMethod); + } + + if (crossProfileCallbackParameters.size() == 1) { + if (!crossProfileMethod.getReturnType().getKind().equals(TypeKind.VOID)) { + isValid = false; + showError(NON_VOID_CALLBACK_ERROR, crossProfileMethod); + } + + isValid = + validateParameterTypesForCrossProfileCallbackInterface( + crossProfileType, crossProfileCallbackParameters.get(0)) + && isValid; + } + + if (!crossProfileCallbackParameters.isEmpty() + && !crossProfileMethod.getReturnType().getKind().equals(TypeKind.VOID)) { + isValid = false; + showError(NON_VOID_CALLBACK_ERROR, crossProfileMethod); + } + + return isValid; + } + + private boolean validateParameterTypeForCrossProfileCallbackInterface( + ValidatorCrossProfileTypeInfo crossProfileType, VariableElement parameter) { + TypeMirror parameterType = parameter.asType(); + + if (crossProfileType + .supportedTypes() + .isValidParameterType( + parameterType, TypeCheckContext.createForCrossProfileCallbackInterface())) { + return true; + } + + showError( + String.format(UNSUPPORTED_PARAMETER_TYPE_CROSS_ASYNC_CALLBACK, parameterType), + parameter, + validationMessageFormatterFor(crossProfileType.crossProfileMethods().get(0))); + return false; + } + + private boolean validateParameterTypesForCrossProfileCallbackInterface( + ValidatorCrossProfileTypeInfo crossProfileType, TypeElement crossProfileCallbackInterface) { + return crossProfileCallbackInterface.getEnclosedElements().stream() + .filter(m -> m instanceof ExecutableElement) + .map(m -> (ExecutableElement) m) + .map(m -> validateParameterTypesForCrossProfileCallbackInterface(crossProfileType, m)) + .allMatch(b -> b); + } + + private boolean validateParameterTypesForCrossProfileCallbackInterface( + ValidatorCrossProfileTypeInfo crossProfileType, ExecutableElement method) { + return method.getParameters().stream() + .allMatch(m -> validateParameterTypeForCrossProfileCallbackInterface(crossProfileType, m)); + } + + private boolean validateParameterTypeForCrossProfileMethod( + ValidatorCrossProfileTypeInfo crossProfileType, VariableElement parameter) { + TypeMirror parameterType = parameter.asType(); + + if (crossProfileType.supportedTypes().isValidParameterType(parameterType)) { + return true; + } + + showError( + String.format(UNSUPPORTED_PARAMETER_TYPE_CROSS_PROFILE_METHOD, parameterType), + parameter, + validationMessageFormatterFor(crossProfileType.crossProfileMethods().get(0))); + return false; + } + + private boolean validateCrossProfileCallbackInterfaces( + Collection<TypeElement> crossProfileCallbackInterfaces) { + return crossProfileCallbackInterfaces.stream() + .allMatch(this::validateCrossProfileCallbackInterface); + } + + private boolean validateCrossProfileCallbackInterface(TypeElement crossProfileCallbackInterface) { + boolean isValid = true; + + CrossProfileCallbackAnnotationInfo annotationInfo = + AnnotationFinder.extractCrossProfileCallbackAnnotationInfo( + crossProfileCallbackInterface, validatorContext.types(), validatorContext.elements()); + + PackageElement packageElement = + (PackageElement) crossProfileCallbackInterface.getEnclosingElement(); + if (packageElement.getQualifiedName().toString().isEmpty()) { + showError(CALLBACK_INTERFACE_DEFAULT_PACKAGE_ERROR, crossProfileCallbackInterface); + isValid = false; + } + + if (crossProfileCallbackInterface.getKind() != ElementKind.INTERFACE) { + showError(NOT_INTERFACE_ERROR, crossProfileCallbackInterface); + isValid = false; + } + + if (!crossProfileCallbackInterface.getTypeParameters().isEmpty()) { + showError(GENERIC_CALLBACK_INTERFACE_ERROR, crossProfileCallbackInterface); + isValid = false; + } + + Collection<ExecutableElement> methods = getMethods(crossProfileCallbackInterface); + + if (methods.isEmpty()) { + showError(NO_METHODS_ERROR, crossProfileCallbackInterface); + isValid = false; + } + + if (annotationInfo.simple() && methods.size() > 1) { + showError(NOT_ONE_METHOD_ERROR, crossProfileCallbackInterface); + isValid = false; + } + + isValid = + methods.stream() + .allMatch( + (method) -> + validateMethodOnCrossProfileCallbackInterface( + annotationInfo, method, crossProfileCallbackInterface)) + && isValid; + + return isValid; + } + + private boolean validateMethodOnCrossProfileCallbackInterface( + CrossProfileCallbackAnnotationInfo annotationInfo, + ExecutableElement method, + TypeElement crossProfileCallbackInterface) { + boolean isValid = true; + + if (method.isDefault()) { + showError( + DEFAULT_METHOD_ERROR, + method, + validationMessageFormatterFor(crossProfileCallbackInterface)); + isValid = false; + } + + if (method.getModifiers().contains(Modifier.STATIC)) { + showError( + STATIC_METHOD_ERROR, + method, + validationMessageFormatterFor(crossProfileCallbackInterface)); + isValid = false; + } + + if (!method.getReturnType().getKind().equals(TypeKind.VOID)) { + showError( + NOT_VOID_ERROR, method, validationMessageFormatterFor(crossProfileCallbackInterface)); + isValid = false; + } + + if (annotationInfo.simple() && method.getParameters().size() > 1) { + showError( + MORE_THAN_ONE_PARAMETER_ERROR, + method, + validationMessageFormatterFor(crossProfileCallbackInterface)); + isValid = false; + } + + return isValid; + } + + private boolean validateCrossProfileTests( + Collection<ValidatorCrossProfileTestInfo> crossProfileTests) { + return crossProfileTests.stream().allMatch(this::validateCrossProfileTest); + } + + private boolean validateCrossProfileTest(ValidatorCrossProfileTestInfo crossProfileTest) { + boolean isValid = true; + + if (!hasCrossProfileConfigurationAnnotation(crossProfileTest.configurationElement()) + && !hasCrossProfileConfigurationsAnnotation(crossProfileTest.configurationElement())) { + showError(NOT_A_CONFIGURATION_ERROR, crossProfileTest.crossProfileTestElement()); + isValid = false; + } + return isValid; + } + + private boolean validateCustomParcelableWrappers( + Collection<TypeElement> customParcelableWrappers) { + return customParcelableWrappers.stream().allMatch(this::validateCustomParcelableWrapper); + } + + private boolean validateCustomParcelableWrapper(TypeElement customParcelableWrapper) { + boolean isValid = true; + if (!validatorContext.types().isAssignable(customParcelableWrapper.asType(), parcelableType)) { + showError(NOT_PARCELABLE_ERROR, customParcelableWrapper); + isValid = false; + } + + ClassName parcelableWrapperRawType = + TypeUtils.getRawTypeClassName(customParcelableWrapper.asType()); + ClassName wrappedParamRawType = + TypeUtils.getRawTypeClassName( + ParcelableWrapperAnnotationInfo.extractFromParcelableWrapperAnnotation( + validatorContext.types(), + customParcelableWrapper.getAnnotation(CustomParcelableWrapper.class)) + .originalType() + .asType()); + + Optional<ExecutableElement> ofMethod = + customParcelableWrapper.getEnclosedElements().stream() + .filter(p -> p.getKind().equals(ElementKind.METHOD)) + .map(p -> (ExecutableElement) p) + .filter(p -> p.getSimpleName().contentEquals("of")) + // We drop generics as without being overly prescriptive it's impossible to know that + // the method is returning the correct generic type + .filter( + p -> + TypeUtils.getRawTypeClassName(p.getReturnType()) + .equals(TypeUtils.getRawTypeClassName(customParcelableWrapper.asType()))) + .filter(p -> ofMethodHasExpectedArguments(wrappedParamRawType, p)) + .findFirst(); + + if (!ofMethod.isPresent()) { + showError(INCORRECT_OF_METHOD, customParcelableWrapper); + isValid = false; + } + + Optional<ExecutableElement> getMethod = + customParcelableWrapper.getEnclosedElements().stream() + .filter(p -> p.getKind().equals(ElementKind.METHOD)) + .map(p -> (ExecutableElement) p) + .filter(p -> p.getSimpleName().contentEquals("get")) + // We drop generics as without being overly prescriptive it's impossible to know that + // the method is returning the correct generic type + .filter( + p -> TypeUtils.getRawTypeClassName(p.getReturnType()).equals(wrappedParamRawType)) + .findFirst(); + + if (!getMethod.isPresent()) { + showError(INCORRECT_GET_METHOD, customParcelableWrapper); + isValid = false; + } + + TypeName creatorType = + ParameterizedTypeName.get(PARCELABLE_CREATOR_CLASSNAME, parcelableWrapperRawType); + + Optional<VariableElement> creator = + customParcelableWrapper.getEnclosedElements().stream() + .filter(p -> p.getKind().equals(ElementKind.FIELD)) + .map(p -> (VariableElement) p) + .filter(p -> p.getSimpleName().contentEquals("CREATOR")) + .filter( + p -> + p.getModifiers() + .containsAll( + Arrays.asList(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC))) + .filter(p -> ClassName.get(p.asType()).equals(creatorType)) + .findFirst(); + + if (!creator.isPresent()) { + showError(INCORRECT_PARCELABLE_IMPLEMENTATION, customParcelableWrapper); + isValid = false; + } + + return isValid; + } + + private boolean ofMethodHasExpectedArguments( + ClassName wrappedParamRawType, ExecutableElement ofMethod) { + List<? extends VariableElement> parameters = ofMethod.getParameters(); + if (parameters.size() != 3) { + return false; + } + + if (!validatorContext.types().isSameType(parameters.get(0).asType(), bundlerType)) { + return false; + } + + if (!validatorContext.types().isSameType(parameters.get(1).asType(), bundlerTypeType)) { + return false; + } + + if (!TypeUtils.getRawTypeClassName(parameters.get(2).asType()).equals(wrappedParamRawType)) { + return false; + } + + return true; + } + + private boolean validateCustomFutureWrappers(Collection<TypeElement> futureWrappers) { + return futureWrappers.stream().map(this::validateCustomFutureWrapper).allMatch(b -> b); + } + + private boolean validateCustomFutureWrapper(TypeElement futureWrapper) { + boolean isValid = true; + + ClassName wrappedFutureRawType = + TypeUtils.getRawTypeClassName( + FutureWrapperAnnotationInfo.extractFromFutureWrapperAnnotation( + validatorContext.types(), + futureWrapper.getAnnotation(CustomFutureWrapper.class)) + .originalType() + .asType()); + + if (!TypeUtils.getRawTypeQualifiedName(futureWrapper.getSuperclass()) + .equals("com.google.android.enterprise.connectedapps.FutureWrapper")) { + showError(DOES_NOT_EXTEND_FUTURE_WRAPPER_ERROR, futureWrapper); + isValid = false; + } + + if (futureWrapper.getTypeParameters().size() != 1) { + showError(MUST_HAVE_ONE_TYPE_PARAMETER_ERROR, futureWrapper); + isValid = false; + } + + Optional<ExecutableElement> createMethod = + futureWrapper.getEnclosedElements().stream() + .filter(e -> e instanceof ExecutableElement) + .map(e -> (ExecutableElement) e) + .filter(e -> e.getSimpleName().contentEquals("create")) + .filter( + e -> e.getModifiers().containsAll(Arrays.asList(Modifier.PUBLIC, Modifier.STATIC))) + // We drop generics as without being overly prescriptive it's impossible to know that + // the method is returning the correct generic type + .filter( + e -> + TypeUtils.getRawTypeClassName(e.getReturnType()) + .equals(TypeUtils.getRawTypeClassName(futureWrapper.asType()))) + .filter(this::createMethodHasExpectedArguments) + .findFirst(); + + if (!createMethod.isPresent()) { + showError(INCORRECT_CREATE_METHOD_ERROR, futureWrapper); + isValid = false; + } + + Optional<ExecutableElement> getFutureMethod = + futureWrapper.getEnclosedElements().stream() + .filter(e -> e instanceof ExecutableElement) + .map(e -> (ExecutableElement) e) + .filter(e -> e.getSimpleName().contentEquals("getFuture")) + .filter(e -> e.getModifiers().contains(Modifier.PUBLIC)) + .filter(e -> !e.getModifiers().contains(Modifier.STATIC)) + // We drop generics as without being overly prescriptive it's impossible to know that + // the method is returning the correct generic type + .filter( + e -> TypeUtils.getRawTypeClassName(e.getReturnType()).equals(wrappedFutureRawType)) + .filter(e -> e.getParameters().isEmpty()) + .findFirst(); + + if (!getFutureMethod.isPresent()) { + showError(INCORRECT_GET_FUTURE_METHOD_ERROR, futureWrapper); + isValid = false; + } + + Optional<ExecutableElement> writeFutureResultMethod = + futureWrapper.getEnclosedElements().stream() + .filter(e -> e instanceof ExecutableElement) + .map(e -> (ExecutableElement) e) + .filter(e -> e.getSimpleName().contentEquals("writeFutureResult")) + .filter( + e -> e.getModifiers().containsAll(Arrays.asList(Modifier.PUBLIC, Modifier.STATIC))) + .filter(e -> e.getReturnType().toString().equals("void")) + .filter(e -> writeFutureResultMethodHasExpectedArguments(e, wrappedFutureRawType)) + .findFirst(); + + if (!writeFutureResultMethod.isPresent()) { + showError(INCORRECT_RESOLVE_CALLBACK_WHEN_FUTURE_IS_SET_METHOD_ERROR, futureWrapper); + isValid = false; + } + + Optional<ExecutableElement> groupResultsMethod = + futureWrapper.getEnclosedElements().stream() + .filter(e -> e instanceof ExecutableElement) + .map(e -> (ExecutableElement) e) + .filter(e -> e.getSimpleName().contentEquals("groupResults")) + .filter( + e -> e.getModifiers().containsAll(Arrays.asList(Modifier.PUBLIC, Modifier.STATIC))) + .filter(e -> groupResultsMethodHasExpectedReturnType(e, wrappedFutureRawType)) + .filter(e -> groupResultsMethodHasExpectedArguments(e, wrappedFutureRawType)) + .findFirst(); + + if (!groupResultsMethod.isPresent()) { + showError(INCORRECT_GROUP_RESULTS_METHOD_ERROR, futureWrapper); + isValid = false; + } + + return isValid; + } + + private boolean groupResultsMethodHasExpectedReturnType( + ExecutableElement groupResultsMethod, ClassName wrappedFutureRawType) { + + if (!TypeUtils.getRawTypeClassName(groupResultsMethod.getReturnType()) + .equals(wrappedFutureRawType)) { + return false; + } + + TypeMirror wrappedReturnType = + TypeUtils.extractTypeArguments(groupResultsMethod.getReturnType()).get(0); + + if (!TypeUtils.getRawTypeClassName(wrappedReturnType).equals(ClassName.get(Map.class))) { + return false; + } + + TypeMirror wrappedReturnTypeKey = TypeUtils.extractTypeArguments(wrappedReturnType).get(0); + + if (!validatorContext.types().isSameType(wrappedReturnTypeKey, profileType)) { + return false; + } + + return true; + } + + private boolean groupResultsMethodHasExpectedArguments( + ExecutableElement groupResultsMethod, ClassName wrappedFutureRawType) { + if (groupResultsMethod.getParameters().size() != 1) { + return false; + } + + TypeMirror param = groupResultsMethod.getParameters().get(0).asType(); + + if (!TypeUtils.getRawTypeClassName(param).equals(ClassName.get(Map.class))) { + return false; + } + + List<TypeMirror> params = TypeUtils.extractTypeArguments(param); + + TypeMirror keyParam = params.get(0); + TypeMirror valueParam = params.get(1); + + if (!validatorContext.types().isSameType(keyParam, profileType)) { + return false; + } + + if (!TypeUtils.getRawTypeClassName(valueParam).equals(wrappedFutureRawType)) { + return false; + } + + return true; + } + + private boolean createMethodHasExpectedArguments(ExecutableElement createMethod) { + if (createMethod.getParameters().size() != 2) { + return false; + } + + if (!validatorContext + .types() + .isSameType(createMethod.getParameters().get(0).asType(), bundlerType)) { + return false; + } + + if (!validatorContext + .types() + .isSameType(createMethod.getParameters().get(1).asType(), bundlerTypeType)) { + return false; + } + + return true; + } + + private boolean writeFutureResultMethodHasExpectedArguments( + ExecutableElement method, ClassName wrappedFutureRawType) { + if (method.getParameters().size() != 2) { + return false; + } + + if (!TypeUtils.getRawTypeClassName(method.getParameters().get(0).asType()) + .equals(wrappedFutureRawType)) { + return false; + } + + if (!validatorContext + .types() + .isAssignable( + TypeUtils.removeTypeArguments(method.getParameters().get(1).asType()), + futureResultWriterType)) { + return false; + } + + return true; + } + + private Collection<ExecutableElement> getMethods(TypeElement typeElement) { + return typeElement.getEnclosedElements().stream() + .filter(e -> e instanceof ExecutableElement) + .map(e -> (ExecutableElement) e) + .collect(toSet()); + } + + private void showError( + String errorText, + Element errorElement, + ValidationMessageFormatter validationMessageFormatter) { + showErrorPreformatted(validationMessageFormatter.format(errorText), errorElement); + } + + private void showError(String errorText, Element errorElement) { + showErrorPreformatted( + validationMessageFormatterFor(errorElement).format(errorText), errorElement); + } + + private void showErrorPreformatted(String errorText, Element errorElement) { + validatorContext + .processingEnv() + .getMessager() + .printMessage(Kind.ERROR, errorText, errorElement); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeCrossProfileTypeGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeCrossProfileTypeGenerator.java new file mode 100644 index 0000000..0abba95 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeCrossProfileTypeGenerator.java @@ -0,0 +1,477 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.ABSTRACT_FAKE_PROFILE_CONNECTOR_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_AWARE_UTILS_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CurrentProfileGenerator.getCurrentProfileClassName; +import static com.google.android.enterprise.connectedapps.processor.InterfaceGenerator.getMultipleSenderInterfaceClassName; +import static com.google.android.enterprise.connectedapps.processor.InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName; +import static com.google.android.enterprise.connectedapps.processor.InterfaceGenerator.getSingleSenderInterfaceClassName; +import static com.google.android.enterprise.connectedapps.processor.MultipleProfilesGenerator.getMultipleProfilesClassName; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector.ProfileType; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.squareup.javapoet.ArrayTypeName; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeSpec; +import java.util.HashMap; +import java.util.Map; +import javax.lang.model.element.Modifier; + +class FakeCrossProfileTypeGenerator { + private boolean generated = false; + private final CrossProfileTypeInfo crossProfileType; + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + + public FakeCrossProfileTypeGenerator( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.crossProfileType = checkNotNull(crossProfileType); + } + + void generate() { + if (generated) { + throw new IllegalStateException( + "FakeCrossProfileTypeGenerator#generate can only be called once"); + } + generated = true; + + generateFakeCrossProfileType(); + } + + private void generateFakeCrossProfileType() { + ClassName className = getFakeCrossProfileTypeClassName(generatorContext, crossProfileType); + ClassName builderClassName = + getFakeCrossProfileTypeBuilderClassName(generatorContext, crossProfileType); + ClassName crossProfileTypeInterfaceClassName = + InterfaceGenerator.getCrossProfileTypeInterfaceClassName( + generatorContext, crossProfileType); + ClassName fakeProfileConnectorClassName = + crossProfileType.profileConnector().isPresent() + ? FakeProfileConnectorGenerator.getFakeProfileConnectorClassName( + crossProfileType.profileConnector().get()) + : ABSTRACT_FAKE_PROFILE_CONNECTOR_CLASSNAME; + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addJavadoc( + "Fake implementation of {@link $T} for use during tests.\n\n" + + "<p>This should be injected into your code under test and the {@link $T}\n" + + "used to control the fake state. Calls will be routed to the correct {@link" + + " $T}.\n", + crossProfileTypeInterfaceClassName, + fakeProfileConnectorClassName, + crossProfileType.className()) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addSuperinterface(crossProfileTypeInterfaceClassName); + + classBuilder.addField( + FieldSpec.builder(fakeProfileConnectorClassName, "connector") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .build()); + + addConstructor(classBuilder); + + classBuilder.addMethod( + MethodSpec.methodBuilder("builder") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(builderClassName) + .addStatement("return new $T()", builderClassName) + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("current") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(getSingleSenderInterfaceClassName(generatorContext, crossProfileType)) + .beginControlFlow("if (connector.utils().runningOnPersonal())") + .addStatement( + "return ($T) personal()", + InterfaceGenerator.getSingleSenderInterfaceClassName( + generatorContext, crossProfileType)) + .nextControlFlow("else") + .addStatement( + "return ($T) work()", + InterfaceGenerator.getSingleSenderInterfaceClassName( + generatorContext, crossProfileType)) + .endControlFlow() + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("other") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .beginControlFlow("if (connector.utils().runningOnPersonal())") + .addStatement("return work()") + .nextControlFlow("else") + .addStatement("return personal()") + .endControlFlow() + .build()); + + addPersonalMethod(classBuilder); + addWorkMethod(classBuilder); + + classBuilder.addMethod( + MethodSpec.methodBuilder("profile") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .addParameter(PROFILE_CLASSNAME, "profile") + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .beginControlFlow("if (profile.isCurrent())") + .addStatement( + "return ($T) current()", + getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .nextControlFlow("else") + .addComment("must be other profile") + .addStatement("return other()") + .endControlFlow() + .build()); + + ParameterizedTypeName senderMapType = + ParameterizedTypeName.get( + ClassName.get(Map.class), + PROFILE_CLASSNAME, + InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName( + generatorContext, crossProfileType)); + + classBuilder.addMethod( + MethodSpec.methodBuilder("profiles") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .addParameter(ArrayTypeName.of(PROFILE_CLASSNAME), "profiles") + .varargs(true) + .returns(getMultipleSenderInterfaceClassName(generatorContext, crossProfileType)) + .addStatement("$T senders = new $T<>()", senderMapType, HashMap.class) + .beginControlFlow("for ($1T profileIdentifier : profiles)", PROFILE_CLASSNAME) + .addStatement("senders.put(profileIdentifier, profile(profileIdentifier))") + .endControlFlow() + .addStatement( + "return new $1T(senders)", + getMultipleProfilesClassName(generatorContext, crossProfileType)) + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("both") + .addAnnotation(Override.class) + .addJavadoc("Run a method on both the personal and work profile, if accessible.") + .addModifiers(Modifier.PUBLIC) + .returns(getMultipleSenderInterfaceClassName(generatorContext, crossProfileType)) + .addStatement("$1T utils = connector.utils()", PROFILE_AWARE_UTILS_CLASSNAME) + .addStatement( + "$1T currentProfileIdentifier = utils.getCurrentProfile()", PROFILE_CLASSNAME) + .addStatement("$1T otherProfileIdentifier = utils.getOtherProfile()", PROFILE_CLASSNAME) + .addStatement("return profiles(currentProfileIdentifier, otherProfileIdentifier)") + .build()); + + if (!crossProfileType.profileConnector().isPresent() + || crossProfileType.profileConnector().get().primaryProfile() != ProfileType.NONE) { + generatePrimarySecondaryMethods(classBuilder); + } + + generateFakeCrossProfileTypeBuilder(classBuilder); + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private void addConstructor(TypeSpec.Builder classBuilder) { + ClassName fakeProfileConnectorClassName = + crossProfileType.profileConnector().isPresent() + ? FakeProfileConnectorGenerator.getFakeProfileConnectorClassName( + crossProfileType.profileConnector().get()) + : ABSTRACT_FAKE_PROFILE_CONNECTOR_CLASSNAME; + + if (crossProfileType.isStatic()) { + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PRIVATE) + .addParameter(fakeProfileConnectorClassName, "connector") + .addStatement("this.connector = connector") + .build()); + } else { + classBuilder.addField( + FieldSpec.builder(crossProfileType.className(), "personal") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .build()); + + classBuilder.addField( + FieldSpec.builder(crossProfileType.className(), "work") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .build()); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PRIVATE) + .addParameter(crossProfileType.className(), "personal") + .addParameter(crossProfileType.className(), "work") + .addParameter(fakeProfileConnectorClassName, "connector") + .addStatement("this.personal = personal") + .addStatement("this.work = work") + .addStatement("this.connector = connector") + .build()); + } + } + + private void addPersonalMethod(TypeSpec.Builder classBuilder) { + ClassName currentProfileClassName = + getCurrentProfileClassName(generatorContext, crossProfileType); + CodeBlock currentPersonalFakeConstructor = + crossProfileType.isStatic() + ? CodeBlock.of("new $T(connector.applicationContext())", currentProfileClassName) + : CodeBlock.of( + "new $T(connector.applicationContext(), personal)", currentProfileClassName); + ClassName fakeOtherClassName = + FakeOtherGenerator.getFakeOtherClassName(generatorContext, crossProfileType); + CodeBlock otherPersonalFakeConstructor = + crossProfileType.isStatic() + ? CodeBlock.of("new $T(connector)", fakeOtherClassName) + : CodeBlock.of("new $T(connector, personal)", fakeOtherClassName); + + classBuilder.addMethod( + MethodSpec.methodBuilder("personal") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .beginControlFlow( + "if (connector.runningOnProfile() == $T.ProfileType.PERSONAL)", + CustomProfileConnector.class) + .addStatement("return $L", currentPersonalFakeConstructor) + .nextControlFlow("else") + .addStatement("return $L", otherPersonalFakeConstructor) + .endControlFlow() + .build()); + } + + private void addWorkMethod(TypeSpec.Builder classBuilder) { + ClassName currentProfileClassName = + getCurrentProfileClassName(generatorContext, crossProfileType); + CodeBlock currentWorkFakeConstructor = + crossProfileType.isStatic() + ? CodeBlock.of("new $T(connector.applicationContext())", currentProfileClassName) + : CodeBlock.of("new $T(connector.applicationContext(), work)", currentProfileClassName); + ClassName fakeOtherClassName = + FakeOtherGenerator.getFakeOtherClassName(generatorContext, crossProfileType); + CodeBlock otherWorkFakeConstructor = + crossProfileType.isStatic() + ? CodeBlock.of("new $T(connector)", fakeOtherClassName) + : CodeBlock.of("new $T(connector, work)", fakeOtherClassName); + + classBuilder.addMethod( + MethodSpec.methodBuilder("work") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .beginControlFlow( + "if (connector.runningOnProfile() == $T.ProfileType.WORK)", + CustomProfileConnector.class) + .addStatement("return $L", currentWorkFakeConstructor) + .nextControlFlow("else") + .addStatement("return $L", otherWorkFakeConstructor) + .endControlFlow() + .build()); + } + + private void generateFakeCrossProfileTypeBuilder(TypeSpec.Builder fakeCrossProfileType) { + ClassName fakeCrossProfileTypeClassName = + getFakeCrossProfileTypeClassName(generatorContext, crossProfileType); + ClassName builderClassName = + getFakeCrossProfileTypeBuilderClassName(generatorContext, crossProfileType); + ClassName fakeProfileConnectorClassName = + crossProfileType.profileConnector().isPresent() + ? FakeProfileConnectorGenerator.getFakeProfileConnectorClassName( + crossProfileType.profileConnector().get()) + : ABSTRACT_FAKE_PROFILE_CONNECTOR_CLASSNAME; + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(builderClassName) + .addJavadoc("Builder for {@link $T}.\n", fakeCrossProfileTypeClassName) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC); + + if (crossProfileType.isStatic()) { + setupStaticBuilder(fakeCrossProfileTypeClassName, classBuilder); + } else { + setupNonStaticBuilder(builderClassName, fakeCrossProfileTypeClassName, classBuilder); + } + + classBuilder.addField( + FieldSpec.builder(fakeProfileConnectorClassName, "connector") + .addModifiers(Modifier.PRIVATE) + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("connector") + .addJavadoc( + "Set the {@link $T} to be used to manage the state of this fake.\n", + fakeProfileConnectorClassName) + .addModifiers(Modifier.PUBLIC) + .returns(builderClassName) + .addParameter(fakeProfileConnectorClassName, "connector") + .addStatement("this.connector = connector") + .addStatement("return this") + .build()); + + fakeCrossProfileType.addType(classBuilder.build()); + } + + private static void setupStaticBuilder( + ClassName fakeCrossProfileTypeClassName, TypeSpec.Builder classBuilder) { + classBuilder.addMethod( + MethodSpec.methodBuilder("build") + .addJavadoc("Build the {@link $T}.\n", fakeCrossProfileTypeClassName) + .addModifiers(Modifier.PUBLIC) + .returns(fakeCrossProfileTypeClassName) + .beginControlFlow("if (connector == null)") + .addStatement( + "throw new $T($S)", + IllegalStateException.class, + "All arguments must be set to build fake") + .endControlFlow() + .addStatement("return new $1T(connector)", fakeCrossProfileTypeClassName) + .build()); + } + + private void setupNonStaticBuilder( + ClassName builderClassName, + ClassName fakeCrossProfileTypeClassName, + TypeSpec.Builder classBuilder) { + classBuilder.addField( + FieldSpec.builder(crossProfileType.className(), "personal") + .addModifiers(Modifier.PRIVATE) + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("personal") + .addJavadoc( + "Set the {@link $T} to be used when a call needs to be made to the personal" + + " profile.\n", + crossProfileType.className()) + .addModifiers(Modifier.PUBLIC) + .returns(builderClassName) + .addParameter(crossProfileType.className(), "personal") + .addStatement("this.personal = personal") + .addStatement("return this") + .build()); + + classBuilder.addField( + FieldSpec.builder(crossProfileType.className(), "work") + .addModifiers(Modifier.PRIVATE) + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("work") + .addJavadoc( + "Set the {@link $T} to be used when a call needs to be made to the work profile.\n", + crossProfileType.className()) + .addModifiers(Modifier.PUBLIC) + .returns(builderClassName) + .addParameter(crossProfileType.className(), "work") + .addStatement("this.work = work") + .addStatement("return this") + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("build") + .addJavadoc("Build the {@link $T}.\n", fakeCrossProfileTypeClassName) + .addModifiers(Modifier.PUBLIC) + .returns(fakeCrossProfileTypeClassName) + .beginControlFlow("if (personal == null || work == null || connector == null)") + .addStatement( + "throw new $T($S)", + IllegalStateException.class, + "All arguments must be set to build fake") + .endControlFlow() + .addStatement( + "return new $1T(personal, work, connector)", fakeCrossProfileTypeClassName) + .build()); + } + + private void generatePrimarySecondaryMethods(TypeSpec.Builder classBuilder) { + generatePrimaryMethod(classBuilder); + generateSecondaryMethod(classBuilder); + generateSuppliersMethod(classBuilder); + } + + private void generatePrimaryMethod(TypeSpec.Builder classBuilder) { + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder("primary") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .addStatement("return profile(connector.utils().getPrimaryProfile())"); + + classBuilder.addMethod(methodBuilder.build()); + } + + private void generateSecondaryMethod(TypeSpec.Builder classBuilder) { + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder("secondary") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .addStatement("return profile(connector.utils().getSecondaryProfile())"); + + classBuilder.addMethod(methodBuilder.build()); + } + + private void generateSuppliersMethod(TypeSpec.Builder classBuilder) { + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder("suppliers") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(getMultipleSenderInterfaceClassName(generatorContext, crossProfileType)) + .addStatement("$1T utils = connector.utils()", PROFILE_AWARE_UTILS_CLASSNAME) + .addStatement( + "$1T currentProfileIdentifier = utils.getCurrentProfile()", PROFILE_CLASSNAME) + .addStatement( + "$1T secondaryProfileIdentifier = utils.getSecondaryProfile()", PROFILE_CLASSNAME) + .addStatement("return profiles(currentProfileIdentifier, secondaryProfileIdentifier)"); + + classBuilder.addMethod(methodBuilder.build()); + } + + static ClassName getFakeCrossProfileTypeClassName( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + ClassName crossProfileTypeClassName = + InterfaceGenerator.getCrossProfileTypeInterfaceClassName( + generatorContext, crossProfileType); + return ClassName.get( + crossProfileTypeClassName.packageName(), "Fake" + crossProfileTypeClassName.simpleName()); + } + + static ClassName getFakeCrossProfileTypeBuilderClassName( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + ClassName crossProfileTypeClassName = + InterfaceGenerator.getCrossProfileTypeInterfaceClassName( + generatorContext, crossProfileType); + return ClassName.get( + crossProfileTypeClassName.packageName() + + "." + + "Fake" + + crossProfileTypeClassName.simpleName(), + "Builder"); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeOtherGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeOtherGenerator.java new file mode 100644 index 0000000..20bf0fb --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeOtherGenerator.java @@ -0,0 +1,344 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.ABSTRACT_FAKE_PROFILE_CONNECTOR_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CONTEXT_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.EXCEPTION_CALLBACK_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_RUNTIME_EXCEPTION_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS; +import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo; +import com.google.android.enterprise.connectedapps.processor.containers.FutureWrapper; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.squareup.javapoet.AnnotationSpec; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeSpec; +import javax.lang.model.element.Modifier; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +/** + * Generate the {@code Profile_*_FakeOther} class for a single cross-profile type. + * + * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to + * validate that the annotated code is correct. + */ +final class FakeOtherGenerator { + + private boolean generated = false; + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final CrossProfileTypeInfo crossProfileType; + + FakeOtherGenerator(GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.crossProfileType = checkNotNull(crossProfileType); + } + + void generate() { + if (generated) { + throw new IllegalStateException("FakeSingleSenderGenerator#generate can only be called once"); + } + generated = true; + + generateFakeOther(); + } + + private void generateFakeOther() { + ClassName className = getFakeOtherClassName(generatorContext, crossProfileType); + + ClassName singleSenderCanThrowInterface = + InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName( + generatorContext, crossProfileType); + + ClassName fakeProfileConnectorClassName = + crossProfileType.profileConnector().isPresent() + ? FakeProfileConnectorGenerator.getFakeProfileConnectorClassName( + crossProfileType.profileConnector().get()) + : ABSTRACT_FAKE_PROFILE_CONNECTOR_CLASSNAME; + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addJavadoc( + "Fake implementation of {@link $T} for use during tests.\n\n" + + "<p>This acts based on the state of the passed in {@link $T} and acts as if" + + " making a call on the other profile.\n", + singleSenderCanThrowInterface, + fakeProfileConnectorClassName) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addSuperinterface(singleSenderCanThrowInterface); + + classBuilder.addField( + fakeProfileConnectorClassName, "connector", Modifier.PRIVATE, Modifier.FINAL); + + addConstructor(classBuilder); + + classBuilder.addMethod( + MethodSpec.methodBuilder("timeout") + .addAnnotation(Override.class) + .addAnnotation( + AnnotationSpec.builder(SuppressWarnings.class) + .addMember("value", "$S", "GoodTime") + .build()) + .addModifiers(Modifier.PUBLIC) + .returns(className) + .addParameter(long.class, "timeout") + .addStatement("return this") + .build()); + + ClassName ifAvailableClass = + IfAvailableGenerator.getIfAvailableClassName(generatorContext, crossProfileType); + + classBuilder.addMethod( + MethodSpec.methodBuilder("ifAvailable") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(ifAvailableClass) + .addStatement("return new $T(this)", ifAvailableClass) + .build()); + + for (CrossProfileMethodInfo method : crossProfileType.crossProfileMethods()) { + if (method.isBlocking(generatorContext, crossProfileType)) { + generateBlockingMethodOnFakeOther(classBuilder, method, crossProfileType); + } else if (method.isCrossProfileCallback(generatorContext)) { + generateCrossProfileCallbackMethodOnFakeOther(classBuilder, method, crossProfileType); + } else if (method.isFuture(crossProfileType)) { + generateFutureMethodOnFakeOther(classBuilder, method, crossProfileType); + } else { + throw new IllegalStateException("Unknown method type: " + method); + } + } + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private void addConstructor(TypeSpec.Builder classBuilder) { + ClassName fakeProfileConnectorClassName = + crossProfileType.profileConnector().isPresent() + ? FakeProfileConnectorGenerator.getFakeProfileConnectorClassName( + crossProfileType.profileConnector().get()) + : ABSTRACT_FAKE_PROFILE_CONNECTOR_CLASSNAME; + + classBuilder.addField(CONTEXT_CLASSNAME, "context", Modifier.PRIVATE, Modifier.FINAL); + + if (crossProfileType.isStatic()) { + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(fakeProfileConnectorClassName, "connector") + .addStatement("this.context = connector.applicationContext()") + .addStatement("this.connector = connector") + .build()); + } else { + classBuilder.addField( + crossProfileType.className(), "crossProfileType", Modifier.PRIVATE, Modifier.FINAL); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(fakeProfileConnectorClassName, "connector") + .addParameter(crossProfileType.className(), "crossProfileType") + .addStatement("this.context = connector.applicationContext()") + .addStatement("this.connector = connector") + .addStatement("this.crossProfileType = crossProfileType") + .build()); + } + } + + private void generateBlockingMethodOnFakeOther( + TypeSpec.Builder classBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder(method.simpleName()) + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .addExceptions(method.thrownExceptions()) + .addException(UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME) + .returns(method.returnTypeTypeName()) + .addParameters( + GeneratorUtilities.extractParametersFromMethod( + crossProfileType.supportedTypes(), + method.methodElement(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)); + + CodeBlock methodCall = + CodeBlock.of( + "$L.$L($L)", + getCrossProfileTypeReference(method, crossProfileType), + method.simpleName(), + method.commaSeparatedParameters( + crossProfileType.supportedTypes(), REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS)); + + if (method.returnType().getKind() != TypeKind.VOID) { + methodCall = CodeBlock.of("return $L", methodCall); + } + + methodBuilder.beginControlFlow("if (!connector.isConnected())"); + methodBuilder.addStatement( + "throw new $T($S)", + UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME, + "Could not access other profile"); + methodBuilder.endControlFlow(); + + methodBuilder.beginControlFlow("if (!connector.isManuallyManagingConnection())"); + methodBuilder.addStatement( + "throw new $T($S)", + UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME, + "Synchronous calls can only be used when manually connected"); + methodBuilder.endControlFlow(); + + methodBuilder.beginControlFlow("try"); + methodBuilder.addStatement(methodCall); + methodBuilder.nextControlFlow("catch ($T e)", RuntimeException.class); + methodBuilder.addStatement("throw new $T(e)", PROFILE_RUNTIME_EXCEPTION_CLASSNAME); + methodBuilder.endControlFlow(); + classBuilder.addMethod(methodBuilder.build()); + } + + private static CodeBlock getCrossProfileTypeReference( + CrossProfileMethodInfo method, CrossProfileTypeInfo crossProfileType) { + return method.isStatic() + ? CodeBlock.of("$1T", crossProfileType.className()) + : CodeBlock.of("crossProfileType"); + } + + private void generateCrossProfileCallbackMethodOnFakeOther( + TypeSpec.Builder classBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder(method.simpleName()) + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(method.returnTypeTypeName()) + .addParameters( + GeneratorUtilities.extractParametersFromMethod( + crossProfileType.supportedTypes(), + method.methodElement(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)) + .addParameter(EXCEPTION_CALLBACK_CLASSNAME, "exceptionCallback"); + + CodeBlock methodCall = + CodeBlock.of( + "$L.$L($L)", + getCrossProfileTypeReference(method, crossProfileType), + method.simpleName(), + method.commaSeparatedParameters( + crossProfileType.supportedTypes(), REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS)); + + if (method.returnType().getKind() != TypeKind.VOID) { + methodCall = CodeBlock.of("return $L", methodCall); + } + + methodBuilder.beginControlFlow("if (!connector.isAvailable())"); + methodBuilder.addStatement( + "exceptionCallback.onException(new $T($S))", + UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME, + "Could not access other profile"); + methodBuilder.addStatement("return"); + methodBuilder.endControlFlow(); + + methodBuilder.addStatement("connector.automaticallyConnect()"); + methodBuilder.beginControlFlow("try"); + methodBuilder.addStatement(methodCall); + methodBuilder.nextControlFlow("catch ($T e)", RuntimeException.class); + methodBuilder.addStatement("throw new $T(e)", PROFILE_RUNTIME_EXCEPTION_CLASSNAME); + methodBuilder.endControlFlow(); + + classBuilder.addMethod(methodBuilder.build()); + } + + private void generateFutureMethodOnFakeOther( + TypeSpec.Builder classBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder(method.simpleName()) + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(method.returnTypeTypeName()) + .addParameters( + GeneratorUtilities.extractParametersFromMethod( + crossProfileType.supportedTypes(), + method.methodElement(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)); + + CodeBlock methodCall = + CodeBlock.of( + "$L.$L($L)", + getCrossProfileTypeReference(method, crossProfileType), + method.simpleName(), + method.commaSeparatedParameters( + crossProfileType.supportedTypes(), REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS)); + + if (method.returnType().getKind() != TypeKind.VOID) { + methodCall = CodeBlock.of("$1T returnValue = $2L", method.returnType(), methodCall); + } + + TypeMirror rawFutureType = TypeUtils.removeTypeArguments(method.returnType()); + + FutureWrapper futureWrapper = + crossProfileType.supportedTypes().getType(rawFutureType).getFutureWrapper().get(); + + // This assumes futures are only generic on one argument, which is enforced + TypeMirror wrappedType = TypeUtils.extractTypeArguments(method.returnType()).get(0); + ParameterizedTypeName futureWrapperType = + ParameterizedTypeName.get(futureWrapper.wrapperClassName(), ClassName.get(wrappedType)); + + methodBuilder.beginControlFlow("if (!connector.isAvailable())"); + methodBuilder.addStatement( + "$1T failedFuture = $2T.create(new $3T(), $4L)", + futureWrapperType, + futureWrapper.wrapperClassName(), + BundlerGenerator.getBundlerClassName(generatorContext, crossProfileType), + TypeUtils.generateBundlerType(wrappedType)); + methodBuilder.addStatement( + "failedFuture.onException(new $1T($2S))", + UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME, + "Could not access other profile"); + methodBuilder.addStatement("return failedFuture.getFuture()"); + methodBuilder.endControlFlow(); + + methodBuilder.beginControlFlow("try"); + methodBuilder.addStatement("connector.automaticallyConnect()"); + methodBuilder.addStatement(methodCall); + if (method.returnType().getKind() != TypeKind.VOID) { + methodBuilder.addStatement("return returnValue"); + } + methodBuilder.nextControlFlow("catch ($T e)", RuntimeException.class); + methodBuilder.addStatement("throw new $T(e)", PROFILE_RUNTIME_EXCEPTION_CLASSNAME); + methodBuilder.endControlFlow(); + + classBuilder.addMethod(methodBuilder.build()); + } + + static ClassName getFakeOtherClassName( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + return GeneratorUtilities.appendToClassName(crossProfileType.profileClassName(), "_FakeOther"); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeProfileConnectorGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeProfileConnectorGenerator.java new file mode 100644 index 0000000..5ff51ea --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeProfileConnectorGenerator.java @@ -0,0 +1,100 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.ABSTRACT_FAKE_PROFILE_CONNECTOR_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CONTEXT_CLASSNAME; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector.ProfileType; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.google.android.enterprise.connectedapps.processor.containers.ProfileConnectorInfo; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeSpec; +import javax.lang.model.element.Modifier; + +class FakeProfileConnectorGenerator { + private boolean generated = false; + private final ProfileConnectorInfo connector; + private final GeneratorUtilities generatorUtilities; + + public FakeProfileConnectorGenerator( + GeneratorContext generatorContext, ProfileConnectorInfo connector) { + this.generatorUtilities = new GeneratorUtilities(checkNotNull(generatorContext)); + this.connector = checkNotNull(connector); + } + + void generate() { + if (generated) { + throw new IllegalStateException( + "FakeProfileConectorGenerator#generate can only be called once"); + } + generated = true; + + generateFakeProfileConnector(); + } + + private void generateFakeProfileConnector() { + ClassName className = getFakeProfileConnectorClassName(connector); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addJavadoc( + "Fake Profile Connector for {@link $1T}.\n\n" + + "<p>All functionality is implemented by {@link $2T}, this class is just used" + + " for compatibility with the {@link $1T} interface.\n", + connector.connectorClassName(), + ABSTRACT_FAKE_PROFILE_CONNECTOR_CLASSNAME) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addSuperinterface(connector.connectorClassName()) + .superclass(ABSTRACT_FAKE_PROFILE_CONNECTOR_CLASSNAME); + + if (connector.primaryProfile().equals(ProfileType.UNKNOWN)) { + // Special case - we need to provide the profile type to the fake. + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addParameter(CONTEXT_CLASSNAME, "context") + .addModifiers(Modifier.PUBLIC) + .addStatement("super(context, $T.NONE)", ProfileType.class) + .build()); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(CONTEXT_CLASSNAME, "context") + .addParameter(ProfileType.class, "primaryProfile") + .addStatement("super(context, primaryProfile)") + .build()); + } else { + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(CONTEXT_CLASSNAME, "context") + .addStatement( + "super(context, $T.$L)", ProfileType.class, connector.primaryProfile().name()) + .build()); + } + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + static ClassName getFakeProfileConnectorClassName(ProfileConnectorInfo connector) { + return ClassName.get( + connector.connectorClassName().packageName(), + "Fake" + connector.connectorClassName().simpleName()); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FutureWrappersGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FutureWrappersGenerator.java new file mode 100644 index 0000000..78902c4 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FutureWrappersGenerator.java @@ -0,0 +1,126 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toSet; + +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo; +import com.google.android.enterprise.connectedapps.processor.containers.FutureWrapper; +import com.google.android.enterprise.connectedapps.processor.containers.FutureWrapper.WrapperType; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.google.android.enterprise.connectedapps.processor.containers.Type; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.nio.charset.Charset; +import java.util.Collection; +import java.util.Optional; +import javax.tools.JavaFileObject; + +/** + * Generate the wrapper classes for every used future type. + * + * <p>This is intended to be initialised and used once, which will generate all needed code. + * + * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to + * validate that the annotated code is correct. + */ +final class FutureWrappersGenerator { + + private boolean generated = false; + private final GeneratorContext generatorContext; + + FutureWrappersGenerator(GeneratorContext generatorContext) { + this.generatorContext = checkNotNull(generatorContext); + } + + void generate() { + if (generated) { + throw new IllegalStateException("FutureWrappersGenerator#generate can only be called once"); + } + generated = true; + + generateFutureWrappers(); + } + + private void generateFutureWrappers() { + Collection<FutureWrapper> futureWrappersToGenerate = + generatorContext.crossProfileTypes().stream() + .map(CrossProfileTypeInfo::supportedTypes) + .flatMap(s -> s.usableTypes().stream()) + .filter(s -> s.getFutureWrapper().isPresent()) + .map(Type::getFutureWrapper) + .map(Optional::get) + .filter(w -> w.wrapperType().equals(WrapperType.DEFAULT)) + .collect(toSet()); + + for (FutureWrapper futureWrapper : futureWrappersToGenerate) { + generateFutureWrapper(futureWrapper); + } + } + + private void generateFutureWrapper(FutureWrapper futureWrapper) { + String futureWrapperSimpleName = futureWrapper.defaultWrapperClassName().simpleName(); + + String contents; + InputStream in = + ParcelableWrappersGenerator.class.getResourceAsStream( + "/futurewrappers/" + futureWrapperSimpleName + ".java"); + + try (BufferedReader br = + new BufferedReader(new InputStreamReader(in, Charset.defaultCharset()))) { + contents = br.lines().collect(joining(System.lineSeparator())); + } catch (IOException e) { + throw new IllegalStateException( + "Could not read futurewrapper file for " + futureWrapperSimpleName, e); + } + + contents = + contents.replace( + futureWrapper.defaultWrapperClassName().packageName(), + futureWrapper.wrapperClassName().packageName()); + contents = + contents.replace( + futureWrapper.defaultWrapperClassName().simpleName(), + futureWrapper.wrapperClassName().simpleName()); + + JavaFileObject builderFile; + try { + builderFile = + generatorContext + .processingEnv() + .getFiler() + .createSourceFile( + futureWrapper.wrapperClassName().packageName() + + "." + + futureWrapper.wrapperClassName().simpleName()); + } catch (IOException e) { + throw new IllegalStateException( + "Could not write futurewrapper for " + futureWrapperSimpleName, e); + } + + try (PrintWriter out = new PrintWriter(builderFile.openWriter())) { + out.write(contents); + } catch (IOException e) { + throw new IllegalStateException( + "Could not write futurewrapper for " + futureWrapperSimpleName, e); + } + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/GeneratorUtilities.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/GeneratorUtilities.java new file mode 100644 index 0000000..7d4dbd7 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/GeneratorUtilities.java @@ -0,0 +1,299 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCELABLE_CREATOR_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.LEAVE_AUTOMATICALLY_RESOLVED_PARAMETERS; +import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS; +import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.containers.Context; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo; +import com.google.common.collect.Iterables; +import com.squareup.javapoet.AnnotationSpec; +import com.squareup.javapoet.ArrayTypeName; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.JavaFile; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.List; +import java.util.Set; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.MirroredTypeException; +import javax.lang.model.type.MirroredTypesException; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Types; +import javax.tools.JavaFileObject; + +/** Utility methods used for code generation. */ +public final class GeneratorUtilities { + + private final Context context; + + public GeneratorUtilities(Context context) { + this.context = checkNotNull(context); + } + + /** + * Extract a class provided in an annotation. + * + * <p>The {@code runnable} should call the annotation method that the class is being extracted + * for. + */ + public static TypeElement extractClassFromAnnotation(Types types, Runnable runnable) { + // From https://docs.oracle.com/javase/8/docs/api/javax/lang/model/AnnotatedConstruct.html + // "The annotation returned by this method could contain an element whose value is of type + // Class. This value cannot be returned directly: information necessary to locate and load a + // class (such as the class loader to use) is not available, and the class might not be loadable + // at all. Attempting to read a Class object by invoking the relevant method on the returned + // annotation will result in a MirroredTypeException, from which the corresponding TypeMirror + // may be extracted." + try { + runnable.run(); + } catch (MirroredTypeException e) { + return e.getTypeMirrors().stream() + .map(t -> (TypeElement) types.asElement(t)) + .findFirst() + .get(); + } + throw new AssertionError("Could not extract class from annotation"); + } + + /** + * Extract classes provided in an annotation. + * + * <p>The {@code runnable} should call the annotation method that the classes are being extracted + * for. + */ + public static List<TypeElement> extractClassesFromAnnotation(Types types, Runnable runnable) { + // From https://docs.oracle.com/javase/8/docs/api/javax/lang/model/AnnotatedConstruct.html + // "The annotation returned by this method could contain an element whose value is of type + // Class. This value cannot be returned directly: information necessary to locate and load a + // class (such as the class loader to use) is not available, and the class might not be loadable + // at all. Attempting to read a Class object by invoking the relevant method on the returned + // annotation will result in a MirroredTypeException, from which the corresponding TypeMirror + // may be extracted." + try { + runnable.run(); + } catch (MirroredTypesException e) { + return e.getTypeMirrors().stream() + .map(t -> (TypeElement) types.asElement(t)) + .collect(toList()); + } + throw new AssertionError("Could not extract classes from annotation"); + } + + public static Set<ExecutableElement> findCrossProfileMethodsInClass(TypeElement clazz) { + return clazz.getEnclosedElements().stream() + .filter(e -> e instanceof ExecutableElement) + .map(e -> (ExecutableElement) e) + .filter(e -> e.getKind() == ElementKind.METHOD) + .filter(AnnotationFinder::hasCrossProfileAnnotation) + .collect(toSet()); + } + + public static Set<ExecutableElement> findCrossProfileProviderMethodsInClass(TypeElement clazz) { + return clazz.getEnclosedElements().stream() + .filter(e -> e instanceof ExecutableElement) + .map(e -> (ExecutableElement) e) + .filter(e -> e.getKind() == ElementKind.METHOD) + .filter(AnnotationFinder::hasCrossProfileProviderAnnotation) + .collect(toSet()); + } + + /** Generate a {@code @link} reference to a given method. */ + public static CodeBlock methodJavadocReference(ExecutableElement method) { + CodeBlock.Builder methodCall = CodeBlock.builder(); + methodCall.add("{@link $T#", method.getEnclosingElement()); + methodCall.add("$L(", method.getSimpleName()); + + if (!method.getParameters().isEmpty()) { + methodCall.add("$T", method.getParameters().iterator().next().asType()); + + for (VariableElement param : + method.getParameters().subList(1, method.getParameters().size())) { + methodCall.add(",$T", param.asType()); + } + } + + methodCall.add(")}"); + return methodCall.build(); + } + + public void writeClassToFile(String packageName, TypeSpec.Builder clazzBuilder) { + writeClassToFile(packageName, clazzBuilder.build()); + } + + void writeClassToFile(String packageName, TypeSpec clazz) { + final String qualifiedClassName = + packageName.isEmpty() ? clazz.name : packageName + "." + clazz.name; + + JavaFile javaFile = JavaFile.builder(packageName, clazz).build(); + try { + JavaFileObject builderFile = + context.processingEnv().getFiler().createSourceFile(qualifiedClassName); + try (PrintWriter out = new PrintWriter(builderFile.openWriter())) { + javaFile.writeTo(out); + } + } catch (IOException e) { + throw new IllegalStateException("Error writing " + qualifiedClassName + " to file", e); + } + } + + /** + * Take the parameters of an {@link ExecutableElement} and return {@link ParameterSpec} instances + * ready to be used with a generated method. + */ + static List<ParameterSpec> extractParametersFromMethod( + SupportedTypes supportedTypes, + ExecutableElement method, + AutomaticallyResolvedParameterFilterBehaviour filterBehaviour) { + if (filterBehaviour == LEAVE_AUTOMATICALLY_RESOLVED_PARAMETERS) { + return extractParametersFromMethod(method); + } else if (filterBehaviour == REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS) { + return method.getParameters().stream() + .filter(param -> !supportedTypes.isAutomaticallyResolved(param.asType())) + .map(GeneratorUtilities::convertVariableToParameterSpec) + .collect(toList()); + } else if (filterBehaviour == REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS) { + throw new IllegalArgumentException("Can not replace parameters when extracting"); + } + throw new IllegalArgumentException("Unknown filterBehaviour " + filterBehaviour); + } + + /** + * Take the parameters of an {@link ExecutableElement} and return {@link ParameterSpec} instances + * ready to be used with a generated method. + * + * <p>This will not filter automatically resolved parameters. For that functionality use {@link + * #extractParametersFromMethod(SupportedTypes, ExecutableElement, + * AutomaticallyResolvedParameterFilterBehaviour)}. + */ + static List<ParameterSpec> extractParametersFromMethod(ExecutableElement method) { + return method.getParameters().stream() + .map(GeneratorUtilities::convertVariableToParameterSpec) + .collect(toList()); + } + + private static ParameterSpec convertVariableToParameterSpec(VariableElement variable) { + ParameterSpec.Builder builder = + ParameterSpec.builder( + ClassName.get(variable.asType()), variable.getSimpleName().toString()); + builder.addModifiers(variable.getModifiers()); + return builder.build(); + } + + /** If type is primitive, return the boxed version of that type, otherwise return the type. */ + TypeMirror boxIfNecessary(TypeMirror type) { + if (!type.getKind().isPrimitive()) { + return type; + } + + PrimitiveType primitiveType = (PrimitiveType) type; + return context.types().boxedClass(primitiveType).asType(); + } + + void addDefaultParcelableMethods(TypeSpec.Builder classBuilder, ClassName className) { + classBuilder.addMethod( + MethodSpec.methodBuilder("describeContents") + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .returns(int.class) + .addStatement("return 0") + .build()); + + TypeName creatorType = ParameterizedTypeName.get(PARCELABLE_CREATOR_CLASSNAME, className); + + TypeSpec creator = + TypeSpec.anonymousClassBuilder("") + .addSuperinterface(creatorType) + .addMethod( + MethodSpec.methodBuilder("createFromParcel") + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .returns(className) + .addParameter(PARCEL_CLASSNAME, "in") + .addStatement("return new $T(in)", className) + .build()) + .addMethod( + MethodSpec.methodBuilder("newArray") + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .returns(ArrayTypeName.of(className)) + .addParameter(int.class, "size") + .addStatement("return new $T[size]", className) + .build()) + .build(); + + classBuilder.addField( + FieldSpec.builder(creatorType, "CREATOR") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + .addAnnotation( + AnnotationSpec.builder(SuppressWarnings.class) + .addMember("value", "$S", "rawtypes") + .build()) + .initializer("$L", creator) + .build()); + } + + /** Generate a reference to a cross-profile method which can be used in javadoc. */ + public static CodeBlock generateMethodReference( + CrossProfileTypeInfo crossProfileType, CrossProfileMethodInfo method) { + CodeBlock.Builder reference = CodeBlock.builder(); + + reference.add("$T#$L(", crossProfileType.className(), method.simpleName()); + + List<TypeMirror> parameterTypes = convertParametersToTypes(method); + + if (!parameterTypes.isEmpty()) { + for (int i = 0; i < parameterTypes.size() - 1; i++) { + reference.add("$T, ", TypeUtils.getRawTypeClassName(parameterTypes.get(i))); + } + reference.add("$T", TypeUtils.getRawTypeClassName(Iterables.getLast(parameterTypes))); + } + + reference.add(")"); + return reference.build(); + } + + private static List<TypeMirror> convertParametersToTypes(CrossProfileMethodInfo method) { + return method.methodElement().getParameters().stream().map(Element::asType).collect(toList()); + } + + static ClassName appendToClassName(ClassName originalClassName, String suffix) { + return ClassName.get(originalClassName.packageName(), originalClassName.simpleName() + suffix); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/IfAvailableGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/IfAvailableGenerator.java new file mode 100644 index 0000000..68d84f4 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/IfAvailableGenerator.java @@ -0,0 +1,232 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.IF_AVAILABLE_FUTURE_RESULT_WRITER; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.GeneratorUtilities.generateMethodReference; +import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackInterfaceInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo; +import com.google.android.enterprise.connectedapps.processor.containers.FutureWrapper; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeSpec; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +/** + * Generate the {@code Profile_*_IfAvailable} class for a single cross-profile type. + * + * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to + * validate that the annotated code is correct. + */ +final class IfAvailableGenerator { + + private boolean generated = false; + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final CrossProfileTypeInfo crossProfileType; + + IfAvailableGenerator(GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.crossProfileType = checkNotNull(crossProfileType); + } + + void generate() { + if (generated) { + throw new IllegalStateException("IfAvailableGenerator#generate can only be called once"); + } + generated = true; + + generateIfAvailableClass(); + } + + private void generateIfAvailableClass() { + ClassName className = getIfAvailableClassName(generatorContext, crossProfileType); + + ClassName singleSenderCanThrowInterface = + InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName( + generatorContext, crossProfileType); + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addJavadoc( + "Wrapper of {@link $T} which will replace\n{@link $T} with default values.\n", + singleSenderCanThrowInterface, + UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL); + + classBuilder.addField( + FieldSpec.builder(singleSenderCanThrowInterface, "singleSenderCanThrow") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .build()); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(singleSenderCanThrowInterface, "singleSenderCanThrow") + .addStatement("this.singleSenderCanThrow = singleSenderCanThrow") + .build()); + + for (CrossProfileMethodInfo method : crossProfileType.crossProfileMethods()) { + generateMethodOnIfAvailableClass(classBuilder, method, crossProfileType); + } + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private void generateMethodOnIfAvailableClass( + TypeSpec.Builder classBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder(method.simpleName()) + .addModifiers(Modifier.PUBLIC) + .addExceptions(method.thrownExceptions()) + .addParameters( + GeneratorUtilities.extractParametersFromMethod( + crossProfileType.supportedTypes(), + method.methodElement(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)) + .addJavadoc("Call {@link $L}.", generateMethodReference(crossProfileType, method)); + + if (method.isBlocking(generatorContext, crossProfileType)) { + if (method.returnType().getKind().equals(TypeKind.VOID)) { + methodBuilder + .addJavadoc( + "\n\n<p>{@link $T} will be ignored.\n", UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME) + .beginControlFlow("try") + .addStatement( + "singleSenderCanThrow.$L($L)", + method.simpleName(), + method.commaSeparatedParameters( + crossProfileType.supportedTypes(), REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)) + .nextControlFlow("catch ($T e)", UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME) + .addComment("Ignore exception") + .endControlFlow(); + } else { + methodBuilder.addParameter(method.returnTypeTypeName(), "defaultValue"); + methodBuilder + .addJavadoc( + "\n\n<p>In case of {@link $T}, {@code defaultValue} will be returned.\n", + UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME) + .returns(method.returnTypeTypeName()) + .beginControlFlow("try") + .addStatement( + "return singleSenderCanThrow.$L($L)", + method.simpleName(), + method.commaSeparatedParameters( + crossProfileType.supportedTypes(), REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)) + .nextControlFlow("catch ($T e)", UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME) + .addStatement("return defaultValue") + .endControlFlow(); + } + } else if (method.isCrossProfileCallback(generatorContext)) { + if (!method.isSimpleCrossProfileCallback(generatorContext)) { + // Non-simple callbacks can't be used with multiple profiles + return; + } + + CrossProfileCallbackInterfaceInfo callbackInterface = + CrossProfileCallbackInterfaceInfo.create( + (TypeElement) + generatorContext + .types() + .asElement( + method.getCrossProfileCallbackParam(generatorContext).get().asType())); + if (callbackInterface.argumentTypes().isEmpty()) { + // Void + // This assumes a single callback method + methodBuilder + .addJavadoc( + "\n\n<p>If the profile is not available, the callback will be called anyway.\n") + .addStatement( + "singleSenderCanThrow.$1L(\n $2L,\n (e) -> {$3L.$4L();})", + method.simpleName(), + method.commaSeparatedParameters( + crossProfileType.supportedTypes(), REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS), + method.getCrossProfileCallbackParam(generatorContext).get().getSimpleName(), + callbackInterface.methods().get(0).getSimpleName()); + } else { + // This assumes a single callback method + methodBuilder.addParameter( + ClassName.get(callbackInterface.argumentTypes().iterator().next()), "defaultValue"); + methodBuilder + .addJavadoc( + "\n\n" + + "<p>If the profile is not available, the callback will be called with the" + + " {@code defaultValue}.\n") + .addStatement( + "singleSenderCanThrow.$1L(\n $2L,\n (e) -> {$3L.$4L(defaultValue);})", + method.simpleName(), + method.commaSeparatedParameters( + crossProfileType.supportedTypes(), REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS), + method.getCrossProfileCallbackParam(generatorContext).get().getSimpleName(), + callbackInterface.methods().get(0).getSimpleName()); + } + } else if (method.isFuture(crossProfileType)) { + // This assumes a Future is generic on a single type + TypeMirror wrappedReturnType = TypeUtils.extractTypeArguments(method.returnType()).get(0); + TypeMirror rawFutureType = TypeUtils.removeTypeArguments(method.returnType()); + FutureWrapper futureWrapper = + crossProfileType.supportedTypes().getType(rawFutureType).getFutureWrapper().get(); + + methodBuilder + .addParameter(ClassName.get(wrappedReturnType), "defaultValue") + .returns(ClassName.get(method.returnType())); + methodBuilder + .addJavadoc( + "\n\n" + + "<p>If the profile is not available, the future will be resolved with the" + + " {@code defaultValue}.\n") + .addStatement( + "$1T internalCrossProfileClass = $1T.instance()", + InternalCrossProfileClassGenerator.getInternalCrossProfileClassName( + generatorContext, crossProfileType)) + .addStatement( + "$1T<$2T> futureWrapper = $1T.create(internalCrossProfileClass.bundler(), $3L)", + futureWrapper.wrapperClassName(), + wrappedReturnType, + TypeUtils.generateBundlerType(wrappedReturnType)) + .addStatement( + "$T.writeFutureResult(singleSenderCanThrow.$L($L), new" + + " $T<$T>(futureWrapper, defaultValue))", + futureWrapper.wrapperClassName(), + method.simpleName(), + method.commaSeparatedParameters( + crossProfileType.supportedTypes(), REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS), + IF_AVAILABLE_FUTURE_RESULT_WRITER, + wrappedReturnType) + .addStatement("return futureWrapper.getFuture()"); + } + classBuilder.addMethod(methodBuilder.build()); + } + + static ClassName getIfAvailableClassName( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + return GeneratorUtilities.appendToClassName( + crossProfileType.profileClassName(), "_IfAvailable"); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InterfaceGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InterfaceGenerator.java new file mode 100644 index 0000000..a8c76c6 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InterfaceGenerator.java @@ -0,0 +1,690 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.EXCEPTION_CALLBACK_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_CONNECTOR_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_RUNTIME_EXCEPTION_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.GeneratorUtilities.generateMethodReference; +import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.stream.Collectors.toList; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfile; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector.ProfileType; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackInterfaceInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.google.common.base.Ascii; +import com.squareup.javapoet.AnnotationSpec; +import com.squareup.javapoet.ArrayTypeName; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; +import java.util.List; +import java.util.Map; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +/** Generator of cross-profile code for a single {@link CrossProfile} type. */ +final class InterfaceGenerator { + + private boolean generated = false; + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final CrossProfileTypeInfo crossProfileType; + + InterfaceGenerator(GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.crossProfileType = checkNotNull(crossProfileType); + } + + void generate() { + if (generated) { + throw new IllegalStateException("InterfaceGenerator#generate can only be called once"); + } + generated = true; + + generateCrossProfileTypeInterface(); + generateSingleSenderInterface(); + generateSingleSenderCanThrowInterface(); + generateMultipleSenderInterface(); + } + + private void generateCrossProfileTypeInterface() { + ClassName interfaceName = + getCrossProfileTypeInterfaceClassName(generatorContext, crossProfileType); + + TypeSpec.Builder interfaceBuilder = + TypeSpec.interfaceBuilder(interfaceName) + .addJavadoc( + "Entry point for cross-profile calls to {@link $T}.\n", + crossProfileType.className()) + .addModifiers(Modifier.PUBLIC); + + ClassName connectorClassName = + crossProfileType.profileConnector().isPresent() + ? crossProfileType.profileConnector().get().connectorClassName() + : PROFILE_CONNECTOR_CLASSNAME; + + interfaceBuilder.addMethod( + MethodSpec.methodBuilder("create") + .returns(interfaceName) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(connectorClassName, "connector") + .addStatement( + "return new $T(connector)", + DefaultProfileClassGenerator.getDefaultProfileClassName( + generatorContext, crossProfileType)) + .build()); + + interfaceBuilder.addMethod( + MethodSpec.methodBuilder("current") + .addJavadoc("Run a method on the current profile.\n") + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(getSingleSenderInterfaceClassName(generatorContext, crossProfileType)) + .build()); + + interfaceBuilder.addMethod( + MethodSpec.methodBuilder("other") + .addJavadoc("Run a method on the other profile, if accessible.\n") + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .build()); + + interfaceBuilder.addMethod( + MethodSpec.methodBuilder("personal") + .addJavadoc("Run a method on the personal profile, if accessible.\n") + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .build()); + + interfaceBuilder.addMethod( + MethodSpec.methodBuilder("work") + .addJavadoc("Run a method on the work profile, if accessible.\n") + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .build()); + + interfaceBuilder.addMethod( + MethodSpec.methodBuilder("profile") + .addJavadoc("Run a method on the given profile, if accessible.\n") + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .addParameter(PROFILE_CLASSNAME, "profile") + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .build()); + + interfaceBuilder.addMethod( + MethodSpec.methodBuilder("profiles") + .addJavadoc( + CodeBlock.builder() + .add("Run a method on the given profiles, if accessible.\n\n") + .add( + "<p>This will deduplicate profiles to ensure that the method is only run" + + " at most once on each profile.\n") + .build()) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .addParameter(ArrayTypeName.of(PROFILE_CLASSNAME), "profiles") + .varargs(true) + .returns(getMultipleSenderInterfaceClassName(generatorContext, crossProfileType)) + .build()); + + interfaceBuilder.addMethod( + MethodSpec.methodBuilder("both") + .addJavadoc("Run a method on both the personal and work profile, if accessible.\n") + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(getMultipleSenderInterfaceClassName(generatorContext, crossProfileType)) + .build()); + + if (!crossProfileType.profileConnector().isPresent() + || crossProfileType.profileConnector().get().primaryProfile() != ProfileType.NONE) { + generatePrimarySecondaryMethods(interfaceBuilder); + } + + generatorUtilities.writeClassToFile(interfaceName.packageName(), interfaceBuilder); + } + + private void generatePrimarySecondaryMethods(TypeSpec.Builder interfaceBuilder) { + generatePrimaryMethod(interfaceBuilder); + generateSecondaryMethod(interfaceBuilder); + generateSuppliersMethod(interfaceBuilder); + } + + private void generatePrimaryMethod(TypeSpec.Builder interfaceBuilder) { + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder("primary") + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)); + + if (crossProfileType.profileConnector().isPresent()) { + methodBuilder.addJavadoc( + "Run a method on the primary (" + + Ascii.toLowerCase(crossProfileType.profileConnector().get().primaryProfile().name()) + + ") profile, if accessible.\n\n@see $T#primaryProfile()\n", + CustomProfileConnector.class); + } else { + methodBuilder.addJavadoc( + "Run a method on the primary profile, if accessible.\n\n" + + "@throws $1T if the {@link $2T} does not have a primary profile set\n" + + "@see $2T#primaryProfile()\n", + IllegalStateException.class, + CustomProfileConnector.class); + } + + interfaceBuilder.addMethod(methodBuilder.build()); + } + + private void generateSecondaryMethod(TypeSpec.Builder interfaceBuilder) { + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder("secondary") + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)); + + if (crossProfileType.profileConnector().isPresent()) { + String secondaryProfileName = + crossProfileType.profileConnector().get().primaryProfile().equals(ProfileType.WORK) + ? Ascii.toLowerCase(ProfileType.PERSONAL.name()) + : Ascii.toLowerCase(ProfileType.WORK.name()); + methodBuilder.addJavadoc( + "Run a method on the secondary (" + + secondaryProfileName + + ") profile, if accessible.\n\n@see $T#primaryProfile()\n", + CustomProfileConnector.class); + } else { + methodBuilder.addJavadoc( + "Run a method on the secondary profile, if accessible.\n\n" + + "@throws $1T if the {@link $2T} does not have a primary profile set\n" + + "@see $2T#primaryProfile()\n", + IllegalStateException.class, + CustomProfileConnector.class); + } + + interfaceBuilder.addMethod(methodBuilder.build()); + } + + private void generateSuppliersMethod(TypeSpec.Builder interfaceBuilder) { + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder("suppliers") + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(getMultipleSenderInterfaceClassName(generatorContext, crossProfileType)); + + if (crossProfileType.profileConnector().isPresent()) { + String primaryProfileName = + crossProfileType.profileConnector().get().primaryProfile().equals(ProfileType.WORK) + ? Ascii.toLowerCase(ProfileType.WORK.name()) + : Ascii.toLowerCase(ProfileType.PERSONAL.name()); + String secondaryProfileName = + crossProfileType.profileConnector().get().primaryProfile().equals(ProfileType.WORK) + ? Ascii.toLowerCase(ProfileType.PERSONAL.name()) + : Ascii.toLowerCase(ProfileType.WORK.name()); + methodBuilder + .addJavadoc("Run a method on supplier profiles, if accessible.\n\n") + .addJavadoc( + "<p>When run from the primary ($1L) profile, supplier profiles are the primary ($1L)" + + " and secondary ($2L) profiles. When run from the secondary ($2L) profile," + + " supplier profiles includes only the secondary ($2L) profile.\n\n", + primaryProfileName, + secondaryProfileName) + .addJavadoc("@see $T#primaryProfile()\n", CustomProfileConnector.class); + } else { + methodBuilder + .addJavadoc("Run a method on supplier profiles, if accessible.\n\n") + .addJavadoc( + "<p>When run from the primary profile, supplier profiles are the primary and" + + " secondary profiles. When run from the secondary profile, supplier profiles" + + " includes only the secondary profile.\n\n") + .addJavadoc( + "@throws $1T if the {@link $2T} does not have a primary profile set\n", + IllegalStateException.class, + CustomProfileConnector.class) + .addJavadoc("@see $T#primaryProfile()\n", CustomProfileConnector.class); + } + + interfaceBuilder.addMethod(methodBuilder.build()); + } + + private void generateSingleSenderInterface() { + ClassName interfaceName = getSingleSenderInterfaceClassName(generatorContext, crossProfileType); + + TypeSpec.Builder interfaceBuilder = + TypeSpec.interfaceBuilder(interfaceName) + .addModifiers(Modifier.PUBLIC) + .addJavadoc( + "Interface used for interacting with an instance of {@link $T} on a given" + + " profile.\n\n", + crossProfileType.className()) + .addJavadoc( + "<p>The profile is guaranteed to be available, so no {@link $T} will be thrown for" + + " any call.\n", + UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME); + + for (CrossProfileMethodInfo method : crossProfileType.crossProfileMethods()) { + generateMethodOnSingleSenderInterface(interfaceBuilder, method, crossProfileType); + } + + generatorUtilities.writeClassToFile(interfaceName.packageName(), interfaceBuilder); + } + + private void generateMethodOnSingleSenderInterface( + TypeSpec.Builder interfaceBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + + CodeBlock methodReference = generateMethodReference(crossProfileType, method); + + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder(method.simpleName()) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(method.returnTypeTypeName()) + .addJavadoc("Make a call to {@link $L} on the given profile.\n\n", methodReference); + + for (TypeMirror automaticallyResolvedType : + method.automaticallyResolvedParameterTypes(crossProfileType.supportedTypes())) { + methodBuilder.addJavadoc( + "<p>A {@link $T} will automatically be passed to {@link $L}.\n\n", + automaticallyResolvedType, + methodReference); + } + + methodBuilder + .addJavadoc("@see $L\n", methodReference) + .addExceptions(method.thrownExceptions()) + .addParameters( + GeneratorUtilities.extractParametersFromMethod( + crossProfileType.supportedTypes(), + method.methodElement(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)); + + interfaceBuilder.addMethod(methodBuilder.build()); + } + + private void generateSingleSenderCanThrowInterface() { + ClassName interfaceName = + getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType); + + TypeSpec.Builder interfaceBuilder = + TypeSpec.interfaceBuilder(interfaceName) + .addModifiers(Modifier.PUBLIC) + .addJavadoc( + "Interface used for interacting with a {@link $T} on a given profile.\n", + crossProfileType.className()); + + for (CrossProfileMethodInfo method : crossProfileType.crossProfileMethods()) { + generateMethodOnSingleSenderCanThrowInterface(interfaceBuilder, method, crossProfileType); + } + + interfaceBuilder.addMethod( + MethodSpec.methodBuilder("ifAvailable") + .addJavadoc( + "Make a call, returning a default value in case of error rather than throwing" + + " {@link $T}.\n", + UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns( + IfAvailableGenerator.getIfAvailableClassName(generatorContext, crossProfileType)) + .build()); + + interfaceBuilder.addMethod( + MethodSpec.methodBuilder("timeout") + .addJavadoc( + "Set a timeout to be used when making asynchronous calls to other profiles.\n\n" + + "<p>This overrides any timeout set on the type or method being called.\n") + .addAnnotation( + AnnotationSpec.builder(SuppressWarnings.class) + .addMember("value", "$S", "GoodTime") + .build()) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(interfaceName) + .addParameter(long.class, "timeout") + .build()); + + generatorUtilities.writeClassToFile(interfaceName.packageName(), interfaceBuilder); + } + + private void generateMethodOnSingleSenderCanThrowInterface( + TypeSpec.Builder interfaceBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + + CodeBlock methodReference = generateMethodReference(crossProfileType, method); + + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder(method.simpleName()) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .addExceptions(method.thrownExceptions()) + .returns(method.returnTypeTypeName()) + .addJavadoc("Make a call to {@link $L} on the given profile.\n\n", methodReference) + .addParameters( + GeneratorUtilities.extractParametersFromMethod( + crossProfileType.supportedTypes(), + method.methodElement(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)); + + for (TypeMirror automaticallyResolvedType : + method.automaticallyResolvedParameterTypes(crossProfileType.supportedTypes())) { + methodBuilder.addJavadoc( + "<p>A {@link $T} will automatically be passed to {@link $L}.\n\n", + automaticallyResolvedType, + methodReference); + } + + if (method.isBlocking(generatorContext, crossProfileType)) { + methodBuilder.addException(UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME); + methodBuilder.addJavadoc( + "<p>If an unchecked exception is thrown and this call is made to a profile other than" + + " the current one, a {@link $T} will be thrown with the original exception as the" + + " cause.\n\n", + PROFILE_RUNTIME_EXCEPTION_CLASSNAME); + methodBuilder.addJavadoc( + "@throws $T if the profile is not connected.\n", UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME); + } else if (method.isCrossProfileCallback(generatorContext)) { + methodBuilder.addJavadoc( + "<p>If an unchecked exception is thrown and this call is made to a profile other than" + + " the current one, a {@link $T} will be thrown on another thread with the original" + + " exception as the cause.\n\n", + PROFILE_RUNTIME_EXCEPTION_CLASSNAME); + methodBuilder.addJavadoc( + "<p>If the profile does not exist or is not available, an {@link $T} will be passed into" + + " the {@code exceptionCallback}.\n\n", + UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME); + methodBuilder.addParameter(EXCEPTION_CALLBACK_CLASSNAME, "exceptionCallback"); + } else { + // Future + methodBuilder.addJavadoc( + "<p>If an unchecked exception is thrown and this call is made to a profile other than" + + " the current one, a {@link $T} will be thrown on another thread with the original" + + " exception as the cause.\n\n", + PROFILE_RUNTIME_EXCEPTION_CLASSNAME); + methodBuilder.addJavadoc( + "<p>If the profile does not exist or is not available, the future will be set with an" + + " {@link $T}.\n\n", + UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME); + } + + methodBuilder.addJavadoc("@see $L\n", methodReference); + + interfaceBuilder.addMethod(methodBuilder.build()); + } + + private void generateMultipleSenderInterface() { + ClassName interfaceName = + getMultipleSenderInterfaceClassName(generatorContext, crossProfileType); + + TypeSpec.Builder interfaceBuilder = + TypeSpec.interfaceBuilder(interfaceName) + .addModifiers(Modifier.PUBLIC) + .addJavadoc( + "Interface used for interacting with a {@link $T} on multiple profiles.\n\n", + crossProfileType.className()) + .addJavadoc( + "<p>If any profiles are unavailable, the profile will not be included in the" + + " results. No {@link $T} will be thrown for any call.\n", + UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME); + + for (CrossProfileMethodInfo method : crossProfileType.crossProfileMethods()) { + if (method.isBlocking(generatorContext, crossProfileType)) { + generateBlockingMethodOnMultipleSenderInterface(interfaceBuilder, method, crossProfileType); + } else if (method.isCrossProfileCallback(generatorContext)) { + generateCrossProfileCallbackMethodOnMultipleSenderInterface( + interfaceBuilder, method, crossProfileType); + } else if (method.isFuture(crossProfileType)) { + generateFutureMethodOnMultipleSenderInterface(interfaceBuilder, method, crossProfileType); + } else { + throw new IllegalStateException("Unknown method type: " + method); + } + } + + interfaceBuilder.addMethod( + MethodSpec.methodBuilder("timeout") + .addJavadoc( + "Set a timeout to be used when making asynchronous calls to other profiles.\n\n" + + "<p>This overrides any timeout set on the type or method being called.") + .addAnnotation( + AnnotationSpec.builder(SuppressWarnings.class) + .addMember("value", "$S", "GoodTime") + .build()) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(interfaceName) + .addParameter(long.class, "timeout") + .build()); + + generatorUtilities.writeClassToFile(interfaceName.packageName(), interfaceBuilder); + } + + private void generateBlockingMethodOnMultipleSenderInterface( + TypeSpec.Builder interfaceBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + + TypeName returnType; + + if (!method.thrownExceptions().isEmpty()) { + // We don't add methods with exceptions to the multiplesender interface + return; + } + + if (method.returnType().getKind().equals(TypeKind.VOID)) { + // void is a special case so we don't return a map + returnType = TypeName.VOID; + } else { + TypeName boxedMethodReturnType = + TypeName.get(generatorUtilities.boxIfNecessary(method.returnType())); + returnType = + ParameterizedTypeName.get( + ClassName.get(Map.class), PROFILE_CLASSNAME, boxedMethodReturnType); + } + + CodeBlock methodReference = generateMethodReference(crossProfileType, method); + + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder(method.simpleName()) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(returnType) + .addParameters( + GeneratorUtilities.extractParametersFromMethod( + crossProfileType.supportedTypes(), + method.methodElement(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)) + .addJavadoc("Make a call to {@link $L} on the given profiles.\n\n", methodReference); + + for (TypeMirror automaticallyResolvedType : + method.automaticallyResolvedParameterTypes(crossProfileType.supportedTypes())) { + methodBuilder.addJavadoc( + "<p>A {@link $T} will automatically be passed to {@link $L}.\n\n", + automaticallyResolvedType, + methodReference); + } + + methodBuilder + .addJavadoc( + "<p>If any profiles are not connected, they will not be included in the" + + " results.\n\n") + .addJavadoc( + "<p>If an unchecked exception is thrown on any profile other than the current one," + + " a {@link $T} will be thrown with the original exception as the cause.\n\n", + PROFILE_RUNTIME_EXCEPTION_CLASSNAME) + .addJavadoc("@see $L\n", methodReference); + + interfaceBuilder.addMethod(methodBuilder.build()); + } + + private void generateCrossProfileCallbackMethodOnMultipleSenderInterface( + TypeSpec.Builder interfaceBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + + if (!method.isSimpleCrossProfileCallback(generatorContext)) { + // Non-simple callbacks can't be used with multiple profiles + return; + } + + VariableElement callbackParameter = method.getCrossProfileCallbackParam(generatorContext).get(); + TypeElement callbackType = + generatorContext.elements().getTypeElement(callbackParameter.asType().toString()); + CrossProfileCallbackInterfaceInfo callbackInterface = + CrossProfileCallbackInterfaceInfo.create(callbackType); + + List<ParameterSpec> parameters = + convertCallbackParametersIntoMulti( + GeneratorUtilities.extractParametersFromMethod( + crossProfileType.supportedTypes(), + method.methodElement(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS), + callbackParameter, + callbackInterface); + + CodeBlock methodReference = generateMethodReference(crossProfileType, method); + + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder(method.simpleName()) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .addParameters(parameters) + .addJavadoc("Make a call to {@link $L} on the given profiles.\n\n", methodReference); + + for (TypeMirror automaticallyResolvedType : + method.automaticallyResolvedParameterTypes(crossProfileType.supportedTypes())) { + methodBuilder.addJavadoc( + "<p>A {@link $T} will automatically be passed to {@link $L}.\n\n", + automaticallyResolvedType, + methodReference); + } + + methodBuilder + .addJavadoc( + "<p>If any profiles are not available, they will not be included in the" + + " results.\n\n") + .addJavadoc( + "<p>If an unchecked exception is thrown on any profile other than the current one," + + " a {@link $T} will be thrown on another thread with the original exception" + + " as the cause.\n\n", + PROFILE_RUNTIME_EXCEPTION_CLASSNAME) + .addJavadoc("@see $L\n", methodReference); + + interfaceBuilder.addMethod(methodBuilder.build()); + } + + private void generateFutureMethodOnMultipleSenderInterface( + TypeSpec.Builder interfaceBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + + ClassName rawFutureType = TypeUtils.getRawTypeClassName(method.returnType()); + // We assume all Futures are generic with a single generic type + TypeMirror wrappedType = TypeUtils.extractTypeArguments(method.returnType()).iterator().next(); + + TypeMirror boxedWrappedType = generatorUtilities.boxIfNecessary(wrappedType); + + TypeName mapType = + ParameterizedTypeName.get( + ClassName.get(Map.class), PROFILE_CLASSNAME, ClassName.get(boxedWrappedType)); + + ParameterizedTypeName returnType = ParameterizedTypeName.get(rawFutureType, mapType); + + CodeBlock methodReference = generateMethodReference(crossProfileType, method); + + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder(method.simpleName()) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(returnType) + .addParameters( + GeneratorUtilities.extractParametersFromMethod( + crossProfileType.supportedTypes(), + method.methodElement(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)) + .addJavadoc("Make a call to {@link $L} on the given profiles.\n\n", methodReference); + + for (TypeMirror automaticallyResolvedType : + method.automaticallyResolvedParameterTypes(crossProfileType.supportedTypes())) { + methodBuilder.addJavadoc( + "<p>A {@link $T} will automatically be passed to {@link $L}.\n\n", + automaticallyResolvedType, + methodReference); + } + + methodBuilder + .addJavadoc( + "<p>If any profiles are not available, or if any profiles set the future with an" + + " {@link $T}, they will not be included in the results.\n\n", + Exception.class) + .addJavadoc( + "<p>If an unchecked exception is thrown on any profile other than the current one," + + " a {@link $T} will be thrown on another thread with the original exception" + + " as the cause.\n\n", + PROFILE_RUNTIME_EXCEPTION_CLASSNAME) + .addJavadoc("@see $L\n", methodReference); + + interfaceBuilder.addMethod(methodBuilder.build()); + } + + private List<ParameterSpec> convertCallbackParametersIntoMulti( + List<ParameterSpec> parameters, + VariableElement callbackParameter, + CrossProfileCallbackInterfaceInfo callbackInterface) { + return parameters.stream() + .map( + e -> + e.name.equals(callbackParameter.getSimpleName().toString()) + ? convertCallbackToMulti(e, callbackInterface) + : e) + .collect(toList()); + } + + private ParameterSpec convertCallbackToMulti( + ParameterSpec parameter, CrossProfileCallbackInterfaceInfo callbackInterface) { + return ParameterSpec.builder( + CrossProfileCallbackCodeGenerator.getCrossProfileCallbackMultiInterfaceClassName( + generatorContext, callbackInterface), + parameter.name) + .addModifiers(parameter.modifiers) + .addAnnotations(parameter.annotations) + .build(); + } + + static ClassName getCrossProfileTypeInterfaceClassName( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + return crossProfileType.profileClassName(); + } + + static ClassName getSingleSenderInterfaceClassName( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + return GeneratorUtilities.appendToClassName( + crossProfileType.profileClassName(), "_SingleSender"); + } + + static ClassName getSingleSenderCanThrowInterfaceClassName( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + return GeneratorUtilities.appendToClassName( + crossProfileType.profileClassName(), "_SingleSenderCanThrow"); + } + + static ClassName getMultipleSenderInterfaceClassName( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + return GeneratorUtilities.appendToClassName( + crossProfileType.profileClassName(), "_MultipleSender"); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalCrossProfileClassGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalCrossProfileClassGenerator.java new file mode 100644 index 0000000..f4aad75 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalCrossProfileClassGenerator.java @@ -0,0 +1,421 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.BUNDLER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CONTEXT_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CROSS_PROFILE_CALLBACK_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CROSS_PROFILE_FUTURE_RESULT_WRITER; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.METHOD_RUNNER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_UTILITIES_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.stream.Collectors.joining; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfile; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackInterfaceInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo; +import com.google.android.enterprise.connectedapps.processor.containers.FutureWrapper; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.google.android.enterprise.connectedapps.processor.containers.ProviderClassInfo; +import com.squareup.javapoet.ArrayTypeName; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; +import java.util.Optional; +import java.util.stream.IntStream; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +/** + * Generate the {@code Profile_*_Internal} class for a single {@link CrossProfile} type. + * + * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to + * validate that the annotated code is correct. + */ +final class InternalCrossProfileClassGenerator { + + private boolean generated = false; + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final ProviderClassInfo providerClass; + private final CrossProfileTypeInfo crossProfileType; + + InternalCrossProfileClassGenerator( + GeneratorContext generatorContext, + ProviderClassInfo providerClass, + CrossProfileTypeInfo crossProfileType) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.providerClass = checkNotNull(providerClass); + this.crossProfileType = checkNotNull(crossProfileType); + } + + void generate() { + if (generated) { + throw new IllegalStateException( + "InternalCrossProfileClassGenerator#generate can only be called once"); + } + generated = true; + + generateInternalCrossProfileClass(); + } + + private void generateInternalCrossProfileClass() { + ClassName className = getInternalCrossProfileClassName(generatorContext, crossProfileType); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className).addModifiers(Modifier.PUBLIC, Modifier.FINAL); + + classBuilder.addJavadoc( + "Internal class for {@link $T}.\n\n" + + "<p>This is used by the Connected Apps SDK to dispatch cross-profile calls.\n\n" + + "<p>Cross-profile type identifier: $L.\n", + crossProfileType.crossProfileTypeElement().asType(), + crossProfileType.identifier()); + + classBuilder.addField( + FieldSpec.builder(className, "instance") + .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) + .initializer("new $T()", className) + .build()); + + classBuilder.addField( + FieldSpec.builder(BUNDLER_CLASSNAME, "bundler") + .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) + .initializer( + "new $T()", + BundlerGenerator.getBundlerClassName(generatorContext, crossProfileType)) + .build()); + + if (!crossProfileType.isStatic()) { + ExecutableElement providerMethod = + providerClass.findProviderMethodFor(generatorContext, crossProfileType); + String paramsString = providerMethod.getParameters().isEmpty() ? "()" : "(context)"; + CodeBlock providerMethodCall = + CodeBlock.of("$L$L", providerMethod.getSimpleName(), paramsString); + + classBuilder.addMethod( + MethodSpec.methodBuilder("crossProfileType") + .addParameter(CONTEXT_CLASSNAME, "context") + .returns(crossProfileType.className()) + .addStatement( + "return $T.instance().providerClass(context).$L", + InternalProviderClassGenerator.getInternalProviderClassName( + generatorContext, providerClass), + providerMethodCall) + .build()); + } + + classBuilder.addMethod( + MethodSpec.methodBuilder("bundler") + .returns(BUNDLER_CLASSNAME) + .addStatement("return bundler") + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("instance") + .addModifiers(Modifier.STATIC) + .returns(className) + .addStatement("return instance") + .build()); + + classBuilder.addMethod(MethodSpec.constructorBuilder().addModifiers(Modifier.PRIVATE).build()); + + addMethodsField(classBuilder, crossProfileType); + addCrossProfileTypeMethods(classBuilder, crossProfileType); + addCallMethod(classBuilder); + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private void addMethodsField( + TypeSpec.Builder classBuilder, CrossProfileTypeInfo crossProfileType) { + int totalMethods = crossProfileType.crossProfileMethods().size(); + + classBuilder.addField( + FieldSpec.builder(ArrayTypeName.of(METHOD_RUNNER_CLASSNAME), "methods") + .addModifiers(Modifier.PRIVATE) + .initializer( + "new $T[]{$L}", + METHOD_RUNNER_CLASSNAME, + IntStream.range(0, totalMethods) + .mapToObj(n -> "this::method" + n) + .collect(joining(","))) + .build()); + } + + private void addCrossProfileTypeMethods( + TypeSpec.Builder classBuilder, CrossProfileTypeInfo crossProfileType) { + for (CrossProfileMethodInfo method : crossProfileType.crossProfileMethods()) { + if (method.isBlocking(generatorContext, crossProfileType)) { + addBlockingCrossProfileTypeMethod(classBuilder, method); + } else if (method.isCrossProfileCallback(generatorContext)) { + addCrossProfileCallbackCrossProfileTypeMethod(classBuilder, method); + } else if (method.isFuture(crossProfileType)) { + addFutureCrossProfileTypeMethod(classBuilder, method); + } else { + throw new IllegalStateException("Unknown method type: " + method); + } + } + } + + private void addBlockingCrossProfileTypeMethod( + TypeSpec.Builder classBuilder, CrossProfileMethodInfo method) { + CodeBlock.Builder methodCode = CodeBlock.builder(); + + // parcle is recycled by caller + methodCode.addStatement("$1T returnParcel = $1T.obtain()", PARCEL_CLASSNAME); + + addExtractParametersCode(methodCode, method); + + CodeBlock methodCall = + CodeBlock.of( + "$L.$L($L)", + getCrossProfileTypeReference(method), + method.simpleName(), + method.commaSeparatedParameters( + crossProfileType.supportedTypes(), REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS)); + + if (!method.thrownExceptions().isEmpty()) { + methodCode.beginControlFlow("try"); + } + + if (isPrimitiveOrObjectVoid(method.returnType())) { + methodCode.addStatement(methodCall); + } else { + methodCall = CodeBlock.of("$T returnValue = $L", method.returnType(), methodCall); + methodCode.addStatement(methodCall); + methodCode.add("returnParcel.writeInt(0); // No errors\n"); + methodCode.addStatement( + "bundler.writeToParcel(returnParcel, returnValue, $L, /* flags= */ 0)", + TypeUtils.generateBundlerType(method.returnType())); + } + + if (!method.thrownExceptions().isEmpty()) { + for (TypeName exceptionType : method.thrownExceptions()) { + methodCode.nextControlFlow("catch ($L e)", exceptionType); + methodCode.add("returnParcel.writeInt(1); // Errors\n"); + methodCode.addStatement( + "$T.writeThrowableToParcel(returnParcel, e)", PARCEL_UTILITIES_CLASSNAME); + } + methodCode.endControlFlow(); + } + + methodCode.addStatement("return returnParcel"); + + classBuilder.addMethod( + MethodSpec.methodBuilder("method" + method.identifier()) + .addModifiers(Modifier.PRIVATE) + .returns(PARCEL_CLASSNAME) + .addParameter(CONTEXT_CLASSNAME, "context") + .addParameter(PARCEL_CLASSNAME, "params") + .addParameter(CROSS_PROFILE_CALLBACK_CLASSNAME, "callback") + .addCode(methodCode.build()) + .addJavadoc( + "Call $1L and return a {@link $2T} containing the return value.\n\n" + + "<p>The {@link $2T} must be recycled after use.\n", + GeneratorUtilities.methodJavadocReference(method.methodElement()), + PARCEL_CLASSNAME) + .build()); + } + + private void addCrossProfileCallbackCrossProfileTypeMethod( + TypeSpec.Builder classBuilder, CrossProfileMethodInfo method) { + CodeBlock.Builder methodCode = CodeBlock.builder(); + + // parcel is recycled by caller + methodCode.addStatement("$1T returnParcel = $1T.obtain()", PARCEL_CLASSNAME); + + addExtractParametersCode(methodCode, method); + + createCrossProfileCallbackParameter(methodCode, method); + + CodeBlock methodCall = + CodeBlock.of( + "$L.$L($L)", + getCrossProfileTypeReference(method), + method.simpleName(), + method.commaSeparatedParameters( + crossProfileType.supportedTypes(), REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS)); + + if (isPrimitiveOrObjectVoid(method.returnType())) { + methodCode.addStatement(methodCall); + } else { + methodCall = CodeBlock.of("$T returnValue = $L", method.returnType(), methodCall); + methodCode.addStatement(methodCall); + methodCode.add("returnParcel.writeInt(0); // No errors\n"); + methodCode.addStatement( + "bundler.writeToParcel(returnParcel, returnValue, $L, /* flags= */ 0)", + TypeUtils.generateBundlerType(method.returnType())); + } + + methodCode.addStatement("return returnParcel"); + + classBuilder.addMethod( + MethodSpec.methodBuilder("method" + method.identifier()) + .addModifiers(Modifier.PRIVATE) + .returns(PARCEL_CLASSNAME) + .addParameter(CONTEXT_CLASSNAME, "context") + .addParameter(PARCEL_CLASSNAME, "params") + // TODO: This should be renamed to "callback" once we prefix unpacked parameter names + // (without doing this, a param named "callback" will cause a compile error) + .addParameter(CROSS_PROFILE_CALLBACK_CLASSNAME, "crossProfileCallback") + .addCode(methodCode.build()) + .addJavadoc( + "Call $1L, and link the callback to {@code crossProfileCallback}.\n\n" + + "@return An empty parcel. This must be recycled after use.\n", + GeneratorUtilities.methodJavadocReference(method.methodElement())) + .build()); + } + + private void addFutureCrossProfileTypeMethod( + TypeSpec.Builder classBuilder, CrossProfileMethodInfo method) { + CodeBlock.Builder methodCode = CodeBlock.builder(); + + // parcel is recycled by caller + methodCode.addStatement("$1T returnParcel = $1T.obtain()", PARCEL_CLASSNAME); + + addExtractParametersCode(methodCode, method); + + CodeBlock methodCall = + CodeBlock.of( + "$L.$L($L)", + getCrossProfileTypeReference(method), + method.simpleName(), + method.commaSeparatedParameters( + crossProfileType.supportedTypes(), REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS)); + + methodCode.addStatement("$T future = $L", method.returnType(), methodCall); + + TypeMirror rawFutureType = TypeUtils.removeTypeArguments(method.returnType()); + + FutureWrapper futureWrapper = + crossProfileType.supportedTypes().getType(rawFutureType).getFutureWrapper().get(); + // This assumes every Future is generic with one type argument + TypeMirror wrappedReturnType = + TypeUtils.extractTypeArguments(method.returnType()).iterator().next(); + methodCode.addStatement( + "$T.writeFutureResult(future, new $T<>(callback, bundler, $L))", + futureWrapper.wrapperClassName(), + CROSS_PROFILE_FUTURE_RESULT_WRITER, + TypeUtils.generateBundlerType(wrappedReturnType)); + + // TODO: Can this just return null? where does it go? that'd avoid having to obtain/recycle + methodCode.addStatement("return returnParcel"); + + classBuilder.addMethod( + MethodSpec.methodBuilder("method" + method.identifier()) + .addModifiers(Modifier.PRIVATE) + .returns(PARCEL_CLASSNAME) + .addParameter(CONTEXT_CLASSNAME, "context") + .addParameter(PARCEL_CLASSNAME, "params") + .addParameter(CROSS_PROFILE_CALLBACK_CLASSNAME, "callback") + .addCode(methodCode.build()) + .addJavadoc( + "Call $1L, and link the returned future to {@code crossProfileCallback}.\n\n" + + "@return An empty parcel. This must be recycled after use.\n", + GeneratorUtilities.methodJavadocReference(method.methodElement())) + .build()); + } + + private void createCrossProfileCallbackParameter( + CodeBlock.Builder methodCode, CrossProfileMethodInfo method) { + VariableElement asyncCallbackParam = + method.getCrossProfileCallbackParam(generatorContext).get(); + + TypeElement callbackType = + generatorContext.elements().getTypeElement(asyncCallbackParam.asType().toString()); + CrossProfileCallbackInterfaceInfo callbackInterface = + CrossProfileCallbackInterfaceInfo.create(callbackType); + + methodCode.addStatement( + "$T $L = new $L(crossProfileCallback, bundler)", + asyncCallbackParam.asType(), + asyncCallbackParam.getSimpleName(), + CrossProfileCallbackCodeGenerator.getCrossProfileCallbackReceiverClassName( + generatorContext, callbackInterface)); + } + + private static boolean isPrimitiveOrObjectVoid(TypeMirror typeMirror) { + return typeMirror.getKind().equals(TypeKind.VOID) + || typeMirror.toString().equals("java.lang.Void"); + } + + private void addExtractParametersCode(CodeBlock.Builder code, CrossProfileMethodInfo method) { + Optional<VariableElement> callbackParameter = + method.getCrossProfileCallbackParam(generatorContext); + for (VariableElement parameter : method.methodElement().getParameters()) { + if (callbackParameter.isPresent() + && callbackParameter.get().getSimpleName().equals(parameter.getSimpleName())) { + continue; // Don't extract a callback parameter + } + if (crossProfileType.supportedTypes().isAutomaticallyResolved(parameter.asType())) { + continue; + } + code.addStatement( + "@SuppressWarnings(\"unchecked\") $1T $2L = ($1T) bundler.readFromParcel(params, $3L)", + parameter.asType(), + parameter.getSimpleName().toString(), + TypeUtils.generateBundlerType(parameter.asType())); + } + } + + private static void addCallMethod(TypeSpec.Builder classBuilder) { + classBuilder.addMethod( + MethodSpec.methodBuilder("call") + .addModifiers(Modifier.PUBLIC) + .returns(PARCEL_CLASSNAME) + .addParameter(CONTEXT_CLASSNAME, "context") + .addParameter(int.class, "methodIdentifier") + .addParameter(PARCEL_CLASSNAME, "params") + .addParameter(CROSS_PROFILE_CALLBACK_CLASSNAME, "callback") + .beginControlFlow("if (methodIdentifier >= methods.length)") + .addStatement( + "throw new $T(\"Invalid method identifier\" + methodIdentifier)", + IllegalArgumentException.class) + .endControlFlow() + .addStatement("return methods[methodIdentifier].call(context, params, callback)") + .addJavadoc( + "Call the method referenced by {@code methodIdentifier}.\n\n" + + "<p>If the method is synchronous, this will return a {@link $1T} containing" + + " the return value, otherwise it will return an empty {@link $1T}. The" + + " {@link $1T} must be recycled after use.\n", + PARCEL_CLASSNAME) + .build()); + } + + private CodeBlock getCrossProfileTypeReference(CrossProfileMethodInfo method) { + if (method.isStatic()) { + return CodeBlock.of("$1T", crossProfileType.className()); + } + return CodeBlock.of("crossProfileType(context)"); + } + + static ClassName getInternalCrossProfileClassName( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + return GeneratorUtilities.appendToClassName(crossProfileType.profileClassName(), "_Internal"); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalProviderClassGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalProviderClassGenerator.java new file mode 100644 index 0000000..c02bbfb --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalProviderClassGenerator.java @@ -0,0 +1,186 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CONTEXT_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CROSS_PROFILE_CALLBACK_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_CLASSNAME; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.google.android.enterprise.connectedapps.processor.containers.ProviderClassInfo; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeSpec; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.PackageElement; + +/** + * Generate the {@code Profile_*_Internal} class for a single provider class. + * + * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to + * validate that the annotated code is correct. + */ +final class InternalProviderClassGenerator { + + private boolean generated = false; + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final ProviderClassInfo providerClass; + + InternalProviderClassGenerator( + GeneratorContext generatorContext, ProviderClassInfo providerClass) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.providerClass = checkNotNull(providerClass); + } + + void generate() { + if (generated) { + throw new IllegalStateException( + "InternalProviderClassGenerator#generate can only be called once"); + } + generated = true; + + generateInternalProviderClassClass(); + } + + private void generateInternalProviderClassClass() { + ClassName className = getInternalProviderClassName(generatorContext, providerClass); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className).addModifiers(Modifier.PUBLIC, Modifier.FINAL); + + classBuilder.addJavadoc( + "Internal provider class for $L\n", + providerClass.providerClassElement().asType().toString()); + + classBuilder.addField( + FieldSpec.builder(className, "instance") + .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) + .initializer("new $T()", className) + .build()); + + classBuilder.addField( + FieldSpec.builder(providerClass.className(), "providerClass") + .addModifiers(Modifier.PRIVATE) + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("instance") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(className) + .addStatement("return instance") + .addJavadoc("Get the singleton instance of this provider.\n") + .build()); + + String providerClassConstructorParameters = + providerClass.publicConstructorArgumentTypes().isEmpty() + ? "" + : "context"; // We only allow a context or no-args + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PRIVATE) + + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("providerClass") + .addJavadoc( + "Get a singleton instance of the original class which caused generation of this" + + " class.\n") + .addModifiers(Modifier.PUBLIC) + .addParameter(CONTEXT_CLASSNAME, "context") + .returns(providerClass.className()) + .addComment( + "This can't be constructed with the field declaration as sometimes these classes" + + " need arguments") + .beginControlFlow("if (providerClass == null)") + .beginControlFlow( + "synchronized ($T.class)", + getInternalProviderClassName(generatorContext, providerClass)) + .beginControlFlow("if (providerClass == null)") + .addStatement( + "this.providerClass = new $T($L)", + providerClass.className(), + providerClassConstructorParameters) + .endControlFlow() + .endControlFlow() + .endControlFlow() + .addStatement("return providerClass") + .build()); + + addCallMethod(classBuilder); + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private void addCallMethod(TypeSpec.Builder classBuilder) { + CodeBlock.Builder methodCode = CodeBlock.builder(); + + for (CrossProfileTypeInfo crossProfileType : providerClass.allCrossProfileTypes()) { + addCrossProfileTypeDispatcher(methodCode, crossProfileType); + } + + methodCode.addStatement( + "throw new $T(\"Unknown type identifier \" + crossProfileTypeIdentifier)", + IllegalArgumentException.class); + + classBuilder.addMethod( + MethodSpec.methodBuilder("call") + .addModifiers(Modifier.PUBLIC) + .returns(PARCEL_CLASSNAME) + .addParameter(CONTEXT_CLASSNAME, "context") + .addParameter(long.class, "crossProfileTypeIdentifier") + .addParameter(int.class, "methodIdentifier") + .addParameter(PARCEL_CLASSNAME, "params") + .addParameter(CROSS_PROFILE_CALLBACK_CLASSNAME, "callback") + .addCode(methodCode.build()) + .addJavadoc( + "Call the {@code call} method on the internal type referenced by the {@code" + + " crossProfileTypeIdentifier}.\n\n" + + "@return A {@link $1T} which contains the return value (if a synchronous" + + " call) or is empty\n (if asynchronous). This {@link $1T} must be recycled" + + " after use.\n", + PARCEL_CLASSNAME) + .build()); + } + + private void addCrossProfileTypeDispatcher( + CodeBlock.Builder methodCode, CrossProfileTypeInfo crossProfileType) { + methodCode.beginControlFlow( + "if (crossProfileTypeIdentifier == $LL)", crossProfileType.identifier()); + methodCode.addStatement( + "return $T.instance().call(context, methodIdentifier, params, callback)", + InternalCrossProfileClassGenerator.getInternalCrossProfileClassName( + generatorContext, crossProfileType)); + methodCode.endControlFlow(); + } + + static ClassName getInternalProviderClassName( + GeneratorContext generatorContext, ProviderClassInfo providerClass) { + PackageElement originalPackage = + generatorContext.elements().getPackageOf(providerClass.providerClassElement()); + String internalProviderClassName = + String.format("Profile_%s_Internal", providerClass.simpleName()); + + return ClassName.get(originalPackage.getQualifiedName().toString(), internalProviderClassName); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/LateValidator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/LateValidator.java new file mode 100644 index 0000000..4b55483 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/LateValidator.java @@ -0,0 +1,213 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.validationMessageFormatterFor; +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.validationMessageFormatterForClass; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; + +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.google.android.enterprise.connectedapps.processor.containers.ProfileConnectorInfo; +import com.google.android.enterprise.connectedapps.processor.containers.ProviderClassInfo; +import com.google.common.collect.Streams; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; +import javax.lang.model.element.Element; +import javax.lang.model.type.TypeMirror; +import javax.tools.Diagnostic.Kind; + +/** Validator to check that annotations have been used correctly before generating code. */ +final class LateValidator { + + private final GeneratorContext generatorContext; + + private static final String PROVIDER_CLASS_DIFFERENT_CONNECTOR_ERROR = + "All @CROSS_PROFILE_ANNOTATION types provided by a provider class must use the same" + + " ProfileConnector"; + private static final String CONFIGURATION_DIFFERENT_CONNECTOR_ERROR = + "All @CROSS_PROFILE_ANNOTATION types specified in the same configuration must use the same" + + " ProfileConnector"; + private static final String INCORRECT_SERVICE_CLASS = + "The class specified by serviceClass must match the serviceClassName given by the" + + " ProfileConnector"; + private static final String STATICTYPES_ERROR = + "@CROSS_PROFILE_ANNOTATION classes referenced in @CROSS_PROFILE_PROVIDER_ANNOTATION" + + " staticTypes annotations must not have non-static @CROSS_PROFILE_ANNOTATION annotated" + + " methods"; + + LateValidator(GeneratorContext generatorContext) { + this.generatorContext = checkNotNull(generatorContext); + } + + /** + * Validate code. + * + * <p>This will show errors for all issues found. It will not terminate upon finding the first + * error. + * + * @return True if the state is valid + */ + boolean validate() { + return Stream.of( + validateProviderClasses(generatorContext.providers()), + validateConfigurations(generatorContext.configurations())) + .allMatch(b -> b); + } + + private boolean validateProviderClasses(Collection<ProviderClassInfo> providerClasses) { + boolean isValid = true; + + for (ProviderClassInfo providerClass : providerClasses) { + isValid = validateProviderClass(providerClass) && isValid; + } + + return isValid; + } + + private boolean validateProviderClass(ProviderClassInfo providerClass) { + boolean isValid = true; + + if (getConnectorQualifiedNamesUsedInProviderClass(providerClass).size() > 1) { + showError( + PROVIDER_CLASS_DIFFERENT_CONNECTOR_ERROR, + providerClass.providerClassElement(), + validationMessageFormatterForClass(providerClass.providerClassElement())); + isValid = false; + } + + for (CrossProfileTypeInfo crossProfileType : providerClass.staticTypes()) { + if (!crossProfileType.isStatic()) { + showError(STATICTYPES_ERROR, providerClass.providerClassElement()); + isValid = false; + } + } + + return isValid; + } + + private boolean validateConfigurations(Collection<CrossProfileConfigurationInfo> configurations) { + boolean isValid = true; + + for (CrossProfileConfigurationInfo configuration : configurations) { + isValid = validateConfiguration(configuration) && isValid; + } + + return isValid; + } + + private boolean validateConfiguration(CrossProfileConfigurationInfo configuration) { + boolean isValid = true; + + isValid = + validateCrossProfileTypesHaveUniqueIdentifiers( + configuration.providers().stream() + .flatMap(m -> m.allCrossProfileTypes().stream()) + .collect(toList())) + && isValid; + + if (getConnectorQualifiedNamesUsedInConfiguration(configuration).size() > 1) { + showError(CONFIGURATION_DIFFERENT_CONNECTOR_ERROR, configuration.configurationElement()); + isValid = false; + } + + if (configuration.serviceClass().isPresent() + && !configuration + .serviceClass() + .get() + .toString() + .equals(configuration.profileConnector().serviceName().toString())) { + showError(INCORRECT_SERVICE_CLASS, configuration.configurationElement()); + isValid = false; + } + + return isValid; + } + + private static Collection<String> getConnectorQualifiedNamesUsedInConfiguration( + CrossProfileConfigurationInfo configuration) { + Set<String> connectorQualifiedNames = + configuration.providers().stream() + .flatMap(m -> getConnectorQualifiedNamesUsedInProviderClass(m).stream()) + .collect(toSet()); + connectorQualifiedNames.add( + configuration.profileConnector().connectorElement().asType().toString()); + return connectorQualifiedNames; + } + + private boolean validateCrossProfileTypesHaveUniqueIdentifiers( + Collection<CrossProfileTypeInfo> crossProfileTypes) { + boolean isValid = true; + Map<Long, List<CrossProfileTypeInfo>> crossProfileTypeByIdentifier = + crossProfileTypes.stream().collect(groupingBy(CrossProfileTypeInfo::identifier)); + + for (long identifier : crossProfileTypeByIdentifier.keySet()) { + if (crossProfileTypeByIdentifier.get(identifier).size() > 1) { + isValid = false; + String crossProfileTypesString = + crossProfileTypeByIdentifier.get(identifier).stream() + .map(m -> m.className().toString()) + .collect(joining(",")); + showError( + "The following cross-profile types all share an identifier(" + + identifier + + "): " + + crossProfileTypesString, + crossProfileTypeByIdentifier.get(identifier).get(0).crossProfileTypeElement()); + } + } + + return isValid; + } + + private static Collection<String> getConnectorQualifiedNamesUsedInProviderClass( + ProviderClassInfo providerClass) { + return providerClass.allCrossProfileTypes().stream() + .map(CrossProfileTypeInfo::profileConnector) + .flatMap(Streams::stream) + .map(ProfileConnectorInfo::connectorElement) + .map(Element::asType) + .map(TypeMirror::toString) + .collect(toSet()); + } + + private void showError( + String errorText, + Element errorElement, + ValidationMessageFormatter validationMessageFormatter) { + showErrorPreformatted(validationMessageFormatter.format(errorText), errorElement); + } + + private void showError(String errorText, Element errorElement) { + showErrorPreformatted( + validationMessageFormatterFor(errorElement).format(errorText), errorElement); + } + + private void showErrorPreformatted(String errorText, Element errorElement) { + generatorContext + .processingEnv() + .getMessager() + .printMessage(Kind.ERROR, errorText, errorElement); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/MultipleProfilesGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/MultipleProfilesGenerator.java new file mode 100644 index 0000000..38b8038 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/MultipleProfilesGenerator.java @@ -0,0 +1,392 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.ASYNC_CALLBACK_PARAM_MULTIMERGER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CALLBACK_MERGER_EXCEPTION_CALLBACK_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.stream.Collectors.toList; + +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackInterfaceInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo; +import com.google.android.enterprise.connectedapps.processor.containers.FutureWrapper; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.squareup.javapoet.AnnotationSpec; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +/** + * Generate the {@code Profile_*_MultipleProfiles} class for a single cross-profile type. + * + * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to + * validate that the annotated code is correct. + */ +final class MultipleProfilesGenerator { + + private boolean generated = false; + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final CrossProfileTypeInfo crossProfileType; + + MultipleProfilesGenerator( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.crossProfileType = checkNotNull(crossProfileType); + } + + void generate() { + if (generated) { + throw new IllegalStateException("MultipleProfilesGenerator#generate can only be called once"); + } + generated = true; + + generateMultipleProfilesClass(); + } + + private void generateMultipleProfilesClass() { + ClassName className = getMultipleProfilesClassName(generatorContext, crossProfileType); + + ClassName multipleSenderInterface = + InterfaceGenerator.getMultipleSenderInterfaceClassName(generatorContext, crossProfileType); + ClassName singleSenderCanThrowInterfaceName = + InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName( + generatorContext, crossProfileType); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addJavadoc( + "Default implementation of {@link $T}.\n\n" + + "<p>Wraps a number of {@link $T} instances and merges their return" + + " values.\n", + multipleSenderInterface, + singleSenderCanThrowInterfaceName) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addSuperinterface(multipleSenderInterface); + + ParameterizedTypeName senderMapType = + ParameterizedTypeName.get( + ClassName.get(Map.class), PROFILE_CLASSNAME, singleSenderCanThrowInterfaceName); + + classBuilder.addField(senderMapType, "senders", Modifier.PRIVATE, Modifier.FINAL); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(senderMapType, "senders") + .addStatement("this.senders = senders") + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("timeout") + .addAnnotation(Override.class) + .addAnnotation( + AnnotationSpec.builder(SuppressWarnings.class) + .addMember("value", "$S", "GoodTime") + .build()) + .addModifiers(Modifier.PUBLIC) + .returns(className) + .addParameter(long.class, "timeout") + .beginControlFlow("for ($T senderProfile : senders.keySet())", PROFILE_CLASSNAME) + .addStatement("senders.put(senderProfile, senders.get(senderProfile).timeout(timeout))") + .endControlFlow() + .addStatement("return this") + .build()); + + for (CrossProfileMethodInfo method : crossProfileType.crossProfileMethods()) { + if (method.isBlocking(generatorContext, crossProfileType)) { + generateBlockingMethodOnMultipleProfilesClass(classBuilder, method, crossProfileType); + } else if (method.isCrossProfileCallback(generatorContext)) { + generateCrossProfileCallbackMethodOnMultipleProfilesClass( + classBuilder, method, crossProfileType); + } else if (method.isFuture(crossProfileType)) { + generateFutureMethodOnMultipleProfilesClass(classBuilder, method, crossProfileType); + } else { + throw new IllegalStateException("Unknown method type: " + method); + } + } + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private void generateBlockingMethodOnMultipleProfilesClass( + TypeSpec.Builder classBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + + if (!method.thrownExceptions().isEmpty()) { + // We don't add methods with exceptions to multiple profiles + return; + } + + TypeName returnType; + if (method.returnType().getKind().equals(TypeKind.VOID)) { + // void is a special case so we don't return a map + returnType = TypeName.VOID; + } else { + TypeName boxedMethodReturnType = + TypeName.get(generatorUtilities.boxIfNecessary(method.returnType())); + returnType = + ParameterizedTypeName.get( + ClassName.get(Map.class), PROFILE_CLASSNAME, boxedMethodReturnType); + } + + String methodName = method.simpleName(); + String params = + method.commaSeparatedParameters( + crossProfileType.supportedTypes(), REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS); + + CodeBlock methodCall = CodeBlock.of("$L($L)", methodName, params); + + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder(methodName) + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(returnType) + .addParameters( + GeneratorUtilities.extractParametersFromMethod( + crossProfileType.supportedTypes(), + method.methodElement(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)); + + if (method.returnType().getKind().equals(TypeKind.VOID)) { + methodBuilder.beginControlFlow( + "for ($T sender : senders.values())", + InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName( + generatorContext, crossProfileType)); + methodBuilder.beginControlFlow("try"); + methodBuilder.addStatement("sender.$L", methodCall); + methodBuilder.nextControlFlow("catch ($T e)", UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME); + methodBuilder.addComment( + "If the profile is not available we just don't include it in results"); + methodBuilder.endControlFlow(); + methodBuilder.endControlFlow(); + } else { + methodBuilder.addStatement("$T returnValue = new $T<>()", returnType, HashMap.class); + methodBuilder.beginControlFlow( + "for ($T senderIdentifier : senders.keySet())", PROFILE_CLASSNAME); + methodBuilder.addStatement( + "$T sender = senders.get(senderIdentifier)", + InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName( + generatorContext, crossProfileType)); + methodBuilder.beginControlFlow("try"); + methodBuilder.addStatement("returnValue.put(senderIdentifier, sender.$L)", methodCall); + methodBuilder.nextControlFlow("catch ($T e)", UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME); + methodBuilder.addComment( + "If the profile is not available we just don't include it in results"); + methodBuilder.endControlFlow(); + methodBuilder.endControlFlow(); + methodBuilder.addStatement("return returnValue"); + } + + classBuilder.addMethod(methodBuilder.build()); + } + + private void generateCrossProfileCallbackMethodOnMultipleProfilesClass( + TypeSpec.Builder classBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + + if (!method.isSimpleCrossProfileCallback(generatorContext)) { + // Non-simple callbacks can't be used with multiple profiles + return; + } + + String methodName = method.simpleName(); + + VariableElement callbackParameter = method.getCrossProfileCallbackParam(generatorContext).get(); + TypeElement callbackType = + generatorContext.elements().getTypeElement(callbackParameter.asType().toString()); + CrossProfileCallbackInterfaceInfo callbackInterface = + CrossProfileCallbackInterfaceInfo.create(callbackType); + + List<ParameterSpec> parameters = + convertCallbackParametersIntoMulti( + GeneratorUtilities.extractParametersFromMethod( + crossProfileType.supportedTypes(), + method.methodElement(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS), + callbackParameter, + callbackInterface); + + TypeMirror paramType = + callbackInterface.methods().get(0).getParameters().isEmpty() + ? generatorContext.elements().getTypeElement("java.lang.Void").asType() + : callbackInterface.methods().get(0).getParameters().get(0).asType(); + + if (paramType.getKind().isPrimitive()) { + PrimitiveType primitiveType = (PrimitiveType) paramType; + paramType = generatorContext.types().boxedClass(primitiveType).asType(); + } + + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder(methodName) + .addModifiers(Modifier.PUBLIC) + .addParameters(parameters) + .addStatement( + "$1T mergedResultListener = new $1T($2L)", + CrossProfileCallbackCodeGenerator.getCrossProfileCallbackMultiMergerResultClassName( + generatorContext, callbackInterface), + callbackParameter.getSimpleName()) + .addStatement( + "$1T<$2T> merger = new $1T<>(senders.size(), mergedResultListener)", + ASYNC_CALLBACK_PARAM_MULTIMERGER_CLASSNAME, + paramType); + + String params = + method.commaSeparatedParameters( + crossProfileType.supportedTypes(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS, + (p) -> + callbackParameter.getSimpleName().contentEquals(p) + ? generateMergerInputConstructor(callbackInterface) + : p); + + methodBuilder.beginControlFlow( + "for ($T senderIdentifier : senders.keySet())", PROFILE_CLASSNAME); + methodBuilder.addStatement( + "$T sender = senders.get(senderIdentifier)", + InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName( + generatorContext, crossProfileType)); + + methodBuilder.addStatement( + "sender.$L($L, new $T<$T>(senderIdentifier, merger))", + methodName, + params, + CALLBACK_MERGER_EXCEPTION_CALLBACK_CLASSNAME, + paramType); + methodBuilder.endControlFlow(); + + classBuilder.addMethod(methodBuilder.build()); + } + + private void generateFutureMethodOnMultipleProfilesClass( + TypeSpec.Builder classBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + + ClassName rawFutureType = TypeUtils.getRawTypeClassName(method.returnType()); + FutureWrapper futureWrapper = + crossProfileType + .supportedTypes() + .getType(TypeUtils.removeTypeArguments(method.returnType())) + .getFutureWrapper() + .get(); + // We assume all Futures are generic with a single generic type + TypeMirror wrappedType = TypeUtils.extractTypeArguments(method.returnType()).iterator().next(); + + TypeMirror boxedWrappedType = generatorUtilities.boxIfNecessary(wrappedType); + + TypeName mapType = + ParameterizedTypeName.get( + ClassName.get(Map.class), PROFILE_CLASSNAME, ClassName.get(boxedWrappedType)); + + ParameterizedTypeName returnType = ParameterizedTypeName.get(rawFutureType, mapType); + + String methodName = method.simpleName(); + String params = + method.commaSeparatedParameters( + crossProfileType.supportedTypes(), REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS); + + CodeBlock methodCall = CodeBlock.of("$L($L)", methodName, params); + + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder(methodName) + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(returnType) + .addParameters( + GeneratorUtilities.extractParametersFromMethod( + crossProfileType.supportedTypes(), + method.methodElement(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)); + + methodBuilder.addStatement( + "$T<$T, $T> results = new $T<>()", + Map.class, + PROFILE_CLASSNAME, + method.returnType(), + HashMap.class); + methodBuilder.beginControlFlow( + "for ($T senderIdentifier : senders.keySet())", PROFILE_CLASSNAME); + methodBuilder.addStatement( + "$T sender = senders.get(senderIdentifier)", + InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName( + generatorContext, crossProfileType)); + methodBuilder.addStatement("results.put(senderIdentifier, sender.$L)", methodCall); + methodBuilder.endControlFlow(); + methodBuilder.addStatement("return $T.groupResults(results)", futureWrapper.wrapperClassName()); + + classBuilder.addMethod(methodBuilder.build()); + } + + private List<ParameterSpec> convertCallbackParametersIntoMulti( + List<ParameterSpec> parameters, + VariableElement callbackParameter, + CrossProfileCallbackInterfaceInfo callbackInterface) { + return parameters.stream() + .map( + e -> + e.name.equals(callbackParameter.getSimpleName().toString()) + ? convertCallbackToMulti(e, callbackInterface) + : e) + .collect(toList()); + } + + private String generateMergerInputConstructor( + CrossProfileCallbackInterfaceInfo callbackInterface) { + return CodeBlock.of( + "new $T(senderIdentifier, merger)", + CrossProfileCallbackCodeGenerator.getCrossProfileCallbackMultiMergerInputClassName( + generatorContext, callbackInterface)) + .toString(); + } + + private ParameterSpec convertCallbackToMulti( + ParameterSpec parameter, CrossProfileCallbackInterfaceInfo callbackInterface) { + return ParameterSpec.builder( + CrossProfileCallbackCodeGenerator.getCrossProfileCallbackMultiInterfaceClassName( + generatorContext, callbackInterface), + parameter.name) + .addModifiers(parameter.modifiers) + .addAnnotations(parameter.annotations) + .build(); + } + + static ClassName getMultipleProfilesClassName( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + return GeneratorUtilities.appendToClassName( + crossProfileType.profileClassName(), "_MultipleProfiles"); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/OtherProfileGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/OtherProfileGenerator.java new file mode 100644 index 0000000..ace2f9e --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/OtherProfileGenerator.java @@ -0,0 +1,379 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.EXCEPTION_CALLBACK_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.LOCAL_CALLBACK_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_CONNECTOR_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileAnnotation; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackInterfaceInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo; +import com.google.android.enterprise.connectedapps.processor.containers.FutureWrapper; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.squareup.javapoet.AnnotationSpec; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +/** + * Generate the {@code Profile_*_OtherProfile} class for a single cross-profile type. + * + * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to + * validate that the annotated code is correct. + */ +final class OtherProfileGenerator { + + private boolean generated = false; + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final CrossProfileTypeInfo crossProfileType; + + OtherProfileGenerator(GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.crossProfileType = checkNotNull(crossProfileType); + } + + void generate() { + if (generated) { + throw new IllegalStateException("OtherProfileGenerator#generate can only be called once"); + } + generated = true; + + generateOtherProfileClass(); + } + + private void generateOtherProfileClass() { + ClassName className = getOtherProfileClassName(generatorContext, crossProfileType); + + ClassName singleSenderCanThrowInterface = + InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName( + generatorContext, crossProfileType); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addJavadoc( + "Implementation of {@link $T} used when interacting with the other profile.\n", + singleSenderCanThrowInterface) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addSuperinterface(singleSenderCanThrowInterface); + + ClassName connectorClassName = + crossProfileType.profileConnector().isPresent() + ? crossProfileType.profileConnector().get().connectorClassName() + : PROFILE_CONNECTOR_CLASSNAME; + + classBuilder.addField(connectorClassName, "connector", Modifier.PRIVATE, Modifier.FINAL); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(connectorClassName, "connector") + .beginControlFlow("if (connector == null)") + .addStatement("throw new $T()", NullPointerException.class) + .endControlFlow() + .addStatement("this.connector = connector") + .build()); + + classBuilder.addField( + FieldSpec.builder(long.class, "timeout") + .addAnnotation( + AnnotationSpec.builder(SuppressWarnings.class) + .addMember("value", "$S", "GoodTime") + .build()) + .addModifiers(Modifier.PRIVATE) + .initializer("$L", CrossProfileAnnotation.TIMEOUT_MILLIS_NOT_SET) + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("timeout") + .addAnnotation(Override.class) + .addAnnotation( + AnnotationSpec.builder(SuppressWarnings.class) + .addMember("value", "$S", "GoodTime") + .build()) + .addModifiers(Modifier.PUBLIC) + .returns(className) + .addParameter(long.class, "timeout") + .addStatement("this.timeout = timeout") + .addStatement("return this") + .build()); + + ClassName ifAvailableClass = + IfAvailableGenerator.getIfAvailableClassName(generatorContext, crossProfileType); + + classBuilder.addMethod( + MethodSpec.methodBuilder("ifAvailable") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(ifAvailableClass) + .addStatement("return new $T(this)", ifAvailableClass) + .build()); + + for (CrossProfileMethodInfo method : crossProfileType.crossProfileMethods()) { + if (method.isBlocking(generatorContext, crossProfileType)) { + generateBlockingMethodOnOtherProfileClass(classBuilder, method, crossProfileType); + } else if (method.isCrossProfileCallback(generatorContext)) { + generateCrossProfileCallbackMethodOnOtherProfileClass( + classBuilder, method, crossProfileType); + } else if (method.isFuture(crossProfileType)) { + generateFutureMethodOnOtherProfileClass(classBuilder, method, crossProfileType); + } else { + throw new IllegalStateException("Unknown method type: " + method); + } + } + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private void generateBlockingMethodOnOtherProfileClass( + TypeSpec.Builder classBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder(method.simpleName()) + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .addExceptions(method.thrownExceptions()) + .addException(UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME) + .returns(method.returnTypeTypeName()) + .addParameters( + GeneratorUtilities.extractParametersFromMethod( + crossProfileType.supportedTypes(), + method.methodElement(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)); + + methodBuilder.addStatement( + "$1T internalCrossProfileClass = $1T.instance()", + InternalCrossProfileClassGenerator.getInternalCrossProfileClassName( + generatorContext, crossProfileType)); + + // parcel is recycled in this method + methodBuilder.addStatement("$1T params = $1T.obtain()", PARCEL_CLASSNAME); + for (VariableElement param : method.methodElement().getParameters()) { + if (crossProfileType.supportedTypes().isAutomaticallyResolved(param.asType())) { + continue; + } + methodBuilder.addStatement( + "internalCrossProfileClass.bundler().writeToParcel(params, $1L, $2L, /* flags= */ 0)", + param.getSimpleName(), + TypeUtils.generateBundlerType(param.asType())); + } + + if (method.thrownExceptions().isEmpty()) { + methodBuilder.addStatement( + "$1T returnParcel = connector.crossProfileSender().call($2LL, $3L, params)", + PARCEL_CLASSNAME, + crossProfileType.identifier(), + method.identifier()); + } else { + methodBuilder.addStatement("$1T returnParcel", PARCEL_CLASSNAME); + methodBuilder.beginControlFlow("try"); + methodBuilder.addStatement( + "returnParcel = connector.crossProfileSender().callWithExceptions($1LL, $2L, params)", + crossProfileType.identifier(), + method.identifier()); + methodBuilder.nextControlFlow("catch ($T e)", UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME); + methodBuilder.addStatement("throw e"); + + for (TypeName exception : method.thrownExceptions()) { + methodBuilder.nextControlFlow("catch ($T e)", exception); + methodBuilder.addStatement("throw e"); + } + + methodBuilder.nextControlFlow("catch ($T e)", Throwable.class); + methodBuilder.addStatement( + "throw new $T($S)", IllegalStateException.class, "Unexpected exception thrown"); + methodBuilder.endControlFlow(); + } + + methodBuilder.addStatement("params.recycle()"); + + if (!method.returnType().getKind().equals(TypeKind.VOID)) { + methodBuilder.addStatement( + CodeBlock.of( + "@SuppressWarnings(\"unchecked\") $1T returnValue = ($1T)" + + " internalCrossProfileClass.bundler().readFromParcel(returnParcel," + + " $2L)", + method.returnType(), + TypeUtils.generateBundlerType(method.returnType()))); + methodBuilder.addStatement("returnParcel.recycle()"); + methodBuilder.addStatement("return returnValue"); + } else { + methodBuilder.addStatement("returnParcel.recycle()"); + } + + classBuilder.addMethod(methodBuilder.build()); + } + + private void generateCrossProfileCallbackMethodOnOtherProfileClass( + TypeSpec.Builder classBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + VariableElement callbackParameter = method.getCrossProfileCallbackParam(generatorContext).get(); + TypeElement callbackType = + generatorContext.elements().getTypeElement(callbackParameter.asType().toString()); + CrossProfileCallbackInterfaceInfo callbackInterface = + CrossProfileCallbackInterfaceInfo.create(callbackType); + + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder(method.simpleName()) + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(method.returnTypeTypeName()) + .addParameters( + GeneratorUtilities.extractParametersFromMethod( + crossProfileType.supportedTypes(), + method.methodElement(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)) + .addParameter(EXCEPTION_CALLBACK_CLASSNAME, "exceptionCallback"); + + methodBuilder.addStatement( + "$1T internalCrossProfileClass = $1T.instance()", + InternalCrossProfileClassGenerator.getInternalCrossProfileClassName( + generatorContext, crossProfileType)); + + // parcel is passed into callAsync where it will be cached and recycled afterwards + methodBuilder.addStatement("$1T params = $1T.obtain()", PARCEL_CLASSNAME); + + for (VariableElement param : method.methodElement().getParameters()) { + if (crossProfileType.supportedTypes().isAutomaticallyResolved(param.asType())) { + continue; + } + if (param.getSimpleName().equals(callbackParameter.getSimpleName())) { + continue; + } + methodBuilder.addStatement( + "internalCrossProfileClass.bundler().writeToParcel(params, $1L, $2L, /* flags= */ 0)", + param.getSimpleName(), + TypeUtils.generateBundlerType(param.asType())); + } + + methodBuilder.addStatement( + "$1T sender = new $2T($3L, exceptionCallback, internalCrossProfileClass.bundler())", + LOCAL_CALLBACK_CLASSNAME, + CrossProfileCallbackCodeGenerator.getCrossProfileCallbackSenderClassName( + generatorContext, callbackInterface), + callbackParameter.getSimpleName()); + + // Suppress GoodTime warning for unboxing Duration. + methodBuilder.addAnnotation( + AnnotationSpec.builder(SuppressWarnings.class) + .addMember("value", "$S", "GoodTime") + .build()); + methodBuilder.addStatement( + "connector.crossProfileSender().callAsync($1LL, $2L, params, sender, timeout ==" + + " $3L ? $4L : timeout)", + crossProfileType.identifier(), + method.identifier(), + CrossProfileAnnotation.TIMEOUT_MILLIS_NOT_SET, + method.timeoutMillis()); + + methodBuilder.addComment( + "We don't recycle the params as they will be stored for the async call and recycled" + + " afterwards"); + + classBuilder.addMethod(methodBuilder.build()); + } + + private void generateFutureMethodOnOtherProfileClass( + TypeSpec.Builder classBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder(method.simpleName()) + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(method.returnTypeTypeName()) + .addParameters( + GeneratorUtilities.extractParametersFromMethod( + crossProfileType.supportedTypes(), + method.methodElement(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)); + + methodBuilder.addStatement( + "$1T internalCrossProfileClass = $1T.instance()", + InternalCrossProfileClassGenerator.getInternalCrossProfileClassName( + generatorContext, crossProfileType)); + + // parcel is passed into callAsync where it will be cached and recycled afterwards + methodBuilder.addStatement("$1T params = $1T.obtain()", PARCEL_CLASSNAME); + for (VariableElement param : method.methodElement().getParameters()) { + if (crossProfileType.supportedTypes().isAutomaticallyResolved(param.asType())) { + continue; + } + methodBuilder.addStatement( + "internalCrossProfileClass.bundler().writeToParcel(params, $1L, $2L, /* flags= */ 0)", + param.getSimpleName(), + TypeUtils.generateBundlerType(param.asType())); + } + + TypeMirror rawFutureType = TypeUtils.removeTypeArguments(method.returnType()); + + FutureWrapper futureWrapper = + crossProfileType.supportedTypes().getType(rawFutureType).getFutureWrapper().get(); + // This assumes every Future is generic with one type argument + TypeMirror wrappedReturnType = + TypeUtils.extractTypeArguments(method.returnType()).iterator().next(); + methodBuilder.addStatement( + "$1T<$2T> futureWrapper = $1T.create(internalCrossProfileClass.bundler(), $3L)", + futureWrapper.wrapperClassName(), + wrappedReturnType, + TypeUtils.generateBundlerType(wrappedReturnType)); + + methodBuilder.addAnnotation( + AnnotationSpec.builder(SuppressWarnings.class) + .addMember("value", "$S", "GoodTime") + .build()); + methodBuilder.addStatement( + "connector.crossProfileSender().callAsync($1LL, $2L, params, futureWrapper," + + " timeout == $3L ? $4L : timeout)", + crossProfileType.identifier(), + method.identifier(), + CrossProfileAnnotation.TIMEOUT_MILLIS_NOT_SET, + method.timeoutMillis()); + + methodBuilder.addComment( + "We don't recycle the params as they will be stored for the async call and recycled" + + " afterwards"); + + methodBuilder.addStatement("return futureWrapper.getFuture()"); + + classBuilder.addMethod(methodBuilder.build()); + } + + static ClassName getOtherProfileClassName( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + return GeneratorUtilities.appendToClassName( + crossProfileType.profileClassName(), "_OtherProfile"); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ParcelableWrappersGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ParcelableWrappersGenerator.java new file mode 100644 index 0000000..dfcea33 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ParcelableWrappersGenerator.java @@ -0,0 +1,154 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toSet; + +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.google.android.enterprise.connectedapps.processor.containers.ParcelableWrapper; +import com.google.android.enterprise.connectedapps.processor.containers.ParcelableWrapper.WrapperType; +import com.google.android.enterprise.connectedapps.processor.containers.Type; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.nio.charset.Charset; +import java.util.Collection; +import java.util.Optional; +import javax.tools.JavaFileObject; + +/** + * Generate the {@code Parcelable*} classes for every used compatible type. + * + * <p>This is intended to be initialised and used once, which will generate all needed code. + * + * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to + * validate that the annotated code is correct. + */ +final class ParcelableWrappersGenerator { + + private boolean generated = false; + private final GeneratorContext generatorContext; + + ParcelableWrappersGenerator(GeneratorContext generatorContext) { + this.generatorContext = checkNotNull(generatorContext); + } + + void generate() { + if (generated) { + throw new IllegalStateException( + "ParcelableWrappersGenerator#generate can only be called once"); + } + generated = true; + + generateParcelableWrappers(); + } + + private void generateParcelableWrappers() { + Collection<ParcelableWrapper> parcelableWrappersToGenerate = + generatorContext.crossProfileTypes().stream() + .map(CrossProfileTypeInfo::supportedTypes) + .flatMap(s -> s.usableTypes().stream()) + .filter(s -> s.getParcelableWrapper().isPresent()) + .map(Type::getParcelableWrapper) + .map(Optional::get) + .collect(toSet()); + + generateDefaultParcelableWrappers(parcelableWrappersToGenerate); + generateProtoParcelableWrappers(parcelableWrappersToGenerate); + } + + private void generateDefaultParcelableWrappers(Collection<ParcelableWrapper> parcelableWrappers) { + Collection<ParcelableWrapper> defaultParcelableWrappersToGenerate = + parcelableWrappers.stream() + .filter(f -> f.wrapperType() == WrapperType.DEFAULT) + .collect(toSet()); + + for (ParcelableWrapper parcelableWrapper : defaultParcelableWrappersToGenerate) { + if (generatorContext + .elements() + .getTypeElement(parcelableWrapper.wrapperClassName().toString()) + != null) { + // We don't generate things which already exist + return; + } + generateDefaultParcelableWrapper(parcelableWrapper); + } + } + + private void generateDefaultParcelableWrapper(ParcelableWrapper parcelableWrapper) { + String parcelableWrapperSimpleName = parcelableWrapper.defaultWrapperClassName().simpleName(); + + String contents; + InputStream in = + ParcelableWrappersGenerator.class.getResourceAsStream( + "/parcelablewrappers/" + parcelableWrapperSimpleName + ".java"); + + try (BufferedReader br = + new BufferedReader(new InputStreamReader(in, Charset.defaultCharset()))) { + contents = br.lines().collect(joining(System.lineSeparator())); + } catch (IOException e) { + throw new IllegalStateException( + "Could not read parcelablewrapper file for " + parcelableWrapperSimpleName, e); + } + + contents = + contents.replace( + parcelableWrapper.defaultWrapperClassName().packageName(), + parcelableWrapper.wrapperClassName().packageName()); + contents = + contents.replace( + parcelableWrapper.defaultWrapperClassName().simpleName(), + parcelableWrapper.wrapperClassName().simpleName()); + + JavaFileObject builderFile; + try { + builderFile = + generatorContext + .processingEnv() + .getFiler() + .createSourceFile( + parcelableWrapper.wrapperClassName().packageName() + + "." + + parcelableWrapper.wrapperClassName().simpleName()); + } catch (IOException e) { + throw new IllegalStateException( + "Could not write parcelablewrapper for " + parcelableWrapperSimpleName, e); + } + + try (PrintWriter out = new PrintWriter(builderFile.openWriter())) { + out.write(contents); + } catch (IOException e) { + throw new IllegalStateException( + "Could not write parcelablewrapper for " + parcelableWrapperSimpleName, e); + } + } + + private void generateProtoParcelableWrappers(Collection<ParcelableWrapper> parcelableWrappers) { + Collection<ParcelableWrapper> protoParcelableWrappersToGenerate = + parcelableWrappers.stream() + .filter(f -> f.wrapperType() == WrapperType.PROTO) + .collect(toSet()); + + for (ParcelableWrapper parcelableWrapper : protoParcelableWrappersToGenerate) { + new ProtoParcelableWrapperGenerator(generatorContext, parcelableWrapper).generate(); + } + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/Processor.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/Processor.java new file mode 100644 index 0000000..4948196 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/Processor.java @@ -0,0 +1,415 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.elementsAnnotatedWithCrossProfile; +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.elementsAnnotatedWithCrossProfileCallback; +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.elementsAnnotatedWithCrossProfileConfiguration; +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.elementsAnnotatedWithCrossProfileConfigurations; +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.elementsAnnotatedWithCrossProfileProvider; +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.elementsAnnotatedWithCrossProfileTest; +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.hasCrossProfileProviderAnnotation; +import static java.util.stream.Collectors.toSet; + +import com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper; +import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector; +import com.google.android.enterprise.connectedapps.annotations.CustomUserConnector; +import com.google.android.enterprise.connectedapps.annotations.GeneratedProfileConnector; +import com.google.android.enterprise.connectedapps.annotations.GeneratedUserConnector; +import com.google.android.enterprise.connectedapps.processor.containers.FutureWrapper; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.google.android.enterprise.connectedapps.processor.containers.ParcelableWrapper; +import com.google.android.enterprise.connectedapps.processor.containers.ProfileConnectorInfo; +import com.google.android.enterprise.connectedapps.processor.containers.UserConnectorInfo; +import com.google.android.enterprise.connectedapps.processor.containers.ValidatorContext; +import com.google.android.enterprise.connectedapps.processor.containers.ValidatorCrossProfileConfigurationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.ValidatorCrossProfileTestInfo; +import com.google.android.enterprise.connectedapps.processor.containers.ValidatorCrossProfileTypeInfo; +import com.google.android.enterprise.connectedapps.processor.containers.ValidatorProviderClassInfo; +import com.google.auto.service.AutoService; +import java.util.Collection; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +/** Processor for generation of cross-profile code. */ +@SupportedAnnotationTypes({ + "com.google.android.enterprise.connectedapps.annotations.CrossProfile", + "com.google.android.enterprise.connectedapps.annotations.CrossProfileCallback", + "com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration", + "com.google.android.enterprise.connectedapps.annotations.CrossProfileConfigurations", + "com.google.android.enterprise.connectedapps.annotations.CrossProfileProvider", + "com.google.android.enterprise.connectedapps.testing.annotations.CrossProfileTest", + "com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector", + "com.google.android.enterprise.connectedapps.annotations.GeneratedProfileConnector", + "com.google.android.enterprise.connectedapps.annotations.CrossUser", + "com.google.android.enterprise.connectedapps.annotations.CrossUserCallback", + "com.google.android.enterprise.connectedapps.annotations.CrossUserConfiguration", + "com.google.android.enterprise.connectedapps.annotations.CrossUserConfigurations", + "com.google.android.enterprise.connectedapps.annotations.CrossUserProvider", + "com.google.android.enterprise.connectedapps.testing.annotations.CrossUserTest", + "com.google.android.enterprise.connectedapps.annotations.CustomUserConnector", + "com.google.android.enterprise.connectedapps.annotations.GeneratedUserConnector", + "com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper", + "com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper" +}) +@AutoService(javax.annotation.processing.Processor.class) +public final class Processor extends AbstractProcessor { + + private Types types; + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latest(); + } + + @Override + public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { + Elements elements = processingEnv.getElementUtils(); + types = processingEnv.getTypeUtils(); + + Collection<ValidatorCrossProfileTestInfo> newCrossProfileTests = + findNewCrossProfileTests(roundEnv); + // Only new configurations need code generating - but we need to support types used by methods + // included in configurations under test + Collection<ValidatorCrossProfileConfigurationInfo> newConfigurations = + findNewConfigurations(roundEnv); + Collection<ValidatorCrossProfileConfigurationInfo> allConfigurations = + findAllConfigurations(newConfigurations, newCrossProfileTests); + + Collection<ValidatorProviderClassInfo> newProviderClasses = findNewProviderClasses(roundEnv); + Collection<ExecutableElement> newProviderMethods = findNewProviderMethods(roundEnv); + Collection<TypeElement> newGeneratedConnectors = findNewGeneratedConnectors(roundEnv); + Collection<TypeElement> newGeneratedUserConnectors = findNewGeneratedUserConnectors(roundEnv); + Collection<ExecutableElement> newCrossProfileMethods = findNewCrossProfileMethods(roundEnv); + Collection<ExecutableElement> allCrossProfileMethods = + findAllCrossProfileMethods( + processingEnv, + elements, + newCrossProfileMethods, + allConfigurations, + newProviderMethods, + newProviderClasses); + Collection<TypeElement> newCrossProfileCallbackInterfaces = + findNewCrossProfileCallbackInterfaces(roundEnv); + + Collection<ExecutableElement> methods = new HashSet<>(allCrossProfileMethods); + methods.addAll( + newCrossProfileCallbackInterfaces.stream() + .flatMap(i -> i.getEnclosedElements().stream()) + .filter(e -> e instanceof ExecutableElement) + .map(e -> (ExecutableElement) e) + .filter(e -> e.getKind() == ElementKind.METHOD) + .collect(toSet())); + + Collection<TypeElement> newCustomParcelableWrappers = findNewParcelableWrappers(roundEnv); + Collection<TypeElement> newCustomFutureWrappers = findNewFutureWrappers(roundEnv); + + Collection<FutureWrapper> globalFutureWrappers = + FutureWrapper.createGlobalFutureWrappers(elements); + Collection<ParcelableWrapper> globalParcelableWrappers = + ParcelableWrapper.createGlobalParcelableWrappers(types, elements, methods); + + SupportedTypes globalSupportedTypes = + SupportedTypes.createFromMethods( + types, elements, globalParcelableWrappers, globalFutureWrappers, methods); + + Collection<ValidatorCrossProfileTypeInfo> newCrossProfileTypes = + findNewCrossProfileTypes(roundEnv, globalSupportedTypes); + Collection<ProfileConnectorInfo> newProfileConnectorInterfaces = + findNewProfileConnectorInterfaces(roundEnv, globalSupportedTypes); + Collection<UserConnectorInfo> newUserConnectorInterfaces = + findNewUserConnectorInterfaces(roundEnv, globalSupportedTypes); + + ValidatorContext validatorContext = + ValidatorContext.builder() + .setProcessingEnv(processingEnv) + .setElements(elements) + .setTypes(types) + .setGlobalSupportedTypes(globalSupportedTypes) + .setNewProfileConnectorInterfaces(newProfileConnectorInterfaces) + .setNewUserConnectorInterfaces(newUserConnectorInterfaces) + .setNewGeneratedProfileConnectors(newGeneratedConnectors) + .setNewGeneratedUserConnectors(newGeneratedUserConnectors) + .setNewConfigurations(newConfigurations) + .setNewCrossProfileTypes(newCrossProfileTypes) + .setNewCrossProfileMethods(newCrossProfileMethods) + .setNewProviderClasses(newProviderClasses) + .setNewProviderMethods(newProviderMethods) + .setNewCrossProfileCallbackInterfaces(newCrossProfileCallbackInterfaces) + .setNewCrossProfileTests(newCrossProfileTests) + .setNewCustomParcelableWrappers(newCustomParcelableWrappers) + .setNewCustomFutureWrappers(newCustomFutureWrappers) + .build(); + + boolean isValid = new EarlyValidator(validatorContext).validate(); + + if (!isValid) { + return false; + } + + GeneratorContext generatorContext = + GeneratorContext.createFromValidatorContext(validatorContext); + + isValid = new LateValidator(generatorContext).validate(); + + if (!isValid) { + return false; + } + + new CodeGenerator(generatorContext).generate(); + + return false; + } + + private Collection<ValidatorCrossProfileConfigurationInfo> findNewConfigurations( + RoundEnvironment roundEnv) { + Set<ValidatorCrossProfileConfigurationInfo> annotations = new HashSet<>(); + + elementsAnnotatedWithCrossProfileConfiguration(roundEnv) + .map( + element -> + ValidatorCrossProfileConfigurationInfo.createFromElement( + processingEnv, (TypeElement) element)) + .forEach(annotations::add); + + elementsAnnotatedWithCrossProfileConfigurations(roundEnv) + .map( + element -> + ValidatorCrossProfileConfigurationInfo.createMultipleFromElement( + processingEnv, (TypeElement) element)) + .forEach(annotations::addAll); + + return annotations; + } + + private Collection<ValidatorCrossProfileConfigurationInfo> findAllConfigurations( + Collection<ValidatorCrossProfileConfigurationInfo> newConfigurations, + Collection<ValidatorCrossProfileTestInfo> crossProfileTests) { + Set<ValidatorCrossProfileConfigurationInfo> allConfigurations = new HashSet<>(); + allConfigurations.addAll(newConfigurations); + allConfigurations.addAll( + crossProfileTests.stream() + .flatMap( + t -> + ValidatorCrossProfileConfigurationInfo.createMultipleFromElement( + processingEnv, t.configurationElement()) + .stream()) + .collect(toSet())); + return allConfigurations; + } + + private Collection<ValidatorProviderClassInfo> findNewProviderClasses(RoundEnvironment roundEnv) { + Set<ValidatorProviderClassInfo> annotatedClasses = + elementsAnnotatedWithCrossProfileProvider(roundEnv) + .filter(m -> m instanceof TypeElement) + .map(m -> (TypeElement) m) + .map(m -> ValidatorProviderClassInfo.create(processingEnv, m)) + .collect(toSet()); + + Set<ValidatorProviderClassInfo> unannotatedClasses = + elementsAnnotatedWithCrossProfileProvider(roundEnv) + .filter(m -> m instanceof ExecutableElement) + .map(m -> (ExecutableElement) m) + .map(Element::getEnclosingElement) + .map(m -> (TypeElement) m) + .filter(m -> !hasCrossProfileProviderAnnotation(m)) + .map(m -> ValidatorProviderClassInfo.create(processingEnv, m)) + .collect(toSet()); + + Collection<ValidatorProviderClassInfo> allProviders = new HashSet<>(); + allProviders.addAll(annotatedClasses); + allProviders.addAll(unannotatedClasses); + return allProviders; + } + + private Collection<ExecutableElement> findNewProviderMethods(RoundEnvironment roundEnv) { + return elementsAnnotatedWithCrossProfileProvider(roundEnv) + .filter(m -> m instanceof ExecutableElement) + .map(m -> (ExecutableElement) m) + .collect(toSet()); + } + + private Collection<ValidatorCrossProfileTypeInfo> findNewCrossProfileTypes( + RoundEnvironment roundEnv, SupportedTypes globalSupportedTypes) { + Collection<ValidatorCrossProfileTypeInfo> annotatedTypes = + elementsAnnotatedWithCrossProfile(roundEnv) + .filter(m -> m instanceof TypeElement) + .map(m -> (TypeElement) m) + .map(m -> ValidatorCrossProfileTypeInfo.create(processingEnv, m, globalSupportedTypes)) + .collect(toSet()); + + Collection<ValidatorCrossProfileTypeInfo> unannotatedTypes = + elementsAnnotatedWithCrossProfile(roundEnv) + .filter(m -> m instanceof ExecutableElement) + .map(m -> (ExecutableElement) m) + .map(ExecutableElement::getEnclosingElement) + .filter(m -> m instanceof TypeElement) + .map(m -> (TypeElement) m) + .map(m -> ValidatorCrossProfileTypeInfo.create(processingEnv, m, globalSupportedTypes)) + .collect(toSet()); + + Collection<ValidatorCrossProfileTypeInfo> allTypes = new HashSet<>(); + allTypes.addAll(annotatedTypes); + allTypes.addAll(unannotatedTypes); + return allTypes; + } + + private Collection<ExecutableElement> findNewCrossProfileMethods(RoundEnvironment roundEnv) { + return elementsAnnotatedWithCrossProfile(roundEnv) + .filter(m -> m instanceof ExecutableElement) + .map(m -> (ExecutableElement) m) + .collect(toSet()); + } + + private Collection<ProfileConnectorInfo> findNewProfileConnectorInterfaces( + RoundEnvironment roundEnv, SupportedTypes globalSupportedTypes) { + Collection<TypeElement> connectorInterfaces = + roundEnv.getElementsAnnotatedWith(CustomProfileConnector.class).stream() + .map(m -> (TypeElement) m) + .collect(toSet()); + + // We manually add the SDK-provided CrossProfileConnector as it won't be detected by roundEnv + connectorInterfaces.add( + processingEnv + .getElementUtils() + .getTypeElement("com.google.android.enterprise.connectedapps.CrossProfileConnector")); + + return connectorInterfaces.stream() + .map(t -> ProfileConnectorInfo.create(processingEnv, t, globalSupportedTypes)) + .collect(Collectors.toSet()); + } + + private Collection<UserConnectorInfo> findNewUserConnectorInterfaces( + RoundEnvironment roundEnv, SupportedTypes globalSupportedTypes) { + Collection<TypeElement> connectorInterfaces = + roundEnv.getElementsAnnotatedWith(CustomUserConnector.class).stream() + .map(m -> (TypeElement) m) + .collect(toSet()); + + // We manually add the SDK-provided CrossUserConnector as it won't be detected by roundEnv + connectorInterfaces.add( + processingEnv + .getElementUtils() + .getTypeElement("com.google.android.enterprise.connectedapps.CrossUserConnector")); + + return connectorInterfaces.stream() + .map(t -> UserConnectorInfo.create(processingEnv, t, globalSupportedTypes)) + .collect(Collectors.toSet()); + } + + private Collection<TypeElement> findNewGeneratedConnectors(RoundEnvironment roundEnv) { + Collection<TypeElement> connectorInterfaces = + roundEnv.getElementsAnnotatedWith(GeneratedProfileConnector.class).stream() + .map(m -> (TypeElement) m) + .collect(toSet()); + + return connectorInterfaces; + } + + private Collection<TypeElement> findNewGeneratedUserConnectors(RoundEnvironment roundEnv) { + Collection<TypeElement> connectorInterfaces = + roundEnv.getElementsAnnotatedWith(GeneratedUserConnector.class).stream() + .map(m -> (TypeElement) m) + .collect(toSet()); + + return connectorInterfaces; + } + + private static Collection<ExecutableElement> findAllCrossProfileMethods( + ProcessingEnvironment processingEnvironment, + Elements elements, + Collection<ExecutableElement> newCrossProfileMethods, + Collection<ValidatorCrossProfileConfigurationInfo> configurations, + Collection<ExecutableElement> newProviderMethods, + Collection<ValidatorProviderClassInfo> newProviderClasses) { + Collection<ExecutableElement> allCrossProfileMethods = new HashSet<>(newCrossProfileMethods); + + Collection<ValidatorProviderClassInfo> foundProviderClasses = + configurations.stream() + .flatMap(a -> a.providerClassElements().stream()) + .map(m -> ValidatorProviderClassInfo.create(processingEnvironment, m)) + .collect(toSet()); + + Collection<ExecutableElement> providerMethods = + foundProviderClasses.stream() + .flatMap( + m -> + GeneratorUtilities.findCrossProfileProviderMethodsInClass( + m.providerClassElement()) + .stream()) + .collect(toSet()); + + providerMethods.addAll(newProviderMethods); + + Collection<TypeElement> crossProfileTypes = + providerMethods.stream() + .map(e -> elements.getTypeElement(e.getReturnType().toString())) + .filter(Objects::nonNull) + .collect(toSet()); + crossProfileTypes.addAll( + foundProviderClasses.stream().flatMap(m -> m.staticTypes().stream()).collect(toSet())); + crossProfileTypes.addAll( + newProviderClasses.stream().flatMap(m -> m.staticTypes().stream()).collect(toSet())); + + Collection<ExecutableElement> foundCrossProfileMethods = + crossProfileTypes.stream() + .flatMap(t -> GeneratorUtilities.findCrossProfileMethodsInClass(t).stream()) + .collect(toSet()); + + allCrossProfileMethods.addAll(foundCrossProfileMethods); + return allCrossProfileMethods; + } + + private Collection<ValidatorCrossProfileTestInfo> findNewCrossProfileTests( + RoundEnvironment roundEnv) { + return elementsAnnotatedWithCrossProfileTest(roundEnv) + .map(e -> (TypeElement) e) + .map(e -> ValidatorCrossProfileTestInfo.create(processingEnv, e)) + .collect(toSet()); + } + + private Collection<TypeElement> findNewCrossProfileCallbackInterfaces(RoundEnvironment roundEnv) { + return elementsAnnotatedWithCrossProfileCallback(roundEnv) + .map(m -> (TypeElement) m) + .collect(toSet()); + } + + private Collection<TypeElement> findNewParcelableWrappers(RoundEnvironment roundEnv) { + return roundEnv.getElementsAnnotatedWith(CustomParcelableWrapper.class).stream() + .map(m -> (TypeElement) m) + .collect(toSet()); + } + + private Collection<TypeElement> findNewFutureWrappers(RoundEnvironment roundEnv) { + return roundEnv.getElementsAnnotatedWith(CustomFutureWrapper.class).stream() + .map(m -> (TypeElement) m) + .collect(toSet()); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProcessorConfiguration.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProcessorConfiguration.java new file mode 100644 index 0000000..75d3c12 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProcessorConfiguration.java @@ -0,0 +1,30 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +/** General configuration. */ +public final class ProcessorConfiguration { + private ProcessorConfiguration() {} + + /** + * When {@code true}, will generate a copy of each parcelable and future wrapper for each type + * which requires it. + * + * <p>This is required to ensure that there are no conflicts due to duplicate classes being + * generated in separate targets. + */ + public static final boolean GENERATE_TYPE_SPECIFIC_WRAPPERS = true; +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProfileConnectorCodeGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProfileConnectorCodeGenerator.java new file mode 100644 index 0000000..68c14f2 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProfileConnectorCodeGenerator.java @@ -0,0 +1,189 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.ABSTRACT_PROFILE_CONNECTOR_BUILDER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.ABSTRACT_PROFILE_CONNECTOR_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.AVAILABILITY_RESTRICTIONS_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CONNECTION_BINDER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CONTEXT_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.SCHEDULED_EXECUTOR_SERVICE_CLASSNAME; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector.ProfileType; +import com.google.android.enterprise.connectedapps.annotations.GeneratedProfileConnector; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.google.android.enterprise.connectedapps.processor.containers.ProfileConnectorInfo; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeSpec; +import javax.lang.model.element.Modifier; + +/** + * Generate the {@code Generated*} class for a single {@link GeneratedProfileConnector} annotated + * class. + * + * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to + * validate that the annotated code is correct. + */ +class ProfileConnectorCodeGenerator { + private boolean generated = false; + + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final ProfileConnectorInfo connector; + + ProfileConnectorCodeGenerator(GeneratorContext generatorContext, ProfileConnectorInfo connector) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.connector = checkNotNull(connector); + } + + void generate() { + if (generated) { + throw new IllegalStateException( + "ProfileConnectorCodeGenerator#generate can only be called once"); + } + generated = true; + + generateProfileConnector(); + } + + private void generateProfileConnector() { + ClassName className = getGeneratedProfileConnectorClassName(generatorContext, connector); + ClassName builderClassName = + getGeneratedProfileConnectorBuilderClassName(generatorContext, connector); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addJavadoc( + "Generated implementation of {@link $T}.\n\n" + + "<p>All logic is implemented by {@link $T}.\n", + connector.connectorClassName(), + ABSTRACT_PROFILE_CONNECTOR_CLASSNAME) + .addModifiers(Modifier.FINAL) + .addSuperinterface(connector.connectorClassName()) + .superclass(ABSTRACT_PROFILE_CONNECTOR_CLASSNAME); + + classBuilder.addMethod( + MethodSpec.methodBuilder("builder") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(builderClassName) + .addParameter(CONTEXT_CLASSNAME, "context") + .addStatement("return new $T(context)", builderClassName) + .build()); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PRIVATE) + .addParameter(builderClassName, "builder") + .addStatement( + "super($1T.class, builder.profileConnectorBuilder)", connector.connectorClassName()) + .build()); + + generateProfileConnectorBuilder(classBuilder); + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private void generateProfileConnectorBuilder(TypeSpec.Builder profileConnector) { + ClassName profileConnectorClassName = + getGeneratedProfileConnectorClassName(generatorContext, connector); + ClassName builderClassName = + getGeneratedProfileConnectorBuilderClassName(generatorContext, connector); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(builderClassName) + .addJavadoc("Builder for {@link $T}.\n", profileConnectorClassName) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC); + + CodeBlock initialiser = + CodeBlock.of( + "new $T().setServiceClassName($S).setAvailabilityRestrictions($T.$L)", + ABSTRACT_PROFILE_CONNECTOR_BUILDER_CLASSNAME, + connector.serviceName().toString(), + AVAILABILITY_RESTRICTIONS_CLASSNAME, + connector.availabilityRestrictions().name()); + + if (connector.primaryProfile() != ProfileType.NONE) { + initialiser = + CodeBlock.of( + "$L.setPrimaryProfileType($T.$L)", + initialiser, + ProfileType.class, + connector.primaryProfile().name()); + } + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addParameter(CONTEXT_CLASSNAME, "context") + .addStatement("profileConnectorBuilder.setContext(context)") + .build()); + + classBuilder.addField( + FieldSpec.builder(ABSTRACT_PROFILE_CONNECTOR_BUILDER_CLASSNAME, "profileConnectorBuilder") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .initializer(initialiser) + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("setScheduledExecutorService") + .addModifiers(Modifier.PUBLIC) + .addParameter(SCHEDULED_EXECUTOR_SERVICE_CLASSNAME, "scheduledExecutorService") + .returns(builderClassName) + .addStatement( + "profileConnectorBuilder.setScheduledExecutorService(scheduledExecutorService)") + .addStatement("return this") + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("setBinder") + .addModifiers(Modifier.PUBLIC) + .addParameter(CONNECTION_BINDER_CLASSNAME, "binder") + .returns(builderClassName) + .addStatement("profileConnectorBuilder.setBinder(binder)") + .addStatement("return this") + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("build") + .addModifiers(Modifier.PUBLIC) + .returns(profileConnectorClassName) + .addStatement("return new $1T(this)", profileConnectorClassName) + .build()); + + profileConnector.addType(classBuilder.build()); + } + + static ClassName getGeneratedProfileConnectorClassName( + GeneratorContext generatorContext, ProfileConnectorInfo connector) { + return ClassName.get( + connector.connectorClassName().packageName(), + "Generated" + connector.connectorClassName().simpleName()); + } + + static ClassName getGeneratedProfileConnectorBuilderClassName( + GeneratorContext generatorContext, ProfileConnectorInfo connector) { + return ClassName.get( + connector.connectorClassName().packageName() + + "." + + "Generated" + + connector.connectorClassName().simpleName(), + "Builder"); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProtoParcelableWrapperGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProtoParcelableWrapperGenerator.java new file mode 100644 index 0000000..302b420 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProtoParcelableWrapperGenerator.java @@ -0,0 +1,171 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.BUNDLER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.BUNDLER_TYPE_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.INVALID_PROTOCOL_BUFFER_EXCEPTION_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCELABLE_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.containers.ParcelableWrapper.PARCELABLE_WRAPPER_PACKAGE; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.google.android.enterprise.connectedapps.processor.containers.ParcelableWrapper; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeSpec; +import javax.lang.model.element.Modifier; +import javax.lang.model.type.TypeMirror; + +/** + * Generate the Parcelable Wrapper for a single Proto. + * + * <p>This is intended to be initialised and used once, which will generate all needed code. + * + * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to + * validate that the annotated code is correct. + */ +public final class ProtoParcelableWrapperGenerator { + + private static final String GENERATED_PARCELABLE_WRAPPER_PACKAGE = + PARCELABLE_WRAPPER_PACKAGE + ".generated"; + + private boolean generated = false; + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final ParcelableWrapper parcelableWrapper; + + ProtoParcelableWrapperGenerator( + GeneratorContext generatorContext, ParcelableWrapper parcelableWrapper) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.parcelableWrapper = checkNotNull(parcelableWrapper); + } + + void generate() { + if (generated) { + throw new IllegalStateException( + "ProtoParcelableWrapperGenerator#generate can only be called once"); + } + generated = true; + + generateProtoParcelableWrapper(); + } + + private void generateProtoParcelableWrapper() { + ClassName wrapperClassName = parcelableWrapper.wrapperClassName(); + + if (generatorContext.elements().getTypeElement(wrapperClassName.toString()) != null) { + // We don't generate things which already exist + return; + } + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(wrapperClassName) + .addModifiers(Modifier.PUBLIC) + .addSuperinterface(PARCELABLE_CLASSNAME) + .addJavadoc( + "Wrapper for reading & writing {@link $T} instances to and from {@link $T}" + + " instances.", + parcelableWrapper.wrappedType(), + PARCEL_CLASSNAME); + + classBuilder.addField( + FieldSpec.builder(ClassName.get(parcelableWrapper.wrappedType()), "proto", Modifier.PRIVATE) + .build()); + + classBuilder.addField( + FieldSpec.builder(int.class, "NULL_SIZE") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC) + .initializer("-1") + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("of") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addJavadoc( + "Create a wrapper for the given {@link $T}.\n", parcelableWrapper.wrappedType()) + .returns(parcelableWrapper.wrapperClassName()) + .addParameter(BUNDLER_CLASSNAME, "bundler") + .addParameter(BUNDLER_TYPE_CLASSNAME, "type") + .addParameter(ClassName.get(parcelableWrapper.wrappedType()), "proto") + .addStatement( + "return new $T(bundler, type, proto)", parcelableWrapper.wrapperClassName()) + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("get") + .addModifiers(Modifier.PUBLIC) + .returns(ClassName.get(parcelableWrapper.wrappedType())) + .addStatement("return proto") + .build()); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PRIVATE) + .addParameter(BUNDLER_CLASSNAME, "bundler") + .addParameter(BUNDLER_TYPE_CLASSNAME, "type") + .addParameter(ClassName.get(parcelableWrapper.wrappedType()), "proto") + .addStatement("this.proto = proto") + .build()); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PRIVATE) + .addParameter(PARCEL_CLASSNAME, "in") + .addStatement("int size = in.readInt()") + .beginControlFlow("if (size == NULL_SIZE)") + .addStatement("proto = null") + .addStatement("return") + .endControlFlow() + .addStatement("byte[] protoBytes = new byte[size]") + .addStatement("in.readByteArray(protoBytes)") + .beginControlFlow("try") + .addStatement("proto = $T.parseFrom(protoBytes)", parcelableWrapper.wrappedType()) + .nextControlFlow("catch ($T e)", INVALID_PROTOCOL_BUFFER_EXCEPTION_CLASSNAME) + .addComment("TODO: Deal with exception") + .endControlFlow() + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("writeToParcel") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .addParameter(PARCEL_CLASSNAME, "dest") + .addParameter(int.class, "flags") + .beginControlFlow("if (proto == null)") + .addStatement("dest.writeInt(NULL_SIZE)") + .addStatement("return") + .endControlFlow() + .addStatement("byte[] protoBytes = proto.toByteArray()") + .addStatement("dest.writeInt(protoBytes.length)") + .addStatement("dest.writeByteArray(protoBytes)") + .build()); + + generatorUtilities.addDefaultParcelableMethods( + classBuilder, parcelableWrapper.wrapperClassName()); + + generatorUtilities.writeClassToFile( + parcelableWrapper.wrapperClassName().packageName(), classBuilder); + } + + public static ClassName getGeneratedProtoWrapperClassName(TypeMirror type) { + String simpleName = type.toString().substring(type.toString().lastIndexOf(".") + 1); + return ClassName.get(GENERATED_PARCELABLE_WRAPPER_PACKAGE, simpleName + "Wrapper"); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProviderClassCodeGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProviderClassCodeGenerator.java new file mode 100644 index 0000000..06f6b71 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProviderClassCodeGenerator.java @@ -0,0 +1,53 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.google.android.enterprise.connectedapps.processor.containers.ProviderClassInfo; + +/** Generator of code for a single provider class. */ +class ProviderClassCodeGenerator { + private boolean generated = false; + + private final GeneratorContext generatorContext; + private final InternalProviderClassGenerator internalProviderClassGenerator; + private final ProviderClassInfo providerClass; + + ProviderClassCodeGenerator(GeneratorContext generatorContext, ProviderClassInfo providerClass) { + this.generatorContext = checkNotNull(generatorContext); + this.providerClass = checkNotNull(providerClass); + this.internalProviderClassGenerator = + new InternalProviderClassGenerator(generatorContext, providerClass); + } + + void generate() { + if (generated) { + throw new IllegalStateException( + "ProviderClassCodeGenerator#generate can only be called once"); + } + generated = true; + + internalProviderClassGenerator.generate(); + + for (CrossProfileTypeInfo crossProfileType : providerClass.allCrossProfileTypes()) { + new CrossProfileTypeCodeGenerator(generatorContext, providerClass, crossProfileType) + .generate(); + } + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ServiceGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ServiceGenerator.java new file mode 100644 index 0000000..28fa128 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ServiceGenerator.java @@ -0,0 +1,178 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.BINDER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CROSSPROFILESERVICE_STUB_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CROSS_PROFILE_CALLBACK_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.INTENT_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.DispatcherGenerator.getDispatcherClassName; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.squareup.javapoet.ArrayTypeName; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeSpec; +import javax.lang.model.element.Modifier; + +/** + * Generate the {@code *_Service} class for a single {@link CrossProfileConfiguration} annotated + * class. + * + * <p>This class includes the dispatch of calls to providers. + * + * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to + * validate that the annotated code is correct. + */ +final class ServiceGenerator { + + private boolean generated = false; + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final CrossProfileConfigurationInfo configuration; + + ServiceGenerator(GeneratorContext generatorContext, CrossProfileConfigurationInfo configuration) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.configuration = checkNotNull(configuration); + } + + void generate() { + if (generated) { + throw new IllegalStateException("ServiceGenerator#generate can only be called once"); + } + generated = true; + + if (configuration.serviceClass().isPresent()) { + // Using a pre-existing service + return; + } + + generateServiceClass(); + } + + private void generateServiceClass() { + ClassName className = getConnectedAppsServiceClassName(generatorContext, configuration); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .superclass(configuration.serviceSuperclass()) + .addJavadoc( + "Generated Service for {@link $T}\n\n" + + "<p>This is bound to by {@link $T} to make cross-profile calls.\n\n" + + "<p>This primarily forwards calls to {@link $T}\n\n" + + "<p>This service must be exposed in a <service> tag in your" + + " AndroidManifest.xml\n", + configuration.configurationElement(), + configuration.profileConnector().connectorClassName(), + getDispatcherClassName(generatorContext, configuration)); + + addBinder(classBuilder); + + classBuilder.addMethod( + MethodSpec.methodBuilder("onBind") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(BINDER_CLASSNAME) + .addParameter(INTENT_CLASSNAME, "intent") + .addStatement("return binder") + .build()); + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private void addBinder(TypeSpec.Builder classBuilder) { + TypeSpec.Builder binderBuilder = + TypeSpec.anonymousClassBuilder("") + .addSuperinterface(CROSSPROFILESERVICE_STUB_CLASSNAME) + .addField( + FieldSpec.builder( + getDispatcherClassName(generatorContext, configuration), "dispatcher") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .initializer( + "new $T()", getDispatcherClassName(generatorContext, configuration)) + .build()); + + addPrepareCallMethod(binderBuilder); + addCallMethod(binderBuilder); + addFetchResponseMethod(binderBuilder); + + classBuilder.addField( + FieldSpec.builder(CROSSPROFILESERVICE_STUB_CLASSNAME, "binder", Modifier.PRIVATE) + .initializer("$L", binderBuilder.build()) + .build()); + } + + private static void addPrepareCallMethod(TypeSpec.Builder classBuilder) { + MethodSpec prepareCallMethod = + MethodSpec.methodBuilder("prepareCall") + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .addParameter(long.class, "callId") + .addParameter(int.class, "blockId") + .addParameter(int.class, "numBytes") + .addParameter(ArrayTypeName.of(byte.class), "paramBytes") + .addStatement( + "dispatcher.prepareCall(getApplicationContext(), callId, blockId, numBytes," + + " paramBytes)") + .build(); + classBuilder.addMethod(prepareCallMethod); + } + + private static void addCallMethod(TypeSpec.Builder classBuilder) { + MethodSpec callMethod = + MethodSpec.methodBuilder("call") + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .returns(ArrayTypeName.of(byte.class)) + .addParameter(long.class, "callId") + .addParameter(int.class, "blockId") + .addParameter(long.class, "crossProfileTypeIdentifier") + .addParameter(int.class, "methodIdentifier") + .addParameter(ArrayTypeName.of(byte.class), "paramBytes") + .addParameter(CROSS_PROFILE_CALLBACK_CLASSNAME, "callback") + .addStatement( + "return dispatcher.call(getApplicationContext()," + + "callId, blockId, crossProfileTypeIdentifier, methodIdentifier, paramBytes," + + " callback)") + .build(); + classBuilder.addMethod(callMethod); + } + + private static void addFetchResponseMethod(TypeSpec.Builder classBuilder) { + MethodSpec prepareCallMethod = + MethodSpec.methodBuilder("fetchResponse") + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .addParameter(long.class, "callId") + .addParameter(int.class, "blockId") + .returns(ArrayTypeName.of(byte.class)) + .addStatement( + "return dispatcher.fetchResponse(getApplicationContext(), callId, blockId)") + .build(); + classBuilder.addMethod(prepareCallMethod); + } + + static ClassName getConnectedAppsServiceClassName( + GeneratorContext generatorContext, CrossProfileConfigurationInfo configuration) { + return configuration.profileConnector().serviceName(); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/SupportedTypes.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/SupportedTypes.java new file mode 100644 index 0000000..1238b4b --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/SupportedTypes.java @@ -0,0 +1,844 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.hasCrossProfileCallbackAnnotation; + +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackInterfaceInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo; +import com.google.android.enterprise.connectedapps.processor.containers.FutureWrapper; +import com.google.android.enterprise.connectedapps.processor.containers.ParcelableWrapper; +import com.google.android.enterprise.connectedapps.processor.containers.Type; +import com.google.android.enterprise.connectedapps.processor.containers.ValidatorContext; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableMap; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Utility methods for generating code related to valid types for use with the Connected Apps SDK. + */ +public final class SupportedTypes { + + @Override + public String toString() { + return "SupportedTypes{" + + "usableTypes=" + usableTypes + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SupportedTypes that = (SupportedTypes) o; + return usableTypes.equals(that.usableTypes); + } + + @Override + public int hashCode() { + return Objects.hash(usableTypes); + } + + /** Record of the current context for type checking. */ + @AutoValue + public abstract static class TypeCheckContext { + + /** True if we are checking inside a generic type or an array. */ + public abstract boolean isWrapped(); + + public abstract boolean isOnCrossProfileCallbackInterface(); + + public abstract Builder toBuilder(); + + public static TypeCheckContext create() { + return new AutoValue_SupportedTypes_TypeCheckContext.Builder() + .setWrapped(false) + .setOnCrossProfileCallbackInterface(false) + .build(); + } + + public static TypeCheckContext createForCrossProfileCallbackInterface() { + return new AutoValue_SupportedTypes_TypeCheckContext.Builder() + .setWrapped(false) + .setOnCrossProfileCallbackInterface(true) + .build(); + } + + @AutoValue.Builder + abstract static class Builder { + abstract Builder setWrapped(boolean wrapped); + + abstract Builder setOnCrossProfileCallbackInterface(boolean onCrossProfileCallbackInterface); + + abstract TypeCheckContext build(); + } + } + + private final ImmutableMap<String, Type> usableTypes; + + public boolean isFuture(TypeMirror type) { + Type supportedType = get(type); + return supportedType != null && supportedType.isFuture(); + } + + boolean isValidReturnType(TypeMirror type) { + return isValidReturnType(type, TypeCheckContext.create()); + } + + private boolean isValidReturnType(TypeMirror type, TypeCheckContext context) { + if (TypeUtils.isArray(type)) { + TypeMirror wrappedType = TypeUtils.extractTypeFromArray(type); + if (TypeUtils.isGeneric(wrappedType)) { + return false; // We don't support generic arrays + } + if (wrappedType.getKind().isPrimitive()) { + return false; // We don't support primitive arrays + } + if (TypeUtils.isArray(wrappedType)) { + return false; // We don't support multidimensional arrays + } + return isValidReturnType(wrappedType, context); + } + + return TypeUtils.isGeneric(type) + ? isValidGenericReturnType(type, context) + : isValidReturnType(get(type), context); + } + + private static boolean isValidReturnType(@Nullable Type supportedType, TypeCheckContext context) { + if (supportedType == null) { + return false; + } + + if (context.isWrapped() && !supportedType.isSupportedInsideWrapper()) { + return false; + } + + return supportedType.isAcceptableReturnType(); + } + + private boolean isValidGenericReturnType(TypeMirror type, TypeCheckContext context) { + TypeMirror genericType = TypeUtils.removeTypeArguments(type); + Type supportedType = get(genericType); + + if (supportedType == null) { + return false; + } + + if (!supportedType.isSupportedWithAnyGenericType()) { + // We need to recursively check all type arguments + for (TypeMirror typeArgument : TypeUtils.extractTypeArguments(type)) { + if (!isValidReturnType(typeArgument, context.toBuilder().setWrapped(true).build())) { + return false; + } + } + } + + return isValidReturnType(supportedType, context); + } + + /** + * Returns true if this type is automatically resolved. + * + * <p>An automatically resolved type does not need to have a value provided by the developer at + * runtime, and should instead use the value of + * {@link #getAutomaticallyResolvedReplacement(TypeMirror)}. + */ + public boolean isAutomaticallyResolved(TypeMirror type) { + Type supportedType = get(type); + return supportedType != null && supportedType.getAutomaticallyResolvedReplacement().isPresent(); + } + + public String getAutomaticallyResolvedReplacement(TypeMirror type) { + Type supportedType = get(type); + return supportedType.getAutomaticallyResolvedReplacement().get(); + } + + boolean isValidParameterType(TypeMirror type) { + return isValidParameterType(type, TypeCheckContext.create()); + } + + boolean isValidParameterType(TypeMirror type, TypeCheckContext context) { + if (TypeUtils.isArray(type)) { + TypeMirror wrappedType = TypeUtils.extractTypeFromArray(type); + if (TypeUtils.isGeneric(wrappedType)) { + return false; // We don't support generic arrays + } + if (wrappedType.getKind().isPrimitive()) { + return false; // We don't support primitive arrays + } + if (TypeUtils.isArray(wrappedType)) { + return false; // We don't support multidimensional arrays + } + return isValidParameterType(wrappedType, context.toBuilder().setWrapped(true).build()); + } + + Type supportedType = get(TypeUtils.removeTypeArguments(type)); + if (context.isOnCrossProfileCallbackInterface()) { + if (supportedType != null && !supportedType.isSupportedInsideCrossProfileCallback()) { + return false; + } + } + + if (context.isWrapped()) { + if (supportedType == null || !supportedType.isSupportedInsideWrapper()) { + return false; + } + } + + return TypeUtils.isGeneric(type) + ? isValidGenericParameterType(type, context) + : isValidParameterType(get(type)); + } + + private static boolean isValidParameterType(Type supportedType) { + return supportedType != null && supportedType.isAcceptableParameterType(); + } + + private boolean isValidGenericParameterType(TypeMirror type, TypeCheckContext context) { + TypeMirror genericType = TypeUtils.removeTypeArguments(type); + Type supportedType = get(genericType); + + if (supportedType == null) { + return false; + } + + if (!supportedType.isSupportedWithAnyGenericType()) { + // We need to recursively check all type arguments + for (TypeMirror typeArgument : TypeUtils.extractTypeArguments(type)) { + if (!isValidParameterType(typeArgument, context.toBuilder().setWrapped(true).build())) { + return false; + } + } + } + + return isValidParameterType(supportedType); + } + + ImmutableCollection<Type> usableTypes() { + return usableTypes.values(); + } + + private Type get(TypeMirror type) { + return usableTypes.getOrDefault(type.toString(), null); + } + + CodeBlock generateWriteToParcelCode(String parcelName, Type type, String valueCode) { + if (type.getWriteToParcelCode().isPresent()) { + return CodeBlock.of(type.getWriteToParcelCode().get(), parcelName, valueCode); + } + + throw new IllegalArgumentException( + String.format("%s can not write to parcel", type.getQualifiedName())); + } + + CodeBlock generateReadFromParcelCode(String parcelName, Type type) { + if (type.getReadFromParcelCode().isPresent()) { + return CodeBlock.of(type.getReadFromParcelCode().get(), parcelName); + } + + throw new IllegalArgumentException( + String.format("%s can not read from parcel", type.getQualifiedName())); + } + + public Type getType(TypeMirror type) { + String typeName = type.toString(); + if (!usableTypes.containsKey(typeName)) { + throw new IllegalArgumentException(String.format("%s type not loaded", type)); + } + + return get(type); + } + + private SupportedTypes(Map<String, Type> usableTypes) { + this.usableTypes = ImmutableMap.copyOf(usableTypes); + } + + public static SupportedTypes createFromMethods( + Types types, + Elements elements, + Collection<ParcelableWrapper> parcelableWrappers, + Collection<FutureWrapper> futureWrappers, + Collection<ExecutableElement> methods) { + Map<String, Type> usableTypes = new HashMap<>(); + + addDefaultTypes(types, elements, usableTypes); + addParcelableWrapperTypes(usableTypes, parcelableWrappers); + addFutureWrapperTypes(usableTypes, futureWrappers); + addSupportForUsedTypes(types, elements, usableTypes, methods); + + return new SupportedTypes(usableTypes); + } + + private static void addSupportForUsedTypes( + Types types, + Elements elements, + Map<String, Type> usableTypes, + Collection<ExecutableElement> methods) { + for (ExecutableElement method : methods) { + addSupportForUsedType(types, elements, usableTypes, method.getReturnType()); + + for (VariableElement parameter : method.getParameters()) { + addSupportForUsedType(types, elements, usableTypes, parameter.asType()); + } + } + } + + private static void addSupportForUsedType( + Types types, Elements elements, Map<String, Type> usableTypes, TypeMirror type) { + if (TypeUtils.isArray(type)) { + addSupportForUsedType(types, elements, usableTypes, TypeUtils.extractTypeFromArray(type)); + if (!TypeUtils.extractTypeFromArray(type).getKind().isPrimitive()) { + type = types.getArrayType(elements.getTypeElement("java.lang.Object").asType()); + } + } + + + if (TypeUtils.isGeneric(type)) { + addSupportForGenericUsedType(types, elements, usableTypes, type); + return; + } + Optional<Type> optionalSupportedType = getSupportedType(types, elements, usableTypes, type); + if (!optionalSupportedType.isPresent()) { + // The type isn't supported + return; + } + + Type supportedType = optionalSupportedType.get(); + + // We don't support generic callbacks so any callback interfaces can be picked up here + if (supportedType.isCrossProfileCallbackInterface()) { + for (TypeMirror typeMirror : + supportedType.getCrossProfileCallbackInterface().get().argumentTypes()) { + addSupportForUsedType(types, elements, usableTypes, typeMirror); + } + } + + addUsableType(usableTypes, supportedType); + } + + private static void addSupportForGenericUsedType( + Types types, Elements elements, Map<String, Type> usableTypes, TypeMirror type) { + TypeMirror genericType = TypeUtils.removeTypeArguments(type); + + Optional<Type> optionalSupportedType = + getSupportedType(types, elements, usableTypes, genericType); + if (!optionalSupportedType.isPresent()) { + // The base type isn't supported + return; + } + + Type supportedType = optionalSupportedType.get(); + + addUsableType(usableTypes, supportedType); + + if (!supportedType.isSupportedWithAnyGenericType()) { + for (TypeMirror typeArgument : TypeUtils.extractTypeArguments(type)) { + addSupportForUsedType(types, elements, usableTypes, typeArgument); + } + } + } + + private static Optional<Type> getSupportedType( + Types types, Elements elements, Map<String, Type> usableTypes, TypeMirror type) { + if (usableTypes.containsKey(type.toString())) { + return Optional.of(usableTypes.get(type.toString())); + } + + TypeMirror parcelable = elements.getTypeElement("android.os.Parcelable").asType(); + if (types.isAssignable(type, parcelable)) { + return Optional.of(createParcelableType(type)); + } + + TypeMirror serializable = elements.getTypeElement("java.io.Serializable").asType(); + if (types.isAssignable(type, serializable)) { + return Optional.of(createSerializableType(type)); + } + + TypeElement element = elements.getTypeElement(type.toString()); + + if (element != null && hasCrossProfileCallbackAnnotation(element)) { + return Optional.of(createCrossProfileCallbackType(element)); + } + + // We don't support this type - it will error in a later stage + return Optional.empty(); + } + + private static Type createCrossProfileCallbackType(TypeElement type) { + return Type.builder() + .setTypeMirror(type.asType()) + .setAcceptableReturnType(false) + .setAcceptableParameterType(true) + .setSupportedInsideWrapper(false) + .setSupportedInsideCrossProfileCallback(false) + .setCrossProfileCallbackInterface(CrossProfileCallbackInterfaceInfo.create(type)) + .build(); + } + + private static Type createParcelableType(TypeMirror typeMirror) { + return Type.builder() + .setTypeMirror(typeMirror) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeParcelable($L, flags)") + .setReadFromParcelCode("$L.readParcelable(Bundler.class.getClassLoader())") + // Parcelables must take care of their own generic types + .setSupportedWithAnyGenericType(true) + .build(); + } + + private static Type createSerializableType(TypeMirror typeMirror) { + return Type.builder() + .setTypeMirror(typeMirror) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeSerializable($L)") + .setReadFromParcelCode("$L.readSerializable()") + // Serializables must take care of their own generic types + .setSupportedWithAnyGenericType(true) + .build(); + } + + /** Create a {@link Builder} to create a new {@link SupportedTypes} with modified entries. */ + public Builder asBuilder() { + return new Builder(usableTypes); + } + + private static void addDefaultTypes( + Types types, Elements elements, Map<String, Type> usableTypes) { + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.getNoType(TypeKind.VOID)) + .setAcceptableReturnType(true) + .setReadFromParcelCode("null") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(elements.getTypeElement("java.lang.Void").asType()) + .setAcceptableReturnType(true) + .setReadFromParcelCode("null") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(elements.getTypeElement("java.lang.String").asType()) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeString($L)") + .setReadFromParcelCode("$L.readString()") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.getPrimitiveType(TypeKind.BYTE)) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeByte($L)") + .setReadFromParcelCode("$L.readByte()") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.boxedClass(types.getPrimitiveType(TypeKind.BYTE)).asType()) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeByte($L)") + .setReadFromParcelCode("$L.readByte()") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.getPrimitiveType(TypeKind.SHORT)) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeInt($L)") + .setReadFromParcelCode("(short)$L.readInt()") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.boxedClass(types.getPrimitiveType(TypeKind.SHORT)).asType()) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeInt($L)") + .setReadFromParcelCode("(short)$L.readInt()") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.getPrimitiveType(TypeKind.INT)) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeInt($L)") + .setReadFromParcelCode("$L.readInt()") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.boxedClass(types.getPrimitiveType(TypeKind.INT)).asType()) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeInt($L)") + .setReadFromParcelCode("$L.readInt()") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.getPrimitiveType(TypeKind.LONG)) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeLong($L)") + .setReadFromParcelCode("$L.readLong()") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.boxedClass(types.getPrimitiveType(TypeKind.LONG)).asType()) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeLong($L)") + .setReadFromParcelCode("$L.readLong()") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.getPrimitiveType(TypeKind.FLOAT)) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeFloat($L)") + .setReadFromParcelCode("$L.readFloat()") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.boxedClass(types.getPrimitiveType(TypeKind.FLOAT)).asType()) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeFloat($L)") + .setReadFromParcelCode("$L.readFloat()") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.getPrimitiveType(TypeKind.DOUBLE)) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeDouble($L)") + .setReadFromParcelCode("$L.readDouble()") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.boxedClass(types.getPrimitiveType(TypeKind.DOUBLE)).asType()) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeDouble($L)") + .setReadFromParcelCode("$L.readDouble()") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.getPrimitiveType(TypeKind.CHAR)) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeInt($L)") + .setReadFromParcelCode("(char)$L.readInt()") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.boxedClass(types.getPrimitiveType(TypeKind.CHAR)).asType()) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeInt($L)") + .setReadFromParcelCode("(char)$L.readInt()") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.getPrimitiveType(TypeKind.BOOLEAN)) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeInt($L ? 1 : 0)") + .setReadFromParcelCode("($L.readInt() == 1)") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.boxedClass(types.getPrimitiveType(TypeKind.BOOLEAN)).asType()) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeInt($L ? 1 : 0)") + .setReadFromParcelCode("($L.readInt() == 1)") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(elements.getTypeElement("android.content.Context").asType()) + .setAcceptableParameterType(true) + .setAutomaticallyResolvedReplacement("context") + .setAcceptableReturnType(false) + .setSupportedInsideWrapper(false) + .setSupportedInsideCrossProfileCallback(false) + .build()); + } + + private static void addUsableType(Map<String, Type> usableTypes, Type type) { + usableTypes.put(type.getQualifiedName(), type); + } + + private static void addParcelableWrapperTypes( + Map<String, Type> usableTypes, Collection<ParcelableWrapper> parcelableWrappers) { + for (ParcelableWrapper parcelableWrapper : parcelableWrappers) { + addParcelableWrapperType(usableTypes, parcelableWrapper); + } + } + + private static void addParcelableWrapperType( + Map<String, Type> usableTypes, ParcelableWrapper parcelableWrapper) { + String createParcelableCode = parcelableWrapper.wrapperClassName() + ".of(this, valueType, $L)"; + // "this" will be a Bundler as this code is only run within a Bundler + + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(parcelableWrapper.wrappedType()) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeParcelable(" + createParcelableCode + ", flags)") + .setReadFromParcelCode( + "((" + + parcelableWrapper.wrapperClassName() + + ") $L.readParcelable(Bundler.class.getClassLoader())).get()") + .setParcelableWrapper(parcelableWrapper) + .build()); + } + + private static void addFutureWrapperTypes( + Map<String, Type> usableTypes, Collection<FutureWrapper> futureWrappers) { + for (FutureWrapper futureWrapper : futureWrappers) { + addFutureWrapperType(usableTypes, futureWrapper); + } + } + + private static void addFutureWrapperType( + Map<String, Type> usableTypes, FutureWrapper futureWrapper) { + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(futureWrapper.wrappedType()) + .setAcceptableReturnType(true) + .setSupportedInsideWrapper(false) + .setFutureWrapper(futureWrapper) + .build()); + } + + public static final class Builder { + + private Map<String, Type> usableTypes; + + private Builder(Map<String, Type> usableTypes) { + this.usableTypes = usableTypes; + } + + /** Filtering to only include used types. */ + public Builder filterUsed( + ValidatorContext context, Collection<CrossProfileMethodInfo> methods) { + + Map<String, Type> usedTypes = new HashMap<>(); + + for (CrossProfileMethodInfo method : methods) { + copySupportedTypesForMethod(context, usedTypes, method); + } + + this.usableTypes = usedTypes; + + return this; + } + + private void copySupportedTypesForMethod( + ValidatorContext context, Map<String, Type> usedTypes, CrossProfileMethodInfo method) { + copySupportedType(context, usedTypes, method.returnType()); + for (TypeMirror argumentType : method.parameterTypes()) { + copySupportedType(context, usedTypes, argumentType); + } + } + + private void copySupportedType( + ValidatorContext context, Map<String, Type> usedTypes, TypeMirror type) { + if (TypeUtils.isGeneric(type)) { + copySupportedGenericType(context, usedTypes, type); + return; + } + + if (TypeUtils.isArray(type)) { + copySupportedType(context, usedTypes, TypeUtils.extractTypeFromArray(type)); + if (!TypeUtils.extractTypeFromArray(type).getKind().isPrimitive()) { + type = + context + .types() + .getArrayType(context.elements().getTypeElement("java.lang.Object").asType()); + } + } + + // The type must have been seen in when constructing the original so this should not + // be null + Type supportedType = usableTypes.get(type.toString()); + + // We don't support generic callbacks so any callback interfaces can be picked up here + if (supportedType.isCrossProfileCallbackInterface()) { + for (TypeMirror typeMirror : + supportedType.getCrossProfileCallbackInterface().get().argumentTypes()) { + copySupportedType(context, usedTypes, typeMirror); + } + } + + copySupportedType(usedTypes, supportedType); + } + + private void copySupportedType(Map<String, Type> usedTypes, Type supportedType) { + addUsableType(usedTypes, supportedType); + } + + private void copySupportedGenericType( + ValidatorContext context, Map<String, Type> usedTypes, TypeMirror type) { + TypeMirror genericType = TypeUtils.removeTypeArguments(type); + + // The type must have been seen in when constructing the oldSupportedTypes so this should not + // be null + Type supportedType = usableTypes.get(genericType.toString()); + + if (!supportedType.isSupportedWithAnyGenericType()) { + // We need to recursively copy all type arguments + for (TypeMirror typeArgument : TypeUtils.extractTypeArguments(type)) { + copySupportedType(context, usedTypes, typeArgument); + } + } + + copySupportedType(usedTypes, supportedType); + } + + /** Add additianal parcelable wrappers. */ + public Builder addParcelableWrappers(Collection<ParcelableWrapper> parcelableWrappers) { + Map<String, Type> newUsableTypes = new HashMap<>(usableTypes); + + addParcelableWrapperTypes(newUsableTypes, parcelableWrappers); + + usableTypes = newUsableTypes; + + return this; + } + + /** Add additianal future wrappers. */ + public Builder addFutureWrappers(Collection<FutureWrapper> futureWrappers) { + Map<String, Type> newUsableTypes = new HashMap<>(usableTypes); + + addFutureWrapperTypes(newUsableTypes, futureWrappers); + + usableTypes = newUsableTypes; + + return this; + } + + public Builder replaceWrapperPrefix(ClassName prefix) { + Map<String, Type> newUsableTypes = new HashMap<>(); + + for (Type usableType : usableTypes.values()) { + if (usableType.getParcelableWrapper().isPresent()) { + replaceParcelableWrapperPrefix(newUsableTypes, prefix, usableType); + } else if (usableType.getFutureWrapper().isPresent()) { + replaceFutureWrapperPrefix(newUsableTypes, prefix, usableType); + } else { + addUsableType(newUsableTypes, usableType); + } + } + + usableTypes = newUsableTypes; + + return this; + } + + private void replaceParcelableWrapperPrefix( + Map<String, Type> newUsableTypes, ClassName prefix, Type usableType) { + ParcelableWrapper parcelableWrapper = usableType.getParcelableWrapper().get(); + + if (parcelableWrapper.wrapperType().equals(ParcelableWrapper.WrapperType.CUSTOM)) { + // Custom types never get prefixed + addUsableType(newUsableTypes, usableType); + return; + } + + addParcelableWrapperType( + newUsableTypes, + ParcelableWrapper.create( + parcelableWrapper.wrappedType(), + parcelableWrapper.defaultWrapperClassName(), + prefix(prefix, parcelableWrapper.wrapperClassName()), + parcelableWrapper.wrapperType())); + } + + private void replaceFutureWrapperPrefix( + Map<String, Type> newUsableTypes, ClassName prefix, Type usableType) { + FutureWrapper futureWrapper = usableType.getFutureWrapper().get(); + + if (futureWrapper.wrapperType().equals(FutureWrapper.WrapperType.CUSTOM)) { + // Custom types never get prefixed + addUsableType(newUsableTypes, usableType); + return; + } + + addFutureWrapperType( + newUsableTypes, + FutureWrapper.create( + futureWrapper.wrappedType(), + futureWrapper.defaultWrapperClassName(), + prefix(prefix, futureWrapper.wrapperClassName()), + futureWrapper.wrapperType())); + } + + private ClassName prefix(ClassName prefix, ClassName finalName) { + return ClassName.get( + prefix.packageName(), prefix.simpleName() + "_" + finalName.simpleName()); + } + + /** Build a new {@link SupportedTypes}. */ + public SupportedTypes build() { + return new SupportedTypes(usableTypes); + } + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/TestCodeGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/TestCodeGenerator.java new file mode 100644 index 0000000..77483f5 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/TestCodeGenerator.java @@ -0,0 +1,97 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTestInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.google.android.enterprise.connectedapps.processor.containers.ProfileConnectorInfo; +import com.google.android.enterprise.connectedapps.processor.containers.ProviderClassInfo; +import java.util.HashSet; +import java.util.Set; + +/** + * Generator of cross-profile test code. + * + * <p>This is intended to be initialised and used once, which will generate all needed code. + * + * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to + * validate that the annotated code is correct. + */ +final class TestCodeGenerator { + private boolean generated = false; + private final GeneratorContext generatorContext; + private final Set<CrossProfileTypeInfo> fakedTypes = new HashSet<>(); + private final Set<ProfileConnectorInfo> fakedConnectors = new HashSet<>(); + + TestCodeGenerator(GeneratorContext generatorContext) { + this.generatorContext = checkNotNull(generatorContext); + } + + void generate() { + if (generated) { + throw new IllegalStateException("TestCodeGenerator#generate can only be called once"); + } + generated = true; + + collectTestTypes(); + generateFakes(); + } + + private void generateFakes() { + for (ProfileConnectorInfo connector : fakedConnectors) { + new FakeProfileConnectorGenerator(generatorContext, connector).generate(); + } + + for (CrossProfileTypeInfo type : fakedTypes) { + new FakeCrossProfileTypeGenerator(generatorContext, type).generate(); + new FakeOtherGenerator(generatorContext, type).generate(); + } + } + + private void collectTestTypes() { + for (CrossProfileTestInfo crossProfileTest : generatorContext.crossProfileTests()) { + collectTestTypes(crossProfileTest); + } + } + + private void collectTestTypes(CrossProfileTestInfo crossProfileTest) { + for (CrossProfileConfigurationInfo configuration : crossProfileTest.configurations()) { + collectTestTypes(configuration); + } + } + + private void collectTestTypes(CrossProfileConfigurationInfo configuration) { + for (ProviderClassInfo provider : configuration.providers()) { + collectTestTypes(provider); + } + + fakedConnectors.add(configuration.profileConnector()); + } + + private void collectTestTypes(ProviderClassInfo provider) { + for (CrossProfileTypeInfo type : provider.allCrossProfileTypes()) { + collectTestTypes(type); + } + } + + private void collectTestTypes(CrossProfileTypeInfo type) { + fakedTypes.add(type); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/TypeUtils.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/TypeUtils.java new file mode 100644 index 0000000..6d5d073 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/TypeUtils.java @@ -0,0 +1,121 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.BUNDLER_TYPE_CLASSNAME; +import static java.util.stream.Collectors.toList; + +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import java.util.ArrayList; +import java.util.List; +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; + +/** Utilities for manipulating {@link TypeMirror} instances. */ +public class TypeUtils { + + public static boolean isArray(TypeMirror type) { + return type instanceof ArrayType; + } + + /** + * Extract a type from an array. + * + * <p>Assumes that {@code type} represents an array. + */ + public static TypeMirror extractTypeFromArray(TypeMirror type) { + return ((ArrayType) type).getComponentType(); + } + + public static boolean isGeneric(TypeMirror type) { + if (type instanceof DeclaredType) { + return !((DeclaredType) type).getTypeArguments().isEmpty(); + } + return false; + } + + public static TypeMirror removeTypeArguments(TypeMirror type) { + if (type instanceof DeclaredType) { + return ((DeclaredType) type).asElement().asType(); + } + return type; + } + + public static List<TypeMirror> extractTypeArguments(TypeMirror type) { + if (!(type instanceof DeclaredType)) { + return null; + } + + return new ArrayList<>(((DeclaredType) type).getTypeArguments()); + } + + static ClassName getRawTypeClassName(TypeMirror type) { + String rawTypeQualifiedName = getRawTypeQualifiedName(type); + + if (!rawTypeQualifiedName.contains(".")) { + return ClassName.get("", rawTypeQualifiedName); + } + + String packageName = rawTypeQualifiedName.substring(0, rawTypeQualifiedName.lastIndexOf(".")); + String simpleName = rawTypeQualifiedName.substring(rawTypeQualifiedName.lastIndexOf(".") + 1); + + return ClassName.get(packageName, simpleName); + } + + static String getRawTypeQualifiedName(TypeMirror type) { + // This converts e.g. java.util.List<String> into java.util.List + return type.toString().split("<", 2)[0]; + } + + static CodeBlock generateBundlerType(TypeMirror type) { + if (isArray(type)) { + return generateArrayBundlerType(type); + } + if (isGeneric(type)) { + return generateGenericBundlerType(type); + } + return CodeBlock.of("$T.of($S)", BUNDLER_TYPE_CLASSNAME, getRawTypeQualifiedName(type)); + } + + private static CodeBlock generateArrayBundlerType(TypeMirror type) { + TypeMirror arrayType = extractTypeFromArray(type); + + return CodeBlock.of( + "$T.of($S, $L)", + BUNDLER_TYPE_CLASSNAME, + "java.lang.Object[]", + generateBundlerType(arrayType)); + } + + private static CodeBlock generateGenericBundlerType(TypeMirror type) { + CodeBlock.Builder typeArgs = CodeBlock.builder(); + + List<CodeBlock> typeArgBlocks = + extractTypeArguments(type).stream().map(TypeUtils::generateBundlerType).collect(toList()); + + typeArgs.add(typeArgBlocks.get(0)); + for (CodeBlock typeArgBlock : typeArgBlocks.subList(1, typeArgBlocks.size())) { + typeArgs.add(", $L", typeArgBlock); + } + + return CodeBlock.of( + "$T.of($S, $L)", BUNDLER_TYPE_CLASSNAME, getRawTypeQualifiedName(type), typeArgs.build()); + } + + private TypeUtils() {} +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/UserConnectorCodeGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/UserConnectorCodeGenerator.java new file mode 100644 index 0000000..415765b --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/UserConnectorCodeGenerator.java @@ -0,0 +1,178 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.ABSTRACT_USER_CONNECTOR_BUILDER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.ABSTRACT_USER_CONNECTOR_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.AVAILABILITY_RESTRICTIONS_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CONNECTION_BINDER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CONTEXT_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.SCHEDULED_EXECUTOR_SERVICE_CLASSNAME; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.annotations.GeneratedUserConnector; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.google.android.enterprise.connectedapps.processor.containers.UserConnectorInfo; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeSpec; +import javax.lang.model.element.Modifier; + +/** + * Generate the {@code Generated*} class for a single {@link GeneratedUserConnector} annotated + * class. + * + * <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to + * validate that the annotated code is correct. + */ +class UserConnectorCodeGenerator { + private boolean generated = false; + + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final UserConnectorInfo connector; + + UserConnectorCodeGenerator(GeneratorContext generatorContext, UserConnectorInfo connector) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.connector = checkNotNull(connector); + } + + void generate() { + if (generated) { + throw new IllegalStateException( + "ProfileConnectorCodeGenerator#generate can only be called once"); + } + generated = true; + + generateUserConnector(); + } + + private void generateUserConnector() { + ClassName className = getGeneratedUserConnectorClassName(generatorContext, connector); + ClassName builderClassName = + getGeneratedUserConnectorBuilderClassName(generatorContext, connector); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addJavadoc( + "Generated implementation of {@link $T}.\n\n" + + "<p>All logic is implemented by {@link $T}.\n", + connector.connectorClassName(), + ABSTRACT_USER_CONNECTOR_CLASSNAME) + .addModifiers(Modifier.FINAL) + .addSuperinterface(connector.connectorClassName()) + .superclass(ABSTRACT_USER_CONNECTOR_CLASSNAME); + + classBuilder.addMethod( + MethodSpec.methodBuilder("builder") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(builderClassName) + .addParameter(CONTEXT_CLASSNAME, "context") + .addStatement("return new $T(context)", builderClassName) + .build()); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PRIVATE) + .addParameter(builderClassName, "builder") + .addStatement( + "super($1T.class, builder.profileConnectorBuilder)", connector.connectorClassName()) + .build()); + + generateUserConnectorBuilder(classBuilder); + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private void generateUserConnectorBuilder(TypeSpec.Builder profileConnector) { + ClassName connectorClassName = getGeneratedUserConnectorClassName(generatorContext, connector); + ClassName builderClassName = + getGeneratedUserConnectorBuilderClassName(generatorContext, connector); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(builderClassName) + .addJavadoc("Builder for {@link $T}.\n", connectorClassName) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC); + + CodeBlock initialiser = + CodeBlock.of( + "new $T().setServiceClassName($S).setAvailabilityRestrictions($T.$L)", + ABSTRACT_USER_CONNECTOR_BUILDER_CLASSNAME, + connector.serviceName().toString(), + AVAILABILITY_RESTRICTIONS_CLASSNAME, + connector.availabilityRestrictions().name()); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addParameter(CONTEXT_CLASSNAME, "context") + .addStatement("profileConnectorBuilder.setContext(context)") + .build()); + + classBuilder.addField( + FieldSpec.builder(ABSTRACT_USER_CONNECTOR_BUILDER_CLASSNAME, "profileConnectorBuilder") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .initializer(initialiser) + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("setScheduledExecutorService") + .addModifiers(Modifier.PUBLIC) + .addParameter(SCHEDULED_EXECUTOR_SERVICE_CLASSNAME, "scheduledExecutorService") + .returns(builderClassName) + .addStatement( + "profileConnectorBuilder.setScheduledExecutorService(scheduledExecutorService)") + .addStatement("return this") + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("setBinder") + .addModifiers(Modifier.PUBLIC) + .addParameter(CONNECTION_BINDER_CLASSNAME, "binder") + .returns(builderClassName) + .addStatement("profileConnectorBuilder.setBinder(binder)") + .addStatement("return this") + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("build") + .addModifiers(Modifier.PUBLIC) + .returns(connectorClassName) + .addStatement("return new $1T(this)", connectorClassName) + .build()); + + profileConnector.addType(classBuilder.build()); + } + + static ClassName getGeneratedUserConnectorClassName( + GeneratorContext generatorContext, UserConnectorInfo connector) { + return ClassName.get( + connector.connectorClassName().packageName(), + "Generated" + connector.connectorClassName().simpleName()); + } + + static ClassName getGeneratedUserConnectorBuilderClassName( + GeneratorContext generatorContext, UserConnectorInfo connector) { + return ClassName.get( + connector.connectorClassName().packageName() + + "." + + "Generated" + + connector.connectorClassName().simpleName(), + "Builder"); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ValidationMessageFormatter.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ValidationMessageFormatter.java new file mode 100644 index 0000000..e5f4a01 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ValidationMessageFormatter.java @@ -0,0 +1,49 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationNames; + +/** Formats annotation validation messages with the provided names of the annotation set. */ +public final class ValidationMessageFormatter { + + private final AnnotationNames annotationNames; + + public static ValidationMessageFormatter forAnnotations(AnnotationNames annotationNames) { + return new ValidationMessageFormatter(annotationNames); + } + + private ValidationMessageFormatter(AnnotationNames annotationNames) { + this.annotationNames = annotationNames; + } + + /** + * Supports the replacement strings CROSS_PROFILE_ANNOTATION, CROSS_PROFILE_CALLBACK_ANNOTATION, + * CROSS_PROFILE_CONFIGURATION_ANNOTATION, CROSS_PROFILE_CONFIGURATIONS_ANNOTATION, + * CROSS_PROFILE_PROVIDER_ANNOTATION, and CROSS_PROFILE_TEST_ANNOTATION. + */ + String format(String message) { + return message + .replace("CROSS_PROFILE_ANNOTATION", annotationNames.crossProfile()) + .replace("CROSS_PROFILE_CALLBACK_ANNOTATION", annotationNames.crossProfileCallback()) + .replace( + "CROSS_PROFILE_CONFIGURATION_ANNOTATION", annotationNames.crossProfileConfiguration()) + .replace( + "CROSS_PROFILE_CONFIGURATIONS_ANNOTATION", annotationNames.crossProfileConfigurations()) + .replace("CROSS_PROFILE_PROVIDER_ANNOTATION", annotationNames.crossProfileProvider()) + .replace("CROSS_PROFILE_TEST_ANNOTATION", annotationNames.crossProfileTest()); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationClasses.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationClasses.java new file mode 100644 index 0000000..1616838 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationClasses.java @@ -0,0 +1,42 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.annotationdiscovery; + +import com.google.android.enterprise.connectedapps.annotations.CrossUser; +import com.google.android.enterprise.connectedapps.annotations.CrossUserCallback; +import com.google.android.enterprise.connectedapps.annotations.CrossUserProvider; +import java.lang.annotation.Annotation; + +/** + * A set of parallel annotation classes. + * + * <p>For example, a valid instance could return {@link CrossUser}, {@link CrossUserCallback}, + * {@link CrossUserProvider}, etc. + */ +public interface AnnotationClasses { + + Class<? extends Annotation> crossProfileAnnotationClass(); + + Class<? extends Annotation> crossProfileCallbackAnnotationClass(); + + Class<? extends Annotation> crossProfileConfigurationAnnotationClass(); + + Class<? extends Annotation> crossProfileConfigurationsAnnotationClass(); + + Class<? extends Annotation> crossProfileProviderAnnotationClass(); + + Class<? extends Annotation> crossProfileTestAnnotationClass(); +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationFinder.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationFinder.java new file mode 100644 index 0000000..4923567 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationFinder.java @@ -0,0 +1,271 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.annotationdiscovery; + +import static java.util.stream.Collectors.toSet; +import static javax.lang.model.element.ElementKind.METHOD; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfile; +import com.google.android.enterprise.connectedapps.annotations.CrossProfileCallback; +import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration; +import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfigurations; +import com.google.android.enterprise.connectedapps.annotations.CrossProfileProvider; +import com.google.android.enterprise.connectedapps.annotations.CrossUser; +import com.google.android.enterprise.connectedapps.annotations.CrossUserCallback; +import com.google.android.enterprise.connectedapps.annotations.CrossUserConfiguration; +import com.google.android.enterprise.connectedapps.annotations.CrossUserConfigurations; +import com.google.android.enterprise.connectedapps.annotations.CrossUserProvider; +import com.google.android.enterprise.connectedapps.processor.ValidationMessageFormatter; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileAnnotationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackAnnotationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationAnnotationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationsAnnotationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileProviderAnnotationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTestAnnotationInfo; +import com.google.android.enterprise.connectedapps.testing.annotations.CrossProfileTest; +import com.google.android.enterprise.connectedapps.testing.annotations.CrossUserTest; +import com.google.common.collect.ImmutableList; +import java.lang.annotation.Annotation; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Stream; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +/** Helper methods to discover all cross-profile annotations of a specific type on elements. */ +public final class AnnotationFinder { + + private static final AnnotationStrings CROSS_PROFILE_ANNOTATION_STRINGS = + AnnotationStrings.builder() + .setCrossProfileAnnotationClass(CrossProfile.class) + .setCrossProfileCallbackAnnotationClass(CrossProfileCallback.class) + .setCrossProfileConfigurationAnnotationClass(CrossProfileConfiguration.class) + .setCrossProfileConfigurationsAnnotationClass(CrossProfileConfigurations.class) + .setCrossProfileProviderAnnotationClass(CrossProfileProvider.class) + .setCrossProfileTestAnnotationClass(CrossProfileTest.class) + .build(); + + private static final AnnotationStrings CROSS_USER_ANNOTATION_STRINGS = + AnnotationStrings.builder() + .setCrossProfileAnnotationClass(CrossUser.class) + .setCrossProfileCallbackAnnotationClass(CrossUserCallback.class) + .setCrossProfileProviderAnnotationClass(CrossUserProvider.class) + .setCrossProfileConfigurationAnnotationClass(CrossUserConfiguration.class) + .setCrossProfileConfigurationsAnnotationClass(CrossUserConfigurations.class) + .setCrossProfileTestAnnotationClass(CrossUserTest.class) + .build(); + + private static final ImmutableList<AnnotationStrings> SUPPORTED_ANNOTATIONS = + ImmutableList.of(CROSS_PROFILE_ANNOTATION_STRINGS, CROSS_USER_ANNOTATION_STRINGS); + + private static final AnnotationStrings DEFAULT_ANNOTATIONS = CROSS_PROFILE_ANNOTATION_STRINGS; + + private static final Set<Class<? extends Annotation>> crossProfileAnnotations = + annotationsOfType(AnnotationClasses::crossProfileAnnotationClass); + + private static final Set<Class<? extends Annotation>> crossProfileCallbackAnnotations = + annotationsOfType(AnnotationClasses::crossProfileCallbackAnnotationClass); + + private static final Set<Class<? extends Annotation>> crossProfileConfigurationAnnotations = + annotationsOfType(AnnotationClasses::crossProfileConfigurationAnnotationClass); + + private static final Set<Class<? extends Annotation>> crossProfileConfigurationsAnnotations = + annotationsOfType(AnnotationClasses::crossProfileConfigurationsAnnotationClass); + + private static final Set<Class<? extends Annotation>> crossProfileProviderAnnotations = + annotationsOfType(AnnotationClasses::crossProfileProviderAnnotationClass); + + private static final Set<Class<? extends Annotation>> crossProfileTestAnnotations = + annotationsOfType(AnnotationClasses::crossProfileTestAnnotationClass); + + public static Iterable<AnnotationStrings> annotationStrings() { + return SUPPORTED_ANNOTATIONS; + } + + public static AnnotationNames crossProfileAnnotationNames() { + return CROSS_PROFILE_ANNOTATION_STRINGS; + } + + public static AnnotationNames crossUserAnnotationNames() { + return CROSS_USER_ANNOTATION_STRINGS; + } + + public static ValidationMessageFormatter validationMessageFormatterFor(Element element) { + return ValidationMessageFormatter.forAnnotations(annotationNamesFor(element)); + } + + private static AnnotationNames annotationNamesFor(Element element) { + for (AnnotationStrings annotationStrings : SUPPORTED_ANNOTATIONS) { + if (hasAnyAnnotationsOfClass(element, annotationStrings)) { + return annotationStrings; + } + } + + return DEFAULT_ANNOTATIONS; + } + + public static ValidationMessageFormatter validationMessageFormatterForClass( + TypeElement typeElement) { + return ValidationMessageFormatter.forAnnotations(annotationNamesForClass(typeElement)); + } + + public static AnnotationNames annotationNamesForClass(TypeElement typeElement) { + for (AnnotationStrings annotationStrings : SUPPORTED_ANNOTATIONS) { + if (hasAnyAnnotationsOfClass(typeElement, annotationStrings)) { + return annotationStrings; + } + + for (ExecutableElement method : + typeElement.getEnclosedElements().stream() + .filter(element -> element.getKind() == METHOD) + .map(element -> (ExecutableElement) element) + .collect(toSet())) { + if (hasAnyAnnotationsOfClass(method, annotationStrings)) { + return annotationStrings; + } + } + } + + return DEFAULT_ANNOTATIONS; + } + + private static boolean hasAnyAnnotationsOfClass( + Element element, AnnotationClasses annotationClasses) { + return hasAnnotationOfClass(element, annotationClasses.crossProfileAnnotationClass()) + || hasAnnotationOfClass(element, annotationClasses.crossProfileCallbackAnnotationClass()) + || hasAnnotationOfClass(element, annotationClasses.crossProfileProviderAnnotationClass()) + || hasAnnotationOfClass( + element, annotationClasses.crossProfileConfigurationAnnotationClass()) + || hasAnnotationOfClass( + element, annotationClasses.crossProfileConfigurationsAnnotationClass()) + || hasAnnotationOfClass(element, annotationClasses.crossProfileTestAnnotationClass()); + } + + private static boolean hasAnnotationOfClass( + Element element, Class<? extends Annotation> annotationClass) { + return element.getAnnotation(annotationClass) != null; + } + + public static CrossProfileAnnotationInfo extractCrossProfileAnnotationInfo( + Element annotatedElement, Types types, Elements elements) { + return new CrossProfileAnnotationInfoExtractor() + .extractAnnotationInfo(SUPPORTED_ANNOTATIONS, annotatedElement, types, elements); + } + + public static CrossProfileCallbackAnnotationInfo extractCrossProfileCallbackAnnotationInfo( + Element annotatedElement, Types types, Elements elements) { + return new CrossProfileCallbackAnnotationInfoExtractor() + .extractAnnotationInfo(SUPPORTED_ANNOTATIONS, annotatedElement, types, elements); + } + + public static CrossProfileConfigurationAnnotationInfo + extractCrossProfileConfigurationAnnotationInfo( + Element annotatedElement, Types types, Elements elements) { + return new CrossProfileConfigurationAnnotationInfoExtractor() + .extractAnnotationInfo(SUPPORTED_ANNOTATIONS, annotatedElement, types, elements); + } + + public static CrossProfileConfigurationsAnnotationInfo + extractCrossProfileConfigurationsAnnotationInfo( + Element annotatedElement, Types types, Elements elements) { + return new CrossProfileConfigurationsAnnotationInfoExtractor() + .extractAnnotationInfo(SUPPORTED_ANNOTATIONS, annotatedElement, types, elements); + } + + public static CrossProfileProviderAnnotationInfo extractCrossProfileProviderAnnotationInfo( + Element annotatedElement, Types types, Elements elements) { + return new CrossProfileProviderAnnotationInfoExtractor() + .extractAnnotationInfo(SUPPORTED_ANNOTATIONS, annotatedElement, types, elements); + } + + public static CrossProfileTestAnnotationInfo extractCrossProfileTestAnnotationInfo( + Element annotatedElement, Types types, Elements elements) { + return new CrossProfileTestAnnotationInfoExtractor() + .extractAnnotationInfo(SUPPORTED_ANNOTATIONS, annotatedElement, types, elements); + } + + public static boolean hasCrossProfileAnnotation(Element element) { + return hasAnyAnnotations(element, crossProfileAnnotations); + } + + public static boolean hasCrossProfileCallbackAnnotation(Element element) { + return hasAnyAnnotations(element, crossProfileCallbackAnnotations); + } + + public static boolean hasCrossProfileConfigurationAnnotation(Element element) { + return hasAnyAnnotations(element, crossProfileConfigurationAnnotations); + } + + public static boolean hasCrossProfileConfigurationsAnnotation(Element element) { + return hasAnyAnnotations(element, crossProfileConfigurationsAnnotations); + } + + public static boolean hasCrossProfileProviderAnnotation(Element element) { + return hasAnyAnnotations(element, crossProfileProviderAnnotations); + } + + private static boolean hasAnyAnnotations( + Element element, Set<Class<? extends Annotation>> annotations) { + return annotations.stream().anyMatch(annotation -> element.getAnnotation(annotation) != null); + } + + public static Stream<? extends Element> elementsAnnotatedWithCrossProfile( + RoundEnvironment roundEnv) { + return findElementsContainingAnnotations(roundEnv, crossProfileAnnotations); + } + + public static Stream<? extends Element> elementsAnnotatedWithCrossProfileCallback( + RoundEnvironment roundEnv) { + return findElementsContainingAnnotations(roundEnv, crossProfileCallbackAnnotations); + } + + public static Stream<? extends Element> elementsAnnotatedWithCrossProfileConfiguration( + RoundEnvironment roundEnv) { + return findElementsContainingAnnotations(roundEnv, crossProfileConfigurationAnnotations); + } + + public static Stream<? extends Element> elementsAnnotatedWithCrossProfileConfigurations( + RoundEnvironment roundEnv) { + return findElementsContainingAnnotations(roundEnv, crossProfileConfigurationsAnnotations); + } + + public static Stream<? extends Element> elementsAnnotatedWithCrossProfileProvider( + RoundEnvironment roundEnv) { + return findElementsContainingAnnotations(roundEnv, crossProfileProviderAnnotations); + } + + public static Stream<? extends Element> elementsAnnotatedWithCrossProfileTest( + RoundEnvironment roundEnv) { + return findElementsContainingAnnotations(roundEnv, crossProfileTestAnnotations); + } + + private static Stream<? extends Element> findElementsContainingAnnotations( + RoundEnvironment roundEnv, Set<Class<? extends Annotation>> annotations) { + return annotations.stream() + .flatMap(annotation -> roundEnv.getElementsAnnotatedWith(annotation).stream()); + } + + private static Set<Class<? extends Annotation>> annotationsOfType( + Function<AnnotationClasses, Class<? extends Annotation>> annotationClassGetter) { + return SUPPORTED_ANNOTATIONS.stream().map(annotationClassGetter).collect(toSet()); + } + + private AnnotationFinder() {} +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationInfoExtractor.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationInfoExtractor.java new file mode 100644 index 0000000..08022c4 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationInfoExtractor.java @@ -0,0 +1,92 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.annotationdiscovery; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileProvider; +import com.google.android.enterprise.connectedapps.annotations.CrossUserProvider; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileProviderAnnotation; +import java.lang.annotation.Annotation; +import java.lang.reflect.Proxy; +import javax.lang.model.element.Element; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +/** + * An extractor which generates {@link AnnotationInfoT} for elements annotated with annotations that + * conform to {@link AnnotationInterfaceT}. + */ +abstract class AnnotationInfoExtractor<AnnotationInfoT, AnnotationInterfaceT> { + + private final Class<AnnotationInterfaceT> annotationInterfaceClass; + + AnnotationInfoExtractor(Class<AnnotationInterfaceT> annotationInterfaceClass) { + this.annotationInterfaceClass = annotationInterfaceClass; + } + + /** + * Returns the {@link AnnotationInfoT} that can be extracted from the first supported annotation + * on {@code annotatedElement}, or a default instance otherwise. + */ + AnnotationInfoT extractAnnotationInfo( + Iterable<? extends AnnotationClasses> availableAnnotations, + Element annotatedElement, + Types types, + Elements elements) { + for (AnnotationClasses annotationClasses : availableAnnotations) { + Annotation annotation = + annotatedElement.getAnnotation(supportedAnnotationClass(annotationClasses)); + + if (annotation != null) { + return annotationInfoFromAnnotation( + wrapAnnotationWithInterface(annotationInterfaceClass, annotation), types); + } + } + + return emptyAnnotationInfo(elements); + } + + /** + * Returns the class of the annotation type that this extractor generates {@link AnnotationInfoT} + * for. + * + * <p>For example, if supporting {@link CrossProfileProvider} and {@link CrossUserProvider} + * annotations, return the value of {@link + * AnnotationClasses#crossProfileProviderAnnotationClass()}. + */ + protected abstract Class<? extends Annotation> supportedAnnotationClass( + AnnotationClasses annotationClasses); + + protected abstract AnnotationInfoT annotationInfoFromAnnotation( + AnnotationInterfaceT annotation, Types types); + + protected abstract AnnotationInfoT emptyAnnotationInfo(Elements elements); + + /** + * Wraps any annotation of a specific type (e.g. {@link CrossProfileProvider} and {@link + * CrossUserProvider}) with its interface (in that case {@link CrossProfileProviderAnnotation}). + * + * <p>Java does not allow annotation subclassing so we use Java proxies to treat these different + * annotations with identical interfaces polymorphically. + */ + protected static <T> T wrapAnnotationWithInterface( + Class<T> annotationInterfaceClass, Annotation annotation) { + return annotationInterfaceClass.cast( + Proxy.newProxyInstance( + annotationInterfaceClass.getClassLoader(), + new Class<?>[] {annotationInterfaceClass}, + new AnnotationInvocationHandler(annotation))); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationInvocationHandler.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationInvocationHandler.java new file mode 100644 index 0000000..0179dd9 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationInvocationHandler.java @@ -0,0 +1,49 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.annotationdiscovery; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Given an annotation, forwards method calls from a mock instance of its interface to its actual + * instance. + * + * <p>This allows us to treat separate annotations with identical interfaces polymorphically. + */ +class AnnotationInvocationHandler implements InvocationHandler { + + private final Annotation annotation; + + AnnotationInvocationHandler(Annotation annotation) { + this.annotation = annotation; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + return invokeAndUnwrapExceptions(annotation.annotationType().getMethod(method.getName())); + } + + private Object invokeAndUnwrapExceptions(Method method) throws Throwable { + try { + return method.invoke(annotation); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationNames.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationNames.java new file mode 100644 index 0000000..37c3961 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationNames.java @@ -0,0 +1,37 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.annotationdiscovery; + +/** + * A set of parallel annotation names. + * + * <p>For example, a valid instance could return "CrossUser", "CrossUserCallback", and + * "CrossUserProvider", etc. + */ +public interface AnnotationNames { + + String crossProfile(); + + String crossProfileCallback(); + + String crossProfileConfiguration(); + + String crossProfileConfigurations(); + + String crossProfileProvider(); + + String crossProfileTest(); +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationPrinter.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationPrinter.java new file mode 100644 index 0000000..adf1662 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationPrinter.java @@ -0,0 +1,52 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.annotationdiscovery; + +/** Prints annotations as they would appear in source code. */ +public interface AnnotationPrinter { + + String crossProfileAsAnnotation(); + + String crossProfileAsAnnotation(String content); + + String crossProfileQualifiedName(); + + String crossProfileCallbackAsAnnotation(); + + String crossProfileCallbackAsAnnotation(String content); + + String crossProfileCallbackQualifiedName(); + + String crossProfileConfigurationAsAnnotation(); + + String crossProfileConfigurationAsAnnotation(String content); + + String crossProfileConfigurationQualifiedName(); + + String crossProfileConfigurationsAsAnnotation(String content); + + String crossProfileConfigurationsQualifiedName(); + + String crossProfileProviderAsAnnotation(); + + String crossProfileProviderAsAnnotation(String content); + + String crossProfileProviderQualifiedName(); + + String crossProfileTestAsAnnotation(String content); + + String crossProfileTestQualifiedName(); +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationStrings.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationStrings.java new file mode 100644 index 0000000..35ce81a --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationStrings.java @@ -0,0 +1,188 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.annotationdiscovery; + +import com.google.auto.value.AutoValue; +import java.lang.annotation.Annotation; + +/** Provides the raw string names for cross-profile annotations. */ +@AutoValue +public abstract class AnnotationStrings + implements AnnotationNames, AnnotationPrinter, AnnotationClasses { + + @Override + public String crossProfile() { + return crossProfileAnnotationClass().getSimpleName(); + } + + @Override + public String crossProfileAsAnnotation() { + return asAnnotation(crossProfile()); + } + + @Override + public String crossProfileAsAnnotation(String content) { + return asAnnotationWithContent(crossProfile(), content); + } + + @Override + public String crossProfileQualifiedName() { + return crossProfileAnnotationClass().getCanonicalName(); + } + + @Override + public String crossProfileCallback() { + return crossProfileCallbackAnnotationClass().getSimpleName(); + } + + @Override + public String crossProfileCallbackAsAnnotation() { + return asAnnotation(crossProfileCallback()); + } + + @Override + public String crossProfileCallbackAsAnnotation(String content) { + return crossProfileCallbackAsAnnotation() + "(" + content + ")"; + } + + @Override + public String crossProfileCallbackQualifiedName() { + return crossProfileCallbackAnnotationClass().getCanonicalName(); + } + + @Override + public String crossProfileConfiguration() { + return crossProfileConfigurationAnnotationClass().getSimpleName(); + } + + @Override + public String crossProfileConfigurationAsAnnotation() { + return asAnnotation(crossProfileConfiguration()); + } + + @Override + public String crossProfileConfigurationAsAnnotation(String content) { + return asAnnotationWithContent(crossProfileConfiguration(), content); + } + + @Override + public String crossProfileConfigurationQualifiedName() { + return crossProfileConfigurationAnnotationClass().getCanonicalName(); + } + + @Override + public String crossProfileConfigurations() { + return crossProfileConfigurationsAnnotationClass().getSimpleName(); + } + + @Override + public String crossProfileConfigurationsAsAnnotation(String content) { + return asAnnotationWithContent(crossProfileProvider(), content); + } + + @Override + public String crossProfileConfigurationsQualifiedName() { + return crossProfileConfigurationsAnnotationClass().getCanonicalName(); + } + + @Override + public String crossProfileProvider() { + return crossProfileProviderAnnotationClass().getSimpleName(); + } + + @Override + public String crossProfileProviderAsAnnotation() { + return asAnnotation(crossProfileProvider()); + } + + @Override + public String crossProfileProviderAsAnnotation(String content) { + return asAnnotationWithContent(crossProfileProvider(), content); + } + + @Override + public String crossProfileProviderQualifiedName() { + return crossProfileProviderAnnotationClass().getCanonicalName(); + } + + @Override + public String crossProfileTest() { + return crossProfileTestAnnotationClass().getSimpleName(); + } + + @Override + public String crossProfileTestAsAnnotation(String content) { + return asAnnotationWithContent(crossProfileTest(), content); + } + + @Override + public String crossProfileTestQualifiedName() { + return crossProfileTestAnnotationClass().getCanonicalName(); + } + + @Override + public final String toString() { + return crossProfile() + " AnnotationStrings"; + } + + private static String asAnnotation(String annotationName) { + return "@" + annotationName; + } + + private static String asAnnotationWithContent(String annotationName, String content) { + return "@" + annotationName + "(" + content + ")"; + } + + @Override + public abstract Class<? extends Annotation> crossProfileAnnotationClass(); + + @Override + public abstract Class<? extends Annotation> crossProfileCallbackAnnotationClass(); + + @Override + public abstract Class<? extends Annotation> crossProfileConfigurationAnnotationClass(); + + @Override + public abstract Class<? extends Annotation> crossProfileConfigurationsAnnotationClass(); + + @Override + public abstract Class<? extends Annotation> crossProfileProviderAnnotationClass(); + + @Override + public abstract Class<? extends Annotation> crossProfileTestAnnotationClass(); + + static Builder builder() { + return new AutoValue_AnnotationStrings.Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + abstract Builder setCrossProfileAnnotationClass(Class<? extends Annotation> value); + + abstract Builder setCrossProfileCallbackAnnotationClass(Class<? extends Annotation> value); + + abstract Builder setCrossProfileConfigurationAnnotationClass(Class<? extends Annotation> value); + + abstract Builder setCrossProfileConfigurationsAnnotationClass( + Class<? extends Annotation> value); + + abstract Builder setCrossProfileProviderAnnotationClass(Class<? extends Annotation> value); + + abstract Builder setCrossProfileTestAnnotationClass(Class<? extends Annotation> value); + + abstract AnnotationStrings build(); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileAnnotationInfoExtractor.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileAnnotationInfoExtractor.java new file mode 100644 index 0000000..49e737d --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileAnnotationInfoExtractor.java @@ -0,0 +1,78 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.annotationdiscovery; + +import com.google.android.enterprise.connectedapps.processor.GeneratorUtilities; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileAnnotation; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileAnnotationInfo; +import com.google.common.collect.ImmutableSet; +import java.lang.annotation.Annotation; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +final class CrossProfileAnnotationInfoExtractor + extends AnnotationInfoExtractor<CrossProfileAnnotationInfo, CrossProfileAnnotation> { + + CrossProfileAnnotationInfoExtractor() { + super(CrossProfileAnnotation.class); + } + + @Override + protected Class<? extends Annotation> supportedAnnotationClass( + AnnotationClasses annotationClasses) { + return annotationClasses.crossProfileAnnotationClass(); + } + + @Override + protected CrossProfileAnnotationInfo annotationInfoFromAnnotation( + CrossProfileAnnotation annotation, Types types) { + CrossProfileAnnotationInfo.Builder builder = + CrossProfileAnnotationInfo.builder() + .setConnectorClass( + GeneratorUtilities.extractClassFromAnnotation(types, annotation::connector)) + .setProfileClassName(annotation.profileClassName()) + .setParcelableWrapperClasses( + ImmutableSet.copyOf( + GeneratorUtilities.extractClassesFromAnnotation( + types, annotation::parcelableWrappers))) + .setFutureWrapperClasses( + ImmutableSet.copyOf( + GeneratorUtilities.extractClassesFromAnnotation( + types, annotation::futureWrappers))) + .setIsStatic(annotation.isStatic()); + + long timeoutMillis = annotation.timeoutMillis(); + + if (timeoutMillis != CrossProfileAnnotation.TIMEOUT_MILLIS_NOT_SET) { + builder.setTimeoutMillis(timeoutMillis); + } + + return builder.build(); + } + + @Override + protected CrossProfileAnnotationInfo emptyAnnotationInfo(Elements elements) { + return CrossProfileAnnotationInfo.builder() + .setConnectorClass( + elements.getTypeElement( + "com.google.android.enterprise.connectedapps.annotations.CrossProfile")) + .setProfileClassName("") + .setParcelableWrapperClasses(ImmutableSet.of()) + .setFutureWrapperClasses(ImmutableSet.of()) + .setIsStatic(false) + .build(); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileCallbackAnnotationInfoExtractor.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileCallbackAnnotationInfoExtractor.java new file mode 100644 index 0000000..37016e8 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileCallbackAnnotationInfoExtractor.java @@ -0,0 +1,48 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.annotationdiscovery; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileCallbackAnnotation; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackAnnotationInfo; +import java.lang.annotation.Annotation; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +final class CrossProfileCallbackAnnotationInfoExtractor + extends AnnotationInfoExtractor< + CrossProfileCallbackAnnotationInfo, CrossProfileCallbackAnnotation> { + + CrossProfileCallbackAnnotationInfoExtractor() { + super(CrossProfileCallbackAnnotation.class); + } + + @Override + protected Class<? extends Annotation> supportedAnnotationClass( + AnnotationClasses annotationClasses) { + return annotationClasses.crossProfileCallbackAnnotationClass(); + } + + @Override + protected CrossProfileCallbackAnnotationInfo annotationInfoFromAnnotation( + CrossProfileCallbackAnnotation annotation, Types types) { + return CrossProfileCallbackAnnotationInfo.builder().setSimple(annotation.simple()).build(); + } + + @Override + protected CrossProfileCallbackAnnotationInfo emptyAnnotationInfo(Elements elements) { + return CrossProfileCallbackAnnotationInfo.builder().setSimple(false).build(); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileConfigurationAnnotationInfoExtractor.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileConfigurationAnnotationInfoExtractor.java new file mode 100644 index 0000000..3b76469 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileConfigurationAnnotationInfoExtractor.java @@ -0,0 +1,69 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.annotationdiscovery; + +import com.google.android.enterprise.connectedapps.processor.GeneratorUtilities; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileConfigurationAnnotation; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationAnnotationInfo; +import com.google.common.collect.ImmutableSet; +import java.lang.annotation.Annotation; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +final class CrossProfileConfigurationAnnotationInfoExtractor + extends AnnotationInfoExtractor< + CrossProfileConfigurationAnnotationInfo, CrossProfileConfigurationAnnotation> { + + CrossProfileConfigurationAnnotationInfoExtractor() { + super(CrossProfileConfigurationAnnotation.class); + } + + @Override + protected Class<? extends Annotation> supportedAnnotationClass( + AnnotationClasses annotationClasses) { + return annotationClasses.crossProfileConfigurationAnnotationClass(); + } + + @Override + protected CrossProfileConfigurationAnnotationInfo annotationInfoFromAnnotation( + CrossProfileConfigurationAnnotation annotation, Types types) { + return CrossProfileConfigurationAnnotationInfo.builder() + .setConnector(GeneratorUtilities.extractClassFromAnnotation(types, annotation::connector)) + .setProviderClasses( + ImmutableSet.copyOf( + GeneratorUtilities.extractClassesFromAnnotation(types, annotation::providers))) + .setServiceClass( + GeneratorUtilities.extractClassFromAnnotation(types, annotation::serviceClass)) + .setServiceSuperclass( + GeneratorUtilities.extractClassFromAnnotation(types, annotation::serviceSuperclass)) + .build(); + } + + @Override + protected CrossProfileConfigurationAnnotationInfo emptyAnnotationInfo(Elements elements) { + TypeElement crossProfileConfiguration = + elements.getTypeElement( + "com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration"); + + return CrossProfileConfigurationAnnotationInfo.builder() + .setConnector(crossProfileConfiguration) + .setProviderClasses(ImmutableSet.of()) + .setServiceClass(crossProfileConfiguration) + .setServiceSuperclass(crossProfileConfiguration) + .build(); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileConfigurationsAnnotationInfoExtractor.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileConfigurationsAnnotationInfoExtractor.java new file mode 100644 index 0000000..99eb04d --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileConfigurationsAnnotationInfoExtractor.java @@ -0,0 +1,69 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.annotationdiscovery; + +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static java.util.Arrays.stream; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileConfigurationAnnotation; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileConfigurationsAnnotation; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationAnnotationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationsAnnotationInfo; +import com.google.common.collect.ImmutableSet; +import java.lang.annotation.Annotation; +import java.util.Set; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +final class CrossProfileConfigurationsAnnotationInfoExtractor + extends AnnotationInfoExtractor< + CrossProfileConfigurationsAnnotationInfo, CrossProfileConfigurationsAnnotation> { + + CrossProfileConfigurationsAnnotationInfoExtractor() { + super(CrossProfileConfigurationsAnnotation.class); + } + + @Override + protected Class<? extends Annotation> supportedAnnotationClass( + AnnotationClasses annotationClasses) { + return annotationClasses.crossProfileConfigurationsAnnotationClass(); + } + + @Override + protected CrossProfileConfigurationsAnnotationInfo annotationInfoFromAnnotation( + CrossProfileConfigurationsAnnotation annotation, Types types) { + CrossProfileConfigurationAnnotationInfoExtractor innerExtractor = + new CrossProfileConfigurationAnnotationInfoExtractor(); + + Set<CrossProfileConfigurationAnnotationInfo> annotationInfos = + stream(annotation.value()) + .map( + configurationAnnotation -> + wrapAnnotationWithInterface( + CrossProfileConfigurationAnnotation.class, configurationAnnotation)) + .map( + configurationAnnotation -> + innerExtractor.annotationInfoFromAnnotation(configurationAnnotation, types)) + .collect(toImmutableSet()); + + return CrossProfileConfigurationsAnnotationInfo.create(ImmutableSet.copyOf(annotationInfos)); + } + + @Override + protected CrossProfileConfigurationsAnnotationInfo emptyAnnotationInfo(Elements elements) { + return CrossProfileConfigurationsAnnotationInfo.create(ImmutableSet.of()); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileProviderAnnotationInfoExtractor.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileProviderAnnotationInfoExtractor.java new file mode 100644 index 0000000..1fb79c4 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileProviderAnnotationInfoExtractor.java @@ -0,0 +1,52 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.annotationdiscovery; + +import com.google.android.enterprise.connectedapps.processor.GeneratorUtilities; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileProviderAnnotation; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileProviderAnnotationInfo; +import com.google.common.collect.ImmutableSet; +import java.lang.annotation.Annotation; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +final class CrossProfileProviderAnnotationInfoExtractor + extends AnnotationInfoExtractor< + CrossProfileProviderAnnotationInfo, CrossProfileProviderAnnotation> { + + CrossProfileProviderAnnotationInfoExtractor() { + super(CrossProfileProviderAnnotation.class); + } + + @Override + protected Class<? extends Annotation> supportedAnnotationClass( + AnnotationClasses annotationClasses) { + return annotationClasses.crossProfileProviderAnnotationClass(); + } + + @Override + protected CrossProfileProviderAnnotationInfo annotationInfoFromAnnotation( + CrossProfileProviderAnnotation annotation, Types types) { + return CrossProfileProviderAnnotationInfo.create( + ImmutableSet.copyOf( + GeneratorUtilities.extractClassesFromAnnotation(types, annotation::staticTypes))); + } + + @Override + protected CrossProfileProviderAnnotationInfo emptyAnnotationInfo(Elements elements) { + return CrossProfileProviderAnnotationInfo.create(ImmutableSet.of()); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileTestAnnotationInfoExtractor.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileTestAnnotationInfoExtractor.java new file mode 100644 index 0000000..4a119de --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileTestAnnotationInfoExtractor.java @@ -0,0 +1,51 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.annotationdiscovery; + +import com.google.android.enterprise.connectedapps.processor.GeneratorUtilities; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileTestAnnotation; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTestAnnotationInfo; +import java.lang.annotation.Annotation; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +final class CrossProfileTestAnnotationInfoExtractor + extends AnnotationInfoExtractor<CrossProfileTestAnnotationInfo, CrossProfileTestAnnotation> { + + CrossProfileTestAnnotationInfoExtractor() { + super(CrossProfileTestAnnotation.class); + } + + @Override + protected Class<? extends Annotation> supportedAnnotationClass( + AnnotationClasses annotationClasses) { + return annotationClasses.crossProfileTestAnnotationClass(); + } + + @Override + protected CrossProfileTestAnnotationInfo annotationInfoFromAnnotation( + CrossProfileTestAnnotation annotation, Types types) { + return CrossProfileTestAnnotationInfo.builder() + .setConfiguration( + GeneratorUtilities.extractClassFromAnnotation(types, annotation::configuration)) + .build(); + } + + @Override + protected CrossProfileTestAnnotationInfo emptyAnnotationInfo(Elements elements) { + throw new UnsupportedOperationException("Annotations of type CrossProfileTest cannot be empty"); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileAnnotation.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileAnnotation.java new file mode 100644 index 0000000..8e76d9c --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileAnnotation.java @@ -0,0 +1,36 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces; + +/** Elements that can be populated on annotations of type CrossProfile. */ +public interface CrossProfileAnnotation { + + long DEFAULT_TIMEOUT_MILLIS = 10000; + + long TIMEOUT_MILLIS_NOT_SET = -1; + + String profileClassName(); + + Class<?> connector(); + + Class<?>[] parcelableWrappers(); + + Class<?>[] futureWrappers(); + + boolean isStatic(); + + long timeoutMillis(); +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileCallbackAnnotation.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileCallbackAnnotation.java new file mode 100644 index 0000000..34e4619 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileCallbackAnnotation.java @@ -0,0 +1,22 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces; + +/* Elements that can be populated on annotations of type CrossProfileCallback. */ +public interface CrossProfileCallbackAnnotation { + + boolean simple(); +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileConfigurationAnnotation.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileConfigurationAnnotation.java new file mode 100644 index 0000000..94b5205 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileConfigurationAnnotation.java @@ -0,0 +1,28 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces; + +/** Elements that can be populated on annotations of type CrossProfileConfiguration. */ +public interface CrossProfileConfigurationAnnotation { + + Class<?>[] providers(); + + Class<?> serviceSuperclass(); + + Class<?> serviceClass(); + + Class<?> connector(); +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileConfigurationsAnnotation.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileConfigurationsAnnotation.java new file mode 100644 index 0000000..17acd78 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileConfigurationsAnnotation.java @@ -0,0 +1,24 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces; + +import java.lang.annotation.Annotation; + +/** Elements that can be populated on annotations of type CrossProfileConfigurations. */ +public interface CrossProfileConfigurationsAnnotation { + + Annotation[] value(); +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileProviderAnnotation.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileProviderAnnotation.java new file mode 100644 index 0000000..b478d36 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileProviderAnnotation.java @@ -0,0 +1,22 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces; + +/* Elements that can be populated on annotations of type CrossProfileProvider. */ +public interface CrossProfileProviderAnnotation { + + Class<?>[] staticTypes(); +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileTestAnnotation.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileTestAnnotation.java new file mode 100644 index 0000000..542748b --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileTestAnnotation.java @@ -0,0 +1,22 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces; + +/* Elements that can be populated on annotations of type CrossProfileTest. */ +public interface CrossProfileTestAnnotation { + + Class<?> configuration(); +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/Context.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/Context.java new file mode 100644 index 0000000..e3b17e7 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/Context.java @@ -0,0 +1,29 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.containers; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +/** A container for a validator or generator context. */ +public abstract class Context { + public abstract ProcessingEnvironment processingEnv(); + + public abstract Elements elements(); + + public abstract Types types(); +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileAnnotationInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileAnnotationInfo.java new file mode 100644 index 0000000..f083a69 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileAnnotationInfo.java @@ -0,0 +1,72 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.containers; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfile; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableCollection; +import java.util.Optional; +import javax.lang.model.element.TypeElement; + +/** Wrapper around information contained in an annotation of type {@link CrossProfile}. */ +@AutoValue +public abstract class CrossProfileAnnotationInfo { + + public static final String DEFAULT_CONNECTOR_NAME = + "com.google.android.enterprise.connectedapps.annotations.CrossProfile"; + + public abstract TypeElement connectorClass(); + + public abstract String profileClassName(); + + public abstract Optional<Long> timeoutMillis(); + + public abstract ImmutableCollection<TypeElement> parcelableWrapperClasses(); + + public abstract ImmutableCollection<TypeElement> futureWrapperClasses(); + + public abstract boolean isStatic(); + + public boolean connectorIsDefault() { + return connectorClass().asType().toString().equals(DEFAULT_CONNECTOR_NAME); + } + + public boolean isProfileClassNameDefault() { + return profileClassName().isEmpty(); + } + + public static Builder builder() { + return new AutoValue_CrossProfileAnnotationInfo.Builder(); + } + + @AutoValue.Builder + public abstract static class Builder { + + public abstract Builder setConnectorClass(TypeElement value); + + public abstract Builder setProfileClassName(String value); + + public abstract Builder setTimeoutMillis(Long value); + + public abstract Builder setParcelableWrapperClasses(ImmutableCollection<TypeElement> value); + + public abstract Builder setFutureWrapperClasses(ImmutableCollection<TypeElement> value); + + public abstract Builder setIsStatic(boolean value); + + public abstract CrossProfileAnnotationInfo build(); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileCallbackAnnotationInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileCallbackAnnotationInfo.java new file mode 100644 index 0000000..080b726 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileCallbackAnnotationInfo.java @@ -0,0 +1,38 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.containers; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileCallback; +import com.google.auto.value.AutoValue; + +/** Wrapper around information contained in an annotation of type {@link CrossProfileCallback}. */ +@AutoValue +public abstract class CrossProfileCallbackAnnotationInfo { + + public abstract boolean simple(); + + public static Builder builder() { + return new AutoValue_CrossProfileCallbackAnnotationInfo.Builder(); + } + + @AutoValue.Builder + public abstract static class Builder { + + public abstract Builder setSimple(boolean value); + + public abstract CrossProfileCallbackAnnotationInfo build(); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileCallbackInterfaceInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileCallbackInterfaceInfo.java new file mode 100644 index 0000000..ea6f068 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileCallbackInterfaceInfo.java @@ -0,0 +1,72 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.containers; + +import static java.util.stream.Collectors.toList; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileCallback; +import com.google.auto.value.AutoValue; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Name; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; + +/** Wrapper of a {@link CrossProfileCallback} annotated interface. */ +@AutoValue +public abstract class CrossProfileCallbackInterfaceInfo { + + public abstract TypeElement interfaceElement(); + + public Name simpleName() { + return interfaceElement().getSimpleName(); + } + + public boolean isSimple() { + List<ExecutableElement> methods = methods(); + return methods.size() == 1 && methods.get(0).getParameters().size() < 2; + } + + public List<ExecutableElement> methods() { + return interfaceElement().getEnclosedElements().stream() + .filter(e -> e instanceof ExecutableElement) + .map(e -> (ExecutableElement) e) + .filter(e -> e.getKind() == ElementKind.METHOD) + .sorted(Comparator.comparing(e -> e.getSimpleName().toString())) + .collect(toList()); + } + + public int getIdentifier(ExecutableElement method) { + return methods().indexOf(method); + } + + /** Get all types used by methods on this interface. */ + public Set<TypeMirror> argumentTypes() { + return methods().stream() + .flatMap(m -> m.getParameters().stream()) + .map(Element::asType) + .collect(Collectors.toSet()); + } + + public static CrossProfileCallbackInterfaceInfo create(TypeElement interfaceElement) { + return new AutoValue_CrossProfileCallbackInterfaceInfo(interfaceElement); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationAnnotationInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationAnnotationInfo.java new file mode 100644 index 0000000..a39c2e3 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationAnnotationInfo.java @@ -0,0 +1,54 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.containers; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableCollection; +import javax.lang.model.element.TypeElement; + +/** + * Wrapper around information contained in an annotation of type {@link CrossProfileConfiguration}. + */ +@AutoValue +public abstract class CrossProfileConfigurationAnnotationInfo { + + public abstract ImmutableCollection<TypeElement> providerClasses(); + + public abstract TypeElement serviceSuperclass(); + + public abstract TypeElement serviceClass(); + + public abstract TypeElement connector(); + + public static Builder builder() { + return new AutoValue_CrossProfileConfigurationAnnotationInfo.Builder(); + } + + @AutoValue.Builder + public abstract static class Builder { + + public abstract Builder setProviderClasses(ImmutableCollection<TypeElement> value); + + public abstract Builder setServiceSuperclass(TypeElement value); + + public abstract Builder setServiceClass(TypeElement value); + + public abstract Builder setConnector(TypeElement value); + + public abstract CrossProfileConfigurationAnnotationInfo build(); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationInfo.java new file mode 100644 index 0000000..2d4ed43 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationInfo.java @@ -0,0 +1,119 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.containers; + +import static java.util.stream.Collectors.toSet; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration; +import com.google.android.enterprise.connectedapps.processor.SupportedTypes; +import com.google.android.enterprise.connectedapps.processor.TypeUtils; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Streams; +import com.squareup.javapoet.ClassName; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Optional; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; + +/** Wrapper of a {@link CrossProfileConfiguration} annotated class. */ +@AutoValue +public abstract class CrossProfileConfigurationInfo { + + public static final String CROSS_PROFILE_CONNECTOR_QUALIFIED_NAME = + "com.google.android.enterprise.connectedapps.CrossProfileConnector"; + + public abstract TypeElement configurationElement(); + + public abstract ImmutableCollection<ProviderClassInfo> providers(); + + public abstract ClassName serviceSuperclass(); + + public abstract Optional<TypeElement> serviceClass(); + + public String simpleName() { + return configurationElement().getSimpleName().toString(); + } + + public ClassName className() { + return ClassName.get(configurationElement()); + } + + public abstract ProfileConnectorInfo profileConnector(); + + public static CrossProfileConfigurationInfo create( + ValidatorContext context, ValidatorCrossProfileConfigurationInfo configuration) { + Collection<ProviderClassInfo> providerClasses = + configuration.providerClassElements().stream() + .map( + m -> + ProviderClassInfo.create( + context, ValidatorProviderClassInfo.create(context.processingEnv(), m))) + .collect(toSet()); + + ProfileConnectorInfo profileConnectorInfo = + providerClasses.stream() + .flatMap(m -> m.allCrossProfileTypes().stream()) + .map(CrossProfileTypeInfo::profileConnector) + .flatMap(Streams::stream) + .findFirst() + .orElseGet( + () -> + ProfileConnectorInfo.create( + context.processingEnv(), + getConfiguredConnectorOrDefault(context, configuration), + context.globalSupportedTypes())); + + return new AutoValue_CrossProfileConfigurationInfo( + configuration.configurationElement(), + ImmutableSet.copyOf(providerClasses), + configuration.serviceSuperclass(), + configuration.serviceClass(), + profileConnectorInfo); + } + + private static TypeElement getConfiguredConnectorOrDefault( + ValidatorContext context, ValidatorCrossProfileConfigurationInfo configuration) { + return configuration + .connector() + .orElseGet(() -> context.elements().getTypeElement(CROSS_PROFILE_CONNECTOR_QUALIFIED_NAME)); + } + + private static Collection<Type> convertTypeMirrorToSupportedTypes( + SupportedTypes supportedTypes, TypeMirror typeMirror) { + if (TypeUtils.isGeneric(typeMirror)) { + return convertGenericTypeMirrorToSupportedTypes(supportedTypes, typeMirror); + } + return Collections.singleton(supportedTypes.getType(typeMirror)); + } + + private static Collection<Type> convertGenericTypeMirrorToSupportedTypes( + SupportedTypes supportedTypes, TypeMirror typeMirror) { + Collection<Type> types = new HashSet<>(); + TypeMirror genericType = TypeUtils.removeTypeArguments(typeMirror); + Type supportedType = supportedTypes.getType(genericType); + if (!supportedType.isSupportedWithAnyGenericType()) { + for (TypeMirror typeArgument : TypeUtils.extractTypeArguments(typeMirror)) { + types.addAll(convertTypeMirrorToSupportedTypes(supportedTypes, typeArgument)); + } + } + types.add(supportedTypes.getType(genericType)); + return types; + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationsAnnotationInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationsAnnotationInfo.java new file mode 100644 index 0000000..1060d68 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationsAnnotationInfo.java @@ -0,0 +1,34 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.containers; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfigurations; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableSet; + +/** + * Wrapper around information contained in an annotation of type {@link CrossProfileConfigurations}. + */ +@AutoValue +public abstract class CrossProfileConfigurationsAnnotationInfo { + + public abstract ImmutableSet<CrossProfileConfigurationAnnotationInfo> configurations(); + + public static CrossProfileConfigurationsAnnotationInfo create( + ImmutableSet<CrossProfileConfigurationAnnotationInfo> configurations) { + return new AutoValue_CrossProfileConfigurationsAnnotationInfo(configurations); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileMethodInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileMethodInfo.java new file mode 100644 index 0000000..72831a9 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileMethodInfo.java @@ -0,0 +1,231 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.containers; + +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.hasCrossProfileAnnotation; +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.hasCrossProfileCallbackAnnotation; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toSet; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfile; +import com.google.android.enterprise.connectedapps.annotations.CrossProfileCallback; +import com.google.android.enterprise.connectedapps.processor.SupportedTypes; +import com.google.android.enterprise.connectedapps.processor.TypeUtils; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileAnnotation; +import com.google.auto.value.AutoValue; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.TypeName; +import java.util.Collection; +import java.util.Optional; +import java.util.function.Function; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Elements; + +/** Wrapper of a {@link CrossProfile} annotated method. */ +@AutoValue +public abstract class CrossProfileMethodInfo { + + public abstract ExecutableElement methodElement(); + + public abstract int identifier(); + + public abstract boolean isStatic(); + + public String simpleName() { + return methodElement().getSimpleName().toString(); + } + + public TypeMirror returnType() { + return methodElement().getReturnType(); + } + + public TypeName returnTypeTypeName() { + return ClassName.get(returnType()); + } + + public Collection<TypeName> thrownExceptions() { + return methodElement().getThrownTypes().stream() + .map(ClassName::get) + .collect(toSet()); + } + + public Collection<TypeMirror> automaticallyResolvedParameterTypes(SupportedTypes supportedTypes) { + return parameterTypes().stream() + .filter(supportedTypes::isAutomaticallyResolved) + .collect(toSet()); + } + + /** + * The number of milliseconds to timeout async calls. This is either set on the method, the type, + * or defaults to {@link CrossProfileAnnotation#DEFAULT_TIMEOUT_MILLIS}. + */ + public abstract long timeoutMillis(); + + /** + * Specify behaviour when encountering parameters of a type which is automatically resolved by the + * SDK. + */ + public enum AutomaticallyResolvedParameterFilterBehaviour { + /** Do not change the parameters. */ + LEAVE_AUTOMATICALLY_RESOLVED_PARAMETERS, + /** Remove the parameters and act as if they are not present. */ + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS, + /** Replace the parameter with the variable specified in the type configuration. */ + REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS + } + + /** + * A string of parameter names separated by commas. + * + * <p>This is useful when generating a call for this method, using the same parameter names. + * + * <p>Parameters which are automatically resolved will be removed. + */ + public String commaSeparatedParameters( + SupportedTypes supportedTypes, + AutomaticallyResolvedParameterFilterBehaviour filterBehaviour) { + return commaSeparatedParameters(supportedTypes, filterBehaviour, Function.identity()); + } + + /** + * A string of parameter names separated by commas. + * + * <p>This is useful when generating a call for this method, using the same parameter names. + * + * <p>Parameters which are automatically resolved will be removed. + */ + public String commaSeparatedParameters( + SupportedTypes supportedTypes, + AutomaticallyResolvedParameterFilterBehaviour filterBehaviour, + Function<String, String> map) { + if (filterBehaviour + == AutomaticallyResolvedParameterFilterBehaviour.REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS) { + return methodElement().getParameters().stream() + .filter(p -> !supportedTypes.isAutomaticallyResolved(p.asType())) + .map(p -> p.getSimpleName().toString()) + .map(map) + .collect(joining(", ")); + } else if (filterBehaviour + == AutomaticallyResolvedParameterFilterBehaviour + .REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS) { + return methodElement().getParameters().stream() + .map( + p -> + supportedTypes.isAutomaticallyResolved(p.asType()) + ? supportedTypes.getAutomaticallyResolvedReplacement(p.asType()) + : p.getSimpleName().toString()) + .map(map) + .collect(joining(", ")); + } else if (filterBehaviour + == AutomaticallyResolvedParameterFilterBehaviour.LEAVE_AUTOMATICALLY_RESOLVED_PARAMETERS) { + return methodElement().getParameters().stream() + .map(p -> p.getSimpleName().toString()) + .map(map) + .collect(joining(", ")); + } + throw new IllegalArgumentException("Invalid filter behaviour: " + filterBehaviour); + } + + /** An unordered collection of the types used in the parameters of this method. */ + public Collection<TypeMirror> parameterTypes() { + return methodElement().getParameters().stream().map(Element::asType).collect(toSet()); + } + + /** + * True if both {@link #isCrossProfileCallback(GeneratorContext)} and {@link + * #isFuture(CrossProfileTypeInfo)} are {@code False}. + */ + public boolean isBlocking(GeneratorContext context, CrossProfileTypeInfo type) { + return !isCrossProfileCallback(context) && !isFuture(type); + } + + /** True if any argument is annotated with {@link CrossProfileCallback}. */ + public boolean isCrossProfileCallback(GeneratorContext generatorContext) { + return getCrossProfileCallbackParam(generatorContext).isPresent(); + } + + /** True if there is only a single {@link CrossProfileCallback} argument and it is simple. */ + public boolean isSimpleCrossProfileCallback(GeneratorContext generatorContext) { + Optional<VariableElement> param = getCrossProfileCallbackParam(generatorContext); + + if (param.isPresent()) { + CrossProfileCallbackInterfaceInfo callbackInterface = + CrossProfileCallbackInterfaceInfo.create( + (TypeElement) generatorContext.types().asElement(param.get().asType())); + return callbackInterface.isSimple(); + } + + return false; + } + + /** True if the return type is a supported {@code Future} type. */ + public boolean isFuture(CrossProfileTypeInfo type) { + return isFuture(type.supportedTypes(), methodElement()); + } + + public static boolean isFuture(SupportedTypes supportedTypes, ExecutableElement method) { + return supportedTypes.isFuture(TypeUtils.removeTypeArguments(method.getReturnType())); + } + + /** Return the {@link CrossProfileCallback} annotated parameter, if any. */ + public Optional<VariableElement> getCrossProfileCallbackParam(GeneratorContext generatorContext) { + return getCrossProfileCallbackParam(generatorContext.elements(), methodElement()); + } + + public static Optional<VariableElement> getCrossProfileCallbackParam( + Elements elements, ExecutableElement method) { + return method.getParameters().stream() + .filter(v -> isCrossProfileCallbackInterface(elements, v.asType())) + .findFirst() + .map(e -> (VariableElement) e); + } + + private static boolean isCrossProfileCallbackInterface(Elements elements, TypeMirror type) { + TypeElement typeElement = elements.getTypeElement(type.toString()); + return typeElement != null && hasCrossProfileCallbackAnnotation(typeElement); + } + + public static CrossProfileMethodInfo create( + int identifier, + ValidatorCrossProfileTypeInfo type, + ExecutableElement methodElement, + Context context) { + return new AutoValue_CrossProfileMethodInfo( + methodElement, + identifier, + methodElement.getModifiers().contains(Modifier.STATIC), + findTimeoutMillis(type, methodElement, context)); + } + + private static long findTimeoutMillis( + ValidatorCrossProfileTypeInfo type, ExecutableElement methodElement, Context context) { + if (hasCrossProfileAnnotation(methodElement)) { + return AnnotationFinder.extractCrossProfileAnnotationInfo( + methodElement, context.types(), context.elements()) + .timeoutMillis() + .filter(timeout -> timeout > 0) + .orElse(type.timeoutMillis()); + } + + return type.timeoutMillis(); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileProviderAnnotationInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileProviderAnnotationInfo.java new file mode 100644 index 0000000..ab05fff --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileProviderAnnotationInfo.java @@ -0,0 +1,33 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.containers; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileProvider; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableCollection; +import javax.lang.model.element.TypeElement; + +/** Wrapper around information contained in an annotation of type {@link CrossProfileProvider}. */ +@AutoValue +public abstract class CrossProfileProviderAnnotationInfo { + + public abstract ImmutableCollection<TypeElement> staticTypes(); + + public static CrossProfileProviderAnnotationInfo create( + ImmutableCollection<TypeElement> staticTypes) { + return new AutoValue_CrossProfileProviderAnnotationInfo(staticTypes); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTestAnnotationInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTestAnnotationInfo.java new file mode 100644 index 0000000..f96a153 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTestAnnotationInfo.java @@ -0,0 +1,39 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.containers; + +import com.google.android.enterprise.connectedapps.testing.annotations.CrossProfileTest; +import com.google.auto.value.AutoValue; +import javax.lang.model.element.TypeElement; + +/** Wrapper around information contained in an annotation of type {@link CrossProfileTest}. */ +@AutoValue +public abstract class CrossProfileTestAnnotationInfo { + + public abstract TypeElement configuration(); + + public static Builder builder() { + return new AutoValue_CrossProfileTestAnnotationInfo.Builder(); + } + + @AutoValue.Builder + public abstract static class Builder { + + public abstract Builder setConfiguration(TypeElement value); + + public abstract CrossProfileTestAnnotationInfo build(); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTestInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTestInfo.java new file mode 100644 index 0000000..a2219be --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTestInfo.java @@ -0,0 +1,47 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.containers; + +import static java.util.stream.Collectors.toSet; + +import com.google.android.enterprise.connectedapps.testing.annotations.CrossProfileTest; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableSet; +import java.util.Set; +import javax.lang.model.element.TypeElement; + +/** Wrapper of a {@link CrossProfileTest} annotated class. */ +@AutoValue +public abstract class CrossProfileTestInfo { + + public abstract TypeElement crossProfileTestElement(); + + public abstract ImmutableSet<CrossProfileConfigurationInfo> configurations(); + + public static CrossProfileTestInfo create( + ValidatorContext context, ValidatorCrossProfileTestInfo validatorCrossProfileTest) { + + Set<CrossProfileConfigurationInfo> configurations = + ValidatorCrossProfileConfigurationInfo.createMultipleFromElement( + context.processingEnv(), validatorCrossProfileTest.configurationElement()) + .stream() + .map(b -> CrossProfileConfigurationInfo.create(context, b)) + .collect(toSet()); + + return new AutoValue_CrossProfileTestInfo( + validatorCrossProfileTest.crossProfileTestElement(), ImmutableSet.copyOf(configurations)); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTypeInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTypeInfo.java new file mode 100644 index 0000000..fffe4a1 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTypeInfo.java @@ -0,0 +1,171 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.containers; + +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.hasCrossProfileAnnotation; +import static java.util.stream.Collectors.toSet; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfile; +import com.google.android.enterprise.connectedapps.processor.ProcessorConfiguration; +import com.google.android.enterprise.connectedapps.processor.SupportedTypes; +import com.google.android.enterprise.connectedapps.processor.TypeUtils; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileAnnotation; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableSet; +import com.google.common.hash.Hashing; +import com.squareup.javapoet.ClassName; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.stream.IntStream; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; + +/** Wrapper of a {@link CrossProfile} type. */ +@AutoValue +public abstract class CrossProfileTypeInfo { + + public abstract TypeElement crossProfileTypeElement(); + + public abstract ImmutableCollection<CrossProfileMethodInfo> crossProfileMethods(); + + public abstract SupportedTypes supportedTypes(); + + public abstract Optional<ProfileConnectorInfo> profileConnector(); + + public abstract ClassName profileClassName(); + + /** + * The specified timeout for async calls, or {@link CrossProfileAnnotation#DEFAULT_TIMEOUT_MILLIS} + * if unspecified. + */ + public abstract long timeoutMillis(); + + public String simpleName() { + return crossProfileTypeElement().getSimpleName().toString(); + } + + public ClassName className() { + return ClassName.get(crossProfileTypeElement()); + } + + public boolean isStatic() { + return crossProfileMethods().stream().allMatch(CrossProfileMethodInfo::isStatic); + } + + /** + * Get a numeric identifier for the cross-profile type. + * + * <p>This identifier is based on the type's qualified name, and will not change between runs. + */ + public long identifier() { + // Stored in a 64 bit long, with ~200 cross-profile types, chance of collision is 1 in 10^15 + return Hashing.murmur3_128() + .hashString(crossProfileTypeElement().getQualifiedName().toString(), StandardCharsets.UTF_8) + .asLong(); + } + + public static CrossProfileTypeInfo create( + ValidatorContext context, ValidatorCrossProfileTypeInfo crossProfileType) { + TypeElement crossProfileTypeElement = crossProfileType.crossProfileTypeElement(); + + List<ExecutableElement> crossProfileMethodElements = crossProfileType.crossProfileMethods(); + + Collection<CrossProfileMethodInfo> crossProfileMethods = + IntStream.range(0, crossProfileMethodElements.size()) + .mapToObj( + t -> + CrossProfileMethodInfo.create( + t, crossProfileType, crossProfileMethodElements.get(t), context)) + .collect(toSet()); + + SupportedTypes.Builder supportedTypesBuilder = crossProfileType.supportedTypes().asBuilder(); + + supportedTypesBuilder.filterUsed(context, crossProfileMethods); + + if (ProcessorConfiguration.GENERATE_TYPE_SPECIFIC_WRAPPERS) { + supportedTypesBuilder.replaceWrapperPrefix( + ClassName.bestGuess( + crossProfileType.crossProfileTypeElement().getQualifiedName().toString())); + } + + return new AutoValue_CrossProfileTypeInfo( + crossProfileTypeElement, + ImmutableSet.copyOf(crossProfileMethods), + supportedTypesBuilder.build(), + crossProfileType.profileConnector(), + findProfileClassName(context, crossProfileTypeElement, crossProfileType), + crossProfileType.timeoutMillis()); + } + + private static ClassName findProfileClassName( + ValidatorContext context, + TypeElement typeElement, + ValidatorCrossProfileTypeInfo crossProfileType) { + return hasCrossProfileAnnotation(typeElement) + ? findAnnotatedProfileClassName(context, typeElement, crossProfileType) + : createDefaultProfileClassName(context, typeElement); + } + + private static ClassName createDefaultProfileClassName( + ValidatorContext context, TypeElement typeElement) { + PackageElement originalPackage = context.elements().getPackageOf(typeElement); + String profileAwareClassName = + String.format("Profile%s", typeElement.getSimpleName().toString()); + + return ClassName.get(originalPackage.getQualifiedName().toString(), profileAwareClassName); + } + + private static ClassName findAnnotatedProfileClassName( + ValidatorContext context, + TypeElement typeElement, + ValidatorCrossProfileTypeInfo crossProfileType) { + String profileClassName = crossProfileType.profileClassName(); + if (!profileClassName.isEmpty()) { + return ClassName.bestGuess(profileClassName); + } + + return createDefaultProfileClassName(context, typeElement); + } + + private static Collection<Type> convertTypeMirrorToSupportedTypes( + SupportedTypes supportedTypes, TypeMirror typeMirror) { + if (TypeUtils.isGeneric(typeMirror)) { + return convertGenericTypeMirrorToSupportedTypes(supportedTypes, typeMirror); + } + return Collections.singleton(supportedTypes.getType(typeMirror)); + } + + private static Collection<Type> convertGenericTypeMirrorToSupportedTypes( + SupportedTypes supportedTypes, TypeMirror typeMirror) { + Collection<Type> types = new HashSet<>(); + TypeMirror genericType = TypeUtils.removeTypeArguments(typeMirror); + Type supportedType = supportedTypes.getType(genericType); + if (!supportedType.isSupportedWithAnyGenericType()) { + for (TypeMirror typeArgument : TypeUtils.extractTypeArguments(typeMirror)) { + types.addAll(convertTypeMirrorToSupportedTypes(supportedTypes, typeArgument)); + } + } + types.add(supportedTypes.getType(genericType)); + return types; + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/FutureWrapper.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/FutureWrapper.java new file mode 100644 index 0000000..5208e7c --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/FutureWrapper.java @@ -0,0 +1,139 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.containers; + +import com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper; +import com.google.auto.value.AutoValue; +import com.squareup.javapoet.ClassName; +import java.util.ArrayList; +import java.util.Collection; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +/** Information about future wrapper. */ +@AutoValue +public abstract class FutureWrapper { + + /** The type of the Wrapper. This controls how supporting code is generated. */ + public enum WrapperType { + DEFAULT, // Copied from a resource + CUSTOM // Included in classpath + } + + public static final String FUTURE_WRAPPER_PACKAGE = + "com.google.android.enterprise.connectedapps.futurewrappers"; + + public abstract TypeMirror wrappedType(); + + public abstract ClassName defaultWrapperClassName(); + + public abstract ClassName wrapperClassName(); + + public abstract WrapperType wrapperType(); + + private static FutureWrapper create( + TypeMirror wrappedType, ClassName defaultWrapperClassName, WrapperType wrapperType) { + return create(wrappedType, defaultWrapperClassName, defaultWrapperClassName, wrapperType); + } + + public static FutureWrapper create( + TypeMirror wrappedType, + ClassName defaultWrapperClassName, + ClassName wrapperClassName, + WrapperType wrapperType) { + return new AutoValue_FutureWrapper( + wrappedType, defaultWrapperClassName, wrapperClassName, wrapperType); + } + + public static Collection<FutureWrapper> createGlobalFutureWrappers(Elements elements) { + Collection<FutureWrapper> wrappers = new ArrayList<>(); + + addDefaultFutureWrappers(elements, wrappers); + + return wrappers; + } + + private static void addDefaultFutureWrappers( + Elements elements, Collection<FutureWrapper> wrappers) { + tryAddWrapper( + elements, + wrappers, + "com.google.common.util.concurrent.ListenableFuture", + ClassName.get(FUTURE_WRAPPER_PACKAGE, "ListenableFutureWrapper"), + WrapperType.DEFAULT); + } + + public static Collection<FutureWrapper> createCustomFutureWrappers( + Types types, Elements elements, Collection<TypeElement> customFutureWrappers) { + Collection<FutureWrapper> wrappers = new ArrayList<>(); + + addCustomFutureWrappers(types, elements, wrappers, customFutureWrappers); + + return wrappers; + } + + private static void addCustomFutureWrappers( + Types types, + Elements elements, + Collection<FutureWrapper> wrappers, + Collection<TypeElement> customFutureWrappers) { + for (TypeElement customFutureWrapper : customFutureWrappers) { + addCustomFutureWrapper(types, elements, wrappers, customFutureWrapper); + } + } + + private static void addCustomFutureWrapper( + Types types, + Elements elements, + Collection<FutureWrapper> wrappers, + TypeElement customFutureWrapper) { + CustomFutureWrapper customFutureWrapperAnnotation = + customFutureWrapper.getAnnotation(CustomFutureWrapper.class); + + if (customFutureWrapperAnnotation == null) { + // This will be dealt with as part of early validation + return; + } + + tryAddWrapper( + elements, + wrappers, + FutureWrapperAnnotationInfo.extractFromFutureWrapperAnnotation( + types, customFutureWrapperAnnotation) + .originalType() + .toString(), + ClassName.get(customFutureWrapper), + WrapperType.CUSTOM); + } + + private static void tryAddWrapper( + Elements elements, + Collection<FutureWrapper> wrappers, + String typeQualifiedName, + ClassName wrapperClassName, + WrapperType wrapperType) { + TypeElement typeElement = elements.getTypeElement(typeQualifiedName); + + if (typeElement == null) { + // The type isn't supported at compile-time - so won't be included in this app + return; + } + + wrappers.add(FutureWrapper.create(typeElement.asType(), wrapperClassName, wrapperType)); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/FutureWrapperAnnotationInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/FutureWrapperAnnotationInfo.java new file mode 100644 index 0000000..18fb34b --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/FutureWrapperAnnotationInfo.java @@ -0,0 +1,45 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.containers; + +import com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper; +import com.google.android.enterprise.connectedapps.processor.GeneratorUtilities; +import com.google.auto.value.AutoValue; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.Types; + +/** + * Wrapper around information contained in a {@link + * com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper} annotation. + */ +@AutoValue +public abstract class FutureWrapperAnnotationInfo { + + public abstract TypeElement originalType(); + + public static FutureWrapperAnnotationInfo extractFromFutureWrapperAnnotation( + Types types, CustomFutureWrapper customFutureWrapperAnnotation) { + if (customFutureWrapperAnnotation == null) { + throw new NullPointerException("customFutureWrapperAnnotation must not be null"); + } + + TypeElement originalType = + GeneratorUtilities.extractClassFromAnnotation( + types, customFutureWrapperAnnotation::originalType); + + return new AutoValue_FutureWrapperAnnotationInfo(originalType); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/GeneratorContext.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/GeneratorContext.java new file mode 100644 index 0000000..ac47218 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/GeneratorContext.java @@ -0,0 +1,142 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.containers; + +import static java.util.stream.Collectors.toSet; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableSet; +import java.util.Collection; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +/** Context for connected apps code generators. */ +@AutoValue +public abstract class GeneratorContext extends Context { + + public static GeneratorContext createFromValidatorContext(ValidatorContext validatorContext) { + Collection<CrossProfileConfigurationInfo> configurations = + validatorContext.newConfigurations().stream() + .map(a -> CrossProfileConfigurationInfo.create(validatorContext, a)) + .collect(toSet()); + + Collection<ProviderClassInfo> providers = + validatorContext.newProviderClasses().stream() + .map(m -> ProviderClassInfo.create(validatorContext, m)) + .collect(toSet()); + + Collection<CrossProfileTypeInfo> crossProfileTypes = + validatorContext.newCrossProfileTypes().stream() + .map(m -> CrossProfileTypeInfo.create(validatorContext, m)) + .collect(toSet()); + + Collection<CrossProfileCallbackInterfaceInfo> crossProfileCallbackInterfaces = + validatorContext.newCrossProfileCallbackInterfaces().stream() + .map(CrossProfileCallbackInterfaceInfo::create) + .collect(toSet()); + + Collection<ProfileConnectorInfo> generatedProfileConnectors = + validatorContext.newGeneratedProfileConnectors().stream() + .map( + t -> + ProfileConnectorInfo.create( + validatorContext.processingEnv(), + t, + validatorContext.globalSupportedTypes())) + .collect(toSet()); + + Collection<UserConnectorInfo> generatedUserConnectors = + validatorContext.newGeneratedUserConnectors().stream() + .map( + t -> + UserConnectorInfo.create( + validatorContext.processingEnv(), + t, + validatorContext.globalSupportedTypes())) + .collect(toSet()); + + Collection<CrossProfileTestInfo> crossProfileTests = + validatorContext.newCrossProfileTests().stream() + .map(t -> CrossProfileTestInfo.create(validatorContext, t)) + .collect(toSet()); + + return GeneratorContext.builder() + .setProcessingEnv(validatorContext.processingEnv()) + .setElements(validatorContext.elements()) + .setTypes(validatorContext.types()) + .setConfigurations(configurations) + .setGeneratedProfileConnectors(generatedProfileConnectors) + .setGeneratedUserConnectors(generatedUserConnectors) + .setProviders(providers) + .setCrossProfileTypes(crossProfileTypes) + .setCrossProfileMethods(validatorContext.newCrossProfileMethods()) + .setCrossProfileCallbackInterfaces(crossProfileCallbackInterfaces) + .setCrossProfileTests(crossProfileTests) + .build(); + } + + static Builder builder() { + return new AutoValue_GeneratorContext.Builder(); + } + + public abstract ImmutableSet<CrossProfileConfigurationInfo> configurations(); + + public abstract ImmutableSet<ProfileConnectorInfo> generatedProfileConnectors(); + + public abstract ImmutableSet<UserConnectorInfo> generatedUserConnectors(); + + public abstract ImmutableSet<ProviderClassInfo> providers(); + + public abstract ImmutableSet<CrossProfileTypeInfo> crossProfileTypes(); + + public abstract ImmutableSet<ExecutableElement> crossProfileMethods(); + + public abstract ImmutableSet<CrossProfileCallbackInterfaceInfo> crossProfileCallbackInterfaces(); + + public abstract ImmutableSet<CrossProfileTestInfo> crossProfileTests(); + + @AutoValue.Builder + abstract static class Builder { + abstract Builder setProcessingEnv(ProcessingEnvironment processingEnv); + + abstract Builder setElements(Elements elements); + + abstract Builder setTypes(Types types); + + abstract Builder setConfigurations(Collection<CrossProfileConfigurationInfo> configurations); + + abstract Builder setGeneratedProfileConnectors( + Collection<ProfileConnectorInfo> generatedProfileConnectors); + + abstract Builder setGeneratedUserConnectors( + Collection<UserConnectorInfo> generatedUserConnectors); + + abstract Builder setProviders(Collection<ProviderClassInfo> providers); + + abstract Builder setCrossProfileTypes(Collection<CrossProfileTypeInfo> crossProfileTypes); + + abstract Builder setCrossProfileMethods(Collection<ExecutableElement> crossProfileMethods); + + abstract Builder setCrossProfileCallbackInterfaces( + Collection<CrossProfileCallbackInterfaceInfo> crossProfileCallbackInterfaces); + + abstract Builder setCrossProfileTests(Collection<CrossProfileTestInfo> crossProfileTests); + + abstract GeneratorContext build(); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ParcelableWrapper.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ParcelableWrapper.java new file mode 100644 index 0000000..114abfb --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ParcelableWrapper.java @@ -0,0 +1,274 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.containers; + +import static com.google.android.enterprise.connectedapps.processor.ProtoParcelableWrapperGenerator.getGeneratedProtoWrapperClassName; +import static java.util.stream.Collectors.toSet; + +import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper; +import com.google.android.enterprise.connectedapps.processor.TypeUtils; +import com.google.auto.value.AutoValue; +import com.squareup.javapoet.ClassName; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +/** Information about a Parcelable Wrapper. */ +@AutoValue +public abstract class ParcelableWrapper { + + /** The type of the Wrapper. This controls how supporting code is generated. */ + public enum WrapperType { + DEFAULT, // Copied from a resource + PROTO, // Generated by ProtoParcelableWrapperGenerator + CUSTOM // Included in classpath + } + + public static final String PARCELABLE_WRAPPER_PACKAGE = + "com.google.android.enterprise.connectedapps.parcelablewrappers"; + + public abstract TypeMirror wrappedType(); + + public abstract ClassName defaultWrapperClassName(); + + public abstract ClassName wrapperClassName(); + + public abstract WrapperType wrapperType(); + + private static ParcelableWrapper create( + TypeMirror wrappedType, ClassName defaultWrapperClassName, WrapperType wrapperType) { + return create(wrappedType, defaultWrapperClassName, defaultWrapperClassName, wrapperType); + } + + public static ParcelableWrapper create( + TypeMirror wrappedType, + ClassName defaultWrapperClassName, + ClassName wrapperClassName, + WrapperType wrapperType) { + return new AutoValue_ParcelableWrapper( + wrappedType, defaultWrapperClassName, wrapperClassName, wrapperType); + } + + public static Collection<ParcelableWrapper> createCustomParcelableWrappers( + Types types, Elements elements, Collection<TypeElement> customParcelableWrappers) { + Collection<ParcelableWrapper> wrappers = new ArrayList<>(); + + addCustomParcelableWrappers(types, wrappers, customParcelableWrappers); + + return wrappers; + } + + public static Collection<ParcelableWrapper> createGlobalParcelableWrappers( + Types types, Elements elements, Collection<ExecutableElement> methods) { + Collection<ParcelableWrapper> wrappers = new ArrayList<>(); + + addDefaultParcelableWrappers(types, elements, wrappers); + + Collection<TypeMirror> usedTypes = extractTypesFromMethods(methods); + + addGeneratedProtoParcelableWrappers(types, elements, wrappers, usedTypes); + + return wrappers; + } + + private static Collection<TypeMirror> extractTypesFromMethods( + Collection<ExecutableElement> methods) { + return methods.stream() + .flatMap(m -> extractReturnTypeAndParameters(m).stream()) + .flatMap(t -> extractTypeArgumentsIfWrapped(t).stream()) + .collect(toSet()); + } + + private static Collection<TypeMirror> extractReturnTypeAndParameters(ExecutableElement method) { + Collection<TypeMirror> types = new HashSet<>(); + types.add(method.getReturnType()); + types.addAll(method.getParameters().stream().map(Element::asType).collect(toSet())); + return types; + } + + private static Collection<TypeMirror> extractTypeArgumentsIfWrapped(TypeMirror type) { + if (TypeUtils.isGeneric(type)) { + return extractTypeArgumentsFromGeneric(type); + } + if (TypeUtils.isArray(type)) { + return extractTypeArgumentsIfWrapped(TypeUtils.extractTypeFromArray(type)); + } + + return Collections.singleton(type); + } + + private static Collection<TypeMirror> extractTypeArgumentsFromGeneric(TypeMirror type) { + Collection<TypeMirror> types = new HashSet<>(); + types.add(TypeUtils.removeTypeArguments(type)); + + types.addAll( + TypeUtils.extractTypeArguments(type).stream() + .flatMap(t -> extractTypeArgumentsIfWrapped(t).stream()) + .collect(toSet())); + return types; + } + + private static void addCustomParcelableWrappers( + Types types, + Collection<ParcelableWrapper> wrappers, + Collection<TypeElement> customParcelableWrappers) { + for (TypeElement parcelableWrapper : customParcelableWrappers) { + addCustomParcelableWrapper(types, wrappers, parcelableWrapper); + } + } + + private static void addCustomParcelableWrapper( + Types types, Collection<ParcelableWrapper> wrappers, TypeElement parcelableWrapper) { + + CustomParcelableWrapper customParcelableWrapperAnnotation = + parcelableWrapper.getAnnotation(CustomParcelableWrapper.class); + + if (customParcelableWrapperAnnotation == null) { + // This will be dealt with as part of early validation + return; + } + + ParcelableWrapperAnnotationInfo annotationInfo = + ParcelableWrapperAnnotationInfo.extractFromParcelableWrapperAnnotation( + types, customParcelableWrapperAnnotation); + wrappers.add( + ParcelableWrapper.create( + annotationInfo.originalType().asType(), + ClassName.get(parcelableWrapper), + WrapperType.CUSTOM)); + } + + private static void addDefaultParcelableWrappers( + Types types, Elements elements, Collection<ParcelableWrapper> wrappers) { + tryAddWrapper( + elements, + wrappers, + "java.util.Collection", + ClassName.get(PARCELABLE_WRAPPER_PACKAGE, "ParcelableCollection")); + + tryAddWrapper( + elements, + wrappers, + "java.util.List", + ClassName.get(PARCELABLE_WRAPPER_PACKAGE, "ParcelableList")); + + tryAddWrapper( + elements, + wrappers, + "java.util.Map", + ClassName.get(PARCELABLE_WRAPPER_PACKAGE, "ParcelableMap")); + + tryAddWrapper( + elements, + wrappers, + "java.util.Set", + ClassName.get(PARCELABLE_WRAPPER_PACKAGE, "ParcelableSet")); + + tryAddWrapper( + elements, + wrappers, + "java.util.Optional", + ClassName.get(PARCELABLE_WRAPPER_PACKAGE, "ParcelableOptional")); + + tryAddWrapper( + elements, + wrappers, + "com.google.common.base.Optional", + ClassName.get(PARCELABLE_WRAPPER_PACKAGE, "ParcelableGuavaOptional")); + + tryAddWrapper( + elements, + wrappers, + "com.google.common.collect.ImmutableMap", + ClassName.get(PARCELABLE_WRAPPER_PACKAGE, "ParcelableImmutableMap")); + + tryAddWrapper( + elements, + wrappers, + "android.util.Pair", + ClassName.get(PARCELABLE_WRAPPER_PACKAGE, "ParcelablePair")); + + tryAddWrapper( + elements, + wrappers, + "android.graphics.Bitmap", + ClassName.get(PARCELABLE_WRAPPER_PACKAGE, "ParcelableBitmap")); + + addArrayWrappers(types, elements, wrappers); + } + + private static void addGeneratedProtoParcelableWrappers( + Types types, + Elements elements, + Collection<ParcelableWrapper> wrappers, + Collection<TypeMirror> usedTypes) { + TypeElement protoElement = elements.getTypeElement("com.google.protobuf.MessageLite"); + if (protoElement == null) { + // Protos are not included at compile-time + return; + } + TypeMirror proto = protoElement.asType(); + + Collection<TypeMirror> protoTypes = + usedTypes.stream() + // <any> is the value when the compiler encounters a type which isn't accessible + // or does not exist. This passes the types.isAssignable filter, which makes such + // bugs hard to debug. This will already fail because the Java compiler won't allow + // it - so this is just to suppress strange test failures + .filter(t -> !t.toString().equals("<any>")) + .filter(t -> types.isAssignable(t, proto)) + .collect(toSet()); + + for (TypeMirror protoType : protoTypes) { + wrappers.add( + ParcelableWrapper.create( + protoType, getGeneratedProtoWrapperClassName(protoType), WrapperType.PROTO)); + } + } + + private static void addArrayWrappers( + Types types, Elements elements, Collection<ParcelableWrapper> wrappers) { + TypeElement typeElement = elements.getTypeElement("java.lang.Object"); + TypeMirror typeMirror = types.getArrayType(typeElement.asType()); + + ClassName wrapperClassName = ClassName.get(PARCELABLE_WRAPPER_PACKAGE, "ParcelableArray"); + + wrappers.add(ParcelableWrapper.create(typeMirror, wrapperClassName, WrapperType.DEFAULT)); + } + + private static void tryAddWrapper( + Elements elements, + Collection<ParcelableWrapper> wrappers, + String typeQualifiedName, + ClassName wrapperClassName) { + TypeElement typeElement = elements.getTypeElement(typeQualifiedName); + + if (typeElement == null) { + // The type isn't supported at compile-time - so won't be included in this app + return; + } + + wrappers.add( + ParcelableWrapper.create(typeElement.asType(), wrapperClassName, WrapperType.DEFAULT)); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ParcelableWrapperAnnotationInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ParcelableWrapperAnnotationInfo.java new file mode 100644 index 0000000..d9e7949 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ParcelableWrapperAnnotationInfo.java @@ -0,0 +1,42 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.containers; + +import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper; +import com.google.android.enterprise.connectedapps.processor.GeneratorUtilities; +import com.google.auto.value.AutoValue; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.Types; + +/** Wrapper around information contained in a {@link CustomParcelableWrapper} annotation. */ +@AutoValue +public abstract class ParcelableWrapperAnnotationInfo { + + public abstract TypeElement originalType(); + + public static ParcelableWrapperAnnotationInfo extractFromParcelableWrapperAnnotation( + Types types, CustomParcelableWrapper customParcelableWrapperAnnotation) { + if (customParcelableWrapperAnnotation == null) { + throw new NullPointerException("parcelableWrapperAnnotation must not be null"); + } + + TypeElement originalType = + GeneratorUtilities.extractClassFromAnnotation( + types, customParcelableWrapperAnnotation::originalType); + + return new AutoValue_ParcelableWrapperAnnotationInfo(originalType); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ProfileConnectorInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ProfileConnectorInfo.java new file mode 100644 index 0000000..9a68099 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ProfileConnectorInfo.java @@ -0,0 +1,160 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.containers; + +import com.google.android.enterprise.connectedapps.annotations.AvailabilityRestrictions; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector.ProfileType; +import com.google.android.enterprise.connectedapps.processor.GeneratorUtilities; +import com.google.android.enterprise.connectedapps.processor.SupportedTypes; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableSet; +import com.squareup.javapoet.ClassName; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.Elements; + +/** Wrapper of an interface used as a profile connector. */ +@AutoValue +public abstract class ProfileConnectorInfo { + + @AutoValue + abstract static class CustomProfileConnectorAnnotationInfo { + abstract ProfileType primaryProfile(); + + abstract ClassName serviceName(); + + abstract ImmutableCollection<TypeElement> parcelableWrapperClasses(); + + abstract ImmutableCollection<TypeElement> futureWrapperClasses(); + + abstract ImmutableCollection<TypeElement> importsClasses(); + + abstract AvailabilityRestrictions availabilityRestrictions(); + } + + public abstract TypeElement connectorElement(); + + public ClassName connectorClassName() { + return ClassName.get(connectorElement()); + } + + public abstract ProfileType primaryProfile(); + + public abstract ClassName serviceName(); + + public abstract SupportedTypes supportedTypes(); + + public abstract ImmutableCollection<TypeElement> parcelableWrapperClasses(); + + public abstract ImmutableCollection<TypeElement> futureWrapperClasses(); + + public abstract ImmutableCollection<TypeElement> importsClasses(); + + public abstract AvailabilityRestrictions availabilityRestrictions(); + + public static ProfileConnectorInfo create( + ProcessingEnvironment processingEnv, + TypeElement connectorElement, + SupportedTypes globalSupportedTypes) { + + Elements elements = processingEnv.getElementUtils(); + + CustomProfileConnectorAnnotationInfo annotationInfo = + extractFromCustomProfileConnectorAnnotation(processingEnv, elements, connectorElement); + + Set<TypeElement> parcelableWrappers = new HashSet<>(annotationInfo.parcelableWrapperClasses()); + Set<TypeElement> futureWrappers = new HashSet<>(annotationInfo.futureWrapperClasses()); + + for (TypeElement importConnectorClass : annotationInfo.importsClasses()) { + ProfileConnectorInfo importConnector = + ProfileConnectorInfo.create(processingEnv, importConnectorClass, globalSupportedTypes); + parcelableWrappers.addAll(importConnector.parcelableWrapperClasses()); + futureWrappers.addAll(importConnector.futureWrapperClasses()); + } + + return new AutoValue_ProfileConnectorInfo( + connectorElement, + annotationInfo.primaryProfile(), + annotationInfo.serviceName(), + globalSupportedTypes + .asBuilder() + .addParcelableWrappers( + ParcelableWrapper.createCustomParcelableWrappers( + processingEnv.getTypeUtils(), + processingEnv.getElementUtils(), + parcelableWrappers)) + .addFutureWrappers( + FutureWrapper.createCustomFutureWrappers( + processingEnv.getTypeUtils(), processingEnv.getElementUtils(), futureWrappers)) + .build(), + ImmutableSet.copyOf(parcelableWrappers), + ImmutableSet.copyOf(futureWrappers), + annotationInfo.importsClasses(), + annotationInfo.availabilityRestrictions()); + } + + private static CustomProfileConnectorAnnotationInfo extractFromCustomProfileConnectorAnnotation( + ProcessingEnvironment processingEnv, Elements elements, TypeElement connectorElement) { + CustomProfileConnector customProfileConnector = + connectorElement.getAnnotation(CustomProfileConnector.class); + + if (customProfileConnector == null) { + return new AutoValue_ProfileConnectorInfo_CustomProfileConnectorAnnotationInfo( + ProfileType.NONE, + getDefaultServiceName(elements, connectorElement), + ImmutableSet.of(), + ImmutableSet.of(), + ImmutableSet.of(), + AvailabilityRestrictions.DEFAULT); + } + + Collection<TypeElement> parcelableWrappers = + GeneratorUtilities.extractClassesFromAnnotation( + processingEnv.getTypeUtils(), customProfileConnector::parcelableWrappers); + Collection<TypeElement> futureWrappers = + GeneratorUtilities.extractClassesFromAnnotation( + processingEnv.getTypeUtils(), customProfileConnector::futureWrappers); + Collection<TypeElement> imports = + GeneratorUtilities.extractClassesFromAnnotation( + processingEnv.getTypeUtils(), customProfileConnector::imports); + + String serviceClassName = customProfileConnector.serviceClassName(); + + return new AutoValue_ProfileConnectorInfo_CustomProfileConnectorAnnotationInfo( + customProfileConnector.primaryProfile(), + serviceClassName.isEmpty() + ? getDefaultServiceName(elements, connectorElement) + : ClassName.bestGuess(serviceClassName), + ImmutableSet.copyOf(parcelableWrappers), + ImmutableSet.copyOf(futureWrappers), + ImmutableSet.copyOf(imports), + customProfileConnector.availabilityRestrictions()); + } + + public static ClassName getDefaultServiceName(Elements elements, TypeElement connectorElement) { + PackageElement originalPackage = elements.getPackageOf(connectorElement); + + return ClassName.get( + originalPackage.getQualifiedName().toString(), + String.format("%s_Service", connectorElement.getSimpleName().toString())); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ProviderClassInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ProviderClassInfo.java new file mode 100644 index 0000000..02adf0c --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ProviderClassInfo.java @@ -0,0 +1,130 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.containers; + +import static com.google.android.enterprise.connectedapps.processor.GeneratorUtilities.findCrossProfileProviderMethodsInClass; +import static java.util.stream.Collectors.toSet; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.squareup.javapoet.ClassName; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.util.Elements; + +/** Wrapper of a cross-profile provider class. */ +@AutoValue +public abstract class ProviderClassInfo { + + public abstract TypeElement providerClassElement(); + + public ImmutableCollection<CrossProfileTypeInfo> allCrossProfileTypes() { + Set<CrossProfileTypeInfo> types = new HashSet<>(); + types.addAll(nonStaticTypes()); + types.addAll(staticTypes()); + return ImmutableSet.copyOf(types); + } + + public abstract ImmutableCollection<CrossProfileTypeInfo> nonStaticTypes(); + + public abstract ImmutableCollection<CrossProfileTypeInfo> staticTypes(); + + public String simpleName() { + return providerClassElement().getSimpleName().toString(); + } + + public ClassName className() { + return ClassName.get(providerClassElement()); + } + + public ImmutableCollection<VariableElement> publicConstructorArgumentTypes() { + return ImmutableList.copyOf( + providerClassElement().getEnclosedElements().stream() + .filter(e -> e instanceof ExecutableElement) + .map(e -> (ExecutableElement) e) + .filter(e -> e.getKind().equals(ElementKind.CONSTRUCTOR)) + .filter(e -> e.getModifiers().contains(Modifier.PUBLIC)) + .findFirst() + .get() + .getParameters()); + } + + public ExecutableElement findProviderMethodFor( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + if (!nonStaticTypes().contains(crossProfileType)) { + throw new IllegalArgumentException("This provider class does not provide this type"); + } + + return providerClassElement().getEnclosedElements().stream() + .filter(e -> e instanceof ExecutableElement) + .map(e -> (ExecutableElement) e) + .filter( + e -> + generatorContext + .types() + .isSameType( + e.getReturnType(), crossProfileType.crossProfileTypeElement().asType())) + .findFirst() + .get(); + } + + public static ProviderClassInfo create( + ValidatorContext context, ValidatorProviderClassInfo provider) { + Set<CrossProfileTypeInfo> nonStaticTypes = + extractCrossProfileTypeElementsFromReturnValues( + context.elements(), provider.providerClassElement()) + .stream() + .map( + crossProfileTypeElement -> + ValidatorCrossProfileTypeInfo.create( + context.processingEnv(), + crossProfileTypeElement, + context.globalSupportedTypes())) + .map(crossProfileType -> CrossProfileTypeInfo.create(context, crossProfileType)) + .collect(toSet()); + + Set<CrossProfileTypeInfo> staticTypes = + provider.staticTypes().stream() + .map( + crossProfileTypeElement -> + ValidatorCrossProfileTypeInfo.create( + context.processingEnv(), + crossProfileTypeElement, + context.globalSupportedTypes())) + .map(crossProfileType -> CrossProfileTypeInfo.create(context, crossProfileType)) + .collect(toSet()); + + return new AutoValue_ProviderClassInfo( + provider.providerClassElement(), + ImmutableSet.copyOf(nonStaticTypes), + ImmutableSet.copyOf(staticTypes)); + } + + public static Collection<TypeElement> extractCrossProfileTypeElementsFromReturnValues( + Elements elements, TypeElement providerClassElement) { + return findCrossProfileProviderMethodsInClass(providerClassElement).stream() + .map(e -> elements.getTypeElement(e.getReturnType().toString())) + .collect(toSet()); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/Type.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/Type.java new file mode 100644 index 0000000..d81f6cd --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/Type.java @@ -0,0 +1,136 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.containers; + +import com.google.android.enterprise.connectedapps.processor.TypeUtils; +import com.google.auto.value.AutoValue; +import java.util.Optional; +import javax.lang.model.type.TypeMirror; + +/** A type which may be supported by a given {@code CrossProfileConfiguration}. */ +@AutoValue +public abstract class Type { + public static Builder builder() { + return new AutoValue_Type.Builder() + .setAcceptableParameterType(false) + .setAcceptableReturnType(false) + .setSupportedWithAnyGenericType(false) + .setSupportedInsideWrapper(true) + .setSupportedInsideCrossProfileCallback(true); + } + + public abstract Builder toBuilder(); + + public abstract TypeMirror getTypeMirror(); + + public String getQualifiedName() { + return getTypeMirror().toString(); + } + + public abstract boolean isAcceptableReturnType(); + + public abstract boolean isAcceptableParameterType(); + + public abstract Optional<String> getAutomaticallyResolvedReplacement(); + + public boolean isArray() { + return TypeUtils.isArray(getTypeMirror()); + } + + public boolean canBeBundled() { + return getWriteToParcelCode().isPresent() && getReadFromParcelCode().isPresent(); + } + + public boolean isPrimitive() { + return getTypeMirror().getKind().isPrimitive(); + } + + public boolean isGeneric() { + return TypeUtils.isGeneric(getTypeMirror()); + } + + /** + * If this is set, then type arguments will not validated. + * + * <p>This allows for Parcelables which take responsibility for their own generics and do not use + * Bundler. + */ + public abstract boolean isSupportedWithAnyGenericType(); + + /** + * Can this type be used inside a wrapper type? For example a List or an array. + * + * <p>This allows for async listeners to only be acceptable as parameter types but not type + * arguments + */ + public abstract boolean isSupportedInsideWrapper(); + + public abstract boolean isSupportedInsideCrossProfileCallback(); + + public abstract Optional<FutureWrapper> getFutureWrapper(); + + public boolean isFuture() { + return getFutureWrapper().isPresent(); + } + + public abstract Optional<CrossProfileCallbackInterfaceInfo> getCrossProfileCallbackInterface(); + + public boolean isCrossProfileCallbackInterface() { + return getCrossProfileCallbackInterface().isPresent(); + } + + // If this is a generated Parcelable Wrapper then this will be set to the simple name + // (e.g. ParcelableList) + public abstract Optional<ParcelableWrapper> getParcelableWrapper(); + + public abstract Optional<String> getWriteToParcelCode(); + + public abstract Optional<String> getReadFromParcelCode(); + + /** A builder for {@link Type}. */ + @AutoValue.Builder + public abstract static class Builder { + + public abstract Builder setTypeMirror(TypeMirror typeMirror); + + public abstract Builder setAcceptableReturnType(boolean acceptableReturnType); + + public abstract Builder setAcceptableParameterType(boolean acceptableParameterType); + + public abstract Builder setAutomaticallyResolvedReplacement( + String automaticallyResolvedReplacement); + + public abstract Builder setSupportedWithAnyGenericType(boolean supportedWithAnyGenericType); + + public abstract Builder setSupportedInsideWrapper(boolean supportedInsideWrapper); + + public abstract Builder setSupportedInsideCrossProfileCallback( + boolean supportedInsideCrossProfileCallback); + + public abstract Builder setFutureWrapper(FutureWrapper futureWrapper); + + public abstract Builder setCrossProfileCallbackInterface( + CrossProfileCallbackInterfaceInfo crossProfileCallbackInterface); + + public abstract Builder setWriteToParcelCode(String writeToParcelCode); + + public abstract Builder setReadFromParcelCode(String readFromParcelCode); + + public abstract Builder setParcelableWrapper(ParcelableWrapper parcelableWrapper); + + public abstract Type build(); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/UserConnectorInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/UserConnectorInfo.java new file mode 100644 index 0000000..33a4d56 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/UserConnectorInfo.java @@ -0,0 +1,150 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.containers; + +import com.google.android.enterprise.connectedapps.annotations.AvailabilityRestrictions; +import com.google.android.enterprise.connectedapps.annotations.CustomUserConnector; +import com.google.android.enterprise.connectedapps.processor.GeneratorUtilities; +import com.google.android.enterprise.connectedapps.processor.SupportedTypes; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableSet; +import com.squareup.javapoet.ClassName; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.Elements; + +/** Wrapper of an interface used as a user connector. */ +@AutoValue +public abstract class UserConnectorInfo { + + @AutoValue + abstract static class CustomUserConnectorAnnotationInfo { + abstract ClassName serviceName(); + + abstract ImmutableCollection<TypeElement> parcelableWrapperClasses(); + + abstract ImmutableCollection<TypeElement> futureWrapperClasses(); + + abstract ImmutableCollection<TypeElement> importsClasses(); + + abstract AvailabilityRestrictions availabilityRestrictions(); + } + + public abstract TypeElement connectorElement(); + + public ClassName connectorClassName() { + return ClassName.get(connectorElement()); + } + + public abstract ClassName serviceName(); + + public abstract SupportedTypes supportedTypes(); + + public abstract ImmutableCollection<TypeElement> parcelableWrapperClasses(); + + public abstract ImmutableCollection<TypeElement> futureWrapperClasses(); + + public abstract ImmutableCollection<TypeElement> importsClasses(); + + public abstract AvailabilityRestrictions availabilityRestrictions(); + + public static UserConnectorInfo create( + ProcessingEnvironment processingEnv, + TypeElement connectorElement, + SupportedTypes globalSupportedTypes) { + Elements elements = processingEnv.getElementUtils(); + CustomUserConnectorAnnotationInfo annotationInfo = + extractFromCustomUserConnectorAnnotation(processingEnv, elements, connectorElement); + + Set<TypeElement> parcelableWrappers = new HashSet<>(annotationInfo.parcelableWrapperClasses()); + Set<TypeElement> futureWrappers = new HashSet<>(annotationInfo.futureWrapperClasses()); + + for (TypeElement importConnectorClass : annotationInfo.importsClasses()) { + UserConnectorInfo importConnector = + UserConnectorInfo.create(processingEnv, importConnectorClass, globalSupportedTypes); + parcelableWrappers.addAll(importConnector.parcelableWrapperClasses()); + futureWrappers.addAll(importConnector.futureWrapperClasses()); + } + + return new AutoValue_UserConnectorInfo( + connectorElement, + annotationInfo.serviceName(), + globalSupportedTypes + .asBuilder() + .addParcelableWrappers( + ParcelableWrapper.createCustomParcelableWrappers( + processingEnv.getTypeUtils(), + processingEnv.getElementUtils(), + parcelableWrappers)) + .addFutureWrappers( + FutureWrapper.createCustomFutureWrappers( + processingEnv.getTypeUtils(), processingEnv.getElementUtils(), futureWrappers)) + .build(), + ImmutableSet.copyOf(parcelableWrappers), + ImmutableSet.copyOf(futureWrappers), + annotationInfo.importsClasses(), + annotationInfo.availabilityRestrictions()); + } + + private static CustomUserConnectorAnnotationInfo extractFromCustomUserConnectorAnnotation( + ProcessingEnvironment processingEnv, Elements elements, TypeElement connectorElement) { + CustomUserConnector customUserConnector = + connectorElement.getAnnotation(CustomUserConnector.class); + + if (customUserConnector == null) { + return new AutoValue_UserConnectorInfo_CustomUserConnectorAnnotationInfo( + getDefaultServiceName(elements, connectorElement), + ImmutableSet.of(), + ImmutableSet.of(), + ImmutableSet.of(), + AvailabilityRestrictions.DEFAULT); + } + + Collection<TypeElement> parcelableWrappers = + GeneratorUtilities.extractClassesFromAnnotation( + processingEnv.getTypeUtils(), customUserConnector::parcelableWrappers); + Collection<TypeElement> futureWrappers = + GeneratorUtilities.extractClassesFromAnnotation( + processingEnv.getTypeUtils(), customUserConnector::futureWrappers); + Collection<TypeElement> imports = + GeneratorUtilities.extractClassesFromAnnotation( + processingEnv.getTypeUtils(), customUserConnector::imports); + + String serviceClassName = customUserConnector.serviceClassName(); + + return new AutoValue_UserConnectorInfo_CustomUserConnectorAnnotationInfo( + serviceClassName.isEmpty() + ? getDefaultServiceName(elements, connectorElement) + : ClassName.bestGuess(serviceClassName), + ImmutableSet.copyOf(parcelableWrappers), + ImmutableSet.copyOf(futureWrappers), + ImmutableSet.copyOf(imports), + customUserConnector.availabilityRestrictions()); + } + + public static ClassName getDefaultServiceName(Elements elements, TypeElement connectorElement) { + PackageElement originalPackage = elements.getPackageOf(connectorElement); + + return ClassName.get( + originalPackage.getQualifiedName().toString(), + String.format("%s_Service", connectorElement.getSimpleName().toString())); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorContext.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorContext.java new file mode 100644 index 0000000..e13f0c5 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorContext.java @@ -0,0 +1,120 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.containers; + +import com.google.android.enterprise.connectedapps.processor.SupportedTypes; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableSet; +import java.util.Collection; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +/** + * Context for connected apps code validators. + * + * <p>This is used to validate enough that a {@link GeneratorContext} can be created for further + * validation and generation. + */ +@AutoValue +public abstract class ValidatorContext extends Context { + + public static Builder builder() { + return new AutoValue_ValidatorContext.Builder(); + } + + public abstract SupportedTypes globalSupportedTypes(); + + public abstract ImmutableSet<ProfileConnectorInfo> newProfileConnectorInterfaces(); + + public abstract ImmutableSet<UserConnectorInfo> newUserConnectorInterfaces(); + + public abstract ImmutableSet<TypeElement> newGeneratedProfileConnectors(); + + public abstract ImmutableSet<TypeElement> newGeneratedUserConnectors(); + + public abstract ImmutableSet<ValidatorCrossProfileConfigurationInfo> newConfigurations(); + + public abstract ImmutableSet<ValidatorCrossProfileTypeInfo> newCrossProfileTypes(); + + public abstract ImmutableSet<ExecutableElement> newCrossProfileMethods(); + + public abstract ImmutableSet<ValidatorProviderClassInfo> newProviderClasses(); + + public abstract ImmutableSet<ExecutableElement> newProviderMethods(); + + public abstract ImmutableSet<TypeElement> newCrossProfileCallbackInterfaces(); + + public abstract ImmutableSet<ValidatorCrossProfileTestInfo> newCrossProfileTests(); + + public abstract ImmutableSet<TypeElement> newCustomParcelableWrappers(); + + public abstract ImmutableSet<TypeElement> newCustomFutureWrappers(); + + /** A builder for {@link ValidatorContext}. */ + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setProcessingEnv(ProcessingEnvironment processingEnv); + + public abstract Builder setElements(Elements elements); + + public abstract Builder setTypes(Types types); + + public abstract Builder setGlobalSupportedTypes(SupportedTypes globalSupportedTypes); + + public abstract Builder setNewProfileConnectorInterfaces( + Collection<ProfileConnectorInfo> newProfileConnectorInterfaces); + + public abstract Builder setNewUserConnectorInterfaces( + Collection<UserConnectorInfo> newUserConnectorInterfaces); + + public abstract Builder setNewGeneratedProfileConnectors( + Collection<TypeElement> newGeneratedConnectors); + + public abstract Builder setNewGeneratedUserConnectors( + Collection<TypeElement> newGeneratedUserConnectors); + + public abstract Builder setNewConfigurations( + Collection<ValidatorCrossProfileConfigurationInfo> newConfigurations); + + public abstract Builder setNewCrossProfileTypes( + Collection<ValidatorCrossProfileTypeInfo> newCrossProfileTypes); + + public abstract Builder setNewCrossProfileMethods( + Collection<ExecutableElement> newCrossProfileMethods); + + public abstract Builder setNewProviderClasses( + Collection<ValidatorProviderClassInfo> newProviderClasses); + + public abstract Builder setNewProviderMethods(Collection<ExecutableElement> newProviderMethods); + + public abstract Builder setNewCrossProfileCallbackInterfaces( + Collection<TypeElement> newCrossProfileCallbackInterfaces); + + public abstract Builder setNewCrossProfileTests( + Collection<ValidatorCrossProfileTestInfo> newCrossProfileTests); + + public abstract Builder setNewCustomParcelableWrappers( + Collection<TypeElement> newCustomParcelableWrappers); + + public abstract Builder setNewCustomFutureWrappers( + Collection<TypeElement> newCustomFutureWrappers); + + public abstract ValidatorContext build(); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileConfigurationInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileConfigurationInfo.java new file mode 100644 index 0000000..973714c --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileConfigurationInfo.java @@ -0,0 +1,125 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.containers; + +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.SERVICE_CLASSNAME; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableSet; +import com.squareup.javapoet.ClassName; +import java.util.Optional; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; + +/** A wrapper around basic information from a {@link CrossProfileConfiguration} annotation. */ +@AutoValue +public abstract class ValidatorCrossProfileConfigurationInfo { + + public abstract TypeElement configurationElement(); + + public abstract ImmutableCollection<TypeElement> providerClassElements(); + + public abstract ClassName serviceSuperclass(); + + public abstract Optional<TypeElement> serviceClass(); + + public abstract Optional<TypeElement> connector(); + + public static ImmutableSet<ValidatorCrossProfileConfigurationInfo> createMultipleFromElement( + ProcessingEnvironment processingEnvironment, TypeElement annotatedElement) { + ImmutableSet<CrossProfileConfigurationAnnotationInfo> infos = + AnnotationFinder.extractCrossProfileConfigurationsAnnotationInfo( + annotatedElement, + processingEnvironment.getTypeUtils(), + processingEnvironment.getElementUtils()) + .configurations(); + ImmutableSet.Builder<ValidatorCrossProfileConfigurationInfo> configurations = + ImmutableSet.builder(); + + if (infos.isEmpty()) { + configurations.add(createFromElement(processingEnvironment, annotatedElement)); + } else { + for (CrossProfileConfigurationAnnotationInfo info : infos) { + configurations.add(createFromAnnotationInfo(info, annotatedElement)); + } + } + + return configurations.build(); + } + + public static ValidatorCrossProfileConfigurationInfo createFromElement( + ProcessingEnvironment processingEnv, TypeElement annotatedElement) { + CrossProfileConfigurationAnnotationInfo annotationInfo = + extractFromCrossProfileConfigurationAnnotation(annotatedElement, processingEnv); + + return createFromAnnotationInfo(annotationInfo, annotatedElement); + } + + private static ValidatorCrossProfileConfigurationInfo createFromAnnotationInfo( + CrossProfileConfigurationAnnotationInfo annotationInfo, TypeElement annotatedElement) { + ClassName serviceSuperclass = + serviceSuperclassIsDefault(annotationInfo.serviceSuperclass()) + ? SERVICE_CLASSNAME + : ClassName.get(annotationInfo.serviceSuperclass()); + + TypeElement serviceClass = + serviceClassIsDefault(annotationInfo.serviceClass()) ? null : annotationInfo.serviceClass(); + + Optional<TypeElement> connector = + connectorIsDefault(annotationInfo.connector()) + ? Optional.empty() + : Optional.of(annotationInfo.connector()); + + return new AutoValue_ValidatorCrossProfileConfigurationInfo( + annotatedElement, + ImmutableSet.copyOf(annotationInfo.providerClasses()), + serviceSuperclass, + Optional.ofNullable(serviceClass), + connector); + } + + private static boolean serviceSuperclassIsDefault(TypeElement serviceSuperclass) { + // CrossProfileConfiguration.class is the default specified serviceSuperclass + return serviceSuperclass + .asType() + .toString() + .equals(CrossProfileConfiguration.class.getCanonicalName()); + } + + private static boolean serviceClassIsDefault(TypeElement serviceClass) { + // CrossProfileConfiguration.class is the default specified serviceClass + return serviceClass + .asType() + .toString() + .equals(CrossProfileConfiguration.class.getCanonicalName()); + } + + private static boolean connectorIsDefault(TypeElement connector) { + // CrossProfileConfiguration.class is the default specified connector + return connector.asType().toString().equals(CrossProfileConfiguration.class.getCanonicalName()); + } + + private static CrossProfileConfigurationAnnotationInfo + extractFromCrossProfileConfigurationAnnotation( + Element annotatedElement, ProcessingEnvironment processingEnv) { + return AnnotationFinder.extractCrossProfileConfigurationAnnotationInfo( + annotatedElement, processingEnv.getTypeUtils(), processingEnv.getElementUtils()); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileTestInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileTestInfo.java new file mode 100644 index 0000000..172c8ed --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileTestInfo.java @@ -0,0 +1,40 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.containers; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.testing.annotations.CrossProfileTest; +import com.google.auto.value.AutoValue; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.TypeElement; + +/** Wrapper of a {@link CrossProfileTest} annotated class. */ +@AutoValue +public abstract class ValidatorCrossProfileTestInfo { + + public abstract TypeElement crossProfileTestElement(); + + public abstract TypeElement configurationElement(); + + public static ValidatorCrossProfileTestInfo create( + ProcessingEnvironment processingEnv, TypeElement crossProfileTestElement) { + CrossProfileTestAnnotationInfo annotationInfo = + AnnotationFinder.extractCrossProfileTestAnnotationInfo( + crossProfileTestElement, processingEnv.getTypeUtils(), processingEnv.getElementUtils()); + return new AutoValue_ValidatorCrossProfileTestInfo( + crossProfileTestElement, annotationInfo.configuration()); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileTypeInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileTypeInfo.java new file mode 100644 index 0000000..c757ba4 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileTypeInfo.java @@ -0,0 +1,115 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.containers; + +import static com.google.android.enterprise.connectedapps.processor.GeneratorUtilities.findCrossProfileMethodsInClass; +import static java.util.stream.Collectors.toList; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfile; +import com.google.android.enterprise.connectedapps.processor.SupportedTypes; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileAnnotation; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; + +/** A wrapper around basic information from a {@link CrossProfile} type annotation. */ +@AutoValue +public abstract class ValidatorCrossProfileTypeInfo { + + public abstract TypeElement crossProfileTypeElement(); + + public abstract ImmutableList<ExecutableElement> crossProfileMethods(); + + public abstract Optional<ProfileConnectorInfo> profileConnector(); + + public abstract SupportedTypes supportedTypes(); + + public abstract ImmutableCollection<TypeElement> parcelableWrapperClasses(); + + public abstract ImmutableCollection<TypeElement> futureWrapperClasses(); + + public abstract String profileClassName(); + + public abstract boolean isStatic(); + + /** + * The specified timeout for async calls, or {@link CrossProfileAnnotation#DEFAULT_TIMEOUT_MILLIS} + * if unspecified. + */ + public abstract long timeoutMillis(); + + public static ValidatorCrossProfileTypeInfo create( + ProcessingEnvironment processingEnv, + TypeElement crossProfileTypeElement, + SupportedTypes globalSupportedTypes) { + CrossProfileAnnotationInfo annotationInfo = + AnnotationFinder.extractCrossProfileAnnotationInfo( + crossProfileTypeElement, processingEnv.getTypeUtils(), processingEnv.getElementUtils()); + + Optional<ProfileConnectorInfo> profileConnectorElement = + annotationInfo.connectorIsDefault() + ? Optional.empty() + : Optional.of( + ProfileConnectorInfo.create( + processingEnv, annotationInfo.connectorClass(), globalSupportedTypes)); + + List<ExecutableElement> crossProfileMethodElements = + findCrossProfileMethodsInClass(crossProfileTypeElement).stream() + .sorted(Comparator.comparing(i -> i.getSimpleName().toString())) + .collect(toList()); + + SupportedTypes incomingSupportedTypes = + profileConnectorElement.isPresent() + ? profileConnectorElement.get().supportedTypes() + : globalSupportedTypes; + + SupportedTypes supportedTypes = + incomingSupportedTypes + .asBuilder() + .addParcelableWrappers( + ParcelableWrapper.createCustomParcelableWrappers( + processingEnv.getTypeUtils(), + processingEnv.getElementUtils(), + annotationInfo.parcelableWrapperClasses())) + .addFutureWrappers( + FutureWrapper.createCustomFutureWrappers( + processingEnv.getTypeUtils(), + processingEnv.getElementUtils(), + annotationInfo.futureWrapperClasses())) + .build(); + + return new AutoValue_ValidatorCrossProfileTypeInfo( + crossProfileTypeElement, + ImmutableList.copyOf(crossProfileMethodElements), + profileConnectorElement, + supportedTypes, + annotationInfo.parcelableWrapperClasses(), + annotationInfo.futureWrapperClasses(), + annotationInfo.profileClassName(), + annotationInfo.isStatic(), + annotationInfo + .timeoutMillis() + .filter(value -> value != CrossProfileAnnotation.TIMEOUT_MILLIS_NOT_SET) + .orElse(CrossProfileAnnotation.DEFAULT_TIMEOUT_MILLIS)); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorProviderClassInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorProviderClassInfo.java new file mode 100644 index 0000000..c3ffc28 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorProviderClassInfo.java @@ -0,0 +1,51 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor.containers; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableSet; +import com.squareup.javapoet.ClassName; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.TypeElement; + +/** Wrapper of basic information for a cross-profile provider class. */ +@AutoValue +public abstract class ValidatorProviderClassInfo { + + public abstract TypeElement providerClassElement(); + + public abstract ImmutableCollection<TypeElement> staticTypes(); + + public String simpleName() { + return providerClassElement().getSimpleName().toString(); + } + + public ClassName className() { + return ClassName.get(providerClassElement()); + } + + public static ValidatorProviderClassInfo create( + ProcessingEnvironment processingEnv, TypeElement providerClassElement) { + CrossProfileProviderAnnotationInfo annotationInfo = + AnnotationFinder.extractCrossProfileProviderAnnotationInfo( + providerClassElement, processingEnv.getTypeUtils(), processingEnv.getElementUtils()); + + return new AutoValue_ValidatorProviderClassInfo( + providerClassElement, ImmutableSet.copyOf(annotationInfo.staticTypes())); + } +} diff --git a/processor/src/main/resources/futurewrappers/ListenableFutureWrapper.java b/processor/src/main/resources/futurewrappers/ListenableFutureWrapper.java new file mode 100644 index 0000000..f9fe728 --- /dev/null +++ b/processor/src/main/resources/futurewrappers/ListenableFutureWrapper.java @@ -0,0 +1,122 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.futurewrappers; + +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; + +import com.google.android.enterprise.connectedapps.FutureWrapper; +import com.google.android.enterprise.connectedapps.Profile; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.internal.BundlerType; +import com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger; +import com.google.android.enterprise.connectedapps.internal.FutureResultWriter; +import com.google.common.util.concurrent.FluentFuture; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import java.util.Map; + +/** Wrapper for adding support for {@link ListenableFuture} to the Connected Apps SDK. */ +public final class ListenableFutureWrapper<E> extends FutureWrapper<E> { + + private final SettableFuture<E> future = SettableFuture.create(); + + public static <E> ListenableFutureWrapper<E> create(Bundler bundler, BundlerType bundlerType) { + return new ListenableFutureWrapper<>(bundler, bundlerType); + } + + private ListenableFutureWrapper(Bundler bundler, BundlerType bundlerType) { + super(bundler, bundlerType); + } + + public ListenableFuture<E> getFuture() { + return future; + } + + @Override + public void onResult(E result) { + future.set(result); + } + + @Override + public void onException(Throwable throwable) { + future.setException(throwable); + } + + public static <E> void writeFutureResult( + ListenableFuture<E> future, FutureResultWriter<E> resultWriter) { + FluentFuture.from(future) + .addCallback( + new FutureCallback<E>() { + @Override + public void onSuccess(E result) { + resultWriter.onSuccess(result); + } + + @Override + public void onFailure(Throwable t) { + resultWriter.onFailure(t); + } + }, + directExecutor()); + } + + private static class MergerFutureCallback<E> implements FutureCallback<E> { + + private final Profile profileId; + private final CrossProfileCallbackMultiMerger<E> merger; + + MergerFutureCallback(Profile profileId, CrossProfileCallbackMultiMerger<E> merger) { + if (profileId == null || merger == null) { + throw new NullPointerException(); + } + this.profileId = profileId; + this.merger = merger; + } + + @Override + public void onSuccess(E result) { + merger.onResult(profileId, result); + } + + @Override + public void onFailure(Throwable t) { + // TODO: What should we do with the Throwable? + merger.missingResult(profileId); + } + } + + public static <E> ListenableFuture<Map<Profile, E>> groupResults( + Map<Profile, ListenableFuture<E>> results) { + SettableFuture<Map<Profile, E>> m = SettableFuture.create(); + CrossProfileCallbackMultiMerger<E> merger = + new CrossProfileCallbackMultiMerger<>(results.size(), m::set); + for (Map.Entry<Profile, ListenableFuture<E>> result : results.entrySet()) { + FluentFuture.from(result.getValue()) + .catching( + UnavailableProfileException.class, + (throwable) -> { + merger.missingResult(result.getKey()); + return null; // This will be passed into the callback but will be rejected by merger + // as duplicate + }, + directExecutor()) + .addCallback(new MergerFutureCallback<>(result.getKey(), merger), directExecutor()); + } + return m; + } +} diff --git a/processor/src/main/resources/parcelablewrappers/ParcelableArray.java b/processor/src/main/resources/parcelablewrappers/ParcelableArray.java new file mode 100644 index 0000000..c1cf335 --- /dev/null +++ b/processor/src/main/resources/parcelablewrappers/ParcelableArray.java @@ -0,0 +1,118 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.parcelablewrappers; + +import android.os.Parcel; +import android.os.Parcelable; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.internal.BundlerType; + +/** Wrapper for reading & writing arrays from and to {@link Parcel} instances. */ +public class ParcelableArray<E> implements Parcelable { + + private static final int NULL_SIZE = -1; + + private final Bundler bundler; + private final BundlerType type; + private final E[] array; + + /** + * Create a wrapper for a given array. + * + * <p>The passed in {@link Bundler} must be capable of bundling {@code F}. + */ + public static <F> ParcelableArray<F> of(Bundler bundler, BundlerType type, F[] array) { + return new ParcelableArray<>(bundler, type, array); + } + + public E[] get() { + return array; + } + + private ParcelableArray(Bundler bundler, BundlerType type, E[] array) { + if (bundler == null || type == null) { + throw new NullPointerException(); + } + this.bundler = bundler; + this.type = type; + this.array = array; + } + + private ParcelableArray(Parcel in) { + bundler = in.readParcelable(Bundler.class.getClassLoader()); + int size = in.readInt(); + + if (size == NULL_SIZE) { + type = null; + array = null; + return; + } + + type = in.readParcelable(Bundler.class.getClassLoader()); + BundlerType valueType = type.typeArguments().get(0); + + @SuppressWarnings("unchecked") + E[] a = (E[]) bundler.createArray(valueType, size); + array = a; + + if (size > 0) { + for (int i = 0; i < size; i++) { + @SuppressWarnings("unchecked") + E value = (E) bundler.readFromParcel(in, valueType); + array[i] = value; + } + } + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(bundler, flags); + + if (array == null) { + dest.writeInt(NULL_SIZE); + return; + } + + dest.writeInt(array.length); + dest.writeParcelable(type, flags); + if (array.length > 0) { + BundlerType valueType = type.typeArguments().get(0); + + for (E value : array) { + bundler.writeToParcel(dest, value, valueType, flags); + } + } + } + + @Override + public int describeContents() { + return 0; + } + + @SuppressWarnings("rawtypes") + public static final Creator<ParcelableArray> CREATOR = + new Creator<ParcelableArray>() { + @Override + public ParcelableArray createFromParcel(Parcel in) { + return new ParcelableArray(in); + } + + @Override + public ParcelableArray[] newArray(int size) { + return new ParcelableArray[size]; + } + }; +} diff --git a/processor/src/main/resources/parcelablewrappers/ParcelableBitmap.java b/processor/src/main/resources/parcelablewrappers/ParcelableBitmap.java new file mode 100644 index 0000000..ba27af8 --- /dev/null +++ b/processor/src/main/resources/parcelablewrappers/ParcelableBitmap.java @@ -0,0 +1,101 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.parcelablewrappers; + +import android.graphics.Bitmap; +import android.os.Parcel; +import android.os.Parcelable; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.internal.BundlerType; + +/** Wrapper for reading & writing {@link Bitmap} instances from and to {@link Parcel} instances. */ +// Though Bitmap is itself Parcelable, in some circumstances the Parcelling process can fail (see +// b/159895007). +public class ParcelableBitmap implements Parcelable { + private final Bitmap bitmap; + + /** Create a wrapper for a given bitmap. */ + public static ParcelableBitmap of(Bundler bundler, BundlerType type, Bitmap bitmap) { + return new ParcelableBitmap(bitmap); + } + + private ParcelableBitmap(Bitmap bitmap) { + this.bitmap = bitmap; + } + + private ParcelableBitmap(Parcel in) { + String configKey = in.readString(); + + if (configKey == null) { + bitmap = null; + return; + } + + Bitmap.Config config = Bitmap.Config.valueOf(configKey); + int width = in.readInt(); + int height = in.readInt(); + int[] colors = new int[width * height]; + in.readIntArray(colors); + + bitmap = Bitmap.createBitmap(colors, width, height, config); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + if (bitmap == null) { + out.writeString(null); + return; + } + + Bitmap.Config config = bitmap.getConfig(); + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + int[] colors = bitmapToPixelArray(bitmap); + + out.writeString(config.toString()); + out.writeInt(width); + out.writeInt(height); + out.writeIntArray(colors); + } + + @Override + public int describeContents() { + return 0; + } + + public Bitmap get() { + return bitmap; + } + + public static final Creator<ParcelableBitmap> CREATOR = + new Creator<ParcelableBitmap>() { + @Override + public ParcelableBitmap createFromParcel(Parcel in) { + return new ParcelableBitmap(in); + } + + @Override + public ParcelableBitmap[] newArray(int size) { + return new ParcelableBitmap[size]; + } + }; + + private static int[] bitmapToPixelArray(Bitmap bitmap) { + int[] pixels = new int[bitmap.getHeight() * bitmap.getWidth()]; + bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight()); + return pixels; + } +} diff --git a/processor/src/main/resources/parcelablewrappers/ParcelableCollection.java b/processor/src/main/resources/parcelablewrappers/ParcelableCollection.java new file mode 100644 index 0000000..66db136 --- /dev/null +++ b/processor/src/main/resources/parcelablewrappers/ParcelableCollection.java @@ -0,0 +1,117 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.parcelablewrappers; + +import android.os.Parcel; +import android.os.Parcelable; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.internal.BundlerType; +import java.util.ArrayList; +import java.util.Collection; + +/** + * Wrapper for reading & writing {@link Collection} instances from and to {@link Parcel} instances. + */ +public class ParcelableCollection<E> implements Parcelable { + + private static final int NULL_SIZE = -1; + + private final Bundler bundler; + private final BundlerType type; + private final Collection<E> collection; + + /** + * Create a wrapper for a given collection. + * + * <p>The passed in {@link Bundler} must be capable of bundling {@code F}. + */ + public static <F> ParcelableCollection<F> of( + Bundler bundler, BundlerType type, Collection<F> collection) { + return new ParcelableCollection<>(bundler, type, collection); + } + + public Collection<E> get() { + return collection; + } + + private ParcelableCollection(Bundler bundler, BundlerType type, Collection<E> collection) { + if (bundler == null || type == null) { + throw new NullPointerException(); + } + this.bundler = bundler; + this.type = type; + this.collection = collection; + } + + private ParcelableCollection(Parcel in) { + bundler = in.readParcelable(Bundler.class.getClassLoader()); + int size = in.readInt(); + if (size == NULL_SIZE) { + type = null; + collection = null; + return; + } + + collection = new ArrayList<>(); + type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader()); + if (size > 0) { + BundlerType valueType = type.typeArguments().get(0); + for (int i = 0; i < size; i++) { + @SuppressWarnings("unchecked") + E value = (E) bundler.readFromParcel(in, valueType); + collection.add(value); + } + } + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(bundler, flags); + + if (collection == null) { + dest.writeInt(NULL_SIZE); + return; + } + + dest.writeInt(collection.size()); + dest.writeParcelable(type, flags); + if (!collection.isEmpty()) { + BundlerType valueType = type.typeArguments().get(0); + for (E value : collection) { + bundler.writeToParcel(dest, value, valueType, flags); + } + } + } + + @Override + public int describeContents() { + return 0; + } + + @SuppressWarnings("rawtypes") + public static final Creator<ParcelableCollection> CREATOR = + new Creator<ParcelableCollection>() { + @Override + public ParcelableCollection createFromParcel(Parcel in) { + return new ParcelableCollection(in); + } + + @Override + public ParcelableCollection[] newArray(int size) { + return new ParcelableCollection[size]; + } + }; +} diff --git a/processor/src/main/resources/parcelablewrappers/ParcelableGuavaOptional.java b/processor/src/main/resources/parcelablewrappers/ParcelableGuavaOptional.java new file mode 100644 index 0000000..e2a14b9 --- /dev/null +++ b/processor/src/main/resources/parcelablewrappers/ParcelableGuavaOptional.java @@ -0,0 +1,120 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.parcelablewrappers; + +import android.os.Parcel; +import android.os.Parcelable; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.internal.BundlerType; +import com.google.common.base.Optional; + +/** + * Wrapper for reading & writing {@link Optional} instances from and to {@link Parcel} instances. + */ +public class ParcelableGuavaOptional<E> implements Parcelable { + + private static final int NULL = -1; + private static final int ABSENT = 0; + private static final int PRESENT = 1; + + private final Bundler bundler; + private final BundlerType type; + private final Optional<E> optional; + + /** + * Create a wrapper for a given optional. + * + * <p>The passed in {@link Bundler} must be capable of bundling {@code F}. + */ + public static <F> ParcelableGuavaOptional<F> of( + Bundler bundler, BundlerType type, Optional<F> optional) { + return new ParcelableGuavaOptional<>(bundler, type, optional); + } + + public Optional<E> get() { + return optional; + } + + private ParcelableGuavaOptional(Bundler bundler, BundlerType type, Optional<E> optional) { + if (bundler == null || type == null) { + throw new NullPointerException(); + } + this.bundler = bundler; + this.type = type; + this.optional = optional; + } + + private ParcelableGuavaOptional(Parcel in) { + bundler = in.readParcelable(Bundler.class.getClassLoader()); + + int presentValue = in.readInt(); + + if (presentValue == NULL) { + type = null; + optional = null; + return; + } + + boolean isPresent = presentValue == PRESENT; + type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader()); + if (isPresent) { + BundlerType valueType = type.typeArguments().get(0); + + @SuppressWarnings("unchecked") + E value = (E) bundler.readFromParcel(in, valueType); + + optional = Optional.of(value); + } else { + optional = Optional.absent(); + } + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(bundler, flags); + + if (optional == null) { + dest.writeInt(NULL); + return; + } + + dest.writeInt(optional.isPresent() ? PRESENT : ABSENT); + dest.writeParcelable(type, flags); + if (optional.isPresent()) { + BundlerType valueType = type.typeArguments().get(0); + bundler.writeToParcel(dest, optional.get(), valueType, flags); + } + } + + @Override + public int describeContents() { + return 0; + } + + @SuppressWarnings("rawtypes") + public static final Creator<ParcelableGuavaOptional> CREATOR = + new Creator<ParcelableGuavaOptional>() { + @Override + public ParcelableGuavaOptional createFromParcel(Parcel in) { + return new ParcelableGuavaOptional(in); + } + + @Override + public ParcelableGuavaOptional[] newArray(int size) { + return new ParcelableGuavaOptional[size]; + } + }; +} diff --git a/processor/src/main/resources/parcelablewrappers/ParcelableImmutableMap.java b/processor/src/main/resources/parcelablewrappers/ParcelableImmutableMap.java new file mode 100644 index 0000000..78b7790 --- /dev/null +++ b/processor/src/main/resources/parcelablewrappers/ParcelableImmutableMap.java @@ -0,0 +1,129 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.parcelablewrappers; + +import android.os.Parcel; +import android.os.Parcelable; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.internal.BundlerType; +import com.google.common.collect.ImmutableMap; + +/** + * Wrapper for reading & writing {@link ImmutableMap} instances from and to {@link Parcel} + * instances. + */ +public class ParcelableImmutableMap<E, F> implements Parcelable { + + private static final int NULL_SIZE = -1; + private static final int KEY_TYPE_INDEX = 0; + private static final int VALUE_TYPE_INDEX = 1; + + private final Bundler bundler; + private final BundlerType type; + private final ImmutableMap<E, F> map; + + /** + * Create a wrapper for a given immutable map. + * + * <p>The passed in {@link Bundler} must be capable of bundling {@code E} and {@code F}. + */ + public static <E, F> ParcelableImmutableMap<E, F> of( + Bundler bundler, BundlerType type, ImmutableMap<E, F> map) { + return new ParcelableImmutableMap<>(bundler, type, map); + } + + public ImmutableMap<E, F> get() { + return map; + } + + private ParcelableImmutableMap(Bundler bundler, BundlerType type, ImmutableMap<E, F> map) { + if (bundler == null || type == null) { + throw new NullPointerException(); + } + this.bundler = bundler; + this.type = type; + this.map = map; + } + + private ParcelableImmutableMap(Parcel in) { + bundler = in.readParcelable(Bundler.class.getClassLoader()); + int size = in.readInt(); + + if (size == NULL_SIZE) { + type = null; + map = null; + return; + } + + ImmutableMap.Builder<E, F> mapBuilder = ImmutableMap.builder(); + + type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader()); + if (size > 0) { + BundlerType keyType = type.typeArguments().get(KEY_TYPE_INDEX); + BundlerType valueType = type.typeArguments().get(VALUE_TYPE_INDEX); + for (int i = 0; i < size; i++) { + @SuppressWarnings("unchecked") + E key = (E) bundler.readFromParcel(in, keyType); + @SuppressWarnings("unchecked") + F value = (F) bundler.readFromParcel(in, valueType); + mapBuilder.put(key, value); + } + } + + map = mapBuilder.build(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(bundler, flags); + + if (map == null) { + dest.writeInt(NULL_SIZE); + return; + } + + dest.writeInt(map.size()); + dest.writeParcelable(type, flags); + if (!map.isEmpty()) { + BundlerType keyType = type.typeArguments().get(0); + BundlerType valueType = type.typeArguments().get(1); + + for (ImmutableMap.Entry<E, F> entry : map.entrySet()) { + bundler.writeToParcel(dest, entry.getKey(), keyType, flags); + bundler.writeToParcel(dest, entry.getValue(), valueType, flags); + } + } + } + + @Override + public int describeContents() { + return 0; + } + + @SuppressWarnings("rawtypes") + public static final Creator<ParcelableImmutableMap> CREATOR = + new Creator<ParcelableImmutableMap>() { + @Override + public ParcelableImmutableMap createFromParcel(Parcel in) { + return new ParcelableImmutableMap(in); + } + + @Override + public ParcelableImmutableMap[] newArray(int size) { + return new ParcelableImmutableMap[size]; + } + }; +} diff --git a/processor/src/main/resources/parcelablewrappers/ParcelableList.java b/processor/src/main/resources/parcelablewrappers/ParcelableList.java new file mode 100644 index 0000000..b1ff12e --- /dev/null +++ b/processor/src/main/resources/parcelablewrappers/ParcelableList.java @@ -0,0 +1,117 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.parcelablewrappers; + +import android.os.Parcel; +import android.os.Parcelable; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.internal.BundlerType; +import java.util.ArrayList; +import java.util.List; + +/** Wrapper for reading & writing {@link List} instances from and to {@link Parcel} instances. */ + +public class ParcelableList<E> implements Parcelable { + + private static final int NULL_SIZE = -1; + + private final Bundler bundler; + private final BundlerType type; + private final List<E> list; + + /** + * Create a wrapper for a given list. + * + * <p>The passed in {@link Bundler} must be capable of bundling {@code F}. + */ + public static <F> ParcelableList<F> of(Bundler bundler, BundlerType type, List<F> list) { + return new ParcelableList<>(bundler, type, list); + } + + public List<E> get() { + return list; + } + + private ParcelableList(Bundler bundler, BundlerType type, List<E> list) { + if (bundler == null || type == null) { + throw new NullPointerException(); + } + this.bundler = bundler; + this.type = type; + this.list = list; + } + + private ParcelableList(Parcel in) { + bundler = in.readParcelable(Bundler.class.getClassLoader()); + int size = in.readInt(); + + if (size == NULL_SIZE) { + type = null; + list = null; + return; + } + + list = new ArrayList<>(); + type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader()); + if (size > 0) { + BundlerType valueType = type.typeArguments().get(0); + for (int i = 0; i < size; i++) { + @SuppressWarnings("unchecked") + E value = (E) bundler.readFromParcel(in, valueType); + list.add(value); + } + } + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(bundler, flags); + + if (list == null) { + dest.writeInt(NULL_SIZE); + return; + } + + dest.writeInt(list.size()); + dest.writeParcelable(type, flags); + if (!list.isEmpty()) { + BundlerType valueType = type.typeArguments().get(0); + + for (E value : list) { + bundler.writeToParcel(dest, value, valueType, flags); + } + } + } + + @Override + public int describeContents() { + return 0; + } + + @SuppressWarnings("rawtypes") + public static final Creator<ParcelableList> CREATOR = + new Creator<ParcelableList>() { + @Override + public ParcelableList createFromParcel(Parcel in) { + return new ParcelableList(in); + } + + @Override + public ParcelableList[] newArray(int size) { + return new ParcelableList[size]; + } + }; +} diff --git a/processor/src/main/resources/parcelablewrappers/ParcelableMap.java b/processor/src/main/resources/parcelablewrappers/ParcelableMap.java new file mode 100644 index 0000000..e90c22b --- /dev/null +++ b/processor/src/main/resources/parcelablewrappers/ParcelableMap.java @@ -0,0 +1,121 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.parcelablewrappers; + +import android.os.Parcel; +import android.os.Parcelable; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.internal.BundlerType; +import java.util.HashMap; +import java.util.Map; + +/** Wrapper for reading & writing {@link Map} instances from and to {@link Parcel} instances. */ +public class ParcelableMap<E, F> implements Parcelable { + + private static final int NULL_SIZE = -1; + + private final Bundler bundler; + private final BundlerType type; + private final Map<E, F> map; + + /** + * Create a wrapper for a given map. + * + * <p>The passed in {@link Bundler} must be capable of bundling {@code E} and {@code F}. + */ + public static <E, F> ParcelableMap<E, F> of(Bundler bundler, BundlerType type, Map<E, F> map) { + return new ParcelableMap<>(bundler, type, map); + } + + public Map<E, F> get() { + return map; + } + + private ParcelableMap(Bundler bundler, BundlerType type, Map<E, F> map) { + if (bundler == null || type == null) { + throw new NullPointerException(); + } + this.bundler = bundler; + this.type = type; + this.map = map; + } + + private ParcelableMap(Parcel in) { + bundler = in.readParcelable(Bundler.class.getClassLoader()); + int size = in.readInt(); + + if (size == NULL_SIZE) { + type = null; + map = null; + return; + } + + map = new HashMap<>(); + type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader()); + if (size > 0) { + BundlerType keyType = type.typeArguments().get(0); + BundlerType valueType = type.typeArguments().get(1); + for (int i = 0; i < size; i++) { + @SuppressWarnings("unchecked") + E key = (E) bundler.readFromParcel(in, keyType); + @SuppressWarnings("unchecked") + F value = (F) bundler.readFromParcel(in, valueType); + map.put(key, value); + } + } + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(bundler, flags); + + if (map == null) { + dest.writeInt(NULL_SIZE); + return; + } + + dest.writeInt(map.size()); + dest.writeParcelable(type, flags); + if (!map.isEmpty()) { + BundlerType keyType = type.typeArguments().get(0); + BundlerType valueType = type.typeArguments().get(1); + + for (Map.Entry<E, F> entry : map.entrySet()) { + bundler.writeToParcel(dest, entry.getKey(), keyType, flags); + bundler.writeToParcel(dest, entry.getValue(), valueType, flags); + } + } + } + + @Override + public int describeContents() { + return 0; + } + + @SuppressWarnings("rawtypes") + public static final Creator<ParcelableMap> CREATOR = + new Creator<ParcelableMap>() { + @Override + public ParcelableMap createFromParcel(Parcel in) { + return new ParcelableMap(in); + } + + @Override + public ParcelableMap[] newArray(int size) { + return new ParcelableMap[size]; + } + }; +} diff --git a/processor/src/main/resources/parcelablewrappers/ParcelableOptional.java b/processor/src/main/resources/parcelablewrappers/ParcelableOptional.java new file mode 100644 index 0000000..aa81dc9 --- /dev/null +++ b/processor/src/main/resources/parcelablewrappers/ParcelableOptional.java @@ -0,0 +1,120 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.parcelablewrappers; + +import android.os.Parcel; +import android.os.Parcelable; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.internal.BundlerType; +import java.util.Optional; + +/** + * Wrapper for reading & writing {@link Optional} instances from and to {@link Parcel} instances. + */ +public class ParcelableOptional<E> implements Parcelable { + + private static final int NULL = -1; + private static final int ABSENT = 0; + private static final int PRESENT = 1; + + private final Bundler bundler; + private final BundlerType type; + private final Optional<E> optional; + + /** + * Create a wrapper for a given optional. + * + * <p>The passed in {@link Bundler} must be capable of bundling {@code F}. + */ + public static <F> ParcelableOptional<F> of( + Bundler bundler, BundlerType type, Optional<F> optional) { + return new ParcelableOptional<>(bundler, type, optional); + } + + public Optional<E> get() { + return optional; + } + + private ParcelableOptional(Bundler bundler, BundlerType type, Optional<E> optional) { + if (bundler == null || type == null) { + throw new NullPointerException(); + } + this.bundler = bundler; + this.type = type; + this.optional = optional; + } + + private ParcelableOptional(Parcel in) { + bundler = in.readParcelable(Bundler.class.getClassLoader()); + + int presentValue = in.readInt(); + + if (presentValue == NULL) { + type = null; + optional = null; + return; + } + + boolean isPresent = presentValue == PRESENT; + type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader()); + if (isPresent) { + BundlerType valueType = type.typeArguments().get(0); + + @SuppressWarnings("unchecked") + E value = (E) bundler.readFromParcel(in, valueType); + + optional = Optional.of(value); + } else { + optional = Optional.empty(); + } + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(bundler, flags); + + if (optional == null) { + dest.writeInt(NULL); + return; + } + + dest.writeInt(optional.isPresent() ? PRESENT : ABSENT); + dest.writeParcelable(type, flags); + if (optional.isPresent()) { + BundlerType valueType = type.typeArguments().get(0); + bundler.writeToParcel(dest, optional.get(), valueType, flags); + } + } + + @Override + public int describeContents() { + return 0; + } + + @SuppressWarnings("rawtypes") + public static final Creator<ParcelableOptional> CREATOR = + new Creator<ParcelableOptional>() { + @Override + public ParcelableOptional createFromParcel(Parcel in) { + return new ParcelableOptional(in); + } + + @Override + public ParcelableOptional[] newArray(int size) { + return new ParcelableOptional[size]; + } + }; +} diff --git a/processor/src/main/resources/parcelablewrappers/ParcelablePair.java b/processor/src/main/resources/parcelablewrappers/ParcelablePair.java new file mode 100644 index 0000000..41dea47 --- /dev/null +++ b/processor/src/main/resources/parcelablewrappers/ParcelablePair.java @@ -0,0 +1,115 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.parcelablewrappers; + +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Pair; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.internal.BundlerType; + +/** Wrapper for reading & writing {@link Pair} instances from and to {@link Parcel} instances. */ +public class ParcelablePair<F, S> implements Parcelable { + + private static final int NULL = -1; + private static final int NOT_NULL = 1; + + private final Bundler bundler; + private final BundlerType type; + private final Pair<F, S> pair; + + /** + * Create a wrapper for a given pair. + * + * <p>The passed in {@link Bundler} must be capable of bundling {@code E} and {@code F}. + */ + public static <F, S> ParcelablePair<F, S> of(Bundler bundler, BundlerType type, Pair<F, S> pair) { + return new ParcelablePair<>(bundler, type, pair); + } + + public Pair<F, S> get() { + return pair; + } + + private ParcelablePair(Bundler bundler, BundlerType type, Pair<F, S> pair) { + if (bundler == null || type == null) { + throw new NullPointerException(); + } + this.bundler = bundler; + this.type = type; + this.pair = pair; + } + + private ParcelablePair(Parcel in) { + bundler = in.readParcelable(Bundler.class.getClassLoader()); + int present = in.readInt(); + + if (present == NULL) { + type = null; + pair = null; + return; + } + + type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader()); + BundlerType fType = type.typeArguments().get(0); + BundlerType sType = type.typeArguments().get(1); + + @SuppressWarnings("unchecked") + F first = (F) bundler.readFromParcel(in, fType); + @SuppressWarnings("unchecked") + S second = (S) bundler.readFromParcel(in, sType); + + pair = new Pair<>(first, second); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(bundler, flags); + + if (pair == null) { + dest.writeInt(NULL); + return; + } + + dest.writeInt(NOT_NULL); + dest.writeParcelable(type, flags); + + BundlerType fType = type.typeArguments().get(0); + BundlerType sType = type.typeArguments().get(1); + + bundler.writeToParcel(dest, pair.first, fType, flags); + bundler.writeToParcel(dest, pair.second, sType, flags); + } + + @Override + public int describeContents() { + return 0; + } + + @SuppressWarnings("rawtypes") + public static final Creator<ParcelablePair> CREATOR = + new Creator<ParcelablePair>() { + @Override + public ParcelablePair createFromParcel(Parcel in) { + return new ParcelablePair(in); + } + + @Override + public ParcelablePair[] newArray(int size) { + return new ParcelablePair[size]; + } + }; +} diff --git a/processor/src/main/resources/parcelablewrappers/ParcelableSet.java b/processor/src/main/resources/parcelablewrappers/ParcelableSet.java new file mode 100644 index 0000000..b032f21 --- /dev/null +++ b/processor/src/main/resources/parcelablewrappers/ParcelableSet.java @@ -0,0 +1,116 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.parcelablewrappers; + +import android.os.Parcel; +import android.os.Parcelable; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.internal.BundlerType; +import java.util.HashSet; +import java.util.Set; + +/** Wrapper for reading & writing {@link Set} instances from and to {@link Parcel} instances. */ +public class ParcelableSet<E> implements Parcelable { + + private static final int NULL_SIZE = -1; + + private final Bundler bundler; + private final BundlerType type; + private final Set<E> set; + + /** + * Create a wrapper for a given set. + * + * <p>The passed in {@link Bundler} must be capable of bundling {@code F}. + */ + public static <F> ParcelableSet<F> of(Bundler bundler, BundlerType type, Set<F> set) { + return new ParcelableSet<>(bundler, type, set); + } + + public Set<E> get() { + return set; + } + + private ParcelableSet(Bundler bundler, BundlerType type, Set<E> set) { + if (bundler == null || type == null) { + throw new NullPointerException(); + } + this.bundler = bundler; + this.type = type; + this.set = set; + } + + private ParcelableSet(Parcel in) { + bundler = in.readParcelable(Bundler.class.getClassLoader()); + int size = in.readInt(); + + if (size == NULL_SIZE) { + type = null; + set = null; + return; + } + + set = new HashSet<>(); + type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader()); + if (size > 0) { + BundlerType valueType = type.typeArguments().get(0); + for (int i = 0; i < size; i++) { + @SuppressWarnings("unchecked") + E value = (E) bundler.readFromParcel(in, valueType); + set.add(value); + } + } + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(bundler, flags); + + if (set == null) { + dest.writeInt(NULL_SIZE); + return; + } + + dest.writeInt(set.size()); + dest.writeParcelable(type, flags); + if (!set.isEmpty()) { + BundlerType valueType = type.typeArguments().get(0); + + for (E value : set) { + bundler.writeToParcel(dest, value, valueType, flags); + } + } + } + + @Override + public int describeContents() { + return 0; + } + + @SuppressWarnings("rawtypes") + public static final Creator<ParcelableSet> CREATOR = + new Creator<ParcelableSet>() { + @Override + public ParcelableSet createFromParcel(Parcel in) { + return new ParcelableSet(in); + } + + @Override + public ParcelableSet[] newArray(int size) { + return new ParcelableSet[size]; + } + }; +} diff --git a/proguard.pgcfg b/proguard.pgcfg new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/proguard.pgcfg diff --git a/sdk/build.gradle b/sdk/build.gradle new file mode 100644 index 0000000..f9a6886 --- /dev/null +++ b/sdk/build.gradle @@ -0,0 +1,71 @@ +plugins { + id 'com.android.library' + id 'maven-publish' +} + +dependencies { + api deps.checkerFramework + implementation project(path: ':connectedapps-annotations') + testImplementation project(path: ':connectedapps-sharedtests') + testImplementation 'org.robolectric:robolectric:4.4' + testImplementation 'junit:junit:4.13.1' + testImplementation 'com.google.truth:truth:1.1.2' + testImplementation 'androidx.test:core:1.3.0' + testImplementation project(path: ':connectedapps') + testImplementation project(path: ':connectedapps-annotations') + testImplementation project(path: ':connectedapps-processor') + testAnnotationProcessor project(path: ':connectedapps-processor') + testImplementation project(path: ':connectedapps-testing') + testImplementation project(path: ':connectedapps-testing-annotations') + testImplementation 'com.google.protobuf:protobuf-java:4.0.0-rc-2' + testAnnotationProcessor deps.autovalue + testImplementation deps.autovalueAnnotations +} + +afterEvaluate { + publishing { + publications { + maven(MavenPublication) { + from components.release + groupId = 'com.google.android.enterprise.connectedapps' + artifactId = 'connectedapps' + version = project.version + + pom { + licenses { + license { + name = 'Apache 2.0' + url = 'https://opensource.org/licenses/Apache-2.0' + } + } + } + } + } + } +} + +android { + sourceSets { + test{ + java.srcDir file('../tests/robotests/src') + } + } +} + +android { + defaultConfig { + compileSdkVersion 30 + minSdkVersion 26 + } + + buildFeatures { + aidl = true + } + + testOptions.unitTests.includeAndroidResources = true + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} diff --git a/sdk/src/main/AndroidManifest.xml b/sdk/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6b0ac31 --- /dev/null +++ b/sdk/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="utf-8"?> + +<manifest package="com.google.android.enterprise.connectedapps" /> diff --git a/sdk/src/main/aidl/com/google/android/enterprise/connectedapps/ICrossProfileCallback.aidl b/sdk/src/main/aidl/com/google/android/enterprise/connectedapps/ICrossProfileCallback.aidl new file mode 100644 index 0000000..581da13 --- /dev/null +++ b/sdk/src/main/aidl/com/google/android/enterprise/connectedapps/ICrossProfileCallback.aidl @@ -0,0 +1,22 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.google.android.enterprise.connectedapps; + +interface ICrossProfileCallback { + void prepareResult(long callId, int blockId, int numBytes, in byte[] params); + void onResult(long callId, int blockId, int methodIdentifier, in byte[] params); + void onException(long callId, int blockId, in byte[] params); +}
\ No newline at end of file diff --git a/sdk/src/main/aidl/com/google/android/enterprise/connectedapps/ICrossProfileService.aidl b/sdk/src/main/aidl/com/google/android/enterprise/connectedapps/ICrossProfileService.aidl new file mode 100644 index 0000000..126f2d4 --- /dev/null +++ b/sdk/src/main/aidl/com/google/android/enterprise/connectedapps/ICrossProfileService.aidl @@ -0,0 +1,41 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.google.android.enterprise.connectedapps; + +import com.google.android.enterprise.connectedapps.ICrossProfileCallback; + +interface ICrossProfileService { + // When making a call containing params larger than + // CrossProfileSender.MAX_BYTES_PER_BLOCK bytes, first split the marshalled + // params parcel byte array into blocks of + // CrossProfileSender.MAX_BYTES_PER_BLOCK bytes, and call prepareCall with + // all but the final block. + // callId is arbitrary and is used to link together calls to prepareCall and + // call. + // numBytes represents the full amount of bytes in total across all blocks + // and is used to prepare the cache with the first use of prepareCall + void prepareCall(long callId, int blockId, int numBytes, in byte[] params); + + // When making a call with params smaller than + // CrossProfileSender.MAX_BYTES_PER_BLOCK bytes bytes, or with the final + // block in a larger call, this method is used. + // crossProfileTypeIdentifier and methodIdentifier are used to identify the + // method to call. + byte[] call(long callId, int blockId, long crossProfileTypeIdentifier, int methodIdentifier, in byte[] params, + ICrossProfileCallback callback); + + byte[] fetchResponse(long callId, int blockId); +}
\ No newline at end of file diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/AbstractProfileBinder.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/AbstractProfileBinder.java new file mode 100644 index 0000000..b4e908f --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/AbstractProfileBinder.java @@ -0,0 +1,138 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import static android.Manifest.permission.INTERACT_ACROSS_PROFILES; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.CrossProfileApps; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.os.UserHandle; +import android.util.Log; +import com.google.android.enterprise.connectedapps.annotations.AvailabilityRestrictions; +import com.google.android.enterprise.connectedapps.exceptions.MissingApiException; + +/** + * Abstract {@link ConnectionBinder} which allows subclasses to define the {@link Intent} to bind + * to. + * + * <p>Methods expect that the app has INTERACT_ACROSS_USERS or INTERACT_ACROSS_PROFILES permission. + */ +public abstract class AbstractProfileBinder implements ConnectionBinder { + + private boolean hasCachedPermissionRequests = false; + private boolean requestsInteractAcrossProfiles = false; + private boolean requestsInteractAcrossUsers = false; + private boolean requestsInteractAcrossUsersFull = false; + + private static final String INTERACT_ACROSS_USERS = "android.permission.INTERACT_ACROSS_USERS"; + private static final String INTERACT_ACROSS_USERS_FULL = + "android.permission.INTERACT_ACROSS_USERS_FULL"; + + protected abstract Intent createIntent(Context context, ComponentName bindToService); + + @Override + public boolean tryBind( + Context context, + ComponentName bindToService, + ServiceConnection connection, + AvailabilityRestrictions availabilityRestrictions) + throws MissingApiException { + UserHandle otherUserHandle = + CrossProfileSender.getOtherUserHandle(context, availabilityRestrictions); + + if (otherUserHandle == null) { + // There is no user to bind to but there might be later + return false; + } + + Intent bindIntent = createIntent(context, bindToService); + + boolean hasBound = + ReflectionUtilities.bindServiceAsUser(context, bindIntent, connection, otherUserHandle); + if (!hasBound) { + context.unbindService(connection); + } + return hasBound; + } + + @Override + public boolean bindingIsPossible( + Context context, AvailabilityRestrictions availabilityRestrictions) { + UserHandle otherUserHandle = + CrossProfileSender.getOtherUserHandle(context, availabilityRestrictions); + return otherUserHandle != null; + } + + @Override + public boolean hasPermissionToBind(Context context) { + cachePermissionRequests(context); + + if (VERSION.SDK_INT >= VERSION_CODES.R + && requestsInteractAcrossProfiles + && context.getSystemService(CrossProfileApps.class).canInteractAcrossProfiles()) { + return true; + } + if (requestsInteractAcrossUsersFull + && context.checkSelfPermission(INTERACT_ACROSS_USERS_FULL) + == PackageManager.PERMISSION_GRANTED) { + return true; + } + if (requestsInteractAcrossUsers + && context.checkSelfPermission(INTERACT_ACROSS_USERS) + == PackageManager.PERMISSION_GRANTED) { + return true; + } + + return false; + } + + private void cachePermissionRequests(Context context) { + if (hasCachedPermissionRequests) { + return; + } + + PackageManager packageManager = context.getPackageManager(); + try { + PackageInfo packageInfo = + packageManager.getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS); + + for (String permission : packageInfo.requestedPermissions) { + if (permission.equals(INTERACT_ACROSS_PROFILES)) { + requestsInteractAcrossProfiles = true; + } else if (permission.equals(INTERACT_ACROSS_USERS)) { + requestsInteractAcrossUsers = true; + } else if (permission.equals(INTERACT_ACROSS_USERS_FULL)) { + requestsInteractAcrossUsersFull = true; + } + } + } catch (NameNotFoundException e) { + Log.e("AbstractProfileBinder", "Could not find package.", e); + requestsInteractAcrossProfiles = false; + requestsInteractAcrossUsers = false; + requestsInteractAcrossUsersFull = false; + } + + hasCachedPermissionRequests = true; + } +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/AbstractProfileConnector.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/AbstractProfileConnector.java new file mode 100644 index 0000000..ae24257 --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/AbstractProfileConnector.java @@ -0,0 +1,251 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import android.content.Context; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import com.google.android.enterprise.connectedapps.annotations.AvailabilityRestrictions; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector.ProfileType; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** Standard implementation of {@link ProfileConnector}. */ +public abstract class AbstractProfileConnector + implements ProfileConnector, ConnectionListener, AvailabilityListener { + + private CrossProfileSender crossProfileSender; + private final Set<ConnectionListener> connectionListeners = new CopyOnWriteArraySet<>(); + private final Set<AvailabilityListener> availabilityListeners = new CopyOnWriteArraySet<>(); + + private final Context context; + private final ScheduledExecutorService scheduledExecutorService; + private final ConnectionBinder binder; + private final String serviceClassName; + private final @Nullable ProfileType primaryProfileType; + private final AvailabilityRestrictions availabilityRestrictions; + + public AbstractProfileConnector( + Class<? extends ProfileConnector> profileConnectorClass, Builder builder) { + if (profileConnectorClass == null || builder == null || builder.context == null) { + throw new NullPointerException(); + } + if (builder.scheduledExecutorService == null) { + scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); + } else { + scheduledExecutorService = builder.scheduledExecutorService; + } + + if (builder.binder == null) { + binder = new DefaultProfileBinder(); + } else { + binder = builder.binder; + } + + context = builder.context.getApplicationContext(); + availabilityRestrictions = builder.availabilityRestrictions; + + if (builder.serviceClassName == null) { + throw new NullPointerException("serviceClassName must be specified"); + } + serviceClassName = builder.serviceClassName; + primaryProfileType = builder.primaryProfileType; + } + + @Override + public void startConnecting() { + if (VERSION.SDK_INT < VERSION_CODES.O) { + return; + } + crossProfileSender().startManuallyBinding(); + } + + @Override + public void connect() throws UnavailableProfileException { + if (VERSION.SDK_INT < VERSION_CODES.O) { + throw new UnavailableProfileException( + "Cross-profile calls are not supported on this version of Android"); + } + crossProfileSender().manuallyBind(); + } + + @Override + public void stopManualConnectionManagement() { + if (VERSION.SDK_INT < VERSION_CODES.O) { + return; + } + crossProfileSender().stopManualConnectionManagement(); + } + + @Override + public CrossProfileSender crossProfileSender() { + if (VERSION.SDK_INT < VERSION_CODES.O) { + return null; + } + if (crossProfileSender == null) { + crossProfileSender = + new CrossProfileSender( + context.getApplicationContext(), + serviceClassName, + binder, + /* connectionListener= */ this, + /* availabilityListener= */ this, + scheduledExecutorService, + availabilityRestrictions); + crossProfileSender.beginMonitoringAvailabilityChanges(); + } + return crossProfileSender; + } + + @Override + public void registerConnectionListener(ConnectionListener listener) { + connectionListeners.add(listener); + } + + @Override + public void unregisterConnectionListener(ConnectionListener listener) { + connectionListeners.remove(listener); + } + + private void notifyConnectionChange() { + for (ConnectionListener listener : connectionListeners) { + listener.connectionChanged(); + } + } + + @Override + public void connectionChanged() { + notifyConnectionChange(); + } + + @Override + public void registerAvailabilityListener(AvailabilityListener listener) { + availabilityListeners.add(listener); + } + + @Override + public void unregisterAvailabilityListener(AvailabilityListener listener) { + availabilityListeners.remove(listener); + } + + private void notifyAvailabilityChange() { + for (AvailabilityListener listener : availabilityListeners) { + listener.availabilityChanged(); + } + } + + @Override + public void availabilityChanged() { + notifyAvailabilityChange(); + } + + @Override + public boolean isAvailable() { + if (VERSION.SDK_INT < VERSION_CODES.O) { + return false; + } + return crossProfileSender().isBindingPossible(); + } + + @Override + public boolean isConnected() { + if (VERSION.SDK_INT < VERSION_CODES.O) { + return false; + } + return crossProfileSender().isBound(); + } + + @Override + public ConnectedAppsUtils utils() { + if (VERSION.SDK_INT < VERSION_CODES.O) { + return new ConnectedAppsUtilsImpl(context); + } + return new ConnectedAppsUtilsImpl(context, getPrimaryProfileIdentifier()); + } + + @Override + public Permissions permissions() { + return new PermissionsImpl(context, binder); + } + + @Nullable + private Profile getPrimaryProfileIdentifier() { + if (Objects.equals(primaryProfileType, ProfileType.WORK)) { + return new ConnectedAppsUtilsImpl(context).getWorkProfile(); + } + + if (Objects.equals(primaryProfileType, ProfileType.PERSONAL)) { + return new ConnectedAppsUtilsImpl(context).getPersonalProfile(); + } + + return null; + } + + @Override + public Context applicationContext() { + return context; + } + + @Override + public boolean isManuallyManagingConnection() { + return crossProfileSender().isManuallyManagingConnection(); + } + + /** A builder for an {@link AbstractProfileConnector}. */ + public static final class Builder { + @Nullable ScheduledExecutorService scheduledExecutorService; + @Nullable ConnectionBinder binder; + @Nullable ProfileType primaryProfileType; + @Nullable AvailabilityRestrictions availabilityRestrictions; + Context context; + String serviceClassName; + + public Builder setContext(Context context) { + this.context = context; + return this; + } + + public Builder setScheduledExecutorService(ScheduledExecutorService scheduledExecutorService) { + this.scheduledExecutorService = scheduledExecutorService; + return this; + } + + public Builder setBinder(ConnectionBinder binder) { + this.binder = binder; + return this; + } + + public Builder setServiceClassName(String serviceClassName) { + this.serviceClassName = serviceClassName; + return this; + } + + public Builder setPrimaryProfileType(ProfileType primaryProfileType) { + this.primaryProfileType = primaryProfileType; + return this; + } + + public Builder setAvailabilityRestrictions(AvailabilityRestrictions availabilityRestrictions) { + this.availabilityRestrictions = availabilityRestrictions; + return this; + } + } +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/AbstractUserConnector.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/AbstractUserConnector.java new file mode 100644 index 0000000..fa07fe2 --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/AbstractUserConnector.java @@ -0,0 +1,131 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import android.content.Context; +import android.os.UserHandle; +import com.google.android.enterprise.connectedapps.annotations.AvailabilityRestrictions; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import java.util.concurrent.ScheduledExecutorService; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** Standard implementation of {@link UserConnector}. */ +public abstract class AbstractUserConnector + implements UserConnector, ConnectionListener, AvailabilityListener { + + public AbstractUserConnector(Class<? extends UserConnector> userConnectorClass, Builder builder) { + if (userConnectorClass == null || builder == null || builder.context == null) { + throw new NullPointerException(); + } + } + + @Override + public void availabilityChanged() {} + + @Override + public void connectionChanged() {} + + @Override + public void startConnecting(UserHandle userHandle) {} + + @Override + public void connect(UserHandle userHandle) throws UnavailableProfileException {} + + @Override + public void stopManualConnectionManagement(UserHandle userHandle) {} + + @Override + public CrossProfileSender crossProfileSender(UserHandle userHandle) { + return null; + } + + @Override + public void registerConnectionListener(UserHandle userHandle, ConnectionListener listener) {} + + @Override + public void unregisterConnectionListener(UserHandle userHandle, ConnectionListener listener) {} + + @Override + public void registerAvailabilityListener(UserHandle userHandle, AvailabilityListener listener) {} + + @Override + public void unregisterAvailabilityListener( + UserHandle userHandle, AvailabilityListener listener) {} + + @Override + public boolean isAvailable(UserHandle userHandle) { + return false; + } + + @Override + public boolean isConnected(UserHandle userHandle) { + return false; + } + + @Override + public ConnectedAppsUtils utils(UserHandle userHandle) { + return null; + } + + @Override + public Permissions permissions(UserHandle userHandle) { + return null; + } + + @Override + public Context applicationContext(UserHandle userHandle) { + return null; + } + + @Override + public boolean isManuallyManagingConnection(UserHandle userHandle) { + return false; + } + + /** A builder for an {@link AbstractUserConnector}. */ + public static final class Builder { + @Nullable ScheduledExecutorService scheduledExecutorService; + @Nullable ConnectionBinder binder; + @Nullable AvailabilityRestrictions availabilityRestrictions; + Context context; + String serviceClassName; + + public Builder setContext(Context context) { + this.context = context; + return this; + } + + public Builder setScheduledExecutorService(ScheduledExecutorService scheduledExecutorService) { + this.scheduledExecutorService = scheduledExecutorService; + return this; + } + + public Builder setBinder(ConnectionBinder binder) { + this.binder = binder; + return this; + } + + public Builder setServiceClassName(String serviceClassName) { + this.serviceClassName = serviceClassName; + return this; + } + + public Builder setAvailabilityRestrictions(AvailabilityRestrictions availabilityRestrictions) { + this.availabilityRestrictions = availabilityRestrictions; + return this; + } + } +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/AvailabilityListener.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/AvailabilityListener.java new file mode 100644 index 0000000..7a5e05f --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/AvailabilityListener.java @@ -0,0 +1,21 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +/** Interface with method to be called when a profile has become available or unavailable. */ +public interface AvailabilityListener { + void availabilityChanged(); +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/ConnectedAppsUtils.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/ConnectedAppsUtils.java new file mode 100644 index 0000000..cc99d47 --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/ConnectedAppsUtils.java @@ -0,0 +1,87 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** Utility methods for dealing with profile awareness. */ +public interface ConnectedAppsUtils { + /** + * Get the identifier of the current profile. + * + * <p>For use with {@code profile(int profileIdentifier) calls. + * + * <p>These values may change between runs of your app and should not be persisted. + */ + Profile getCurrentProfile(); + + /** + * Get the identifier of the other profile. + * + * <p>For use with {@code profile(int profileIdentifier) calls. + * + * <p>These values may change between runs of your app and should not be persisted. + */ + Profile getOtherProfile(); + + /** + * Get the identifier of the primary profile. + * + * <p>For use with {@code profile(int profileIdentifier) calls. + * + * <p>These values may change between runs of your app and should not be persisted. + * + * @throws IllegalStateException if the used connector has not set a primary profile. + */ + @Nullable + Profile getPrimaryProfile(); + + /** + * Get the identifier of the primary profile. + * + * <p>For use with {@code profile(int profileIdentifier) calls. + * + * <p>These values may change between runs of your app and should not be persisted. + * + * @throws IllegalStateException if the used connector has not set a primary profile. + */ + @Nullable + Profile getSecondaryProfile(); + + /** + * Get the identifier of the work profile. + * + * <p>For use with {@code profile(int profileIdentifier) calls. + * + * <p>These values may change between runs of your app and should not be persisted. + */ + Profile getWorkProfile(); + + /** + * Get the identifier of the personal profile. + * + * <p>For use with {@code profile(int profileIdentifier) calls. + * + * <p>These values may change between runs of your app and should not be persisted. + */ + Profile getPersonalProfile(); + + /** Return true if the current profile is the personal profile. */ + boolean runningOnPersonal(); + + /** Return true if the current profile is the work profile. */ + boolean runningOnWork(); +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/ConnectedAppsUtilsImpl.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/ConnectedAppsUtilsImpl.java new file mode 100644 index 0000000..63febbf --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/ConnectedAppsUtilsImpl.java @@ -0,0 +1,114 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import android.content.Context; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** Default implementation of {@link ConnectedAppsUtils}. */ +class ConnectedAppsUtilsImpl implements ConnectedAppsUtils { + + private static final Profile CURRENT_PROFILE_IDENTIFIER = Profile.fromInt(0); + private static final Profile OTHER_PROFILE_IDENTIFIER = Profile.fromInt(1); + + private final Context context; + @Nullable private final Profile primaryProfileIdentifier; + + ConnectedAppsUtilsImpl(Context context) { + this(context, null); + } + + ConnectedAppsUtilsImpl(Context context, Profile primaryProfileIdentifier) { + if (context == null) { + throw new NullPointerException(); + } + this.context = context; + this.primaryProfileIdentifier = primaryProfileIdentifier; + } + + @Override + public Profile getCurrentProfile() { + return CURRENT_PROFILE_IDENTIFIER; + } + + @Override + public Profile getOtherProfile() { + return OTHER_PROFILE_IDENTIFIER; + } + + @Override + @Nullable + public Profile getPrimaryProfile() { + if (VERSION.SDK_INT < VERSION_CODES.O) { + return null; + } + return primaryProfileIdentifier; + } + + @Override + @Nullable + public Profile getSecondaryProfile() { + if (VERSION.SDK_INT < VERSION_CODES.O) { + return null; + } + if (primaryProfileIdentifier == null) { + return null; + } + return primaryProfileIdentifier.isCurrent() + ? OTHER_PROFILE_IDENTIFIER + : CURRENT_PROFILE_IDENTIFIER; + } + + @Override + public Profile getWorkProfile() { + if (VERSION.SDK_INT < VERSION_CODES.O) { + return getCurrentProfile(); + } + if (runningOnWork()) { + return getCurrentProfile(); + } + return getOtherProfile(); + } + + @Override + public Profile getPersonalProfile() { + if (VERSION.SDK_INT < VERSION_CODES.O) { + return getCurrentProfile(); + } + if (runningOnPersonal()) { + return getCurrentProfile(); + } + return getOtherProfile(); + } + + @Override + public boolean runningOnWork() { + if (VERSION.SDK_INT < VERSION_CODES.O) { + return false; + } + return CrossProfileSDKUtilities.isRunningOnWorkProfile(context); + } + + @Override + public boolean runningOnPersonal() { + if (VERSION.SDK_INT < VERSION_CODES.O) { + return false; + } + return CrossProfileSDKUtilities.isRunningOnPersonalProfile(context); + } +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/ConnectionBinder.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/ConnectionBinder.java new file mode 100644 index 0000000..0d793f4 --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/ConnectionBinder.java @@ -0,0 +1,55 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import android.content.ComponentName; +import android.content.Context; +import android.content.ServiceConnection; +import com.google.android.enterprise.connectedapps.annotations.AvailabilityRestrictions; +import com.google.android.enterprise.connectedapps.exceptions.MissingApiException; + +/** {@link ConnectionBinder} instances are used to establish bindings with other profiles. */ +public interface ConnectionBinder { + + /** + * Try to bind to the given {@link ComponentName} with the given {@link ServiceConnection}. + * + * <p>{@link AvailabilityRestrictions} should be enforced. + * + * <p>This should not be called if {@link #hasPermissionToBind(Context)} returns {@code False} or + * {@link #bindingIsPossible(Context, AvailabilityRestrictions)} returns {@code False}. + */ + boolean tryBind( + Context context, + ComponentName bindToService, + ServiceConnection connection, + AvailabilityRestrictions availabilityRestrictions) + throws MissingApiException; + + /** + * Return true if there is a profile available to bind to, while enforcing the passed in {@link + * AvailabilityRestrictions}. + * + * <p>This should not be called if {@link #hasPermissionToBind(Context)} returns {@code False}. + */ + boolean bindingIsPossible(Context context, AvailabilityRestrictions availabilityRestrictions); + + /** + * Return true if the permissions required for {@link #tryBind(Context, ComponentName, + * ServiceConnection, AvailabilityRestrictions)} are granted. + */ + boolean hasPermissionToBind(Context context); +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/ConnectionListener.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/ConnectionListener.java new file mode 100644 index 0000000..7eb0e29 --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/ConnectionListener.java @@ -0,0 +1,21 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +/** Interface with method to be called when a profile has connected or disconnected. */ +public interface ConnectionListener { + void connectionChanged(); +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/CrossProfileConnector.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/CrossProfileConnector.java new file mode 100644 index 0000000..b534152 --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/CrossProfileConnector.java @@ -0,0 +1,84 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import android.content.Context; +import com.google.android.enterprise.connectedapps.annotations.AvailabilityRestrictions; +import com.google.android.enterprise.connectedapps.annotations.CrossProfile; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector.ProfileType; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +/** + * The default {@link ProfileConnector} used if none is specified in a {@link CrossProfile} type. + */ +@CustomProfileConnector(primaryProfile = ProfileType.UNKNOWN) +public interface CrossProfileConnector extends ProfileConnector { + /** Builder for {@link CrossProfileConnector} instances. */ + final class Builder { + + private Builder(Context context) { + implBuilder.setContext(context); + } + + private final AbstractProfileConnector.Builder implBuilder = + new AbstractProfileConnector.Builder() + .setServiceClassName( + "com.google.android.enterprise.connectedapps.CrossProfileConnector_Service") + .setAvailabilityRestrictions(AvailabilityRestrictions.DEFAULT); + + /** + * Use an alternative {@link ScheduledExecutorService}. + * + * <p>Defaults to {@link Executors#newSingleThreadScheduledExecutor()}. + */ + public Builder setScheduledExecutorService(ScheduledExecutorService scheduledExecutorService) { + implBuilder.setScheduledExecutorService(scheduledExecutorService); + return this; + } + + /** + * Specify an alternative {@link ConnectionBinder} for managing the connection. + * + * <p>Defaults to {@link DefaultProfileBinder}. + */ + public Builder setBinder(ConnectionBinder binder) { + implBuilder.setBinder(binder); + return this; + } + + /** + * Specify which set of restrictions should be applied to checking availability. + * + * <p>Defaults to {@link AvailabilityRestrictions#DEFAULT}, which requires that a user be + * running, unlocked, and not in quiet mode + */ + public Builder setAvailabilityRestrictions(AvailabilityRestrictions availabilityRestrictions) { + implBuilder.setAvailabilityRestrictions(availabilityRestrictions); + return this; + } + + /** Instantiate the {@link CrossProfileConnector} for the given settings. */ + public CrossProfileConnector build() { + return new CrossProfileConnectorImpl(implBuilder); + } + } + + static Builder builder(Context context) { + return new Builder(context); + } +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/CrossProfileConnectorImpl.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/CrossProfileConnectorImpl.java new file mode 100644 index 0000000..b9ac025 --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/CrossProfileConnectorImpl.java @@ -0,0 +1,23 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +final class CrossProfileConnectorImpl extends AbstractProfileConnector + implements CrossProfileConnector { + CrossProfileConnectorImpl(AbstractProfileConnector.Builder builder) { + super(CrossProfileConnector.class, builder); + } +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/CrossProfileSDKUtilities.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/CrossProfileSDKUtilities.java new file mode 100644 index 0000000..40cf326 --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/CrossProfileSDKUtilities.java @@ -0,0 +1,132 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.os.UserHandle; +import android.os.UserManager; +import com.google.android.enterprise.connectedapps.annotations.AvailabilityRestrictions; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** Utility methods for acting on profiles. These methods should only be used by the SDK. */ +class CrossProfileSDKUtilities { + private static boolean isRunningOnWorkProfileCached = false; + private static boolean isRunningOnWorkProfile = false; + + static boolean isRunningOnWorkProfile(Context context) { + if (!isRunningOnWorkProfileCached) { + calculateIsRunningOnWorkProfile(context); + } + return isRunningOnWorkProfile; + } + + /** + * Set the {@code isRunningOnWorkProfile} field and return whether or not we can cache this value. + */ + private static void calculateIsRunningOnWorkProfile(Context context) { + UserManager userManager = context.getSystemService(UserManager.class); + isRunningOnWorkProfileCached = true; // By default we cache the result of this calculation + + if (VERSION.SDK_INT >= VERSION_CODES.R) { + isRunningOnWorkProfile = userManager.isManagedProfile(); + return; + } + if (userManager.getUserProfiles().size() < 2) { + // This accounts for situations where a personal profile has management. + isRunningOnWorkProfile = false; + // we can't cache it as this case is also entered if we are on the work profile but it's not + // fully configured + isRunningOnWorkProfileCached = false; + return; + } + + DevicePolicyManager devicePolicyManager = context.getSystemService(DevicePolicyManager.class); + PackageManager packageManager = context.getPackageManager(); + + isRunningOnWorkProfile = false; + for (PackageInfo pkg : packageManager.getInstalledPackages(/* flags= */ 0)) { + if (devicePolicyManager.isProfileOwnerApp(pkg.packageName)) { + isRunningOnWorkProfile = true; + return; + } + } + } + + static boolean isRunningOnPersonalProfile(Context context) { + return !isRunningOnWorkProfile(context); + } + + /** + * Deterministically select the user to bind to. + * + * <p>This will ensure that on a device with multiple profiles, we bind to the same one + * consistently. + */ + @Nullable + static UserHandle selectUserHandleToBind(Context context, List<UserHandle> userHandles) { + if (userHandles.isEmpty()) { + return null; + } + + UserManager userManager = context.getSystemService(UserManager.class); + + return Collections.min( + userHandles, + (o1, o2) -> + (int) + (userManager.getSerialNumberForUser(o1) - userManager.getSerialNumberForUser(o2))); + } + + /** Filter out users according to the passed {@link AvailabilityRestrictions}. */ + static List<UserHandle> filterUsersByAvailabilityRestrictions( + Context context, + List<UserHandle> userHandles, + AvailabilityRestrictions availabilityRestrictions) { + List<UserHandle> filteredUserHandles = new ArrayList<>(); + UserManager userManager = context.getSystemService(UserManager.class); + + for (UserHandle userHandle : userHandles) { + if (!userManager.isUserRunning(userHandle)) { + continue; + } + if (userManager.isQuietModeEnabled(userHandle)) { + continue; + } + if (availabilityRestrictions.requireUnlocked && !userManager.isUserUnlocked(userHandle)) { + continue; + } + + filteredUserHandles.add(userHandle); + } + + return filteredUserHandles; + } + + /** Should only be used during tests where the profile state may change during a single run. */ + static void clearCache() { + isRunningOnWorkProfileCached = false; + } + + private CrossProfileSDKUtilities() {} +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/CrossProfileSender.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/CrossProfileSender.java new file mode 100644 index 0000000..fe34d09 --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/CrossProfileSender.java @@ -0,0 +1,825 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import static com.google.android.enterprise.connectedapps.CrossProfileSDKUtilities.filterUsersByAvailabilityRestrictions; +import static com.google.android.enterprise.connectedapps.CrossProfileSDKUtilities.selectUserHandleToBind; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.content.pm.CrossProfileApps; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import android.os.Looper; +import android.os.Parcel; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.Log; +import com.google.android.enterprise.connectedapps.annotations.AvailabilityRestrictions; +import com.google.android.enterprise.connectedapps.exceptions.MissingApiException; +import com.google.android.enterprise.connectedapps.exceptions.ProfileRuntimeException; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.internal.CrossProfileParcelCallSender; +import com.google.android.enterprise.connectedapps.internal.ParcelCallReceiver; +import com.google.android.enterprise.connectedapps.internal.ParcelUtilities; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** This class is used internally by the Connected Apps SDK to send messages cross-profile. */ +public class CrossProfileSender { + + private static final class CrossProfileCall { + private final long crossProfileTypeIdentifier; + private final int methodIdentifier; + private final Parcel params; + private final LocalCallback callback; + private final long timeoutMillis; + + CrossProfileCall( + long crossProfileTypeIdentifier, + int methodIdentifier, + Parcel params, + LocalCallback callback, + long timeoutMillis) { + if (params == null || callback == null) { + throw new NullPointerException(); + } + this.crossProfileTypeIdentifier = crossProfileTypeIdentifier; + this.methodIdentifier = methodIdentifier; + this.params = params; + this.callback = callback; + this.timeoutMillis = timeoutMillis; + } + + void recycle() { + params.recycle(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CrossProfileCall that = (CrossProfileCall) o; + return crossProfileTypeIdentifier == that.crossProfileTypeIdentifier + && methodIdentifier == that.methodIdentifier + && params.equals(that.params) + && callback.equals(that.callback) + && timeoutMillis == that.timeoutMillis; + } + + @Override + public int hashCode() { + return Objects.hash( + crossProfileTypeIdentifier, methodIdentifier, params, callback, timeoutMillis); + } + } + + private static final class OngoingCrossProfileCall extends ICrossProfileCallback.Stub { + + private final CrossProfileSender sender; + private final LocalCallback originalCallback; + private final AtomicBoolean complete = new AtomicBoolean(false); + private ScheduledFuture<?> timeoutFuture; + private final long timeoutMillis; + private final ParcelCallReceiver parcelCallReceiver = new ParcelCallReceiver(); + + private OngoingCrossProfileCall( + CrossProfileSender sender, LocalCallback originalCallback, long timeoutMillis) { + if (sender == null || originalCallback == null) { + throw new NullPointerException(); + } + this.sender = sender; + this.originalCallback = originalCallback; + this.timeoutMillis = timeoutMillis; + } + + void scheduleTimeout(ScheduledExecutorService timeoutExecutor) { + if (this.timeoutFuture != null) { + throw new IllegalStateException("Each call can only have a single timeout scheduled."); + } + if (complete.get()) { + return; + } + this.timeoutFuture = + timeoutExecutor.schedule(this::onTimeout, timeoutMillis, TimeUnit.MILLISECONDS); + } + + private void onTimeout() { + if (complete.get()) { + return; + } + Parcel throwableParcel = + createThrowableParcel( + new UnavailableProfileException( + "The call timed out after " + timeoutMillis + " milliseconds")); + + onException(throwableParcel); + throwableParcel.recycle(); + } + + @Override + public void prepareResult(long callId, int blockId, int numBytes, byte[] params) { + parcelCallReceiver.prepareCall(callId, blockId, numBytes, params); + } + + @Override + public void onResult(long callId, int blockId, int methodIdentifier, byte[] paramsBytes) { + if (complete.getAndSet(true)) { + return; + } + if (timeoutFuture != null) { + timeoutFuture.cancel(/* mayInterruptIfRunning= */ true); + } + sender.ongoingCallComplete(this); + + Parcel parcel = parcelCallReceiver.getPreparedCall(callId, blockId, paramsBytes); + + originalCallback.onResult(methodIdentifier, parcel); + parcel.recycle(); + + sender.maybeScheduleAutomaticDisconnection(); + } + + @Override + public void onException(long callId, int blockId, byte[] paramsBytes) { + Parcel parcel = parcelCallReceiver.getPreparedCall(callId, blockId, paramsBytes); + + onException(parcel); + + parcel.recycle(); + } + + public void onException(Parcel exception) { + if (complete.getAndSet(true)) { + return; + } + if (timeoutFuture != null) { + timeoutFuture.cancel(/* mayInterruptIfRunning= */ true); + } + sender.ongoingCallComplete(this); + + originalCallback.onException(exception); + + sender.maybeScheduleAutomaticDisconnection(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + OngoingCrossProfileCall that = (OngoingCrossProfileCall) o; + return sender.equals(that.sender) + && originalCallback.equals(that.originalCallback) + && complete.equals(that.complete); + } + + @Override + public int hashCode() { + return Objects.hash(sender, originalCallback, complete); + } + } + + private void ongoingCallComplete(OngoingCrossProfileCall call) { + ongoingCrossProfileCalls.removeFirstOccurrence(call); + } + + public static final int MAX_BYTES_PER_BLOCK = 250000; + + private static final String LOG_TAG = "CrossProfileSender"; + private static final long INITIAL_BIND_RETRY_DELAY_MS = 500; + private static final int DEFAULT_AUTOMATIC_DISCONNECTION_TIMEOUT_SECONDS = 30; + + private final ScheduledExecutorService scheduledExecutorService; + private final Context context; + private final ComponentName bindToService; + private boolean canUseReflectedApis; + private long bindRetryDelayMs = 500; + private AtomicBoolean isBinding = new AtomicBoolean(false); + private final AtomicReference<ICrossProfileService> iCrossProfileService = + new AtomicReference<>(); + private final ConnectionListener connectionListener; + private final AvailabilityListener availabilityListener; + private final ConnectionBinder binder; + @Nullable private volatile ScheduledFuture<Void> automaticDisconnectionFuture; + private final AvailabilityRestrictions availabilityRestrictions; + + private boolean isManuallyManagingConnection = false; + private ConcurrentLinkedDeque<OngoingCrossProfileCall> ongoingCrossProfileCalls = + new ConcurrentLinkedDeque<>(); + private ConcurrentLinkedDeque<CrossProfileCall> asyncCallQueue = new ConcurrentLinkedDeque<>(); + + private static final int NONE = 0; + private static final int UNAVAILABLE = 1; + private static final int AVAILABLE = 2; + private static final int DISCONNECTED = UNAVAILABLE; + private static final int CONNECTED = AVAILABLE; + + private ScheduledFuture<?> scheduledTryBind; + + private int lastReportedAvailabilityStatus = NONE; + private int lastReportedConnectedStatus = NONE; + + CrossProfileSender( + Context context, + String connectedAppsServiceClassName, + ConnectionBinder binder, + ConnectionListener connectionListener, + AvailabilityListener availabilityListener, + ScheduledExecutorService scheduledExecutorService, + AvailabilityRestrictions availabilityRestrictions) { + this.context = context.getApplicationContext(); + if (connectionListener == null + || availabilityListener == null + || availabilityRestrictions == null + || binder == null + || scheduledExecutorService == null) { + throw new NullPointerException(); + } + this.binder = binder; + this.connectionListener = connectionListener; + this.availabilityListener = availabilityListener; + bindToService = new ComponentName(context.getPackageName(), connectedAppsServiceClassName); + canUseReflectedApis = ReflectionUtilities.canUseReflectedApis(); + this.scheduledExecutorService = scheduledExecutorService; + this.availabilityRestrictions = availabilityRestrictions; + } + + private final BroadcastReceiver profileAvailabilityReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + checkAvailability(); + } + }; + + private final ServiceConnection connection = + new ServiceConnection() { + // Called when the connection with the service is established + @Override + public void onServiceConnected(ComponentName className, IBinder service) { + scheduledExecutorService.execute( + () -> { + if (!isBinding.get()) { + unbind(); + return; + } + iCrossProfileService.set(ICrossProfileService.Stub.asInterface(service)); + + tryMakeAsyncCalls(); + checkConnected(); + onBindingAttemptSucceeded(); + }); + } + + // Called when the connection with the service disconnects unexpectedly + @Override + public void onServiceDisconnected(ComponentName className) { + scheduledExecutorService.execute( + () -> { + Log.e(LOG_TAG, "Unexpected disconnection"); + if (!asyncCallQueue.isEmpty() || !ongoingCrossProfileCalls.isEmpty()) { + Log.d(LOG_TAG, "Found in progress calls"); + throwExceptionForAsyncCalls( + new UnavailableProfileException("Lost connection to other profile")); + // These disconnections can be temporary - so to avoid an exception on an async + // call leading to bad user experience - we send the availability update again + // to prompt a retry/refresh + updateAvailability(); + } + iCrossProfileService.set(null); + checkConnected(); + cancelAutomaticDisconnection(); + startTryBinding(); + }); + } + }; + + private final Object automaticDisconnectionFutureLock = new Object(); + + private void cancelAutomaticDisconnection() { + if (automaticDisconnectionFuture != null) { + synchronized (automaticDisconnectionFutureLock) { + if (automaticDisconnectionFuture != null) { + automaticDisconnectionFuture.cancel(/* mayInterruptIfRunning= */ true); + automaticDisconnectionFuture = null; + } + } + } + } + + private void maybeScheduleAutomaticDisconnection() { + if (!isManuallyManagingConnection + && asyncCallQueue.isEmpty() + && ongoingCrossProfileCalls.isEmpty() + && isBound() + && automaticDisconnectionFuture == null) { + synchronized (automaticDisconnectionFutureLock) { + if (automaticDisconnectionFuture == null) { + automaticDisconnectionFuture = + scheduledExecutorService.schedule( + this::automaticallyDisconnect, + DEFAULT_AUTOMATIC_DISCONNECTION_TIMEOUT_SECONDS, + TimeUnit.SECONDS); + } + } + } + } + + private Void automaticallyDisconnect() { + if (!isManuallyManagingConnection + && asyncCallQueue.isEmpty() + && ongoingCrossProfileCalls.isEmpty() + && isBound()) { + unbind(); + } + return null; + } + + void beginMonitoringAvailabilityChanges() { + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNLOCKED); + filter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE); + filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE); + context.registerReceiver(profileAvailabilityReceiver, filter); + } + + private volatile CountDownLatch manuallyBindLatch; + + void manuallyBind() throws UnavailableProfileException { + Log.e(LOG_TAG, "Calling manuallyBind"); + if (isRunningOnUIThread()) { + throw new IllegalStateException("connect()/manuallyBind() cannot be called from UI thread"); + } + + if (!isBindingPossible()) { + throw new UnavailableProfileException("Profile not available"); + } + + if (!binder.hasPermissionToBind(context)) { + throw new UnavailableProfileException("Permission not granted"); + } + + cancelAutomaticDisconnection(); + isManuallyManagingConnection = true; + + if (isBound()) { + // If we're already bound there's no need to block the thread + return; + } + + if (manuallyBindLatch == null) { + synchronized (this) { + if (manuallyBindLatch == null) { + manuallyBindLatch = new CountDownLatch(1); + } + } + } + + bind(); + + Log.i(LOG_TAG, "Blocking for bind"); + try { + if (manuallyBindLatch != null) { + manuallyBindLatch.await(); + } + } catch (InterruptedException e) { + Log.e(LOG_TAG, "Interrupted waiting for manually bind", e); + } + + if (!isBound()) { + unbind(); // ensure we don't continue trying to connect if we throw an exception + isManuallyManagingConnection = false; + throw new UnavailableProfileException("Profile not available"); + } + } + + private static boolean isRunningOnUIThread() { + return Looper.myLooper() == Looper.getMainLooper(); + } + + /** + * Start trying to bind to the other profile and start manually managing the connection. + * + * <p>This will mean that the connection will not be dropped automatically to save resources. + * + * <p>Must be called before interacting with synchronous cross-profile methods. + */ + void startManuallyBinding() { + cancelAutomaticDisconnection(); + isManuallyManagingConnection = true; + bind(); + } + + /** + * Stop manual connection management. + * + * <p>This can be called after {@link #startManuallyBinding()} or {@link #manuallyBind()} to + * return connection management responsibilities to the SDK. + * + * <p>You should not make any synchronous cross-profile calls after calling this method. + */ + public void stopManualConnectionManagement() { + isManuallyManagingConnection = false; + maybeScheduleAutomaticDisconnection(); + } + + /** + * Attempt to bind to the other profile. + * + * <p>This will continually attempt to form a binding to the other profile in a background thread. + */ + private void bind() { + if (isBinding.getAndSet(true)) { + return; + } + + startTryBinding(); + } + + private void onBindingAttemptSucceeded() { + Log.i(LOG_TAG, "Binding attempt succeeded"); + checkTriggerManualConnectionLock(); + } + + private void onBindingAttemptFailed(String reason) { + onBindingAttemptFailed(reason, /* terminal= */ false); + } + + private void onBindingAttemptFailed(String reason, boolean terminal) { + Log.i(LOG_TAG, "Binding attempt failed: " + reason); + throwExceptionForAsyncCalls(new UnavailableProfileException(reason)); + if (terminal || !isManuallyManagingConnection || manuallyBindLatch != null) { + unbind(); + checkTriggerManualConnectionLock(); + } else { + scheduleBindAttempt(); + } + } + + private void checkTriggerManualConnectionLock() { + if (manuallyBindLatch != null) { + synchronized (this) { + if (manuallyBindLatch != null) { + manuallyBindLatch.countDown(); + manuallyBindLatch = null; + } + } + } + } + + /** + * Stop attempting to bind to the other profile. + * + * <p>If there is already a binding present, it will be killed. + */ + void unbind() { + Log.i(LOG_TAG, "Unbind"); + throwExceptionForAsyncCalls(new UnavailableProfileException("No profile available")); + isBinding.set(false); + if (isBound()) { + context.unbindService(connection); + iCrossProfileService.set(null); + checkConnected(); + cancelAutomaticDisconnection(); + } + checkTriggerManualConnectionLock(); + } + + boolean isBindingPossible() { + return binder.bindingIsPossible(context, availabilityRestrictions); + } + + private void startTryBinding() { + bindRetryDelayMs = INITIAL_BIND_RETRY_DELAY_MS; + scheduledExecutorService.execute(this::tryBind); + } + + private void tryBind() { + Log.i(LOG_TAG, "Attempting to bind"); + + if (scheduledTryBind != null) { + scheduledTryBind.cancel(/* mayInterruptIfRunning= */ false); + scheduledTryBind = null; + } + + if (!canUseReflectedApis) { + onBindingAttemptFailed("Required APIs are unavailable. Binding is not possible."); + return; + } + + if (!isBinding.get()) { + onBindingAttemptFailed("Not trying to bind"); + return; + } + + if (isBound()) { + onBindingAttemptSucceeded(); + return; + } + + if (!binder.hasPermissionToBind(context)) { + onBindingAttemptFailed("Permission not granted"); + return; + } + + if (!isBindingPossible()) { + onBindingAttemptFailed("No profile available"); + return; + } + + try { + if (!binder.tryBind(context, bindToService, connection, availabilityRestrictions)) { + onBindingAttemptFailed("No profile available or app not installed in other profile"); + } + } catch (MissingApiException e) { + Log.e(LOG_TAG, "MissingApiException when trying to bind", e); + onBindingAttemptFailed("Missing API"); + } + } + + private void scheduleBindAttempt() { + if (scheduledTryBind != null && !scheduledTryBind.isDone()) { + return; + } + + bindRetryDelayMs *= 2; + scheduledTryBind = + scheduledExecutorService.schedule(this::tryBind, bindRetryDelayMs, TimeUnit.MILLISECONDS); + } + + boolean isBound() { + return iCrossProfileService.get() != null; + } + + /** + * Make a synchronous cross-profile call. + * + * @return A {@link Parcel} containing the return value. This must be recycled after use. + * @throws UnavailableProfileException if a connection is not already established + */ + public Parcel call(long crossProfileTypeIdentifier, int methodIdentifier, Parcel params) + throws UnavailableProfileException { + try { + return callWithExceptions(crossProfileTypeIdentifier, methodIdentifier, params); + } catch (UnavailableProfileException | RuntimeException e) { + throw e; + } catch (Throwable e) { + throw new UnavailableProfileException("Unexpected checked exception", e); + } + } + + /** + * Make a synchronous cross-profile call which expects some checked exceptions to be thrown. + * + * <p>Behaves the same as {@link #call(long, int, Parcel)} except that it deals with checked + * exceptions by throwing {@link Throwable}. + * + * @return A {@link Parcel} containing the return value. This must be recycled after use. + * @throws UnavailableProfileException if a connection is not already established + */ + public Parcel callWithExceptions( + long crossProfileTypeIdentifier, int methodIdentifier, Parcel params) throws Throwable { + + if (!isBound()) { + throw new UnavailableProfileException("Could not access other profile"); + } + + if (!isManuallyManagingConnection) { + throw new UnavailableProfileException( + "Synchronous calls can only be used when manually connected"); + } + + CrossProfileParcelCallSender callSender = + new CrossProfileParcelCallSender( + iCrossProfileService.get(), + crossProfileTypeIdentifier, + methodIdentifier, + /* callback= */ null); + Parcel parcel = callSender.makeParcelCall(params); // Recycled by caller + boolean hasError = parcel.readInt() == 1; + + if (hasError) { + Throwable t = ParcelUtilities.readThrowableFromParcel(parcel); + if (t instanceof RuntimeException) { + throw new ProfileRuntimeException((RuntimeException) t); + } + throw t; + } + + return parcel; + } + + /** + * Make an asynchronous cross-profile call. + * + * @param params These will be cached and will be recycled after the call is complete. + */ + public void callAsync( + long crossProfileTypeIdentifier, + int methodIdentifier, + Parcel params, + LocalCallback callback, + long timeoutMillis) { + + cancelAutomaticDisconnection(); + + asyncCallQueue.add( + new CrossProfileCall( + crossProfileTypeIdentifier, methodIdentifier, params, callback, timeoutMillis)); + + tryMakeAsyncCalls(); + if (isManuallyManagingConnection) { + if (!isBindingPossible()) { + throwExceptionForAsyncCalls(new UnavailableProfileException("Profile not available")); + } + } else { + bind(); + } + } + + private void throwExceptionForAsyncCalls(Throwable throwable) { + Parcel throwableParcel = createThrowableParcel(throwable); + + while (true) { + CrossProfileCall call = asyncCallQueue.pollFirst(); + if (call == null) { + break; + } + + call.callback.onException(throwableParcel); + throwableParcel.setDataPosition(0); + call.recycle(); + } + + while (true) { + OngoingCrossProfileCall call = ongoingCrossProfileCalls.pollFirst(); + if (call == null) { + break; + } + + call.onException(throwableParcel); + throwableParcel.setDataPosition(0); + } + + throwableParcel.recycle(); + } + + private void tryMakeAsyncCalls() { + if (!isBound()) { + return; + } + + scheduledExecutorService.execute(this::drainAsyncQueue); + } + + private void drainAsyncQueue() { + while (true) { + CrossProfileCall call = asyncCallQueue.pollFirst(); + if (call == null) { + break; + } + OngoingCrossProfileCall ongoingCall = + new OngoingCrossProfileCall(this, call.callback, call.timeoutMillis); + ongoingCrossProfileCalls.add(ongoingCall); + + try { + CrossProfileParcelCallSender callSender = + new CrossProfileParcelCallSender( + iCrossProfileService.get(), + call.crossProfileTypeIdentifier, + call.methodIdentifier, + ongoingCall); + Parcel p = callSender.makeParcelCall(call.params); + + boolean hasError = p.readInt() == 1; + call.recycle(); + + if (hasError) { + RuntimeException exception = + (RuntimeException) ParcelUtilities.readThrowableFromParcel(p); + p.recycle(); + ongoingCrossProfileCalls.remove(ongoingCall); + throw new ProfileRuntimeException(exception); + } + + p.recycle(); + ongoingCall.scheduleTimeout(scheduledExecutorService); + } catch (UnavailableProfileException e) { + ongoingCrossProfileCalls.remove(ongoingCall); + asyncCallQueue.add(call); + return; + } + } + } + + void checkAvailability() { + if (isBindingPossible() && (lastReportedAvailabilityStatus != AVAILABLE)) { + updateAvailability(); + } else if (!isBindingPossible() && (lastReportedAvailabilityStatus != UNAVAILABLE)) { + updateAvailability(); + } + } + + void updateAvailability() { + scheduledExecutorService.execute(availabilityListener::availabilityChanged); + lastReportedAvailabilityStatus = isBindingPossible() ? AVAILABLE : UNAVAILABLE; + } + + void checkConnected() { + if (isBound() && lastReportedConnectedStatus != CONNECTED) { + scheduledExecutorService.execute(connectionListener::connectionChanged); + lastReportedConnectedStatus = CONNECTED; + } else if (!isBound() && lastReportedConnectedStatus != DISCONNECTED) { + scheduledExecutorService.execute(connectionListener::connectionChanged); + lastReportedConnectedStatus = DISCONNECTED; + } + } + + boolean isManuallyManagingConnection() { + return isManuallyManagingConnection; + } + + /** + * Create a {@link Parcel} containing a {@link Throwable}. + * + * <p>The {@link Parcel} must be recycled after use. + */ + private static Parcel createThrowableParcel(Throwable throwable) { + Parcel parcel = Parcel.obtain(); // Recycled by caller + ParcelUtilities.writeThrowableToParcel(parcel, throwable); + parcel.setDataPosition(0); + return parcel; + } + + @Nullable + static UserHandle getOtherUserHandle( + Context context, AvailabilityRestrictions availabilityRestrictions) { + if (VERSION.SDK_INT < VERSION_CODES.P) { + // CrossProfileApps was introduced in P + return findDifferentRunningUser( + context, android.os.Process.myUserHandle(), availabilityRestrictions); + } + + CrossProfileApps crossProfileApps = context.getSystemService(CrossProfileApps.class); + List<UserHandle> otherUsers = + filterUsersByAvailabilityRestrictions( + context, crossProfileApps.getTargetUserProfiles(), availabilityRestrictions); + + return selectUserHandleToBind(context, otherUsers); + } + + @Nullable + private static UserHandle findDifferentRunningUser( + Context context, + UserHandle ignoreUserHandle, + AvailabilityRestrictions availabilityRestrictions) { + UserManager userManager = context.getSystemService(UserManager.class); + List<UserHandle> otherUsers = new ArrayList<>(); + + for (UserHandle userHandle : userManager.getUserProfiles()) { + if (!userHandle.equals(ignoreUserHandle)) { + otherUsers.add(userHandle); + } + } + + otherUsers = + filterUsersByAvailabilityRestrictions(context, otherUsers, availabilityRestrictions); + + return selectUserHandleToBind(context, otherUsers); + } +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/CrossUserConnector.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/CrossUserConnector.java new file mode 100644 index 0000000..2e7dc66 --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/CrossUserConnector.java @@ -0,0 +1,81 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import android.content.Context; +import com.google.android.enterprise.connectedapps.annotations.AvailabilityRestrictions; +import com.google.android.enterprise.connectedapps.annotations.CrossUser; +import com.google.android.enterprise.connectedapps.annotations.CustomUserConnector; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +/** The default {@link UserConnector} used if none is specified in a {@link CrossUser} type. */ +@CustomUserConnector +public interface CrossUserConnector extends UserConnector { + /** Builder for {@link CrossUserConnector} instances. */ + final class Builder { + + private Builder(Context context) { + implBuilder.setContext(context); + } + + private final AbstractUserConnector.Builder implBuilder = + new AbstractUserConnector.Builder() + .setServiceClassName( + "com.google.android.enterprise.connectedapps.CrossUserConnector_Service") + .setAvailabilityRestrictions(AvailabilityRestrictions.DEFAULT); + + /** + * Use an alternative {@link ScheduledExecutorService}. + * + * <p>Defaults to {@link Executors#newSingleThreadScheduledExecutor()}. + */ + public Builder setScheduledExecutorService(ScheduledExecutorService scheduledExecutorService) { + implBuilder.setScheduledExecutorService(scheduledExecutorService); + return this; + } + + /** + * Specify an alternative {@link ConnectionBinder} for managing the connection. + * + * <p>Defaults to {@link DefaultProfileBinder}. + */ + public Builder setBinder(ConnectionBinder binder) { + implBuilder.setBinder(binder); + return this; + } + + /** + * Specify which set of restrictions should be applied to checking availability. + * + * <p>Defaults to {@link AvailabilityRestrictions#DEFAULT}, which requires that a user be + * running, unlocked, and not in quiet mode + */ + public Builder setAvailabilityRestrictions(AvailabilityRestrictions availabilityRestrictions) { + implBuilder.setAvailabilityRestrictions(availabilityRestrictions); + return this; + } + + /** Instantiate the {@link CrossProfileConnector} for the given settings. */ + public CrossUserConnector build() { + return new CrossUserConnectorImpl(implBuilder); + } + } + + static Builder builder(Context context) { + return new Builder(context); + } +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/CrossUserConnectorImpl.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/CrossUserConnectorImpl.java new file mode 100644 index 0000000..0f56af8 --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/CrossUserConnectorImpl.java @@ -0,0 +1,22 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +final class CrossUserConnectorImpl extends AbstractUserConnector implements CrossUserConnector { + CrossUserConnectorImpl(AbstractUserConnector.Builder builder) { + super(CrossUserConnector.class, builder); + } +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/DefaultProfileBinder.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/DefaultProfileBinder.java new file mode 100644 index 0000000..a976f94 --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/DefaultProfileBinder.java @@ -0,0 +1,37 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; + +/** + * The {@link ConnectionBinder} used by default. + * + * <p>Methods expect that the app has INTERACT_ACROSS_USERS or INTERACT_ACROSS_PROFILES permission. + */ +class DefaultProfileBinder extends AbstractProfileBinder { + + @Override + protected Intent createIntent(Context context, ComponentName bindToService) { + Intent bindIntent = new Intent(); + bindIntent.setComponent(bindToService); + return bindIntent; + } + +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/DpcProfileBinder.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/DpcProfileBinder.java new file mode 100644 index 0000000..b723efb --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/DpcProfileBinder.java @@ -0,0 +1,95 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import static com.google.android.enterprise.connectedapps.CrossProfileSDKUtilities.filterUsersByAvailabilityRestrictions; +import static com.google.android.enterprise.connectedapps.CrossProfileSDKUtilities.selectUserHandleToBind; + +import android.app.admin.DevicePolicyManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.UserHandle; +import com.google.android.enterprise.connectedapps.annotations.AvailabilityRestrictions; +import java.util.List; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** A {@link ConnectionBinder} used by Device Policy Controllers. */ +public class DpcProfileBinder implements ConnectionBinder { + + private final ComponentName deviceAdminReceiver; + + public DpcProfileBinder(ComponentName deviceAdminReceiver) { + if (deviceAdminReceiver == null) { + throw new NullPointerException(); + } + this.deviceAdminReceiver = deviceAdminReceiver; + } + + @Override + public boolean tryBind( + Context context, + ComponentName bindToService, + ServiceConnection connection, + AvailabilityRestrictions availabilityRestrictions) { + DevicePolicyManager devicePolicyManager = context.getSystemService(DevicePolicyManager.class); + + UserHandle otherUserHandle = + getRunningBindDeviceAdminTargetUser(context, availabilityRestrictions); + + if (otherUserHandle == null) { + return false; + } + + Intent bindIntent = new Intent(); + bindIntent.setComponent(bindToService); + boolean hasBound = + devicePolicyManager.bindDeviceAdminServiceAsUser( + deviceAdminReceiver, bindIntent, connection, Context.BIND_AUTO_CREATE, otherUserHandle); + if (!hasBound) { + context.unbindService(connection); + } + return hasBound; + } + + @Override + public boolean bindingIsPossible( + Context context, AvailabilityRestrictions availabilityRestrictions) { + UserHandle targetUser = getRunningBindDeviceAdminTargetUser(context, availabilityRestrictions); + return targetUser != null; + } + + @Override + public boolean hasPermissionToBind(Context context) { + DevicePolicyManager devicePolicyManager = context.getSystemService(DevicePolicyManager.class); + return !devicePolicyManager.getBindDeviceAdminTargetUsers(deviceAdminReceiver).isEmpty(); + } + + @Nullable + private UserHandle getRunningBindDeviceAdminTargetUser( + Context context, AvailabilityRestrictions availabilityRestrictions) { + DevicePolicyManager devicePolicyManager = context.getSystemService(DevicePolicyManager.class); + + List<UserHandle> userHandles = + filterUsersByAvailabilityRestrictions( + context, + devicePolicyManager.getBindDeviceAdminTargetUsers(deviceAdminReceiver), + availabilityRestrictions); + + return selectUserHandleToBind(context, userHandles); + } +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/ExceptionCallback.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/ExceptionCallback.java new file mode 100644 index 0000000..092c3b5 --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/ExceptionCallback.java @@ -0,0 +1,21 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +/** A callback for receiving {@link Throwable} instances as a result of an asynchronous call. */ +public interface ExceptionCallback { + void onException(Throwable throwable); +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/FutureWrapper.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/FutureWrapper.java new file mode 100644 index 0000000..96d0fd7 --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/FutureWrapper.java @@ -0,0 +1,54 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import android.os.Parcel; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.internal.BundlerType; +import com.google.android.enterprise.connectedapps.internal.ParcelUtilities; + +/** Wrapper for adding support for a future type to the Connected Apps SDK. */ +public abstract class FutureWrapper<E> implements LocalCallback { + private final Bundler bundler; + private final BundlerType bundlerType; + + protected FutureWrapper(Bundler bundler, BundlerType bundlerType) { + if (bundler == null || bundlerType == null) { + throw new NullPointerException(); + } + this.bundler = bundler; + this.bundlerType = bundlerType; + } + + @Override + public void onResult(int methodIdentifier, Parcel params) { + @SuppressWarnings("unchecked") + E result = (E) bundler.readFromParcel(params, bundlerType); + + onResult(result); + } + + public abstract void onResult(E result); + + @Override + public void onException(Parcel exception) { + Throwable throwable = ParcelUtilities.readThrowableFromParcel(exception); + onException(throwable); + } + + public abstract void onException(Throwable throwable); + +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/LocalCallback.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/LocalCallback.java new file mode 100644 index 0000000..6c27bb1 --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/LocalCallback.java @@ -0,0 +1,40 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import android.os.Parcel; + +/** + * Interface used by callbacks used when calling {@link CrossProfileSender#callAsync(long, int, + * Parcel, LocalCallback, long)}. + */ +public interface LocalCallback { + + /** + * Pass a result into the callback. + * + * @param methodIdentifier The method being responded to. + * @param params The result encoded in a {@link Parcel}. This should not be recycled. + */ + void onResult(int methodIdentifier, Parcel params); + + /** + * Pass an exception into the callback. + * + * @param exception The exception encoded in a {@link Parcel}. This should not be recycled. + */ + void onException(Parcel exception); +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/ParcelableWrapperUtils.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/ParcelableWrapperUtils.java new file mode 100644 index 0000000..6469d94 --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/ParcelableWrapperUtils.java @@ -0,0 +1,43 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import android.os.Parcel; +import com.google.android.enterprise.connectedapps.internal.Bundler; + +/** Utility methods for use when writing a parcelable wrapper. */ +public class ParcelableWrapperUtils { + + /** + * Write a {@link Bundler} to a {@link Parcel}. + * + * <p>This should be called as the first line of your {@code #writeToParcel(Parcel, int)} method. + */ + public void writeBundler(Parcel parcel, Bundler bundler, int flags) { + parcel.writeParcelable(bundler, flags); + } + + /** + * Read a {@link Bundler} from a {@link Parcel}. + * + * <p>This should be called in the first line of your {@code createFromParcel(Parcel)} method. + */ + public Bundler readBundler(Parcel parcel) { + return parcel.readParcelable(Parcel.class.getClassLoader()); + } + + private ParcelableWrapperUtils() {} +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/Permissions.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/Permissions.java new file mode 100644 index 0000000..8b4663a --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/Permissions.java @@ -0,0 +1,29 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +/** Utility methods to deal with cross-profile permissions. */ +public interface Permissions { + + /** + * Returns true if the app has permission to make cross-profile calls. + * + * <p>This depends on the {@link ConnectionBinder} used, but by default will return true if the + * app requires the {@code INTERACT_ACROSS_PROFILES} permission and the user and admin have given + * consent, or {@code INTERACT_ACROSS_USERS} has been granted. + */ + boolean canMakeCrossProfileCalls(); +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/PermissionsImpl.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/PermissionsImpl.java new file mode 100644 index 0000000..7cac9f1 --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/PermissionsImpl.java @@ -0,0 +1,43 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import android.content.Context; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; + +class PermissionsImpl implements Permissions { + + private final Context context; + private final ConnectionBinder binder; + + PermissionsImpl(Context context, ConnectionBinder binder) { + if (context == null || binder == null) { + throw new NullPointerException(); + } + + this.context = context; + this.binder = binder; + } + + @Override + public boolean canMakeCrossProfileCalls() { + if (VERSION.SDK_INT < VERSION_CODES.O) { + return false; + } + return binder.hasPermissionToBind(context); + } +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/Profile.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/Profile.java new file mode 100644 index 0000000..0106995 --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/Profile.java @@ -0,0 +1,69 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + + +/** A profile which can be interacted with using the profile-aware SDK. */ +public final class Profile { + + private static final int CURRENT_PROFILE_LEGACY_IDENTIFIER = 0; + private static final int OTHER_PROFILE_LEGACY_IDENTIFIER = 1; + + /** Recreate a {@link Profile} previously serialised using {@link #asInt()}. */ + public static Profile fromInt(int legacyProfileIdentifier) { + return new Profile(legacyProfileIdentifier); + } + + // 0 for "current profile", 1 for "other profile" + // TODO(142042055): Refactor ProfileId so it is stable across profiles, so it can be + // stored, and so it can represent profile types as well as specific profiles + private final int legacyProfileIdentifier; + + private Profile(int legacyProfileIdentifier) { + this.legacyProfileIdentifier = legacyProfileIdentifier; + } + + /** Returns true if this {@link Profile} refers to the current profile. */ + public boolean isCurrent() { + return legacyProfileIdentifier == CURRENT_PROFILE_LEGACY_IDENTIFIER; + } + + /** Returns true if this {@link Profile} refers to the other profile. */ + public boolean isOther() { + return legacyProfileIdentifier == OTHER_PROFILE_LEGACY_IDENTIFIER; + } + + public int asInt() { + return legacyProfileIdentifier; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Profile that = (Profile) o; + return legacyProfileIdentifier == that.legacyProfileIdentifier; + } + + @Override + public int hashCode() { + return legacyProfileIdentifier; + } +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/ProfileConnector.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/ProfileConnector.java new file mode 100644 index 0000000..f5335b3 --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/ProfileConnector.java @@ -0,0 +1,131 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import android.content.Context; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; + +/** A {@link ProfileConnector} is used to manage the connection between profiles. */ +public interface ProfileConnector { + /** + * Start trying to connect to the other profile and start manually managing the connection. + * + * <p>This will mean that the connection will not be dropped automatically to save resources. + * + * <p>Must be called before interacting with synchronous cross-profile methods. + * + * <p>If the connection can not be made, then no errors will be thrown and connections will + * re-attempted indefinitely. + * + * @see #connect() + * @see #stopManualConnectionManagement() + */ + void startConnecting(); + + /** + * Attempt to connect to the other profile and start manually managing the connection. + * + * <p>This will mean that the connection will not be dropped automatically to save resources. + * + * <p>Must be called before interacting with synchronous cross-profile methods. + * + * <p>This must not be called from the main thread. + * + * @see #startConnecting() + * @see #stopManualConnectionManagement() + * @throws UnavailableProfileException If the connection cannot be made. + */ + void connect() throws UnavailableProfileException; + + /** + * Stop manual connection management. + * + * <p>This can be called after {@link #startConnecting()} to return connection management + * responsibilities to the SDK. + * + * <p>You should not make any synchronous cross-profile calls after calling this method. + */ + void stopManualConnectionManagement(); + + /** + * Return the {@link CrossProfileSender} being used for this connection. + * + * <p>This API should only be used by generated code. + */ + CrossProfileSender crossProfileSender(); + + /** + * Register a listener to be called when a profile is connected or disconnected. + * + * <p>{@link #isConnected()} can be called to check if a connection is established. + * + * @see #unregisterConnectionListener(ConnectionListener) + */ + void registerConnectionListener(ConnectionListener listener); + + /** + * Unregister a listener registered using {@link #registerConnectionListener( + * ConnectionListener)}. + */ + void unregisterConnectionListener(ConnectionListener listener); + + /** + * Register a listener to be called when a profile becomes available or unavailable. + * + * <p>{@link #isAvailable()} can be called to check if a profile is available. + * + * @see #unregisterAvailabilityListener(AvailabilityListener) + */ + void registerAvailabilityListener(AvailabilityListener listener); + + /** + * Unregister a listener registered using {@link #registerAvailabilityListener( + * AvailabilityListener)}. + */ + void unregisterAvailabilityListener(AvailabilityListener listener); + + /** + * Return true if there is another profile which could be connected to. + * + * <p>If this returns true, then asynchronous calls should succeed. Synchronous calls will only + * succeed if {@link #isConnected()} also returns true. + */ + boolean isAvailable(); + + /** + * Return true if there is another profile connected. + * + * <p>If this returns true, then synchronous calls should succeed unless they are disconnected + * before the call completes. + */ + boolean isConnected(); + + /** Return an instance of {@link ConnectedAppsUtils} for dealing with this connection. */ + ConnectedAppsUtils utils(); + + Permissions permissions(); + + /** Return the application context used by this connector. */ + Context applicationContext(); + + /** + * Returns true if this connection is being managed manually. + * + * <p>Use {@link #startConnecting()} to begin manual connection management, and {@link + * #stopManualConnectionManagement()} to end it. + */ + boolean isManuallyManagingConnection(); +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/ReflectionUtilities.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/ReflectionUtilities.java new file mode 100644 index 0000000..218a0fc --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/ReflectionUtilities.java @@ -0,0 +1,96 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.UserHandle; +import android.util.Log; +import com.google.android.enterprise.connectedapps.exceptions.MissingApiException; +import java.lang.reflect.InvocationTargetException; + +/** + * Utility functions when using reflection to access APIs. + */ +public class ReflectionUtilities { + + private static final String LOG_TAG = "ReflectionUtilities"; + private static final String BIND_SERVICE_AS_USER_METHOD_NAME = "bindServiceAsUser"; + + private static boolean canUseReflectedApisIsCached = false; + private static boolean canUseReflectedApis = false; + + /** + * Check that needed APIs are available and can be run. + * + * <p>This should be checked before calling any other methods on this class. + */ + static boolean canUseReflectedApis() { + if (canUseReflectedApisIsCached) { + return canUseReflectedApis; + } + + try { + Context.class.getMethod( + BIND_SERVICE_AS_USER_METHOD_NAME, + Intent.class, + ServiceConnection.class, + int.class, + UserHandle.class); + canUseReflectedApis = true; + canUseReflectedApisIsCached = true; + return true; + } catch (NoSuchMethodException e) { + // One of the methods cannot be called + Log.e(LOG_TAG, "canUseReflectedApis is false", e); + canUseReflectedApis = false; + canUseReflectedApisIsCached = true; + return false; + } + } + + /** + * Bind a {@link ServiceConnection} to a different running user. + * + * <p>Reflection is required for this call because {@code Context#bindServiceAsUser} was not + * made public until Android R. + * + * <p>This must only be called if {@link #canUseReflectedApis()} has returned true. + */ + public static boolean bindServiceAsUser( + Context context, Intent bindIntent, ServiceConnection connection, UserHandle otherUserHandle) + throws MissingApiException { + try { + return (Boolean) + context + .getClass() + .getMethod( + BIND_SERVICE_AS_USER_METHOD_NAME, + Intent.class, + ServiceConnection.class, + int.class, + UserHandle.class) + .invoke(context, bindIntent, connection, Context.BIND_AUTO_CREATE, otherUserHandle); + } catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { + // This indicates that this version of framework doesn't support the APIs needed - binding + // will not be possible + throw new MissingApiException("Error binding to other profile", e); + } + } + + private ReflectionUtilities() {} +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/UserConnector.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/UserConnector.java new file mode 100644 index 0000000..29f888b --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/UserConnector.java @@ -0,0 +1,134 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import android.content.Context; +import android.os.UserHandle; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; + +/** A {@link UserConnector} is used to manage the connection between users. */ +public interface UserConnector { + /** + * Start trying to connect to another user and start manually managing the connection. + * + * <p>This will mean that the connection will not be dropped automatically to save resources. + * + * <p>Must be called before interacting with synchronous cross-user methods. + * + * <p>If the connection can not be made, then no errors will be thrown and connections will + * re-attempted indefinitely. + * + * @see #connect(UserHandle) + * @see #stopManualConnectionManagement(UserHandle) + */ + void startConnecting(UserHandle userHandle); + + /** + * Attempt to connect to the user and start manually managing the connection. + * + * <p>This will mean that the connection will not be dropped automatically to save resources. + * + * <p>Must be called before interacting with synchronous cross-profile methods. + * + * <p>This must not be called from the main thread. + * + * @see #startConnecting(UserHandle) + * @see #stopManualConnectionManagement(UserHandle) + * @throws UnavailableProfileException If the connection cannot be made. + */ + void connect(UserHandle userHandle) throws UnavailableProfileException; + + /** + * Stop manual connection management. + * + * <p>This can be called after {@link #startConnecting(UserHandle)} to return connection + * management responsibilities to the SDK. + * + * <p>You should not make any synchronous cross-profile calls after calling this method. + */ + void stopManualConnectionManagement(UserHandle userHandle); + + /** + * Return the {@link CrossProfileSender} being used for the connection to the user. + * + * <p>This API should only be used by generated code. + */ + CrossProfileSender crossProfileSender(UserHandle userHandle); + + /** + * Register a listener to be called when the user is connected or disconnected. + * + * <p>{@link #isConnected(UserHandle)} can be called to check if a connection is established. + * + * @see #unregisterConnectionListener(UserHandle, ConnectionListener) + */ + void registerConnectionListener(UserHandle userHandle, ConnectionListener listener); + + /** + * Unregister a listener registered using {@link #registerConnectionListener(UserHandle, + * ConnectionListener)}. + */ + void unregisterConnectionListener(UserHandle userHandle, ConnectionListener listener); + + /** + * Register a listener to be called when a user becomes available or unavailable. + * + * <p>{@link #isAvailable(UserHandle)} can be called to check if a user is available. + * + * @see #unregisterAvailabilityListener(UserHandle, AvailabilityListener) + */ + void registerAvailabilityListener(UserHandle userHandle, AvailabilityListener listener); + + /** + * Unregister a listener registered using {@link #registerAvailabilityListener(UserHandle, + * AvailabilityListener)}. + */ + void unregisterAvailabilityListener(UserHandle userHandle, AvailabilityListener listener); + + /** + * Return true if the user can be connected to. + * + * <p>If this returns true, then asynchronous calls should succeed. Synchronous calls will only + * succeed if {@link #isConnected(UserHandle)} also returns true. + */ + boolean isAvailable(UserHandle userHandle); + + /** + * Return true if the user is connected. + * + * <p>If this returns true, then synchronous calls should succeed unless they are disconnected + * before the call completes. + */ + boolean isConnected(UserHandle userHandle); + + /** + * Return an instance of {@link ConnectedAppsUtils} for dealing with the connection to the user. + */ + ConnectedAppsUtils utils(UserHandle userHandle); + + Permissions permissions(UserHandle userHandle); + + /** Return the application context used by the user. */ + Context applicationContext(UserHandle userHandle); + + /** + * Returns true if the connection to the user is being managed manually. + * + * <p>Use {@link #startConnecting(UserHandle)} to begin manual connection management, and {@link + * #stopManualConnectionManagement(UserHandle)} to end it. + */ + boolean isManuallyManagingConnection(UserHandle userHandle); +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/exceptions/MissingApiException.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/exceptions/MissingApiException.java new file mode 100644 index 0000000..b354ce2 --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/exceptions/MissingApiException.java @@ -0,0 +1,27 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.exceptions; + +/** A required API is not available. */ +public class MissingApiException extends Exception { + public MissingApiException(String message) { + super(message); + } + + public MissingApiException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/exceptions/ProfileRuntimeException.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/exceptions/ProfileRuntimeException.java new file mode 100644 index 0000000..6ea9005 --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/exceptions/ProfileRuntimeException.java @@ -0,0 +1,27 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.exceptions; + +/** + * Thrown when a {@link RuntimeException} is thrown during a cross-profile call. + * + * <p>To get the original exception, call {@link #getCause()}. + */ +public class ProfileRuntimeException extends RuntimeException { + public ProfileRuntimeException(RuntimeException cause) { + super(cause); + } +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/exceptions/UnavailableProfileException.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/exceptions/UnavailableProfileException.java new file mode 100644 index 0000000..050b9ec --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/exceptions/UnavailableProfileException.java @@ -0,0 +1,29 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.exceptions; + +/** An exception representing a problem accessing the other profile. */ +// This can happen even if the developer has already checked available as the availability of the +// other profile can change at any time. +public class UnavailableProfileException extends Exception { + public UnavailableProfileException(String message) { + super(message); + } + + public UnavailableProfileException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/BackgroundExceptionThrower.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/BackgroundExceptionThrower.java new file mode 100644 index 0000000..0999a35 --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/BackgroundExceptionThrower.java @@ -0,0 +1,44 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.internal; + +import android.os.Handler; +import android.os.Looper; + +/** Utility class for throwing an exception in the background after a delay. */ +public final class BackgroundExceptionThrower { + + private BackgroundExceptionThrower() {} + + private static class ThrowingRunnable implements Runnable { + RuntimeException throwable; + + ThrowingRunnable(RuntimeException throwable) { + this.throwable = throwable; + } + + @Override + public void run() { + throw throwable; + } + } + + /** Throw the given {@link Throwable} after a delay on the main looper. */ + public static void throwInBackground(RuntimeException throwable) { + // We add a small delay to ensure that the return can be completed before crashing + new Handler(Looper.getMainLooper()).postDelayed(new ThrowingRunnable(throwable), 1000); + } +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/Bundler.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/Bundler.java new file mode 100644 index 0000000..830fc67 --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/Bundler.java @@ -0,0 +1,89 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.internal; + +import android.os.Parcel; +import android.os.Parcelable; +import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration; + +/** + * A {@link Bundler} is used to read and write {@link Parcel} instances without needing to use the + * specific methods for the type of object being read/written. + * + * <p>Each {@link CrossProfileConfiguration} will have a {@link Bundler} which can deal with all of + * the types used by that {@link CrossProfileConfiguration}. + */ +// TODO(158552516): Rename now this no longer concerns Bundles +public interface Bundler extends Parcelable { + /* + * We make {@link Bundler} instances implement {@link Parcelable} so that they can be passed + * as part of the Parcelable Wrapper classes. This ensures the wrappers read the correct types + * using the same {@link Bundler} that they wrote them with. + */ + + /** + * Write a value to a {@link Parcel}. + * + * @throws IllegalArgumentException if the {@code value} type is unsupported. + */ + void writeToParcel(Parcel parcel, Object value, BundlerType valueClass, int flags); + + default void writeToParcel(Parcel parcel, byte value, BundlerType valueType, int flags) { + parcel.writeByte(value); + } + + default void writeToParcel(Parcel parcel, short value, BundlerType valueType, int flags) { + parcel.writeInt(value); + } + + default void writeToParcel(Parcel parcel, int value, BundlerType valueType, int flags) { + parcel.writeInt(value); + } + + default void writeToParcel(Parcel parcel, long value, BundlerType valueType, int flags) { + parcel.writeLong(value); + } + + default void writeToParcel(Parcel parcel, char value, BundlerType valueType, int flags) { + parcel.writeInt(value); + } + + default void writeToParcel(Parcel parcel, float value, BundlerType valueType, int flags) { + parcel.writeFloat(value); + } + + default void writeToParcel(Parcel parcel, double value, BundlerType valueType, int flags) { + parcel.writeDouble(value); + } + + default void writeToParcel(Parcel parcel, boolean value, BundlerType valueType, int flags) { + parcel.writeInt(value ? 1 : 0); + } + + /** + * Read a value from a {@link Parcel}. + * + * @throws IllegalArgumentException if the {@code valueClass} type is unsupported. + */ + Object readFromParcel(Parcel parcel, BundlerType valueClass); + + /** + * Create an array of the given type. + * + * @throws IllegalArgumentException if the {@code valueClass} type is unsupported. + */ + Object[] createArray(BundlerType valueClass, int size); +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/BundlerType.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/BundlerType.java new file mode 100644 index 0000000..2f97201 --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/BundlerType.java @@ -0,0 +1,100 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.internal; + +import static java.util.Collections.emptyList; + +import android.os.Parcel; +import android.os.Parcelable; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +// This does not make a copy of the lists in setter/getter as the only caller is generated code + +/** Type used internally by the SDK to record the declared types of method calls. */ +public final class BundlerType implements Parcelable { + private final String rawTypeQualifiedName; + private final List<BundlerType> typeArguments; + + public String rawTypeQualifiedName() { + return rawTypeQualifiedName; + } + + public List<BundlerType> typeArguments() { + return typeArguments; + } + + private BundlerType(String rawTypeQualifiedName, List<BundlerType> typeArguments) { + this.rawTypeQualifiedName = rawTypeQualifiedName; + this.typeArguments = typeArguments; + } + + private BundlerType(Parcel in) { + rawTypeQualifiedName = in.readString(); + typeArguments = in.createTypedArrayList(BundlerType.CREATOR); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(rawTypeQualifiedName); + dest.writeTypedList(typeArguments); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator<BundlerType> CREATOR = + new Creator<BundlerType>() { + @Override + public BundlerType createFromParcel(Parcel in) { + return new BundlerType(in); + } + + @Override + public BundlerType[] newArray(int size) { + return new BundlerType[size]; + } + }; + + public static BundlerType of(String rawTypeQualifiedName, BundlerType... typeArguments) { + return new BundlerType(rawTypeQualifiedName, Arrays.asList(typeArguments)); + } + + public static BundlerType of(String rawTypeQualifiedName) { + return new BundlerType(rawTypeQualifiedName, emptyList()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BundlerType that = (BundlerType) o; + return rawTypeQualifiedName.equals(that.rawTypeQualifiedName) + && typeArguments.equals(that.typeArguments); + } + + @Override + public int hashCode() { + return Objects.hash(rawTypeQualifiedName, typeArguments); + } +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/ByteUtilities.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/ByteUtilities.java new file mode 100644 index 0000000..4b34350 --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/ByteUtilities.java @@ -0,0 +1,29 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.internal; + +/** This class is only for internal use by the SDK. */ +public final class ByteUtilities { + private ByteUtilities() {} + + /** Join two byte arrays into a single byte array, with firstArray followed by secondArray. */ + public static byte[] joinByteArrays(byte[] firstArray, byte[] secondArray) { + byte[] results = new byte[firstArray.length + secondArray.length]; + System.arraycopy(firstArray, 0, results, 0, firstArray.length); + System.arraycopy(secondArray, 0, results, firstArray.length, secondArray.length); + return results; + } +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/CallbackMergerExceptionCallback.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/CallbackMergerExceptionCallback.java new file mode 100644 index 0000000..4cf3ff7 --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/CallbackMergerExceptionCallback.java @@ -0,0 +1,43 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.internal; + +import com.google.android.enterprise.connectedapps.ExceptionCallback; +import com.google.android.enterprise.connectedapps.Profile; + +/** + * An {@link ExceptionCallback} which marks a {@link CrossProfileCallbackMultiMerger} result missing + * when an exception is received. + */ +public class CallbackMergerExceptionCallback<R> implements ExceptionCallback { + + private final Profile profileId; + private final CrossProfileCallbackMultiMerger<R> merger; + + public CallbackMergerExceptionCallback( + Profile profileId, CrossProfileCallbackMultiMerger<R> merger) { + if (profileId == null || merger == null) { + throw new NullPointerException(); + } + this.profileId = profileId; + this.merger = merger; + } + + @Override + public void onException(Throwable throwable) { + merger.missingResult(profileId); + } +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/CrossProfileCallbackExceptionParcelCallSender.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/CrossProfileCallbackExceptionParcelCallSender.java new file mode 100644 index 0000000..cf63fd7 --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/CrossProfileCallbackExceptionParcelCallSender.java @@ -0,0 +1,58 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.internal; + +import android.os.RemoteException; +import com.google.android.enterprise.connectedapps.ICrossProfileCallback; + +/** Implementation of {@link ParcelCallSender} used when passing a callback exception. */ +public class CrossProfileCallbackExceptionParcelCallSender extends ParcelCallSender { + + private final ICrossProfileCallback callback; + + public CrossProfileCallbackExceptionParcelCallSender(ICrossProfileCallback callback) { + if (callback == null) { + throw new NullPointerException("callback must not be null"); + } + this.callback = callback; + } + + /** Relays to {@link ICrossProfileCallback#prepareResult(long, int, int, byte[])} */ + @Override + void prepareCall(long callId, int blockId, int totalBytes, byte[] bytes) throws RemoteException { + callback.prepareResult(callId, blockId, totalBytes, bytes); + } + + /** + * Relays to {@link ICrossProfileCallback#onException(long, int, byte[])}}. + * + * <p>Always returns empty byte array. + */ + @Override + byte[] call(long callId, int blockId, byte[] bytes) throws RemoteException { + callback.onException(callId, blockId, bytes); + return new byte[0]; + } + + /** + * Callbacks cannot themselves return values, so this method will always throw an {@link + * IllegalStateException}. + */ + @Override + byte[] fetchResponse(long callId, int blockId) throws RemoteException { + throw new IllegalStateException(); + } +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/CrossProfileCallbackMultiMerger.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/CrossProfileCallbackMultiMerger.java new file mode 100644 index 0000000..cb7fada --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/CrossProfileCallbackMultiMerger.java @@ -0,0 +1,97 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.internal; + +import com.google.android.enterprise.connectedapps.Profile; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** Receives a number of async results, merge them, and relays the merged results. */ +public class CrossProfileCallbackMultiMerger<R> { + + // TODO: This should time-out if it doesn't receive the expected number + + /** + * A listener for results from the {@link CrossProfileCallbackMultiMerger}. + * + * <p>This will be called when all results are received. + */ + public interface CrossProfileCallbackMultiMergerCompleteListener<R> { + void onResult(Map<Profile, R> results); + } + + private boolean hasCompleted = false; + private final int expectedResults; + private final Map<Profile, R> results = new HashMap<>(); + private final Set<Profile> missedResults = new HashSet<>(); + private final CrossProfileCallbackMultiMergerCompleteListener<R> listener; + + public CrossProfileCallbackMultiMerger( + int expectedResults, CrossProfileCallbackMultiMergerCompleteListener<R> listener) { + if (listener == null) { + throw new NullPointerException(); + } + + this.expectedResults = expectedResults; + this.listener = listener; + + checkIfCompleted(); + } + + /** + * Indicate that a result is missing, so results can be posted with fewer than expected. + * + * <p>This should be called for every missing result. For example, if a remote call fails. + */ + public void missingResult(Profile profileId) { + if (hasCompleted) { + // Once a result has been posted we don't check any more + return; + } + + if (results.containsKey(profileId) || missedResults.contains(profileId)) { + // Only one result per profile is accepted + return; + } + missedResults.add(profileId); + + checkIfCompleted(); + } + + public void onResult(Profile profileId, R value) { + if (hasCompleted) { + // Once a result has been posted we don't check any more + return; + } + if (results.containsKey(profileId) || missedResults.contains(profileId)) { + // Only one result per profile is accepted + return; + } + + results.put(profileId, value); + + checkIfCompleted(); + } + + private void checkIfCompleted() { + if (results.size() + missedResults.size() >= expectedResults) { + hasCompleted = true; + listener.onResult(results); + } + } +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/CrossProfileCallbackParcelCallSender.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/CrossProfileCallbackParcelCallSender.java new file mode 100644 index 0000000..5bd4b65 --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/CrossProfileCallbackParcelCallSender.java @@ -0,0 +1,61 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.internal; + +import android.os.RemoteException; +import com.google.android.enterprise.connectedapps.ICrossProfileCallback; + +/** Implementation of {@link ParcelCallSender} used when passing a callback return value. */ +public class CrossProfileCallbackParcelCallSender extends ParcelCallSender { + + private final ICrossProfileCallback callback; + private final int methodIdentifier; + + public CrossProfileCallbackParcelCallSender( + ICrossProfileCallback callback, int methodIdentifier) { + if (callback == null) { + throw new NullPointerException("callback must not be null"); + } + this.callback = callback; + this.methodIdentifier = methodIdentifier; + } + + /** Relays to {@link ICrossProfileCallback#prepareResult(long, int, int, byte[])} */ + @Override + void prepareCall(long callId, int blockId, int totalBytes, byte[] bytes) throws RemoteException { + callback.prepareResult(callId, blockId, totalBytes, bytes); + } + + /** + * Relays to {@link ICrossProfileCallback#onResult(long, int, int, byte[])}. + * + * <p>Always returns empty byte array. + */ + @Override + byte[] call(long callId, int blockId, byte[] bytes) throws RemoteException { + callback.onResult(callId, blockId, methodIdentifier, bytes); + return new byte[0]; + } + + /** + * Callbacks cannot themselves return values, so this method will always throw an {@link + * IllegalStateException}. + */ + @Override + byte[] fetchResponse(long callId, int blockId) throws RemoteException { + throw new IllegalStateException(); + } +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/CrossProfileFutureResultWriter.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/CrossProfileFutureResultWriter.java new file mode 100644 index 0000000..7b58047 --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/CrossProfileFutureResultWriter.java @@ -0,0 +1,76 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.internal; + +import android.os.Parcel; +import android.util.Log; +import com.google.android.enterprise.connectedapps.ICrossProfileCallback; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; + +/** + * Implementation of {@link FutureResultWriter} used for writing results to a {@link + * ICrossProfileCallback}. + */ +public class CrossProfileFutureResultWriter<E> implements FutureResultWriter<E> { + + private final ICrossProfileCallback callback; + private final Bundler bundler; + private final BundlerType bundlerType; + + public CrossProfileFutureResultWriter( + ICrossProfileCallback callback, Bundler bundler, BundlerType bundlerType) { + if (callback == null || bundler == null || bundlerType == null) { + throw new NullPointerException(); + } + this.callback = callback; + this.bundler = bundler; + this.bundlerType = bundlerType; + } + + @Override + public void onSuccess(E result) { + Parcel parcel = Parcel.obtain(); // Recycled in this method + bundler.writeToParcel(parcel, result, bundlerType, /* flags= */ 0); + + try { + CrossProfileCallbackParcelCallSender parcelCallSender = + new CrossProfileCallbackParcelCallSender(callback, /* methodIdentifier= */ 0); + parcelCallSender.makeParcelCall(parcel); + } catch (UnavailableProfileException e) { + Log.e("FutureResult", "Connection was dropped before response"); + } catch (RuntimeException e) { + onFailure(new UnavailableProfileException("Error when writing result of future", e)); + } finally { + parcel.recycle(); + } + } + + @Override + public void onFailure(Throwable throwable) { + Parcel parcel = Parcel.obtain(); // Recycled in this method + ParcelUtilities.writeThrowableToParcel(parcel, throwable); + + try { + CrossProfileCallbackExceptionParcelCallSender parcelCallSender = + new CrossProfileCallbackExceptionParcelCallSender(callback); + parcelCallSender.makeParcelCall(parcel); + } catch (UnavailableProfileException e) { + Log.e("FutureResult", "Connection was dropped before response"); + } finally { + parcel.recycle(); + } + } +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/CrossProfileParcelCallSender.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/CrossProfileParcelCallSender.java new file mode 100644 index 0000000..3d66792 --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/CrossProfileParcelCallSender.java @@ -0,0 +1,64 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.internal; + +import android.os.RemoteException; +import com.google.android.enterprise.connectedapps.ICrossProfileCallback; +import com.google.android.enterprise.connectedapps.ICrossProfileService; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Implementation of {@link ParcelCallSender} used when making synchronous or asynchronous + * cross-profile calls. + */ +public final class CrossProfileParcelCallSender extends ParcelCallSender { + + private final ICrossProfileService wrappedService; + private final long crossProfileTypeIdentifier; + private final int methodIdentifier; + private final @Nullable ICrossProfileCallback callback; + + public CrossProfileParcelCallSender( + ICrossProfileService service, + long crossProfileTypeIdentifier, + int methodIdentifier, + @Nullable ICrossProfileCallback callback) { + if (service == null) { + throw new NullPointerException("service must not be null"); + } + + wrappedService = service; + this.crossProfileTypeIdentifier = crossProfileTypeIdentifier; + this.methodIdentifier = methodIdentifier; + this.callback = callback; + } + + @Override + void prepareCall(long callId, int blockId, int numBytes, byte[] params) throws RemoteException { + wrappedService.prepareCall(callId, blockId, numBytes, params); + } + + @Override + byte[] call(long callId, int blockId, byte[] params) throws RemoteException { + return wrappedService.call( + callId, blockId, crossProfileTypeIdentifier, methodIdentifier, params, callback); + } + + @Override + byte[] fetchResponse(long callId, int blockId) throws RemoteException { + return wrappedService.fetchResponse(callId, blockId); + } +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/FutureResultWriter.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/FutureResultWriter.java new file mode 100644 index 0000000..3bf3eca --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/FutureResultWriter.java @@ -0,0 +1,25 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.internal; + +/** Interface used for passing the results from futures. */ +public interface FutureResultWriter<E> { + + void onSuccess(E result); + + + void onFailure(Throwable throwable); +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/IfAvailableFutureResultWriter.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/IfAvailableFutureResultWriter.java new file mode 100644 index 0000000..4e6ffbe --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/IfAvailableFutureResultWriter.java @@ -0,0 +1,53 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.internal; + +import com.google.android.enterprise.connectedapps.FutureWrapper; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; + +/** + * An implementation of {@link FutureResultWriter} which suppresses any {@link + * UnavailableProfileException} instances, instead returning the specified {@code defaultValue}. + * + * <p>All other exceptions will be passed to the future as normal. + */ +public final class IfAvailableFutureResultWriter<E> implements FutureResultWriter<E> { + + private final FutureWrapper<E> futureWrapper; + private final E defaultValue; + + public IfAvailableFutureResultWriter(FutureWrapper<E> futureWrapper, E defaultValue) { + if (futureWrapper == null) { + throw new NullPointerException(); + } + this.futureWrapper = futureWrapper; + this.defaultValue = defaultValue; + } + + @Override + public void onSuccess(E result) { + this.futureWrapper.onResult(result); + } + + @Override + public void onFailure(Throwable throwable) { + if (throwable instanceof UnavailableProfileException) { + this.futureWrapper.onResult(defaultValue); + } else { + this.futureWrapper.onException(throwable); + } + } +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/MethodRunner.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/MethodRunner.java new file mode 100644 index 0000000..c9ee3dd --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/MethodRunner.java @@ -0,0 +1,25 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.internal; + +import android.content.Context; +import android.os.Parcel; +import com.google.android.enterprise.connectedapps.ICrossProfileCallback; + +/** Interface used internally by the SDK */ +public interface MethodRunner { + Parcel call(Context context, Parcel params, ICrossProfileCallback callback); +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/ParcelCallReceiver.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/ParcelCallReceiver.java new file mode 100644 index 0000000..e52386f --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/ParcelCallReceiver.java @@ -0,0 +1,152 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.internal; + +import android.os.Parcel; +import com.google.android.enterprise.connectedapps.CrossProfileSender; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * Build up parcels over multiple calls and prepare responses. + * + * <p>This is the counterpart to {@link ParcelCallSender}. Calls by the {@link ParcelCallSender} + * should be relayed to an instance of this class. + */ +public final class ParcelCallReceiver { + private final Map<Long, byte[]> preparedCalls = new HashMap<>(); + private final Map<Long, Integer> preparedCallParts = new HashMap<>(); + private final Map<Long, byte[]> preparedResponses = new HashMap<>(); + + /** + * Prepare a response to be returned by calls to {@link #getPreparedResponse(long, int)}. + * + * <p>The {@code byte[]} returned will begin with a 0 if all can be contained in a single call and + * 1 if further calls to {@link #getPreparedResponse(long, int)} are required. If the first byte + * is a 1, then the following 4 bytes will be an {@link Integer} representing the total number of + * bytes in the response. + * + * <p>The @{link Parcel} will not be recycled.</p> + */ + public byte[] prepareResponse(long callId, Parcel responseParcel) { + byte[] responseBytes = responseParcel.marshall(); + + if (responseBytes.length <= CrossProfileSender.MAX_BYTES_PER_BLOCK) { + // Prepend with 0 to indicate the bytes are complete + return ByteUtilities.joinByteArrays(new byte[] {0}, responseBytes); + } + // Record the bytes to be sent and send the first block + preparedResponses.put(callId, responseBytes); + byte[] response = new byte[CrossProfileSender.MAX_BYTES_PER_BLOCK + 5]; + // 1 = has additional content + response[0] = 1; + byte[] sizeBytes = ByteBuffer.allocate(4).putInt(responseBytes.length).array(); + System.arraycopy(sizeBytes, /* srcPos= */ 0, response, /* destPos= */ 1, /* length= */ 4); + System.arraycopy( + responseBytes, + /* srcPos= */ 0, + response, + /* destPos= */ 5, + /* length= */ CrossProfileSender.MAX_BYTES_PER_BLOCK); + return response; + } + + /** + * Prepare a call, storing one block of bytes for a call which will be completed with a call to + * {@link #getPreparedCall(long, int, byte[])}. + */ + public void prepareCall(long callId, int blockId, int numBytes, byte[] paramBytes) { + if (!preparedCalls.containsKey(callId)) { + preparedCalls.put(callId, new byte[numBytes]); + preparedCallParts.put(callId, 0); + } + System.arraycopy( + paramBytes, + /* srcPos= */ 0, + preparedCalls.get(callId), + /* destPos= */ blockId * CrossProfileSender.MAX_BYTES_PER_BLOCK, + /* length= */ CrossProfileSender.MAX_BYTES_PER_BLOCK); + preparedCallParts.put( + callId, + preparedCallParts.get(callId) + + 1 + + blockId); // +1 to have a difference when preparing the 0th block + } + + /** + * Fetch the full {@link Parcel using bytes previously stored by calls to + * {@link #prepareCall(long, int, int, byte[])}. + * + * <p>If this is the only block, then the {@code paramBytes} will be unmarshalled directly into a + * {@link Parcel}. + * + * <p>The returned {@link Parcel} must be recycled after use. + * + * @throws IllegalStateException If this is not the only block, and any previous blocks are + * missing. + */ + public Parcel getPreparedCall(long callId, int blockId, byte[] paramBytes) { + if (blockId > 0) { + int expectedBlocks = 0; + for (int i = 0; i < blockId; i++) { + expectedBlocks += 1 + i; + } + if (!preparedCallParts.containsKey(callId) + || expectedBlocks != preparedCallParts.get(callId)) { + throw new IllegalStateException("Call " + callId + " not prepared"); + } + byte[] fullParamBytes = preparedCalls.get(callId); + System.arraycopy( + paramBytes, + /* srcPos= */ 0, + fullParamBytes, + /* destPos= */ blockId * CrossProfileSender.MAX_BYTES_PER_BLOCK, + /* length= */ paramBytes.length); + paramBytes = fullParamBytes; + preparedCalls.remove(callId); + preparedCallParts.remove(callId); + } + + Parcel parcel = Parcel.obtain(); // Recycled by caller + parcel.unmarshall(paramBytes, 0, paramBytes.length); + parcel.setDataPosition(0); + return parcel; + } + + /** + * Get a block from a response previously prepared with {@link #prepareResponse(long, Parcel)}. + * + * <p>If this is the final block, then the prepared blocks will be dropped, and future calls to + * this method will fail. + */ + public byte[] getPreparedResponse(long callId, int blockId) { + byte[] preparedBytes = preparedResponses.get(callId); + byte[] response = + Arrays.copyOfRange( + preparedBytes, + /* from= */ blockId * CrossProfileSender.MAX_BYTES_PER_BLOCK, + /* to= */ Math.min( + preparedBytes.length, (blockId + 1) * CrossProfileSender.MAX_BYTES_PER_BLOCK)); + int numberOfBlocks = + (int) Math.ceil(preparedBytes.length * 1.0 / CrossProfileSender.MAX_BYTES_PER_BLOCK); + if (blockId == numberOfBlocks - 1) { + preparedResponses.remove(callId); + } + return response; + } +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/ParcelCallSender.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/ParcelCallSender.java new file mode 100644 index 0000000..a5b729a --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/ParcelCallSender.java @@ -0,0 +1,225 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.internal; + +import static com.google.android.enterprise.connectedapps.CrossProfileSender.MAX_BYTES_PER_BLOCK; + +import android.os.Parcel; +import android.os.RemoteException; +import android.os.TransactionTooLargeException; +import android.util.Log; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.UUID; + +/** + * This represents a single action of (sending a {@link Parcel} and possibly fetching a response, + * which may be split up over many calls (if the payload is large). + * + * <p>The receiver should relay calls to a {@link ParcelCallReceiver}. + */ +abstract class ParcelCallSender { + + private static final long RETRY_DELAY_MILLIS = 10; + private static final int MAX_RETRIES = 10; + + /** + * The arguments passed to this should be passed to {@link ParcelCallReceiver#prepareCall(long, + * int, int, byte[])}. + */ + abstract void prepareCall(long callId, int blockId, int totalBytes, byte[] bytes) + throws RemoteException; + + private void prepareCallAndRetry( + long callId, int blockId, int totalBytes, byte[] bytes, int retries) throws RemoteException { + while (true) { + try { + prepareCall(callId, blockId, totalBytes, bytes); + break; + } catch (TransactionTooLargeException e) { + if (retries-- <= 0) { + throw e; + } + + try { + Thread.sleep(RETRY_DELAY_MILLIS); + } catch (InterruptedException ex) { + Log.w("ParcelCallSender", "Interrupted on prepare retry", ex); + // If we can't sleep we'll just try again immediately + } + } + } + } + + /** + * The arguments passed to this should be passed to {@link + * ParcelCallReceiver#getPreparedCall(long, int, byte[])} and used to complete the call. + */ + abstract byte[] call(long callId, int blockId, byte[] bytes) throws RemoteException; + + private byte[] callAndRetry(long callId, int blockId, byte[] bytes, int retries) + throws RemoteException { + while (true) { + try { + return call(callId, blockId, bytes); + } catch (TransactionTooLargeException e) { + if (retries-- <= 0) { + throw e; + } + + try { + Thread.sleep(RETRY_DELAY_MILLIS); + } catch (InterruptedException ex) { + Log.w("ParcelCallSender", "Interrupted on prepare retry", ex); + // If we can't sleep we'll just try again immediately + } + } + } + } + + /** + * The arguments passed to this should be passed to {@link + * ParcelCallReceiver#getPreparedResponse(long, int)}. + */ + abstract byte[] fetchResponse(long callId, int blockId) throws RemoteException; + + private byte[] fetchResponseAndRetry(long callId, int blockId, int retries) + throws RemoteException { + while (true) { + try { + return fetchResponse(callId, blockId); + } catch (TransactionTooLargeException e) { + if (retries-- <= 0) { + throw e; + } + + try { + Thread.sleep(RETRY_DELAY_MILLIS); + } catch (InterruptedException ex) { + Log.w("ParcelCallSender", "Interrupted on prepare retry", ex); + // If we can't sleep we'll just try again immediately + } + } + } + } + + /** + * Use the prepareCall(long, int, int, byte[])} and {@link #call(long, int, byte[])} methods to + * make a call. + * + * <p>The returned {@link Parcel} must be recycled after use. + * + * <p>Returns {@code null} if the call does not return anything + * + * @throws UnavailableProfileException if any call fails + */ + public Parcel makeParcelCall(Parcel parcel) throws UnavailableProfileException { + long callIdentifier = UUID.randomUUID().getMostSignificantBits(); + byte[] bytes = parcel.marshall(); + try { + int numberOfBlocks = (int) Math.ceil(bytes.length * 1.0 / MAX_BYTES_PER_BLOCK); + int blockIdentifier = 0; + + if (numberOfBlocks > 1) { + byte[] block = new byte[MAX_BYTES_PER_BLOCK]; + + // Loop through all but the last one and send them over to be cached (retrying any failures) + while (blockIdentifier < numberOfBlocks - 1) { + System.arraycopy( + bytes, blockIdentifier * MAX_BYTES_PER_BLOCK, block, 0, MAX_BYTES_PER_BLOCK); + + // Since we know block size is below the limit any errors will be temporary so we should + // retry + prepareCallAndRetry(callIdentifier, blockIdentifier, bytes.length, block, MAX_RETRIES); + blockIdentifier++; + } + + bytes = Arrays.copyOfRange(bytes, blockIdentifier * MAX_BYTES_PER_BLOCK, bytes.length); + } + + // Since we know block size is below the limit any errors will be temporary so we should retry + byte[] returnBytes = callAndRetry(callIdentifier, blockIdentifier, bytes, MAX_RETRIES); + + if (returnBytes.length == 0) { + return null; + } + + return fetchResponseParcel(callIdentifier, returnBytes); + } catch (RemoteException e) { + throw new UnavailableProfileException("Could not access other profile", e); + } + } + + /** + * Use the {@link ParcelCallSender#prepareCall(long, int, int, byte[])} and {@link + * ParcelCallSender#fetchResponse(long, int)} methods to fetch a prepared response. + * + * <p>The returned {@link Parcel} must be recycled after use. + * + * @throws UnavailableProfileException if any call fails + */ + private Parcel fetchResponseParcel(long callIdentifier, byte[] returnBytes) + throws UnavailableProfileException { + + // returnBytes[0] is 0 if the bytes are complete, or 1 if we need to fetch more + int byteOffset = 1; + if (returnBytes[0] == 1) { + // returnBytes[1] - returnBytes[4] are an int representing the total size of the return + // value + int totalBytes = ByteBuffer.wrap(returnBytes).getInt(/* index= */ 1); + + try { + returnBytes = fetchReturnBytes(totalBytes, callIdentifier, returnBytes); + } catch (RemoteException e) { + throw new UnavailableProfileException("Could not access other profile", e); + } + byteOffset = 0; + } + Parcel p = Parcel.obtain(); // Recycled by caller + p.unmarshall( + returnBytes, /* offset= */ byteOffset, /* length= */ returnBytes.length - byteOffset); + p.setDataPosition(0); + return p; + } + + private byte[] fetchReturnBytes(int totalBytes, long callId, byte[] initialBytes) + throws RemoteException { + byte[] returnBytes = new byte[totalBytes]; + + // Skip the first 5 bytes which are used for status + System.arraycopy( + initialBytes, + /* srcPos= */ 5, + returnBytes, + /* destPos= */ 0, + /* length= */ MAX_BYTES_PER_BLOCK); + + int numberOfBlocks = (int) Math.ceil(totalBytes * 1.0 / MAX_BYTES_PER_BLOCK); + + for (int block = 1; block < numberOfBlocks; block++) { // Skip 0 as we already have it + // Since we know block size is below the limit any errors will be temporary so we should retry + byte[] bytes = fetchResponseAndRetry(callId, block, MAX_RETRIES); + System.arraycopy( + bytes, + /* srcPos= */ 0, + returnBytes, + /* destPos= */ block * MAX_BYTES_PER_BLOCK, + /* length= */ bytes.length); + } + return returnBytes; + } +} diff --git a/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/ParcelUtilities.java b/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/ParcelUtilities.java new file mode 100644 index 0000000..b519dae --- /dev/null +++ b/sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/ParcelUtilities.java @@ -0,0 +1,31 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.internal; + +import android.os.Parcel; + +/** This class is only for internal use by the SDK. */ +public final class ParcelUtilities { + private ParcelUtilities() {} + + public static void writeThrowableToParcel(Parcel parcel, Throwable throwable) { + parcel.writeSerializable(throwable); + } + + public static Throwable readThrowableFromParcel(Parcel parcel) { + return (Throwable) parcel.readSerializable(); + } +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..e49f709 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,42 @@ +include ":connectedapps-annotations" +project(":connectedapps-annotations").projectDir = file("annotations") +include ":connectedapps-testing-annotations" +project(":connectedapps-testing-annotations").projectDir = file("testing/annotations") +include ":connectedapps" +project(":connectedapps").projectDir = file("sdk") +include ":connectedapps-testing" +project(":connectedapps-testing").projectDir = file("testing/sdk") +include ":connectedapps-processor" +project(":connectedapps-processor").projectDir = file("processor") +include ":connectedapps-sharedtests" +project(":connectedapps-sharedtests").projectDir = file("tests/shared") + +include ":connectedapps-testapp" +project(":connectedapps-testapp").projectDir = file("tests/shared/testapp") + +include ":connectedapps-testapp_additional_types" +project(":connectedapps-testapp_additional_types").projectDir = file("tests/shared/additional_types") + +include ":connectedapps-testapp_basictypes" +project(":connectedapps-testapp_basictypes").projectDir = file("tests/shared/basictypes") + +include ":connectedapps-testapp_configuration" +project(":connectedapps-testapp_configuration").projectDir = file("tests/shared/configuration") + +include ":connectedapps-testapp_connector" +project(":connectedapps-testapp_connector").projectDir = file("tests/shared/connector") + +include ":connectedapps-testapp_types" +project(":connectedapps-testapp_types").projectDir = file("tests/shared/types") + +include ":connectedapps-testapp_types_providers" +project(":connectedapps-testapp_types_providers").projectDir = file("tests/shared/types_providers") + +include ":connectedapps-testapp_wrappers" +project(":connectedapps-testapp_wrappers").projectDir = file("tests/shared/wrappers") + +include ":connectedapps-testapp_crossuser" +project(":connectedapps-testapp_crossuser").projectDir = file("tests/shared/crossuser") + +include ":connectedapps-testapp_app" +project(":connectedapps-testapp_app").projectDir = file("tests/shared/app")
\ No newline at end of file diff --git a/testing/annotations/build.gradle b/testing/annotations/build.gradle new file mode 100644 index 0000000..cc3eb36 --- /dev/null +++ b/testing/annotations/build.gradle @@ -0,0 +1,29 @@ +plugins { + id 'java-library' + id 'maven-publish' +} + +publishing { + publications { + maven(MavenPublication) { + from components.java + groupId = 'com.google.android.enterprise.connectedapps' + artifactId = 'connectedapps-testing-annotations' + version = project.version + + pom { + licenses { + license { + name = 'Apache 2.0' + url = 'https://opensource.org/licenses/Apache-2.0' + } + } + } + } + } +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} diff --git a/testing/annotations/src/main/java/com/google/android/enterprise/connectedapps/testing/annotations/CrossProfileTest.java b/testing/annotations/src/main/java/com/google/android/enterprise/connectedapps/testing/annotations/CrossProfileTest.java new file mode 100644 index 0000000..1324a06 --- /dev/null +++ b/testing/annotations/src/main/java/com/google/android/enterprise/connectedapps/testing/annotations/CrossProfileTest.java @@ -0,0 +1,35 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testing.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Annotation for classes which test cross-profile functionality. */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.CLASS) +public @interface CrossProfileTest { + + /** + * The configuration to be tested. + * + * <p>This triggers the generation of test code for providers, types, and connectors under this + * configuration. + */ + Class<?> configuration(); +} diff --git a/testing/annotations/src/main/java/com/google/android/enterprise/connectedapps/testing/annotations/CrossUserTest.java b/testing/annotations/src/main/java/com/google/android/enterprise/connectedapps/testing/annotations/CrossUserTest.java new file mode 100644 index 0000000..ac57d14 --- /dev/null +++ b/testing/annotations/src/main/java/com/google/android/enterprise/connectedapps/testing/annotations/CrossUserTest.java @@ -0,0 +1,35 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testing.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Annotation for classes which test cross-user functionality. */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.CLASS) +public @interface CrossUserTest { + + /** + * The configuration to be tested. + * + * <p>This triggers the generation of test code for providers, types, and connectors under this + * configuration. + */ + Class<?> configuration(); +} diff --git a/testing/sdk/build.gradle b/testing/sdk/build.gradle new file mode 100644 index 0000000..89e80cb --- /dev/null +++ b/testing/sdk/build.gradle @@ -0,0 +1,50 @@ +plugins { + id 'com.android.library' + id 'maven-publish' +} + +dependencies { + implementation project(path: ':connectedapps-annotations') + implementation project(path: ':connectedapps') + implementation deps.androidxTest +} + +afterEvaluate { + publishing { + publications { + maven(MavenPublication) { + from components.release + groupId = 'com.google.android.enterprise.connectedapps' + artifactId = 'connectedapps-testing' + version = project.version + + pom { + licenses { + license { + name = 'Apache 2.0' + url = 'https://opensource.org/licenses/Apache-2.0' + } + } + } + } + } + } +} + +android { + defaultConfig { + compileSdkVersion 30 + minSdkVersion 26 + } + + buildFeatures { + aidl = true + } + + testOptions.unitTests.includeAndroidResources = true + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} diff --git a/testing/sdk/src/AndroidManifest.xml b/testing/sdk/src/AndroidManifest.xml new file mode 100644 index 0000000..109023a --- /dev/null +++ b/testing/sdk/src/AndroidManifest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.google.android.enterprise.connectedapps.testing"> + <application> + <receiver + android:name="com.google.android.enterprise.connectedapps.testing.DeviceAdminReceiver" + android:permission="android.permission.BIND_DEVICE_ADMIN" + android:exported="true"> + <meta-data + android:name="android.app.device_admin" + android:resource="@xml/device_admin_receiver" /> + <intent-filter> + <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/> + <action android:name="android.app.action.PROFILE_PROVISIONING_COMPLETE"/> + <action android:name="android.intent.action.BOOT_COMPLETED"/> + <action android:name="android.app.action.PROFILE_OWNER_CHANGED"/> + <action android:name="android.app.action.DEVICE_OWNER_CHANGED"/> + </intent-filter> + </receiver> + </application> +</manifest> diff --git a/testing/sdk/src/main/AndroidManifest.xml b/testing/sdk/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6b06f04 --- /dev/null +++ b/testing/sdk/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="utf-8"?> + +<manifest package="com.google.android.enterprise.connectedapps.testing" /> diff --git a/testing/sdk/src/main/java/com/google/android/enterprise/connectedapps/testing/AbstractFakeProfileConnector.java b/testing/sdk/src/main/java/com/google/android/enterprise/connectedapps/testing/AbstractFakeProfileConnector.java new file mode 100644 index 0000000..1fdb5f3 --- /dev/null +++ b/testing/sdk/src/main/java/com/google/android/enterprise/connectedapps/testing/AbstractFakeProfileConnector.java @@ -0,0 +1,303 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testing; + +import android.content.Context; +import com.google.android.enterprise.connectedapps.AvailabilityListener; +import com.google.android.enterprise.connectedapps.ConnectedAppsUtils; +import com.google.android.enterprise.connectedapps.ConnectionListener; +import com.google.android.enterprise.connectedapps.CrossProfileSender; +import com.google.android.enterprise.connectedapps.Permissions; +import com.google.android.enterprise.connectedapps.ProfileConnector; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector.ProfileType; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import java.util.HashSet; +import java.util.Set; + +/** + * A fake {@link ProfileConnector} for use in tests. + * + * <p>This should be extended to make it compatible with a specific {@link ProfileConnector} + * interface. + */ +public abstract class AbstractFakeProfileConnector implements ProfileConnector { + + enum WorkProfileState { + DOES_NOT_EXIST, + TURNED_OFF, + TURNED_ON + } + + private final Context applicationContext; + private final ProfileType primaryProfileType; + private ProfileType runningOnProfile = ProfileType.PERSONAL; + private WorkProfileState workProfileState = WorkProfileState.DOES_NOT_EXIST; + private boolean isConnected = false; + private boolean hasPermissionToMakeCrossProfileCalls = true; + private boolean isManuallyManagingConnection = false; + + private final Set<ConnectionListener> connectionListeners = new HashSet<>(); + private final Set<AvailabilityListener> availabilityListeners = new HashSet<>(); + + public AbstractFakeProfileConnector(Context context, ProfileType primaryProfileType) { + if (context == null || primaryProfileType == null) { + throw new NullPointerException(); + } + this.applicationContext = context.getApplicationContext(); + this.primaryProfileType = primaryProfileType; + } + + /** + * Simulate running on a particular profile type. + * + * <p>If {@code currentProfile} is {@link ProfileType#WORK} and a work profile does not exist or + * is not turned on, then a work profile will be created and turned on. + * + * @see #runningOnProfile + */ + public void setRunningOnProfile(ProfileType currentProfile) { + if (currentProfile == ProfileType.WORK && workProfileState != WorkProfileState.TURNED_ON) { + turnOnWorkProfile(); + } + this.runningOnProfile = currentProfile; + } + + /** + * Get the current profile type being simulated. + * + * @see #setRunningOnProfile(ProfileType) + */ + public ProfileType runningOnProfile() { + return runningOnProfile; + } + + /** + * Simulate the creation of a work profile. + * + * <p>The new work profile will be turned off by default. + */ + public void createWorkProfile() { + if (workProfileState != WorkProfileState.DOES_NOT_EXIST) { + return; + } + this.workProfileState = WorkProfileState.TURNED_OFF; + } + + /** + * Remove a simulated work profile. + * + * <p>The simulated work profile will be turned off first. + */ + public void removeWorkProfile() { + if (workProfileState == WorkProfileState.DOES_NOT_EXIST) { + return; + } + + turnOffWorkProfile(); + this.workProfileState = WorkProfileState.DOES_NOT_EXIST; + } + + /** + * Simulate a work profile being turned on. + * + * <p>If no simulated work profile exists, then it will be created. + */ + public void turnOnWorkProfile() { + if (workProfileState == WorkProfileState.TURNED_ON) { + return; + } + if (workProfileState == WorkProfileState.DOES_NOT_EXIST) { + createWorkProfile(); + } + workProfileState = WorkProfileState.TURNED_ON; + notifyAvailabilityChanged(); + } + + /** + * Simulate a work profile being turned off. + * + * <p>If no simulated work profile exists, then it will be created. + * + * <p>This fake will also be set to simulate running on the personal profile for future calls. + */ + public void turnOffWorkProfile() { + if (workProfileState == WorkProfileState.DOES_NOT_EXIST) { + createWorkProfile(); + } + setRunningOnProfile(ProfileType.PERSONAL); + if (workProfileState == WorkProfileState.TURNED_OFF) { + return; + } + + if (isConnected) { + isConnected = false; + notifyConnectionChanged(); + } + + workProfileState = WorkProfileState.TURNED_OFF; + notifyAvailabilityChanged(); + } + + /** + * Force the connector to be "automatically" connected. + * + * <p>This call should only be used by the SDK and should not be called in tests. If you want to + * connect manually, use {@link #startConnecting()}, or for automatic management just make the + * asynchronous call directly. + * + * @hide + */ + public void automaticallyConnect() { + if (isAvailable() && !isConnected) { + isConnected = true; + notifyConnectionChanged(); + } + } + + /** + * Disconnect after an automatic connection. + * + * <p>In reality, this timeout happens some arbitrary time of no interaction with the other + * profile. + * + * <p>If {@link #isManuallyManagingConnection()} is true, then this will do nothing. + */ + public void timeoutConnection() { + if (isManuallyManagingConnection) { + return; + } + + if (isConnected) { + isConnected = false; + notifyConnectionChanged(); + } + } + + @Override + public void startConnecting() { + isManuallyManagingConnection = true; + automaticallyConnect(); + } + + /** + * This fake does not enforce the requirement that calls to {@link #connect()} do not occur on the + * UI Thread. + */ + @Override + public void connect() throws UnavailableProfileException { + if (!isAvailable()) { + throw new UnavailableProfileException("No profile available"); + } + + isManuallyManagingConnection = true; + automaticallyConnect(); + } + + /** + * Stop manually managing the connection and ensure that the connector is disconnected. + */ + public void disconnect() { + stopManualConnectionManagement(); + timeoutConnection(); + } + + @Override + public void stopManualConnectionManagement() { + isManuallyManagingConnection = false; + } + + /** Unsupported by the fake so always returns {@code null}. */ + @Override + public CrossProfileSender crossProfileSender() { + return null; + } + + @Override + public void registerConnectionListener(ConnectionListener listener) { + connectionListeners.add(listener); + } + + @Override + public void unregisterConnectionListener(ConnectionListener listener) { + connectionListeners.remove(listener); + } + + private void notifyConnectionChanged() { + for (ConnectionListener listener : connectionListeners) { + listener.connectionChanged(); + } + } + + @Override + public void registerAvailabilityListener(AvailabilityListener listener) { + availabilityListeners.add(listener); + } + + @Override + public void unregisterAvailabilityListener(AvailabilityListener listener) { + availabilityListeners.remove(listener); + } + + private void notifyAvailabilityChanged() { + for (AvailabilityListener listener : availabilityListeners) { + listener.availabilityChanged(); + } + } + + @Override + public boolean isAvailable() { + return (runningOnProfile == ProfileType.WORK || workProfileState == WorkProfileState.TURNED_ON); + } + + @Override + public boolean isConnected() { + return isConnected; + } + + @Override + public ConnectedAppsUtils utils() { + return new FakeConnectedAppsUtils(this, primaryProfileType); + } + + @Override + public Permissions permissions() { + return new FakePermissions(this); + } + + /** Not supported by the fake so returns null. */ + @Override + public Context applicationContext() { + return applicationContext; + } + + @Override + public boolean isManuallyManagingConnection() { + return isManuallyManagingConnection; + } + + /** + * Set whether or not the app has been given the appropriate permission to make cross-profile + * calls. + */ + public void setHasPermissionToMakeCrossProfileCalls( + boolean hasPermissionToMakeCrossProfileCalls) { + this.hasPermissionToMakeCrossProfileCalls = hasPermissionToMakeCrossProfileCalls; + } + + boolean hasPermissionToMakeCrossProfileCalls() { + return hasPermissionToMakeCrossProfileCalls; + } +} diff --git a/testing/sdk/src/main/java/com/google/android/enterprise/connectedapps/testing/BlockingPoll.java b/testing/sdk/src/main/java/com/google/android/enterprise/connectedapps/testing/BlockingPoll.java new file mode 100644 index 0000000..7bdb242 --- /dev/null +++ b/testing/sdk/src/main/java/com/google/android/enterprise/connectedapps/testing/BlockingPoll.java @@ -0,0 +1,50 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testing; + +/** Utility for blocking a thread while polling for a state. */ +public class BlockingPoll { + + /** Interface implemented when using {@link #poll(BooleanSupplier, long, long)}. */ + public interface BooleanSupplier { + boolean getAsBoolean(); + } + + /** + * Poll for a state at a given frequency while blocking the thread. + * + * @param func returns true when the polling condition is met. False otherwise + * @param pollFrequency The number of milliseconds to wait between calling {@code func} + * @param timeoutMillis The maximum number of milliseconds before an {@link + * IllegalStateException} is thrown. + */ + public static void poll(BooleanSupplier func, long pollFrequency, long timeoutMillis) { + long endTime = System.currentTimeMillis() + timeoutMillis; + while (!func.getAsBoolean() && System.currentTimeMillis() < endTime) { + try { + Thread.sleep(pollFrequency); + } catch (InterruptedException e) { + throw new IllegalStateException("Sleep interrupted", e); + } + } + + if (!func.getAsBoolean()) { + throw new IllegalStateException("Timeout after " + timeoutMillis); + } + } + + private BlockingPoll() {} +} diff --git a/testing/sdk/src/main/java/com/google/android/enterprise/connectedapps/testing/DeviceAdminReceiver.java b/testing/sdk/src/main/java/com/google/android/enterprise/connectedapps/testing/DeviceAdminReceiver.java new file mode 100644 index 0000000..2c21568 --- /dev/null +++ b/testing/sdk/src/main/java/com/google/android/enterprise/connectedapps/testing/DeviceAdminReceiver.java @@ -0,0 +1,19 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testing; + +/** {@link android.app.admin.DeviceAdminReceiver} implementation for test apps. */ +public class DeviceAdminReceiver extends android.app.admin.DeviceAdminReceiver {} diff --git a/testing/sdk/src/main/java/com/google/android/enterprise/connectedapps/testing/FakeConnectedAppsUtils.java b/testing/sdk/src/main/java/com/google/android/enterprise/connectedapps/testing/FakeConnectedAppsUtils.java new file mode 100644 index 0000000..0cc2b7e --- /dev/null +++ b/testing/sdk/src/main/java/com/google/android/enterprise/connectedapps/testing/FakeConnectedAppsUtils.java @@ -0,0 +1,96 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testing; + +import com.google.android.enterprise.connectedapps.ConnectedAppsUtils; +import com.google.android.enterprise.connectedapps.Profile; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector.ProfileType; + +class FakeConnectedAppsUtils implements ConnectedAppsUtils { + + private static final Profile CURRENT_PROFILE_IDENTIFIER = Profile.fromInt(0); + private static final Profile OTHER_PROFILE_IDENTIFIER = Profile.fromInt(1); + + private final AbstractFakeProfileConnector fakeProfileConnector; + private final ProfileType primaryProfileType; + + FakeConnectedAppsUtils( + AbstractFakeProfileConnector fakeProfileConnector, ProfileType primaryProfileType) { + if (fakeProfileConnector == null || primaryProfileType == null) { + throw new NullPointerException(); + } + this.fakeProfileConnector = fakeProfileConnector; + this.primaryProfileType = primaryProfileType; + } + + @Override + public Profile getCurrentProfile() { + return CURRENT_PROFILE_IDENTIFIER; + } + + @Override + public Profile getOtherProfile() { + return OTHER_PROFILE_IDENTIFIER; + } + + @Override + public Profile getPrimaryProfile() { + if (primaryProfileType != ProfileType.WORK && primaryProfileType != ProfileType.PERSONAL) { + throw new IllegalStateException("No primary profile set"); + } + + return (primaryProfileType == fakeProfileConnector.runningOnProfile()) + ? CURRENT_PROFILE_IDENTIFIER + : OTHER_PROFILE_IDENTIFIER; + } + + @Override + public Profile getSecondaryProfile() { + if (primaryProfileType != ProfileType.WORK && primaryProfileType != ProfileType.PERSONAL) { + throw new IllegalStateException("No primary profile set"); + } + + return (primaryProfileType == fakeProfileConnector.runningOnProfile()) + ? OTHER_PROFILE_IDENTIFIER + : CURRENT_PROFILE_IDENTIFIER; + } + + @Override + public Profile getWorkProfile() { + if (fakeProfileConnector.runningOnProfile() == ProfileType.WORK) { + return getCurrentProfile(); + } + return getOtherProfile(); + } + + @Override + public Profile getPersonalProfile() { + if (fakeProfileConnector.runningOnProfile() == ProfileType.WORK) { + return getOtherProfile(); + } + return getCurrentProfile(); + } + + @Override + public boolean runningOnWork() { + return fakeProfileConnector.runningOnProfile() == ProfileType.WORK; + } + + @Override + public boolean runningOnPersonal() { + return fakeProfileConnector.runningOnProfile() == ProfileType.PERSONAL; + } +} diff --git a/testing/sdk/src/main/java/com/google/android/enterprise/connectedapps/testing/FakePermissions.java b/testing/sdk/src/main/java/com/google/android/enterprise/connectedapps/testing/FakePermissions.java new file mode 100644 index 0000000..763b67c --- /dev/null +++ b/testing/sdk/src/main/java/com/google/android/enterprise/connectedapps/testing/FakePermissions.java @@ -0,0 +1,32 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testing; + +import com.google.android.enterprise.connectedapps.Permissions; + +class FakePermissions implements Permissions { + + private final AbstractFakeProfileConnector fakeProfileConnector; + + FakePermissions(AbstractFakeProfileConnector fakeProfileConnector) { + this.fakeProfileConnector = fakeProfileConnector; + } + + @Override + public boolean canMakeCrossProfileCalls() { + return fakeProfileConnector.hasPermissionToMakeCrossProfileCalls(); + } +} diff --git a/testing/sdk/src/main/java/com/google/android/enterprise/connectedapps/testing/InstrumentedTestUtilities.java b/testing/sdk/src/main/java/com/google/android/enterprise/connectedapps/testing/InstrumentedTestUtilities.java new file mode 100644 index 0000000..2d2ee60 --- /dev/null +++ b/testing/sdk/src/main/java/com/google/android/enterprise/connectedapps/testing/InstrumentedTestUtilities.java @@ -0,0 +1,282 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testing; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import android.content.Context; +import android.os.ParcelFileDescriptor; +import android.os.UserHandle; +import android.util.Log; +import androidx.test.platform.app.InstrumentationRegistry; +import com.google.android.enterprise.connectedapps.ConnectionListener; +import com.google.android.enterprise.connectedapps.ProfileConnector; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.NoSuchElementException; +import java.util.Scanner; +import java.util.concurrent.CountDownLatch; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** Utilities for interacting with an instrumented environment for cross-profile tests. */ +public class InstrumentedTestUtilities { + + private static final String LOG_TAG = "InstrumentedTestUtilities"; + + private static final Pattern USERINFO_PATTERN = Pattern.compile("UserInfo\\{(.*):.*:.*\\}"); + + private final ProfileConnector connector; + private final Context context; + + public InstrumentedTestUtilities(Context context, ProfileConnector connector) { + if (context == null || connector == null) { + throw new NullPointerException(); + } + this.context = context; + this.connector = connector; + } + + /** + * Ensure that a work profile exists, this app is installed in both profiles, a relevant + * permission is granted for this app, and the work profile is unlocked. + */ + public void ensureReadyForCrossProfileCalls() { + ensureReadyForCrossProfileCalls(context.getPackageName()); + } + + /** + * Ensure that a work profile exists, the given package is installed in both profiles, a relevant + * permission is granted for the app, and the work profile is unlocked. + */ + public void ensureReadyForCrossProfileCalls(String packageName) { + ensureWorkProfileExists(); + + if (!packageName.equals(context.getPackageName())) { + // ensureWorkProfileExists will install the test package + installInWorkProfile(packageName); + } + + int workProfileUserId = getWorkProfileUserId(); + startUser(workProfileUserId); + + grantInteractAcrossUsers(packageName); + + ProfileAvailabilityPoll.blockUntilProfileRunningAndUnlocked( + context, getWorkProfileUserHandle()); + } + + private UserHandle getWorkProfileUserHandle() { + try { + return (UserHandle) + UserHandle.class + .getMethod("of", int.class) + .invoke(/* object= */ null, getWorkProfileUserId()); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Error getting current user handle", e); + } + } + + /** + * Create a work profile if one does not exist. + * + * <p>This will also install this app into the new profile and set it as the profile owner. + */ + public void ensureWorkProfileExists() { + if (hasWorkProfile()) { + return; + } + + createWorkProfile(); + } + + private static void grantInteractAcrossUsers(String packageName) { + runCommandWithOutput("pm grant " + packageName + " android.permission.INTERACT_ACROSS_USERS"); + runCommandWithOutput( + "pm grant " + packageName + " android.permission.INTERACT_ACROSS_PROFILES"); + } + + /** Remove a work profile if one exists. */ + public void ensureNoWorkProfile() { + if (hasWorkProfile()) { + removeWorkProfile(); + } + } + + /** + * Create a work profile. + * + * <p>This will install this app in the profile and set it as profile owner. + * + * <p>If {@link #hasWorkProfile()} returns true then an {@link IllegalStateException} will be + * thrown. + */ + private void createWorkProfile() { + if (hasWorkProfile()) { + throw new IllegalStateException( + "There is already a work profile on the device with user id " + + getWorkProfileUserId() + + "."); + } + runCommandWithOutput("pm create-user --profileOf 0 --managed TestProfile123"); + BlockingPoll.poll(this::hasWorkProfile, 100, 10000); + installInWorkProfile(context.getPackageName()); + int workProfileUserId = getWorkProfileUserId(); + startUser(workProfileUserId); + setTestAsProfileOwner(workProfileUserId); + } + + private static void startUser(int userId) { + runCommandWithOutput("am start-user " + userId); + } + + private void setTestAsProfileOwner(int userId) { + runCommandWithOutput( + "dpm set-profile-owner --user " + + userId + + " " + + context.getPackageName() + + "/com.google.android.enterprise.connectedapps.testing.DeviceAdminReceiver"); + } + + private void removeWorkProfile() { + removeUser(getWorkProfileUserId()); + } + + private static void removeUser(int userId) { + runCommandWithOutput("pm remove-user " + userId); + } + + /** Install this app in the work profile. */ + private void installInWorkProfile(String packageName) { + if (!hasWorkProfile()) { + throw new IllegalStateException("There is no work profile on the device."); + } + + installInUser(getWorkProfileUserId(), packageName); + } + + private static void installInUser(int userId, String packageName) { + runCommandWithOutput("cmd package install-existing --user " + userId + " " + packageName); + } + + /** Return true if a work profile exists on the device. */ + public boolean hasWorkProfile() { + try { + getWorkProfileUserId(); + return true; + } catch (IllegalStateException e) { + Log.i(LOG_TAG, "hasWorkProfile() found no work profile", e); + return false; + } + } + + /** + * Get the user ID of the work profile. + * + * <p>If there is no work profile, an {@link IllegalStateException} will be thrown. + */ + public int getWorkProfileUserId() { + String userList = runCommandWithOutput("pm list users"); + + // TODO(162219825): Instead of assuming the first non-system user is managed, actually check + Matcher matcher = USERINFO_PATTERN.matcher(userList); + + while (matcher.find()) { + int userId = Integer.parseInt(matcher.group(1)); + if (userId != 0) { + // Skip system user + return userId; + } + } + + throw new IllegalStateException("No non-system user found: " + userList); + } + + /** + * Block until the given {@link ProfileConnector} has disconnected. + * + * <p>This should not be run from the UI thread. + */ + public void waitForDisconnected() { + CountDownLatch connectionLatch = new CountDownLatch(1); + + ConnectionListener connectionListener = + () -> { + if (!connector.isConnected()) { + connectionLatch.countDown(); + } + }; + + connector.registerConnectionListener(connectionListener); + connectionListener.connectionChanged(); + + try { + connectionLatch.await(); + } catch (InterruptedException e) { + throw new AssertionError("Error waiting to disconnect", e); + } + + connector.unregisterConnectionListener(connectionListener); + } + + /** + * Block until the given {@link ProfileConnector} has connected. + * + * <p>This should not be run from the UI thread. + */ + public void waitForConnected() { + CountDownLatch connectionLatch = new CountDownLatch(1); + + ConnectionListener connectionListener = + () -> { + if (connector.isConnected()) { + connectionLatch.countDown(); + } + }; + + connector.registerConnectionListener(connectionListener); + connectionListener.connectionChanged(); + + try { + connectionLatch.await(); + } catch (InterruptedException e) { + throw new AssertionError("Error waiting to connect", e); + } + + connector.unregisterConnectionListener(connectionListener); + } + + private static String runCommandWithOutput(String command) { + try (ParcelFileDescriptor p = runCommand(command); + InputStream inputStream = new FileInputStream(p.getFileDescriptor()); + Scanner scanner = new Scanner(inputStream, UTF_8.name())) { + return scanner.useDelimiter("\\A").next(); + } catch (NoSuchElementException e) { + // Empty output + return ""; + } catch (IOException e) { + throw new IllegalStateException("Error getting command output", e); + } + } + + private static ParcelFileDescriptor runCommand(String command) { + return InstrumentationRegistry.getInstrumentation() + .getUiAutomation() + .executeShellCommand(command); + } +} diff --git a/testing/sdk/src/main/java/com/google/android/enterprise/connectedapps/testing/ProfileAvailabilityPoll.java b/testing/sdk/src/main/java/com/google/android/enterprise/connectedapps/testing/ProfileAvailabilityPoll.java new file mode 100644 index 0000000..42ff10c --- /dev/null +++ b/testing/sdk/src/main/java/com/google/android/enterprise/connectedapps/testing/ProfileAvailabilityPoll.java @@ -0,0 +1,45 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testing; + +import android.content.Context; +import android.os.UserHandle; +import android.os.UserManager; + +/** A class to allow blocking until a profile is available. */ +public final class ProfileAvailabilityPoll { + + private static final int POLL_FREQUENCY_MS = 1000; + private static final int POLL_TIMEOUT_MS = 30000; + + public static void blockUntilProfileRunningAndUnlocked(Context context, UserHandle userHandle) { + UserManager userManager = context.getSystemService(UserManager.class); + BlockingPoll.poll( + () -> userManager.isUserRunning(userHandle) && userManager.isUserUnlocked(userHandle), + POLL_FREQUENCY_MS, + POLL_TIMEOUT_MS); + } + + public static void blockUntilProfileNotAvailable(Context context, UserHandle userHandle) { + UserManager userManager = context.getSystemService(UserManager.class); + BlockingPoll.poll( + () -> !userManager.isUserRunning(userHandle) || userManager.isQuietModeEnabled(userHandle), + POLL_FREQUENCY_MS, + POLL_TIMEOUT_MS); + } + + private ProfileAvailabilityPoll() {} +} diff --git a/testing/sdk/src/main/res/xml/device_admin_receiver.xml b/testing/sdk/src/main/res/xml/device_admin_receiver.xml new file mode 100644 index 0000000..586504d --- /dev/null +++ b/testing/sdk/src/main/res/xml/device_admin_receiver.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<device-admin> + <support-transfer-ownership/> +</device-admin> diff --git a/tests/instrumented/src/AndroidManifest.xml b/tests/instrumented/src/AndroidManifest.xml new file mode 100644 index 0000000..e1ba8b8 --- /dev/null +++ b/tests/instrumented/src/AndroidManifest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + package="com.google.android.enterprise.connectedapps"> + + <uses-sdk + android:minSdkVersion="21" + android:targetSdkVersion="28"/> + + <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" /> + + <application> + <uses-library android:name="android.test.runner" /> + <service android:name="com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector_Service" android:exported="false" /> + </application> + + <instrumentation android:name="com.google.android.apps.common.testing.testrunner.Google3InstrumentationTestRunner" + android:targetPackage="com.google.android.enterprise.connectedapps" + android:label="Connected Apps SDK test"/> +</manifest> diff --git a/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/AvailabilityListenerTest.java b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/AvailabilityListenerTest.java new file mode 100644 index 0000000..f35f3b5 --- /dev/null +++ b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/AvailabilityListenerTest.java @@ -0,0 +1,102 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.instrumented.tests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.assertFutureHasException; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeTrue; + +import android.app.Application; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.AvailabilityListener; +import com.google.android.enterprise.connectedapps.TestAvailabilityListener; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.instrumented.utils.InstrumentedTestUtilities; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.common.util.concurrent.ListenableFuture; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link AvailabilityListener}. */ +@RunWith(JUnit4.class) +public class AvailabilityListenerTest { + private static final Application context = ApplicationProvider.getApplicationContext(); + + private final TestProfileConnector connector = TestProfileConnector.create(context); + private final InstrumentedTestUtilities utilities = + new InstrumentedTestUtilities(context, connector); + private final ProfileTestCrossProfileType type = ProfileTestCrossProfileType.create(connector); + + @Before + public void setup() { + utilities.ensureReadyForCrossProfileCalls(); + } + + @After + public void teardown() { + utilities.ensureNoWorkProfile(); + } + + @Test + public void workProfileTurnedOff_availabilityListenerFires() throws InterruptedException { + assumeTrue( + "Tests can only turn work profile on/off after O", VERSION.SDK_INT >= VERSION_CODES.P); + + utilities.ensureWorkProfileTurnedOn(); + TestAvailabilityListener availabilityListener = new TestAvailabilityListener(); + connector.registerAvailabilityListener(availabilityListener); + + utilities.turnOffWorkProfileAndWait(); + + assertThat(availabilityListener.awaitAvailabilityChange()).isGreaterThan(0); + assertThat(connector.isAvailable()).isFalse(); + } + + @Test + public void workProfileTurnedOn_availabilityListenerFires() throws InterruptedException { + assumeTrue( + "Tests can only turn work profile on/off after O", VERSION.SDK_INT >= VERSION_CODES.P); + + utilities.ensureWorkProfileTurnedOff(); + TestAvailabilityListener availabilityListener = new TestAvailabilityListener(); + connector.registerAvailabilityListener(availabilityListener); + + utilities.turnOnWorkProfileAndWait(); + + assertThat(availabilityListener.awaitAvailabilityChange()).isGreaterThan(0); + assertThat(connector.isAvailable()).isTrue(); + } + + @Test + public void temporaryConnectionError_inProgressCall_availabilityListenerFires() + throws InterruptedException { + utilities.ensureWorkProfileTurnedOn(); + TestAvailabilityListener availabilityListener = new TestAvailabilityListener(); + connector.registerAvailabilityListener(availabilityListener); + + ListenableFuture<Void> unusedFuture = type.other().killApp(); + + assertFutureHasException(unusedFuture, UnavailableProfileException.class); + assertThat(availabilityListener.awaitAvailabilityChange()).isGreaterThan(0); + assertThat(connector.isAvailable()).isTrue(); + } +} diff --git a/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/BothProfilesTest.java b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/BothProfilesTest.java new file mode 100644 index 0000000..b90ae24 --- /dev/null +++ b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/BothProfilesTest.java @@ -0,0 +1,98 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.instrumented.tests; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.Profile; +import com.google.android.enterprise.connectedapps.instrumented.utils.BlockingStringCallbackListenerMulti; +import com.google.android.enterprise.connectedapps.instrumented.utils.InstrumentedTestUtilities; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileTypeWhichNeedsContext; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests regarding calling a method on both profiles. */ +@RunWith(JUnit4.class) +public class BothProfilesTest { + private static final int FIVE_SECONDS = 5000; + private static final String STRING = "String"; + + private static final Application context = ApplicationProvider.getApplicationContext(); + + private final TestProfileConnector connector = TestProfileConnector.create(context); + private final InstrumentedTestUtilities utilities = + new InstrumentedTestUtilities(context, connector); + + private final ProfileTestCrossProfileTypeWhichNeedsContext type = + ProfileTestCrossProfileTypeWhichNeedsContext.create(connector); + + @Before + public void setup() { + utilities.ensureReadyForCrossProfileCalls(); + } + + /** This test could not be covered by Robolectric. */ + @Test + public void both_synchronous_timesOutOnWorkProfile_timeoutNotEnforcedOnSynchronousCalls() { + utilities.manuallyConnectAndWait(); + + Map<Profile, String> result = + type.both() + .timeout(FIVE_SECONDS) + .identityStringMethodWhichDelays10SecondsOnWorkProfile(STRING); + + assertThat(result).containsKey(connector.utils().getPersonalProfile()); + assertThat(result).containsKey(connector.utils().getWorkProfile()); + } + + /** This test could not be covered by Robolectric. */ + @Test + public void both_async_timesOutOnWorkProfile_onlyIncludesPersonalProfile() + throws InterruptedException { + + BlockingStringCallbackListenerMulti callbackListener = + new BlockingStringCallbackListenerMulti(); + + type.both() + .timeout(FIVE_SECONDS) + .asyncIdentityStringMethodWhichDelays10SecondsOnWorkProfile(STRING, callbackListener); + Map<Profile, String> result = callbackListener.await(); + + assertThat(result).containsKey(connector.utils().getPersonalProfile()); + assertThat(result).doesNotContainKey(connector.utils().getWorkProfile()); + } + + /** This test could not be covered by Robolectric. */ + @Test + public void both_future_timesOutOnWorkProfile_onlyIncludesPersonalProfile() + throws InterruptedException, ExecutionException { + Map<Profile, String> result = + type.both() + .timeout(FIVE_SECONDS) + .futureIdentityStringMethodWhichDelays10SecondsOnWorkProfile(STRING) + .get(); + + assertThat(result).containsKey(connector.utils().getPersonalProfile()); + assertThat(result).doesNotContainKey(connector.utils().getWorkProfile()); + } +} diff --git a/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/ConnectTest.java b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/ConnectTest.java new file mode 100644 index 0000000..0ab9c44 --- /dev/null +++ b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/ConnectTest.java @@ -0,0 +1,112 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.instrumented.tests; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import android.app.Application; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.instrumented.utils.InstrumentedTestUtilities; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Tests regarding manual connections. + * + * <p>These must be instrumented tests as they require multiple threads. + * + * <p>Tests for manual connections when not installed in the other profile are in {@link + * NotInstalledInOtherUserTest}. + */ +@RunWith(JUnit4.class) +public class ConnectTest { + private static final Application context = ApplicationProvider.getApplicationContext(); + + private final TestProfileConnector connector = TestProfileConnector.create(context); + private final InstrumentedTestUtilities utilities = + new InstrumentedTestUtilities(context, connector); + + @Before + public void setup() { + utilities.ensureReadyForCrossProfileCalls(); + } + + @Test + public void connect_connects() throws Exception { + utilities.ensureReadyForCrossProfileCalls(); + + connector.connect(); + + assertThat(connector.isConnected()).isTrue(); + } + + @Test + public void connect_startsManuallyManagingConnection() throws Exception { + utilities.ensureReadyForCrossProfileCalls(); + + connector.connect(); + + assertThat(connector.isManuallyManagingConnection()).isTrue(); + } + + @Test + public void connect_otherProfileNotAvailable_throwsUnavailableProfileException() { + utilities.ensureNoWorkProfile(); + + assertThrows(UnavailableProfileException.class, connector::connect); + } + + @Test + public void connect_otherProfileNotAvailable_doesNotConnect() { + utilities.ensureNoWorkProfile(); + + connectIgnoreExceptions(); + + assertThat(connector.isConnected()).isFalse(); + } + + @Test + public void connect_otherProfileNotAvailable_doesNotStartManuallyManagingConnection() { + utilities.ensureNoWorkProfile(); + + connectIgnoreExceptions(); + + assertThat(connector.isManuallyManagingConnection()).isFalse(); + } + + @Test + public void connect_alreadyConnected_returns() throws UnavailableProfileException { + utilities.ensureReadyForCrossProfileCalls(); + connector.connect(); + + connector.connect(); + + assertThat(connector.isConnected()).isTrue(); + } + + private void connectIgnoreExceptions() { + try { + connector.connect(); + } catch (UnavailableProfileException ignored) { + // Ignore + } + } +} diff --git a/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/HappyPathEndToEndTest.java b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/HappyPathEndToEndTest.java new file mode 100644 index 0000000..de542d1 --- /dev/null +++ b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/HappyPathEndToEndTest.java @@ -0,0 +1,152 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.instrumented.tests; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.instrumented.utils.BlockingExceptionCallbackListener; +import com.google.android.enterprise.connectedapps.instrumented.utils.BlockingStringCallbackListener; +import com.google.android.enterprise.connectedapps.instrumented.utils.InstrumentedTestUtilities; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileTypeWhichNeedsContext; +import com.google.android.enterprise.connectedapps.testing.BlockingPoll; +import java.util.concurrent.ExecutionException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Tests for high level behaviour running on a correctly configured device (with a managed profile + * with the app installed in both sides, granted INTERACT_ACROSS_USERS). + * + * <p>This tests that each type of call works in both directions. + */ +@RunWith(JUnit4.class) +public class HappyPathEndToEndTest { + private static final Application context = ApplicationProvider.getApplicationContext(); + + private static final String STRING = "String"; + + private final TestProfileConnector connector = TestProfileConnector.create(context); + private final InstrumentedTestUtilities utilities = + new InstrumentedTestUtilities(context, connector); + private final ProfileTestCrossProfileType type = ProfileTestCrossProfileType.create(connector); + private final ProfileTestCrossProfileTypeWhichNeedsContext typeWithContext = + ProfileTestCrossProfileTypeWhichNeedsContext.create(connector); + + @Before + public void setup() { + utilities.ensureReadyForCrossProfileCalls(); + } + + @After + public void teardown() { + connector.stopManualConnectionManagement(); + utilities.waitForDisconnected(); + } + + @Test + public void isAvailable_isTrue() { + assertThat(connector.isAvailable()).isTrue(); + } + + @Test + public void isConnected_isFalse() { + connector.stopManualConnectionManagement(); + utilities.waitForDisconnected(); + + assertThat(connector.isConnected()).isFalse(); + } + + @Test + public void isConnected_hasConnected_isTrue() { + utilities.manuallyConnectAndWait(); + + assertThat(connector.isConnected()).isTrue(); + } + + @Test + public void synchronousMethod_resultIsCorrect() throws UnavailableProfileException { + utilities.manuallyConnectAndWait(); + + assertThat(type.other().identityStringMethod(STRING)).isEqualTo(STRING); + } + + @Test + public void futureMethod_resultIsCorrect() throws InterruptedException, ExecutionException { + assertThat(type.other().listenableFutureIdentityStringMethod(STRING).get()).isEqualTo(STRING); + } + + @Test + public void asyncMethod_resultIsCorrect() throws InterruptedException { + BlockingStringCallbackListener stringCallbackListener = new BlockingStringCallbackListener(); + + type.other() + .asyncIdentityStringMethod( + STRING, stringCallbackListener, new BlockingExceptionCallbackListener()); + + assertThat(stringCallbackListener.await()).isEqualTo(STRING); + } + + @Test + public void synchronousMethod_fromOtherProfile_resultIsCorrect() + throws UnavailableProfileException { + utilities.manuallyConnectAndWait(); + typeWithContext.other().connectToOtherProfile(); + BlockingPoll.poll( + () -> { + try { + return typeWithContext.other().isConnectedToOtherProfile(); + } catch (UnavailableProfileException e) { + return false; + } + }, + /* pollFrequency= */ 100, + /* timeoutMillis= */ 10000); + + assertThat(typeWithContext.other().methodWhichCallsIdentityStringMethodOnOtherProfile(STRING)) + .isEqualTo(STRING); + } + + @Test + public void asyncMethod_fromOtherProfile_resultIsCorrect() throws InterruptedException { + BlockingStringCallbackListener stringCallbackListener = new BlockingStringCallbackListener(); + + typeWithContext + .other() + .asyncMethodWhichCallsIdentityStringMethodOnOtherProfile( + STRING, stringCallbackListener, new BlockingExceptionCallbackListener()); + + assertThat(stringCallbackListener.await()).isEqualTo(STRING); + } + + @Test + public void futureMethod_fromOtherProfile_resultIsCorrect() + throws ExecutionException, InterruptedException { + assertThat( + typeWithContext + .other() + .listenableFutureMethodWhichCallsIdentityStringMethodOnOtherProfile(STRING) + .get()) + .isEqualTo(STRING); + } +} diff --git a/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/InstrumentedTestUtilitiesTest.java b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/InstrumentedTestUtilitiesTest.java new file mode 100644 index 0000000..7a84852 --- /dev/null +++ b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/InstrumentedTestUtilitiesTest.java @@ -0,0 +1,97 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.instrumented.tests; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.AvailabilityListener; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testing.InstrumentedTestUtilities; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link AvailabilityListener}. */ +@RunWith(JUnit4.class) +public class InstrumentedTestUtilitiesTest { + + private static final Application context = ApplicationProvider.getApplicationContext(); + + private final TestProfileConnector connector = TestProfileConnector.create(context); + private final InstrumentedTestUtilities utilities = + new InstrumentedTestUtilities(context, connector); + + @Test + public void isAvailable_ensureReadyForCrossProfileCalls_isTrue() { + utilities.ensureReadyForCrossProfileCalls(); + + assertThat(connector.isAvailable()).isTrue(); + } + + @Test + public void isAvailable_ensureNoWorkProfile_isFalse() { + utilities.ensureReadyForCrossProfileCalls(); + + utilities.ensureNoWorkProfile(); + + assertThat(connector.isAvailable()).isFalse(); + } + + @Test + public void hasWorkProfile_createdWorkProfile_isTrue() { + utilities.ensureWorkProfileExists(); + + assertThat(utilities.hasWorkProfile()).isTrue(); + } + + @Test + public void hasWorkProfile_removedWorkProfile_isFalse() { + utilities.ensureNoWorkProfile(); + + assertThat(utilities.hasWorkProfile()).isFalse(); + } + + @Test + public void getWorkProfileUserId_createdWorkProfile_isNotZero() { + utilities.ensureWorkProfileExists(); + + assertThat(utilities.getWorkProfileUserId()).isNotEqualTo(0); + } + + @Test + public void isConnected_waitForConnected_isTrue() { + utilities.ensureReadyForCrossProfileCalls(); + + connector.startConnecting(); + utilities.waitForConnected(); + + assertThat(connector.isConnected()).isTrue(); + } + + @Test + public void isConnected_waitForDisconnected_isFalse() { + utilities.ensureReadyForCrossProfileCalls(); + connector.startConnecting(); + utilities.waitForConnected(); + + connector.stopManualConnectionManagement(); + utilities.waitForDisconnected(); + + assertThat(connector.isConnected()).isFalse(); + } +} diff --git a/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/MessageSizeTest.java b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/MessageSizeTest.java new file mode 100644 index 0000000..25be188 --- /dev/null +++ b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/MessageSizeTest.java @@ -0,0 +1,103 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.instrumented.tests; + +import static com.google.android.enterprise.connectedapps.StringUtilities.randomString; +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.TestExceptionCallbackListener; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.instrumented.utils.BlockingStringCallbackListener; +import com.google.android.enterprise.connectedapps.instrumented.utils.InstrumentedTestUtilities; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import java.util.concurrent.ExecutionException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for passing large messages across profiles. */ +@RunWith(JUnit4.class) +public class MessageSizeTest { + private static final Application context = ApplicationProvider.getApplicationContext(); + + private static final String SMALL_STRING = "String"; + private static final String LARGE_STRING = randomString(1500000); // 3Mb + + private final TestProfileConnector connector = TestProfileConnector.create(context); + private final InstrumentedTestUtilities utilities = + new InstrumentedTestUtilities(context, connector); + private final ProfileTestCrossProfileType type = ProfileTestCrossProfileType.create(connector); + + private final BlockingStringCallbackListener stringCallbackListener = + new BlockingStringCallbackListener(); + private final TestExceptionCallbackListener exceptionCallbackListener = + new TestExceptionCallbackListener(); + + @Before + public void setup() { + utilities.ensureReadyForCrossProfileCalls(); + } + + @Test + public void synchronous_smallMessage_sends() throws UnavailableProfileException { + utilities.manuallyConnectAndWait(); + + assertThat(type.other().identityStringMethod(SMALL_STRING)).isEqualTo(SMALL_STRING); + } + + @Test + public void synchronous_largeMessage_sends() throws UnavailableProfileException { + utilities.manuallyConnectAndWait(); + + // We can't use the asserts which compare Strings because of b/158998985 + assertThat(type.other().identityStringMethod(LARGE_STRING).equals(LARGE_STRING)).isTrue(); + } + + @Test + public void async_smallMessage_sends() throws InterruptedException { + type.other() + .asyncIdentityStringMethod(SMALL_STRING, stringCallbackListener, exceptionCallbackListener); + + assertThat(stringCallbackListener.await()).isEqualTo(SMALL_STRING); + } + + @Test + public void async_largeMessage_sends() throws InterruptedException { + type.other() + .asyncIdentityStringMethod(LARGE_STRING, stringCallbackListener, exceptionCallbackListener); + + // We can't use the asserts which compare Strings because of b/158998985 + assertThat(stringCallbackListener.await().equals(LARGE_STRING)).isTrue(); + } + + @Test + public void future_smallMessage_sends() throws ExecutionException, InterruptedException { + String result = type.other().listenableFutureIdentityStringMethod(SMALL_STRING).get(); + + assertThat(result).isEqualTo(SMALL_STRING); + } + + @Test + public void future_largeMessage_sends() throws ExecutionException, InterruptedException { + String result = type.other().listenableFutureIdentityStringMethod(LARGE_STRING).get(); + + assertThat(result).isEqualTo(LARGE_STRING); + } +} diff --git a/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/NotInstalledInOtherUserTest.java b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/NotInstalledInOtherUserTest.java new file mode 100644 index 0000000..b3c4b38 --- /dev/null +++ b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/NotInstalledInOtherUserTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.instrumented.tests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.assertFutureHasException; +import static org.junit.Assert.assertThrows; + +import android.app.Application; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.instrumented.utils.InstrumentedTestUtilities; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.common.util.concurrent.ListenableFuture; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Tests how the SDK behaves when running on a device with a work profile but without the + * app installed. + */ +@RunWith(JUnit4.class) +public class NotInstalledInOtherUserTest { + + private static final String STRING = "String"; + + private static final Application context = ApplicationProvider.getApplicationContext(); + + private final TestProfileConnector connector = TestProfileConnector.create(context); + private final ProfileTestCrossProfileType type = ProfileTestCrossProfileType.create(connector); + private final InstrumentedTestUtilities utilities = + new InstrumentedTestUtilities(context, connector); + + @Test + public void asyncCall_notInstalledInOtherProfile_failsFast() { + utilities.ensureWorkProfileExistsWithoutTestApp(); + + ListenableFuture<String> future = type.other().listenableFutureIdentityStringMethod(STRING); + + assertFutureHasException(future, UnavailableProfileException.class); + } + + @Test + public void connect_notInstalledInOtherProfile_failsFast() { + utilities.ensureWorkProfileExistsWithoutTestApp(); + + assertThrows(UnavailableProfileException.class, connector::connect); + } +} diff --git a/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/NotReallySerializableTest.java b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/NotReallySerializableTest.java new file mode 100644 index 0000000..abb5133 --- /dev/null +++ b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/NotReallySerializableTest.java @@ -0,0 +1,85 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.instrumented.tests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.assertFutureHasException; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import android.app.Application; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.exceptions.ProfileRuntimeException; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.instrumented.utils.BlockingExceptionCallbackListener; +import com.google.android.enterprise.connectedapps.instrumented.utils.InstrumentedTestUtilities; +import com.google.android.enterprise.connectedapps.testapp.NotReallySerializableObject; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.common.util.concurrent.ListenableFuture; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Tests regarding types which claim to be Serializable but are not. + * + * <p>This requires instrumented tests as there is no way to force the serialization in Robolectric + * tests. + */ +@RunWith(JUnit4.class) +public class NotReallySerializableTest { + private static final Application context = ApplicationProvider.getApplicationContext(); + + private final TestProfileConnector connector = TestProfileConnector.create(context); + private final InstrumentedTestUtilities utilities = + new InstrumentedTestUtilities(context, connector); + + private final ProfileTestCrossProfileType type = ProfileTestCrossProfileType.create(connector); + private final BlockingExceptionCallbackListener exceptionCallbackListener = + new BlockingExceptionCallbackListener(); + + @Before + public void setup() { + utilities.ensureReadyForCrossProfileCalls(); + } + + @Test + public void + synchronous_serializableObjectIsNotReallySerializable_throwsProfileRuntimeException() { + utilities.manuallyConnectAndWait(); + + assertThrows( + ProfileRuntimeException.class, + () -> type.other().returnNotReallySerializableObjectMethod()); + } + + @Test + public void asyncMethod_serializableObjectIsNotReallySerializable_throwsException() + throws InterruptedException { + type.other().asyncGetNotReallySerializableObjectMethod(object -> {}, exceptionCallbackListener); + + assertThat(exceptionCallbackListener.await()).isInstanceOf(UnavailableProfileException.class); + } + + @Test + public void future_serializableObjectIsNotReallySerializable_throwsException() { + ListenableFuture<NotReallySerializableObject> future = + type.other().futureGetNotReallySerializableObjectMethod(); + + assertFutureHasException(future, UnavailableProfileException.class); + } +} diff --git a/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/SecondUserTest.java b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/SecondUserTest.java new file mode 100644 index 0000000..b882aa6 --- /dev/null +++ b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/SecondUserTest.java @@ -0,0 +1,111 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.instrumented.tests; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.instrumented.utils.InstrumentedTestUtilities; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileTypeWhichNeedsContext; +import com.google.android.enterprise.connectedapps.testing.BlockingPoll; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests regarding how the SDK behaves when running on a device with a second user. */ +@RunWith(JUnit4.class) +public class SecondUserTest { + private static final Application context = ApplicationProvider.getApplicationContext(); + + private final TestProfileConnector connector = TestProfileConnector.create(context); + private final InstrumentedTestUtilities utilities = + new InstrumentedTestUtilities(context, connector); + + private final ProfileTestCrossProfileTypeWhichNeedsContext type = + ProfileTestCrossProfileTypeWhichNeedsContext.create(connector); + + @After + public void teardown() { + utilities.ensureNoWorkProfile(); + } + + @Test + public void isAvailable_noWorkProfile_hasSecondUser_isFalse() { + int secondUserId = utilities.createUser("SecondUser"); + + try { + utilities.startUser(secondUserId); + utilities.installInUser(secondUserId); + utilities.grantInteractAcrossUsers(); + + assertThat(connector.isAvailable()).isFalse(); + } finally { + utilities.removeUser(secondUserId); + } + } + + @Test + public void call_hasWorkProfile_hasSecondUser_executesOnWorkProfile() + throws UnavailableProfileException { + utilities.ensureReadyForCrossProfileCalls(); + utilities.manuallyConnectAndWait(); + int secondUserId = utilities.createUser("SecondUser"); + + try { + utilities.startUser(secondUserId); + utilities.installInUser(secondUserId); + utilities.grantInteractAcrossUsers(); + + assertThat(type.other().getUserId()).isEqualTo(utilities.getWorkProfileUserId()); + } finally { + utilities.removeUser(secondUserId); + } + } + + @Test + public void call_hasWorkProfile_hasSecondUser_fromWorkProfile_executesOnThisUser() + throws UnavailableProfileException { + utilities.ensureReadyForCrossProfileCalls(); + utilities.manuallyConnectAndWait(); + int secondUserId = utilities.createUser("SecondUser"); + + try { + utilities.startUser(secondUserId); + utilities.installInUser(secondUserId); + utilities.grantInteractAcrossUsers(); + + type.other().connectToOtherProfile(); + BlockingPoll.poll( + () -> { + try { + return type.other().isConnectedToOtherProfile(); + } catch (UnavailableProfileException e) { + return false; + } + }, + /* pollFrequency= */ 100, + /* timeoutMillis= */ 10000); + + assertThat(type.other().getOtherUserId()).isEqualTo(0); + } finally { + utilities.removeUser(secondUserId); + } + } +} diff --git a/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingBroadcastReceiver.java b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingBroadcastReceiver.java new file mode 100644 index 0000000..28baf55 --- /dev/null +++ b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingBroadcastReceiver.java @@ -0,0 +1,88 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.instrumented.utils; + +import static java.util.Collections.singleton; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import java.util.Collection; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; + +/** A {@link BroadcastReceiver} which can block until a broadcast is received. */ +public class BlockingBroadcastReceiver extends BroadcastReceiver { + private static final int DEFAULT_TIMEOUT_SECONDS = 30; + + private final BlockingQueue<Intent> blockingQueue; + private final Collection<String> expectedActions; + private final Context context; + + public BlockingBroadcastReceiver(Context context, Collection<String> expectedActions) { + this.context = context; + this.expectedActions = expectedActions; + blockingQueue = new ArrayBlockingQueue<>(/* capacity= */ 10); + } + + public BlockingBroadcastReceiver(Context context, String expectedAction) { + this(context, singleton(expectedAction)); + } + + @Override + public void onReceive(Context context, Intent intent) { + if (expectedActions.contains(intent.getAction())) { + blockingQueue.add(intent); + } + } + + /** Call before making the call which should trigger the broadcast. */ + public void register() { + for (String expectedAction : expectedActions) { + context.registerReceiver(this, new IntentFilter(expectedAction)); + } + } + + /** + * Wait until the broadcast and return the received broadcast intent. {@code null} is returned if + * no broadcast with expected action is received within 10 seconds. + */ + public Intent awaitForBroadcast() { + return awaitForBroadcast(DEFAULT_TIMEOUT_SECONDS * 1000); + } + + /** + * Wait until the broadcast and return the received broadcast intent. {@code null} is returned if + * no broadcast with expected action is received within the given timeout. + */ + public Intent awaitForBroadcast(long timeoutMillis) { + try { + return blockingQueue.poll(timeoutMillis, MILLISECONDS); + } catch (InterruptedException e) { + throw new AssertionError("Awaiting broadcast interrupted", e); + } + } + + public void unregisterQuietly() { + try { + context.unregisterReceiver(this); + } catch (RuntimeException ex) { + // ignore issues unregistering + } + } +} diff --git a/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingCallbackListener.java b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingCallbackListener.java new file mode 100644 index 0000000..6963fdd --- /dev/null +++ b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingCallbackListener.java @@ -0,0 +1,39 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.instrumented.utils; + +import java.util.concurrent.CountDownLatch; + +/** + * Base class for callback listeners which can block until a result is received. + * + * <p>To use, extend this class passing {@code E} as the type of value being received, and call + * {@link #receive(Object)} when the callback completes. + */ +public abstract class BlockingCallbackListener<E> { + private E callbackValue; + private final CountDownLatch latch = new CountDownLatch(1); + + public E await() throws InterruptedException { + latch.await(); + return callbackValue; + } + + protected void receive(E value) { + callbackValue = value; + latch.countDown(); + } +} diff --git a/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingExceptionCallbackListener.java b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingExceptionCallbackListener.java new file mode 100644 index 0000000..dcd8c03 --- /dev/null +++ b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingExceptionCallbackListener.java @@ -0,0 +1,27 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.instrumented.utils; + +import com.google.android.enterprise.connectedapps.ExceptionCallback; + +/** An {@link ExceptionCallback} which can block for a result. */ +public class BlockingExceptionCallbackListener extends BlockingCallbackListener<Throwable> + implements ExceptionCallback { + @Override + public void onException(Throwable throwable) { + receive(throwable); + } +} diff --git a/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingStringCallbackListener.java b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingStringCallbackListener.java new file mode 100644 index 0000000..ba24e68 --- /dev/null +++ b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingStringCallbackListener.java @@ -0,0 +1,27 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.instrumented.utils; + +import com.google.android.enterprise.connectedapps.testapp.TestStringCallbackListener; + +/** A {@link TestStringCallbackListener} which can block for a result. */ +public class BlockingStringCallbackListener extends BlockingCallbackListener<String> + implements TestStringCallbackListener { + @Override + public void stringCallback(String s) { + receive(s); + } +} diff --git a/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingStringCallbackListenerMulti.java b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingStringCallbackListenerMulti.java new file mode 100644 index 0000000..b7231c4 --- /dev/null +++ b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingStringCallbackListenerMulti.java @@ -0,0 +1,29 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.instrumented.utils; + +import com.google.android.enterprise.connectedapps.Profile; +import com.google.android.enterprise.connectedapps.testapp.TestStringCallbackListener_Multi; +import java.util.Map; + +public class BlockingStringCallbackListenerMulti + extends BlockingCallbackListener<Map<Profile, String>> + implements TestStringCallbackListener_Multi { + @Override + public void stringCallback(Map<Profile, String> s) { + receive(s); + } +} diff --git a/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/InstrumentedTestUtilities.java b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/InstrumentedTestUtilities.java new file mode 100644 index 0000000..e76ee9c --- /dev/null +++ b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/InstrumentedTestUtilities.java @@ -0,0 +1,353 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.instrumented.utils; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import android.content.Context; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.os.ParcelFileDescriptor; +import android.os.UserHandle; +import androidx.test.platform.app.InstrumentationRegistry; +import com.google.android.enterprise.connectedapps.ProfileConnector; +import com.google.android.enterprise.connectedapps.SharedTestUtilities; +import com.google.android.enterprise.connectedapps.instrumented.utils.ServiceCall.Parameter; +import com.google.android.enterprise.connectedapps.testing.ProfileAvailabilityPoll; +import java.io.FileInputStream; +import java.io.InputStream; +import java.util.NoSuchElementException; +import java.util.Scanner; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Wrapper around {@link + * com.google.android.enterprise.connectedapps.testing.InstrumentedTestUtilities} which adds + * features needed only by the SDK. + */ +public class InstrumentedTestUtilities { + + private final ProfileConnector connector; + private final Context context; + private final com.google.android.enterprise.connectedapps.testing.InstrumentedTestUtilities + instrumentedTestUtilities; + + private static final int R_REQUEST_QUIET_MODE_ENABLED_ID = 72; + private static final int REQUEST_QUIET_MODE_ENABLED_ID = 58; + + private static final String USER_ID_KEY = "USER_ID"; + private static final Parameter USER_ID_PARAMETER = new Parameter(USER_ID_KEY); + + private static final ServiceCall R_TURN_OFF_WORK_PROFILE_COMMAND = + new ServiceCall("user", R_REQUEST_QUIET_MODE_ENABLED_ID) + .setUser(1000) // user 1000 has packageName "android" + .addStringParam("android") // callingPackage + .addBooleanParam(true) // enableQuietMode + .addIntParam(USER_ID_PARAMETER) // userId + .addIntParam(0) // target + .addIntParam(0); // flags + + private static final ServiceCall TURN_OFF_WORK_PROFILE_COMMAND = + new ServiceCall("user", REQUEST_QUIET_MODE_ENABLED_ID) + .setUser(1000) // user 1000 has packageName "android" + .addStringParam("android") // callingPackage + .addBooleanParam(true) // enableQuietMode + .addIntParam(USER_ID_PARAMETER) // userId + .addIntParam(0); // target + + private static final ServiceCall R_TURN_ON_WORK_PROFILE_COMMAND = + new ServiceCall("user", R_REQUEST_QUIET_MODE_ENABLED_ID) + .setUser(1000) // user 1000 has packageName "android" + .addStringParam("android") // callingPackage + .addBooleanParam(false) // enableQuietMode + .addIntParam(USER_ID_PARAMETER) // userId + .addIntParam(0) // target + .addIntParam(0); // flags + + private static final ServiceCall TURN_ON_WORK_PROFILE_COMMAND = + new ServiceCall("user", REQUEST_QUIET_MODE_ENABLED_ID) + .setUser(1000) // user 1000 has packageName "android" + .addStringParam("android") // callingPackage + .addBooleanParam(false) // enableQuietMode + .addIntParam(USER_ID_PARAMETER) // userId + .addIntParam(0); // target + + public InstrumentedTestUtilities(Context context, ProfileConnector connector) { + this.context = context; + this.connector = connector; + this.instrumentedTestUtilities = + new com.google.android.enterprise.connectedapps.testing.InstrumentedTestUtilities( + context, connector); + } + + /** + * See {@link + * com.google.android.enterprise.connectedapps.testing.InstrumentedTestUtilities#hasWorkProfile()}. + */ + public boolean hasWorkProfile() { + return instrumentedTestUtilities.hasWorkProfile(); + } + + /** + * See {@link + * com.google.android.enterprise.connectedapps.testing.InstrumentedTestUtilities#getWorkProfileUserId()}. + */ + public int getWorkProfileUserId() { + return instrumentedTestUtilities.getWorkProfileUserId(); + } + + private UserHandle getWorkProfileUserHandle() { + return SharedTestUtilities.getUserHandleForUserId(getWorkProfileUserId()); + } + + /** + * See {@link + * com.google.android.enterprise.connectedapps.testing.InstrumentedTestUtilities#ensureReadyForCrossProfileCalls()}. + */ + public void ensureReadyForCrossProfileCalls() { + instrumentedTestUtilities.ensureReadyForCrossProfileCalls(); + ensureWorkProfileTurnedOn(); + } + + /** + * See {@link + * com.google.android.enterprise.connectedapps.testing.InstrumentedTestUtilities#ensureNoWorkProfile()}. + */ + public void ensureNoWorkProfile() { + instrumentedTestUtilities.ensureNoWorkProfile(); + } + + public void removeUser(int userId) { + runCommandWithOutput("pm remove-user " + userId); + } + + public void installInUser(int userId) { + runCommandWithOutput( + "cmd package install-existing --user " + userId + " " + context.getPackageName()); + } + + /** + * Grant the {@code INTERACT_ACROSS_USERS} permission if this app declares it. + * + * <p>This is required before cross-profile interaction will work. + */ + public void grantInteractAcrossUsers() { + // TODO(scottjonathan): Support INTERACT_ACROSS_PROFILES in these tests. + runCommandWithOutput( + "pm grant " + context.getPackageName() + " android.permission.INTERACT_ACROSS_USERS"); + } + + /** + * See {@link + * com.google.android.enterprise.connectedapps.testing.InstrumentedTestUtilities#ensureWorkProfileExists()} + */ + public void ensureWorkProfileExists() { + instrumentedTestUtilities.ensureWorkProfileExists(); + } + + /** + * Create a work profile but do not install the test app. + * + * <p>This means that, as there is no profile owner, it will not be recognised as a work profile + * by the SDK when running on that profile. + */ + public void ensureWorkProfileExistsWithoutTestApp() { + if (hasWorkProfile()) { + if (!userHasPackageInstalled(getWorkProfileUserId(), context.getPackageName())) { + return; + } + + // TODO(162219825): Try to remove the package + + throw new IllegalStateException( + "There is already a work profile on the device with user id " + + getWorkProfileUserId() + + "."); + } + runCommandWithOutput("pm create-user --profileOf 0 --managed TestProfile123"); + int workProfileUserId = getWorkProfileUserId(); + startUser(workProfileUserId); + } + + private static boolean userHasPackageInstalled(int userId, String packageName) { + String expectedPackageLine = "package:" + packageName; + String[] installedPackages = + runCommandWithOutput("pm list packages --user " + userId).split("\n"); + for (String installedPackage : installedPackages) { + if (installedPackage.equals(expectedPackageLine)) { + return true; + } + } + return false; + } + + /** Ensure that the work profile is running. */ + public void ensureWorkProfileTurnedOn() { + turnOnWorkProfileAndWait(); + } + + /** Ensure that the work profile is not running. */ + public void ensureWorkProfileTurnedOff() { + turnOffWorkProfileAndWait(); + } + + /** + * Turn off the work profile and block until it has been turned off. + * + * <p>This uses {@link ServiceCall} and so is only guaranteed to work correctly on AOSP. + * + * @see #turnOffWorkProfile() + */ + public void turnOffWorkProfileAndWait() { + turnOffWorkProfile(); + + ProfileAvailabilityPoll.blockUntilProfileNotAvailable(context, getWorkProfileUserHandle()); + } + + // TODO(160147511): Remove use of service calls for versions after R + /** + * Turn off the work profile + * + * <p>This uses {@link ServiceCall} and so is only guaranteed to work correctly on AOSP. + * + * @see #turnOffWorkProfileAndWait() + */ + public void turnOffWorkProfile() { + if (VERSION.SDK_INT == VERSION_CODES.R) { + runCommandWithOutput( + R_TURN_OFF_WORK_PROFILE_COMMAND + .prepare() + .setInt(USER_ID_KEY, getWorkProfileUserId()) + .getCommand()); + } else if (VERSION.SDK_INT == VERSION_CODES.Q || VERSION.SDK_INT == VERSION_CODES.P) { + runCommandWithOutput( + TURN_OFF_WORK_PROFILE_COMMAND + .prepare() + .setInt(USER_ID_KEY, getWorkProfileUserId()) + .getCommand()); + } else { + throw new IllegalStateException("Cannot turn off work on this version of android"); + } + } + + /** + * Turn on the work profile and block until it has been turned on. + * + * <p>This uses {@link ServiceCall} and so is only guaranteed to work correctly on AOSP. + * + * @see #turnOnWorkProfile() + */ + public void turnOnWorkProfileAndWait() { + if (connector.isAvailable()) { + return; // Already on + } + + turnOnWorkProfile(); + + ProfileAvailabilityPoll.blockUntilProfileRunningAndUnlocked( + context, getWorkProfileUserHandle()); + } + + // TODO(160147511): Remove use of service calls for versions after R + /** + * Turn on the work profile and block until it has been turned on. + * + * <p>This uses {@link ServiceCall} and so is only guaranteed to work correctly on AOSP. + * + * @see #turnOnWorkProfileAndWait() + */ + public void turnOnWorkProfile() { + if (VERSION.SDK_INT == VERSION_CODES.R) { + runCommandWithOutput( + R_TURN_ON_WORK_PROFILE_COMMAND + .prepare() + .setInt(USER_ID_KEY, getWorkProfileUserId()) + .getCommand()); + } else if (VERSION.SDK_INT == VERSION_CODES.Q || VERSION.SDK_INT == VERSION_CODES.P) { + runCommandWithOutput( + TURN_ON_WORK_PROFILE_COMMAND + .prepare() + .setInt(USER_ID_KEY, getWorkProfileUserId()) + .getCommand()); + } else { + throw new IllegalStateException("Cannot turn on work on this version of android"); + } + } + + /** + * See {@link + * com.google.android.enterprise.connectedapps.testing.InstrumentedTestUtilities#waitForDisconnected()}. + */ + public void waitForDisconnected() { + instrumentedTestUtilities.waitForDisconnected(); + } + + /** + * See {@link + * com.google.android.enterprise.connectedapps.testing.InstrumentedTestUtilities#waitForConnected()}. + */ + public void waitForConnected() { + instrumentedTestUtilities.waitForConnected(); + } + + /** + * Manually call {@link ProfileConnector#startConnecting()} and wait for connection to be + * complete. + */ + public void manuallyConnectAndWait() { + connector.startConnecting(); + waitForConnected(); + } + + private static final Pattern CREATE_USER_PATTERN = + Pattern.compile("Success: created user id (\\d+)"); + + public int createUser(String username) { + String output = runCommandWithOutput("pm create-user " + username); + + Matcher userMatcher = CREATE_USER_PATTERN.matcher(output); + if (userMatcher.find()) { + return Integer.parseInt(userMatcher.group(1)); + } + + throw new IllegalStateException("Could not create user. Output: " + output); + } + + public void startUser(int userId) { + UserHandle userHandle = SharedTestUtilities.getUserHandleForUserId(userId); + InstrumentedTestUtilities.runCommandWithOutput("am start-user " + userId); + ProfileAvailabilityPoll.blockUntilProfileRunningAndUnlocked(context, userHandle); + } + + private static String runCommandWithOutput(String command) { + ParcelFileDescriptor p = runCommand(command); + + InputStream inputStream = new FileInputStream(p.getFileDescriptor()); + + try (Scanner scanner = new Scanner(inputStream, UTF_8.name())) { + return scanner.useDelimiter("\\A").next(); + } catch (NoSuchElementException e) { + return ""; + } + } + + private static ParcelFileDescriptor runCommand(String command) { + return InstrumentationRegistry.getInstrumentation() + .getUiAutomation() + .executeShellCommand(command); + } +} diff --git a/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/ServiceCall.java b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/ServiceCall.java new file mode 100644 index 0000000..ae6e479 --- /dev/null +++ b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/ServiceCall.java @@ -0,0 +1,201 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.instrumented.utils; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * Class used when building a service call command to be used on the shell. + * + * <p>These commands are likely to break in future android versions so should be replaced as soon as + * possible. + */ +class ServiceCall { + + enum DataType { + STRING, + INT, + BOOLEAN + } + + /** This reflects a parameter which is passed in to complete the service call command */ + static class Parameter { + + final String name; + + public Parameter(String name) { + this.name = name; + } + + String placeholder() { + return "{{" + name + "}}"; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Parameter)) { + return false; + } + Parameter parameter = (Parameter) o; + return Objects.equals(name, parameter.name); + } + + @Override + public int hashCode() { + return Objects.hashCode(name); + } + } + + static class PreparedServiceCall { + private final ServiceCall serviceCall; + private final Map<Parameter, String> setParameters = new HashMap<>(); + + private PreparedServiceCall(ServiceCall serviceCall) { + this.serviceCall = serviceCall; + } + + public PreparedServiceCall setString(String key, String value) { + Parameter keyParameter = new Parameter(key); + if (!serviceCall.parameters.containsKey(keyParameter)) { + throw new IllegalStateException("No such key " + key); + } + if (serviceCall.parameters.get(keyParameter) != DataType.STRING) { + throw new IllegalStateException(key + " is not a String"); + } + setParameters.put(keyParameter, value); + return this; + } + + public PreparedServiceCall setInt(String key, int value) { + Parameter keyParameter = new Parameter(key); + if (!serviceCall.parameters.containsKey(keyParameter)) { + throw new IllegalStateException( + "No such key " + key + " valid keys " + serviceCall.parameters.keySet()); + } + if (serviceCall.parameters.get(keyParameter) != DataType.INT) { + throw new IllegalStateException(key + " is not an int"); + } + setParameters.put(keyParameter, Integer.toString(value)); + return this; + } + + public PreparedServiceCall setBoolean(String key, boolean value) { + Parameter keyParameter = new Parameter(key); + if (!serviceCall.parameters.containsKey(keyParameter)) { + throw new IllegalStateException("No such key " + key); + } + if (serviceCall.parameters.get(keyParameter) != DataType.BOOLEAN) { + throw new IllegalStateException(key + " is not a boolean"); + } + setParameters.put(keyParameter, value ? "1" : "0"); + return this; + } + + public String getCommand() { + Set<Parameter> parametersToSet = new HashSet<>(serviceCall.parameters.keySet()); + parametersToSet.removeAll(setParameters.keySet()); + + if (!parametersToSet.isEmpty()) { + throw new IllegalStateException("Unset parameters: " + parametersToSet); + } + + String command = serviceCall.getCommandUnchecked(); + for (Map.Entry<Parameter, String> entry : setParameters.entrySet()) { + command = command.replace(entry.getKey().placeholder(), entry.getValue()); + } + + return command; + } + } + + private Integer user; + private final String serviceName; + private final int methodId; + private final Map<Parameter, DataType> parameters = new HashMap<>(); + + private final StringBuilder commandBuilder = new StringBuilder(); + + ServiceCall(String serviceName, int methodId) { + this.serviceName = serviceName; + this.methodId = methodId; + } + + ServiceCall addStringParam(String value) { + commandBuilder.append(" s16 ").append(value); + return this; + } + + ServiceCall addStringParam(Parameter value) { + commandBuilder.append(" s16 ").append(value.placeholder()); + parameters.put(value, DataType.STRING); + return this; + } + + ServiceCall addIntParam(int value) { + commandBuilder.append(" i32 ").append(value); + return this; + } + + ServiceCall addIntParam(Parameter value) { + commandBuilder.append(" i32 ").append(value.placeholder()); + parameters.put(value, DataType.INT); + return this; + } + + ServiceCall addBooleanParam(boolean value) { + return addIntParam(value ? 1 : 0); + } + + ServiceCall addBooleanParam(Parameter value) { + commandBuilder.append(" i32 ").append(value.placeholder()); + parameters.put(value, DataType.BOOLEAN); + return this; + } + + ServiceCall setUser(int user) { + this.user = user; + return this; + } + + PreparedServiceCall prepare() { + return new PreparedServiceCall(this); + } + + String getCommand() { + if (!parameters.isEmpty()) { + throw new IllegalStateException("This ServiceCall requires parameters, use #prepare"); + } + + return getCommandUnchecked(); + } + + private String getCommandUnchecked() { + String cmd = "service call " + serviceName + " " + methodId + commandBuilder; + + if (user != null) { + cmd = "su " + user + " " + cmd; + } + + return cmd; + } +} diff --git a/tests/processor/src/main/AndroidManifest.xml b/tests/processor/src/main/AndroidManifest.xml new file mode 100644 index 0000000..0669799 --- /dev/null +++ b/tests/processor/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" package="com.google.android.enterprise.connectedapps.testing"> + <uses-sdk + android:minSdkVersion="14" + android:targetSdkVersion="28"/> +</manifest> diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/AlwaysThrowsTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/AlwaysThrowsTest.java new file mode 100644 index 0000000..08e4e2a --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/AlwaysThrowsTest.java @@ -0,0 +1,89 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class AlwaysThrowsTest { + + private final AnnotationPrinter annotationPrinter; + + public AlwaysThrowsTest(AnnotationPrinter annotationPrinter) { + this.annotationPrinter = annotationPrinter; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void compile_generatesAlwaysThrowsClass() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation).generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_AlwaysThrows"); + } + + @Test + public void compile_alwaysThrowsClassImplementsSingleSender() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_AlwaysThrows") + .contentsAsUtf8String() + .contains( + "class ProfileNotesType_AlwaysThrows implements" + " ProfileNotesType_SingleSender"); + } + + @Test + public void compile_alwaysThrowsClassHasConstructorTakingErrorMessage() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_AlwaysThrows") + .contentsAsUtf8String() + .contains("public ProfileNotesType_AlwaysThrows(String errorMessage)"); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/BundlerTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/BundlerTest.java new file mode 100644 index 0000000..ca62a9d --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/BundlerTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class BundlerTest { + + private final AnnotationPrinter annotationPrinter; + + public BundlerTest(AnnotationPrinter annotationPrinter) { + this.annotationPrinter = annotationPrinter; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void generatesBundlerClass() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation).generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_Bundler"); + } + + @Test + public void bundlerClassImplementsBundler() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_Bundler") + .contentsAsUtf8String() + .contains("ProfileNotesType_Bundler implements Bundler"); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileCallbackSupportedParameterTypeTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileCallbackSupportedParameterTypeTest.java new file mode 100644 index 0000000..136fa36 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileCallbackSupportedParameterTypeTest.java @@ -0,0 +1,120 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.PARCELABLE_OBJECT; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.SERIALIZABLE_OBJECT; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.combineParameters; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileCallback; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import java.util.Arrays; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/** + * Test supported parameter types for methods on an {@link CrossProfileCallback} annotated + * interface. + * + * <p>This tests a single parameter of each supported type. Multiple parameters and unsupported + * types are tested in {@link CrossProfileCallbackTest}. + */ +@RunWith(Parameterized.class) +public class CrossProfileCallbackSupportedParameterTypeTest { + + @Parameters(name = "{0} for {1}") + public static Iterable<Object[]> data() { + String[] types = { + "String", + "String[]", + "byte", + "Byte", + "short", + "Short", + "int", + "Integer", + "long", + "Long", + "float", + "Float", + "double", + "Double", + "char", + "Character", + "boolean", + "Boolean", + "ParcelableObject", + "ParcelableObject[]", + "java.util.List<ParcelableObject>", + "java.util.List<ParcelableObject[]>", + "java.util.List<java.util.List<ParcelableObject>>", + "java.util.List<SerializableObject>", + "java.util.List<SerializableObject[]>", + "java.util.List<java.util.List<SerializableObject>>", + "java.util.List<String>", + "java.util.List<String[]>", + "java.util.Map<String, String>", + "java.util.Set<String>", + "java.util.Collection<String>", + "com.google.protos.connectedappssdk.TestProtoOuterClass.TestProto", + "com.google.protos.connectedappssdk.TestProtoOuterClass.TestProto[]", + "java.util.List<com.google.protos.connectedappssdk.TestProtoOuterClass.TestProto>", + "com.google.common.collect.ImmutableMap<String, String>", + "android.util.Pair<String, Integer>", + "com.google.common.base.Optional<ParcelableObject>" + }; + return combineParameters(AnnotationFinder.annotationStrings(), Arrays.asList(types)); + } + + private final AnnotationPrinter annotationPrinter; + + private final String type; + + public CrossProfileCallbackSupportedParameterTypeTest( + AnnotationPrinter annotationPrinter, String type) { + this.annotationPrinter = annotationPrinter; + this.type = type; + } + + @Test + public void crossProfileCallbackInterfaceWithSupportedParameterType_compiles() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileCallbackQualifiedName() + ";", + annotationPrinter.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete(" + type + " param);", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(callbackInterface, PARCELABLE_OBJECT, SERIALIZABLE_OBJECT); + + assertThat(compilation).succeededWithoutWarnings(); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileCallbackTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileCallbackTest.java new file mode 100644 index 0000000..adc1c3d --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileCallbackTest.java @@ -0,0 +1,847 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.CUSTOM_WRAPPER; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.GENERIC_PARCELABLE_OBJECT; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.GENERIC_SERIALIZABLE_OBJECT; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.PARCELABLE_CUSTOM_WRAPPER; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.UNSUPPORTED_TYPE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.UNSUPPORTED_TYPE_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.formatErrorMessage; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.installationListener; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.installationListenerWithListStringParam; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.installationListenerWithStringParam; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.notesCrossProfileTypeWhichUsesInstallationListener; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class CrossProfileCallbackTest { + + private static final String UNSUPPORTED_PARAMETER_TYPE_ERROR = + "cannot be used by parameters of methods on interfaces annotated" + + " @CROSS_PROFILE_CALLBACK_ANNOTATION"; + private static final String CALLBACK_INTERFACE_DEFAULT_PACKAGE_ERROR = + "Interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION must not be in the default package"; + private static final String NOT_INTERFACE_ERROR = + "Only interfaces may be annotated @CROSS_PROFILE_CALLBACK_ANNOTATION"; + private static final String NO_METHODS_ERROR = + "Interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION must have at least one method"; + private static final String NOT_ONE_METHOD_ERROR = + "Interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION(simple=true) must have exactly one" + + " method"; + private static final String DEFAULT_METHOD_ERROR = + "Interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION must have no default methods"; + private static final String STATIC_METHOD_ERROR = + "Interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION must have no static methods"; + private static final String NOT_VOID_ERROR = + "Methods on interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION must return void"; + private static final String GENERIC_CALLBACK_INTERFACE_ERROR = + "Interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION can not be generic"; + private static final String MORE_THAN_ONE_PARAMETER_ERROR = + "Methods on interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION(simple=true) can only" + + " take a single parameter"; + + private final AnnotationStrings annotationStrings; + + public CrossProfileCallbackTest(AnnotationStrings annotationStrings) { + this.annotationStrings = annotationStrings; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void crossProfileCallbackInterface_compiles() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete(int state);", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(callbackInterface); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileCallbackInterface_defaultPackage_hasError() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + "InstallationListener", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete(int state);", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(callbackInterface); + + assertThat(compilation) + .hadErrorContaining( + formatErrorMessage(CALLBACK_INTERFACE_DEFAULT_PACKAGE_ERROR, annotationStrings)) + .inFile(callbackInterface); + } + + @Test + public void crossProfileCallbackInterface_notInterface_hasError() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public abstract class InstallationListener {", + " abstract void installationComplete(int state);", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(callbackInterface); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(NOT_INTERFACE_ERROR, annotationStrings)) + .inFile(callbackInterface); + } + + @Test + public void crossProfileCallbackInterface_noMethods_hasError() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(callbackInterface); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(NO_METHODS_ERROR, annotationStrings)) + .inFile(callbackInterface); + } + + @Test + public void crossProfileCallbackInterface_simple_moreThanOneMethod_hasError() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation("simple=true"), + "public interface InstallationListener {", + " abstract void installationComplete(int state);", + " abstract void secondMethod(String s);", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(callbackInterface); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(NOT_ONE_METHOD_ERROR, annotationStrings)) + .inFile(callbackInterface); + } + + @Test + public void crossProfileCallbackInterface_moreThanOneMethod_compiles() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " abstract void installationComplete(int state);", + " abstract void secondMethod(String s);", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(callbackInterface); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileCallbackInterface_defaultMethod_hasError() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + "default void defaultMethod() {};", + " void installationComplete(int state);", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(callbackInterface); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(DEFAULT_METHOD_ERROR, annotationStrings)) + .inFile(callbackInterface); + } + + @Test + public void crossProfileCallbackInterface_staticMethod_hasError() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + "static void staticMethod() {};", + " void installationComplete(int state);", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(callbackInterface); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(STATIC_METHOD_ERROR, annotationStrings)) + .inFile(callbackInterface); + } + + @Test + public void crossProfileCallbackInterface_nonVoidReturnType_hasError() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " int installationComplete(int state);", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(callbackInterface); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(NOT_VOID_ERROR, annotationStrings)) + .inFile(callbackInterface); + } + + @Test + public void multipleSupportedParameters_simple_hasError() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation("simple=true"), + "public interface InstallationListener {", + " void installationComplete(String s, String t);", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(callbackInterface); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(MORE_THAN_ONE_PARAMETER_ERROR, annotationStrings)) + .inFile(callbackInterface); + } + + @Test + public void multipleSupportedParameters_notSimple_compiles() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete(String s, String t);", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(callbackInterface); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileCallbackInterfaceMethodWithUnsupportedParameterType_notUsed_compiles() { + // Cross-profile callbacks are only evaluated in the context of a Cross-profile Type + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete(" + UNSUPPORTED_TYPE_NAME + " s);", + "}"); + + Compilation compilation = + javac().withProcessors(new Processor()).compile(callbackInterface, UNSUPPORTED_TYPE); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void + crossProfileCallbackInterfaceMethodWithUnsupportedParameterTypeInGeneric_notUsed_compiles() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete(java.util.List<" + UNSUPPORTED_TYPE_NAME + "> s);", + "}"); + + Compilation compilation = + javac().withProcessors(new Processor()).compile(callbackInterface, UNSUPPORTED_TYPE); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void + crossProfileCallbackInterfaceMethodWithUnsupportedParameterTypeInGeneric_isUsed_hasError() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete(java.util.List<" + UNSUPPORTED_TYPE_NAME + "> s);", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + callbackInterface, + UNSUPPORTED_TYPE, + notesCrossProfileTypeWhichUsesInstallationListener(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_PARAMETER_TYPE_ERROR, annotationStrings)) + .inFile(callbackInterface); + } + + @Test + public void crossProfileCallbackInterfaceMethodWithContextParameterTypeInGeneric_hasError() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + "import android.content.Context;", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete(java.util.List<Context> s);", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + callbackInterface, + UNSUPPORTED_TYPE, + notesCrossProfileTypeWhichUsesInstallationListener(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_PARAMETER_TYPE_ERROR, annotationStrings)) + .inFile(callbackInterface); + } + + @Test + public void genericCrossProfileCallbackMethod_hasError() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener<R> {", + " void installationComplete(String s);", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(callbackInterface); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(GENERIC_CALLBACK_INTERFACE_ERROR, annotationStrings)) + .inFile(callbackInterface); + } + + @Test + public void + crossProfileCallbackInterfaceWithUnsupportedParameterTypeInGenericParcelable_compiles() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " public void installationComplete(GenericParcelableObject<" + + UNSUPPORTED_TYPE_NAME + + "> s);", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(callbackInterface, UNSUPPORTED_TYPE, GENERIC_PARCELABLE_OBJECT); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void + crossProfileCallbackInterfaceWithUnsupportedParameterTypeInGenericSerializable_compiles() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " public void installationComplete(GenericSerializableObject<" + + UNSUPPORTED_TYPE_NAME + + "> s);", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(callbackInterface, UNSUPPORTED_TYPE, GENERIC_SERIALIZABLE_OBJECT); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void + crossProfileCallbackInterface_hasCrossProfileCallbackInterfaceParameter_notUsed_compiles() { + JavaFileObject otherCallbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".OtherCallbackInterface", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface OtherCallbackInterface {", + " void installationComplete(String s);", + "}"); + + JavaFileObject installationListener = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete(OtherCallbackInterface s);", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(installationListener, otherCallbackInterface); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void + crossProfileCallbackInterface_hasCrossProfileCallbackInterfaceParameter_isUsed_hasError() { + JavaFileObject otherCallbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".OtherCallbackInterface", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface OtherCallbackInterface {", + " void installationComplete(String s);", + "}"); + + JavaFileObject installationListener = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete(OtherCallbackInterface s);", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + installationListener, + otherCallbackInterface, + notesCrossProfileTypeWhichUsesInstallationListener(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_PARAMETER_TYPE_ERROR, annotationStrings)) + .inFile(installationListener); + } + + @Test + public void crossProfileCallbackInterface_generatesMultiClass() { + Compilation compilation = + javac().withProcessors(new Processor()).compile(installationListener(annotationStrings)); + + assertThat(compilation) + .generatedSourceFile("com.google.android.enterprise.notes.InstallationListener_Multi"); + } + + @Test + public void crossProfileCallbackInterface_multiClassIncludesNoArgsMethod() { + JavaFileObject installationListener = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete();", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(installationListener); + + assertThat(compilation) + .generatedSourceFile("com.google.android.enterprise.notes.InstallationListener_Multi") + .contentsAsUtf8String() + .contains("void installationComplete();"); + } + + @Test + public void crossProfileCallbackInterface_multiClassIncludesSingleArgMethod() { + JavaFileObject installationListener = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete(String s);", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(installationListener); + + assertThat(compilation) + .generatedSourceFile("com.google.android.enterprise.notes.InstallationListener_Multi") + .contentsAsUtf8String() + .contains("void installationComplete(Map<Profile, String> s);"); + } + + @Test + public void crossProfileCallbackInterface_generatesReceiverClass() { + Compilation compilation = + javac().withProcessors(new Processor()).compile(installationListener(annotationStrings)); + + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.notes.Profile_InstallationListener_Receiver"); + } + + @Test + public void crossProfileCallbackInterface_receiverClassImplementsOriginalInterface() { + Compilation compilation = + javac().withProcessors(new Processor()).compile(installationListener(annotationStrings)); + + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.notes.Profile_InstallationListener_Receiver") + .contentsAsUtf8String() + .contains("implements InstallationListener"); + } + + @Test + public void crossProfileCallbackInterface_generatesSenderClass() { + Compilation compilation = + javac().withProcessors(new Processor()).compile(installationListener(annotationStrings)); + + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.notes.Profile_InstallationListener_Sender"); + } + + @Test + public void crossProfileCallbackInterface_senderClassImplementsLocalCallback() { + Compilation compilation = + javac().withProcessors(new Processor()).compile(installationListener(annotationStrings)); + + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.notes.Profile_InstallationListener_Sender") + .contentsAsUtf8String() + .contains("implements LocalCallback"); + } + + @Test + public void crossProfileCallbackInterface_generatesMultiMergerInputClass() { + Compilation compilation = + javac().withProcessors(new Processor()).compile(installationListener(annotationStrings)); + + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.notes.Profile_InstallationListener_MultiMergerInput"); + } + + @Test + public void crossProfileCallbackInterface_multiMergerInputClassImplementsInterface() { + Compilation compilation = + javac().withProcessors(new Processor()).compile(installationListener(annotationStrings)); + + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.notes.Profile_InstallationListener_MultiMergerInput") + .contentsAsUtf8String() + .contains("implements InstallationListener"); + } + + @Test + public void crossProfileCallbackInterface_generatesMultiMergerResultClass() { + Compilation compilation = + javac().withProcessors(new Processor()).compile(installationListener(annotationStrings)); + + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.notes.Profile_InstallationListener_MultiMergerResult"); + } + + @Test + public void + crossProfileCallbackInterface_multiMergerResultClassImplementsCompleteListenerInterface() { + Compilation compilation = + javac().withProcessors(new Processor()).compile(installationListener(annotationStrings)); + + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.notes.Profile_InstallationListener_MultiMergerResult") + .contentsAsUtf8String() + .contains("implements CrossProfileCallbackMultiMergerCompleteListener<Void>"); + } + + @Test + public void crossProfileCallbackInterface_withArgumentCompilesSuccessfully() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void doInstall(InstallationListener l) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationStrings), + installationListener(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileCallbackInterface_argumentTypeIsIncludedInBundler() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void doInstall(InstallationListener l) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationStrings), + installationListenerWithStringParam(annotationStrings)); + + assertThat(compilation) + .generatedSourceFile("com.google.android.enterprise.notes.ProfileNotesType_Bundler") + .contentsAsUtf8String() + .contains(".writeString"); + } + + @Test + public void + crossProfileCallbackInterfaceWithMultipleMethods_allArgumentTypesAreIncludedInBundler() { + JavaFileObject installationListener = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete(String s, Float f);", + " void installationFailed(Boolean b, Byte p);", + "}"); + + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void doInstall(InstallationListener l) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings), installationListener); + + assertThat(compilation) + .generatedSourceFile("com.google.android.enterprise.notes.ProfileNotesType_Bundler") + .contentsAsUtf8String() + .contains(".writeString"); + + assertThat(compilation) + .generatedSourceFile("com.google.android.enterprise.notes.ProfileNotesType_Bundler") + .contentsAsUtf8String() + .contains(".writeFloat"); + + assertThat(compilation) + .generatedSourceFile("com.google.android.enterprise.notes.ProfileNotesType_Bundler") + .contentsAsUtf8String() + .contains(".writeInt"); // used for Boolean + + assertThat(compilation) + .generatedSourceFile("com.google.android.enterprise.notes.ProfileNotesType_Bundler") + .contentsAsUtf8String() + .contains(".writeByte"); + } + + @Test + public void crossProfileCallbackInterface_wrappedArgumentTypeIsIncludedInBundler() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void doInstall(InstallationListener l) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationStrings), + installationListenerWithListStringParam(annotationStrings)); + + assertThat(compilation) + .generatedSourceFile("com.google.android.enterprise.notes.ProfileNotesType_Bundler") + .contentsAsUtf8String() + .contains("ParcelableList"); + } + + @Test + public void + crossProfileCallbackInterfaceMethodWithCustomParcelableWrapperParameterType_compiles() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete(CustomWrapper s);", + "}"); + + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation( + "connector=CrossProfileConnectorWhichSupportsCustomWrapper.class"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void install(InstallationListener l) {", + " }", + "}"); + + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".CrossProfileConnectorWhichSupportsCustomWrapper", + "package " + NOTES_PACKAGE + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(parcelableWrappers={ParcelableCustomWrapper.class})", + "public interface CrossProfileConnectorWhichSupportsCustomWrapper extends" + + " ProfileConnector {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + callbackInterface, + CUSTOM_WRAPPER, + PARCELABLE_CUSTOM_WRAPPER, + crossProfileType, + connector); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_PARAMETER_TYPE_ERROR, annotationStrings)) + .inFile(callbackInterface); + } + + @Test + public void interfaceMarkedSimple_isSimple_compiles() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation("simple=true"), + "public interface InstallationListener {", + " void installationComplete(String s);", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(callbackInterface); + + assertThat(compilation).succeededWithoutWarnings(); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileProviderClassTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileProviderClassTest.java new file mode 100644 index 0000000..08c19a2 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileProviderClassTest.java @@ -0,0 +1,321 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.ANNOTATED_NOTES_CONNECTOR; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.formatErrorMessage; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.staticType; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class CrossProfileProviderClassTest { + + private static final String INVALID_CONSTRUCTORS_ERROR = + "Provider classes must have a single public constructor which takes either a single Context" + + " argument or no arguments"; + private static final String PROVIDER_CLASS_DIFFERENT_CONNECTOR_ERROR = + "All @CROSS_PROFILE_ANNOTATION types provided by a provider class must use the same" + + " ProfileConnector"; + private static final String STATICTYPES_ERROR = + "@CROSS_PROFILE_ANNOTATION classes referenced in @CROSS_PROFILE_PROVIDER_ANNOTATION" + + " staticTypes annotations must not have non-static @CROSS_PROFILE_ANNOTATION annotated" + + " methods"; + + private final AnnotationStrings annotationStrings; + + public CrossProfileProviderClassTest(AnnotationStrings annotationStrings) { + this.annotationStrings = annotationStrings; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void hasACustomNoArgsConstructor_compiles() { + JavaFileObject providerClass = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + " public NotesProvider() {", + " }", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(providerClass, annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void hasNonPublicNoArgsConstructor_hasError() { + JavaFileObject providerClass = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + " NotesProvider() {", + " }", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(providerClass, annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(INVALID_CONSTRUCTORS_ERROR, annotationStrings)) + .inFile(providerClass); + } + + @Test + public void hasNoNoArgsConstructor_hasError() { + JavaFileObject providerClass = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + " public NotesProvider(String p) {", + " }", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(providerClass, annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(INVALID_CONSTRUCTORS_ERROR, annotationStrings)) + .inFile(providerClass); + } + + @Test + public void hasPublicConstructorTakingContext_compiles() { + JavaFileObject providerClass = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import android.content.Context;", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + " public NotesProvider(Context c) {", + " }", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(providerClass, annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void hasMoreThanOnePublicConstructor_errors() { + JavaFileObject providerClass = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import android.content.Context;", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + " public NotesProvider(Context c) {", + " }", + " public NotesProvider() {", + " }", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(providerClass, annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(INVALID_CONSTRUCTORS_ERROR, annotationStrings)) + .inFile(providerClass); + } + + @Test + public void hasCrossProfileTypesWithDifferentConnectors_hasError() { + JavaFileObject providerClass = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType2 provideNotesType2() {", + " return new NotesType2();", + " }", + "}"); + + JavaFileObject notesTypeWithCrossProfileConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import com.google.android.enterprise.connectedapps.CrossProfileConnector;", + annotationStrings.crossProfileAsAnnotation("connector=CrossProfileConnector.class"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + JavaFileObject notesType2WithNotesConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType2", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation("connector=NotesConnector.class"), + "public final class NotesType2 {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + providerClass, + notesTypeWithCrossProfileConnector, + notesType2WithNotesConnector, + ANNOTATED_NOTES_CONNECTOR); + + assertThat(compilation) + .hadErrorContaining( + formatErrorMessage(PROVIDER_CLASS_DIFFERENT_CONNECTOR_ERROR, annotationStrings)) + .inFile(providerClass); + } + + @Test + public void staticTypes_onlyReferencesStaticTypes_compiles() { + JavaFileObject notesProvider = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "import com.google.android.enterprise.connectedapps.CrossProfileConnector;", + annotationStrings.crossProfileProviderAsAnnotation("staticTypes={StaticType.class}"), + "public final class NotesProvider {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesProvider, staticType(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void staticTypes_referencesNonStaticType_hasError() { + JavaFileObject notesProvider = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "import com.google.android.enterprise.connectedapps.CrossProfileConnector;", + annotationStrings.crossProfileProviderAsAnnotation("staticTypes={NotesType.class}"), + "public final class NotesProvider {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesProvider, annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(STATICTYPES_ERROR, annotationStrings)) + .inFile(notesProvider); + } + + @Test + public void staticProvidedClass_usedTypeIsIncludedInBundler() { + JavaFileObject notesProvider = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "import com.google.android.enterprise.connectedapps.CrossProfileConnector;", + annotationStrings.crossProfileProviderAsAnnotation("staticTypes={StaticType.class}"), + "public final class NotesProvider {", + "}"); + JavaFileObject staticType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".StaticType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class StaticType {", + annotationStrings.crossProfileAsAnnotation(), + " public static void refreshNotes(String param) {", + " }", + "}"); + + Compilation compilation = + javac().withProcessors(new Processor()).compile(notesProvider, staticType); + + assertThat(compilation) + .generatedSourceFile("com.google.android.enterprise.notes.ProfileStaticType_Bundler") + .contentsAsUtf8String() + .contains("parcel.writeString("); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileProviderTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileProviderTest.java new file mode 100644 index 0000000..9e37596 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileProviderTest.java @@ -0,0 +1,337 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.formatErrorMessage; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.staticType; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class CrossProfileProviderTest { + + private static final String MULTIPLE_PROVIDERS_ERROR = "has been provided more than once"; + private static final String PROVIDING_NON_CROSS_PROFILE_TYPE_ERROR = + "Methods annotated @CROSS_PROFILE_PROVIDER_ANNOTATION must only return" + + " @CROSS_PROFILE_ANNOTATION annotated types"; + private static final String PROVIDER_INCORRECT_ARGS_ERROR = + "Methods annotated @CROSS_PROFILE_PROVIDER_ANNOTATION can only take a single Context" + + " argument, or no-args"; + private static final String STATIC_PROVIDER_ERROR = + "Methods annotated @CROSS_PROFILE_PROVIDER_ANNOTATION can not be static"; + private static final String METHOD_STATICTYPES_ERROR = + "@CROSS_PROFILE_PROVIDER_ANNOTATION annotations on methods can not specify staticTypes"; + + private final AnnotationStrings annotationStrings; + + public CrossProfileProviderTest(AnnotationStrings annotationStrings) { + this.annotationStrings = annotationStrings; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void providesAValidCrossProfileType_compiles() { + final JavaFileObject validNotesProvider = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(validNotesProvider, annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void providesANotCrossProfileType_hasError() { + final JavaFileObject stringProvider = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + annotationStrings.crossProfileProviderAsAnnotation(), + " public String provideString() {", + " return \"Test\";", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(stringProvider, annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining( + formatErrorMessage(PROVIDING_NON_CROSS_PROFILE_TYPE_ERROR, annotationStrings)) + .inFile(stringProvider); + } + + @Test + public void takesContextArgument_compiles() { + final JavaFileObject validNotesProvider = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import android.content.Context;", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType(Context context) {", + " return new NotesType();", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(validNotesProvider, annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void takesNonContextArgument_hasError() { + final JavaFileObject notesProviderTakesNonContextArgument = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType(String s) {", + " return new NotesType();", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesProviderTakesNonContextArgument, + annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(PROVIDER_INCORRECT_ARGS_ERROR, annotationStrings)) + .inFile(notesProviderTakesNonContextArgument); + } + + @Test + public void takesMultipleContextArguments_hasError() { + final JavaFileObject notesProviderTakesMultipleContextArguments = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType(Context c1, Context c2) {", + " return new NotesType();", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesProviderTakesMultipleContextArguments, + annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(PROVIDER_INCORRECT_ARGS_ERROR, annotationStrings)) + .inFile(notesProviderTakesMultipleContextArguments); + } + + @Test + public void isStaticMethod_hasError() { + final JavaFileObject staticNotesProvider = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + annotationStrings.crossProfileProviderAsAnnotation(), + " public static NotesType provideNotesType() {", + " return new NotesType();", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(staticNotesProvider, annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(STATIC_PROVIDER_ERROR, annotationStrings)) + .inFile(staticNotesProvider); + } + + @Test + public void providesSameTypeTwice_hasError() { + final JavaFileObject staticNotesProvider = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType2() {", + " return new NotesType();", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(staticNotesProvider, annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(MULTIPLE_PROVIDERS_ERROR, annotationStrings)) + .inFile(staticNotesProvider); + } + + @Test + public void providesSameTypeTwiceInDifferentProviders_hasError() { + final JavaFileObject notesProvider1 = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider1", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider1 {", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + "}"); + final JavaFileObject notesProvider2 = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider2", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider2 {", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesProvider1, notesProvider2, annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(MULTIPLE_PROVIDERS_ERROR, annotationStrings)) + .inFile(notesProvider1); + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(MULTIPLE_PROVIDERS_ERROR, annotationStrings)) + .inFile(notesProvider2); + } + + @Test + public void providesSameTypeTwiceInStaticAndNonStaticProviders_hasError() { + final JavaFileObject staticNotesProvider = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".StaticNotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + annotationStrings.crossProfileProviderAsAnnotation("staticTypes=StaticType.class"), + "public final class StaticNotesProvider {", + "}"); + final JavaFileObject nonStaticNotesProvider = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NonStaticNotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NonStaticNotesProvider {", + annotationStrings.crossProfileProviderAsAnnotation(), + " public StaticType provideNotesType() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(staticNotesProvider, nonStaticNotesProvider, staticType(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(MULTIPLE_PROVIDERS_ERROR, annotationStrings)) + .inFile(staticNotesProvider); + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(MULTIPLE_PROVIDERS_ERROR, annotationStrings)) + .inFile(nonStaticNotesProvider); + } + + @Test + public void specifyStaticTypesOnMethodAnnotation_hasError() { + JavaFileObject notesProvider = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "import com.google.android.enterprise.connectedapps.CrossProfileConnector;", + "public final class NotesProvider {", + annotationStrings.crossProfileProviderAsAnnotation("staticTypes=NotesType.class"), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesProvider, annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(METHOD_STATICTYPES_ERROR, annotationStrings)) + .inFile(notesProvider); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileSupportedParameterTypeTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileSupportedParameterTypeTest.java new file mode 100644 index 0000000..0b4f7f1 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileSupportedParameterTypeTest.java @@ -0,0 +1,131 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.PARCELABLE_OBJECT; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.SERIALIZABLE_OBJECT; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.combineParameters; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.installationListener; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfile; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import java.util.Arrays; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/** + * Test supported parameter types for {@link CrossProfile} annotated methods. + * + * <p>This tests a single parameter of each supported type. Multiple parameters and unsupported + * types are tested in {@link CrossProfileTest}. + */ +@RunWith(Parameterized.class) +public class CrossProfileSupportedParameterTypeTest { + + @Parameters(name = "{0} for {1}") + public static Iterable<Object[]> data() { + String[] types = { + "String", + "String[]", + "byte", + "Byte", + "short", + "Short", + "int", + "Integer", + "long", + "Long", + "float", + "Float", + "double", + "Double", + "char", + "Character", + "boolean", + "Boolean", + "ParcelableObject", + "ParcelableObject[]", + "java.util.List<ParcelableObject>", + "java.util.List<ParcelableObject[]>", + "java.util.List<java.util.List<ParcelableObject>>", + "SerializableObject", + "SerializableObject[]", + "java.util.List<SerializableObject>", + "java.util.List<SerializableObject[]>", + "java.util.List<java.util.List<SerializableObject>>", + "java.util.List<String>", + "java.util.List<String[]>", + "java.util.Map<String, String>", + "java.util.Set<String>", + "java.util.Collection<String>", + "java.util.Optional<String>", + "com.google.protos.connectedappssdk.TestProtoOuterClass.TestProto", + "com.google.protos.connectedappssdk.TestProtoOuterClass.TestProto[]", + "java.util.List<com.google.protos.connectedappssdk.TestProtoOuterClass.TestProto>", + "InstallationListener", + "com.google.common.collect.ImmutableMap<String, String>", + "android.util.Pair<String, Integer>", + "android.graphics.Bitmap", + "android.content.Context" + }; + return combineParameters(AnnotationFinder.annotationStrings(), Arrays.asList(types)); + } + + private final AnnotationPrinter annotationPrinter; + + private final String type; + + public CrossProfileSupportedParameterTypeTest(AnnotationPrinter annotationPrinter, String type) { + this.annotationPrinter = annotationPrinter; + this.type = type; + } + + @Test + public void crossProfileMethodWithSupportedParameterType_compiles() { + JavaFileObject crossProfileMethodWithSupportedParameterType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes(" + type + " a) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + crossProfileMethodWithSupportedParameterType, + annotatedNotesProvider(annotationPrinter), + PARCELABLE_OBJECT, + SERIALIZABLE_OBJECT, + installationListener(annotationPrinter)); + + assertThat(compilation).succeededWithoutWarnings(); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileSupportedReturnTypeTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileSupportedReturnTypeTest.java new file mode 100644 index 0000000..9335557 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileSupportedReturnTypeTest.java @@ -0,0 +1,170 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.PARCELABLE_OBJECT; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.SERIALIZABLE_OBJECT; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.combineParameters; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfile; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import java.util.Arrays; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/** + * Test supported return types for {@link CrossProfile} annotated methods. + * + * <p>This does not test the {@code void} return type as that does not return anything. This is + * tested in {@link CrossProfileTest}. + */ +@RunWith(Parameterized.class) +public class CrossProfileSupportedReturnTypeTest { + + @Parameters(name = "{0} for {1}") + public static Iterable<Object[]> data() { + TypeWithReturnValue[] typesWithReturnValues = + new TypeWithReturnValue[] { + TypeWithReturnValue.referenceType("Void"), + TypeWithReturnValue.referenceType("String"), + TypeWithReturnValue.referenceType("String[]"), + TypeWithReturnValue.primitiveType("byte", "0"), + TypeWithReturnValue.referenceType("Byte"), + TypeWithReturnValue.primitiveType("short", "0"), + TypeWithReturnValue.referenceType("Short"), + TypeWithReturnValue.primitiveType("int", "0"), + TypeWithReturnValue.referenceType("Integer"), + TypeWithReturnValue.primitiveType("long", "0"), + TypeWithReturnValue.referenceType("Long"), + TypeWithReturnValue.primitiveType("float", "0"), + TypeWithReturnValue.referenceType("Float"), + TypeWithReturnValue.primitiveType("double", "0"), + TypeWithReturnValue.referenceType("Double"), + TypeWithReturnValue.primitiveType("char", "'a'"), + TypeWithReturnValue.referenceType("Character"), + TypeWithReturnValue.primitiveType("boolean", "false"), + TypeWithReturnValue.referenceType("Boolean"), + TypeWithReturnValue.referenceType("ParcelableObject"), + TypeWithReturnValue.referenceType("ParcelableObject[]"), + TypeWithReturnValue.referenceType("java.util.List<ParcelableObject>"), + TypeWithReturnValue.referenceType("java.util.List<ParcelableObject[]>"), + TypeWithReturnValue.referenceType("java.util.List<java.util.List<ParcelableObject>>"), + TypeWithReturnValue.referenceType("SerializableObject"), + TypeWithReturnValue.referenceType("SerializableObject[]"), + TypeWithReturnValue.referenceType("java.util.List<SerializableObject>"), + TypeWithReturnValue.referenceType("java.util.List<SerializableObject[]>"), + TypeWithReturnValue.referenceType("java.util.List<java.util.List<SerializableObject>>"), + TypeWithReturnValue.referenceType("java.util.List<String>"), + TypeWithReturnValue.referenceType("java.util.List<String[]>"), + TypeWithReturnValue.referenceType("java.util.Map<String, String>"), + TypeWithReturnValue.referenceType("java.util.Set<String>"), + TypeWithReturnValue.referenceType("java.util.Collection<String>"), + TypeWithReturnValue.referenceType("java.util.Optional<String>"), + TypeWithReturnValue.referenceType( + "com.google.protos.connectedappssdk.TestProtoOuterClass.TestProto"), + TypeWithReturnValue.referenceType( + "com.google.protos.connectedappssdk.TestProtoOuterClass.TestProto[]"), + TypeWithReturnValue.referenceType( + "java.util.List<com.google.protos.connectedappssdk.TestProtoOuterClass.TestProto>"), + TypeWithReturnValue.referenceType( + "com.google.common.util.concurrent.ListenableFuture<String>"), + TypeWithReturnValue.referenceType( + "com.google.common.util.concurrent.ListenableFuture<String[]>"), + TypeWithReturnValue.referenceType( + "com.google.common.util.concurrent.ListenableFuture<com.google.protos.connectedappssdk.TestProtoOuterClass.TestProto>"), + TypeWithReturnValue.referenceType( + "com.google.common.util.concurrent.ListenableFuture<java.util.List<String>>"), + TypeWithReturnValue.referenceType( + "com.google.common.collect.ImmutableMap<String, String>"), + TypeWithReturnValue.referenceType("android.util.Pair<String, Integer>"), + TypeWithReturnValue.referenceType("com.google.common.base.Optional<ParcelableObject>"), + TypeWithReturnValue.referenceType("android.graphics.Bitmap"), + }; + return combineParameters( + AnnotationFinder.annotationStrings(), Arrays.asList(typesWithReturnValues)); + } + + private final AnnotationPrinter annotationPrinter; + + private final TypeWithReturnValue supportedReturnType; + + public CrossProfileSupportedReturnTypeTest( + AnnotationPrinter annotationPrinter, TypeWithReturnValue supportedReturnType) { + this.annotationPrinter = annotationPrinter; + this.supportedReturnType = supportedReturnType; + } + + @Test + public void crossProfileMethodWithSupportedReturnType_compiles() { + JavaFileObject crossProfileMethodWithSupportedReturnType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public " + supportedReturnType.type + " refreshNotes() {", + " return " + supportedReturnType.returnValue + ";", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + crossProfileMethodWithSupportedReturnType, + annotatedNotesProvider(annotationPrinter), + PARCELABLE_OBJECT, + SERIALIZABLE_OBJECT); + + assertThat(compilation).succeededWithoutWarnings(); + } + + private static class TypeWithReturnValue { + + final String type; + + final String returnValue; + + static TypeWithReturnValue primitiveType(String type, String returnValue) { + return new TypeWithReturnValue(type, returnValue); + } + + static TypeWithReturnValue referenceType(String type) { + return new TypeWithReturnValue(type, "null"); + } + + private TypeWithReturnValue(String type, String returnValue) { + this.type = type; + this.returnValue = returnValue; + } + + @Override + public String toString() { + return type; + } + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileTest.java new file mode 100644 index 0000000..eecccac --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileTest.java @@ -0,0 +1,1698 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.CROSS_PROFILE_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.CUSTOM_WRAPPER; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.GENERIC_PARCELABLE_OBJECT; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.GENERIC_SERIALIZABLE_OBJECT; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.INSTALLATION_LISTENER_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.PARCELABLE_CUSTOM_WRAPPER; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.SIMPLE_FUTURE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.SIMPLE_FUTURE_WRAPPER; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.UNSUPPORTED_TYPE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.UNSUPPORTED_TYPE_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.formatErrorMessage; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.installationListener; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class CrossProfileTest { + + private static final String UNSUPPORTED_RETURN_TYPE_ERROR = + "cannot be returned by methods annotated @CROSS_PROFILE_ANNOTATION"; + private static final String UNSUPPORTED_PARAMETER_TYPE_ERROR = + "cannot be used by parameters of methods annotated @CROSS_PROFILE_ANNOTATION"; + private static final String MULTIPLE_ASYNC_CALLBACK_PARAMETERS_ERROR = + "Methods annotated @CROSS_PROFILE_ANNOTATION can have a maximum of one parameter of a type" + + " annotated @CROSS_PROFILE_CALLBACK_ANNOTATION"; + private static final String NON_VOID_CALLBACK_ERROR = + "Methods annotated @CROSS_PROFILE_ANNOTATION which take a parameter type annotated" + + " @CROSS_PROFILE_CALLBACK_ANNOTATION must return void"; + private static final String METHOD_ISSTATIC_ERROR = + "@CROSS_PROFILE_ANNOTATION annotations on methods can not specify isStatic"; + private static final String METHOD_CONNECTOR_ERROR = + "@CROSS_PROFILE_ANNOTATION annotations on methods can not specify a connector"; + private static final String METHOD_PARCELABLE_WRAPPERS_ERROR = + "@CROSS_PROFILE_ANNOTATION annotations on methods can not specify parcelable wrappers"; + private static final String METHOD_CLASSNAME_ERROR = + "@CROSS_PROFILE_ANNOTATION annotations on methods can not specify a profile class name"; + private static final String INVALID_TIMEOUT_MILLIS = "timeoutMillis must be positive"; + private static final String ASYNC_DECLARED_EXCEPTION_ERROR = + "Asynchronous methods annotated @CROSS_PROFILE_ANNOTATION cannot declare exceptions"; + private static final String PARCELABLE_WRAPPER_ANNOTATION_ERROR = + "Parcelable Wrappers must be annotated @CustomParcelableWrapper"; + private static final String FUTURE_WRAPPER_ANNOTATION_ERROR = + "Future Wrappers must be annotated @CustomFutureWrapper"; + + private final AnnotationStrings annotationStrings; + + public CrossProfileTest(AnnotationStrings annotationStrings) { + this.annotationStrings = annotationStrings; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void validCrossProfileAnnotation_compiles() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationStrings), + annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void staticCrossProfileMethod_compiles() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public static void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethodWithUnsupportedReturnType_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public " + UNSUPPORTED_TYPE_NAME + " refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings), UNSUPPORTED_TYPE); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_RETURN_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithContextReturnType_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import android.content.Context;", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public Context refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings), UNSUPPORTED_TYPE); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_RETURN_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithUnsupportedReturnTypeArray_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public " + UNSUPPORTED_TYPE_NAME + "[] refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings), UNSUPPORTED_TYPE); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_RETURN_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithUnsupportedReturnTypeInGeneric_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public java.util.List<" + UNSUPPORTED_TYPE_NAME + "> refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings), UNSUPPORTED_TYPE); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_RETURN_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithContextReturnTypeInGeneric_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import android.content.Context;", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public java.util.List<Context> refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings), UNSUPPORTED_TYPE); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_RETURN_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithUnsupportedReturnTypeInGenericParcelable_compiles() { + // Parcelables take responsibility for their generics so we don't validate them + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public GenericParcelableObject<" + UNSUPPORTED_TYPE_NAME + "> refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationStrings), + UNSUPPORTED_TYPE, + GENERIC_PARCELABLE_OBJECT); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethodWithUnsupportedReturnTypeInGenericSerializable_compiles() { + // Serializables take responsibility for their generics so we don't validate them + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public GenericSerializableObject<" + UNSUPPORTED_TYPE_NAME + "> refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationStrings), + UNSUPPORTED_TYPE, + GENERIC_SERIALIZABLE_OBJECT); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethodWithVoidReturnType_compiles() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethodWithCrossProfileCallbackInterfaceReturnType_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public " + INSTALLATION_LISTENER_NAME + " refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationStrings), + installationListener(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_RETURN_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithCrossProfileCallbackInterfaceArrayReturnType_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public " + INSTALLATION_LISTENER_NAME + "[] refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationStrings), + installationListener(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_RETURN_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithMultipleSupportedParameters_compiles() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(String s, String t) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethodWithUnsupportedParameterType_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(" + UNSUPPORTED_TYPE_NAME + " s) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings), UNSUPPORTED_TYPE); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_PARAMETER_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithUnsupportedArrayParameterType_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(" + UNSUPPORTED_TYPE_NAME + "[] s) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings), UNSUPPORTED_TYPE); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_PARAMETER_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithUnsupportedParameterTypeInGeneric_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(java.util.List<" + UNSUPPORTED_TYPE_NAME + "> s) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings), UNSUPPORTED_TYPE); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_PARAMETER_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithCrossProfileCallbackListenerInGeneric_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(java.util.List<" + INSTALLATION_LISTENER_NAME + "> s) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationStrings), + installationListener(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_PARAMETER_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithCrossProfileCallbackListenerInArray_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(" + INSTALLATION_LISTENER_NAME + "[] s) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationStrings), + installationListener(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_PARAMETER_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithMultipleCrossProfileCallbackListenerParameters_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(" + + INSTALLATION_LISTENER_NAME + + " a, " + + INSTALLATION_LISTENER_NAME + + " b) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationStrings), + installationListener(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining( + formatErrorMessage(MULTIPLE_ASYNC_CALLBACK_PARAMETERS_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithUnsupportedParameterTypeInGenericParcelable_compiles() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(GenericParcelableObject<" + + UNSUPPORTED_TYPE_NAME + + "> s) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationStrings), + UNSUPPORTED_TYPE, + GENERIC_PARCELABLE_OBJECT); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethodWithUnsupportedParameterTypeInGenericSerializable_compiles() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(GenericSerializableObject<" + + UNSUPPORTED_TYPE_NAME + + "> s) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationStrings), + UNSUPPORTED_TYPE, + GENERIC_SERIALIZABLE_OBJECT); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethodWithCrossProfileCallbackListenerInGenericParcelable_compiles() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(GenericParcelableObject<" + + INSTALLATION_LISTENER_NAME + + "> s) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationStrings), + installationListener(annotationStrings), + GENERIC_PARCELABLE_OBJECT); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethodWithCrossProfileCallbackListenerInGenericSerializable_compiles() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(GenericSerializableObject<" + + INSTALLATION_LISTENER_NAME + + "> s) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationStrings), + installationListener(annotationStrings), + GENERIC_SERIALIZABLE_OBJECT); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethodWithListenableFutureParameter_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import com.google.common.util.concurrent.ListenableFuture;", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(ListenableFuture<String> s) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_PARAMETER_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithListenableFutureArrayParameter_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import com.google.common.util.concurrent.ListenableFuture;", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(ListenableFuture<String>[] s) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_PARAMETER_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithListenableFutureInParcelableWrapperParameter_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import com.google.common.util.concurrent.ListenableFuture;", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(java.util.List<ListenableFuture<String>> s) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_PARAMETER_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithListenableFutureInParcelableWrapperReturnType_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import com.google.common.util.concurrent.ListenableFuture;", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public java.util.List<ListenableFuture<String>> refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_RETURN_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithListenableFutureReturnWithUnsupportedGenericType_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import com.google.common.util.concurrent.ListenableFuture;", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public ListenableFuture<" + UNSUPPORTED_TYPE_NAME + "> refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings), UNSUPPORTED_TYPE); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_RETURN_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithNonVoidReturnAndCrossProfileCallbackParameter_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import com.google.common.util.concurrent.ListenableFuture;", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public String refreshNotes(" + INSTALLATION_LISTENER_NAME + " a) {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationStrings), + installationListener(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(NON_VOID_CALLBACK_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithListenableFutureInGenericParcelable_compiles() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import com.google.common.util.concurrent.ListenableFuture;", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(GenericParcelableObject<ListenableFuture<String>> s) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, annotatedNotesProvider(annotationStrings), GENERIC_PARCELABLE_OBJECT); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethodWithListenableFutureInGenericSerializable_compiles() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import com.google.common.util.concurrent.ListenableFuture;", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(GenericSerializableObject<ListenableFuture<String>> s) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, annotatedNotesProvider(annotationStrings), GENERIC_SERIALIZABLE_OBJECT); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethodWithGenericArrayParameter_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(java.util.Collection<String>[] s) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_PARAMETER_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithGenericArrayReturnType_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public java.util.Collection<String>[] refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_RETURN_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithPrimitiveArrayParameterType_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(int[] i) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_PARAMETER_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithPrimitiveArrayReturnType_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public int[] refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_RETURN_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithMultiDimensionalArrayParameterType_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(java.lang.String[][] s) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_PARAMETER_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithMultiDimensionalArrayReturnType_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public java.lang.String[][] refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_RETURN_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void specifyConnectorOnMethodAnnotation_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import com.google.android.enterprise.connectedapps.CrossProfileConnector;", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation("connector=CrossProfileConnector.class"), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(METHOD_CONNECTOR_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void specifyParcelableWrappersOnMethodAnnotation_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation( + "parcelableWrappers=ParcelableCustomWrapper.class"), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationStrings), + PARCELABLE_CUSTOM_WRAPPER, + CUSTOM_WRAPPER); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(METHOD_PARCELABLE_WRAPPERS_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void specifyProfileClassNameOnMethodAnnotation_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation( + "profileClassName=\"" + NOTES_PACKAGE + ".ProfileNotes\""), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(METHOD_CLASSNAME_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileInterface_works() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesInterface", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public interface NotesInterface {", + annotationStrings.crossProfileAsAnnotation(), + " void refreshNotes();", + "}"); + JavaFileObject providerClass = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesInterface provideNotesInterface() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac().withProcessors(new Processor()).compile(notesType, providerClass); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfile_specifiesValidTimeoutMillisAndAlsoOnType_compiles() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation("timeoutMillis=30"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation("timeoutMillis=10"), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(crossProfileType); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfile_specifiesValidTimeoutMillis_compiles() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation("timeoutMillis=10"), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(crossProfileType); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfile_specifiesNegativeTimeoutMillis_hasError() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation("timeoutMillis=-10"), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(crossProfileType); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(INVALID_TIMEOUT_MILLIS, annotationStrings)) + .inFile(crossProfileType); + } + + @Test + public void crossProfileType_specifiesZeroTimeoutMillis_hasError() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation("timeoutMillis=0"), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(crossProfileType); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(INVALID_TIMEOUT_MILLIS, annotationStrings)) + .inFile(crossProfileType); + } + + @Test + public void crossProfileMethod_synchronous_declaresException_compiles() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import java.io.IOException;", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() throws IOException {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(crossProfileType, installationListener(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethod_async_declaresException_hasError() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import java.io.IOException;", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(" + + INSTALLATION_LISTENER_NAME + + " callback) throws IOException {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(crossProfileType, installationListener(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(ASYNC_DECLARED_EXCEPTION_ERROR, annotationStrings)) + .inFile(crossProfileType); + } + + @Test + public void crossProfileMethod_future_declaresException_hasError() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import java.io.IOException;", + "import com.google.common.util.concurrent.ListenableFuture;", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public ListenableFuture<String> refreshNotes() throws IOException {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(crossProfileType, installationListener(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(ASYNC_DECLARED_EXCEPTION_ERROR, annotationStrings)) + .inFile(crossProfileType); + } + + @Test + public void crossProfileMethod_returnsCustomParcelableType_compiles() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation( + "connector=CrossProfileConnectorWhichSupportsCustomWrapper.class"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public CustomWrapper<String> refreshNotes() {", + " return null;", + " }", + "}"); + + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".CrossProfileConnectorWhichSupportsCustomWrapper", + "package " + NOTES_PACKAGE + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(parcelableWrappers={ParcelableCustomWrapper.class})", + "public interface CrossProfileConnectorWhichSupportsCustomWrapper extends" + + " ProfileConnector {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(crossProfileType, connector, CUSTOM_WRAPPER, PARCELABLE_CUSTOM_WRAPPER); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethod_takesCustomParcelableTypeArgument_compiles() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation( + "connector=CrossProfileConnectorWhichSupportsCustomWrapper.class"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(CustomWrapper<String> a) {", + " }", + "}"); + + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".CrossProfileConnectorWhichSupportsCustomWrapper", + "package " + NOTES_PACKAGE + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(parcelableWrappers={ParcelableCustomWrapper.class})", + "public interface CrossProfileConnectorWhichSupportsCustomWrapper extends" + + " ProfileConnector {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(crossProfileType, connector, CUSTOM_WRAPPER, PARCELABLE_CUSTOM_WRAPPER); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethod_usesCustomParcelableTypeFromDifferentConnector_hasError() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation( + "connector=CrossProfileConnectorWhichSupportsCustomWrapper.class"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public CustomWrapper<String> refreshNotes() {", + " return null;", + " }", + "}"); + + JavaFileObject secondCrossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType2", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType2 {", + annotationStrings.crossProfileAsAnnotation(), + " public CustomWrapper<String> refreshNotes() {", + " return null;", + " }", + "}"); + + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".CrossProfileConnectorWhichSupportsCustomWrapper", + "package " + NOTES_PACKAGE + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(parcelableWrappers={ParcelableCustomWrapper.class})", + "public interface CrossProfileConnectorWhichSupportsCustomWrapper extends" + + " ProfileConnector {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + crossProfileType, + secondCrossProfileType, + connector, + CUSTOM_WRAPPER, + PARCELABLE_CUSTOM_WRAPPER); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_RETURN_TYPE_ERROR, annotationStrings)) + .inFile(secondCrossProfileType); + } + + @Test + public void crossProfileMethod_returnsCustomParcelableTypeForCrossProfileType_compiles() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation( + "parcelableWrappers=ParcelableCustomWrapper.class"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public CustomWrapper<String> refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(crossProfileType, CUSTOM_WRAPPER, PARCELABLE_CUSTOM_WRAPPER); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethod_takesCustomParcelableTypeForCrossProfileTypeAsArgument_compiles() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation( + "parcelableWrappers=ParcelableCustomWrapper.class"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(CustomWrapper<String> a) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(crossProfileType, CUSTOM_WRAPPER, PARCELABLE_CUSTOM_WRAPPER); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileType_includesNonParcelableWrapperInParcelableWrappers_hasError() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation("parcelableWrappers=String.class"), + "public final class NotesType {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(crossProfileType); + + assertThat(compilation) + .hadErrorContaining( + formatErrorMessage(PARCELABLE_WRAPPER_ANNOTATION_ERROR, annotationStrings)) + .inFile(crossProfileType); + } + + @Test + public void crossProfileMethod_returnsCustomFutureType_compiles() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation( + "connector=CrossProfileConnectorWhichSupportsSimpleFuture.class"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public SimpleFuture<String> refreshNotes() {", + " return null;", + " }", + "}"); + + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".CrossProfileConnectorWhichSupportsSimpleFuture", + "package " + NOTES_PACKAGE + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(futureWrappers={SimpleFutureWrapper.class})", + "public interface CrossProfileConnectorWhichSupportsSimpleFuture extends" + + " ProfileConnector {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(crossProfileType, connector, SIMPLE_FUTURE, SIMPLE_FUTURE_WRAPPER); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethod_usesCustomFutureTypeFromDifferentConnector_hasError() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation( + "connector=CrossProfileConnectorWhichSupportsSimpleFuture.class"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public SimpleFuture<String> refreshNotes() {", + " return null;", + " }", + "}"); + + JavaFileObject secondCrossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType2", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType2 {", + annotationStrings.crossProfileAsAnnotation(), + " public SimpleFuture<String> refreshNotes() {", + " return null;", + " }", + "}"); + + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".CrossProfileConnectorWhichSupportsSimpleFuture", + "package " + NOTES_PACKAGE + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(futureWrappers={SimpleFutureWrapper.class})", + "public interface CrossProfileConnectorWhichSupportsSimpleFuture extends" + + " ProfileConnector {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + crossProfileType, + secondCrossProfileType, + connector, + SIMPLE_FUTURE, + SIMPLE_FUTURE_WRAPPER); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_RETURN_TYPE_ERROR, annotationStrings)) + .inFile(secondCrossProfileType); + } + + @Test + public void crossProfileMethod_usesCustomFutureTypeImportedFromDifferentConnector_compiles() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + CROSS_PROFILE_QUALIFIED_NAME + ";", + "@CrossProfile(connector=CrossProfileConnectorWithImport.class)", + "public final class NotesType {", + " @CrossProfile", + " public SimpleFuture<String> refreshNotes() {", + " return null;", + " }", + "}"); + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".CrossProfileConnectorWhichSupportsSimpleFuture", + "package " + NOTES_PACKAGE + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(futureWrappers={SimpleFutureWrapper.class})", + "public interface CrossProfileConnectorWhichSupportsSimpleFuture extends" + + " ProfileConnector {", + "}"); + JavaFileObject connectorWithImport = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".CrossProfileConnectorWithImport", + "package " + NOTES_PACKAGE + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(imports=CrossProfileConnectorWhichSupportsSimpleFuture.class)", + "public interface CrossProfileConnectorWithImport extends" + + " ProfileConnector {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + crossProfileType, + connector, + connectorWithImport, + SIMPLE_FUTURE, + SIMPLE_FUTURE_WRAPPER); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethod_usesCustomFutureTypeImportedIndirectly_compiles() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + CROSS_PROFILE_QUALIFIED_NAME + ";", + "@CrossProfile(connector=CrossProfileConnectorWithImportOfImport.class)", + "public final class NotesType {", + " @CrossProfile", + " public SimpleFuture<String> refreshNotes() {", + " return null;", + " }", + "}"); + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".CrossProfileConnectorWhichSupportsSimpleFuture", + "package " + NOTES_PACKAGE + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(futureWrappers={SimpleFutureWrapper.class})", + "public interface CrossProfileConnectorWhichSupportsSimpleFuture extends" + + " ProfileConnector {", + "}"); + JavaFileObject connectorWithImport = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".CrossProfileConnectorWithImport", + "package " + NOTES_PACKAGE + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(imports=CrossProfileConnectorWhichSupportsSimpleFuture.class)", + "public interface CrossProfileConnectorWithImport extends" + + " ProfileConnector {", + "}"); + JavaFileObject connectorWithImportOfImport = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".CrossProfileConnectorWithImportOfImport", + "package " + NOTES_PACKAGE + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(imports=CrossProfileConnectorWithImport.class)", + "public interface CrossProfileConnectorWithImportOfImport extends" + + " ProfileConnector {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + crossProfileType, + connector, + connectorWithImport, + connectorWithImportOfImport, + SIMPLE_FUTURE, + SIMPLE_FUTURE_WRAPPER); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethod_usesCustomFutureTypeForCrossProfileType_compiles() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation("futureWrappers=SimpleFutureWrapper.class"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public SimpleFuture<String> refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + crossProfileType, + SIMPLE_FUTURE, + SIMPLE_FUTURE_WRAPPER, + annotatedNotesProvider(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileType_includesNonFutureWrapperInFutureWrappers_hasError() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation("futureWrappers=String.class"), + "public final class NotesType {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(crossProfileType); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(FUTURE_WRAPPER_ANNOTATION_ERROR, annotationStrings)) + .inFile(crossProfileType); + } + + @Test + public void specifyIsStaticOnMethodAnnotation_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation("isStatic=true"), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(METHOD_ISSTATIC_ERROR, annotationStrings)) + .inFile(notesType); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileTestTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileTestTest.java new file mode 100644 index 0000000..0050a6e --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileTestTest.java @@ -0,0 +1,225 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesConfigurationWithNotesProvider; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.formatErrorMessage; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.notesTypeWithDefaultConnector; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class CrossProfileTestTest { + + private static final String NOT_A_CONFIGURATION_ERROR = + "Configurations referenced in a @CROSS_PROFILE_TEST_ANNOTATION annotation must be annotated" + + " @CROSS_PROFILE_CONFIGURATION_ANNOTATION or @CROSS_PROFILE_CONFIGURATIONS_ANNOTATION"; + + private final AnnotationStrings annotationStrings; + + public CrossProfileTestTest(AnnotationStrings annotationStrings) { + this.annotationStrings = annotationStrings; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void crossProfileTest_compiles() { + JavaFileObject crossProfileTest = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesTest", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileTestQualifiedName() + ";", + annotationStrings.crossProfileTestAsAnnotation( + "configuration=NotesConfiguration.class"), + "public final class NotesTest {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + crossProfileTest, + annotatedNotesConfigurationWithNotesProvider(annotationStrings), + annotatedNotesProvider(annotationStrings), + notesTypeWithDefaultConnector(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileTest_referencesNonConfigurationClass_hasError() { + JavaFileObject crossProfileTest = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesTest", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileTestQualifiedName() + ";", + annotationStrings.crossProfileTestAsAnnotation("configuration=String.class"), + "public final class NotesTest {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(crossProfileTest); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(NOT_A_CONFIGURATION_ERROR, annotationStrings)) + .inFile(crossProfileTest); + } + + @Test + public void crossProfileTest_generatesFakeConnector() { + JavaFileObject crossProfileTest = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesTest", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileTestQualifiedName() + ";", + annotationStrings.crossProfileTestAsAnnotation( + "configuration=NotesConfiguration.class"), + "public final class NotesTest {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + crossProfileTest, + annotatedNotesConfigurationWithNotesProvider(annotationStrings), + annotatedNotesProvider(annotationStrings), + notesTypeWithDefaultConnector(annotationStrings)); + + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.connectedapps.FakeCrossProfileConnector"); + } + + @Test + public void crossProfileTest_fakeConnectorImplementsConnector() { + JavaFileObject crossProfileTest = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesTest", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileTestQualifiedName() + ";", + annotationStrings.crossProfileTestAsAnnotation( + "configuration=NotesConfiguration.class"), + "public final class NotesTest {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + crossProfileTest, + annotatedNotesConfigurationWithNotesProvider(annotationStrings), + annotatedNotesProvider(annotationStrings), + notesTypeWithDefaultConnector(annotationStrings)); + + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.connectedapps.FakeCrossProfileConnector") + .contentsAsUtf8String() + .contains("implements CrossProfileConnector"); + } + + @Test + public void + crossProfileTest_connectorDoesNotSpecifyPrimaryProfile_fakeConnectorHasConstructorToSpecifyProfile() { + JavaFileObject crossProfileTest = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesTest", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileTestQualifiedName() + ";", + annotationStrings.crossProfileTestAsAnnotation( + "configuration=NotesConfiguration.class"), + "public final class NotesTest {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + crossProfileTest, + annotatedNotesConfigurationWithNotesProvider(annotationStrings), + annotatedNotesProvider(annotationStrings), + notesTypeWithDefaultConnector(annotationStrings)); + + // We can't assert multi-line methods, so we check just that we have a constructer with an + // additional param + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.connectedapps.FakeCrossProfileConnector") + .contentsAsUtf8String() + .contains("public FakeCrossProfileConnector(Context context,"); + } + + @Test + public void + crossProfileTest_connectorSpecifiesPrimaryProfile_fakeConnectorDoesNotHaveConstructorToSpecifyProfile() { + JavaFileObject crossProfileTest = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesTest", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileTestQualifiedName() + ";", + annotationStrings.crossProfileTestAsAnnotation( + "configuration=NotesConfiguration.class"), + "public final class NotesTest {", + "}"); + JavaFileObject notesConfiguration = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + "import com.google.android.enterprise.connectedapps.CrossProfileConnector;", + annotationStrings.crossProfileConfigurationAsAnnotation( + "connector=NotesConnector.class"), + "public abstract class NotesConfiguration {", + "}"); + JavaFileObject notesConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(primaryProfile=CustomProfileConnector.ProfileType.WORK)", + "public interface NotesConnector extends ProfileConnector {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(crossProfileTest, notesConfiguration, notesConnector); + + assertThat(compilation) + .generatedSourceFile("com.google.android.enterprise.notes.FakeNotesConnector") + .contentsAsUtf8String() + .doesNotContain("CustomProfileConnector.ProfileType primaryProfile"); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileTypeTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileTypeTest.java new file mode 100644 index 0000000..cb692ff --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileTypeTest.java @@ -0,0 +1,363 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.ANNOTATED_NOTES_CONNECTOR; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.formatErrorMessage; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class CrossProfileTypeTest { + + private static JavaFileObject secondAnnotatedNotesProvider(AnnotationPrinter annotationPrinter) { + return JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider2", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider2 {", + annotationPrinter.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + "}"); + } + + private static final String MULTIPLE_PROVIDERS_ERROR = "has been provided more than once"; + private static final String DEFAULT_PACKAGE_ERROR = + "@CROSS_PROFILE_ANNOTATION types must not be in the default package"; + private static final String NON_PUBLIC_CLASS_ERROR = + "@CROSS_PROFILE_ANNOTATION types must be public"; + private static final String CONNECTOR_MUST_EXTEND_CONNECTOR = + "Interfaces specified as a connector must extend ProfileConnector"; + private static final String INVALID_TIMEOUT_MILLIS = "timeoutMillis must be positive"; + private static final String CONNECTOR_MUST_BE_INTERFACE = "Connectors must be interfaces"; + private static final String NOT_STATIC_ERROR = + "Types annotated @CROSS_PROFILE_ANNOTATION(isStatic=true) must not contain any non-static" + + " methods annotated @CROSS_PROFILE_ANNOTATION"; + + private final AnnotationStrings annotationStrings; + + public CrossProfileTypeTest(AnnotationStrings annotationStrings) { + this.annotationStrings = annotationStrings; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationNames() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void crossProfileType_inDefaultPackage_hasError() { + JavaFileObject crossProfileTypeInDefaultPackage = + JavaFileObjects.forSourceLines( + "NotesType", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac().withProcessors(new Processor()).compile(crossProfileTypeInDefaultPackage); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(DEFAULT_PACKAGE_ERROR, annotationStrings)) + .inFile(crossProfileTypeInDefaultPackage); + } + + @Test + public void crossProfileType_inNonPublicClass_hasError() { + JavaFileObject crossProfileTypeInNonPublicClass = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac().withProcessors(new Processor()).compile(crossProfileTypeInNonPublicClass); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(NON_PUBLIC_CLASS_ERROR, annotationStrings)) + .inFile(crossProfileTypeInNonPublicClass); + } + + @Test + public void crossProfileType_doesNotSpecifyConnector_compiles() { + JavaFileObject crossProfileTypeWithoutConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation(), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac().withProcessors(new Processor()).compile(crossProfileTypeWithoutConnector); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileType_specifiesValidConnector_compiles() { + JavaFileObject crossProfileTypeWithValidConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation("connector=NotesConnector.class"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(crossProfileTypeWithValidConnector, ANNOTATED_NOTES_CONNECTOR); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileType_specifiesValidTimeoutMillis_compiles() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation("timeoutMillis=10"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(crossProfileType); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileType_specifiesNegativeTimeoutMillis_hasError() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation("timeoutMillis=-10"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(crossProfileType); + + assertThat(compilation).hadErrorContaining(INVALID_TIMEOUT_MILLIS).inFile(crossProfileType); + } + + @Test + public void crossProfileType_specifiesZeroTimeoutMillis_hasError() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation("timeoutMillis=0"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(crossProfileType); + + assertThat(compilation).hadErrorContaining(INVALID_TIMEOUT_MILLIS).inFile(crossProfileType); + } + + @Test + public void crossProfileType_specifiesNotInterfaceConnector_hasError() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation("connector=NotesConnector.class"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + JavaFileObject notesConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "public class NotesConnector {", + "}"); + + Compilation compilation = + javac().withProcessors(new Processor()).compile(crossProfileType, notesConnector); + + assertThat(compilation) + .hadErrorContaining(CONNECTOR_MUST_BE_INTERFACE) + .inFile(crossProfileType); + } + + @Test + public void crossProfileType_specifiesConnectorNotExtendingProfileConnector_hasError() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation("connector=NotesConnector.class"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + JavaFileObject notesConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "public interface NotesConnector {", + "}"); + + Compilation compilation = + javac().withProcessors(new Processor()).compile(crossProfileType, notesConnector); + + assertThat(compilation) + .hadErrorContaining(CONNECTOR_MUST_EXTEND_CONNECTOR) + .inFile(crossProfileType); + } + + @Test + public void multipleCrossProfileProviders_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesCrossProfileType(annotationStrings), + annotatedNotesProvider(annotationStrings), + secondAnnotatedNotesProvider(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(MULTIPLE_PROVIDERS_ERROR) + .inFile(annotatedNotesProvider(annotationStrings)); + assertThat(compilation) + .hadErrorContaining(MULTIPLE_PROVIDERS_ERROR) + .inFile(secondAnnotatedNotesProvider(annotationStrings)); + } + + @Test + public void specifiesAlternativeProfileClassName_generatesCorrectClass() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation( + "profileClassName=\"" + NOTES_PACKAGE + ".CrossProfileNotes\""), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(crossProfileType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation).generatedSourceFile(NOTES_PACKAGE + ".CrossProfileNotes"); + } + + @Test + public void isStaticContainsNoNonStaticMethods_compiles() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import com.google.android.enterprise.connectedapps.CrossProfileConnector;", + annotationStrings.crossProfileAsAnnotation("isStatic=true"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public static void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void isStaticContainsNonStaticMethods_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import com.google.android.enterprise.connectedapps.CrossProfileConnector;", + annotationStrings.crossProfileAsAnnotation("isStatic=true"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(NOT_STATIC_ERROR, annotationStrings)) + .inFile(notesType); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CustomFutureWrapperTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CustomFutureWrapperTest.java new file mode 100644 index 0000000..e5f6008 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CustomFutureWrapperTest.java @@ -0,0 +1,855 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.SIMPLE_FUTURE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.SIMPLE_FUTURE_WRAPPER; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class CustomFutureWrapperTest { + + private static final String DOES_NOT_EXTEND_FUTURE_WRAPPER_ERROR = + "Classes annotated @CustomFutureWrapper must extend FutureWrapper"; + private static final String INCORRECT_CREATE_METHOD_ERROR = + "Classes annotated @CustomFutureWrapper must have a create method which returns an instance" + + " of the class and takes a Bundler and BundlerType argument"; + private static final String INCORRECT_GET_FUTURE_METHOD_ERROR = + "Classes annotated @CustomFutureWrapper must have a getFuture method which returns an" + + " instance of the wrapped future and takes no arguments"; + private static final String INCORRECT_RESOLVE_CALLBACK_WHEN_FUTURE_IS_SET_METHOD_ERROR = + "Classes annotated @CustomFutureWrapper must have a writeFutureResult method" + + " which returns void and takes as arguments an instance of the wrapped future and a" + + " FutureResultWriter"; + private static final String INCORRECT_GROUP_RESULTS_METHOD_ERROR = + "Classes annotated @CustomFutureWrapper must have a groupResults method which returns an" + + " instance of the wrapped future containing a map from Profile to the wrapped future" + + " type, and takes as an argument a map from Profile to an instance of the wrapped" + + " future"; + private static final String MUST_HAVE_ONE_TYPE_PARAMETER_ERROR = + "Classes annotated @CustomFutureWrapper must have a single type parameter"; + + static final JavaFileObject SIMPLE_FUTURE_WRAPPER_IS_NOT_GENERIC = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".SimpleFutureWrapper", + "package " + NOTES_PACKAGE + ";", + "import com.google.android.enterprise.connectedapps.FutureWrapper;", + "import com.google.android.enterprise.connectedapps.internal.FutureResultWriter;", + "import com.google.android.enterprise.connectedapps.Profile;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import" + + " com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger;", + "import java.util.Map;", + "@com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper(", + "originalType = SimpleFuture.class)", + "public final class SimpleFutureWrapper extends FutureWrapper<String> {", + "private final SimpleFuture<String> future = new SimpleFuture<>();", + "public static SimpleFutureWrapper create(Bundler bundler, BundlerType" + + " bundlerType) {", + "return new SimpleFutureWrapper(bundler, bundlerType);", + "}", + "private SimpleFutureWrapper(Bundler bundler, BundlerType bundlerType) {", + "super(bundler, bundlerType);", + "}", + "public SimpleFuture<String> getFuture() {", + "return future;", + "}", + "@Override", + "public void onResult(String result) {", + "future.set(result);", + "}", + "@Override", + "public void onException(Throwable throwable) {", + "future.setException(throwable);", + "}", + "public static void writeFutureResult(", + "SimpleFuture<String> future,", + "FutureResultWriter<E> resultWriter) {", + "future.setCallback(", + "(value) -> {", + "resultWriter.onSuccess(value);", + "},", + "(exception) -> {", + "resultWriter.onFailure(exception);", + "});", + "}", + "public static SimpleFuture<Map<Profile, String>> groupResults(", + "Map<Profile, SimpleFuture<String>> results) {", + "SimpleFuture<Map<Profile, String>> m = new SimpleFuture<>();", + "CrossProfileCallbackMultiMerger<String> merger =", + "new CrossProfileCallbackMultiMerger<>(results.size(), m::set);", + "for (Map.Entry<Profile, SimpleFuture<String>> result : results.entrySet()) {", + "result", + ".getValue()", + ".setCallback(", + "(value) -> {", + "merger.onResult(result.getKey(), value);", + "},", + "(throwable) -> {", + "merger.missingResult(result.getKey());", + "});", + "}", + "return m;", + "}", + "}"); + + static final JavaFileObject SIMPLE_FUTURE_WRAPPER_MULTIPLE_TYPE_PARAMETERS = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".SimpleFutureWrapper", + "package " + NOTES_PACKAGE + ";", + "import com.google.android.enterprise.connectedapps.FutureWrapper;", + "import com.google.android.enterprise.connectedapps.internal.FutureResultWriter;", + "import com.google.android.enterprise.connectedapps.Profile;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import" + + " com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger;", + "import java.util.Map;", + "@com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper(", + "originalType = SimpleFuture.class)", + "public final class SimpleFutureWrapper<E, R> extends FutureWrapper<E> {", + "private final SimpleFuture<E> future = new SimpleFuture<>();", + "public static <E> SimpleFutureWrapper<E, String> create(Bundler bundler, BundlerType" + + " bundlerType) {", + "return new SimpleFutureWrapper<E, String>(bundler, bundlerType);", + "}", + "private SimpleFutureWrapper(Bundler bundler, BundlerType bundlerType) {", + "super(bundler, bundlerType);", + "}", + "public SimpleFuture<E> getFuture() {", + "return future;", + "}", + "@Override", + "public void onResult(E result) {", + "future.set(result);", + "}", + "@Override", + "public void onException(Throwable throwable) {", + "future.setException(throwable);", + "}", + "public static <E> void writeFutureResult(", + "SimpleFuture<E> future,", + "FutureResultWriter<E> resultWriter) {", + "future.setCallback(", + "(value) -> {", + "resultWriter.onSuccess(value);", + "},", + "(exception) -> {", + "resultWriter.onFailure(exception);", + "});", + "}", + "public static <E> SimpleFuture<Map<Profile, E>> groupResults(", + "Map<Profile, SimpleFuture<E>> results) {", + "SimpleFuture<Map<Profile, E>> m = new SimpleFuture<>();", + "CrossProfileCallbackMultiMerger<E> merger =", + "new CrossProfileCallbackMultiMerger<>(results.size(), m::set);", + "for (Map.Entry<Profile, SimpleFuture<E>> result : results.entrySet()) {", + "result", + ".getValue()", + ".setCallback(", + "(value) -> {", + "merger.onResult(result.getKey(), value);", + "},", + "(throwable) -> {", + "merger.missingResult(result.getKey());", + "});", + "}", + "return m;", + "}", + "}"); + + private static final JavaFileObject SIMPLE_FUTURE_WRAPPER_DOES_NOT_EXTEND_FUTURE_WRAPPER = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".SimpleFutureWrapper", + "package " + NOTES_PACKAGE + ";", + "import com.google.android.enterprise.connectedapps.FutureWrapper;", + "import com.google.android.enterprise.connectedapps.internal.FutureResultWriter;", + "import com.google.android.enterprise.connectedapps.Profile;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import" + + " com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger;", + "import java.util.Map;", + "@com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper(", + "originalType = SimpleFuture.class)", + "public final class SimpleFutureWrapper<E> {", + "private final SimpleFuture<E> future = new SimpleFuture<>();", + "public static <E> SimpleFutureWrapper<E> create(Bundler bundler, BundlerType" + + " bundlerType) {", + "return new SimpleFutureWrapper<>(bundler, bundlerType);", + "}", + "private SimpleFutureWrapper(Bundler bundler, BundlerType bundlerType) {", + "}", + "public SimpleFuture<E> getFuture() {", + "return future;", + "}", + "public void onResult(E result) {", + "future.set(result);", + "}", + "public void onException(Throwable throwable) {", + "future.setException(throwable);", + "}", + "public static <E> void writeFutureResult(", + "SimpleFuture<E> future,", + "FutureResultWriter<E> resultWriter) {", + "future.setCallback(", + "(value) -> {", + "resultWriter.onSuccess(value);", + "},", + "(exception) -> {", + "resultWriter.onFailure(exception);", + "});", + "}", + "public static <E> SimpleFuture<Map<Profile, E>> groupResults(", + "Map<Profile, SimpleFuture<E>> results) {", + "SimpleFuture<Map<Profile, E>> m = new SimpleFuture<>();", + "CrossProfileCallbackMultiMerger<E> merger =", + "new CrossProfileCallbackMultiMerger<>(results.size(), m::set);", + "for (Map.Entry<Profile, SimpleFuture<E>> result : results.entrySet()) {", + "result", + ".getValue()", + ".setCallback(", + "(value) -> {", + "merger.onResult(result.getKey(), value);", + "},", + "(throwable) -> {", + "merger.missingResult(result.getKey());", + "});", + "}", + "return m;", + "}", + "}"); + + private static final JavaFileObject SIMPLE_FUTURE_WRAPPER_NO_CREATE_METHOD = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".SimpleFutureWrapper", + "package " + NOTES_PACKAGE + ";", + "import com.google.android.enterprise.connectedapps.FutureWrapper;", + "import com.google.android.enterprise.connectedapps.internal.FutureResultWriter;", + "import com.google.android.enterprise.connectedapps.Profile;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import" + + " com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger;", + "import java.util.Map;", + "@com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper(", + "originalType = SimpleFuture.class)", + "public final class SimpleFutureWrapper<E> extends FutureWrapper<E> {", + "private final SimpleFuture<E> future = new SimpleFuture<>();", + "private SimpleFutureWrapper(Bundler bundler, BundlerType bundlerType) {", + "super(bundler, bundlerType);", + "}", + "public SimpleFuture<E> getFuture() {", + "return future;", + "}", + "@Override", + "public void onResult(E result) {", + "future.set(result);", + "}", + "@Override", + "public void onException(Throwable throwable) {", + "future.setException(throwable);", + "}", + "public static <E> void writeFutureResult(", + "SimpleFuture<E> future,", + "FutureResultWriter<E> resultWriter) {", + "future.setCallback(", + "(value) -> {", + "resultWriter.onSuccess(value);", + "},", + "(exception) -> {", + "resultWriter.onFailure(exception);", + "});", + "}", + "public static <E> SimpleFuture<Map<Profile, E>> groupResults(", + "Map<Profile, SimpleFuture<E>> results) {", + "SimpleFuture<Map<Profile, E>> m = new SimpleFuture<>();", + "CrossProfileCallbackMultiMerger<E> merger =", + "new CrossProfileCallbackMultiMerger<>(results.size(), m::set);", + "for (Map.Entry<Profile, SimpleFuture<E>> result : results.entrySet()) {", + "result", + ".getValue()", + ".setCallback(", + "(value) -> {", + "merger.onResult(result.getKey(), value);", + "},", + "(throwable) -> {", + "merger.missingResult(result.getKey());", + "});", + "}", + "return m;", + "}", + "}"); + + private static final JavaFileObject SIMPLE_FUTURE_WRAPPER_INCORRECT_CREATE_METHOD = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".SimpleFutureWrapper", + "package " + NOTES_PACKAGE + ";", + "import com.google.android.enterprise.connectedapps.FutureWrapper;", + "import com.google.android.enterprise.connectedapps.internal.FutureResultWriter;", + "import com.google.android.enterprise.connectedapps.Profile;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import" + + " com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger;", + "import java.util.Map;", + "@com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper(", + "originalType = SimpleFuture.class)", + "public final class SimpleFutureWrapper<E> extends FutureWrapper<E> {", + "private final SimpleFuture<E> future = new SimpleFuture<>();", + "public static <E> SimpleFutureWrapper<E> create(BundlerType bundlerType) {", + "return null;", + "}", + "private SimpleFutureWrapper(Bundler bundler, BundlerType bundlerType) {", + "super(bundler, bundlerType);", + "}", + "public SimpleFuture<E> getFuture() {", + "return future;", + "}", + "@Override", + "public void onResult(E result) {", + "future.set(result);", + "}", + "@Override", + "public void onException(Throwable throwable) {", + "future.setException(throwable);", + "}", + "public static <E> void writeFutureResult(", + "SimpleFuture<E> future,", + "FutureResultWriter<E> resultWriter) {", + "future.setCallback(", + "(value) -> {", + "resultWriter.onSuccess(value);", + "},", + "(exception) -> {", + "resultWriter.onFailure(exception);", + "});", + "}", + "public static <E> SimpleFuture<Map<Profile, E>> groupResults(", + "Map<Profile, SimpleFuture<E>> results) {", + "SimpleFuture<Map<Profile, E>> m = new SimpleFuture<>();", + "CrossProfileCallbackMultiMerger<E> merger =", + "new CrossProfileCallbackMultiMerger<>(results.size(), m::set);", + "for (Map.Entry<Profile, SimpleFuture<E>> result : results.entrySet()) {", + "result", + ".getValue()", + ".setCallback(", + "(value) -> {", + "merger.onResult(result.getKey(), value);", + "},", + "(throwable) -> {", + "merger.missingResult(result.getKey());", + "});", + "}", + "return m;", + "}", + "}"); + + private static final JavaFileObject SIMPLE_FUTURE_WRAPPER_NO_GET_FUTURE_METHOD = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".SimpleFutureWrapper", + "package " + NOTES_PACKAGE + ";", + "import com.google.android.enterprise.connectedapps.FutureWrapper;", + "import com.google.android.enterprise.connectedapps.internal.FutureResultWriter;", + "import com.google.android.enterprise.connectedapps.Profile;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import" + + " com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger;", + "import java.util.Map;", + "@com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper(", + "originalType = SimpleFuture.class)", + "public final class SimpleFutureWrapper<E> extends FutureWrapper<E> {", + "private final SimpleFuture<E> future = new SimpleFuture<>();", + "public static <E> SimpleFutureWrapper<E> create(Bundler bundler, BundlerType" + + " bundlerType) {", + "return new SimpleFutureWrapper<>(bundler, bundlerType);", + "}", + "private SimpleFutureWrapper(Bundler bundler, BundlerType bundlerType) {", + "super(bundler, bundlerType);", + "}", + "@Override", + "public void onResult(E result) {", + "future.set(result);", + "}", + "@Override", + "public void onException(Throwable throwable) {", + "future.setException(throwable);", + "}", + "public static <E> void writeFutureResult(", + "SimpleFuture<E> future,", + "FutureResultWriter<E> resultWriter) {", + "future.setCallback(", + "(value) -> {", + "resultWriter.onSuccess(value);", + "},", + "(exception) -> {", + "resultWriter.onFailure(exception);", + "});", + "}", + "public static <E> SimpleFuture<Map<Profile, E>> groupResults(", + "Map<Profile, SimpleFuture<E>> results) {", + "SimpleFuture<Map<Profile, E>> m = new SimpleFuture<>();", + "CrossProfileCallbackMultiMerger<E> merger =", + "new CrossProfileCallbackMultiMerger<>(results.size(), m::set);", + "for (Map.Entry<Profile, SimpleFuture<E>> result : results.entrySet()) {", + "result", + ".getValue()", + ".setCallback(", + "(value) -> {", + "merger.onResult(result.getKey(), value);", + "},", + "(throwable) -> {", + "merger.missingResult(result.getKey());", + "});", + "}", + "return m;", + "}", + "}"); + + private static final JavaFileObject SIMPLE_FUTURE_WRAPPER_INCORRECT_GET_FUTURE_METHOD = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".SimpleFutureWrapper", + "package " + NOTES_PACKAGE + ";", + "import com.google.android.enterprise.connectedapps.FutureWrapper;", + "import com.google.android.enterprise.connectedapps.internal.FutureResultWriter;", + "import com.google.android.enterprise.connectedapps.Profile;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import" + + " com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger;", + "import java.util.Map;", + "@com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper(", + "originalType = SimpleFuture.class)", + "public final class SimpleFutureWrapper<E> extends FutureWrapper<E> {", + "private final SimpleFuture<E> future = new SimpleFuture<>();", + "public static <E> SimpleFutureWrapper<E> create(Bundler bundler, BundlerType" + + " bundlerType) {", + "return new SimpleFutureWrapper<>(bundler, bundlerType);", + "}", + "private SimpleFutureWrapper(Bundler bundler, BundlerType bundlerType) {", + "super(bundler, bundlerType);", + "}", + "public SimpleFuture<E> getFuture(String s) {", + "return future;", + "}", + "@Override", + "public void onResult(E result) {", + "future.set(result);", + "}", + "@Override", + "public void onException(Throwable throwable) {", + "future.setException(throwable);", + "}", + "public static <E> void writeFutureResult(", + "SimpleFuture<E> future,", + "FutureResultWriter<E> resultWriter) {", + "future.setCallback(", + "(value) -> {", + "resultWriter.onSuccess(value);", + "},", + "(exception) -> {", + "resultWriter.onFailure(exception);", + "});", + "}", + "public static <E> SimpleFuture<Map<Profile, E>> groupResults(", + "Map<Profile, SimpleFuture<E>> results) {", + "SimpleFuture<Map<Profile, E>> m = new SimpleFuture<>();", + "CrossProfileCallbackMultiMerger<E> merger =", + "new CrossProfileCallbackMultiMerger<>(results.size(), m::set);", + "for (Map.Entry<Profile, SimpleFuture<E>> result : results.entrySet()) {", + "result", + ".getValue()", + ".setCallback(", + "(value) -> {", + "merger.onResult(result.getKey(), value);", + "},", + "(throwable) -> {", + "merger.missingResult(result.getKey());", + "});", + "}", + "return m;", + "}", + "}"); + + private static final JavaFileObject SIMPLE_FUTURE_WRAPPER_NO_RESOLVE_METHOD = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".SimpleFutureWrapper", + "package " + NOTES_PACKAGE + ";", + "import com.google.android.enterprise.connectedapps.FutureWrapper;", + "import com.google.android.enterprise.connectedapps.internal.FutureResultWriter;", + "import com.google.android.enterprise.connectedapps.Profile;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import" + + " com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger;", + "import java.util.Map;", + "@com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper(", + "originalType = SimpleFuture.class)", + "public final class SimpleFutureWrapper<E> extends FutureWrapper<E> {", + "private final SimpleFuture<E> future = new SimpleFuture<>();", + "public static <E> SimpleFutureWrapper<E> create(Bundler bundler, BundlerType" + + " bundlerType) {", + "return new SimpleFutureWrapper<>(bundler, bundlerType);", + "}", + "private SimpleFutureWrapper(Bundler bundler, BundlerType bundlerType) {", + "super(bundler, bundlerType);", + "}", + "public SimpleFuture<E> getFuture() {", + "return future;", + "}", + "@Override", + "public void onResult(E result) {", + "future.set(result);", + "}", + "@Override", + "public void onException(Throwable throwable) {", + "future.setException(throwable);", + "}", + "public static <E> SimpleFuture<Map<Profile, E>> groupResults(", + "Map<Profile, SimpleFuture<E>> results) {", + "SimpleFuture<Map<Profile, E>> m = new SimpleFuture<>();", + "CrossProfileCallbackMultiMerger<E> merger =", + "new CrossProfileCallbackMultiMerger<>(results.size(), m::set);", + "for (Map.Entry<Profile, SimpleFuture<E>> result : results.entrySet()) {", + "result", + ".getValue()", + ".setCallback(", + "(value) -> {", + "merger.onResult(result.getKey(), value);", + "},", + "(throwable) -> {", + "merger.missingResult(result.getKey());", + "});", + "}", + "return m;", + "}", + "}"); + + private static final JavaFileObject SIMPLE_FUTURE_WRAPPER_INCORRECT_RESOLVE_METHOD = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".SimpleFutureWrapper", + "package " + NOTES_PACKAGE + ";", + "import com.google.android.enterprise.connectedapps.FutureWrapper;", + "import com.google.android.enterprise.connectedapps.internal.FutureResultWriter;", + "import com.google.android.enterprise.connectedapps.Profile;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import" + + " com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger;", + "import java.util.Map;", + "@com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper(", + "originalType = SimpleFuture.class)", + "public final class SimpleFutureWrapper<E> extends FutureWrapper<E> {", + "private final SimpleFuture<E> future = new SimpleFuture<>();", + "public static <E> SimpleFutureWrapper<E> create(Bundler bundler, BundlerType" + + " bundlerType) {", + "return new SimpleFutureWrapper<>(bundler, bundlerType);", + "}", + "private SimpleFutureWrapper(Bundler bundler, BundlerType bundlerType) {", + "super(bundler, bundlerType);", + "}", + "public SimpleFuture<E> getFuture() {", + "return future;", + "}", + "@Override", + "public void onResult(E result) {", + "future.set(result);", + "}", + "@Override", + "public void onException(Throwable throwable) {", + "future.setException(throwable);", + "}", + "public static <E> void writeFutureResult(", + "String s,", + "SimpleFuture<E> future,", + "FutureResultWriter<E> resultWriter) {", + "future.setCallback(", + "(value) -> {", + "resultWriter.onSuccess(value);", + "},", + "(exception) -> {", + "resultWriter.onFailure(exception);", + "});", + "}", + "public static <E> SimpleFuture<Map<Profile, E>> groupResults(", + "Map<Profile, SimpleFuture<E>> results) {", + "SimpleFuture<Map<Profile, E>> m = new SimpleFuture<>();", + "CrossProfileCallbackMultiMerger<E> merger =", + "new CrossProfileCallbackMultiMerger<>(results.size(), m::set);", + "for (Map.Entry<Profile, SimpleFuture<E>> result : results.entrySet()) {", + "result", + ".getValue()", + ".setCallback(", + "(value) -> {", + "merger.onResult(result.getKey(), value);", + "},", + "(throwable) -> {", + "merger.missingResult(result.getKey());", + "});", + "}", + "return m;", + "}", + "}"); + + private static final JavaFileObject SIMPLE_FUTURE_WRAPPER_NO_GROUP_METHOD = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".SimpleFutureWrapper", + "package " + NOTES_PACKAGE + ";", + "import com.google.android.enterprise.connectedapps.FutureWrapper;", + "import com.google.android.enterprise.connectedapps.internal.FutureResultWriter;", + "import com.google.android.enterprise.connectedapps.Profile;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import" + + " com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger;", + "import java.util.Map;", + "@com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper(", + "originalType = SimpleFuture.class)", + "public final class SimpleFutureWrapper<E> extends FutureWrapper<E> {", + "private final SimpleFuture<E> future = new SimpleFuture<>();", + "public static <E> SimpleFutureWrapper<E> create(Bundler bundler, BundlerType" + + " bundlerType) {", + "return new SimpleFutureWrapper<>(bundler, bundlerType);", + "}", + "private SimpleFutureWrapper(Bundler bundler, BundlerType bundlerType) {", + "super(bundler, bundlerType);", + "}", + "public SimpleFuture<E> getFuture() {", + "return future;", + "}", + "@Override", + "public void onResult(E result) {", + "future.set(result);", + "}", + "@Override", + "public void onException(Throwable throwable) {", + "future.setException(throwable);", + "}", + "public static <E> void writeFutureResult(", + "SimpleFuture<E> future,", + "FutureResultWriter<E> resultWriter) {", + "future.setCallback(", + "(value) -> {", + "resultWriter.onSuccess(value);", + "},", + "(exception) -> {", + "resultWriter.onFailure(exception);", + "});", + "}", + "}"); + + private static final JavaFileObject SIMPLE_FUTURE_WRAPPER_INCORRECT_GROUP_METHOD = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".SimpleFutureWrapper", + "package " + NOTES_PACKAGE + ";", + "import com.google.android.enterprise.connectedapps.FutureWrapper;", + "import com.google.android.enterprise.connectedapps.internal.FutureResultWriter;", + "import com.google.android.enterprise.connectedapps.Profile;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import" + + " com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger;", + "import java.util.Map;", + "@com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper(", + "originalType = SimpleFuture.class)", + "public final class SimpleFutureWrapper<E> extends FutureWrapper<E> {", + "private final SimpleFuture<E> future = new SimpleFuture<>();", + "public static <E> SimpleFutureWrapper<E> create(Bundler bundler, BundlerType" + + " bundlerType) {", + "return new SimpleFutureWrapper<>(bundler, bundlerType);", + "}", + "private SimpleFutureWrapper(Bundler bundler, BundlerType bundlerType) {", + "super(bundler, bundlerType);", + "}", + "public SimpleFuture<E> getFuture() {", + "return future;", + "}", + "@Override", + "public void onResult(E result) {", + "future.set(result);", + "}", + "@Override", + "public void onException(Throwable throwable) {", + "future.setException(throwable);", + "}", + "public static <E> void writeFutureResult(", + "SimpleFuture<E> future,", + "FutureResultWriter<E> resultWriter) {", + "future.setCallback(", + "(value) -> {", + "resultWriter.onSuccess(value);", + "},", + "(exception) -> {", + "resultWriter.onFailure(exception);", + "});", + "}", + "public static <E> SimpleFuture<Map<Profile, E>> groupResults(", + "Map<String, SimpleFuture<E>> results) {", + "return null;", + "}", + "}"); + + @Test + public void validFutureWrapperAnnotation_compiles() { + Compilation compilation = + javac().withProcessors(new Processor()).compile(SIMPLE_FUTURE_WRAPPER, SIMPLE_FUTURE); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void futureWrapperAnnotation_isNotGeneric_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(SIMPLE_FUTURE_WRAPPER_IS_NOT_GENERIC, SIMPLE_FUTURE); + + assertThat(compilation) + .hadErrorContaining(MUST_HAVE_ONE_TYPE_PARAMETER_ERROR) + .inFile(SIMPLE_FUTURE_WRAPPER_IS_NOT_GENERIC); + } + + @Test + public void futureWrapperAnnotation_hasMultipleTypeParameters_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(SIMPLE_FUTURE_WRAPPER_MULTIPLE_TYPE_PARAMETERS, SIMPLE_FUTURE); + + assertThat(compilation) + .hadErrorContaining(MUST_HAVE_ONE_TYPE_PARAMETER_ERROR) + .inFile(SIMPLE_FUTURE_WRAPPER_MULTIPLE_TYPE_PARAMETERS); + } + + @Test + public void futureWrapperAnnotation_doesNotExtendFutureWrapper_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(SIMPLE_FUTURE_WRAPPER_DOES_NOT_EXTEND_FUTURE_WRAPPER, SIMPLE_FUTURE); + + assertThat(compilation) + .hadErrorContaining(DOES_NOT_EXTEND_FUTURE_WRAPPER_ERROR) + .inFile(SIMPLE_FUTURE_WRAPPER_DOES_NOT_EXTEND_FUTURE_WRAPPER); + } + + @Test + public void futureWrapperAnnotation_noCreateMethod_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(SIMPLE_FUTURE_WRAPPER_NO_CREATE_METHOD, SIMPLE_FUTURE); + + assertThat(compilation) + .hadErrorContaining(INCORRECT_CREATE_METHOD_ERROR) + .inFile(SIMPLE_FUTURE_WRAPPER_NO_CREATE_METHOD); + } + + @Test + public void futureWrapperAnnotation_incorrectCreateMethod_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(SIMPLE_FUTURE_WRAPPER_INCORRECT_CREATE_METHOD, SIMPLE_FUTURE); + + assertThat(compilation) + .hadErrorContaining(INCORRECT_CREATE_METHOD_ERROR) + .inFile(SIMPLE_FUTURE_WRAPPER_INCORRECT_CREATE_METHOD); + } + + @Test + public void futureWrapperAnnotation_noGetFutureMethod_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(SIMPLE_FUTURE_WRAPPER_NO_GET_FUTURE_METHOD, SIMPLE_FUTURE); + + assertThat(compilation) + .hadErrorContaining(INCORRECT_GET_FUTURE_METHOD_ERROR) + .inFile(SIMPLE_FUTURE_WRAPPER_NO_GET_FUTURE_METHOD); + } + + @Test + public void futureWrapperAnnotation_incorrectGetFutureMethod_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(SIMPLE_FUTURE_WRAPPER_INCORRECT_GET_FUTURE_METHOD, SIMPLE_FUTURE); + + assertThat(compilation) + .hadErrorContaining(INCORRECT_GET_FUTURE_METHOD_ERROR) + .inFile(SIMPLE_FUTURE_WRAPPER_INCORRECT_GET_FUTURE_METHOD); + } + + @Test + public void futureWrapperAnnotation_noResolveFutureMethod_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(SIMPLE_FUTURE_WRAPPER_NO_RESOLVE_METHOD, SIMPLE_FUTURE); + + assertThat(compilation) + .hadErrorContaining(INCORRECT_RESOLVE_CALLBACK_WHEN_FUTURE_IS_SET_METHOD_ERROR) + .inFile(SIMPLE_FUTURE_WRAPPER_NO_RESOLVE_METHOD); + } + + @Test + public void futureWrapperAnnotation_incorrectResolveFutureMethod_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(SIMPLE_FUTURE_WRAPPER_INCORRECT_RESOLVE_METHOD, SIMPLE_FUTURE); + + assertThat(compilation) + .hadErrorContaining(INCORRECT_RESOLVE_CALLBACK_WHEN_FUTURE_IS_SET_METHOD_ERROR) + .inFile(SIMPLE_FUTURE_WRAPPER_INCORRECT_RESOLVE_METHOD); + } + + @Test + public void futureWrapperAnnotation_noGroupResultsMethod_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(SIMPLE_FUTURE_WRAPPER_NO_GROUP_METHOD, SIMPLE_FUTURE); + + assertThat(compilation) + .hadErrorContaining(INCORRECT_GROUP_RESULTS_METHOD_ERROR) + .inFile(SIMPLE_FUTURE_WRAPPER_NO_GROUP_METHOD); + } + + @Test + public void futureWrapperAnnotation_incorrectGroupResultsMethod_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(SIMPLE_FUTURE_WRAPPER_INCORRECT_GROUP_METHOD, SIMPLE_FUTURE); + + assertThat(compilation) + .hadErrorContaining(INCORRECT_GROUP_RESULTS_METHOD_ERROR) + .inFile(SIMPLE_FUTURE_WRAPPER_INCORRECT_GROUP_METHOD); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CustomParcelableWrapperTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CustomParcelableWrapperTest.java new file mode 100644 index 0000000..4c4b4ee --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CustomParcelableWrapperTest.java @@ -0,0 +1,799 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.CUSTOM_WRAPPER; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.PARCELABLE_CUSTOM_WRAPPER; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class CustomParcelableWrapperTest { + + private static final String NOT_PARCELABLE_ERROR = + "Classes annotated @CustomParcelableWrapper must implement Parcelable"; + private static final String INCORRECT_OF_METHOD = + "Classes annotated @CustomParcelableWrapper must have a static 'of' method which takes a" + + " Bundler, a BundlerType, and an instance of the wrapped type as arguments and returns" + + " an instance of the parcelable wrapper"; + private static final String INCORRECT_GET_METHOD = + "Classes annotated @CustomParcelableWrapper must have a static 'get' method which takes no" + + " arguments and returns an instance of the wrapped class"; + private static final String INCORRECT_PARCELABLE_IMPLEMENTATION = + "Classes annotated @CustomParcelableWrapper must correctly implement Parcelable"; + + static final JavaFileObject PARCELABLE_CUSTOM_WRAPPER_NOT_GENERIC = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".ParcelableCustomWrapper", + "package " + NOTES_PACKAGE + ";", + "import android.os.Parcel;", + "import android.os.Parcelable;", + "import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "@CustomParcelableWrapper(originalType = CustomWrapper.class)", + "public class ParcelableCustomWrapper implements Parcelable {", + "private static final int NULL = -1;", + "private static final int NOT_NULL = 1;", + "public static ParcelableCustomWrapper of(", + "Bundler bundler, BundlerType type, CustomWrapper customWrapper) {", + "return null;", + "}", + "public CustomWrapper get() {", + "return null;", + "}", + "private ParcelableCustomWrapper(Parcel in) {", + "}", + "@Override", + "public void writeToParcel(Parcel dest, int flags) {", + "}", + "@Override", + "public int describeContents() {", + "return 0;", + "}", + "@SuppressWarnings(\"rawtypes\")", + "public static final Creator<ParcelableCustomWrapper> CREATOR =", + "new Creator<ParcelableCustomWrapper>() {", + "@Override", + "public ParcelableCustomWrapper createFromParcel(Parcel in) {", + "return new ParcelableCustomWrapper(in);", + "}", + "@Override", + "public ParcelableCustomWrapper[] newArray(int size) {", + "return new ParcelableCustomWrapper[size];", + "}", + "};", + "}"); + + static final JavaFileObject PARCELABLE_CUSTOM_WRAPPER_DOES_NOT_IMPLEMENT_PARCELABLE = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".ParcelableCustomWrapper", + "package " + NOTES_PACKAGE + ";", + "import android.os.Parcel;", + "import android.os.Parcelable;", + "import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import com.google.android.enterprise.connectedapps.testapp.CustomWrapper;", + "@CustomParcelableWrapper(originalType = CustomWrapper.class)", + "public class ParcelableCustomWrapper<E> {", + "private static final int NULL = -1;", + "private static final int NOT_NULL = 1;", + "private final Bundler bundler;", + "private final BundlerType type;", + "private final CustomWrapper<E> customWrapper;", + "public static <F> ParcelableCustomWrapper<F> of(", + "Bundler bundler, BundlerType type, CustomWrapper<F> customWrapper) {", + "return new ParcelableCustomWrapper<>(bundler, type, customWrapper);", + "}", + "public CustomWrapper<E> get() {", + "return customWrapper;", + "}", + "private ParcelableCustomWrapper(Bundler bundler, BundlerType type, CustomWrapper<E>" + + " customWrapper) {", + "if (bundler == null || type == null) {", + "throw new NullPointerException();", + "}", + "this.bundler = bundler;", + "this.type = type;", + "this.customWrapper = customWrapper;", + "}", + "private ParcelableCustomWrapper(Parcel in) {", + "bundler = in.readParcelable(Bundler.class.getClassLoader());", + "int presentValue = in.readInt();", + "if (presentValue == NULL) {", + "type = null;", + "customWrapper = null;", + "return;", + "}", + "type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader());", + "BundlerType valueType = type.typeArguments().get(0);", + "@SuppressWarnings(\"unchecked\")", + "E value = (E) bundler.readFromParcel(in, valueType);", + "customWrapper = new CustomWrapper<>(value);", + "}", + "@Override", + "public void writeToParcel(Parcel dest, int flags) {", + "dest.writeParcelable(bundler, flags);", + "if (customWrapper == null) {", + "dest.writeInt(NULL);", + "return;", + "}", + "dest.writeInt(NOT_NULL);", + "dest.writeParcelable(type, flags);", + "BundlerType valueType = type.typeArguments().get(0);", + "bundler.writeToParcel(dest, customWrapper.value(), valueType, flags);", + "}", + "@Override", + "public int describeContents() {", + "return 0;", + "}", + "@SuppressWarnings(\"rawtypes\")", + "public static final Creator<ParcelableCustomWrapper> CREATOR =", + "new Creator<ParcelableCustomWrapper>() {", + "@Override", + "public ParcelableCustomWrapper createFromParcel(Parcel in) {", + "return new ParcelableCustomWrapper(in);", + "}", + "@Override", + "public ParcelableCustomWrapper[] newArray(int size) {", + "return new ParcelableCustomWrapper[size];", + "}", + "};", + "}"); + + static final JavaFileObject PARCELABLE_CUSTOM_WRAPPER_NO_OF_METHOD = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".ParcelableCustomWrapper", + "package " + NOTES_PACKAGE + ";", + "import android.os.Parcel;", + "import android.os.Parcelable;", + "import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import com.google.android.enterprise.connectedapps.testapp.CustomWrapper;", + "@CustomParcelableWrapper(originalType = CustomWrapper.class)", + "public class ParcelableCustomWrapper<E> implements Parcelable {", + "private static final int NULL = -1;", + "private static final int NOT_NULL = 1;", + "private final Bundler bundler;", + "private final BundlerType type;", + "private final CustomWrapper<E> customWrapper;", + "public CustomWrapper<E> get() {", + "return customWrapper;", + "}", + "private ParcelableCustomWrapper(Bundler bundler, BundlerType type, CustomWrapper<E>" + + " customWrapper) {", + "if (bundler == null || type == null) {", + "throw new NullPointerException();", + "}", + "this.bundler = bundler;", + "this.type = type;", + "this.customWrapper = customWrapper;", + "}", + "private ParcelableCustomWrapper(Parcel in) {", + "bundler = in.readParcelable(Bundler.class.getClassLoader());", + "int presentValue = in.readInt();", + "if (presentValue == NULL) {", + "type = null;", + "customWrapper = null;", + "return;", + "}", + "type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader());", + "BundlerType valueType = type.typeArguments().get(0);", + "@SuppressWarnings(\"unchecked\")", + "E value = (E) bundler.readFromParcel(in, valueType);", + "customWrapper = new CustomWrapper<>(value);", + "}", + "@Override", + "public void writeToParcel(Parcel dest, int flags) {", + "dest.writeParcelable(bundler, flags);", + "if (customWrapper == null) {", + "dest.writeInt(NULL);", + "return;", + "}", + "dest.writeInt(NOT_NULL);", + "dest.writeParcelable(type, flags);", + "BundlerType valueType = type.typeArguments().get(0);", + "bundler.writeToParcel(dest, customWrapper.value(), valueType, flags);", + "}", + "@Override", + "public int describeContents() {", + "return 0;", + "}", + "@SuppressWarnings(\"rawtypes\")", + "public static final Creator<ParcelableCustomWrapper> CREATOR =", + "new Creator<ParcelableCustomWrapper>() {", + "@Override", + "public ParcelableCustomWrapper createFromParcel(Parcel in) {", + "return new ParcelableCustomWrapper(in);", + "}", + "@Override", + "public ParcelableCustomWrapper[] newArray(int size) {", + "return new ParcelableCustomWrapper[size];", + "}", + "};", + "}"); + + static final JavaFileObject PARCELABLE_CUSTOM_WRAPPER_OF_METHOD_WRONG_ARGUMENTS = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".ParcelableCustomWrapper", + "package " + NOTES_PACKAGE + ";", + "import android.os.Parcel;", + "import android.os.Parcelable;", + "import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import com.google.android.enterprise.connectedapps.testapp.CustomWrapper;", + "@CustomParcelableWrapper(originalType = CustomWrapper.class)", + "public class ParcelableCustomWrapper<E> implements Parcelable {", + "private static final int NULL = -1;", + "private static final int NOT_NULL = 1;", + "private final Bundler bundler;", + "private final BundlerType type;", + "private final CustomWrapper<E> customWrapper;", + "public static <F> ParcelableCustomWrapper<F> of(", + "Bundler bundler, CustomWrapper<F> customWrapper) {", + "return null;", + "}", + "public CustomWrapper<E> get() {", + "return customWrapper;", + "}", + "private ParcelableCustomWrapper(Bundler bundler, BundlerType type, CustomWrapper<E>" + + " customWrapper) {", + "if (bundler == null || type == null) {", + "throw new NullPointerException();", + "}", + "this.bundler = bundler;", + "this.type = type;", + "this.customWrapper = customWrapper;", + "}", + "private ParcelableCustomWrapper(Parcel in) {", + "bundler = in.readParcelable(Bundler.class.getClassLoader());", + "int presentValue = in.readInt();", + "if (presentValue == NULL) {", + "type = null;", + "customWrapper = null;", + "return;", + "}", + "type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader());", + "BundlerType valueType = type.typeArguments().get(0);", + "@SuppressWarnings(\"unchecked\")", + "E value = (E) bundler.readFromParcel(in, valueType);", + "customWrapper = new CustomWrapper<>(value);", + "}", + "@Override", + "public void writeToParcel(Parcel dest, int flags) {", + "dest.writeParcelable(bundler, flags);", + "if (customWrapper == null) {", + "dest.writeInt(NULL);", + "return;", + "}", + "dest.writeInt(NOT_NULL);", + "dest.writeParcelable(type, flags);", + "BundlerType valueType = type.typeArguments().get(0);", + "bundler.writeToParcel(dest, customWrapper.value(), valueType, flags);", + "}", + "@Override", + "public int describeContents() {", + "return 0;", + "}", + "@SuppressWarnings(\"rawtypes\")", + "public static final Creator<ParcelableCustomWrapper> CREATOR =", + "new Creator<ParcelableCustomWrapper>() {", + "@Override", + "public ParcelableCustomWrapper createFromParcel(Parcel in) {", + "return new ParcelableCustomWrapper(in);", + "}", + "@Override", + "public ParcelableCustomWrapper[] newArray(int size) {", + "return new ParcelableCustomWrapper[size];", + "}", + "};", + "}"); + + static final JavaFileObject PARCELABLE_CUSTOM_WRAPPER_OF_METHOD_WRONG_RETURN_TYPE = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".ParcelableCustomWrapper", + "package " + NOTES_PACKAGE + ";", + "import android.os.Parcel;", + "import android.os.Parcelable;", + "import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import com.google.android.enterprise.connectedapps.testapp.CustomWrapper;", + "@CustomParcelableWrapper(originalType = CustomWrapper.class)", + "public class ParcelableCustomWrapper<E> implements Parcelable {", + "private static final int NULL = -1;", + "private static final int NOT_NULL = 1;", + "private final Bundler bundler;", + "private final BundlerType type;", + "private final CustomWrapper<E> customWrapper;", + "public static<F> String of(", + "Bundler bundler, BundlerType bundlerType, CustomWrapper<F> customWrapper) {", + "return null;", + "}", + "public CustomWrapper<E> get() {", + "return customWrapper;", + "}", + "private ParcelableCustomWrapper(Bundler bundler, BundlerType type, CustomWrapper<E>" + + " customWrapper) {", + "if (bundler == null || type == null) {", + "throw new NullPointerException();", + "}", + "this.bundler = bundler;", + "this.type = type;", + "this.customWrapper = customWrapper;", + "}", + "private ParcelableCustomWrapper(Parcel in) {", + "bundler = in.readParcelable(Bundler.class.getClassLoader());", + "int presentValue = in.readInt();", + "if (presentValue == NULL) {", + "type = null;", + "customWrapper = null;", + "return;", + "}", + "type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader());", + "BundlerType valueType = type.typeArguments().get(0);", + "@SuppressWarnings(\"unchecked\")", + "E value = (E) bundler.readFromParcel(in, valueType);", + "customWrapper = new CustomWrapper<>(value);", + "}", + "@Override", + "public void writeToParcel(Parcel dest, int flags) {", + "dest.writeParcelable(bundler, flags);", + "if (customWrapper == null) {", + "dest.writeInt(NULL);", + "return;", + "}", + "dest.writeInt(NOT_NULL);", + "dest.writeParcelable(type, flags);", + "BundlerType valueType = type.typeArguments().get(0);", + "bundler.writeToParcel(dest, customWrapper.value(), valueType, flags);", + "}", + "@Override", + "public int describeContents() {", + "return 0;", + "}", + "@SuppressWarnings(\"rawtypes\")", + "public static final Creator<ParcelableCustomWrapper> CREATOR =", + "new Creator<ParcelableCustomWrapper>() {", + "@Override", + "public ParcelableCustomWrapper createFromParcel(Parcel in) {", + "return new ParcelableCustomWrapper(in);", + "}", + "@Override", + "public ParcelableCustomWrapper[] newArray(int size) {", + "return new ParcelableCustomWrapper[size];", + "}", + "};", + "}"); + + static final JavaFileObject PARCELABLE_CUSTOM_WRAPPER_NO_GET_METHOD = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".ParcelableCustomWrapper", + "package " + NOTES_PACKAGE + ";", + "import android.os.Parcel;", + "import android.os.Parcelable;", + "import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import com.google.android.enterprise.connectedapps.testapp.CustomWrapper;", + "@CustomParcelableWrapper(originalType = CustomWrapper.class)", + "public class ParcelableCustomWrapper<E> implements Parcelable {", + "private static final int NULL = -1;", + "private static final int NOT_NULL = 1;", + "private final Bundler bundler;", + "private final BundlerType type;", + "private final CustomWrapper<E> customWrapper;", + "public static <F> ParcelableCustomWrapper<F> of(", + "Bundler bundler, BundlerType type, CustomWrapper<F> customWrapper) {", + "return new ParcelableCustomWrapper<>(bundler, type, customWrapper);", + "}", + "private ParcelableCustomWrapper(Bundler bundler, BundlerType type, CustomWrapper<E>" + + " customWrapper) {", + "if (bundler == null || type == null) {", + "throw new NullPointerException();", + "}", + "this.bundler = bundler;", + "this.type = type;", + "this.customWrapper = customWrapper;", + "}", + "private ParcelableCustomWrapper(Parcel in) {", + "bundler = in.readParcelable(Bundler.class.getClassLoader());", + "int presentValue = in.readInt();", + "if (presentValue == NULL) {", + "type = null;", + "customWrapper = null;", + "return;", + "}", + "type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader());", + "BundlerType valueType = type.typeArguments().get(0);", + "@SuppressWarnings(\"unchecked\")", + "E value = (E) bundler.readFromParcel(in, valueType);", + "customWrapper = new CustomWrapper<>(value);", + "}", + "@Override", + "public void writeToParcel(Parcel dest, int flags) {", + "dest.writeParcelable(bundler, flags);", + "if (customWrapper == null) {", + "dest.writeInt(NULL);", + "return;", + "}", + "dest.writeInt(NOT_NULL);", + "dest.writeParcelable(type, flags);", + "BundlerType valueType = type.typeArguments().get(0);", + "bundler.writeToParcel(dest, customWrapper.value(), valueType, flags);", + "}", + "@Override", + "public int describeContents() {", + "return 0;", + "}", + "@SuppressWarnings(\"rawtypes\")", + "public static final Creator<ParcelableCustomWrapper> CREATOR =", + "new Creator<ParcelableCustomWrapper>() {", + "@Override", + "public ParcelableCustomWrapper createFromParcel(Parcel in) {", + "return new ParcelableCustomWrapper(in);", + "}", + "@Override", + "public ParcelableCustomWrapper[] newArray(int size) {", + "return new ParcelableCustomWrapper[size];", + "}", + "};", + "}"); + + static final JavaFileObject PARCELABLE_CUSTOM_WRAPPER_GET_METHOD_WRONG_RETURN_TYPE = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".ParcelableCustomWrapper", + "package " + NOTES_PACKAGE + ";", + "import android.os.Parcel;", + "import android.os.Parcelable;", + "import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import com.google.android.enterprise.connectedapps.testapp.CustomWrapper;", + "@CustomParcelableWrapper(originalType = CustomWrapper.class)", + "public class ParcelableCustomWrapper<E> implements Parcelable {", + "private static final int NULL = -1;", + "private static final int NOT_NULL = 1;", + "private final Bundler bundler;", + "private final BundlerType type;", + "private final CustomWrapper<E> customWrapper;", + "public static <F> ParcelableCustomWrapper<F> of(", + "Bundler bundler, BundlerType type, CustomWrapper<F> customWrapper) {", + "return new ParcelableCustomWrapper<>(bundler, type, customWrapper);", + "}", + "public String get() {", + "return null;", + "}", + "private ParcelableCustomWrapper(Bundler bundler, BundlerType type, CustomWrapper<E>" + + " customWrapper) {", + "if (bundler == null || type == null) {", + "throw new NullPointerException();", + "}", + "this.bundler = bundler;", + "this.type = type;", + "this.customWrapper = customWrapper;", + "}", + "private ParcelableCustomWrapper(Parcel in) {", + "bundler = in.readParcelable(Bundler.class.getClassLoader());", + "int presentValue = in.readInt();", + "if (presentValue == NULL) {", + "type = null;", + "customWrapper = null;", + "return;", + "}", + "type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader());", + "BundlerType valueType = type.typeArguments().get(0);", + "@SuppressWarnings(\"unchecked\")", + "E value = (E) bundler.readFromParcel(in, valueType);", + "customWrapper = new CustomWrapper<>(value);", + "}", + "@Override", + "public void writeToParcel(Parcel dest, int flags) {", + "dest.writeParcelable(bundler, flags);", + "if (customWrapper == null) {", + "dest.writeInt(NULL);", + "return;", + "}", + "dest.writeInt(NOT_NULL);", + "dest.writeParcelable(type, flags);", + "BundlerType valueType = type.typeArguments().get(0);", + "bundler.writeToParcel(dest, customWrapper.value(), valueType, flags);", + "}", + "@Override", + "public int describeContents() {", + "return 0;", + "}", + "@SuppressWarnings(\"rawtypes\")", + "public static final Creator<ParcelableCustomWrapper> CREATOR =", + "new Creator<ParcelableCustomWrapper>() {", + "@Override", + "public ParcelableCustomWrapper createFromParcel(Parcel in) {", + "return new ParcelableCustomWrapper(in);", + "}", + "@Override", + "public ParcelableCustomWrapper[] newArray(int size) {", + "return new ParcelableCustomWrapper[size];", + "}", + "};", + "}"); + + static final JavaFileObject PARCELABLE_CUSTOM_WRAPPER_NO_CREATOR = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".ParcelableCustomWrapper", + "package " + NOTES_PACKAGE + ";", + "import android.os.Parcel;", + "import android.os.Parcelable;", + "import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import com.google.android.enterprise.connectedapps.testapp.CustomWrapper;", + "@CustomParcelableWrapper(originalType = CustomWrapper.class)", + "public class ParcelableCustomWrapper<E> implements Parcelable {", + "private static final int NULL = -1;", + "private static final int NOT_NULL = 1;", + "private final Bundler bundler;", + "private final BundlerType type;", + "private final CustomWrapper<E> customWrapper;", + "public static <F> ParcelableCustomWrapper<F> of(", + "Bundler bundler, BundlerType type, CustomWrapper<F> customWrapper) {", + "return new ParcelableCustomWrapper<>(bundler, type, customWrapper);", + "}", + "public CustomWrapper<E> get() {", + "return customWrapper;", + "}", + "private ParcelableCustomWrapper(Bundler bundler, BundlerType type, CustomWrapper<E>" + + " customWrapper) {", + "if (bundler == null || type == null) {", + "throw new NullPointerException();", + "}", + "this.bundler = bundler;", + "this.type = type;", + "this.customWrapper = customWrapper;", + "}", + "private ParcelableCustomWrapper(Parcel in) {", + "bundler = in.readParcelable(Bundler.class.getClassLoader());", + "int presentValue = in.readInt();", + "if (presentValue == NULL) {", + "type = null;", + "customWrapper = null;", + "return;", + "}", + "type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader());", + "BundlerType valueType = type.typeArguments().get(0);", + "@SuppressWarnings(\"unchecked\")", + "E value = (E) bundler.readFromParcel(in, valueType);", + "customWrapper = new CustomWrapper<>(value);", + "}", + "@Override", + "public void writeToParcel(Parcel dest, int flags) {", + "dest.writeParcelable(bundler, flags);", + "if (customWrapper == null) {", + "dest.writeInt(NULL);", + "return;", + "}", + "dest.writeInt(NOT_NULL);", + "dest.writeParcelable(type, flags);", + "BundlerType valueType = type.typeArguments().get(0);", + "bundler.writeToParcel(dest, customWrapper.value(), valueType, flags);", + "}", + "@Override", + "public int describeContents() {", + "return 0;", + "}", + "}"); + + static final JavaFileObject PARCELABLE_CUSTOM_WRAPPER_INCORRECT_CREATOR = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".ParcelableCustomWrapper", + "package " + NOTES_PACKAGE + ";", + "import android.os.Parcel;", + "import android.os.Parcelable;", + "import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import com.google.android.enterprise.connectedapps.testapp.CustomWrapper;", + "@CustomParcelableWrapper(originalType = CustomWrapper.class)", + "public class ParcelableCustomWrapper<E> implements Parcelable {", + "private static final int NULL = -1;", + "private static final int NOT_NULL = 1;", + "private final Bundler bundler;", + "private final BundlerType type;", + "private final CustomWrapper<E> customWrapper;", + "public static <F> ParcelableCustomWrapper<F> of(", + "Bundler bundler, BundlerType type, CustomWrapper<F> customWrapper) {", + "return new ParcelableCustomWrapper<>(bundler, type, customWrapper);", + "}", + "public CustomWrapper<E> get() {", + "return customWrapper;", + "}", + "private ParcelableCustomWrapper(Bundler bundler, BundlerType type, CustomWrapper<E>" + + " customWrapper) {", + "if (bundler == null || type == null) {", + "throw new NullPointerException();", + "}", + "this.bundler = bundler;", + "this.type = type;", + "this.customWrapper = customWrapper;", + "}", + "private ParcelableCustomWrapper(Parcel in) {", + "bundler = in.readParcelable(Bundler.class.getClassLoader());", + "int presentValue = in.readInt();", + "if (presentValue == NULL) {", + "type = null;", + "customWrapper = null;", + "return;", + "}", + "type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader());", + "BundlerType valueType = type.typeArguments().get(0);", + "@SuppressWarnings(\"unchecked\")", + "E value = (E) bundler.readFromParcel(in, valueType);", + "customWrapper = new CustomWrapper<>(value);", + "}", + "@Override", + "public void writeToParcel(Parcel dest, int flags) {", + "dest.writeParcelable(bundler, flags);", + "if (customWrapper == null) {", + "dest.writeInt(NULL);", + "return;", + "}", + "dest.writeInt(NOT_NULL);", + "dest.writeParcelable(type, flags);", + "BundlerType valueType = type.typeArguments().get(0);", + "bundler.writeToParcel(dest, customWrapper.value(), valueType, flags);", + "}", + "@Override", + "public int describeContents() {", + "return 0;", + "}", + "@SuppressWarnings(\"rawtypes\")", + "public static final Creator<String> CREATOR =", + "new Creator<String>() {", + "@Override", + "public String createFromParcel(Parcel in) {", + "return null;", + "}", + "@Override", + "public String[] newArray(int size) {", + "return new String[size];", + "}", + "};", + "}"); + + @Test + public void validParcelableWrapperAnnotation_compiles() { + Compilation compilation = + javac().withProcessors(new Processor()).compile(CUSTOM_WRAPPER, PARCELABLE_CUSTOM_WRAPPER); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void validParcelableWrapperAnnotation_notGeneric_compiles() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(CUSTOM_WRAPPER, PARCELABLE_CUSTOM_WRAPPER_NOT_GENERIC); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void parcelableWrapper_doesNotImplementParcelable_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(CUSTOM_WRAPPER, PARCELABLE_CUSTOM_WRAPPER_DOES_NOT_IMPLEMENT_PARCELABLE); + + assertThat(compilation) + .hadErrorContaining(NOT_PARCELABLE_ERROR) + .inFile(PARCELABLE_CUSTOM_WRAPPER_DOES_NOT_IMPLEMENT_PARCELABLE); + } + + @Test + public void parcelableWrapper_hasNoOfMethod_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(CUSTOM_WRAPPER, PARCELABLE_CUSTOM_WRAPPER_NO_OF_METHOD); + + assertThat(compilation) + .hadErrorContaining(INCORRECT_OF_METHOD) + .inFile(PARCELABLE_CUSTOM_WRAPPER_NO_OF_METHOD); + } + + @Test + public void parcelableWrapper_ofMethodWrongReturnType_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(CUSTOM_WRAPPER, PARCELABLE_CUSTOM_WRAPPER_OF_METHOD_WRONG_RETURN_TYPE); + + assertThat(compilation) + .hadErrorContaining(INCORRECT_OF_METHOD) + .inFile(PARCELABLE_CUSTOM_WRAPPER_OF_METHOD_WRONG_RETURN_TYPE); + } + + @Test + public void parcelableWrapper_ofMethodWrongArguments_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(CUSTOM_WRAPPER, PARCELABLE_CUSTOM_WRAPPER_OF_METHOD_WRONG_ARGUMENTS); + + assertThat(compilation) + .hadErrorContaining(INCORRECT_OF_METHOD) + .inFile(PARCELABLE_CUSTOM_WRAPPER_OF_METHOD_WRONG_ARGUMENTS); + } + + @Test + public void parcelableWrapper_hasNoGetMethod_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(CUSTOM_WRAPPER, PARCELABLE_CUSTOM_WRAPPER_NO_GET_METHOD); + + assertThat(compilation) + .hadErrorContaining(INCORRECT_GET_METHOD) + .inFile(PARCELABLE_CUSTOM_WRAPPER_NO_GET_METHOD); + } + + @Test + public void parcelableWrapper_getMethodWrongReturnType_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(CUSTOM_WRAPPER, PARCELABLE_CUSTOM_WRAPPER_GET_METHOD_WRONG_RETURN_TYPE); + + assertThat(compilation) + .hadErrorContaining(INCORRECT_GET_METHOD) + .inFile(PARCELABLE_CUSTOM_WRAPPER_GET_METHOD_WRONG_RETURN_TYPE); + } + + @Test + public void parcelableWrapper_noCreator_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(CUSTOM_WRAPPER, PARCELABLE_CUSTOM_WRAPPER_NO_CREATOR); + + assertThat(compilation) + .hadErrorContaining(INCORRECT_PARCELABLE_IMPLEMENTATION) + .inFile(PARCELABLE_CUSTOM_WRAPPER_NO_CREATOR); + } + + @Test + public void parcelableWrapper_incorrectCreator_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(CUSTOM_WRAPPER, PARCELABLE_CUSTOM_WRAPPER_INCORRECT_CREATOR); + + assertThat(compilation) + .hadErrorContaining(INCORRECT_PARCELABLE_IMPLEMENTATION) + .inFile(PARCELABLE_CUSTOM_WRAPPER_INCORRECT_CREATOR); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CustomProfileConnectorTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CustomProfileConnectorTest.java new file mode 100644 index 0000000..6498559 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CustomProfileConnectorTest.java @@ -0,0 +1,160 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class CustomProfileConnectorTest { + + private static final String NOT_INTERFACE_ERROR = + "@CustomProfileConnector must only be applied to interfaces"; + private static final String NOT_IMPLEMENTING_CONNECTOR_ERROR = + "Interfaces annotated with @CustomProfileConnector must extend ProfileConnector"; + private static final String PARCELABLE_WRAPPER_ANNOTATION_ERROR = + "Parcelable Wrappers must be annotated @CustomParcelableWrapper"; + private static final String FUTURE_WRAPPER_ANNOTATION_ERROR = + "Future Wrappers must be annotated @CustomFutureWrapper"; + private static final String IMPORTS_NOT_CONNECTOR_ERROR = + "Classes included in includes= must be annotated @CustomProfileConnector"; + + @Test + public void isNotInterface_hasError() { + final JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector", + "public class NotesConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation).hadErrorContaining(NOT_INTERFACE_ERROR).inFile(connector); + } + + @Test + public void doesNotExtendProfileConnector_hasError() { + final JavaFileObject notImplementingBaseConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector", + "public interface NotesConnector {", + "}"); + + Compilation compilation = + javac().withProcessors(new Processor()).compile(notImplementingBaseConnector); + + assertThat(compilation) + .hadErrorContaining(NOT_IMPLEMENTING_CONNECTOR_ERROR) + .inFile(notImplementingBaseConnector); + } + + @Test + public void includesNonParcelableWrapperInParcelableWrappers_hasError() { + final JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(parcelableWrappers=String.class)", + "public interface NotesConnector extends ProfileConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation) + .hadErrorContaining(PARCELABLE_WRAPPER_ANNOTATION_ERROR) + .inFile(connector); + } + + @Test + public void includesNonFutureWrapperInFutureWrappers_hasError() { + final JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(futureWrappers=String.class)", + "public interface NotesConnector extends ProfileConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation).hadErrorContaining(FUTURE_WRAPPER_ANNOTATION_ERROR).inFile(connector); + } + + @Test + public void imports_containsNonProfileConnector_hasError() { + final JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(imports=String.class)", + "public interface NotesConnector extends ProfileConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation).hadErrorContaining(IMPORTS_NOT_CONNECTOR_ERROR).inFile(connector); + } + + @Test + public void imports_containsProfileConnector_compiles() { + final JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(imports=NotesConnector2.class)", + "public interface NotesConnector extends ProfileConnector {", + "}"); + final JavaFileObject connector2 = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector2", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector", + "public interface NotesConnector2 extends ProfileConnector {", + "}"); + + Compilation compilation = + javac().withProcessors(new Processor()).compile(connector, connector2); + + assertThat(compilation).succeededWithoutWarnings(); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CustomUserConnectorTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CustomUserConnectorTest.java new file mode 100644 index 0000000..ba7a554 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CustomUserConnectorTest.java @@ -0,0 +1,160 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.CUSTOM_USER_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.USER_CONNECTOR_QUALIFIED_NAME; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class CustomUserConnectorTest { + + private static final String NOT_INTERFACE_ERROR = + "@CustomUserConnector must only be applied to interfaces"; + private static final String NOT_IMPLEMENTING_CONNECTOR_ERROR = + "Interfaces annotated with @CustomUserConnector must extend UserConnector"; + private static final String PARCELABLE_WRAPPER_ANNOTATION_ERROR = + "Parcelable Wrappers must be annotated @CustomParcelableWrapper"; + private static final String FUTURE_WRAPPER_ANNOTATION_ERROR = + "Future Wrappers must be annotated @CustomFutureWrapper"; + private static final String IMPORTS_NOT_CONNECTOR_ERROR = + "Classes included in includes= must be annotated @CustomUserConnector"; + + @Test + public void isNotInterface_hasError() { + final JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_USER_CONNECTOR_QUALIFIED_NAME + ";", + "import " + USER_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomUserConnector", + "public class NotesConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation).hadErrorContaining(NOT_INTERFACE_ERROR).inFile(connector); + } + + @Test + public void doesNotExtendUserConnector_hasError() { + final JavaFileObject notImplementingBaseConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_USER_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomUserConnector", + "public interface NotesConnector {", + "}"); + + Compilation compilation = + javac().withProcessors(new Processor()).compile(notImplementingBaseConnector); + + assertThat(compilation) + .hadErrorContaining(NOT_IMPLEMENTING_CONNECTOR_ERROR) + .inFile(notImplementingBaseConnector); + } + + @Test + public void includesNonParcelableWrapperInParcelableWrappers_hasError() { + final JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_USER_CONNECTOR_QUALIFIED_NAME + ";", + "import " + USER_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomUserConnector(parcelableWrappers=String.class)", + "public interface NotesConnector extends UserConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation) + .hadErrorContaining(PARCELABLE_WRAPPER_ANNOTATION_ERROR) + .inFile(connector); + } + + @Test + public void includesNonFutureWrapperInFutureWrappers_hasError() { + final JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_USER_CONNECTOR_QUALIFIED_NAME + ";", + "import " + USER_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomUserConnector(futureWrappers=String.class)", + "public interface NotesConnector extends UserConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation).hadErrorContaining(FUTURE_WRAPPER_ANNOTATION_ERROR).inFile(connector); + } + + @Test + public void imports_containsNonUserConnector_hasError() { + final JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_USER_CONNECTOR_QUALIFIED_NAME + ";", + "import " + USER_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomUserConnector(imports=String.class)", + "public interface NotesConnector extends UserConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation).hadErrorContaining(IMPORTS_NOT_CONNECTOR_ERROR).inFile(connector); + } + + @Test + public void imports_containsUserConnector_compiles() { + final JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_USER_CONNECTOR_QUALIFIED_NAME + ";", + "import " + USER_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomUserConnector(imports=NotesConnector2.class)", + "public interface NotesConnector extends UserConnector {", + "}"); + final JavaFileObject connector2 = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector2", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_USER_CONNECTOR_QUALIFIED_NAME + ";", + "import " + USER_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomUserConnector", + "public interface NotesConnector2 extends UserConnector {", + "}"); + + Compilation compilation = + javac().withProcessors(new Processor()).compile(connector, connector2); + + assertThat(compilation).succeededWithoutWarnings(); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DefaultProfileClassTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DefaultProfileClassTest.java new file mode 100644 index 0000000..4bc9e35 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DefaultProfileClassTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class DefaultProfileClassTest { + + private final AnnotationPrinter annotationPrinter; + + public DefaultProfileClassTest(AnnotationPrinter annotationPrinter) { + this.annotationPrinter = annotationPrinter; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void compile_generatesDefaultProfileClass() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation).generatedSourceFile(NOTES_PACKAGE + ".DefaultProfileNotesType"); + } + + @Test + public void compile_defaultProfileClassImplementsProfileInterface() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".DefaultProfileNotesType") + .contentsAsUtf8String() + .contains("class DefaultProfileNotesType implements ProfileNotesType"); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DispatcherTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DispatcherTest.java new file mode 100644 index 0000000..1ce6023 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DispatcherTest.java @@ -0,0 +1,110 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesConfigurationWithNotesProvider; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class DispatcherTest { + + private final AnnotationPrinter annotationPrinter; + + public DispatcherTest(AnnotationPrinter annotationPrinter) { + this.annotationPrinter = annotationPrinter; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void generatesDispatcherClass() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesConfigurationWithNotesProvider(annotationPrinter), + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.connectedapps.CrossProfileConnector_Service_Dispatcher"); + } + + @Test + public void specifiedClassName_generatesSpecifiedClassNameDispatcher() { + JavaFileObject notesConfiguration = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileConfigurationQualifiedName() + ";", + annotationPrinter.crossProfileConfigurationAsAnnotation( + "providers=NotesProvider.class"), + "public abstract class NotesConfiguration {", + "}"); + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + annotationPrinter.crossProfileAsAnnotation("connector=NotesConnector.class"), + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + JavaFileObject notesConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(serviceClassName=\"com.google.android.CustomConnector\")", + "public interface NotesConnector extends ProfileConnector {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesConfiguration, + annotatedNotesProvider(annotationPrinter), + notesConnector, + crossProfileType); + + assertThat(compilation).generatedSourceFile("com.google.android.CustomConnector_Dispatcher"); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/GeneratedProfileConnectorTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/GeneratedProfileConnectorTest.java new file mode 100644 index 0000000..63b8630 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/GeneratedProfileConnectorTest.java @@ -0,0 +1,152 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.GENERATED_PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class GeneratedProfileConnectorTest { + + private static final String NOT_INTERFACE_ERROR = + "@GeneratedProfileConnector must only be applied to interfaces"; + private static final String NOT_IMPLEMENTING_CONNECTOR_ERROR = + "Interfaces annotated with @GeneratedProfileConnector must extend ProfileConnector"; + private static final String ADDITIONAL_METHODS_ERROR = + "Interfaces annotated with @GeneratedProfileConnector can not declare non-static methods"; + + @Test + public void isNotInterface_hasError() { + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + GENERATED_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@GeneratedProfileConnector", + "public class NotesConnector implements ProfileConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation).hadErrorContaining(NOT_INTERFACE_ERROR).inFile(connector); + } + + @Test + public void doesNotExtendProfileConnector_hasError() { + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + GENERATED_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@GeneratedProfileConnector", + "public interface NotesConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation) + .hadErrorContaining(NOT_IMPLEMENTING_CONNECTOR_ERROR) + .inFile(connector); + } + + @Test + public void addsAdditionalMethod_hasError() { + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + GENERATED_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@GeneratedProfileConnector", + "public interface NotesConnector extends ProfileConnector {", + " String getString();", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation) + .hadErrorContaining(ADDITIONAL_METHODS_ERROR) + .inFile(connector); + } + + @Test + public void generatedProfileConnector_compiles() { + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + GENERATED_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@GeneratedProfileConnector", + "public interface NotesConnector extends ProfileConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation).generatedSourceFile(NOTES_PACKAGE + ".GeneratedNotesConnector"); + } + + @Test + public void generatedProfileConnector_extendsAbstractProfileConnector() { + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + GENERATED_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@GeneratedProfileConnector", + "public interface NotesConnector extends ProfileConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".GeneratedNotesConnector") + .contentsAsUtf8String() + .contains("final class GeneratedNotesConnector extends AbstractProfileConnector"); + } + + @Test + public void generatedProfileConnector_implementsConnector() { + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + GENERATED_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@GeneratedProfileConnector", + "public interface NotesConnector extends ProfileConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".GeneratedNotesConnector") + .contentsAsUtf8String() + .contains("implements NotesConnector"); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/GeneratedUserConnectorTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/GeneratedUserConnectorTest.java new file mode 100644 index 0000000..a7fe1f2 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/GeneratedUserConnectorTest.java @@ -0,0 +1,152 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.GENERATED_USER_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.USER_CONNECTOR_QUALIFIED_NAME; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class GeneratedUserConnectorTest { + + private static final String NOT_INTERFACE_ERROR = + "@GeneratedUserConnector must only be applied to interfaces"; + private static final String NOT_IMPLEMENTING_CONNECTOR_ERROR = + "Interfaces annotated with @GeneratedUserConnector must extend UserConnector"; + private static final String ADDITIONAL_METHODS_ERROR = + "Interfaces annotated with @GeneratedUserConnector can not declare non-static methods"; + + @Test + public void isNotInterface_hasError() { + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + GENERATED_USER_CONNECTOR_QUALIFIED_NAME + ";", + "import " + USER_CONNECTOR_QUALIFIED_NAME + ";", + "@GeneratedUserConnector", + "public class NotesConnector implements UserConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation).hadErrorContaining(NOT_INTERFACE_ERROR).inFile(connector); + } + + @Test + public void doesNotExtendProfileConnector_hasError() { + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + GENERATED_USER_CONNECTOR_QUALIFIED_NAME + ";", + "@GeneratedUserConnector", + "public interface NotesConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation) + .hadErrorContaining(NOT_IMPLEMENTING_CONNECTOR_ERROR) + .inFile(connector); + } + + @Test + public void addsAdditionalMethod_hasError() { + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + GENERATED_USER_CONNECTOR_QUALIFIED_NAME + ";", + "import " + USER_CONNECTOR_QUALIFIED_NAME + ";", + "@GeneratedUserConnector", + "public interface NotesConnector extends UserConnector {", + " String getString();", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation) + .hadErrorContaining(ADDITIONAL_METHODS_ERROR) + .inFile(connector); + } + + @Test + public void generatedUserConnector_compiles() { + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + GENERATED_USER_CONNECTOR_QUALIFIED_NAME + ";", + "import " + USER_CONNECTOR_QUALIFIED_NAME + ";", + "@GeneratedUserConnector", + "public interface NotesConnector extends UserConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation).generatedSourceFile(NOTES_PACKAGE + ".GeneratedNotesConnector"); + } + + @Test + public void generatedUserConnector_extendsAbstractUserConnector() { + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + GENERATED_USER_CONNECTOR_QUALIFIED_NAME + ";", + "import " + USER_CONNECTOR_QUALIFIED_NAME + ";", + "@GeneratedUserConnector", + "public interface NotesConnector extends UserConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".GeneratedNotesConnector") + .contentsAsUtf8String() + .contains("final class GeneratedNotesConnector extends AbstractUserConnector"); + } + + @Test + public void generatedUserConnector_implementsConnector() { + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + GENERATED_USER_CONNECTOR_QUALIFIED_NAME + ";", + "import " + USER_CONNECTOR_QUALIFIED_NAME + ";", + "@GeneratedUserConnector", + "public interface NotesConnector extends UserConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".GeneratedNotesConnector") + .contentsAsUtf8String() + .contains("implements NotesConnector"); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/IfAvailableTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/IfAvailableTest.java new file mode 100644 index 0000000..59de5e9 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/IfAvailableTest.java @@ -0,0 +1,192 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.installationListener; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.installationListenerWithStringParam; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class IfAvailableTest { + + private final AnnotationPrinter annotationPrinter; + + public IfAvailableTest(AnnotationPrinter annotationPrinter) { + this.annotationPrinter = annotationPrinter; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void compile_generatesIfAvailableClass() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation).generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_IfAvailable"); + } + + @Test + public void compile_synchronousVoidMethod_ifAvailableClassHasMethod() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_IfAvailable") + .contentsAsUtf8String() + .contains("void refreshNotes()"); + } + + @Test + public void compile_synchronousNotVoidMethod_ifAvailableClassHasMethod() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public int refreshNotes() {", + " return 0;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_IfAvailable") + .contentsAsUtf8String() + .contains("int refreshNotes(int defaultValue)"); + } + + @Test + public void compile_callbackVoidMethod_ifAvailableClassHasMethod() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes(InstallationListener listener) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationPrinter), + installationListener(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_IfAvailable") + .contentsAsUtf8String() + .contains("void refreshNotes(InstallationListener listener)"); + } + + @Test + public void compile_callbackNotVoidMethod_ifAvailableClassHasMethod() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes(String s, InstallationListener listener) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationPrinter), + installationListenerWithStringParam(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_IfAvailable") + .contentsAsUtf8String() + .contains( + "void refreshNotes(String s, InstallationListener listener, String defaultValue)"); + } + + @Test + public void compile_futureMethod_ifAvailableClassHasMethod() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "import com.google.common.util.concurrent.ListenableFuture;", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public ListenableFuture<String> refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_IfAvailable") + .contentsAsUtf8String() + .contains("ListenableFuture<String> refreshNotes(String defaultValue)"); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InterfaceTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InterfaceTest.java new file mode 100644 index 0000000..e59a306 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InterfaceTest.java @@ -0,0 +1,560 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class InterfaceTest { + + private final AnnotationPrinter annotationPrinter; + + public InterfaceTest(AnnotationPrinter annotationPrinter) { + this.annotationPrinter = annotationPrinter; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void compile_generatesSingleSenderInterface() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesCrossProfileType(annotationPrinter), + annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation).generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSender"); + } + + @Test + public void compile_singleAnnotatedMethod_singleSenderInterfaceHasMethod() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSender") + .contentsAsUtf8String() + .contains("void refreshNotes()"); + } + + @Test + public void compile_multipleAnnotatedMethods_singleSenderInterfaceHasAllMethods() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + annotationPrinter.crossProfileAsAnnotation(), + " public int anotherMethod(String s) {", + " return 0;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSender") + .contentsAsUtf8String() + .contains("void refreshNotes()"); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSender") + .contentsAsUtf8String() + .contains("int anotherMethod(String s)"); + } + + @Test + public void compile_multipleMethods_singleSenderInterfaceDoesNotHaveUnannotatedMethods() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + " public int anotherMethod(String s) {", + " return 0;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSender") + .contentsAsUtf8String() + .doesNotContain("anotherMethod"); + } + + @Test + public void compile_generatesSingleSenderCanThrowInterface() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesCrossProfileType(annotationPrinter), + annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSenderCanThrow"); + } + + @Test + public void compile_singleAnnotatedMethod_singleSenderCanThrowInterfaceHasMethod() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSenderCanThrow") + .contentsAsUtf8String() + .contains("void refreshNotes() throws UnavailableProfileException"); + } + + @Test + public void compile_multipleAnnotatedMethods_singleSenderCanThrowInterfaceHasAllMethods() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + annotationPrinter.crossProfileAsAnnotation(), + " public int anotherMethod(String s) {", + " return 0;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSenderCanThrow") + .contentsAsUtf8String() + .contains("void refreshNotes() throws UnavailableProfileException"); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSenderCanThrow") + .contentsAsUtf8String() + .contains("int anotherMethod(String s) throws UnavailableProfileException"); + } + + @Test + public void compile_multipleMethods_singleSenderCanThrowInterfaceDoesNotHaveUnannotatedMethods() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + " public int anotherMethod(String s) {", + " return 0;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSenderCanThrow") + .contentsAsUtf8String() + .doesNotContain("anotherMethod"); + } + + @Test + public void compile_singleSenderCanThrowInterfaceHasIfAvailableMethod() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSenderCanThrow") + .contentsAsUtf8String() + .contains("ProfileNotesType_IfAvailable ifAvailable()"); + } + + @Test + public void compile_generatesMultipleSenderInterface() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesCrossProfileType(annotationPrinter), + annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation).generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_MultipleSender"); + } + + @Test + public void compile_singleVoidAnnotatedMethod_multipleSenderInterfaceHasMethod() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_MultipleSender") + .contentsAsUtf8String() + .contains("void refreshNotes()"); + } + + @Test + public void compile_singlePrimitiveAnnotatedMethod_multipleSenderInterfaceHasMethod() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public int refreshNotes() {", + " return 0;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_MultipleSender") + .contentsAsUtf8String() + .contains("Map<Profile, Integer> refreshNotes()"); + } + + @Test + public void compile_singleObjectAnnotatedMethod_multipleSenderInterfaceHasMethod() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public String refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_MultipleSender") + .contentsAsUtf8String() + .contains("Map<Profile, String> refreshNotes()"); + } + + @Test + public void compile_multipleAnnotatedMethods_multipleSenderInterfaceHasAllMethods() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + annotationPrinter.crossProfileAsAnnotation(), + " public int anotherMethod(String s) {", + " return 0;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_MultipleSender") + .contentsAsUtf8String() + .contains("void refreshNotes()"); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_MultipleSender") + .contentsAsUtf8String() + .contains("Map<Profile, Integer> anotherMethod(String s)"); + } + + @Test + public void compile_multipleMethods_multipleSenderInterfaceDoesNotHaveUnannotatedMethods() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + " public int anotherMethod(String s) {", + " return 0;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_MultipleSender") + .contentsAsUtf8String() + .doesNotContain("anotherMethod"); + } + + @Test + public void compile_synchronousMethodWithDeclaredException_singleSenderInterfaceHasMethod() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "import java.io.IOException;", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() throws IOException {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSender") + .contentsAsUtf8String() + .contains("refreshNotes() throws IOException"); + } + + @Test + public void + compile_synchronousMethodWithMultipleDeclaredExceptions_singleSenderInterfaceHasMethod() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "import java.io.IOException;", + "import java.sql.SQLException;", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() throws IOException, SQLException {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + // Order is not predictable + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSender") + .contentsAsUtf8String() + .contains("refreshNotes() throws "); + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSender") + .contentsAsUtf8String() + .contains("SQLException"); + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSender") + .contentsAsUtf8String() + .contains("IOException"); + } + + @Test + public void + compile_synchronousMethodWithDeclaredException_singleSenderCanThrowInterfaceHasMethod() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "import java.io.IOException;", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() throws IOException {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSender") + .contentsAsUtf8String() + .contains("refreshNotes() throws IOException"); + } + + @Test + public void + compile_synchronousMethodWithMultipleDeclaredExceptions_singleSenderCanThrowInterfaceHasMethod() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "import java.io.IOException;", + "import java.sql.SQLException;", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() throws IOException, SQLException {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + // Order is not predictable + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSender") + .contentsAsUtf8String() + .contains("refreshNotes() throws "); + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSender") + .contentsAsUtf8String() + .contains("SQLException"); + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSender") + .contentsAsUtf8String() + .contains("IOException"); + } + + @Test + public void + compile_synchronousMethodWithDeclaredException_multipleSenderInterfaceDoesNotHaveMethod() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "import java.io.IOException;", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() throws IOException {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_MultipleSender") + .contentsAsUtf8String() + .doesNotContain("refreshNotes()"); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalCrossProfileTypeTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalCrossProfileTypeTest.java new file mode 100644 index 0000000..d0af193 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalCrossProfileTypeTest.java @@ -0,0 +1,103 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class InternalCrossProfileTypeTest { + + private final AnnotationPrinter annotationPrinter; + + public InternalCrossProfileTypeTest(AnnotationPrinter annotationPrinter) { + this.annotationPrinter = annotationPrinter; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void generatesInternalClass() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation).generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_Internal"); + } + + @Test + public void internalClassHasPrivateConstructor() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_Internal") + .contentsAsUtf8String() + .contains("private ProfileNotesType_Internal() {"); + } + + @Test + public void internalClassHasPublicCallMethod() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_Internal") + .contentsAsUtf8String() + .contains("public Parcel call(Context context, int methodIdentifier, Parcel params,"); + } + + @Test + public void internalClassHasInstanceMethod() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_Internal") + .contentsAsUtf8String() + .contains("static ProfileNotesType_Internal instance()"); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalProviderClassTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalProviderClassTest.java new file mode 100644 index 0000000..2afee1b --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalProviderClassTest.java @@ -0,0 +1,145 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class InternalProviderClassTest { + + private final AnnotationPrinter annotationPrinter; + + public InternalProviderClassTest(AnnotationPrinter annotationPrinter) { + this.annotationPrinter = annotationPrinter; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void generatesInternalProviderClass() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation).generatedSourceFile(NOTES_PACKAGE + ".Profile_NotesProvider_Internal"); + } + + @Test + public void internalProviderClassHasPrivateConstructor() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".Profile_NotesProvider_Internal") + .contentsAsUtf8String() + .contains("private Profile_NotesProvider_Internal() {"); + } + + @Test + public void internalProviderClassHasPublicCallMethod() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".Profile_NotesProvider_Internal") + .contentsAsUtf8String() + .contains( + "public Parcel call(Context context, long crossProfileTypeIdentifier, int" + + " methodIdentifier,"); + } + + @Test + public void internalProviderClassHasInstanceMethod() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".Profile_NotesProvider_Internal") + .contentsAsUtf8String() + .contains("public static Profile_NotesProvider_Internal instance()"); + } + + @Test + public void internalProviderClassHasProviderClassGetter() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".Profile_NotesProvider_Internal") + .contentsAsUtf8String() + .contains("public NotesProvider providerClass(Context context)"); + } + + @Test + public void providerClassHasField_compiles() { + final JavaFileObject providerClass = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + " int test = 3;", + annotationPrinter.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(providerClass, annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation).succeededWithoutWarnings(); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/OtherProfileTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/OtherProfileTest.java new file mode 100644 index 0000000..8d38369 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/OtherProfileTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class OtherProfileTest { + + private final AnnotationPrinter annotationPrinter; + + public OtherProfileTest(AnnotationPrinter annotationPrinter) { + this.annotationPrinter = annotationPrinter; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void compile_generatesOtherProfileClass() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation).generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_OtherProfile"); + } + + @Test + public void compile_otherProfileClassImplementsSingleSender() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_OtherProfile") + .contentsAsUtf8String() + .contains( + "class ProfileNotesType_OtherProfile implements" + " ProfileNotesType_SingleSender"); + } + + @Test + public void compile_otherProfileClassHasConstructorTakingConnector() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_OtherProfile") + .contentsAsUtf8String() + .contains( + "public ProfileNotesType_OtherProfile(ProfileConnector connector)"); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProcessorCrossProfileConfigurationTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProcessorCrossProfileConfigurationTest.java new file mode 100644 index 0000000..49b797e --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProcessorCrossProfileConfigurationTest.java @@ -0,0 +1,463 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.ANNOTATED_NOTES_CONNECTOR; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedDifferentCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedDifferentProvider; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.formatErrorMessage; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.notesTypeWithDefaultConnector; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class ProcessorCrossProfileConfigurationTest { + + private static final String NOT_A_PROVIDER_CLASS_ERROR = + "All classes specified in 'providers' must be provider classes"; + private static final String CONFIGURATION_DIFFERENT_CONNECTOR_ERROR = + "All @CROSS_PROFILE_ANNOTATION types specified in the same configuration must use the same" + + " ProfileConnector"; + private static final String INCORRECT_SERVICE_CLASS = + "The class specified by serviceClass must match the serviceClassName given by the" + + " ProfileConnector"; + private static final String CONNECTOR_MUST_BE_INTERFACE = "Connectors must be interfaces"; + private static final String CONNECTOR_MUST_EXTEND_CONNECTOR = + "Interfaces specified as a connector must extend ProfileConnector"; + + private final AnnotationStrings annotationStrings; + + public ProcessorCrossProfileConfigurationTest(AnnotationStrings annotationStrings) { + this.annotationStrings = annotationStrings; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void multipleConfigurations_compiles() { + final JavaFileObject configuration1 = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + "import com.google.android.enterprise.connectedapps.CrossProfileConnector;", + annotationStrings.crossProfileConfigurationAsAnnotation( + "connector=CrossProfileConnector.class"), + "public abstract class NotesConfiguration {", + "}"); + final JavaFileObject configuration2 = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration2", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + annotationStrings.crossProfileConfigurationAsAnnotation( + "connector=NotesConnector.class"), + "public abstract class NotesConfiguration2 {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(configuration1, configuration2, ANNOTATED_NOTES_CONNECTOR); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void providersContainsNoProviders_compiles() { + final JavaFileObject configuration = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + "import com.google.android.enterprise.connectedapps.CrossProfileConnector;", + annotationStrings.crossProfileConfigurationAsAnnotation( + "connector=CrossProfileConnector.class"), + "public abstract class NotesConfiguration {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(configuration); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void providersContainsNoProvidersAndNoConnector_generatesCrossProfileConnectorService() { + final JavaFileObject configuration = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + annotationStrings.crossProfileConfigurationAsAnnotation(), + "public abstract class NotesConfiguration {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(configuration); + + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.connectedapps.CrossProfileConnector_Service"); + } + + @Test + public void + providersContainsProviderWithoutConnectorAndNoConnector_generatesCrossProfileConnectorService() { + JavaFileObject configuration = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + annotationStrings.crossProfileConfigurationAsAnnotation( + "providers=NotesProvider.class"), + "public abstract class NotesConfiguration {", + "}"); + JavaFileObject noConnectorProvider = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + "}"); + JavaFileObject notesTypeWithoutConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(configuration, noConnectorProvider, notesTypeWithoutConnector); + + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.connectedapps.CrossProfileConnector_Service"); + } + + @Test + public void providersContainsSingleValidProvider_compiles() { + JavaFileObject configuration = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + annotationStrings.crossProfileConfigurationAsAnnotation( + "providers=NotesProvider.class"), + "public abstract class NotesConfiguration {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + configuration, + annotatedNotesProvider(annotationStrings), + annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void providersContainsMultipleValidProviders_compiles() { + JavaFileObject configuration = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + annotationStrings.crossProfileConfigurationAsAnnotation( + "providers={NotesProvider.class, DifferentProvider.class}"), + "public abstract class NotesConfiguration {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + configuration, + annotatedNotesProvider(annotationStrings), + annotatedNotesCrossProfileType(annotationStrings), + annotatedDifferentCrossProfileType(annotationStrings), + annotatedDifferentProvider(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void providersContainsNonProvider_hasError() { + JavaFileObject configuration = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + annotationStrings.crossProfileConfigurationAsAnnotation("providers={String.class}"), + "public abstract class NotesConfiguration {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(configuration); + + assertThat(compilation).hadErrorContaining(NOT_A_PROVIDER_CLASS_ERROR).inFile(configuration); + } + + @Test + public void isNotAbstract_compiles() { + JavaFileObject notAbstractConfiguration = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + "import com.google.android.enterprise.connectedapps.CrossProfileConnector;", + annotationStrings.crossProfileConfigurationAsAnnotation( + "connector=CrossProfileConnector.class"), + "public class NotesConfiguration {", + "}"); + + Compilation compilation = + javac().withProcessors(new Processor()).compile(notAbstractConfiguration); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void hasCrossProfileTypeWithDifferentConnectors_hasError() { + JavaFileObject configurationClassWithDifferentConnectors = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + annotationStrings.crossProfileConfigurationAsAnnotation( + "providers={NotesProvider.class, NotesProvider2.class}"), + "abstract class NotesConfiguration {", + "}"); + JavaFileObject providerClassWithDefaultConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + "}"); + JavaFileObject providerClassWithNotesConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider2", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider2 {", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType2 provideNotesType2() {", + " return new NotesType2();", + " }", + "}"); + JavaFileObject notesTypeWithCrossProfileConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import com.google.android.enterprise.connectedapps.CrossProfileConnector;", + annotationStrings.crossProfileAsAnnotation("connector=CrossProfileConnector.class"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + JavaFileObject notesType2WithNotesConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType2", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation("connector=NotesConnector.class"), + "public final class NotesType2 {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + configurationClassWithDifferentConnectors, + providerClassWithDefaultConnector, + providerClassWithNotesConnector, + notesTypeWithCrossProfileConnector, + notesType2WithNotesConnector, + ANNOTATED_NOTES_CONNECTOR); + + assertThat(compilation) + .hadErrorContaining( + formatErrorMessage(CONFIGURATION_DIFFERENT_CONNECTOR_ERROR, annotationStrings)) + .inFile(configurationClassWithDifferentConnectors); + } + + @Test + public void correctServiceClass_compiles() { + JavaFileObject configuration = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + "import com.google.android.enterprise.connectedapps.CrossProfileConnector;", + annotationStrings.crossProfileConfigurationAsAnnotation( + "serviceClass=com.google.android.enterprise.connectedapps.CrossProfileConnector_Service.class," + + " providers=NotesProvider.class, connector=CrossProfileConnector.class"), + "public abstract class NotesConfiguration {", + "}"); + JavaFileObject serviceClass = + JavaFileObjects.forSourceLines( + "com.google.android.enterprise.connectedapps.CrossProfileConnector_Service", + "package com.google.android.enterprise.connectedapps;", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + "import android.app.Service;", + "import android.content.Intent;", + "import android.os.Binder;", + "public final class CrossProfileConnector_Service extends Service {", + " @Override", + " public Binder onBind(Intent intent) {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + configuration, + serviceClass, + annotatedNotesProvider(annotationStrings), + notesTypeWithDefaultConnector(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void incorrectlyNamedServiceClass_hasError() { + JavaFileObject configuration = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + "import com.google.android.enterprise.connectedapps.CrossProfileConnector;", + annotationStrings.crossProfileConfigurationAsAnnotation( + "serviceClass=com.google.android.enterprise.connectedapps.CrossProfileConnector_Service2.class," + + " providers=NotesProvider.class, connector=CrossProfileConnector.class"), + "public abstract class NotesConfiguration {", + "}"); + JavaFileObject serviceClass = + JavaFileObjects.forSourceLines( + "com.google.android.enterprise.connectedapps.CrossProfileConnector_Service2", + "package com.google.android.enterprise.connectedapps;", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + "import android.app.Service;", + "import android.content.Intent;", + "import android.os.Binder;", + "public final class CrossProfileConnector_Service2 extends Service {", + " @Override", + " public Binder onBind(Intent intent) {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + configuration, + serviceClass, + annotatedNotesProvider(annotationStrings), + notesTypeWithDefaultConnector(annotationStrings)); + + assertThat(compilation).hadErrorContaining(INCORRECT_SERVICE_CLASS).inFile(configuration); + } + + @Test + public void specifiesNotInterfaceConnector_hasError() { + final JavaFileObject configuration = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + annotationStrings.crossProfileConfigurationAsAnnotation( + "connector=NotesConnector.class"), + "public abstract class NotesConfiguration {", + "}"); + + JavaFileObject notesConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "public class NotesConnector {", + "}"); + + Compilation compilation = + javac().withProcessors(new Processor()).compile(configuration, notesConnector); + + assertThat(compilation).hadErrorContaining(CONNECTOR_MUST_BE_INTERFACE).inFile(configuration); + } + + @Test + public void specifiesConnectorNotExtendingProfileConnector_hasError() { + final JavaFileObject configuration = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + annotationStrings.crossProfileConfigurationAsAnnotation( + "connector=NotesConnector.class"), + "public abstract class NotesConfiguration {", + "}"); + + JavaFileObject notesConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "public interface NotesConnector {", + "}"); + + Compilation compilation = + javac().withProcessors(new Processor()).compile(configuration, notesConnector); + + assertThat(compilation) + .hadErrorContaining(CONNECTOR_MUST_EXTEND_CONNECTOR) + .inFile(configuration); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProcessorCurrentProfileTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProcessorCurrentProfileTest.java new file mode 100644 index 0000000..b009f2f --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProcessorCurrentProfileTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class ProcessorCurrentProfileTest { + + private final AnnotationPrinter annotationPrinter; + + public ProcessorCurrentProfileTest(AnnotationPrinter annotationPrinter) { + this.annotationPrinter = annotationPrinter; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void compile_generatesCurrentProfileClass() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation).generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_CurrentProfile"); + } + + @Test + public void compile_currentProfileClassImplementsSingleSender() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_CurrentProfile") + .contentsAsUtf8String() + .contains( + "class ProfileNotesType_CurrentProfile implements" + " ProfileNotesType_SingleSender"); + } + + @Test + public void compile_currentProfileClassHasConstructorTakingCrossProfileType() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_CurrentProfile") + .contentsAsUtf8String() + .contains( + "public ProfileNotesType_CurrentProfile(Context context, NotesType crossProfileType)"); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProcessorMultipleProfilesTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProcessorMultipleProfilesTest.java new file mode 100644 index 0000000..2f732d1 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProcessorMultipleProfilesTest.java @@ -0,0 +1,96 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class ProcessorMultipleProfilesTest { + + private final AnnotationPrinter annotationPrinter; + + public ProcessorMultipleProfilesTest(AnnotationPrinter annotationPrinter) { + this.annotationPrinter = annotationPrinter; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void compile_generatesMultipleProfilesClass() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_MultipleProfiles"); + } + + @Test + public void compile_multipleProfilesClassImplementsMultipleSender() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_MultipleProfiles") + .contentsAsUtf8String() + .contains( + "class ProfileNotesType_MultipleProfiles implements" + + " ProfileNotesType_MultipleSender"); + } + + @Test + public void compile_multipleProfilesClassHasConstructorTakingSenders() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_MultipleProfiles") + .contentsAsUtf8String() + .contains("public ProfileNotesType_MultipleProfiles("); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_MultipleProfiles") + .contentsAsUtf8String() + .contains("Map<Profile, ProfileNotesType_SingleSenderCanThrow>" + " senders) {"); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProfileInterfaceTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProfileInterfaceTest.java new file mode 100644 index 0000000..7922750 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProfileInterfaceTest.java @@ -0,0 +1,225 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class ProfileInterfaceTest { + + private final AnnotationPrinter annotationPrinter; + + public ProfileInterfaceTest(AnnotationPrinter annotationPrinter) { + this.annotationPrinter = annotationPrinter; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void compile_generatesProfileInterface() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation).generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType"); + } + + @Test + public void compile_profileInterfaceHasCreateMethod() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType") + .contentsAsUtf8String() + .contains("static ProfileNotesType create(ProfileConnector connector)"); + } + + @Test + public void compile_profileInterfaceContainsExpectedMethods() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType") + .contentsAsUtf8String() + .contains("ProfileNotesType_SingleSender current()"); + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType") + .contentsAsUtf8String() + .contains("ProfileNotesType_SingleSenderCanThrow other()"); + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType") + .contentsAsUtf8String() + // We ignore the "profile" argument as it gets moved onto another line by the processor + .contains("ProfileNotesType_SingleSenderCanThrow profile("); + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType") + .contentsAsUtf8String() + .contains("ProfileNotesType_MultipleSender both()"); + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType") + .contentsAsUtf8String() + .contains("ProfileNotesType_MultipleSender profiles("); + } + + @Test + public void + compile_withoutPrimaryProfile_profileInterfaceDoesNotContainPrimarySecondaryMethods() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + annotationPrinter.crossProfileAsAnnotation("connector=NotesConnector.class"), + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + JavaFileObject notesConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "public interface NotesConnector extends ProfileConnector {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter), notesConnector); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType") + .contentsAsUtf8String() + .doesNotContain("primary()"); + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType") + .contentsAsUtf8String() + .doesNotContain("secondary()"); + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType") + .contentsAsUtf8String() + .doesNotContain("suppliers()"); + } + + @Test + public void compile_withPrimaryProfile_profileInterfaceDoesContainPrimarySecondaryMethods() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + annotationPrinter.crossProfileAsAnnotation("connector=NotesConnector.class"), + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + JavaFileObject notesConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(primaryProfile=CustomProfileConnector.ProfileType.WORK)", + "public interface NotesConnector extends ProfileConnector {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter), notesConnector); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType") + .contentsAsUtf8String() + .contains("ProfileNotesType_SingleSenderCanThrow primary()"); + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType") + .contentsAsUtf8String() + .contains("ProfileNotesType_SingleSenderCanThrow secondary()"); + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType") + .contentsAsUtf8String() + .contains("ProfileNotesType_MultipleSender suppliers()"); + } + + @Test + public void compile_withoutConnector_profileInterfaceDoesContainPrimarySecondaryMethods() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + annotationPrinter.crossProfileAsAnnotation(), + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType") + .contentsAsUtf8String() + .contains("ProfileNotesType_SingleSenderCanThrow primary()"); + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType") + .contentsAsUtf8String() + .contains("ProfileNotesType_SingleSenderCanThrow secondary()"); + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType") + .contentsAsUtf8String() + .contains("ProfileNotesType_MultipleSender suppliers()"); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ServiceTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ServiceTest.java new file mode 100644 index 0000000..b1ea7f0 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ServiceTest.java @@ -0,0 +1,165 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesConfigurationWithNotesProvider; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class ServiceTest { + + private final AnnotationPrinter annotationPrinter; + + public ServiceTest(AnnotationPrinter annotationPrinter) { + this.annotationPrinter = annotationPrinter; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void generatesServiceClass() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesConfigurationWithNotesProvider(annotationPrinter), + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.connectedapps.CrossProfileConnector_Service"); + } + + @Test + public void serviceClassExtendsService() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesConfigurationWithNotesProvider(annotationPrinter), + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.connectedapps.CrossProfileConnector_Service") + .contentsAsUtf8String() + .contains("CrossProfileConnector_Service extends Service"); + } + + @Test + public void serviceClass_specifiedAlternativeClass_extendsAlternativeServiceClass() { + JavaFileObject serviceBaseClass = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".ServiceBaseClass", + "package " + NOTES_PACKAGE + ";", + "import android.app.Service;", + "public abstract class ServiceBaseClass extends Service {", + "}"); + JavaFileObject notesConfiguration = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileConfigurationQualifiedName() + ";", + annotationPrinter.crossProfileConfigurationAsAnnotation( + "serviceSuperclass=ServiceBaseClass.class, providers=NotesProvider.class"), + "public abstract class NotesConfiguration {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesConfiguration, + serviceBaseClass, + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.connectedapps.CrossProfileConnector_Service") + .contentsAsUtf8String() + .contains("CrossProfileConnector_Service extends ServiceBaseClass"); + } + + @Test + public void serviceClass_specifiedClassName_generatesSpecifiedClassName() { + JavaFileObject notesConfiguration = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileConfigurationQualifiedName() + ";", + annotationPrinter.crossProfileConfigurationAsAnnotation( + "providers=NotesProvider.class"), + "public abstract class NotesConfiguration {", + "}"); + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + annotationPrinter.crossProfileAsAnnotation("connector=NotesConnector.class"), + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + JavaFileObject notesConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(serviceClassName=\"com.google.android.CustomConnector\")", + "public interface NotesConnector extends ProfileConnector {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesConfiguration, + annotatedNotesProvider(annotationPrinter), + notesConnector, + crossProfileType); + + assertThat(compilation) + .generatedSourceFile("com.google.android.CustomConnector") + .contentsAsUtf8String() + .contains("CustomConnector extends Service"); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/TestUtilities.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/TestUtilities.java new file mode 100644 index 0000000..b1c05bf --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/TestUtilities.java @@ -0,0 +1,599 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationNames; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.testing.compile.JavaFileObjects; +import java.util.ArrayList; +import java.util.List; +import javax.tools.JavaFileObject; + +final class TestUtilities { + + public static final String CROSS_PROFILE_QUALIFIED_NAME = + "com.google.android.enterprise.connectedapps.annotations.CrossProfile"; + public static final String CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME = + "com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector"; + public static final String CUSTOM_USER_CONNECTOR_QUALIFIED_NAME = + "com.google.android.enterprise.connectedapps.annotations.CustomUserConnector"; + public static final String GENERATED_PROFILE_CONNECTOR_QUALIFIED_NAME = + "com.google.android.enterprise.connectedapps.annotations.GeneratedProfileConnector"; + public static final String GENERATED_USER_CONNECTOR_QUALIFIED_NAME = + "com.google.android.enterprise.connectedapps.annotations.GeneratedUserConnector"; + public static final String PROFILE_CONNECTOR_QUALIFIED_NAME = + "com.google.android.enterprise.connectedapps.ProfileConnector"; + public static final String USER_CONNECTOR_QUALIFIED_NAME = + "com.google.android.enterprise.connectedapps.UserConnector"; + public static final String NOTES_PACKAGE = "com.google.android.enterprise.notes"; + + public static final String UNSUPPORTED_TYPE_NAME = "UnsupportedType"; + public static final JavaFileObject UNSUPPORTED_TYPE = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".UnsupportedType", + "package " + NOTES_PACKAGE + ";", + "public final class UnsupportedType {}"); + + public static final JavaFileObject ANNOTATED_NOTES_CONNECTOR = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "public interface NotesConnector extends ProfileConnector {", + "}"); + + public static final String INSTALLATION_LISTENER_NAME = "InstallationListener"; + + public static final JavaFileObject PARCELABLE_OBJECT = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".ParcelableObject", + "package " + NOTES_PACKAGE + ";", + "import android.os.Parcel;", + "import android.os.Parcelable;", + "import java.util.Objects;", + "public final class ParcelableObject implements Parcelable {", + "@SuppressWarnings(\"rawtypes\")", + "public static final Parcelable.Creator CREATOR =", + "new Parcelable.Creator() {", + "@Override", + "public ParcelableObject createFromParcel(Parcel in) {", + "return new ParcelableObject(in);", + "}", + "@Override", + "public ParcelableObject[] newArray(int size) {", + "return new ParcelableObject[size];", + "}", + "};", + "private String value;", + "public String value() {", + "return value;", + "}", + "public ParcelableObject(Parcel in) {", + "this(in.readString());", + "}", + "public ParcelableObject(String value) {", + "this.value = value;", + "}", + "@Override", + "public int describeContents() {", + "return 0;", + "}", + "@Override", + "public void writeToParcel(Parcel dest, int flags) {", + "dest.writeString(value);", + "}", + "@Override", + "public boolean equals(Object o) {", + "if (this == o) {", + "return true;", + "}", + "if (o == null || getClass() != o.getClass()) {", + "return false;", + "}", + "ParcelableObject that = (ParcelableObject) o;", + "return value.equals(that.value);", + "}", + "@Override", + "public int hashCode() {", + "return Objects.hash(value);", + "}", + "}"); + + public static final JavaFileObject GENERIC_PARCELABLE_OBJECT = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".GenericParcelableObject", + "package " + NOTES_PACKAGE + ";", + "import android.os.Parcel;", + "import android.os.Parcelable;", + "import java.util.Objects;", + "public final class GenericParcelableObject<E> implements Parcelable {", + "@SuppressWarnings(\"rawtypes\")", + "public static final Parcelable.Creator CREATOR =", + "new Parcelable.Creator() {", + "@Override", + "public GenericParcelableObject createFromParcel(Parcel in) {", + "return new GenericParcelableObject(in);", + "}", + "@Override", + "public GenericParcelableObject[] newArray(int size) {", + "return new GenericParcelableObject[size];", + "}", + "};", + "private String value;", + "public String value() {", + "return value;", + "}", + "public GenericParcelableObject(Parcel in) {", + "this(in.readString());", + "}", + "public GenericParcelableObject(String value) {", + "this.value = value;", + "}", + "@Override", + "public int describeContents() {", + "return 0;", + "}", + "@Override", + "public void writeToParcel(Parcel dest, int flags) {", + "dest.writeString(value);", + "}", + "@Override", + "public boolean equals(Object o) {", + "if (this == o) {", + "return true;", + "}", + "if (o == null || getClass() != o.getClass()) {", + "return false;", + "}", + "GenericParcelableObject that = (GenericParcelableObject) o;", + "return value.equals(that.value);", + "}", + "@Override", + "public int hashCode() {", + "return Objects.hash(value);", + "}", + "}"); + + public static final JavaFileObject SERIALIZABLE_OBJECT = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".SerializableObject", + "package " + NOTES_PACKAGE + ";", + "import java.io.Serializable;", + "import java.util.Objects;", + "public final class SerializableObject implements Serializable {", + "private final String value;", + "public String value() {", + "return value;", + "}", + "public SerializableObject(String value) {", + "this.value = value;", + "}", + "@Override", + "public boolean equals(Object o) {", + "if (this == o) {", + "return true;", + "}", + "if (o == null || getClass() != o.getClass()) {", + "return false;", + "}", + "SerializableObject that = (SerializableObject) o;", + "return Objects.equals(value, that.value);", + "}", + "@Override", + "public int hashCode() {", + "return Objects.hash(value);", + "}", + "}"); + + public static final JavaFileObject GENERIC_SERIALIZABLE_OBJECT = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".GenericSerializableObject", + "package " + NOTES_PACKAGE + ";", + "import java.io.Serializable;", + "import java.util.Objects;", + "public class GenericSerializableObject<R> implements Serializable {", + "private final String value;", + "public String value() {", + "return value;", + "}", + "public GenericSerializableObject(String value) {", + "this.value = value;", + "}", + "@Override", + "public boolean equals(Object o) {", + "if (this == o) {", + "return true;", + "}", + "if (o == null || getClass() != o.getClass()) {", + "return false;", + "}", + "GenericSerializableObject that = (GenericSerializableObject) o;", + "return Objects.equals(value, that.value);", + "}", + "@Override", + "public int hashCode() {", + "return Objects.hash(value);", + "}", + "}"); + + static final JavaFileObject CUSTOM_WRAPPER = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".CustomWrapper", + "package " + NOTES_PACKAGE + ";", + "import java.util.Objects;", + "public class CustomWrapper<F> {", + "private F value;", + "public CustomWrapper(F value) {", + "this.value = value;", + "}", + "public F value() {", + "return value;", + "}", + "@Override", + "public boolean equals(Object o) {", + "if (this == o) {", + "return true;", + "}", + "if (o == null || getClass() != o.getClass()) {", + "return false;", + "}", + "CustomWrapper that = (CustomWrapper) o;", + "return Objects.equals(value, that.value);", + "}", + "@Override", + "public int hashCode() {", + "return Objects.hash(value);", + "}", + "}"); + + static final JavaFileObject PARCELABLE_CUSTOM_WRAPPER = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".ParcelableCustomWrapper", + "package " + NOTES_PACKAGE + ";", + "import android.os.Parcel;", + "import android.os.Parcelable;", + "import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "@CustomParcelableWrapper(originalType = CustomWrapper.class)", + "public class ParcelableCustomWrapper<E> implements Parcelable {", + "private static final int NULL = -1;", + "private static final int NOT_NULL = 1;", + "private final Bundler bundler;", + "private final BundlerType type;", + "private final CustomWrapper<E> customWrapper;", + "public static <F> ParcelableCustomWrapper<F> of(", + "Bundler bundler, BundlerType type, CustomWrapper<F> customWrapper) {", + "return new ParcelableCustomWrapper<>(bundler, type, customWrapper);", + "}", + "public CustomWrapper<E> get() {", + "return customWrapper;", + "}", + "private ParcelableCustomWrapper(Bundler bundler, BundlerType type, CustomWrapper<E>" + + " customWrapper) {", + "if (bundler == null || type == null) {", + "throw new NullPointerException();", + "}", + "this.bundler = bundler;", + "this.type = type;", + "this.customWrapper = customWrapper;", + "}", + "private ParcelableCustomWrapper(Parcel in) {", + "bundler = in.readParcelable(Bundler.class.getClassLoader());", + "int presentValue = in.readInt();", + "if (presentValue == NULL) {", + "type = null;", + "customWrapper = null;", + "return;", + "}", + "type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader());", + "BundlerType valueType = type.typeArguments().get(0);", + "@SuppressWarnings(\"unchecked\")", + "E value = (E) bundler.readFromParcel(in, valueType);", + "customWrapper = new CustomWrapper<>(value);", + "}", + "@Override", + "public void writeToParcel(Parcel dest, int flags) {", + "dest.writeParcelable(bundler, flags);", + "if (customWrapper == null) {", + "dest.writeInt(NULL);", + "return;", + "}", + "dest.writeInt(NOT_NULL);", + "dest.writeParcelable(type, flags);", + "BundlerType valueType = type.typeArguments().get(0);", + "bundler.writeToParcel(dest, customWrapper.value(), valueType, flags);", + "}", + "@Override", + "public int describeContents() {", + "return 0;", + "}", + "@SuppressWarnings(\"rawtypes\")", + "public static final Creator<ParcelableCustomWrapper> CREATOR =", + "new Creator<ParcelableCustomWrapper>() {", + "@Override", + "public ParcelableCustomWrapper createFromParcel(Parcel in) {", + "return new ParcelableCustomWrapper(in);", + "}", + "@Override", + "public ParcelableCustomWrapper[] newArray(int size) {", + "return new ParcelableCustomWrapper[size];", + "}", + "};", + "}"); + + static final JavaFileObject SIMPLE_FUTURE = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".SimpleFuture", + "package " + NOTES_PACKAGE + ";", + "import java.util.concurrent.CountDownLatch;", + "import java.util.function.Consumer;", + "public class SimpleFuture<E> {", + "private E value;", + "private Throwable thrown;", + "private final CountDownLatch countDownLatch = new CountDownLatch(1);", + "private Consumer<E> callback;", + "private Consumer<Throwable> exceptionCallback;", + "public void set(E value) {", + "this.value = value;", + "countDownLatch.countDown();", + "if (callback != null) {", + "callback.accept(value);", + "}", + "}", + "public void setException(Throwable t) {", + "this.thrown = t;", + "countDownLatch.countDown();", + "if (exceptionCallback != null) {", + "exceptionCallback.accept(thrown);", + "}", + "}", + "public E get() {", + "try {", + "countDownLatch.await();", + "} catch (InterruptedException e) {", + "return null;", + "}", + "if (thrown != null) {", + "throw new RuntimeException(thrown);", + "}", + "return value;", + "}", + "public void setCallback(Consumer<E> callback, Consumer<Throwable> exceptionCallback) {", + "if (value != null) {", + "callback.accept(value);", + "} else if (thrown != null) {", + "exceptionCallback.accept(thrown);", + "} else {", + "this.callback = callback;", + "this.exceptionCallback = exceptionCallback;", + "}", + "}", + "}"); + + static final JavaFileObject SIMPLE_FUTURE_WRAPPER = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".SimpleFutureWrapper", + "package " + NOTES_PACKAGE + ";", + "import com.google.android.enterprise.connectedapps.FutureWrapper;", + "import com.google.android.enterprise.connectedapps.internal.FutureResultWriter;", + "import com.google.android.enterprise.connectedapps.Profile;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import" + + " com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger;", + "import java.util.Map;", + "@com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper(", + "originalType = SimpleFuture.class)", + "public final class SimpleFutureWrapper<E> extends FutureWrapper<E> {", + "private final SimpleFuture<E> future = new SimpleFuture<>();", + "public static <E> SimpleFutureWrapper<E> create(Bundler bundler, BundlerType" + + " bundlerType) {", + "return new SimpleFutureWrapper<>(bundler, bundlerType);", + "}", + "private SimpleFutureWrapper(Bundler bundler, BundlerType bundlerType) {", + "super(bundler, bundlerType);", + "}", + "public SimpleFuture<E> getFuture() {", + "return future;", + "}", + "@Override", + "public void onResult(E result) {", + "future.set(result);", + "}", + "@Override", + "public void onException(Throwable throwable) {", + "future.setException(throwable);", + "}", + "public static <E> void writeFutureResult(", + "SimpleFuture<E> future,", + "FutureResultWriter<E> resultWriter) {", + "future.setCallback(", + "(value) -> {", + "resultWriter.onSuccess(value);", + "},", + "(exception) -> {", + "resultWriter.onFailure(exception);", + "});", + "}", + "public static <E> SimpleFuture<Map<Profile, E>> groupResults(", + "Map<Profile, SimpleFuture<E>> results) {", + "SimpleFuture<Map<Profile, E>> m = new SimpleFuture<>();", + "CrossProfileCallbackMultiMerger<E> merger =", + "new CrossProfileCallbackMultiMerger<>(results.size(), m::set);", + "for (Map.Entry<Profile, SimpleFuture<E>> result : results.entrySet()) {", + "result", + ".getValue()", + ".setCallback(", + "(value) -> {", + "merger.onResult(result.getKey(), value);", + "},", + "(throwable) -> {", + "merger.missingResult(result.getKey());", + "});", + "}", + "return m;", + "}", + "}"); + + public static JavaFileObject annotatedNotesCrossProfileType(AnnotationPrinter annotationPrinter) { + return JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + } + + public static JavaFileObject notesCrossProfileTypeWhichUsesInstallationListener( + AnnotationPrinter annotationPrinter) { + return JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void install(InstallationListener l) {", + " }", + "}"); + } + + public static JavaFileObject annotatedNotesProvider(AnnotationPrinter annotationPrinter) { + return JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + annotationPrinter.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + "}"); + } + + public static JavaFileObject installationListener(AnnotationPrinter annotationPrinter) { + return JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileCallbackQualifiedName() + ";", + annotationPrinter.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete();", + "}"); + } + + public static JavaFileObject installationListenerWithStringParam( + AnnotationPrinter annotationPrinter) { + return JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileCallbackQualifiedName() + ";", + annotationPrinter.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete(String s);", + "}"); + } + + public static JavaFileObject installationListenerWithListStringParam( + AnnotationPrinter annotationPrinter) { + return JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileCallbackQualifiedName() + ";", + annotationPrinter.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete(java.util.List<String> s);", + "}"); + } + + public static JavaFileObject staticType(AnnotationPrinter annotationPrinter) { + return JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".StaticType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class StaticType {", + annotationPrinter.crossProfileAsAnnotation(), + " public static void refreshNotes() {", + " }", + "}"); + } + + public static JavaFileObject annotatedDifferentCrossProfileType( + AnnotationPrinter annotationPrinter) { + return JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".DifferentType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class DifferentType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void differentMethod() {", + " }", + "}"); + } + + public static JavaFileObject annotatedDifferentProvider(AnnotationPrinter annotationPrinter) { + return JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".DifferentProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileProviderQualifiedName() + ";", + "public final class DifferentProvider {", + annotationPrinter.crossProfileProviderAsAnnotation(), + " public DifferentType provideDifferentType() {", + " return new DifferentType();", + " }", + "}"); + } + + public static JavaFileObject notesTypeWithDefaultConnector(AnnotationPrinter annotationPrinter) { + return annotatedNotesCrossProfileType(annotationPrinter); + } + + public static JavaFileObject annotatedNotesConfigurationWithNotesProvider( + AnnotationPrinter annotationPrinter) { + return JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileConfigurationQualifiedName() + ";", + "import com.google.android.enterprise.connectedapps.CrossProfileConnector;", + annotationPrinter.crossProfileConfigurationAsAnnotation( + "providers=NotesProvider.class, connector=CrossProfileConnector.class"), + "public abstract class NotesConfiguration {", + "}"); + } + + /** Combines two iterables into an iterable of all possible pairs. */ + public static Iterable<Object[]> combineParameters( + Iterable<?> parameters1, Iterable<?> parameters2) { + List<Object[]> testParameters = new ArrayList<>(); + + for (Object parameter1 : parameters1) { + for (Object parameter2 : parameters2) { + testParameters.add(new Object[] {parameter1, parameter2}); + } + } + + return testParameters; + } + + public static String formatErrorMessage(String errorMessage, AnnotationNames annotationNames) { + return ValidationMessageFormatter.forAnnotations(annotationNames).format(errorMessage); + } + + private TestUtilities() {} +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ValidationMessageFormatterTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ValidationMessageFormatterTest.java new file mode 100644 index 0000000..d2198a1 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ValidationMessageFormatterTest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationNames; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ValidationMessageFormatterTest { + + private static final String ERROR_MESSAGE = + "Methods annotated @CROSS_PROFILE_ANNOTATION should also be annotated" + + " @CROSS_PROFILE_CALLBACK_ANNOTATION(simple=true) and" + + " @CROSS_PROFILE_PROVIDER_ANNOTATION and @CROSS_PROFILE_CONFIGURATION_ANNOTATION and" + + " @CROSS_PROFILE_CONFIGURATIONS_ANNOTATION and @CROSS_PROFILE_TEST_ANNOTATION"; + + @Test + public void crossProfileAnnotationNames_formatsCorrectly() { + AnnotationNames annotationNames = AnnotationFinder.crossProfileAnnotationNames(); + + assertThat(ValidationMessageFormatter.forAnnotations(annotationNames).format(ERROR_MESSAGE)) + .isEqualTo( + "Methods annotated @CrossProfile should also be annotated" + + " @CrossProfileCallback(simple=true) and @CrossProfileProvider and" + + " @CrossProfileConfiguration and @CrossProfileConfigurations and" + + " @CrossProfileTest"); + } + + @Test + public void crossUserAnnotationNames_formatsCorrectly() { + AnnotationNames annotationNames = AnnotationFinder.crossUserAnnotationNames(); + + assertThat(ValidationMessageFormatter.forAnnotations(annotationNames).format(ERROR_MESSAGE)) + .isEqualTo( + "Methods annotated @CrossUser should also be annotated @CrossUserCallback(simple=true)" + + " and @CrossUserProvider and @CrossUserConfiguration and" + + " @CrossUserConfigurations and @CrossUserTest"); + } +} diff --git a/tests/processor/src/main/proto/connectedappssdk/TestProto.proto b/tests/processor/src/main/proto/connectedappssdk/TestProto.proto new file mode 100644 index 0000000..0753f5f --- /dev/null +++ b/tests/processor/src/main/proto/connectedappssdk/TestProto.proto @@ -0,0 +1,22 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +syntax = "proto2"; + +package connectedappssdk; + +message TestProto { + optional string text = 1; +} diff --git a/tests/robotests/src/test/AndroidManifest.xml b/tests/robotests/src/test/AndroidManifest.xml new file mode 100644 index 0000000..3ac0c30 --- /dev/null +++ b/tests/robotests/src/test/AndroidManifest.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + package="com.google.android.enterprise.connectedapps"> + + <uses-sdk + android:minSdkVersion="14" + android:targetSdkVersion="28"/> + + <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" /> + + <application> + </application> +</manifest>
\ No newline at end of file diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ConnectedAppsUtilsManagedPersonalProfileTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ConnectedAppsUtilsManagedPersonalProfileTest.java new file mode 100644 index 0000000..b87995e --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ConnectedAppsUtilsManagedPersonalProfileTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Tests for {@link ConnectedAppsUtils} when running on a personal profile which has management. + * + * <p>This is on a device which has only one profile. + * + * <p>This should behave as if running on a personal profile, not a work profile. + */ +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class ConnectedAppsUtilsManagedPersonalProfileTest { + + private final Application context = ApplicationProvider.getApplicationContext(); + private final ConnectedAppsUtils connectedAppsUtils = new ConnectedAppsUtilsImpl(context); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(context, /* scheduledExecutorService= */ null); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, CrossProfileConnector.class.getName()); + } + + @Test + public void getPersonalProfile_runningOnPersonalProfileWithManagement_returnsCurrentProfile() { + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setHasProfileOwner(); + + assertThat(connectedAppsUtils.getPersonalProfile()) + .isEqualTo(connectedAppsUtils.getCurrentProfile()); + } + + @Test + public void getWorkProfile_runningOnPersonalProfileWithManagement_returnsDifferentToCurrent() { + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setHasProfileOwner(); + + assertThat(connectedAppsUtils.getWorkProfile()) + .isNotEqualTo(connectedAppsUtils.getCurrentProfile()); + } + + @Test + public void runningOnWork_runningOnPersonalProfileWithManagement_returnsFalse() { + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setHasProfileOwner(); + + assertThat(connectedAppsUtils.runningOnWork()).isFalse(); + } + + @Test + public void runningOnPersonal_runningOnPersonalProfileWithManagement_returnsTrue() { + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setHasProfileOwner(); + + assertThat(connectedAppsUtils.runningOnPersonal()).isTrue(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ConnectedAppsUtilsTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ConnectedAppsUtilsTest.java new file mode 100644 index 0000000..6692067 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ConnectedAppsUtilsTest.java @@ -0,0 +1,183 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class ConnectedAppsUtilsTest { + + private final Application context = ApplicationProvider.getApplicationContext(); + private final ConnectedAppsUtils connectedAppsUtils = new ConnectedAppsUtilsImpl(context); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, CrossProfileConnector.class.getName()); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.startConnectingAndWait(); + } + + @Test + public void getPersonalProfile_runningOnPersonalProfile_returnsSameAsCurrentProfile() { + testUtilities.setRunningOnPersonalProfile(); + + assertThat(connectedAppsUtils.getPersonalProfile()) + .isEqualTo(connectedAppsUtils.getCurrentProfile()); + } + + @Test + public void getPersonalProfile_runningOnWorkProfile_returnsDifferentToCurrentProfile() { + testUtilities.setRunningOnWorkProfile(); + + assertThat(connectedAppsUtils.getPersonalProfile()) + .isNotEqualTo(connectedAppsUtils.getCurrentProfile()); + } + + @Test + public void getWorkProfile_runningOnWorkProfile_returnsSameAsCurrentProfile() { + testUtilities.setRunningOnWorkProfile(); + + assertThat(connectedAppsUtils.getWorkProfile()) + .isEqualTo(connectedAppsUtils.getCurrentProfile()); + } + + @Test + public void getWorkProfile_runningOnPersonalProfile_returnsDifferentToCurrentProfile() { + testUtilities.setRunningOnPersonalProfile(); + + assertThat(connectedAppsUtils.getWorkProfile()) + .isNotEqualTo(connectedAppsUtils.getCurrentProfile()); + } + + @Test + public void getPrimaryProfile_noPrimaryProfileSet_returnsNull() { + assertThat(connectedAppsUtils.getPrimaryProfile()).isNull(); + } + + @Test + public void getSecondaryProfile_noPrimaryProfileSet_returnsNull() { + assertThat(connectedAppsUtils.getSecondaryProfile()).isNull(); + } + + @Test + public void getPrimaryProfile_primaryProfileIsPersonal_runningOnPersonal_returnsCurrent() { + testUtilities.setRunningOnPersonalProfile(); + ConnectedAppsUtils utils = + new ConnectedAppsUtilsImpl(context, connectedAppsUtils.getPersonalProfile()); + + assertThat(utils.getPrimaryProfile()).isEqualTo(connectedAppsUtils.getCurrentProfile()); + } + + @Test + public void + getSecondaryProfile_primaryProfileIsPersonal_runningOnPersonal_returnsDifferentToCurrent() { + testUtilities.setRunningOnPersonalProfile(); + ConnectedAppsUtils utils = + new ConnectedAppsUtilsImpl(context, connectedAppsUtils.getPersonalProfile()); + + assertThat(utils.getSecondaryProfile()).isNotEqualTo(connectedAppsUtils.getCurrentProfile()); + } + + @Test + public void getPrimaryProfile_primaryProfileIsWork_runningOnPersonal_returnsDifferentToCurrent() { + testUtilities.setRunningOnPersonalProfile(); + ConnectedAppsUtils utils = + new ConnectedAppsUtilsImpl(context, connectedAppsUtils.getWorkProfile()); + + assertThat(utils.getPrimaryProfile()).isNotEqualTo(connectedAppsUtils.getCurrentProfile()); + } + + @Test + public void getSecondaryProfile_primaryProfileIsWork_runningOnPersonal_returnsCurrent() { + ConnectedAppsUtils utils = + new ConnectedAppsUtilsImpl(context, connectedAppsUtils.getWorkProfile()); + testUtilities.setRunningOnPersonalProfile(); + + assertThat(utils.getSecondaryProfile()).isEqualTo(connectedAppsUtils.getCurrentProfile()); + } + + @Test + public void getPrimaryProfile_primaryProfileIsWork_runningOnWork_returnsCurrent() { + testUtilities.setRunningOnWorkProfile(); + ConnectedAppsUtils utils = + new ConnectedAppsUtilsImpl(context, connectedAppsUtils.getWorkProfile()); + + assertThat(utils.getPrimaryProfile()).isEqualTo(connectedAppsUtils.getCurrentProfile()); + } + + @Test + public void getSecondaryProfile_primaryProfileIsWork_runningOnWork_returnsDifferentToCurrent() { + testUtilities.setRunningOnWorkProfile(); + ConnectedAppsUtils utils = + new ConnectedAppsUtilsImpl(context, connectedAppsUtils.getWorkProfile()); + + assertThat(utils.getSecondaryProfile()).isNotEqualTo(connectedAppsUtils.getCurrentProfile()); + } + + @Test + public void runningOnWork_runningOnPersonal_returnsFalse() { + testUtilities.setRunningOnPersonalProfile(); + + assertThat(connectedAppsUtils.runningOnWork()).isFalse(); + } + + @Test + public void runningOnWork_runningOnWork_returnsTrue() { + testUtilities.setRunningOnWorkProfile(); + + assertThat(connectedAppsUtils.runningOnWork()).isTrue(); + } + + @Test + public void runningOnPersonal_runningOnPersonal_returnsTrue() { + testUtilities.setRunningOnPersonalProfile(); + + assertThat(connectedAppsUtils.runningOnPersonal()).isTrue(); + } + + @Test + public void runningOnPersonal_runningOnWork_returnsFalse() { + testUtilities.setRunningOnWorkProfile(); + + assertThat(connectedAppsUtils.runningOnPersonal()).isFalse(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ConnectedAppsUtilsUnsupportedTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ConnectedAppsUtilsUnsupportedTest.java new file mode 100644 index 0000000..8df07b6 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ConnectedAppsUtilsUnsupportedTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import android.os.Build.VERSION_CODES; +import androidx.test.core.app.ApplicationProvider; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Tests for the {@link ConnectedAppsUtils} class running on unsupported Android versions. */ +@RunWith(RobolectricTestRunner.class) +@Config(maxSdk = VERSION_CODES.N_MR1) +public class ConnectedAppsUtilsUnsupportedTest { + private final Application context = ApplicationProvider.getApplicationContext(); + private final ConnectedAppsUtils connectedAppsUtils = new ConnectedAppsUtilsImpl(context); + + @Test + public void getCurrentProfile_returnsProfile() { + assertThat(connectedAppsUtils.getCurrentProfile()).isNotNull(); + } + + @Test + public void getOtherProfile_returnsProfile() { + assertThat(connectedAppsUtils.getOtherProfile()).isNotNull(); + } + + @Test + public void getWorkProfile_returnsProfile() { + assertThat(connectedAppsUtils.getWorkProfile()).isNotNull(); + } + + @Test + public void getPersonalProfile_returnsProfile() { + assertThat(connectedAppsUtils.getPersonalProfile()).isNotNull(); + } + + @Test + public void getPrimaryProfile_returnsNull() { + assertThat(connectedAppsUtils.getPrimaryProfile()).isNull(); + } + + @Test + public void getSecondaryProfile_returnsNull() { + assertThat(connectedAppsUtils.getSecondaryProfile()).isNull(); + } + + @Test + public void runningOnPersonal_returnsFalse() { + assertThat(connectedAppsUtils.runningOnPersonal()).isFalse(); + } + + @Test + public void runningOnWork_returnsFalse() { + assertThat(connectedAppsUtils.runningOnWork()).isFalse(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/CrossProfileSenderTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/CrossProfileSenderTest.java new file mode 100644 index 0000000..a8f14d8 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/CrossProfileSenderTest.java @@ -0,0 +1,499 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import static com.google.android.enterprise.connectedapps.RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME; +import static com.google.android.enterprise.connectedapps.RobolectricTestUtilities.TEST_SERVICE_CLASS_NAME; +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.robolectric.Shadows.shadowOf; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.app.Application; +import android.app.admin.DevicePolicyManager; +import android.content.ComponentName; +import android.os.Build.VERSION_CODES; +import android.os.Parcel; +import android.os.UserHandle; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.annotations.AvailabilityRestrictions; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.common.collect.ImmutableList; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +@LooperMode(LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class CrossProfileSenderTest { + + private final Application context = ApplicationProvider.getApplicationContext(); + private final DevicePolicyManager devicePolicyManager = + context.getSystemService(DevicePolicyManager.class); + private final TestService testService = new TestService(); + + private CrossProfileSender sender; + private final TestConnectionListener connectionListener = new TestConnectionListener(); + private final TestAvailabilityListener availabilityListener = new TestAvailabilityListener(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(context, scheduledExecutorService); + + @Before + public void setUp() { + testUtilities.initTests(); + sender = + new CrossProfileSender( + context, + TEST_SERVICE_CLASS_NAME, + new DefaultProfileBinder(), + connectionListener, + availabilityListener, + scheduledExecutorService, + AvailabilityRestrictions.DEFAULT); + sender.beginMonitoringAvailabilityChanges(); + + testUtilities.setBinding(testService, TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + } + + @Test + public void construct_nullContext_throwsNullPointerException() { + assertThrows( + NullPointerException.class, + () -> + new CrossProfileSender( + /* context= */ null, + TEST_SERVICE_CLASS_NAME, + new DefaultProfileBinder(), + connectionListener, + availabilityListener, + scheduledExecutorService, + AvailabilityRestrictions.DEFAULT)); + } + + @Test + public void construct_nullConnectedAppsServiceClassName_throwsNullPointerException() { + assertThrows( + NullPointerException.class, + () -> + new CrossProfileSender( + context, + /* connectedAppsServiceClassName= */ null, + new DefaultProfileBinder(), + connectionListener, + availabilityListener, + scheduledExecutorService, + AvailabilityRestrictions.DEFAULT)); + } + + @Test + public void construct_nullConnectionListener_throwsNullPointerException() { + assertThrows( + NullPointerException.class, + () -> + new CrossProfileSender( + context, + TEST_SERVICE_CLASS_NAME, + new DefaultProfileBinder(), + /* connectionListener= */ null, + availabilityListener, + scheduledExecutorService, + AvailabilityRestrictions.DEFAULT)); + } + + @Test + public void construct_nullAvailabilityListener_throwsNullPointerException() { + assertThrows( + NullPointerException.class, + () -> + new CrossProfileSender( + context, + TEST_SERVICE_CLASS_NAME, + new DefaultProfileBinder(), + connectionListener, + /* availabilityListener= */ null, + scheduledExecutorService, + AvailabilityRestrictions.DEFAULT)); + } + + @Test + public void construct_nullBindingConfig_throwsNullPointerException() { + assertThrows( + NullPointerException.class, + () -> + new CrossProfileSender( + context, + TEST_SERVICE_CLASS_NAME, + /* binder= */ null, + connectionListener, + availabilityListener, + scheduledExecutorService, + AvailabilityRestrictions.DEFAULT)); + } + + @Test + public void construct_nullTimeoutExecutor_throwsNullPointerException() { + assertThrows( + NullPointerException.class, + () -> + new CrossProfileSender( + context, + TEST_SERVICE_CLASS_NAME, + new DefaultProfileBinder(), + connectionListener, + availabilityListener, + /* scheduledExecutorService= */ null, + AvailabilityRestrictions.DEFAULT)); + } + + @Test + public void construct_nullAvailabilityRestrictions_throwsNullPointerException() { + assertThrows( + NullPointerException.class, + () -> + new CrossProfileSender( + context, + TEST_SERVICE_CLASS_NAME, + new DefaultProfileBinder(), + connectionListener, + availabilityListener, + scheduledExecutorService, + /* availabilityRestrictions= */ null)); + } + + // Other manuallyBind tests are covered in Instrumented ConnectTest because Robolectric doesn't + // handle the multiple threads very well + @Test + public void manuallyBind_callingFromUIThread_throwsIllegalStateException() { + assertThrows(IllegalStateException.class, sender::manuallyBind); + } + + @Test + public void startManuallyBinding_otherProfileIsNotAvailable_doesNotbind() { + testUtilities.turnOffWorkProfile(); + sender.startManuallyBinding(); + + assertThat(sender.isBound()).isFalse(); + } + + @Test + public void startManuallyBinding_bindingIsNotPossible_doesNotCallConnectionListener() { + testUtilities.turnOffWorkProfile(); + + sender.startManuallyBinding(); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(0); + } + + @Test + public void startManuallyBinding_otherProfileIsAvailable_binds() { + testUtilities.turnOnWorkProfile(); + sender.startManuallyBinding(); + + assertThat(sender.isBound()).isTrue(); + } + + @Test + public void startManuallyBinding_binds_callsConnectionListener() { + testUtilities.turnOnWorkProfile(); + sender.startManuallyBinding(); + testUtilities.advanceTimeBySeconds(1); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(1); + } + + @Test + public void startManuallyBinding_otherProfileBecomesAvailable_binds() { + testUtilities.turnOffWorkProfile(); + sender.startManuallyBinding(); + + testUtilities.turnOnWorkProfile(); + + assertThat(sender.isBound()).isTrue(); + } + + @Test + public void startManuallyBinding_otherProfileBecomesAvailable_callsConnectionListener() { + testUtilities.turnOffWorkProfile(); + sender.startManuallyBinding(); + + testUtilities.turnOnWorkProfile(); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(1); + } + + @Test + public void startManuallyBinding_profileBecomesUnavailable_unbinds() { + sender.startManuallyBinding(); + testUtilities.advanceTimeBySeconds(10); + + testUtilities.turnOffWorkProfile(); + + assertThat(sender.isBound()).isFalse(); + } + + @Test + public void startManuallyBinding_profileBecomesUnavailable_callsConnectionListener() { + sender.startManuallyBinding(); + testUtilities.advanceTimeBySeconds(10); + connectionListener.resetConnectionChangedCount(); + + testUtilities.turnOffWorkProfile(); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(1); + } + + @Test + public void startManuallyBinding_profileBecomesAvailableAgain_rebinds() { + sender.startManuallyBinding(); + testUtilities.advanceTimeBySeconds(10); + testUtilities.turnOffWorkProfile(); + + testUtilities.turnOnWorkProfile(); + + assertThat(sender.isBound()).isTrue(); + } + + @Test + public void startManuallyBinding_profileBecomesAvailableAgain_callsConnectionListener() { + sender.startManuallyBinding(); + testUtilities.advanceTimeBySeconds(10); + testUtilities.turnOffWorkProfile(); + connectionListener.resetConnectionChangedCount(); + + testUtilities.turnOnWorkProfile(); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(1); + } + + @Test + public void unbind_isNotBound() { + sender.startManuallyBinding(); + + sender.unbind(); + + assertThat(sender.isBound()).isFalse(); + } + + @Test + public void unbind_callsConnectionListener() { + sender.startManuallyBinding(); + testUtilities.advanceTimeBySeconds(1); + connectionListener.resetConnectionChangedCount(); + + sender.unbind(); + testUtilities.advanceTimeBySeconds(1); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(1); + } + + @Test + public void unbind_profileBecomesAvailable_doesNotBind() { + testUtilities.turnOffWorkProfile(); + sender.startManuallyBinding(); + sender.unbind(); + + testUtilities.turnOnWorkProfile(); + + assertThat(sender.isBound()).isFalse(); + } + + @Test + public void bind_bindingFromPersonalProfile_binds() { + testUtilities.setRunningOnPersonalProfile(); + sender.startManuallyBinding(); + + assertThat(sender.isBound()).isTrue(); + } + + @Test + public void bind_bindingFromWorkProfile_binds() { + testUtilities.setRunningOnWorkProfile(); + sender.startManuallyBinding(); + + assertThat(sender.isBound()).isTrue(); + } + + @Test + public void call_isNotBound_throwsUnavailableProfileException() { + int crossProfileTypeIdentifier = 1; + int methodIdentifier = 0; + Parcel params = Parcel.obtain(); + sender.unbind(); + + assertThrows( + UnavailableProfileException.class, + () -> sender.call(crossProfileTypeIdentifier, methodIdentifier, params)); + } + + @Test + public void call_isBound_callsMethod() throws UnavailableProfileException { + int crossProfileTypeIdentifier = 1; + int methodIdentifier = 0; + Parcel params = Parcel.obtain(); + params.writeString("value"); + sender.startManuallyBinding(); + + sender.call(crossProfileTypeIdentifier, methodIdentifier, params); + + assertThat(testService.lastCall().getCrossProfileTypeIdentifier()) + .isEqualTo(crossProfileTypeIdentifier); + assertThat(testService.lastCall().getMethodIdentifier()).isEqualTo(methodIdentifier); + assertThat(testService.lastCall().getParams().readString()).isEqualTo("value"); + } + + @Test + public void call_isBound_returnsResponse() throws UnavailableProfileException { + int crossProfileTypeIdentifier = 1; + int methodIdentifier = 0; + Parcel params = Parcel.obtain(); + Parcel expectedResponseParcel = Parcel.obtain(); + expectedResponseParcel.writeInt(0); // No error + expectedResponseParcel.writeString("value"); + testService.setResponseParcel(expectedResponseParcel); + sender.startManuallyBinding(); + + Parcel actualResponseParcel = sender.call(crossProfileTypeIdentifier, methodIdentifier, params); + + assertThat(actualResponseParcel.readString()).isEqualTo("value"); + } + + @Test + public void bind_usingDpcBinding_otherProfileIsAvailable_binds() { + initWithDpcBinding(); + testUtilities.turnOnWorkProfile(); + sender.startManuallyBinding(); + + assertThat(sender.isBound()).isTrue(); + } + + @Test + public void bind_usingDpcBinding_binds_callsConnectionListener() { + initWithDpcBinding(); + testUtilities.turnOnWorkProfile(); + sender.startManuallyBinding(); + testUtilities.advanceTimeBySeconds(1); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(1); + } + + @Test + public void bind_usingDpcBinding_otherProfileDoesNotExist_doesNotBind() { + initWithDpcBinding(); + shadowOf(devicePolicyManager).setBindDeviceAdminTargetUsers(ImmutableList.of()); + + sender.startManuallyBinding(); + testUtilities.advanceTimeBySeconds(10); + + assertThat(sender.isBound()).isFalse(); + } + + @Test + public void bind_usingDpcBinding_otherProfileIsCreated_binds() { + initWithDpcBinding(); + shadowOf(devicePolicyManager).setBindDeviceAdminTargetUsers(ImmutableList.of()); + sender.startManuallyBinding(); + testUtilities.advanceTimeBySeconds(10); + + shadowOf(devicePolicyManager) + .setBindDeviceAdminTargetUsers(ImmutableList.of(getWorkUserHandle())); + testUtilities.turnOnWorkProfile(); + + assertThat(sender.isBound()).isTrue(); + } + + @Test + public void bind_usingDpcBinding_otherProfileBecomesAvailable_binds() { + initWithDpcBinding(); + testUtilities.turnOffWorkProfile(); + sender.startManuallyBinding(); + testUtilities.advanceTimeBySeconds(10); + + testUtilities.turnOnWorkProfile(); + + assertThat(sender.isBound()).isTrue(); + } + + @Test + public void bind_usingDpcBinding_otherProfileBecomesAvailable_callsConnectionListener() { + initWithDpcBinding(); + testUtilities.turnOffWorkProfile(); + sender.startManuallyBinding(); + testUtilities.advanceTimeBySeconds(10); + + testUtilities.turnOnWorkProfile(); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(1); + } + + @Test + public void workProfileBecomesAvailable_callsAvailabilityListener() { + testUtilities.turnOffWorkProfile(); + availabilityListener.reset(); + + testUtilities.turnOnWorkProfile(); + + assertThat(availabilityListener.availabilityChangedCount()).isEqualTo(1); + } + + @Test + public void workProfileBecomesUnavailable_callsAvailabilityListener() { + testUtilities.turnOnWorkProfile(); + availabilityListener.reset(); + + testUtilities.turnOffWorkProfile(); + + assertThat(availabilityListener.availabilityChangedCount()).isEqualTo(1); + } + + private void initWithDpcBinding() { + shadowOf(devicePolicyManager) + .setBindDeviceAdminTargetUsers(ImmutableList.of(getWorkUserHandle())); + + ComponentName deviceAdminReceiver = new ComponentName("A", "B"); + + testUtilities.initTests(); + sender = + new CrossProfileSender( + context, + TEST_SERVICE_CLASS_NAME, + new DpcProfileBinder(deviceAdminReceiver), + connectionListener, + availabilityListener, + scheduledExecutorService, + AvailabilityRestrictions.DEFAULT); + + testUtilities.setBinding(testService, TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + } + + private static UserHandle getWorkUserHandle() { + return SharedTestUtilities.getUserHandleForUserId(10); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/DpcProfileBinderTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/DpcProfileBinderTest.java new file mode 100644 index 0000000..2a62d46 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/DpcProfileBinderTest.java @@ -0,0 +1,31 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import static org.junit.Assert.assertThrows; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class DpcProfileBinderTest { + + @Test + public void construct_nullDeviceAdminReceiver_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new DpcProfileBinder(null)); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/PermissionsTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/PermissionsTest.java new file mode 100644 index 0000000..9083b6c --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/PermissionsTest.java @@ -0,0 +1,106 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import static android.Manifest.permission.INTERACT_ACROSS_PROFILES; +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS_FULL; +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import android.os.Build.VERSION_CODES; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class PermissionsTest { + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector connector = + TestProfileConnector.create(context, scheduledExecutorService); + private final Permissions permissions = connector.permissions(); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(connector, scheduledExecutorService); + + @Test + public void canMakeCrossProfileCalls_defaultProfileBinder_doesntDeclareAnyPermissions_isFalse() { + testUtilities.setRequestsPermissions(); + + assertThat(permissions.canMakeCrossProfileCalls()).isFalse(); + } + + @Test + public void + canMakeCrossProfileCalls_defaultProfileBinder_interactAcrossUsersNotGranted_isFalse() { + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.denyPermissions(INTERACT_ACROSS_USERS); + + assertThat(permissions.canMakeCrossProfileCalls()).isFalse(); + } + + @Test + public void + canMakeCrossProfileCalls_defaultProfileBinder_interactAcrossUsersFullNotGranted_isFalse() { + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS_FULL); + testUtilities.denyPermissions(INTERACT_ACROSS_USERS_FULL); + + assertThat(permissions.canMakeCrossProfileCalls()).isFalse(); + } + + @Test + public void + canMakeCrossProfileCalls_defaultProfileBinder_interactAcrossProfilesNotGranted_isFalse() { + testUtilities.setRequestsPermissions(INTERACT_ACROSS_PROFILES); + testUtilities.denyPermissions(INTERACT_ACROSS_PROFILES); + + assertThat(permissions.canMakeCrossProfileCalls()).isFalse(); + } + + @Test + public void canMakeCrossProfileCalls_defaultProfileBinder_interactAcrossUsersGranted_isTrue() { + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + + assertThat(permissions.canMakeCrossProfileCalls()).isTrue(); + } + + @Test + public void + canMakeCrossProfileCalls_defaultProfileBinder_interactAcrossUsersFullGranted_isTrue() { + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS_FULL); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS_FULL); + + assertThat(permissions.canMakeCrossProfileCalls()).isTrue(); + } + + @Test + @Ignore // TODO(161541780): Enable this test when building against a supported version of + // Robolectric + public void canMakeCrossProfileCalls_defaultProfileBinder_interactAcrossProfilesGranted_isTrue() { + testUtilities.setRequestsPermissions(INTERACT_ACROSS_PROFILES); + testUtilities.grantPermissions(INTERACT_ACROSS_PROFILES); + + assertThat(permissions.canMakeCrossProfileCalls()).isTrue(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/PermissionsUnsupportedTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/PermissionsUnsupportedTest.java new file mode 100644 index 0000000..b6b1758 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/PermissionsUnsupportedTest.java @@ -0,0 +1,41 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import android.os.Build.VERSION_CODES; +import androidx.test.core.app.ApplicationProvider; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Tests for the {@link Permissions} class running on unsupported Android versions. */ +@RunWith(RobolectricTestRunner.class) +@Config(maxSdk = VERSION_CODES.N_MR1) +public class PermissionsUnsupportedTest { + + private final Application context = ApplicationProvider.getApplicationContext(); + private final ConnectionBinder binder = new DefaultProfileBinder(); + private final Permissions permissions = new PermissionsImpl(context, binder); + + @Test + public void canMakeCrossProfileCalls_unsupportedVersion_returnsFalse() { + assertThat(permissions.canMakeCrossProfileCalls()).isFalse(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ProfileConnectorTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ProfileConnectorTest.java new file mode 100644 index 0000000..a6b5b17 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ProfileConnectorTest.java @@ -0,0 +1,254 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import static com.google.android.enterprise.connectedapps.RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME; +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.robolectric.Shadows.shadowOf; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.DirectBootAwareConnector; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnectorWithCustomServiceClass; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +/** Tests for the {@link CustomProfileConnector} class. */ +@LooperMode(LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class ProfileConnectorTest { + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestConnectionListener connectionListener = new TestConnectionListener(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final DirectBootAwareConnector directBootAwareConnector = + DirectBootAwareConnector.create(context); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, TEST_CONNECTOR_CLASS_NAME); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + } + + @Test + public void construct_nullConnector_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> TestProfileConnector.create(null)); + } + + // Other connect tests are covered in Instrumented ConnectTest because Robolectric doesn't + // handle the multiple threads very well + @Test + public void connect_callingFromUIThread_throwsIllegalStateException() { + assertThrows(IllegalStateException.class, testProfileConnector::connect); + } + + @Test + public void startConnecting_fromPersonalProfile_binds() { + testUtilities.setRunningOnPersonalProfile(); + testUtilities.startConnectingAndWait(); + + assertThat(testProfileConnector.isConnected()).isTrue(); + } + + @Test + public void startConnecting_fromWorkProfile_binds() { + testUtilities.setRunningOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + assertThat(testProfileConnector.isConnected()).isTrue(); + } + + @Test + public void disconnect_fromPersonalProfile_doesNotBind() { + testUtilities.setRunningOnPersonalProfile(); + testUtilities.disconnect(); + + assertThat(testProfileConnector.isConnected()).isFalse(); + } + + @Test + public void disconnect_fromWorkProfile_doesNotBind() { + testUtilities.setRunningOnWorkProfile(); + testUtilities.disconnect(); + + assertThat(testProfileConnector.isConnected()).isFalse(); + } + + @Test + public void disconnect_isBound_unbinds() { + testUtilities.startConnectingAndWait(); + + testUtilities.disconnect(); + + assertThat(testProfileConnector.isConnected()).isFalse(); + } + + @Test + public void startConnecting_callsConnectionListener() { + testProfileConnector.registerConnectionListener(connectionListener); + testUtilities.startConnectingAndWait(); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(1); + } + + @Test + public void startConnecting_doesNotCallUnregisteredConnectionListener() { + testProfileConnector.registerConnectionListener(connectionListener); + testProfileConnector.unregisterConnectionListener(connectionListener); + testUtilities.startConnectingAndWait(); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(0); + } + + @Test + public void disconnect_callsConnectionListener() { + testProfileConnector.registerConnectionListener(connectionListener); + testUtilities.startConnectingAndWait(); + connectionListener.resetConnectionChangedCount(); + + testUtilities.disconnect(); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(1); + } + + @Test + public void bindingDies_callsConnectionListener() { + testProfileConnector.registerConnectionListener(connectionListener); + testUtilities.startConnectingAndWait(); + connectionListener.resetConnectionChangedCount(); + + testUtilities.turnOffWorkProfile(); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(1); + } + + @Test + public void startConnecting_profileConnectorWithCustomServiceClass() { + TestProfileConnectorWithCustomServiceClass.create(context, scheduledExecutorService) + .startConnecting(); + testUtilities.advanceTimeBySeconds(1); // Allow connection + + assertThat(shadowOf(context).getNextStartedService().getComponent().getClassName()) + .isEqualTo("com.google.CustomServiceClass"); + } + + @Test + public void isAvailable_workProfileIsTurnedOn_returnsTrue() { + testUtilities.setRunningOnPersonalProfile(); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + + assertThat(testProfileConnector.isAvailable()).isTrue(); + } + + @Test + public void isAvailable_workProfileIsTurnedOff_returnsFalse() { + testUtilities.setRunningOnPersonalProfile(); + testUtilities.createWorkUser(); + testUtilities.turnOffWorkProfile(); + + assertThat(testProfileConnector.isAvailable()).isFalse(); + } + + @Test + public void isAvailable_runningOnWorkProfile_returnsTrue() { + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnWorkProfile(); + + assertThat(testProfileConnector.isAvailable()).isTrue(); + } + + @Test + public void isAvailable_defaultAvailabilityRestrictions_isNotUnlocked_returnsFalse() { + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfileWithoutUnlocking(); + + assertThat(testProfileConnector.isAvailable()).isFalse(); + } + + @Test + public void isAvailable_defaultAvailabilityRestrictions_isUnlocked_returnsTrue() { + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + + assertThat(testProfileConnector.isAvailable()).isTrue(); + } + + @Test + public void isAvailable_directBootAwareAvailabilityRestrictions_isNotUnlocked_returnsTrue() { + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfileWithoutUnlocking(); + + assertThat(directBootAwareConnector.isAvailable()).isTrue(); + } + + @Test + public void isAvailable_directBootAwareAvailabilityRestrictions_isUnlocked_returnsTrue() { + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + + assertThat(directBootAwareConnector.isAvailable()).isTrue(); + } + + @Test + public void isManuallyManagingConnection_returnsFalse() { + assertThat(testProfileConnector.isManuallyManagingConnection()).isFalse(); + } + + @Test + public void isManuallyManagingConnection_hasManuallyConnected_returnsTrue() { + testUtilities.startConnectingAndWait(); + + assertThat(testProfileConnector.isManuallyManagingConnection()).isTrue(); + } + + @Test + public void isManuallyManagingConnection_hasCalledStopManualConnectionManagement_returnsFalse() { + testUtilities.startConnectingAndWait(); + + testProfileConnector.stopManualConnectionManagement(); + + assertThat(testProfileConnector.isManuallyManagingConnection()).isFalse(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ProfileConnectorUnsupportedTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ProfileConnectorUnsupportedTest.java new file mode 100644 index 0000000..d6bb9ca --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ProfileConnectorUnsupportedTest.java @@ -0,0 +1,93 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import android.app.Application; +import android.os.Build.VERSION_CODES; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Tests for the {@link CustomProfileConnector} class running on unsupported Android versions. */ +@RunWith(RobolectricTestRunner.class) +@Config(maxSdk = VERSION_CODES.N_MR1) +public class ProfileConnectorUnsupportedTest { + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestProfileConnector testProfileConnector = TestProfileConnector.create(context); + + @Test + public void startConnecting_doesNotCrash() { + testProfileConnector.startConnecting(); + } + + @Test + public void connect_throwsUnavailableProfileException() { + assertThrows(UnavailableProfileException.class, testProfileConnector::connect); + } + + @Test + public void isAvailable_returnsFalse() { + assertThat(testProfileConnector.isAvailable()).isFalse(); + } + + @Test + public void isConnected_returnsFalse() { + assertThat(testProfileConnector.isConnected()).isFalse(); + } + + @Test + public void stopManualConnectionManagement_doesNotCrash() { + testProfileConnector.stopManualConnectionManagement(); + } + + @Test + public void crossProfileSender_returnsNull() { + assertThat(testProfileConnector.crossProfileSender()).isNull(); + } + + @Test + public void registerConnectionListener_doesNotCrash() { + testProfileConnector.registerConnectionListener(() -> {}); + } + + @Test + public void unregisterConnectionListener_doesNotCrash() { + testProfileConnector.unregisterConnectionListener(() -> {}); + } + + @Test + public void registerAvailabilityListener_doesNotCrash() { + testProfileConnector.registerAvailabilityListener(() -> {}); + } + + @Test + public void unregisterAvailabilityListener_doesNotCrash() { + testProfileConnector.unregisterAvailabilityListener(() -> {}); + } + + @Test + public void utils_returnsInstance() { + assertThat(testProfileConnector.utils()).isNotNull(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ProfileTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ProfileTest.java new file mode 100644 index 0000000..6c087fb --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ProfileTest.java @@ -0,0 +1,76 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import android.os.Build.VERSION_CODES; +import androidx.test.core.app.ApplicationProvider; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class ProfileTest { + + private final Application context = ApplicationProvider.getApplicationContext(); + private final ConnectedAppsUtils connectedAppsUtils = new ConnectedAppsUtilsImpl(context); + + @Test + public void isCurrent_currentProfile_returnsTrue() { + Profile identifier = connectedAppsUtils.getCurrentProfile(); + + assertThat(identifier.isCurrent()).isTrue(); + } + + @Test + public void isCurrent_notCurrent_returnsFalse() { + Profile identifier = connectedAppsUtils.getOtherProfile(); + + assertThat(identifier.isCurrent()).isFalse(); + } + + @Test + public void isOther_otherProfile_returnsTrue() { + Profile identifier = connectedAppsUtils.getOtherProfile(); + + assertThat(identifier.isOther()).isTrue(); + } + + @Test + public void isOther_notOtherProfile_returnsFalse() { + Profile identifier = connectedAppsUtils.getCurrentProfile(); + + assertThat(identifier.isOther()).isFalse(); + } + + @Test + public void fromInt_intFromCurrentProfile_equalsCurrentProfile() { + Profile identifier = connectedAppsUtils.getCurrentProfile(); + + assertThat(Profile.fromInt(identifier.asInt())).isEqualTo(identifier); + } + + @Test + public void fromInt_intFromOtherProfile_equalsOtherProfile() { + Profile identifier = connectedAppsUtils.getCurrentProfile(); + + assertThat(Profile.fromInt(identifier.asInt())).isEqualTo(identifier); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/RobolectricTestUtilities.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/RobolectricTestUtilities.java new file mode 100644 index 0000000..ae14549 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/RobolectricTestUtilities.java @@ -0,0 +1,362 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import static android.Manifest.permission.INTERACT_ACROSS_PROFILES; +import static android.os.Looper.getMainLooper; +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS_FULL; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.robolectric.Shadows.shadowOf; + +import android.app.Application; +import android.app.admin.DevicePolicyManager; +import android.content.ComponentName; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.ApplicationInfo; +import android.content.pm.CrossProfileApps; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import android.os.UserHandle; +import android.os.UserManager; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.TestCrossProfileType; +import com.google.common.util.concurrent.FluentFuture; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicReference; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.robolectric.Robolectric; +import org.robolectric.shadow.api.Shadow; +import org.robolectric.shadows.ShadowContextImpl; +import org.robolectric.shadows.ShadowProcess; +import org.robolectric.shadows.ShadowUserManager.UserState; + +public class RobolectricTestUtilities { + + private static final int PERSONAL_PROFILE_USER_ID = 0; + private static final int WORK_PROFILE_USER_ID = 10; + + /* Matches UserHandle#PER_USER_RANGE */ + private static final int PER_USER_RANGE = 100000; + + private final UserHandle personalProfileUserHandle = + SharedTestUtilities.getUserHandleForUserId(PERSONAL_PROFILE_USER_ID); + private final UserHandle workProfileUserHandle = + SharedTestUtilities.getUserHandleForUserId(WORK_PROFILE_USER_ID); + private static final int WORK_UID = PER_USER_RANGE * WORK_PROFILE_USER_ID; + private static final int PERSONAL_UID = PER_USER_RANGE * PERSONAL_PROFILE_USER_ID; + private final Application context; + private final DevicePolicyManager devicePolicyManager; + private final UserManager userManager; + private final CrossProfileApps crossProfileApps; + private final ShadowContextImpl shadowContext; + private final PackageManager packageManager; + private final ComponentName profileOwnerComponentName = new ComponentName("profileowner", ""); + private final PackageInfo profileOwnerPackage = new PackageInfo(); + private final TestScheduledExecutorService scheduledExecutorService; + + public static final String TEST_CONNECTOR_CLASS_NAME = TestProfileConnector.class.getName(); + public static final String TEST_SERVICE_CLASS_NAME = TEST_CONNECTOR_CLASS_NAME + "_Service"; + + // These permissions should persist across profiles + private boolean hasGrantedInteractAcrossProfiles = false; + private boolean hasGrantedInteractAcrossUsers = false; + private boolean hasGrantedInteractAcrossUsersFull = false; + + private @Nullable ProfileConnector connector; + + public RobolectricTestUtilities( + ProfileConnector connector, TestScheduledExecutorService scheduledExecutorService) { + this((Application) connector.applicationContext(), scheduledExecutorService); + this.connector = connector; + } + + public RobolectricTestUtilities( + Application context, TestScheduledExecutorService scheduledExecutorService) { + this.context = context; + devicePolicyManager = context.getSystemService(DevicePolicyManager.class); + userManager = context.getSystemService(UserManager.class); + crossProfileApps = context.getSystemService(CrossProfileApps.class); + packageManager = context.getPackageManager(); + shadowContext = Shadow.extract(context.getBaseContext()); + this.scheduledExecutorService = scheduledExecutorService; + + profileOwnerPackage.applicationInfo = new ApplicationInfo(); + profileOwnerPackage.packageName = profileOwnerComponentName.getPackageName(); + } + + public void initTests() { + TestCrossProfileType.voidMethodCalls = 0; + CrossProfileSDKUtilities.clearCache(); + createPersonalUser(); + } + + public void startConnectingAndWait() { + connector.startConnecting(); + advanceTimeBySeconds(1); + } + + public void disconnect() { + connector.stopManualConnectionManagement(); + advanceTimeBySeconds(31); // Give time to timeout connection + } + + public void createPersonalUser() { + shadowOf(userManager).addUser(PERSONAL_PROFILE_USER_ID, "Personal Profile", /* flags= */ 0); + shadowOf(userManager) + .addProfile(PERSONAL_PROFILE_USER_ID, PERSONAL_PROFILE_USER_ID, "Personal Profile", 0); + shadowOf(userManager).setUserState(personalProfileUserHandle, UserState.STATE_RUNNING_UNLOCKED); + } + + public void createWorkUser() { + shadowOf(userManager).addUser(WORK_PROFILE_USER_ID, "Work Profile", /* flags= */ 0); + shadowOf(userManager) + .addProfile(PERSONAL_PROFILE_USER_ID, WORK_PROFILE_USER_ID, "Work Profile", 0); + shadowOf(userManager).addProfile(WORK_PROFILE_USER_ID, WORK_PROFILE_USER_ID, "Work Profile", 0); + shadowOf(userManager) + .addProfile(WORK_PROFILE_USER_ID, PERSONAL_PROFILE_USER_ID, "Personal Profile", 0); + } + + public void turnOnWorkProfileWithoutUnlocking() { + shadowOf(userManager).setUserState(workProfileUserHandle, UserState.STATE_RUNNING_LOCKED); + tryAddTargetUserProfile(workProfileUserHandle); + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE); + context.sendBroadcast(intent); + shadowOf(getMainLooper()).idle(); + } + + public void turnOnWorkProfile() { + shadowOf(userManager).setUserState(workProfileUserHandle, UserState.STATE_RUNNING_UNLOCKED); + tryAddTargetUserProfile(workProfileUserHandle); + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE); + context.sendBroadcast(intent); + intent = new Intent(); + intent.setAction(Intent.ACTION_USER_UNLOCKED); + context.sendBroadcast(intent); + advanceTimeBySeconds(10); + } + + public void turnOffWorkProfile() { + shadowOf(userManager).setUserState(workProfileUserHandle, UserState.STATE_SHUTDOWN); + removeTargetUserProfile(workProfileUserHandle); + simulateDisconnectingServiceConnection(); + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE); + context.sendBroadcast(intent); + advanceTimeBySeconds(10); + } + + private void tryAddTargetUserProfile(UserHandle userHandle) { + try { + addTargetUserProfile(userHandle); + } catch (IllegalArgumentException e) { + // This is thrown if we are running on that profile + } + } + + private void addTargetUserProfile(UserHandle userHandle) { + if (VERSION.SDK_INT < VERSION_CODES.P) { + return; + } + shadowOf(crossProfileApps).addTargetUserProfile(userHandle); + } + + private void tryRemoveTargetUserProfile(UserHandle userHandle) { + try { + removeTargetUserProfile(userHandle); + } catch (IllegalArgumentException e) { + // This is thrown if we are running on that profile + } + } + + private void removeTargetUserProfile(UserHandle userHandle) { + if (VERSION.SDK_INT < VERSION_CODES.P) { + return; + } + shadowOf(crossProfileApps).removeTargetUserProfile(userHandle); + } + + public void simulateDisconnectingServiceConnection() { + ServiceConnection serviceConnection = getServiceConnection(); + if (serviceConnection == null) { + return; + } + serviceConnection.onServiceDisconnected(new ComponentName("", "")); + } + + private ServiceConnection getServiceConnection() { + if (getBoundServiceConnections().isEmpty()) { + return null; + } + return getBoundServiceConnections().get(0); + } + + private List<ServiceConnection> getBoundServiceConnections() { + return shadowOf(context).getBoundServiceConnections(); + } + + public void setRunningOnPersonalProfile() { + shadowContext.setUserId(PERSONAL_PROFILE_USER_ID); + shadowOf(context.getPackageManager()).removePackage(profileOwnerPackage.packageName); + shadowOf(devicePolicyManager).setProfileOwner(null); + shadowOf(userManager).setManagedProfile(false); + setUid(PERSONAL_UID); + shadowOf(context.getPackageManager()).setPackagesForUid(PERSONAL_UID, context.getPackageName()); + tryRemoveTargetUserProfile(personalProfileUserHandle); + tryAddTargetUserProfile(workProfileUserHandle); + regrantPermissions(); + } + + public void setRunningOnWorkProfile() { + shadowContext.setUserId(WORK_PROFILE_USER_ID); + setHasProfileOwner(); + shadowOf(userManager).setManagedProfile(true); + tryRemoveTargetUserProfile(workProfileUserHandle); + setUid(WORK_UID); + shadowOf(context.getPackageManager()).setPackagesForUid(WORK_UID, context.getPackageName()); + shadowOf(userManager).setUserState(personalProfileUserHandle, UserState.STATE_RUNNING_UNLOCKED); + addTargetUserProfile(personalProfileUserHandle); + regrantPermissions(); + } + + public void setHasProfileOwner() { + shadowOf(context.getPackageManager()).installPackage(profileOwnerPackage); + shadowOf(devicePolicyManager).setProfileOwner(profileOwnerComponentName); + } + + private void setUid(int uid) { + ShadowProcess.setUid(uid); + // This is needed for CrossProfileApps but causes issues for < P + if (VERSION.SDK_INT >= VERSION_CODES.P) { + shadowOf(context.getPackageManager()).setNameForUid(uid, context.getPackageName()); + } + } + + public void setBinding(IBinder binder, String connectorClassQualifiedName) { + ComponentName serviceClassComponentName = + new ComponentName(context.getPackageName(), connectorClassQualifiedName + "_Service"); + Intent bindIntent = new Intent(); + bindIntent.setComponent(serviceClassComponentName); + + ICrossProfileService.Stub actualServiceStub = (ICrossProfileService.Stub) binder; + + shadowOf(context) + .setComponentNameAndServiceForBindServiceForIntent( + bindIntent, serviceClassComponentName, actualServiceStub); + } + + public void setRequestsPermissions(String... permissions) { + PackageInfo packageInfo = new PackageInfo(); + packageInfo.packageName = context.getPackageName(); + packageInfo.requestedPermissions = permissions; + shadowOf(packageManager).installPackage(packageInfo); + } + + public void grantPermissions(String... permissions) { + for (String permission : permissions) { + if (permission.equals(INTERACT_ACROSS_USERS)) { + hasGrantedInteractAcrossUsers = true; + } + if (permission.equals(INTERACT_ACROSS_USERS_FULL)) { + hasGrantedInteractAcrossUsersFull = true; + } + if (permission.equals(INTERACT_ACROSS_PROFILES)) { + hasGrantedInteractAcrossProfiles = true; + } + } + shadowOf(context).grantPermissions(permissions); + } + + public void denyPermissions(String... permissions) { + for (String permission : permissions) { + if (permission.equals(INTERACT_ACROSS_USERS)) { + hasGrantedInteractAcrossUsers = false; + } + if (permission.equals(INTERACT_ACROSS_USERS_FULL)) { + hasGrantedInteractAcrossUsersFull = false; + } + if (permission.equals(INTERACT_ACROSS_PROFILES)) { + hasGrantedInteractAcrossProfiles = false; + } + } + shadowOf(context).denyPermissions(permissions); + } + + private void regrantPermissions() { + if (hasGrantedInteractAcrossProfiles) { + grantPermissions(INTERACT_ACROSS_PROFILES); + } else { + denyPermissions(INTERACT_ACROSS_PROFILES); + } + if (hasGrantedInteractAcrossUsers) { + grantPermissions(INTERACT_ACROSS_USERS); + } else { + denyPermissions(INTERACT_ACROSS_USERS); + } + if (hasGrantedInteractAcrossUsersFull) { + grantPermissions(INTERACT_ACROSS_USERS_FULL); + } else { + denyPermissions(INTERACT_ACROSS_USERS_FULL); + } + } + + public @Nullable Throwable assertFutureHasException( + ListenableFuture<?> future, Class<? extends Throwable> throwable) { + AtomicReference<Throwable> thrown = new AtomicReference<>(); + try { + FluentFuture.from(future) + .catching( + throwable, + t -> { + // Expected + thrown.set(t); + return null; + }, + directExecutor()) + .get(); + } catch (InterruptedException | ExecutionException e) { + throw new AssertionError("Unhandled exception", e); + } + + assertThat(thrown.get()).isNotNull(); + return thrown.get(); + } + + public void advanceTimeBySeconds(int intervalSeconds) { + for (int i = 0; i < intervalSeconds; i++) { + if (scheduledExecutorService != null) { + try { + scheduledExecutorService.advanceTimeBy(1, SECONDS); + } catch (Exception e) { + throw new IllegalStateException("Error advancing time", e); + } + + } + Robolectric.getForegroundThreadScheduler().advanceBy(1, SECONDS); + shadowOf(getMainLooper()).idle(); + } + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/TestICrossProfileCallback.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/TestICrossProfileCallback.java new file mode 100644 index 0000000..461cc9a --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/TestICrossProfileCallback.java @@ -0,0 +1,63 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import android.os.IBinder; +import android.os.Parcel; +import android.os.RemoteException; + +/** + * An implementation of {@link ICrossProfileCallback} which just redirects call to a given {@link + * LocalCallback}. + * + * <p>This does not support preparing results, it only supports results which fit into a single + * block. + */ +public class TestICrossProfileCallback implements ICrossProfileCallback { + private final LocalCallback localCallback; + + public TestICrossProfileCallback(LocalCallback localCallback) { + this.localCallback = localCallback; + } + + @Override + public void prepareResult(long callId, int blockId, int numBytes, byte[] params) + throws RemoteException {} + + @Override + public void onResult(long callId, int blockId, int methodIdentifier, byte[] params) + throws RemoteException { + Parcel p = Parcel.obtain(); // Recycled in this method + p.unmarshall(params, 0, params.length); + p.setDataPosition(0); + localCallback.onResult(methodIdentifier, p); + p.recycle(); + } + + @Override + public void onException(long callId, int blockId, byte[] params) throws RemoteException { + Parcel p = Parcel.obtain(); // Recycled in this method + p.unmarshall(params, 0, params.length); + p.setDataPosition(0); + localCallback.onException(p); + p.recycle(); + } + + @Override + public IBinder asBinder() { + return null; + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/TestScheduledExecutorService.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/TestScheduledExecutorService.java new file mode 100644 index 0000000..f237e66 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/TestScheduledExecutorService.java @@ -0,0 +1,179 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import java.util.List; +import java.util.Queue; +import java.util.concurrent.AbstractExecutorService; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Delayed; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +/** + * A test {@link ScheduledExecutorService} which supports only the methods used by the Connected + * Apps SDK. + * + * <p>Use {@link #advanceTimeBy(long, TimeUnit)} for progress time. Everything is executed on + * the calling thread. + */ +public class TestScheduledExecutorService extends AbstractExecutorService implements ScheduledExecutorService { + + private long millisPast = 0; + private final Queue<SimpleScheduledFuture<?>> executeQueue = new ConcurrentLinkedQueue<>(); + public TestScheduledExecutorService() {} + + @Override + public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) { + return schedule((Callable<Void>) () -> { + command.run(); + return null; + }, delay, unit); + } + + @Override + public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) { + if (executeQueue.isEmpty()) { + millisPast = 0; + } + SimpleScheduledFuture<V> future = + new SimpleScheduledFuture<>(callable, millisPast + unit.toMillis(delay)); + executeQueue.add(future); + return future; + } + + @Override + public ScheduledFuture<?> scheduleAtFixedRate( + Runnable command, long initialDelay, long period, TimeUnit unit) { + throw new UnsupportedOperationException(); + } + + @Override + public ScheduledFuture<?> scheduleWithFixedDelay( + Runnable command, long initialDelay, long delay, TimeUnit unit) { + throw new UnsupportedOperationException(); + } + + @Override + public void shutdown() { + throw new UnsupportedOperationException(); + } + + @Override + public List<Runnable> shutdownNow() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isShutdown() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isTerminated() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) { + throw new UnsupportedOperationException(); + } + + @Override + public void execute(Runnable command) { + command.run(); + } + + public void advanceTimeBy(long timeout, TimeUnit unit) throws Exception { + advanceTimeByMillis(unit.toMillis(timeout)); + } + + private void advanceTimeByMillis(long timeoutMillis) throws Exception { + millisPast += timeoutMillis; + while (!executeQueue.isEmpty() && executeQueue.peek().getDelay(MILLISECONDS) <= millisPast) { + executeQueue.remove().complete(); + } + } + + private static class SimpleScheduledFuture<T> implements ScheduledFuture<T> { + + private final Callable<T> callable; + private final long timeoutMillis; + private boolean isCancelled = false; + private boolean isDone = false; + private T value; + + public SimpleScheduledFuture(Callable<T> callable, long timeoutMillis) { + this.callable = callable; + this.timeoutMillis = timeoutMillis; + } + + public void complete() throws Exception { + if (isDone || isCancelled()) { + return; + } + + isDone = true; + value = callable.call(); + } + + @Override + public long getDelay(TimeUnit unit) { + return unit.convert(timeoutMillis, MILLISECONDS); + } + + @Override + public int compareTo(Delayed o) { + return 0; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + isCancelled = true; + return true; + } + + @Override + public boolean isCancelled() { + return isCancelled; + } + + @Override + public boolean isDone() { + return isDone; + } + + @Override + public T get() { + if (!isDone) { + throw new IllegalStateException("Not executed yet"); + } + return value; + } + + @Override + public T get(long timeout, TimeUnit unit) { + if (!isDone) { + throw new IllegalStateException("Not executed yet"); + } + return value; + } + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/TestService.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/TestService.java new file mode 100644 index 0000000..cb11f3d --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/TestService.java @@ -0,0 +1,102 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import android.os.Parcel; +import android.os.RemoteException; +import com.google.android.enterprise.connectedapps.internal.ByteUtilities; +import com.google.auto.value.AutoValue; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class TestService extends ICrossProfileService.Stub { + + @AutoValue + abstract static class LoggedCrossProfileMethodCall { + abstract long getCrossProfileTypeIdentifier(); + + abstract long getMethodIdentifier(); + + abstract Parcel getParams(); + + @Nullable + abstract ICrossProfileCallback callback(); + + static LoggedCrossProfileMethodCall create( + long crossProfileTypeIdentifier, + long methodIdentifier, + Parcel params, + ICrossProfileCallback callback) { + return new AutoValue_TestService_LoggedCrossProfileMethodCall( + crossProfileTypeIdentifier, methodIdentifier, params, callback); + } + } + + private LoggedCrossProfileMethodCall lastCall; + private Parcel responseParcel = Parcel.obtain(); // Recycled in #setResponseParcel + + LoggedCrossProfileMethodCall lastCall() { + return lastCall; + } + + /** + * Set the parcel to be returned from a call to this service. + * + * <p>The previously set parcel will be recycled. + */ + void setResponseParcel(Parcel responseParcel) { + this.responseParcel.recycle(); + this.responseParcel = responseParcel; + } + + @Override + public void prepareCall(long callId, int blockId, int numBytes, byte[] paramsBytes) {} + + @Override + public byte[] call( + long callId, + int blockId, + long crossProfileTypeIdentifier, + int methodIdentifier, + byte[] paramsBytes, + ICrossProfileCallback callback) + throws RemoteException { + + Parcel parcel = Parcel.obtain(); // Recycled by this method on next call + parcel.unmarshall(paramsBytes, 0, paramsBytes.length); + parcel.setDataPosition(0); + + if (lastCall != null) { + lastCall.getParams().recycle(); + } + + lastCall = + LoggedCrossProfileMethodCall.create( + crossProfileTypeIdentifier, methodIdentifier, parcel, callback); + + byte[] parcelBytes = responseParcel.marshall(); + return prepareResponse(parcelBytes); + } + + private static byte[] prepareResponse(byte[] parcelBytes) { + // This doesn't deal with large responses. + return ByteUtilities.joinByteArrays(new byte[] {0}, parcelBytes); + } + + @Override + public byte[] fetchResponse(long callId, int blockId) { + return null; + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/TestStringCrossProfileCallback.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/TestStringCrossProfileCallback.java new file mode 100644 index 0000000..a88c222 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/TestStringCrossProfileCallback.java @@ -0,0 +1,57 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import android.os.IBinder; +import android.os.Parcel; +import android.os.RemoteException; +import com.google.android.enterprise.connectedapps.internal.ParcelUtilities; + +public class TestStringCrossProfileCallback implements ICrossProfileCallback { + + public int lastReceivedMethodIdentifier = -1; + public String lastReceivedMethodParam; + public Throwable lastReceivedException; + + @Override + public void prepareResult(long callId, int blockId, int numBytes, byte[] params) {} + + @Override + public void onResult(long callId, int blockId, int methodIdentifier, byte[] paramsBytes) + throws RemoteException { + lastReceivedMethodIdentifier = methodIdentifier; + Parcel parcel = Parcel.obtain(); // Recycled in this method + parcel.unmarshall(paramsBytes, 0, paramsBytes.length); + parcel.setDataPosition(0); + lastReceivedMethodParam = parcel.readString(); + parcel.recycle(); + } + + @Override + public void onException(long callId, int blockId, byte[] paramsBytes) throws RemoteException { + Parcel parcel = Parcel.obtain(); // Recycled in this method + parcel.unmarshall(paramsBytes, 0, paramsBytes.length); + parcel.setDataPosition(0); + + lastReceivedException = ParcelUtilities.readThrowableFromParcel(parcel); + parcel.recycle(); + } + + @Override + public IBinder asBinder() { + return null; + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/internal/ByteUtilitiesTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/internal/ByteUtilitiesTest.java new file mode 100644 index 0000000..4e0774e --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/internal/ByteUtilitiesTest.java @@ -0,0 +1,50 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.internal; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Build.VERSION_CODES; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class ByteUtilitiesTest { + + private static final byte[] FIRST_ARRAY = new byte[] {1, 2}; + private static final byte[] SECOND_ARRAY = new byte[] {3, 4, 5}; + + @Test + public void joinByteArrays_resultIsMerged() { + // This is the merge of FIRST_ARRAY and SECOND_ARRAY + byte[] expectedResult = new byte[] {1, 2, 3, 4, 5}; + + assertThat(ByteUtilities.joinByteArrays(FIRST_ARRAY, SECOND_ARRAY)).isEqualTo(expectedResult); + } + + @Test + public void joinByteArrays_emptyFirstArray_equalsSecondArray() { + assertThat(ByteUtilities.joinByteArrays(new byte[0], SECOND_ARRAY)).isEqualTo(SECOND_ARRAY); + } + + @Test + public void joinByteArrays_emptySecondArray_equalsFirstArray() { + assertThat(ByteUtilities.joinByteArrays(FIRST_ARRAY, new byte[0])).isEqualTo(FIRST_ARRAY); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/internal/CrossProfileCallbackMultiMergerTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/internal/CrossProfileCallbackMultiMergerTest.java new file mode 100644 index 0000000..b7dbc95 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/internal/CrossProfileCallbackMultiMergerTest.java @@ -0,0 +1,197 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.internal; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Build.VERSION_CODES; +import com.google.android.enterprise.connectedapps.Profile; +import com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger.CrossProfileCallbackMultiMergerCompleteListener; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class CrossProfileCallbackMultiMergerTest { + + static class TestStringListener + implements CrossProfileCallbackMultiMergerCompleteListener<String> { + int timesResultsPosted = 0; + Map<Profile, String> results; + + @Override + public void onResult(Map<Profile, String> results) { + timesResultsPosted++; + this.results = results; + } + } + + private final Profile profile0 = Profile.fromInt(0); + private final Profile profile1 = Profile.fromInt(1); + private final Profile profile2 = Profile.fromInt(2); + private static final String STRING = "String"; + + private final TestStringListener stringListener = new TestStringListener(); + + @Test + public void onResult_expectedResultsNotReached_doesNotReportResult() { + int expectedResults = 2; + CrossProfileCallbackMultiMerger<String> merger = + new CrossProfileCallbackMultiMerger<>(expectedResults, stringListener); + + merger.onResult(profile0, STRING); + + assertThat(stringListener.timesResultsPosted).isEqualTo(0); + } + + @Test + public void onResult_expectedResultsReached_doesReportResult() { + int expectedResults = 2; + CrossProfileCallbackMultiMerger<String> merger = + new CrossProfileCallbackMultiMerger<>(expectedResults, stringListener); + merger.onResult(profile0, STRING); + + merger.onResult(profile1, STRING); + + assertThat(stringListener.timesResultsPosted).isEqualTo(1); + } + + @Test + public void onResult_reportsCorrectResults() { + int expectedResults = 2; + CrossProfileCallbackMultiMerger<String> merger = + new CrossProfileCallbackMultiMerger<>(expectedResults, stringListener); + merger.onResult(profile0, STRING); + + merger.onResult(profile1, STRING); + + assertThat(stringListener.results.get(profile0)).isEqualTo(STRING); + assertThat(stringListener.results.get(profile1)).isEqualTo(STRING); + } + + @Test + public void onResult_sameProfileReportsMultipleTimes_ignored() { + int expectedResults = 2; + CrossProfileCallbackMultiMerger<String> merger = + new CrossProfileCallbackMultiMerger<>(expectedResults, stringListener); + merger.onResult(profile0, STRING); + + merger.onResult(profile0, STRING); + + assertThat(stringListener.timesResultsPosted).isEqualTo(0); + } + + @Test + public void onResult_newResult_resultAlreadyReported_ignored() { + int expectedResults = 2; + CrossProfileCallbackMultiMerger<String> merger = + new CrossProfileCallbackMultiMerger<>(expectedResults, stringListener); + merger.onResult(profile0, STRING); + merger.onResult(profile1, STRING); + + merger.onResult(profile2, STRING); + + assertThat(stringListener.timesResultsPosted).isEqualTo(1); + assertThat(stringListener.results).doesNotContainKey(profile2); + } + + @Test + public void onResult_previousResultMissing_expectedResultsReached_doesReportResult() { + int expectedResults = 2; + CrossProfileCallbackMultiMerger<String> merger = + new CrossProfileCallbackMultiMerger<>(expectedResults, stringListener); + merger.missingResult(profile0); + + merger.onResult(profile1, STRING); + + assertThat(stringListener.timesResultsPosted).isEqualTo(1); + } + + @Test + public void missingResult_allResultsMissing_doesReportResult() { + int expectedResults = 2; + CrossProfileCallbackMultiMerger<String> merger = + new CrossProfileCallbackMultiMerger<>(expectedResults, stringListener); + merger.missingResult(profile0); + + merger.missingResult(profile1); + + assertThat(stringListener.timesResultsPosted).isEqualTo(1); + } + + @Test + public void missingResult_expectedResultsReached_doesReportResult() { + int expectedResults = 2; + CrossProfileCallbackMultiMerger<String> merger = + new CrossProfileCallbackMultiMerger<>(expectedResults, stringListener); + merger.onResult(profile1, STRING); + + merger.missingResult(profile0); + + assertThat(stringListener.timesResultsPosted).isEqualTo(1); + } + + @Test + public void missingResult_expectedResultsNotReached_doesNotReportResult() { + int expectedResults = 2; + CrossProfileCallbackMultiMerger<String> merger = + new CrossProfileCallbackMultiMerger<>(expectedResults, stringListener); + + merger.missingResult(profile0); + + assertThat(stringListener.timesResultsPosted).isEqualTo(0); + } + + @Test + public void missingResult_resultAlreadyPosted_doesNotRecord() { + int expectedResults = 2; + CrossProfileCallbackMultiMerger<String> merger = + new CrossProfileCallbackMultiMerger<>(expectedResults, stringListener); + merger.onResult(profile0, STRING); + + merger.missingResult(profile0); + merger.onResult(profile1, STRING); + + assertThat(stringListener.results.get(profile0)).isEqualTo(STRING); + } + + @Test + public void onResult_resultAlreadyPosted_doesNotRecord() { + int expectedResults = 2; + CrossProfileCallbackMultiMerger<String> merger = + new CrossProfileCallbackMultiMerger<>(expectedResults, stringListener); + merger.missingResult(profile0); + + merger.onResult(profile0, STRING); + merger.onResult(profile1, STRING); + + assertThat(stringListener.results).doesNotContainKey(profile0); + } + + @Test + public void construct_noExpectedResults_reportsResultImmediately() { + int expectedResults = 0; + + CrossProfileCallbackMultiMerger<String> ignoredMerger = + new CrossProfileCallbackMultiMerger<>(expectedResults, stringListener); + + assertThat(stringListener.timesResultsPosted).isEqualTo(1); + assertThat(stringListener.results).isEmpty(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/internal/ParcelCallSenderTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/internal/ParcelCallSenderTest.java new file mode 100644 index 0000000..dd39fa1 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/internal/ParcelCallSenderTest.java @@ -0,0 +1,125 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.internal; + +import static com.google.android.enterprise.connectedapps.StringUtilities.randomString; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import android.os.Parcel; +import android.os.RemoteException; +import android.os.TransactionTooLargeException; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class ParcelCallSenderTest { + + static class TestParcelCallSender extends ParcelCallSender { + + int failPrepareCalls = 0; + int failCalls = 0; + int failFetchResponse = 0; + + private final ParcelCallReceiver parcelCallReceiver = new ParcelCallReceiver(); + + @Override + void prepareCall(long callId, int blockId, int totalBytes, byte[] bytes) + throws RemoteException { + if (failPrepareCalls-- > 0) { + throw new TransactionTooLargeException(); + } + + parcelCallReceiver.prepareCall(callId, blockId, totalBytes, bytes); + } + + @Override + byte[] call(long callId, int blockId, byte[] bytes) throws RemoteException { + if (failCalls-- > 0) { + throw new TransactionTooLargeException(); + } + + return parcelCallReceiver.prepareResponse( + callId, parcelCallReceiver.getPreparedCall(callId, blockId, bytes)); + } + + @Override + byte[] fetchResponse(long callId, int blockId) throws RemoteException { + if (failFetchResponse-- > 0) { + throw new TransactionTooLargeException(); + } + + return parcelCallReceiver.getPreparedResponse(callId, blockId); + } + } + + private final TestParcelCallSender parcelCallSender = new TestParcelCallSender(); + private static final String LARGE_STRING = randomString(1500000); // 3Mb + private static final Parcel LARGE_PARCEL = Parcel.obtain(); + + static { + LARGE_PARCEL.writeString(LARGE_STRING); + } + + @Test + public void makeParcelCall_prepareCallHasError_retriesUntilSuccess() + throws UnavailableProfileException { + parcelCallSender.failPrepareCalls = 5; + + assertThat(parcelCallSender.makeParcelCall(LARGE_PARCEL).readString()).isEqualTo(LARGE_STRING); + } + + @Test + public void makeParcelCall_prepareCallHasError_failsAfter10Retries() { + parcelCallSender.failPrepareCalls = 11; + + assertThrows( + UnavailableProfileException.class, () -> parcelCallSender.makeParcelCall(LARGE_PARCEL)); + } + + @Test + public void makeParcelCall_callHasError_retriesUntilSuccess() throws UnavailableProfileException { + parcelCallSender.failCalls = 5; + + assertThat(parcelCallSender.makeParcelCall(LARGE_PARCEL).readString()).isEqualTo(LARGE_STRING); + } + + @Test + public void makeParcelCall_callHasError_failsAfter10Retries() { + parcelCallSender.failCalls = 11; + + assertThrows( + UnavailableProfileException.class, () -> parcelCallSender.makeParcelCall(LARGE_PARCEL)); + } + + @Test + public void makeParcelCall_fetchResponseHasError_retriesUntilSuccess() + throws UnavailableProfileException { + parcelCallSender.failFetchResponse = 5; + + assertThat(parcelCallSender.makeParcelCall(LARGE_PARCEL).readString()).isEqualTo(LARGE_STRING); + } + + @Test + public void makeParcelCall_fetchResponseHasError_failsAfter10Retries() { + parcelCallSender.failFetchResponse = 11; + + assertThrows( + UnavailableProfileException.class, () -> parcelCallSender.makeParcelCall(LARGE_PARCEL)); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/AutomaticConnectionManagementTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/AutomaticConnectionManagementTest.java new file mode 100644 index 0000000..3f43c16 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/AutomaticConnectionManagementTest.java @@ -0,0 +1,239 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.assertFutureDoesNotHaveException; +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.assertFutureHasException; +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestExceptionCallbackListener; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.TestVoidCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.common.util.concurrent.ListenableFuture; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import org.robolectric.annotation.LooperMode; + +@LooperMode(LooperMode.Mode.LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class AutomaticConnectionManagementTest { + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestVoidCallbackListenerImpl testVoidCallbackListener = + new TestVoidCallbackListenerImpl(); + private final TestExceptionCallbackListener testExceptionCallbackListener = + new TestExceptionCallbackListener(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testProfileConnector.stopManualConnectionManagement(); + } + + @Test + public void lessThanThirtySecondsWithNoCalls_doesNotDisconnect() { + profileTestCrossProfileType + .other() + .asyncVoidMethod(testVoidCallbackListener, testExceptionCallbackListener); + + testUtilities.advanceTimeBySeconds(29); + + assertThat(testProfileConnector.isConnected()).isTrue(); + } + + @Test + public void thirtySecondsWithNoCalls_disconnects() { + profileTestCrossProfileType + .other() + .asyncVoidMethod(testVoidCallbackListener, testExceptionCallbackListener); + + testUtilities.advanceTimeBySeconds(31); + + assertThat(testProfileConnector.isConnected()).isFalse(); + } + + @Test + public void moreThanThirtySecondsWithNoCalls_manualManagementStarted_doesNotDisconnect() { + profileTestCrossProfileType + .other() + .asyncVoidMethod(testVoidCallbackListener, testExceptionCallbackListener); + testUtilities.advanceTimeBySeconds(29); + testUtilities.startConnectingAndWait(); + + testUtilities.advanceTimeBySeconds(31); + + assertThat(testProfileConnector.isConnected()).isTrue(); + } + + @Test + public void lessThanThirtySecondsWithNoCalls_previousCallsWereChained_doesNotDisconnect() { + profileTestCrossProfileType + .other() + .asyncVoidMethod(testVoidCallbackListener, testExceptionCallbackListener); + testUtilities.advanceTimeBySeconds(29); + profileTestCrossProfileType + .other() + .asyncVoidMethod(testVoidCallbackListener, testExceptionCallbackListener); + + testUtilities.advanceTimeBySeconds(29); + + assertThat(testProfileConnector.isConnected()).isTrue(); + } + + @Test + public void thirtySecondsWithNoCalls_previousCallsWereChained_disconnects() { + profileTestCrossProfileType + .other() + .asyncVoidMethod(testVoidCallbackListener, testExceptionCallbackListener); + testUtilities.advanceTimeBySeconds(29); + profileTestCrossProfileType + .other() + .asyncVoidMethod(testVoidCallbackListener, testExceptionCallbackListener); + + testUtilities.advanceTimeBySeconds(31); + + assertThat(testProfileConnector.isConnected()).isFalse(); + } + + @Test + public void callWhichTakesALongTime_doesNotDisconnectDuringCall() { + profileTestCrossProfileType + .other() + .asyncVoidMethodWithNonBlockingDelayWith50SecondTimeout( + testVoidCallbackListener, /* secondsDelay= */ 40, testExceptionCallbackListener); + + testUtilities.advanceTimeBySeconds(31); + + assertThat(testProfileConnector.isConnected()).isTrue(); + } + + @Test + public void lessThanThirtySecondsAfterCallWhichTakesALongTime_doesNotDisconnect() { + profileTestCrossProfileType + .other() + .asyncVoidMethodWithNonBlockingDelayWith50SecondTimeout( + testVoidCallbackListener, /* secondsDelay= */ 40, testExceptionCallbackListener); + + testUtilities.advanceTimeBySeconds(69); + + assertThat(testProfileConnector.isConnected()).isTrue(); + } + + @Test + public void thirtySecondsAfterCallWhichTakesALongTime_disconnects() { + profileTestCrossProfileType + .other() + .asyncVoidMethodWithNonBlockingDelayWith50SecondTimeout( + testVoidCallbackListener, /* secondsDelay= */ 40, testExceptionCallbackListener); + + testUtilities.advanceTimeBySeconds(70); + + assertThat(testProfileConnector.isConnected()).isFalse(); + } + + @Test + public void newCall_afterDisconnection_reconnects() { + profileTestCrossProfileType + .other() + .asyncVoidMethod(testVoidCallbackListener, testExceptionCallbackListener); + testUtilities.advanceTimeBySeconds(31); + + profileTestCrossProfileType + .other() + .asyncVoidMethod(testVoidCallbackListener, testExceptionCallbackListener); + testUtilities.advanceTimeBySeconds(1); + + assertThat(testProfileConnector.isConnected()).isTrue(); + } + + @Test + public void stopManualConnectionManagement_lessThan30SecondsLater_doesNotDisconnect() { + testUtilities.startConnectingAndWait(); + testProfileConnector.stopManualConnectionManagement(); + + testUtilities.advanceTimeBySeconds(29); + + assertThat(testProfileConnector.isConnected()).isTrue(); + } + + @Test + public void stopManualConnectionManagement_moreThan30SecondsLater_disconnects() { + testUtilities.startConnectingAndWait(); + testProfileConnector.stopManualConnectionManagement(); + + testUtilities.advanceTimeBySeconds(29); + + assertThat(testProfileConnector.isConnected()).isTrue(); + } + + @Test + public void asyncCall_doesNotHavePermission_failsImmediately() { + testUtilities.denyPermissions(INTERACT_ACROSS_USERS); + + ListenableFuture<Void> future = + profileTestCrossProfileType.other().listenableFutureVoidMethod(); + testUtilities.advanceTimeBySeconds(1); + + assertFutureHasException(future, UnavailableProfileException.class); + } + + @Test + public void asyncCall_getsPermissionAfterPreviousFailure_doesNotFail() { + testUtilities.denyPermissions(INTERACT_ACROSS_USERS); + ListenableFuture<Void> unusedFuture = + profileTestCrossProfileType.other().listenableFutureVoidMethod(); + testUtilities.advanceTimeBySeconds(5); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + + ListenableFuture<Void> future = + profileTestCrossProfileType.other().listenableFutureVoidMethod(); + + assertFutureDoesNotHaveException(future, UnavailableProfileException.class); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/AvailabilityListenerTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/AvailabilityListenerTest.java new file mode 100644 index 0000000..d999d22 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/AvailabilityListenerTest.java @@ -0,0 +1,112 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestAvailabilityListener; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.concurrent.ExecutionException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class AvailabilityListenerTest { + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final ProfileTestCrossProfileType type = + ProfileTestCrossProfileType.create(testProfileConnector); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testProfileConnector.stopManualConnectionManagement(); + } + + @Test + public void successfulCall_availabilityListenerDoesNotFire() + throws InterruptedException, ExecutionException { + testUtilities.turnOnWorkProfile(); + TestAvailabilityListener availabilityListener = new TestAvailabilityListener(); + testProfileConnector.registerAvailabilityListener(availabilityListener); + + ListenableFuture<Void> unusedFuture = type.other().listenableFutureVoidMethod(); + testUtilities.advanceTimeBySeconds(1); + unusedFuture.get(); + + assertThat(availabilityListener.availabilityChangedCount()).isEqualTo(0); + assertThat(testProfileConnector.isAvailable()).isTrue(); + } + + @Test + public void temporaryConnectionError_inProgressCall_availabilityListenerFires() { + testUtilities.turnOnWorkProfile(); + TestAvailabilityListener availabilityListener = new TestAvailabilityListener(); + testProfileConnector.registerAvailabilityListener(availabilityListener); + + ListenableFuture<Void> unusedFuture = + type.other().listenableFutureVoidMethodWithNonBlockingDelay(/* secondsDelay= */ 5); + testUtilities.simulateDisconnectingServiceConnection(); + testUtilities.advanceTimeBySeconds(1); + + assertThat(availabilityListener.availabilityChangedCount()).isGreaterThan(0); + assertThat(testProfileConnector.isAvailable()).isTrue(); + } + + @Test + public void temporaryConnectionError_noInProgressCall_availabilityListenerDoesNotFire() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + TestAvailabilityListener availabilityListener = new TestAvailabilityListener(); + testProfileConnector.registerAvailabilityListener(availabilityListener); + + testUtilities.simulateDisconnectingServiceConnection(); + testUtilities.advanceTimeBySeconds(1); + + assertThat(availabilityListener.availabilityChangedCount()).isEqualTo(0); + assertThat(testProfileConnector.isAvailable()).isTrue(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesAsyncTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesAsyncTest.java new file mode 100644 index 0000000..cb89aad --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesAsyncTest.java @@ -0,0 +1,223 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.Profile; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestBooleanCallbackListenerMultiImpl; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.TestStringCallbackListenerMultiImpl; +import com.google.android.enterprise.connectedapps.TestVoidCallbackListenerMultiImpl; +import com.google.android.enterprise.connectedapps.testapp.CustomRuntimeException; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.TestCrossProfileType; +import java.util.concurrent.ExecutionException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +@LooperMode(LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class BothProfilesAsyncTest { + private static final String STRING = "String"; + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + + private final Profile currentProfileIdentifier = testProfileConnector.utils().getCurrentProfile(); + private final Profile otherProfileIdentifier = testProfileConnector.utils().getOtherProfile(); + + private final TestVoidCallbackListenerMultiImpl voidCallback = + new TestVoidCallbackListenerMultiImpl(); + private final TestStringCallbackListenerMultiImpl stringCallback = + new TestStringCallbackListenerMultiImpl(); + private final TestBooleanCallbackListenerMultiImpl booleanCallback = + new TestBooleanCallbackListenerMultiImpl(); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testProfileConnector.stopManualConnectionManagement(); + } + + @Test + public void both_async_canBind_calledOnBothProfiles() + throws ExecutionException, InterruptedException { + testUtilities.turnOnWorkProfile(); + + profileTestCrossProfileType.both().asyncVoidMethod(voidCallback); + + // This calls on the same profile because of robolectric + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(2); + } + + @Test + public void both_async_canBind_resultContainsBothProfilesResults() + throws ExecutionException, InterruptedException { + testUtilities.turnOnWorkProfile(); + + profileTestCrossProfileType.both().asyncIdentityStringMethod(STRING, stringCallback); + + assertThat(stringCallback.stringCallbackValues.get(currentProfileIdentifier)).isEqualTo(STRING); + assertThat(stringCallback.stringCallbackValues.get(otherProfileIdentifier)).isEqualTo(STRING); + } + + @Test // This behaviour is expected right now but will change + public void both_async_blockingMethod_blocks() { + testUtilities.turnOnWorkProfile(); + + profileTestCrossProfileType + .both() + .asyncVoidMethodWithDelay(voidCallback, /* secondsDelay= */ 5); + + assertThat(voidCallback.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void both_async_nonblockingMethod_doesNotBlock() { + testUtilities.turnOnWorkProfile(); + + profileTestCrossProfileType + .both() + .asyncVoidMethodWithNonBlockingDelay(voidCallback, /* secondsDelay= */ 5); + + assertThat(voidCallback.callbackMethodCalls).isEqualTo(0); + } + + @Test + public void both_async_nonblockingMethod_doesCallback() { + testUtilities.turnOnWorkProfile(); + + profileTestCrossProfileType + .both() + .asyncVoidMethodWithNonBlockingDelay(voidCallback, /* secondsDelay= */ 5); + testUtilities.advanceTimeBySeconds(10); + + assertThat(voidCallback.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void both_async_canNotBind_calledOnOnlyCurrentProfile() { + testUtilities.turnOffWorkProfile(); + profileTestCrossProfileType.both().asyncVoidMethod(voidCallback); + + // This calls on the same profile because of robolectric + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void both_async_canNotBind_resultContainsOnlyCurrentProfilesResult() { + testUtilities.turnOffWorkProfile(); + + profileTestCrossProfileType.both().asyncIdentityStringMethod(STRING, stringCallback); + + assertThat(stringCallback.stringCallbackValues.get(currentProfileIdentifier)).isEqualTo(STRING); + assertThat(stringCallback.stringCallbackValues.get(otherProfileIdentifier)).isEqualTo(null); + } + + @Test + public void both_async_connectionDropsDuringCall_resultContainsOnlyCurrentProfilesResult() { + testUtilities.turnOnWorkProfile(); + profileTestCrossProfileType + .both() + .asyncIdentityStringMethodWithNonBlockingDelay( + STRING, stringCallback, /* secondsDelay= */ 5); + testUtilities.advanceTimeBySeconds(2); + + testUtilities.turnOffWorkProfile(); + testUtilities.advanceTimeBySeconds(3); + + assertThat(stringCallback.stringCallbackValues).containsKey(currentProfileIdentifier); + assertThat(stringCallback.stringCallbackValues).doesNotContainKey(otherProfileIdentifier); + } + + @Test + public void both_async_timeoutSet_doesTimeout() { + profileTestCrossProfileType + .both() + .asyncIdentityStringMethodWithNonBlockingDelayWith3SecondTimeout( + STRING, stringCallback, /* secondsDelay= */ 5); + + testUtilities.advanceTimeBySeconds(6); + + assertThat(stringCallback.stringCallbackValues.get(currentProfileIdentifier)).isEqualTo(STRING); + assertThat(stringCallback.stringCallbackValues).doesNotContainKey(otherProfileIdentifier); + } + + @Test + public void both_async_timeoutSetByCaller_doesTimeout() { + profileTestCrossProfileType + .both() + .timeout(3000) + .asyncIdentityStringMethodWithNonBlockingDelay( + STRING, stringCallback, /* secondsDelay= */ 5); + + testUtilities.advanceTimeBySeconds(6); + + assertThat(stringCallback.stringCallbackValues.get(currentProfileIdentifier)).isEqualTo(STRING); + assertThat(stringCallback.stringCallbackValues).doesNotContainKey(otherProfileIdentifier); + } + + @Test + public void both_async_throwsRuntimeException_exceptionThrownOnCurrentProfileIsThrown() { + assertThrows( + CustomRuntimeException.class, + () -> + profileTestCrossProfileType + .both() + .asyncStringMethodWhichThrowsRuntimeException(stringCallback)); + } + + @Test + public void both_async_contextArgument_works() { + profileTestCrossProfileType.both().asyncIsContextArgumentPassed(booleanCallback); + + assertThat(booleanCallback.booleanCallbackValues.get(currentProfileIdentifier)).isTrue(); + assertThat(booleanCallback.booleanCallbackValues.get(otherProfileIdentifier)).isTrue(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesListenableFutureTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesListenableFutureTest.java new file mode 100644 index 0000000..78b70c6 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesListenableFutureTest.java @@ -0,0 +1,278 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.Profile; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.testapp.CustomRuntimeException; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.TestCrossProfileType; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +@LooperMode(LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class BothProfilesListenableFutureTest { + private static final String STRING = "String"; + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + + private final Profile currentProfileIdentifier = testProfileConnector.utils().getCurrentProfile(); + private final Profile otherProfileIdentifier = testProfileConnector.utils().getOtherProfile(); + + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testProfileConnector.stopManualConnectionManagement(); + } + + @Test + public void both_listenableFuture_canBind_calledOnBothProfiles() + throws ExecutionException, InterruptedException { + testUtilities.turnOnWorkProfile(); + + profileTestCrossProfileType.both().listenableFutureVoidMethod().get(); + + // This calls on the same profile because of robolectric + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(2); + } + + @Test + public void both_listenableFuture_canBind_resultContainsBothProfilesResults() + throws ExecutionException, InterruptedException { + testUtilities.turnOnWorkProfile(); + + Map<Profile, String> results = + profileTestCrossProfileType.both().listenableFutureIdentityStringMethod(STRING).get(); + + assertThat(results.get(currentProfileIdentifier)).isEqualTo(STRING); + assertThat(results.get(otherProfileIdentifier)).isEqualTo(STRING); + } + + @Test // This behaviour is expected right now but will change + public void both_listenableFuture_blockingMethod_blocks() { + testUtilities.turnOnWorkProfile(); + + ListenableFuture<Map<Profile, Void>> future = + profileTestCrossProfileType + .both() + .listenableFutureVoidMethodWithDelay(/* secondsDelay= */ 5); + + assertThat(future.isDone()).isTrue(); + } + + @Test + public void both_listenableFuture_nonblockingMethod_doesNotBlock() { + testUtilities.turnOnWorkProfile(); + + ListenableFuture<Map<Profile, Void>> future = + profileTestCrossProfileType + .both() + .listenableFutureVoidMethodWithNonBlockingDelay(/* secondsDelay= */ 5); + + assertThat(future.isDone()).isFalse(); + } + + @Test + public void both_listenableFuture_nonblockingMethod_doesCallback() { + testUtilities.turnOnWorkProfile(); + + ListenableFuture<Map<Profile, Void>> future = + profileTestCrossProfileType + .both() + .listenableFutureVoidMethodWithNonBlockingDelay(/* secondsDelay= */ 5); + testUtilities.advanceTimeBySeconds(10); + + assertThat(future.isDone()).isTrue(); + } + + @Test + public void both_listenableFuture_canNotBind_calledOnOnlyCurrentProfile() + throws ExecutionException, InterruptedException { + testUtilities.turnOffWorkProfile(); + + profileTestCrossProfileType.both().listenableFutureVoidMethod().get(); + + // This calls on the same profile because of robolectric + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void both_listenableFuture_canNotBind_resultContainsOnlyCurrentProfilesResult() + throws ExecutionException, InterruptedException { + testUtilities.turnOffWorkProfile(); + + Map<Profile, String> results = + profileTestCrossProfileType.both().listenableFutureIdentityStringMethod(STRING).get(); + + assertThat(results.get(currentProfileIdentifier)).isEqualTo(STRING); + assertThat(results.get(otherProfileIdentifier)).isEqualTo(null); + } + + @Test + public void both_listenableFuture_isBound_becomesUnbound_calledOnBothProfiles() throws Exception { + testUtilities.turnOnWorkProfile(); + ListenableFuture<Map<Profile, Void>> unusedFuture = + profileTestCrossProfileType + .both() + .listenableFutureVoidMethodWithNonBlockingDelay(/* secondsDelay= */ 5); + + // Because of the way Robolectric currently works - the method is guaranteed to have executed + // before the work profile is turned off. This may change with later changes to the SDK so + // this test will be updated. + testUtilities.turnOffWorkProfile(); + + // This calls on the same profile because of robolectric + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(2); + } + + @Test + public void both_listenableFuture_isBound_becomesUnbound_callbackFires() { + testUtilities.turnOnWorkProfile(); + ListenableFuture<Map<Profile, Void>> future = + profileTestCrossProfileType + .both() + .listenableFutureVoidMethodWithNonBlockingDelay(/* secondsDelay= */ 5); + + testUtilities.turnOffWorkProfile(); + + assertThat(future.isDone()).isTrue(); + } + + @Test + public void both_listenableFuture_profilesWithExceptionsAreNotIncludedInResults() + throws ExecutionException, InterruptedException { + ListenableFuture<Map<Profile, Void>> future = + profileTestCrossProfileType + .both() + .listenableFutureVoidMethodWhichSetsIllegalStateException(); + + assertThat(future.get()).hasSize(0); + } + + @Test + public void + both_listenableFuture_connectionDropsDuringCall_resultContainsOnlyCurrentProfilesResult() + throws ExecutionException, InterruptedException { + ListenableFuture<Map<Profile, String>> future = + profileTestCrossProfileType + .both() + .listenableFutureIdentityStringMethodWithNonBlockingDelay( + STRING, /* secondsDelay= */ 5); + testUtilities.advanceTimeBySeconds(2); + + testUtilities.turnOffWorkProfile(); + + Map<Profile, String> results = future.get(); + + assertThat(results).containsKey(currentProfileIdentifier); + assertThat(results).doesNotContainKey(otherProfileIdentifier); + } + + @Test + public void both_listenableFuture_timeoutSet_doesTimeout() + throws ExecutionException, InterruptedException { + ListenableFuture<Map<Profile, String>> future = + profileTestCrossProfileType + .both() + .listenableFutureIdentityStringMethodWithNonBlockingDelayWith3SecondTimeout( + STRING, /* secondsDelay= */ 5); + + testUtilities.advanceTimeBySeconds(6); + + Map<Profile, String> results = future.get(); + assertThat(results.get(currentProfileIdentifier)).isEqualTo(STRING); + assertThat(results).doesNotContainKey(otherProfileIdentifier); + } + + @Test + public void both_listenableFuture_timeoutSetByCaller_doesTimeout() + throws ExecutionException, InterruptedException { + ListenableFuture<Map<Profile, String>> future = + profileTestCrossProfileType + .both() + .timeout(3000) + .listenableFutureIdentityStringMethodWithNonBlockingDelay( + STRING, /* secondsDelay= */ 5); + + testUtilities.advanceTimeBySeconds(6); + + Map<Profile, String> results = future.get(); + assertThat(results.get(currentProfileIdentifier)).isEqualTo(STRING); + assertThat(results).doesNotContainKey(otherProfileIdentifier); + } + + @Test + public void + both_listenableFuture_throwsRuntimeException_exceptionThrownOnCurrentProfileIsThrown() { + assertThrows( + CustomRuntimeException.class, + () -> + profileTestCrossProfileType + .both() + .listenableFutureVoidMethodWhichThrowsRuntimeException()); + } + + @Test + public void both_listenableFuture_contextArgument_works() throws Exception { + ListenableFuture<Map<Profile, Boolean>> resultFuture = + profileTestCrossProfileType.both().futureIsContextArgumentPassed(); + + Map<Profile, Boolean> result = resultFuture.get(); + + assertThat(result.get(currentProfileIdentifier)).isTrue(); + assertThat(result.get(otherProfileIdentifier)).isTrue(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesManualAsyncTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesManualAsyncTest.java new file mode 100644 index 0000000..66b7ef1 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesManualAsyncTest.java @@ -0,0 +1,212 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.Profile; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.TestStringCallbackListenerMultiImpl; +import com.google.android.enterprise.connectedapps.TestVoidCallbackListenerMultiImpl; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.TestCrossProfileType; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +@LooperMode(LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class BothProfilesManualAsyncTest { + private static final String STRING = "String"; + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + + private final Profile currentProfileIdentifier = testProfileConnector.utils().getCurrentProfile(); + private final Profile otherProfileIdentifier = testProfileConnector.utils().getOtherProfile(); + + private final TestStringCallbackListenerMultiImpl stringCallback = + new TestStringCallbackListenerMultiImpl(); + private final TestVoidCallbackListenerMultiImpl voidCallback = + new TestVoidCallbackListenerMultiImpl(); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testUtilities.startConnectingAndWait(); + } + + @Test + public void both_async_manualConnection_isBound_calledOnBothProfiles() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType.both().asyncVoidMethod(voidCallback); + + // This calls on the same profile because of robolectric + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(2); + } + + @Test + public void both_async_manualConnection_isBound_resultContainsBothProfilesResults() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType.both().asyncIdentityStringMethod(STRING, stringCallback); + + assertThat(stringCallback.stringCallbackValues.get(currentProfileIdentifier)).isEqualTo(STRING); + assertThat(stringCallback.stringCallbackValues.get(otherProfileIdentifier)).isEqualTo(STRING); + } + + @Test // This behaviour is expected right now but will change + public void both_async_manualConnection_isBound_blockingMethod_blocks() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType + .both() + .asyncVoidMethodWithDelay(voidCallback, /* secondsDelay= */ 5); + + assertThat(voidCallback.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void both_async_manualConnection_isBound_nonblockingMethod_doesNotBlock() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType + .both() + .asyncVoidMethodWithNonBlockingDelay(voidCallback, /* secondsDelay= */ 5); + + assertThat(voidCallback.callbackMethodCalls).isEqualTo(0); + } + + @Test + public void both_async_manualConnection_isBound_nonblockingMethod_doesCallback() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType + .both() + .asyncVoidMethodWithNonBlockingDelay(voidCallback, /* secondsDelay= */ 5); + testUtilities.advanceTimeBySeconds(10); + + assertThat(voidCallback.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void both_async_manualConnection_isNotBound_calledOnOnlyCurrentProfile() { + testUtilities.startConnectingAndWait(); + testUtilities.turnOffWorkProfile(); + + profileTestCrossProfileType.both().asyncVoidMethod(voidCallback); + + // This calls on the same profile because of robolectric + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + @Ignore // Will be supported when async methods are supported with exceptions + public void both_async_manualConnection_isNotBound_resultContainsOnlyCurrentProfilesResult() { + testUtilities.turnOffWorkProfile(); + + profileTestCrossProfileType.both().asyncIdentityStringMethod(STRING, stringCallback); + + assertThat(stringCallback.stringCallbackValues.get(currentProfileIdentifier)).isEqualTo(STRING); + assertThat(stringCallback.stringCallbackValues.get(otherProfileIdentifier)).isEqualTo(null); + } + + @Test + public void both_async_manualConnection_isBound_becomesUnbound_calledOnBothProfiles() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + profileTestCrossProfileType + .both() + .asyncVoidMethodWithNonBlockingDelay(voidCallback, /* secondsDelay= */ 5); + + // Because of the way Robolectric currently works - the method is guaranteed to have executed + // before the work profile is turned off. This may change with later changes to the SDK so + // this test will be updated. + testUtilities.turnOffWorkProfile(); + testUtilities.advanceTimeBySeconds(5); // Complete local call + + // This calls on the same profile because of robolectric + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(2); + } + + @Test + public void both_async_manualConnection_isBound_becomesUnbound_callbackFires() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + profileTestCrossProfileType + .both() + .asyncVoidMethodWithNonBlockingDelay(voidCallback, /* secondsDelay= */ 5); + + testUtilities.turnOffWorkProfile(); + testUtilities.advanceTimeBySeconds(5); // Complete local call + + assertThat(voidCallback.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void + both_async_manualConnection_connectionDropsDuringCall_resultContainsOnlyCurrentProfilesResult() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + profileTestCrossProfileType + .both() + .asyncIdentityStringMethodWithNonBlockingDelay( + STRING, stringCallback, /* secondsDelay= */ 5); + + testUtilities.turnOffWorkProfile(); + testUtilities.advanceTimeBySeconds(5); // Complete local call + + assertThat(stringCallback.stringCallbackValues).containsKey(currentProfileIdentifier); + assertThat(stringCallback.stringCallbackValues).doesNotContainKey(otherProfileIdentifier); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesManualListenableFutureTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesManualListenableFutureTest.java new file mode 100644 index 0000000..a3ca7eb --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesManualListenableFutureTest.java @@ -0,0 +1,234 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.Profile; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.TestCrossProfileType; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +@LooperMode(LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class BothProfilesManualListenableFutureTest { + private static final String STRING = "String"; + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final Profile currentProfileIdentifier = testProfileConnector.utils().getCurrentProfile(); + private final Profile otherProfileIdentifier = testProfileConnector.utils().getOtherProfile(); + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testUtilities.startConnectingAndWait(); + } + + @Test + public void both_listenableFuture_manualConnection_isBound_calledOnBothProfiles() + throws ExecutionException, InterruptedException { + testUtilities.startConnectingAndWait(); + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType.both().listenableFutureVoidMethod().get(); + + // This calls on the same profile because of robolectric + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(2); + } + + @Test + public void both_listenableFuture_manualConnection_isBound_resultContainsBothProfilesResults() + throws ExecutionException, InterruptedException { + testUtilities.startConnectingAndWait(); + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + Map<Profile, String> results = + profileTestCrossProfileType.both().listenableFutureIdentityStringMethod(STRING).get(); + + assertThat(results.get(currentProfileIdentifier)).isEqualTo(STRING); + assertThat(results.get(otherProfileIdentifier)).isEqualTo(STRING); + } + + @Test // This behaviour is expected right now but will change + public void both_listenableFuture_manualConnection_isBound_blockingMethod_blocks() { + testUtilities.startConnectingAndWait(); + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + ListenableFuture<Map<Profile, Void>> future = + profileTestCrossProfileType + .both() + .listenableFutureVoidMethodWithDelay(/* secondsDelay= */ 5); + + assertThat(future.isDone()).isTrue(); + } + + @Test + public void both_listenableFuture_manualConnection_isBound_nonblockingMethod_doesNotBlock() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + ListenableFuture<Map<Profile, Void>> future = + profileTestCrossProfileType + .both() + .listenableFutureVoidMethodWithNonBlockingDelay(/* secondsDelay= */ 5); + + assertThat(future.isDone()).isFalse(); + } + + @Test + public void both_listenableFuture_manualConnection_isBound_nonblockingMethod_doesCallback() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + ListenableFuture<Map<Profile, Void>> future = + profileTestCrossProfileType + .both() + .listenableFutureVoidMethodWithNonBlockingDelay(/* secondsDelay= */ 5); + testUtilities.advanceTimeBySeconds(10); + + assertThat(future.isDone()).isTrue(); + } + + @Test + public void both_listenableFuture_manualConnection_isNotBound_calledOnOnlyCurrentProfile() + throws ExecutionException, InterruptedException { + testUtilities.startConnectingAndWait(); + testUtilities.turnOffWorkProfile(); + + profileTestCrossProfileType.both().listenableFutureVoidMethod().get(); + + // This calls on the same profile because of robolectric + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void + both_listenableFuture_manualConnection_isNotBound_resultContainsOnlyCurrentProfilesResult() + throws ExecutionException, InterruptedException { + testUtilities.startConnectingAndWait(); + testUtilities.turnOffWorkProfile(); + + Map<Profile, String> results = + profileTestCrossProfileType.both().listenableFutureIdentityStringMethod(STRING).get(); + + assertThat(results.get(currentProfileIdentifier)).isEqualTo(STRING); + assertThat(results.get(otherProfileIdentifier)).isEqualTo(null); + } + + @Test + public void both_listenableFuture_manualConnection_isBound_becomesUnbound_calledOnBothProfiles() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + ListenableFuture<Map<Profile, Void>> unusedFuture = + profileTestCrossProfileType + .both() + .listenableFutureVoidMethodWithNonBlockingDelay(/* secondsDelay= */ 5); + + // Because of the way Robolectric currently works - the method is guaranteed to have executed + // before the work profile is turned off. This may change with later changes to the SDK so + // this test will be updated. + testUtilities.turnOffWorkProfile(); + + // This calls on the same profile because of robolectric + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(2); + } + + @Test + public void both_listenableFuture_manualConnection_isBound_becomesUnbound_callbackFires() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + ListenableFuture<Map<Profile, Void>> future = + profileTestCrossProfileType + .both() + .listenableFutureVoidMethodWithNonBlockingDelay(/* secondsDelay= */ 5); + + testUtilities.turnOffWorkProfile(); + + assertThat(future.isDone()).isTrue(); + } + + @Test + public void both_listenableFuture_manualConnection_profilesWithExceptionsAreNotIncludedInResults() + throws ExecutionException, InterruptedException { + testUtilities.startConnectingAndWait(); + ListenableFuture<Map<Profile, Void>> future = + profileTestCrossProfileType + .both() + .listenableFutureVoidMethodWhichSetsIllegalStateException(); + + assertThat(future.get()).isEmpty(); + } + + @Test + public void + both_listenableFuture_manualConnection_connectionDropsDuringCall_resultContainsOnlyCurrentProfilesResult() + throws ExecutionException, InterruptedException { + testUtilities.startConnectingAndWait(); + ListenableFuture<Map<Profile, String>> future = + profileTestCrossProfileType + .both() + .listenableFutureIdentityStringMethodWithNonBlockingDelay( + STRING, /* secondsDelay= */ 5); + testUtilities.advanceTimeBySeconds(2); + + testUtilities.turnOffWorkProfile(); + + Map<Profile, String> results = future.get(); + + assertThat(results).containsKey(currentProfileIdentifier); + assertThat(results).doesNotContainKey(otherProfileIdentifier); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesSynchronousTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesSynchronousTest.java new file mode 100644 index 0000000..20ee748 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesSynchronousTest.java @@ -0,0 +1,121 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; +import static junit.framework.TestCase.fail; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.Profile; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.exceptions.ProfileRuntimeException; +import com.google.android.enterprise.connectedapps.testapp.CustomRuntimeException; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +@LooperMode(LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class BothProfilesSynchronousTest { + + private static final String STRING = "String"; + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + + private final Profile currentProfileIdentifier = testProfileConnector.utils().getCurrentProfile(); + private final Profile otherProfileIdentifier = testProfileConnector.utils().getOtherProfile(); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testUtilities.startConnectingAndWait(); + } + + @Test + public void both_synchronous_isBound_resultContainsBothProfileResults() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + Map<Profile, String> result = profileTestCrossProfileType.both().identityStringMethod(STRING); + + assertThat(result.get(currentProfileIdentifier)).isEqualTo(STRING); + assertThat(result.get(otherProfileIdentifier)).isEqualTo(STRING); + } + + @Test + public void both_synchronous_isNotBound_resultOnlyContainsCurrentProfileResult() { + testUtilities.turnOffWorkProfile(); + + Map<Profile, String> result = profileTestCrossProfileType.both().identityStringMethod(STRING); + + assertThat(result.get(currentProfileIdentifier)).isEqualTo(STRING); + assertThat(result).doesNotContainKey(otherProfileIdentifier); + } + + @Test + public void both_synchronous_throwsRuntimeException_exceptionThrownOnCurrentProfileIsThrown() { + // Since the exception is thrown on both sides, which is thrown first is not deterministic. + // This test just confirms one of the two is thrown + try { + profileTestCrossProfileType.both().methodWhichThrowsRuntimeException(); + fail(); + } catch (CustomRuntimeException expected) { + + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void both_synchronous_contextArgument_works() { + Map<Profile, Boolean> result = profileTestCrossProfileType.both().isContextArgumentPassed(); + + assertThat(result.get(currentProfileIdentifier)).isTrue(); + assertThat(result.get(otherProfileIdentifier)).isTrue(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileCallbackReceiverTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileCallbackReceiverTest.java new file mode 100644 index 0000000..b4118b1 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileCallbackReceiverTest.java @@ -0,0 +1,51 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.enterprise.connectedapps.TestStringCrossProfileCallback; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.testapp.Profile_TestStringCallbackListener_Receiver; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType_Bundler; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class CrossProfileCallbackReceiverTest { + + private static final String STRING = "String"; + + private final TestStringCrossProfileCallback callback = new TestStringCrossProfileCallback(); + private final Bundler bundler = new ProfileTestCrossProfileType_Bundler(); + private final Profile_TestStringCallbackListener_Receiver receiver = + new Profile_TestStringCallbackListener_Receiver(callback, bundler); + + @Test + public void asyncCallbackListenerReceiver_calls() { + receiver.stringCallback(STRING); + + assertThat(callback.lastReceivedMethodIdentifier).isNotEqualTo(-1); // Has been called + } + + @Test + public void asyncCallbackListenerReceiver_bundlesParams() { + receiver.stringCallback(STRING); + + assertThat(callback.lastReceivedMethodParam).isEqualTo(STRING); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileCallbackSenderTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileCallbackSenderTest.java new file mode 100644 index 0000000..5bd46b2 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileCallbackSenderTest.java @@ -0,0 +1,67 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.enterprise.connectedapps.TestExceptionCallbackListener; +import com.google.android.enterprise.connectedapps.TestICrossProfileCallback; +import com.google.android.enterprise.connectedapps.TestStringCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.testapp.Profile_TestStringCallbackListener_Receiver; +import com.google.android.enterprise.connectedapps.testapp.Profile_TestStringCallbackListener_Sender; +import com.google.android.enterprise.connectedapps.testapp.TestStringCallbackListener; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType_Bundler; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** + * Test the generated _Sender class for {@link TestStringCallbackListener}. + * + * <p>This tests indirectly by creating a _Receiver instance (tested in {@link + * CrossProfileCallbackReceiverTest}) and confirming that calls are passed through. This is because + * the {@code methodIdentifier} is unpredictable. + */ +@RunWith(RobolectricTestRunner.class) +public class CrossProfileCallbackSenderTest { + + private static final String STRING = "String"; + + private final TestExceptionCallbackListener exceptionCallback = + new TestExceptionCallbackListener(); + private final TestStringCallbackListenerImpl callback = new TestStringCallbackListenerImpl(); + private final Bundler bundler = new ProfileTestCrossProfileType_Bundler(); + private final Profile_TestStringCallbackListener_Sender sender = + new Profile_TestStringCallbackListener_Sender(callback, exceptionCallback, bundler); + private final Profile_TestStringCallbackListener_Receiver receiver = + new Profile_TestStringCallbackListener_Receiver( + new TestICrossProfileCallback(sender), bundler); + + @Test + public void asyncCallbackSender_routesCalls() { + receiver.stringCallback(STRING); + + assertThat(callback.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void internalCallbackSender_unbundlesParams() { + receiver.stringCallback(STRING); + + assertThat(callback.stringCallbackValue).isEqualTo(STRING); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileInterfaceTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileInterfaceTest.java new file mode 100644 index 0000000..eb44ea0 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileInterfaceTest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.Profile; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileInterface; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +@LooperMode(LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class CrossProfileInterfaceTest { + private static final String STRING = "String"; + private static final List<String> listOfString = Collections.singletonList(STRING); + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final ProfileTestCrossProfileInterface profileTestCrossProfileInterface = + ProfileTestCrossProfileInterface.create(testProfileConnector); + + private final Profile currentProfileIdentifier = testProfileConnector.utils().getCurrentProfile(); + private final Profile otherProfileIdentifier = testProfileConnector.utils().getOtherProfile(); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testUtilities.startConnectingAndWait(); + } + + @Test + public void crossProfileInterface_both_getsBothResults() { + Map<Profile, List<String>> results = + profileTestCrossProfileInterface.both().identityListOfStringMethod(listOfString); + + assertThat(results.get(currentProfileIdentifier)).isEqualTo(listOfString); + assertThat(results.get(otherProfileIdentifier)).isEqualTo(listOfString); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileServiceTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileServiceTest.java new file mode 100644 index 0000000..f9af23b --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileServiceTest.java @@ -0,0 +1,112 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import static org.robolectric.Shadows.shadowOf; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.exceptions.ProfileRuntimeException; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; +import org.robolectric.shadows.ShadowBinder; + +@LooperMode(LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class CrossProfileServiceTest { + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + private static final String DIFFERENT_PACKAGE_NAME = "com.different.package"; + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testUtilities.startConnectingAndWait(); + } + + @After + public void tearDown() { + ShadowBinder.reset(); + } + + @Test + public void crossProfileMethodCall_doesNotThrowException() throws UnavailableProfileException { + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType.other().voidMethod(); + } + + @Test + public void crossProfileMethodCall_multiplePackagesForUid_doesNotThrowException() + throws UnavailableProfileException { + ShadowBinder.setCallingUid(10); + shadowOf(context.getPackageManager()) + .setPackagesForUid(10, DIFFERENT_PACKAGE_NAME, context.getPackageName()); + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType.other().voidMethod(); + } + + @Test + public void crossProfileMethodCall_callingFromInvalidPackage_throwsWrappedIllegalStateException() + throws UnavailableProfileException { + testUtilities.startConnectingAndWait(); + ShadowBinder.setCallingUid(10); + shadowOf(context.getPackageManager()).setPackagesForUid(10, DIFFERENT_PACKAGE_NAME); + + try { + profileTestCrossProfileType.other().voidMethod(); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(IllegalStateException.class); + } + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileTypeProfileTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileTypeProfileTest.java new file mode 100644 index 0000000..3adf174 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileTypeProfileTest.java @@ -0,0 +1,553 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; +import static junit.framework.TestCase.fail; +import static org.junit.Assert.assertThrows; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.CrossProfileConnector; +import com.google.android.enterprise.connectedapps.Profile; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestExceptionCallbackListener; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.TestStringCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.exceptions.ProfileRuntimeException; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.testapp.CustomRuntimeException; +import com.google.android.enterprise.connectedapps.testapp.TestStringCallbackListener; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileTypeWhichDoesNotSpecifyConnector; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +@LooperMode(LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class CrossProfileTypeProfileTest { + + private static final String STRING = "String"; + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + private final TestStringCallbackListener stringCallbackListener = + new TestStringCallbackListenerImpl(); + private final TestExceptionCallbackListener exceptionCallbackListener = + new TestExceptionCallbackListener(); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testUtilities.startConnectingAndWait(); + } + + @Test + public void profile_withIdentifierForCurrentProfile_runsOnCurrentProfile() + throws UnavailableProfileException { + testUtilities.turnOffWorkProfile(); + + Profile currentProfileIdentifier = testProfileConnector.utils().getCurrentProfile(); + + // If this runs on the other profile, an exception will be thrown. + profileTestCrossProfileType.profile(currentProfileIdentifier).voidMethod(); + } + + @Test + public void profile_withIdentifierForOtherProfile_runsOnOtherProfile() { + testUtilities.turnOffWorkProfile(); + + Profile otherProfileIdentifier = testProfileConnector.utils().getOtherProfile(); + + // If this runs on the other profile, an exception will be thrown. + assertThrows( + UnavailableProfileException.class, + () -> profileTestCrossProfileType.profile(otherProfileIdentifier).voidMethod()); + } + + @Test + public void work_calledFromWorkProfile_runsOnCurrentProfile() throws UnavailableProfileException { + testUtilities.turnOffWorkProfile(); + testUtilities.setRunningOnWorkProfile(); + + // If this runs on the other profile, an exception will be thrown. + profileTestCrossProfileType.work().voidMethod(); + } + + @Test + public void work_calledFromPersonalProfile_runsOnOtherProfile() { + testUtilities.turnOffWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + + // If this runs on the other profile, an exception will be thrown. + assertThrows( + UnavailableProfileException.class, () -> profileTestCrossProfileType.work().voidMethod()); + } + + @Test + public void personal_calledFromWorkProfile_runsOnOtherProfile() { + testUtilities.turnOffWorkProfile(); + testUtilities.setRunningOnWorkProfile(); + + // If this runs on the other profile, an exception will be thrown. + assertThrows( + UnavailableProfileException.class, + () -> profileTestCrossProfileType.personal().voidMethod()); + } + + @Test + public void personal_calledFromPersonalProfile_runsOnCurrentProfile() + throws UnavailableProfileException { + testUtilities.turnOffWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + + // If this runs on the other profile, an exception will be thrown. + profileTestCrossProfileType.personal().voidMethod(); + } + + @Test + public void primary_calledFromPrimaryProfile_runsOnCurrentProfile() + throws UnavailableProfileException { + // The primary profile is defined as work in TestProfileConnector + testUtilities.turnOffWorkProfile(); + testUtilities.setRunningOnWorkProfile(); + + // If this runs on the other profile, an exception will be thrown. + profileTestCrossProfileType.primary().voidMethod(); + } + + @Test + public void primary_calledFromSecondaryProfile_runsOnOtherProfile() + throws UnavailableProfileException { + // The primary profile is defined as work in TestProfileConnector + testUtilities.turnOffWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + + // If this runs on the other profile, an exception will be thrown. + assertThrows( + UnavailableProfileException.class, + () -> profileTestCrossProfileType.primary().voidMethod()); + } + + @Test + public void secondary_calledFromSecondaryProfile_runsOnCurrentProfile() + throws UnavailableProfileException { + // The primary profile is defined as work in TestProfileConnector + testUtilities.turnOffWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + + // If this runs on the other profile, an exception will be thrown. + profileTestCrossProfileType.secondary().voidMethod(); + } + + @Test + public void secondary_calledFromPrimaryProfile_runsOnOtherProfile() + throws UnavailableProfileException { + // The primary profile is defined as work in TestProfileConnector + testUtilities.turnOffWorkProfile(); + testUtilities.setRunningOnWorkProfile(); + + // If this runs on the other profile, an exception will be thrown. + assertThrows( + UnavailableProfileException.class, + () -> profileTestCrossProfileType.secondary().voidMethod()); + } + + @Test + public void primary_calledOnTypeWithoutConnector_connectorHasPrimary_works() + throws UnavailableProfileException { + ProfileTestCrossProfileTypeWhichDoesNotSpecifyConnector type = + ProfileTestCrossProfileTypeWhichDoesNotSpecifyConnector.create(testProfileConnector); + + assertThat(type.primary().identityStringMethod(STRING)).isEqualTo(STRING); + } + + @Test + public void secondary_calledOnTypeWithoutConnector_connectorHasPrimary_works() + throws UnavailableProfileException { + ProfileTestCrossProfileTypeWhichDoesNotSpecifyConnector type = + ProfileTestCrossProfileTypeWhichDoesNotSpecifyConnector.create(testProfileConnector); + + assertThat(type.secondary().identityStringMethod(STRING)).isEqualTo(STRING); + } + + @Test + public void suppliers_calledOnTypeWithoutConnector_connectorHasPrimary_works() { + ProfileTestCrossProfileTypeWhichDoesNotSpecifyConnector type = + ProfileTestCrossProfileTypeWhichDoesNotSpecifyConnector.create(testProfileConnector); + + assertThat(type.suppliers().identityStringMethod(STRING)).isNotEmpty(); + } + + @Test + public void + primary_calledOnTypeWithoutConnector_connectorDoesNotHavePrimary_throwsIllegalStateException() { + CrossProfileConnector crossProfileConnector = CrossProfileConnector.builder(context).build(); + ProfileTestCrossProfileTypeWhichDoesNotSpecifyConnector type = + ProfileTestCrossProfileTypeWhichDoesNotSpecifyConnector.create(crossProfileConnector); + + assertThrows(IllegalStateException.class, () -> type.primary().identityStringMethod(STRING)); + } + + @Test + public void + secondary_calledOnTypeWithoutConnector_connectorDoesNotHavePrimary_throwsIllegalStateException() { + CrossProfileConnector crossProfileConnector = CrossProfileConnector.builder(context).build(); + ProfileTestCrossProfileTypeWhichDoesNotSpecifyConnector type = + ProfileTestCrossProfileTypeWhichDoesNotSpecifyConnector.create(crossProfileConnector); + + assertThrows(IllegalStateException.class, () -> type.secondary().identityStringMethod(STRING)); + } + + @Test + public void + suppliers_calledOnTypeWithoutConnector_connectorDoesNotHavePrimary_throwsIllegalStateException() { + CrossProfileConnector crossProfileConnector = CrossProfileConnector.builder(context).build(); + ProfileTestCrossProfileTypeWhichDoesNotSpecifyConnector type = + ProfileTestCrossProfileTypeWhichDoesNotSpecifyConnector.create(crossProfileConnector); + + assertThrows(IllegalStateException.class, () -> type.suppliers().identityStringMethod(STRING)); + } + + @Test + public void personal_synchronous_runningOnWork_throwsException_exceptionIsWrapped() + throws UnavailableProfileException { + testUtilities.setRunningOnWorkProfile(); + + try { + profileTestCrossProfileType.personal().methodWhichThrowsRuntimeException(); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void work_synchronous_runningOnPersonal_throwsException_exceptionIsWrapped() + throws UnavailableProfileException { + testUtilities.setRunningOnPersonalProfile(); + + try { + profileTestCrossProfileType.work().methodWhichThrowsRuntimeException(); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void primary_synchronous_runningOnSecondaryProfile_throwsException_exceptionIsWrapped() + throws UnavailableProfileException { + testUtilities.setRunningOnPersonalProfile(); // Work profile is primary for TestProfileConnector + + try { + profileTestCrossProfileType.primary().methodWhichThrowsRuntimeException(); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void secondary_synchronous_runningOnPrimaryProfile_throwsException_exceptionIsWrapped() + throws UnavailableProfileException { + testUtilities.setRunningOnWorkProfile(); // Work profile is primary for TestProfileConnector + + try { + profileTestCrossProfileType.secondary().methodWhichThrowsRuntimeException(); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void personal_synchronous_runningOnPersonal_throwsException_exceptionIsNotWrapped() { + testUtilities.setRunningOnPersonalProfile(); + + assertThrows( + CustomRuntimeException.class, + () -> profileTestCrossProfileType.personal().methodWhichThrowsRuntimeException()); + } + + @Test + public void work_synchronous_runningOnWork_throwsException_exceptionIsNotWrapped() { + testUtilities.setRunningOnWorkProfile(); + + assertThrows( + CustomRuntimeException.class, + () -> profileTestCrossProfileType.work().methodWhichThrowsRuntimeException()); + } + + @Test + public void primary_synchronous_runningOnPrimaryProfile_throwsException_exceptionIsNotWrapped() { + testUtilities.setRunningOnWorkProfile(); // Work profile is primary for TestProfileConnector + + assertThrows( + CustomRuntimeException.class, + () -> profileTestCrossProfileType.primary().methodWhichThrowsRuntimeException()); + } + + @Test + public void + secondary_synchronous_runningOnSecondaryProfile_throwsException_exceptionIsNotWrapped() { + testUtilities.setRunningOnPersonalProfile(); // Work profile is primary for TestProfileConnector + + assertThrows( + CustomRuntimeException.class, + () -> profileTestCrossProfileType.secondary().methodWhichThrowsRuntimeException()); + } + + @Test + public void personal_async_runningOnWork_throwsException_exceptionIsWrapped() { + testUtilities.setRunningOnWorkProfile(); + + try { + profileTestCrossProfileType + .personal() + .asyncStringMethodWhichThrowsRuntimeException( + stringCallbackListener, exceptionCallbackListener); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void work_async_runningOnPersonal_throwsException_exceptionIsWrapped() { + testUtilities.setRunningOnPersonalProfile(); + + try { + profileTestCrossProfileType + .work() + .asyncStringMethodWhichThrowsRuntimeException( + stringCallbackListener, exceptionCallbackListener); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void primary_async_runningOnSecondaryProfile_throwsException_exceptionIsWrapped() { + testUtilities.setRunningOnPersonalProfile(); // Work profile is primary for TestProfileConnector + + try { + profileTestCrossProfileType + .primary() + .asyncStringMethodWhichThrowsRuntimeException( + stringCallbackListener, exceptionCallbackListener); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void secondary_async_runningOnPrimaryProfile_throwsException_exceptionIsWrapped() { + testUtilities.setRunningOnWorkProfile(); // Work profile is primary for TestProfileConnector + + try { + profileTestCrossProfileType + .secondary() + .asyncStringMethodWhichThrowsRuntimeException( + stringCallbackListener, exceptionCallbackListener); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void personal_async_runningOnPersonal_throwsException_exceptionIsNotWrapped() { + testUtilities.setRunningOnPersonalProfile(); + + assertThrows( + CustomRuntimeException.class, + () -> + profileTestCrossProfileType + .personal() + .asyncStringMethodWhichThrowsRuntimeException( + stringCallbackListener, exceptionCallbackListener)); + } + + @Test + public void work_async_runningOnWork_throwsException_exceptionIsNotWrapped() { + testUtilities.setRunningOnWorkProfile(); + + assertThrows( + CustomRuntimeException.class, + () -> + profileTestCrossProfileType + .work() + .asyncStringMethodWhichThrowsRuntimeException( + stringCallbackListener, exceptionCallbackListener)); + } + + @Test + public void primary_async_runningOnPrimaryProfile_throwsException_exceptionIsNotWrapped() { + testUtilities.setRunningOnWorkProfile(); // Work profile is primary for TestProfileConnector + + assertThrows( + CustomRuntimeException.class, + () -> + profileTestCrossProfileType + .primary() + .asyncStringMethodWhichThrowsRuntimeException( + stringCallbackListener, exceptionCallbackListener)); + } + + @Test + public void secondary_async_runningOnSecondaryProfile_throwsException_exceptionIsNotWrapped() { + testUtilities.setRunningOnPersonalProfile(); // Work profile is primary for TestProfileConnector + + assertThrows( + CustomRuntimeException.class, + () -> + profileTestCrossProfileType + .secondary() + .asyncStringMethodWhichThrowsRuntimeException( + stringCallbackListener, exceptionCallbackListener)); + } + + @Test + public void personal_future_runningOnWork_throwsException_wrapsInProfileRuntimeException() { + testUtilities.setRunningOnWorkProfile(); + + try { + profileTestCrossProfileType + .personal() + .listenableFutureVoidMethodWhichThrowsRuntimeException(); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void work_future_runningOnPersonal_throwsException_exceptionIsWrapped() { + testUtilities.setRunningOnPersonalProfile(); + + try { + profileTestCrossProfileType.work().listenableFutureVoidMethodWhichThrowsRuntimeException(); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void primary_future_runningOnSecondaryProfile_throwsException_exceptionIsWrapped() { + testUtilities.setRunningOnPersonalProfile(); // Work profile is primary for TestProfileConnector + + try { + profileTestCrossProfileType.primary().listenableFutureVoidMethodWhichThrowsRuntimeException(); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void secondary_future_runningOnPrimaryProfile_throwsException_exceptionIsWrapped() { + testUtilities.setRunningOnWorkProfile(); // Work profile is primary for TestProfileConnector + + try { + profileTestCrossProfileType + .secondary() + .listenableFutureVoidMethodWhichThrowsRuntimeException(); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void personal_future_runningOnPersonal_throwsException_exceptionIsNotWrapped() { + testUtilities.setRunningOnPersonalProfile(); + + assertThrows( + CustomRuntimeException.class, + () -> + profileTestCrossProfileType + .personal() + .listenableFutureVoidMethodWhichThrowsRuntimeException()); + } + + @Test + public void work_future_runningOnWork_throwsException_exceptionIsNotWrapped() { + testUtilities.setRunningOnWorkProfile(); + + assertThrows( + CustomRuntimeException.class, + () -> + profileTestCrossProfileType + .work() + .listenableFutureVoidMethodWhichThrowsRuntimeException()); + } + + @Test + public void primary_future_runningOnPrimaryProfile_throwsException_exceptionIsNotWrapped() { + testUtilities.setRunningOnWorkProfile(); // Work profile is primary for TestProfileConnector + + assertThrows( + CustomRuntimeException.class, + () -> + profileTestCrossProfileType + .primary() + .listenableFutureVoidMethodWhichThrowsRuntimeException()); + } + + @Test + public void secondary_future_runningOnSecondaryProfile_throwsException_exceptionIsNotWrapped() { + testUtilities.setRunningOnPersonalProfile(); // Work profile is primary for TestProfileConnector + + assertThrows( + CustomRuntimeException.class, + () -> + profileTestCrossProfileType + .secondary() + .listenableFutureVoidMethodWhichThrowsRuntimeException()); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileTypeUnsupportedTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileTypeUnsupportedTest.java new file mode 100644 index 0000000..b21524f --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileTypeUnsupportedTest.java @@ -0,0 +1,301 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.assertFutureHasException; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import android.app.Application; +import android.os.Build.VERSION_CODES; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.TestExceptionCallbackListener; +import com.google.android.enterprise.connectedapps.TestStringCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.TestVoidCallbackListenerMultiImpl; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.TestCrossProfileType; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.concurrent.ExecutionException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Tests for using cross-profile types on unsupported Android versions. */ +@RunWith(RobolectricTestRunner.class) +@Config(maxSdk = VERSION_CODES.N_MR1) +public class CrossProfileTypeUnsupportedTest { + + private static final String STRING = "String"; + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestProfileConnector testProfileConnector = TestProfileConnector.create(context); + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + private final TestStringCallbackListenerImpl testStringCallbackListener = + new TestStringCallbackListenerImpl(); + private final TestVoidCallbackListenerMultiImpl testVoidCallbackListenerMulti = + new TestVoidCallbackListenerMultiImpl(); + private final TestExceptionCallbackListener testExceptionCallbackListener = + new TestExceptionCallbackListener(); + + @Test + public void current_synchronous_works() { + assertThat(profileTestCrossProfileType.current().identityStringMethod(STRING)) + .isEqualTo(STRING); + } + + @Test + public void current_async_works() { + profileTestCrossProfileType + .current() + .asyncIdentityStringMethod(STRING, testStringCallbackListener); + + assertThat(testStringCallbackListener.stringCallbackValue).isEqualTo(STRING); + } + + @Test + public void current_future_works() throws ExecutionException, InterruptedException { + assertThat( + profileTestCrossProfileType + .current() + .listenableFutureIdentityStringMethod(STRING) + .get()) + .isEqualTo(STRING); + } + + @Test + public void other_synchronous_throwsUnavailableProfileException() { + assertThrows( + UnavailableProfileException.class, + () -> profileTestCrossProfileType.other().identityStringMethod(STRING)); + } + + @Test + public void other_async_passesUnavailableProfileException() { + profileTestCrossProfileType + .other() + .asyncIdentityStringMethod( + STRING, testStringCallbackListener, testExceptionCallbackListener); + + assertThat(testExceptionCallbackListener.lastException) + .isInstanceOf(UnavailableProfileException.class); + } + + @Test + public void other_future_passesUnavailableProfileException() { + ListenableFuture<String> future = + profileTestCrossProfileType.other().listenableFutureIdentityStringMethod(STRING); + + assertFutureHasException(future, UnavailableProfileException.class); + } + + @Test + public void work_synchronous_throwsUnavailableProfileException() { + assertThrows( + UnavailableProfileException.class, + () -> profileTestCrossProfileType.work().identityStringMethod(STRING)); + } + + @Test + public void work_async_passesUnavailableProfileException() { + profileTestCrossProfileType + .work() + .asyncIdentityStringMethod( + STRING, testStringCallbackListener, testExceptionCallbackListener); + + assertThat(testExceptionCallbackListener.lastException) + .isInstanceOf(UnavailableProfileException.class); + } + + @Test + public void work_future_passesUnavailableProfileException() { + ListenableFuture<String> future = + profileTestCrossProfileType.work().listenableFutureIdentityStringMethod(STRING); + + assertFutureHasException(future, UnavailableProfileException.class); + } + + @Test + public void personal_synchronous_throwsUnavailableProfileException() { + assertThrows( + UnavailableProfileException.class, + () -> profileTestCrossProfileType.personal().identityStringMethod(STRING)); + } + + @Test + public void personal_async_passesUnavailableProfileException() { + profileTestCrossProfileType + .personal() + .asyncIdentityStringMethod( + STRING, testStringCallbackListener, testExceptionCallbackListener); + + assertThat(testExceptionCallbackListener.lastException) + .isInstanceOf(UnavailableProfileException.class); + } + + @Test + public void personal_future_passesUnavailableProfileException() { + ListenableFuture<String> future = + profileTestCrossProfileType.personal().listenableFutureIdentityStringMethod(STRING); + + assertFutureHasException(future, UnavailableProfileException.class); + } + + @Test + public void primary_synchronous_throwsUnavailableProfileException() { + assertThrows( + UnavailableProfileException.class, + () -> profileTestCrossProfileType.primary().identityStringMethod(STRING)); + } + + @Test + public void primary_async_passesUnavailableProfileException() { + profileTestCrossProfileType + .primary() + .asyncIdentityStringMethod( + STRING, testStringCallbackListener, testExceptionCallbackListener); + + assertThat(testExceptionCallbackListener.lastException) + .isInstanceOf(UnavailableProfileException.class); + } + + @Test + public void primary_future_passesUnavailableProfileException() { + ListenableFuture<String> future = + profileTestCrossProfileType.primary().listenableFutureIdentityStringMethod(STRING); + + assertFutureHasException(future, UnavailableProfileException.class); + } + + @Test + public void secondary_synchronous_throwsUnavailableProfileException() { + assertThrows( + UnavailableProfileException.class, + () -> profileTestCrossProfileType.secondary().identityStringMethod(STRING)); + } + + @Test + public void secondary_async_passesUnavailableProfileException() { + profileTestCrossProfileType + .secondary() + .asyncIdentityStringMethod( + STRING, testStringCallbackListener, testExceptionCallbackListener); + + assertThat(testExceptionCallbackListener.lastException) + .isInstanceOf(UnavailableProfileException.class); + } + + @Test + public void secondary_future_passesUnavailableProfileException() { + ListenableFuture<String> future = + profileTestCrossProfileType.secondary().listenableFutureIdentityStringMethod(STRING); + + assertFutureHasException(future, UnavailableProfileException.class); + } + + @Test + public void profiles_synchronous_callsNothing() { + TestCrossProfileType.voidMethodCalls = 0; + + profileTestCrossProfileType + .profiles(testProfileConnector.utils().getCurrentProfile()) + .voidMethod(); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(0); + } + + @Test + public void profiles_async_callsNothing() { + TestCrossProfileType.voidMethodCalls = 0; + + profileTestCrossProfileType + .profiles(testProfileConnector.utils().getCurrentProfile()) + .asyncVoidMethod(testVoidCallbackListenerMulti); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(0); + } + + @Test + public void profiles_future_callsNothing() throws ExecutionException, InterruptedException { + TestCrossProfileType.voidMethodCalls = 0; + + profileTestCrossProfileType + .profiles(testProfileConnector.utils().getCurrentProfile()) + .listenableFutureVoidMethod() + .get(); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(0); + } + + @Test + public void both_synchronous_callsCurrentProfileOnce() { + TestCrossProfileType.voidMethodCalls = 0; + + profileTestCrossProfileType.both().voidMethod(); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void both_async_callsCurrentProfileOnce() { + TestCrossProfileType.voidMethodCalls = 0; + + profileTestCrossProfileType.both().asyncVoidMethod(testVoidCallbackListenerMulti); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void both_future_callsCurrentProfileOnce() + throws ExecutionException, InterruptedException { + TestCrossProfileType.voidMethodCalls = 0; + + profileTestCrossProfileType.both().listenableFutureVoidMethod().get(); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void suppliers_synchronous_callsCurrentProfileOnce() { + TestCrossProfileType.voidMethodCalls = 0; + + profileTestCrossProfileType.suppliers().voidMethod(); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void suppliers_async_callsCurrentProfileOnce() { + TestCrossProfileType.voidMethodCalls = 0; + + profileTestCrossProfileType.suppliers().asyncVoidMethod(testVoidCallbackListenerMulti); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void suppliers_future_callsCurrentProfileOnce() + throws ExecutionException, InterruptedException { + TestCrossProfileType.voidMethodCalls = 0; + + profileTestCrossProfileType.suppliers().listenableFutureVoidMethod().get(); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossUserTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossUserTest.java new file mode 100644 index 0000000..10b7d1f --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossUserTest.java @@ -0,0 +1,81 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.testapp.crossuser.ProfileTestCrossUserType; +import com.google.android.enterprise.connectedapps.testapp.crossuser.TestCrossUserConfiguration; +import com.google.android.enterprise.connectedapps.testapp.crossuser.TestCrossUserConnector; +import com.google.android.enterprise.connectedapps.testapp.crossuser.TestCrossUserStringCallbackListenerImpl; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class CrossUserTest { + + private static final String STRING = "String"; + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestCrossUserConnector testCrossUserConnector = + TestCrossUserConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testCrossUserConnector, scheduledExecutorService); + private final ProfileTestCrossUserType profileCrossUserType = + ProfileTestCrossUserType.create(testCrossUserConnector); + private final TestCrossUserStringCallbackListenerImpl crossUserStringCallback = + new TestCrossUserStringCallbackListenerImpl(); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestCrossUserConfiguration.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, TestCrossUserConnector.class.getName()); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testCrossUserConnector.stopManualConnectionManagement(); + } + + @Test + // This test covers all CrossUser annotations + public void passArgumentToCallback_works() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + profileCrossUserType.current().passString(STRING, crossUserStringCallback); + + assertThat(crossUserStringCallback.stringCallbackValue).isEqualTo(STRING); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CurrentProfileTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CurrentProfileTest.java new file mode 100644 index 0000000..7693a54 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CurrentProfileTest.java @@ -0,0 +1,413 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.assertFutureDoesNotHaveException; +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.assertFutureHasException; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.CrossProfileConnector; +import com.google.android.enterprise.connectedapps.NonSimpleCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestBooleanCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.TestNotReallySerializableObjectCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.TestStringCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.TestVoidCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.testapp.CustomRuntimeException; +import com.google.android.enterprise.connectedapps.testapp.NotReallySerializableObject; +import com.google.android.enterprise.connectedapps.testapp.ParcelableObject; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.TestCrossProfileType; +import com.google.common.util.concurrent.ListenableFuture; +import java.io.IOException; +import java.sql.SQLException; +import java.util.concurrent.ExecutionException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class CurrentProfileTest { + + private static final String STRING = "String"; + private static final String STRING2 = "String2"; + private static final ParcelableObject PARCELABLE_OBJECT = new ParcelableObject(""); + private static final NotReallySerializableObject NOT_REALLY_SERIALIZABLE_OBJECT = + new NotReallySerializableObject(PARCELABLE_OBJECT); + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestStringCallbackListenerImpl stringCallbackListener = + new TestStringCallbackListenerImpl(); + private final TestBooleanCallbackListenerImpl booleanCallbackListener = + new TestBooleanCallbackListenerImpl(); + private final TestVoidCallbackListenerImpl voidCallbackListener = + new TestVoidCallbackListenerImpl(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + private final TestNotReallySerializableObjectCallbackListenerImpl + notReallySerializableObjectCallbackListener = + new TestNotReallySerializableObjectCallbackListenerImpl(); + private final NonSimpleCallbackListenerImpl nonSimpleCallbackListener = + new NonSimpleCallbackListenerImpl(); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, CrossProfileConnector.class.getName()); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testUtilities.startConnectingAndWait(); + } + + @Test + public void current_isBound_callsMethod() throws UnavailableProfileException { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + assertThat(profileTestCrossProfileType.current().identityStringMethod(STRING)) + .isEqualTo(STRING); + } + + @Test + public void current_isNotBound_callsMethod() { + testUtilities.turnOffWorkProfile(); + + assertThat(profileTestCrossProfileType.current().identityStringMethod(STRING)) + .isEqualTo(STRING); + } + + @Test + public void current_async_isBound_callsMethod() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType.current().asyncVoidMethod(voidCallbackListener); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void current_synchronous_isBound_automaticConnectionManagement_callsMethod() { + testUtilities.turnOnWorkProfile(); + testProfileConnector.stopManualConnectionManagement(); + ListenableFuture<Void> ignored = + profileTestCrossProfileType.other().listenableFutureVoidMethod(); // Causes it to bind + + assertThat(profileTestCrossProfileType.current().identityStringMethod(STRING)) + .isEqualTo(STRING); + } + + @Test + public void current_async_isNotBound_callsMethod() { + testUtilities.turnOffWorkProfile(); + + profileTestCrossProfileType.current().asyncVoidMethod(voidCallbackListener); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test // This behaviour is expected right now but will change + public void current_async_blockingMethod_blocks() { + profileTestCrossProfileType + .current() + .asyncVoidMethodWithDelay(voidCallbackListener, /* secondsDelay= */ 5); + + assertThat(voidCallbackListener.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void current_async_firesCallback() { + profileTestCrossProfileType.current().asyncVoidMethod(voidCallbackListener); + + assertThat(voidCallbackListener.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void current_async_passesParametersCorrectly() { + profileTestCrossProfileType.current().asyncIdentityStringMethod(STRING, stringCallbackListener); + + assertThat(stringCallbackListener.stringCallbackValue).isEqualTo(STRING); + } + + @Test + public void current_async_nonblockingMethod_doesNotBlock() { + profileTestCrossProfileType + .current() + .asyncVoidMethodWithNonBlockingDelay(voidCallbackListener, /* secondsDelay= */ 5); + + assertThat(voidCallbackListener.callbackMethodCalls).isEqualTo(0); + } + + @Test + public void current_async_nonblockingMethod_doesCallback() { + profileTestCrossProfileType + .current() + .asyncVoidMethodWithNonBlockingDelay(voidCallbackListener, /* secondsDelay= */ 5); + testUtilities.advanceTimeBySeconds(10); + + assertThat(voidCallbackListener.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void current_listenableFuture_isBound_callsMethod() + throws ExecutionException, InterruptedException { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType.current().listenableFutureVoidMethod().get(); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void current_listenableFuture_isNotBound_callsMethod() + throws ExecutionException, InterruptedException { + testUtilities.turnOffWorkProfile(); + + profileTestCrossProfileType.current().listenableFutureVoidMethod().get(); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test // This behaviour is expected right now but will change + public void current_listenableFuture_blockingMethod_blocks() { + ListenableFuture<Void> voidFuture = + profileTestCrossProfileType + .current() + .listenableFutureVoidMethodWithDelay(/* secondsDelay= */ 5); + + assertThat(voidFuture.isDone()).isTrue(); + } + + @Test + public void current_listenableFuture_setsFuture() + throws ExecutionException, InterruptedException { + + // This would throw an exception if it wasn't set + assertThat(profileTestCrossProfileType.current().listenableFutureVoidMethod().get()).isNull(); + } + + @Test + public void current_listenableFuture_setsException_isSet() { + assertFutureHasException( + profileTestCrossProfileType + .current() + .listenableFutureVoidMethodWhichSetsIllegalStateException(), + IllegalStateException.class); + } + + @Test + public void current_listenableFuture_passesParametersCorrectly() + throws ExecutionException, InterruptedException { + ListenableFuture<String> future = + profileTestCrossProfileType.current().listenableFutureIdentityStringMethod(STRING); + + assertThat(future.get()).isEqualTo(STRING); + } + + @Test + public void current_listenableFuture_nonblockingMethod_doesNotBlock() { + ListenableFuture<Void> future = + profileTestCrossProfileType + .current() + .listenableFutureVoidMethodWithNonBlockingDelay(/* secondsDelay= */ 5); + + assertThat(future.isDone()).isFalse(); + } + + @Test + public void current_listenableFuture_nonblockingMethod_doesCallback() { + ListenableFuture<Void> future = + profileTestCrossProfileType + .current() + .listenableFutureVoidMethodWithNonBlockingDelay(/* secondsDelay= */ 5); + testUtilities.advanceTimeBySeconds(10); + + assertThat(future.isDone()).isTrue(); + } + + @Test + public void current_listenableFuture_doesNotTimeout() { + ListenableFuture<Void> future = + profileTestCrossProfileType + .current() + .listenableFutureMethodWhichNeverSetsTheValueWith5SecondTimeout(); + testUtilities.advanceTimeBySeconds(10); + + assertFutureDoesNotHaveException(future, UnavailableProfileException.class); + } + + @Test + public void current_async_doesNotTimeout() { + profileTestCrossProfileType + .current() + .asyncMethodWhichNeverCallsBackWith5SecondTimeout(stringCallbackListener); + testUtilities.advanceTimeBySeconds(10); + + assertThat(stringCallbackListener.callbackMethodCalls).isEqualTo(0); + } + + @Test + public void current_serializableObjectIsNotReallySerializable_works() { + assertThat( + profileTestCrossProfileType.current() + .identityNotReallySerializableObjectMethod(NOT_REALLY_SERIALIZABLE_OBJECT)) + .isEqualTo(NOT_REALLY_SERIALIZABLE_OBJECT); + } + + @Test + public void current_async_serializableObjectIsNotReallySerializable_works() { + profileTestCrossProfileType.current() + .asyncGetNotReallySerializableObjectMethod(notReallySerializableObjectCallbackListener); + + assertThat(notReallySerializableObjectCallbackListener.notReallySerializableObjectCallbackValue) + .isNotNull(); + } + + @Test + public void current_future_serializableObjectIsNotReallySerializable_works() + throws ExecutionException, InterruptedException { + ListenableFuture<NotReallySerializableObject> future = + profileTestCrossProfileType.current().futureGetNotReallySerializableObjectMethod(); + + assertThat(future.get()).isNotNull(); + } + + @Test + public void current_synchronous_throwsException_throwsOriginalException() { + assertThrows( + CustomRuntimeException.class, + () -> profileTestCrossProfileType.current().methodWhichThrowsRuntimeException()); + } + + @Test + public void current_async_throwsException_throwsOriginalException() { + assertThrows( + CustomRuntimeException.class, + () -> + profileTestCrossProfileType + .current() + .asyncStringMethodWhichThrowsRuntimeException(stringCallbackListener)); + } + + @Test + public void current_future_throwsException_throwsOriginalException() { + assertThrows( + CustomRuntimeException.class, + () -> + profileTestCrossProfileType + .current() + .listenableFutureVoidMethodWhichThrowsRuntimeException()); + } + + @Test + public void current_synchronous_contextArgument_works() { + assertThat(profileTestCrossProfileType.current().isContextArgumentPassed()).isTrue(); + } + + @Test + public void current_async_contextArgument_works() { + profileTestCrossProfileType.current().asyncIsContextArgumentPassed(booleanCallbackListener); + + assertThat(booleanCallbackListener.booleanCallbackValue).isTrue(); + } + + @Test + public void current_future_contextArgument_works() throws Exception { + ListenableFuture<Boolean> result = + profileTestCrossProfileType.current().futureIsContextArgumentPassed(); + + assertThat(result.get()).isTrue(); + } + + @Test + public void current_synchronous_declaresButDoesNotThrowException_works() throws Exception { + assertThat( + profileTestCrossProfileType + .current() + .identityStringMethodDeclaresButDoesNotThrowIOException(STRING)) + .isEqualTo(STRING); + } + + @Test + public void current_synchronous_throwsException_works() { + assertThrows( + IOException.class, + () -> + profileTestCrossProfileType + .current() + .identityStringMethodThrowsIOException(STRING)); + } + + @Test + public void current_synchronous_declaresMultipleExceptions_throwsException_works() { + assertThrows( + SQLException.class, + () -> + profileTestCrossProfileType + .current() + .identityStringMethodDeclaresIOExceptionThrowsSQLException(STRING)); + } + + @Test + public void current_async_nonSimpleCallback_works() { + nonSimpleCallbackListener.callbackMethodCalls = 0; + profileTestCrossProfileType + .current() + .asyncMethodWithNonSimpleCallback(nonSimpleCallbackListener, STRING, STRING2); + + assertThat(nonSimpleCallbackListener.callbackMethodCalls).isEqualTo(1); + assertThat(nonSimpleCallbackListener.string1CallbackValue).isEqualTo(STRING); + assertThat(nonSimpleCallbackListener.string2CallbackValue).isEqualTo(STRING2); + } + + @Test + public void current_async_nonSimpleCallback_secondMethod_works() { + profileTestCrossProfileType + .current() + .asyncMethodWithNonSimpleCallbackCallsSecondMethod( + nonSimpleCallbackListener, STRING, STRING2); + + assertThat(nonSimpleCallbackListener.string3CallbackValue).isEqualTo(STRING); + assertThat(nonSimpleCallbackListener.string4CallbackValue).isEqualTo(STRING2); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/IfAvailableTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/IfAvailableTest.java new file mode 100644 index 0000000..6106125 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/IfAvailableTest.java @@ -0,0 +1,187 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; +import static org.robolectric.Shadows.shadowOf; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import android.os.Looper; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.TestStringCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.TestVoidCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.concurrent.ExecutionException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Tests for _IfAvailable classes */ +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class IfAvailableTest { + + private static final String STRING1 = "String1"; + private static final String STRING2 = "String2"; + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final TestStringCallbackListenerImpl stringCallbackListener = + new TestStringCallbackListenerImpl(); + private final TestVoidCallbackListenerImpl voidCallbackListener = + new TestVoidCallbackListenerImpl(); + + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + } + + @Test + public void synchronous_notConnected_returnsDefaultValue() { + testUtilities.disconnect(); + + assertThat( + profileTestCrossProfileType + .other() + .ifAvailable() + .identityStringMethod(STRING1, /* defaultValue= */ STRING2)) + .isEqualTo(STRING2); + } + + @Test + public void synchronous_connected_makesCall() throws Exception { + testUtilities.startConnectingAndWait(); + + assertThat(profileTestCrossProfileType.other().identityStringMethod(STRING1)) + .isEqualTo(STRING1); + } + + @Test + public void synchronousVoid_notConnected_doesNotThrowException() { + testUtilities.disconnect(); + + profileTestCrossProfileType.other().ifAvailable().voidMethod(); + } + + @Test + public void synchronousVoid_connected_doesNotThrowException() { + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType.other().ifAvailable().voidMethod(); + } + + @Test + public void callback_notAvailable_returnsDefaultValue() { + testUtilities.turnOffWorkProfile(); + + profileTestCrossProfileType + .other() + .ifAvailable() + .asyncIdentityStringMethod(STRING1, stringCallbackListener, /* defaultValue= */ STRING2); + + assertThat(stringCallbackListener.stringCallbackValue).isEqualTo(STRING2); + } + + @Test + public void callback_available_makesCall() { + testUtilities.turnOnWorkProfile(); + + profileTestCrossProfileType + .other() + .ifAvailable() + .asyncIdentityStringMethod(STRING1, stringCallbackListener, /* defaultValue= */ STRING2); + shadowOf(Looper.getMainLooper()).idle(); + + assertThat(stringCallbackListener.stringCallbackValue).isEqualTo(STRING1); + } + + @Test + public void voidCallback_notAvailable_callsBack() { + testUtilities.turnOffWorkProfile(); + + profileTestCrossProfileType.other().ifAvailable().asyncVoidMethod(voidCallbackListener); + shadowOf(Looper.getMainLooper()).idle(); + + assertThat(voidCallbackListener.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void voidCallback_available_callsBack() { + testUtilities.turnOnWorkProfile(); + + profileTestCrossProfileType.other().ifAvailable().asyncVoidMethod(voidCallbackListener); + shadowOf(Looper.getMainLooper()).idle(); + + assertThat(voidCallbackListener.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void future_notAvailable_setsDefaultValue() + throws ExecutionException, InterruptedException { + testUtilities.turnOffWorkProfile(); + + ListenableFuture<String> future = + profileTestCrossProfileType + .other() + .ifAvailable() + .listenableFutureIdentityStringMethod(STRING1, STRING2); + shadowOf(Looper.getMainLooper()).idle(); + + assertThat(future.get()).isEqualTo(STRING2); + } + + @Test + public void future_available_setsCorrectValue() throws ExecutionException, InterruptedException { + testUtilities.turnOnWorkProfile(); + + ListenableFuture<String> future = + profileTestCrossProfileType + .other() + .ifAvailable() + .listenableFutureIdentityStringMethod(STRING1, STRING2); + shadowOf(Looper.getMainLooper()).idle(); + + assertThat(future.get()).isEqualTo(STRING1); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/ManualConnectionManagementTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/ManualConnectionManagementTest.java new file mode 100644 index 0000000..bb1845e --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/ManualConnectionManagementTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class ManualConnectionManagementTest { + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + } + + @Test + public void connect_doesNotHavePermission_doesNotConnect() throws Exception { + testUtilities.denyPermissions(INTERACT_ACROSS_USERS); + + testUtilities.startConnectingAndWait(); + testUtilities.advanceTimeBySeconds(60); + + assertThat(testProfileConnector.isConnected()).isFalse(); + } + + @Test + public void connect_getsPermissionAfterStartingConnecting_connects() throws Exception { + testUtilities.denyPermissions(INTERACT_ACROSS_USERS); + testUtilities.startConnectingAndWait(); + + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testUtilities.advanceTimeBySeconds(5); + + assertThat(testProfileConnector.isConnected()).isTrue(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/MessageSizeTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/MessageSizeTest.java new file mode 100644 index 0000000..759d0c2 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/MessageSizeTest.java @@ -0,0 +1,130 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.android.enterprise.connectedapps.StringUtilities.randomString; +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestExceptionCallbackListener; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.TestStringCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import java.util.concurrent.ExecutionException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Tests for large messages */ +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class MessageSizeTest { + + private static final String SMALL_STRING = "String"; + private static final String LARGE_STRING = randomString(1500000); // 3Mb + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + + private final TestStringCallbackListenerImpl stringCallbackListener = + new TestStringCallbackListenerImpl(); + private final TestExceptionCallbackListener exceptionCallbackListener = + new TestExceptionCallbackListener(); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testUtilities.startConnectingAndWait(); + } + + @Test + public void synchronous_smallMessage_sends() throws UnavailableProfileException { + assertThat(profileTestCrossProfileType.other().identityStringMethod(SMALL_STRING)) + .isEqualTo(SMALL_STRING); + } + + @Test + public void synchronous_largeMessage_sends() throws UnavailableProfileException { + assertThat(profileTestCrossProfileType.other().identityStringMethod(LARGE_STRING)) + .isEqualTo(LARGE_STRING); + } + + @Test + public void async_smallMessage_sends() { + profileTestCrossProfileType + .other() + .asyncIdentityStringMethod(SMALL_STRING, stringCallbackListener, exceptionCallbackListener); + + assertThat(stringCallbackListener.stringCallbackValue).isEqualTo(SMALL_STRING); + } + + @Test + public void async_largeMessage_sends() { + profileTestCrossProfileType + .other() + .asyncIdentityStringMethod(LARGE_STRING, stringCallbackListener, exceptionCallbackListener); + + assertThat(stringCallbackListener.stringCallbackValue).isEqualTo(LARGE_STRING); + } + + @Test + public void future_smallMessage_sends() throws ExecutionException, InterruptedException { + assertThat( + profileTestCrossProfileType + .other() + .listenableFutureIdentityStringMethod(SMALL_STRING) + .get()) + .isEqualTo(SMALL_STRING); + } + + @Test + public void future_largeMessage_sends() throws ExecutionException, InterruptedException { + assertThat( + profileTestCrossProfileType + .other() + .listenableFutureIdentityStringMethod(LARGE_STRING) + .get()) + .isEqualTo(LARGE_STRING); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileAsyncTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileAsyncTest.java new file mode 100644 index 0000000..92cd481 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileAsyncTest.java @@ -0,0 +1,342 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.NonSimpleCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestBooleanCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.TestExceptionCallbackListener; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.TestStringCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.TestVoidCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.exceptions.ProfileRuntimeException; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileAnnotation; +import com.google.android.enterprise.connectedapps.testapp.CustomRuntimeException; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileTypeWhichNeedsContext; +import com.google.android.enterprise.connectedapps.testapp.types.TestCrossProfileType; +import java.util.concurrent.TimeUnit; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +@LooperMode(LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class OtherProfileAsyncTest { + + private static final String STRING = "String"; + private static final String STRING2 = "String2"; + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestVoidCallbackListenerImpl voidCallbackListener = + new TestVoidCallbackListenerImpl(); + private final TestStringCallbackListenerImpl stringCallbackListener = + new TestStringCallbackListenerImpl(); + private final TestBooleanCallbackListenerImpl booleanCallbackListener = + new TestBooleanCallbackListenerImpl(); + private final TestExceptionCallbackListener exceptionCallbackListener = + new TestExceptionCallbackListener(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + private final ProfileTestCrossProfileTypeWhichNeedsContext + profileTestCrossProfileTypeWhichNeedsContext = + ProfileTestCrossProfileTypeWhichNeedsContext.create(testProfileConnector); + private final NonSimpleCallbackListenerImpl nonSimpleCallbackListener = + new NonSimpleCallbackListenerImpl(); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + } + + @Test + public void other_async_callbackTriggeredMultipleTimes_isOnlyReceivedOnce() { + profileTestCrossProfileType + .other() + .asyncVoidMethodWhichCallsBackTwice(voidCallbackListener, exceptionCallbackListener); + + assertThat(voidCallbackListener.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void + other_async_automaticConnection_workProfileIsTurnedOff_doesReceiveUnavailableProfileExceptionImmediately() { + testProfileConnector.stopManualConnectionManagement(); + testUtilities.turnOffWorkProfile(); + + profileTestCrossProfileType + .other() + .asyncVoidMethod(voidCallbackListener, exceptionCallbackListener); + + assertThat(exceptionCallbackListener.lastException) + .isInstanceOf(UnavailableProfileException.class); + } + + @Test + public void + other_async_automaticConnection_workProfileIsTurnedOn_doesNotSetUnavailableProfileException() { + testProfileConnector.stopManualConnectionManagement(); + testUtilities.turnOnWorkProfile(); + + profileTestCrossProfileType + .other() + .asyncVoidMethodWithNonBlockingDelay( + voidCallbackListener, /* secondsDelay= */ 5, exceptionCallbackListener); + testUtilities.advanceTimeBySeconds(5); + + assertThat(exceptionCallbackListener.lastException).isNull(); + } + + @Test + public void other_async_automaticConnection_callsMethod() { + testProfileConnector.stopManualConnectionManagement(); + testUtilities.turnOnWorkProfile(); + + profileTestCrossProfileType + .other() + .asyncVoidMethod(voidCallbackListener, exceptionCallbackListener); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void other_async_automaticConnection_resultIsSet() { + testProfileConnector.stopManualConnectionManagement(); + testUtilities.turnOnWorkProfile(); + + profileTestCrossProfileType + .other() + .asyncIdentityStringMethod(STRING, stringCallbackListener, exceptionCallbackListener); + + assertThat(stringCallbackListener.stringCallbackValue).isEqualTo(STRING); + } + + @Test + public void + other_async_automaticConnection_connectionIsDroppedDuringCall_setUnavailableProfileException() { + testProfileConnector.stopManualConnectionManagement(); + testUtilities.turnOnWorkProfile(); + profileTestCrossProfileType + .other() + .asyncMethodWhichNeverCallsBack(stringCallbackListener, exceptionCallbackListener); + testUtilities.advanceTimeBySeconds(5); + + testUtilities.turnOffWorkProfile(); + + assertThat(exceptionCallbackListener.lastException) + .isInstanceOf(UnavailableProfileException.class); + } + + @Test + public void other_async_timeoutSetOnMethod_doesNotTimeoutEarly() { + profileTestCrossProfileType + .other() + .asyncMethodWhichNeverCallsBackWith5SecondTimeout( + stringCallbackListener, exceptionCallbackListener); + + testUtilities.advanceTimeBySeconds(4); + + assertThat(exceptionCallbackListener.lastException).isNull(); + } + + @Test + public void other_async_timeoutSetOnMethod_timesOut() { + profileTestCrossProfileType + .other() + .asyncMethodWhichNeverCallsBackWith5SecondTimeout( + stringCallbackListener, exceptionCallbackListener); + + testUtilities.advanceTimeBySeconds(6); + + assertThat(exceptionCallbackListener.lastException) + .isInstanceOf(UnavailableProfileException.class); + } + + @Test + public void other_async_timeoutSetOnType_doesNotTimeoutEarly() { + profileTestCrossProfileType + .other() + .asyncMethodWhichNeverCallsBackWith7SecondTimeout( + stringCallbackListener, exceptionCallbackListener); + + testUtilities.advanceTimeBySeconds(6); + + assertThat(exceptionCallbackListener.lastException).isNull(); + } + + @Test + public void other_async_timeoutSetOnType_timesOut() { + profileTestCrossProfileType + .other() + .asyncMethodWhichNeverCallsBackWith7SecondTimeout( + stringCallbackListener, exceptionCallbackListener); + + testUtilities.advanceTimeBySeconds(8); + + assertThat(exceptionCallbackListener.lastException) + .isInstanceOf(UnavailableProfileException.class); + } + + @Test + public void other_async_timeoutSetByDefault_doesNotTimeoutEarly() throws Exception { + profileTestCrossProfileTypeWhichNeedsContext + .other() + .asyncMethodWhichNeverCallsBackWithDefaultTimeout( + stringCallbackListener, exceptionCallbackListener); + + scheduledExecutorService.advanceTimeBy( + CrossProfileAnnotation.DEFAULT_TIMEOUT_MILLIS - 1, TimeUnit.MILLISECONDS); + + assertThat(exceptionCallbackListener.lastException).isNull(); + } + + @Test + public void other_async_timeoutSetByDefault_timesOut() throws Exception { + profileTestCrossProfileTypeWhichNeedsContext + .other() + .asyncMethodWhichNeverCallsBackWithDefaultTimeout( + stringCallbackListener, exceptionCallbackListener); + + scheduledExecutorService.advanceTimeBy( + CrossProfileAnnotation.DEFAULT_TIMEOUT_MILLIS + 1, TimeUnit.MILLISECONDS); + + assertThat(exceptionCallbackListener.lastException) + .isInstanceOf(UnavailableProfileException.class); + } + + @Test + public void other_async_timeoutSetByCaller_doesNotTimeoutEarly() throws Exception { + long timeoutMillis = 5000; + profileTestCrossProfileTypeWhichNeedsContext + .other() + .timeout(timeoutMillis) + .asyncMethodWhichNeverCallsBackWithDefaultTimeout( + stringCallbackListener, exceptionCallbackListener); + + scheduledExecutorService.advanceTimeBy(timeoutMillis - 1, TimeUnit.MILLISECONDS); + + assertThat(exceptionCallbackListener.lastException).isNull(); + } + + @Test + public void other_async_timeoutSetByCaller_timesOut() throws Exception { + long timeoutMillis = 5000; + profileTestCrossProfileTypeWhichNeedsContext + .other() + .timeout(timeoutMillis) + .asyncMethodWhichNeverCallsBackWithDefaultTimeout( + stringCallbackListener, exceptionCallbackListener); + + scheduledExecutorService.advanceTimeBy(timeoutMillis + 1, TimeUnit.MILLISECONDS); + + assertThat(exceptionCallbackListener.lastException) + .isInstanceOf(UnavailableProfileException.class); + } + + @Test + public void other_async_doesNotTimeoutAfterCompletion() throws Exception { + // We would expect an exception if the timeout continued after completion + profileTestCrossProfileType + .other() + .asyncVoidMethod(voidCallbackListener, exceptionCallbackListener); + + scheduledExecutorService.advanceTimeBy( + CrossProfileAnnotation.DEFAULT_TIMEOUT_MILLIS + 1, TimeUnit.MILLISECONDS); + + assertThat(exceptionCallbackListener.lastException).isNull(); + } + + @Test + public void other_async_throwsException_exceptionIsWrapped() { + // The exception is only catchable when the connection is already established. + testUtilities.startConnectingAndWait(); + + try { + profileTestCrossProfileType + .other() + .asyncStringMethodWhichThrowsRuntimeException( + stringCallbackListener, exceptionCallbackListener); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void other_async_contextArgument_works() { + profileTestCrossProfileType + .other() + .asyncIsContextArgumentPassed(booleanCallbackListener, exceptionCallbackListener); + + assertThat(booleanCallbackListener.booleanCallbackValue).isTrue(); + } + + @Test + public void other_async_nonSimpleCallback_works() { + nonSimpleCallbackListener.callbackMethodCalls = 0; + profileTestCrossProfileType + .other() + .asyncMethodWithNonSimpleCallback( + nonSimpleCallbackListener, STRING, STRING2, exceptionCallbackListener); + + assertThat(nonSimpleCallbackListener.callbackMethodCalls).isEqualTo(1); + assertThat(nonSimpleCallbackListener.string1CallbackValue).isEqualTo(STRING); + assertThat(nonSimpleCallbackListener.string2CallbackValue).isEqualTo(STRING2); + } + + @Test + public void other_async_nonSimpleCallback_secondMethod_works() { + profileTestCrossProfileType + .other() + .asyncMethodWithNonSimpleCallbackCallsSecondMethod( + nonSimpleCallbackListener, STRING, STRING2, exceptionCallbackListener); + + assertThat(nonSimpleCallbackListener.string3CallbackValue).isEqualTo(STRING); + assertThat(nonSimpleCallbackListener.string4CallbackValue).isEqualTo(STRING2); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileListenableFutureTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileListenableFutureTest.java new file mode 100644 index 0000000..7d71ade --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileListenableFutureTest.java @@ -0,0 +1,297 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.assertFutureDoesNotHaveException; +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.assertFutureHasException; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.exceptions.ProfileRuntimeException; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileAnnotation; +import com.google.android.enterprise.connectedapps.testapp.CustomRuntimeException; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileTypeWhichNeedsContext; +import com.google.android.enterprise.connectedapps.testapp.types.TestCrossProfileType; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +@LooperMode(LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class OtherProfileListenableFutureTest { + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + private final ProfileTestCrossProfileTypeWhichNeedsContext + profileTestCrossProfileTypeWhichNeedsContext = + ProfileTestCrossProfileTypeWhichNeedsContext.create(testProfileConnector); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + } + + @Test + public void + other_listenableFuture_automaticConnection_workProfileIsTurnedOff_doesSetUnavailableProfileExceptionImmediately() { + testProfileConnector.stopManualConnectionManagement(); + testUtilities.turnOffWorkProfile(); + + ListenableFuture<Void> future = + profileTestCrossProfileType.other().listenableFutureVoidMethod(); + + assertFutureHasException(future, UnavailableProfileException.class); + } + + @Test + public void + other_listenableFuture_automaticConnection_workProfileIsTurnedOn_doesNotSetUnavailableProfileException() { + testProfileConnector.stopManualConnectionManagement(); + testUtilities.turnOnWorkProfile(); + + ListenableFuture<Void> future = + profileTestCrossProfileType + .other() + .listenableFutureVoidMethodWithNonBlockingDelay(/* secondsDelay= */ 5); + Robolectric.getForegroundThreadScheduler().advanceBy(5, TimeUnit.SECONDS); + + assertFutureDoesNotHaveException(future, UnavailableProfileException.class); + } + + @Test + public void other_listenableFuture_automaticConnection_callsMethod() + throws ExecutionException, InterruptedException { + testProfileConnector.stopManualConnectionManagement(); + testUtilities.turnOnWorkProfile(); + + profileTestCrossProfileType.other().listenableFutureVoidMethod().get(); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void other_listenableFuture_automaticConnection_setsFuture() + throws ExecutionException, InterruptedException { + testProfileConnector.stopManualConnectionManagement(); + testUtilities.turnOnWorkProfile(); + + // This would throw an exception if it wasn't set + assertThat(profileTestCrossProfileType.other().listenableFutureVoidMethod().get()).isNull(); + } + + @Test + public void + other_listenableFuture_automaticConnection_connectionIsDroppedDuringCall_setUnavailableProfileException() { + testProfileConnector.stopManualConnectionManagement(); + testUtilities.turnOnWorkProfile(); + ListenableFuture<Void> future = + profileTestCrossProfileType.other().listenableFutureMethodWhichNeverSetsTheValue(); + Robolectric.getForegroundThreadScheduler().advanceBy(5, TimeUnit.SECONDS); + + testUtilities.turnOffWorkProfile(); + + assertFutureHasException(future, UnavailableProfileException.class); + } + + @Test + public void other_listenableFuture_timeoutSetOnMethod_doesNotTimeoutEarly() throws Exception { + ListenableFuture<Void> listenableFuture = + profileTestCrossProfileType + .other() + .listenableFutureMethodWhichNeverSetsTheValueWith5SecondTimeout(); + + scheduledExecutorService.advanceTimeBy(4, TimeUnit.SECONDS); + + assertFutureDoesNotHaveException(listenableFuture, UnavailableProfileException.class); + } + + @Test + public void other_listenableFuture_timeoutSetOnMethod_timesOut() throws Exception { + ListenableFuture<Void> listenableFuture = + profileTestCrossProfileType + .other() + .listenableFutureMethodWhichNeverSetsTheValueWith5SecondTimeout(); + + scheduledExecutorService.advanceTimeBy(6, TimeUnit.SECONDS); + + assertFutureHasException(listenableFuture, UnavailableProfileException.class); + } + + @Test + public void other_listenableFuture_timeoutSetOnType_doesNotTimeoutEarly() throws Exception { + ListenableFuture<Void> listenableFuture = + profileTestCrossProfileType + .other() + .listenableFutureMethodWhichNeverSetsTheValueWith7SecondTimeout(); + + scheduledExecutorService.advanceTimeBy(6, TimeUnit.SECONDS); + + assertFutureDoesNotHaveException(listenableFuture, UnavailableProfileException.class); + } + + @Test + public void other_listenableFuture_timeoutSetOnType_timesOut() throws Exception { + ListenableFuture<Void> listenableFuture = + profileTestCrossProfileType + .other() + .listenableFutureMethodWhichNeverSetsTheValueWith7SecondTimeout(); + + scheduledExecutorService.advanceTimeBy(8, TimeUnit.SECONDS); + + assertFutureHasException(listenableFuture, UnavailableProfileException.class); + } + + @Test + public void other_listenableFuture_timeoutSetByDefault_doesNotTimeoutEarly() throws Exception { + ListenableFuture<Void> listenableFuture = + profileTestCrossProfileTypeWhichNeedsContext + .other() + .listenableFutureMethodWhichNeverSetsTheValueWithDefaultTimeout(); + + scheduledExecutorService.advanceTimeBy( + CrossProfileAnnotation.DEFAULT_TIMEOUT_MILLIS - 1, TimeUnit.MILLISECONDS); + + assertFutureDoesNotHaveException(listenableFuture, UnavailableProfileException.class); + } + + @Test + public void other_listenableFuture_timeoutSetByDefault_timesOut() throws Exception { + ListenableFuture<Void> listenableFuture = + profileTestCrossProfileTypeWhichNeedsContext + .other() + .listenableFutureMethodWhichNeverSetsTheValueWithDefaultTimeout(); + + scheduledExecutorService.advanceTimeBy( + CrossProfileAnnotation.DEFAULT_TIMEOUT_MILLIS + 1, TimeUnit.MILLISECONDS); + + assertFutureHasException(listenableFuture, UnavailableProfileException.class); + } + + @Test + public void other_listenableFuture_timeoutSetByCaller_doesNotTimeoutEarly() throws Exception { + long timeoutMillis = 5000; + ListenableFuture<Void> listenableFuture = + profileTestCrossProfileTypeWhichNeedsContext + .other() + .timeout(timeoutMillis) + .listenableFutureMethodWhichNeverSetsTheValueWithDefaultTimeout(); + + scheduledExecutorService.advanceTimeBy(timeoutMillis - 1, TimeUnit.MILLISECONDS); + + assertFutureDoesNotHaveException(listenableFuture, UnavailableProfileException.class); + } + + @Test + public void other_listenableFuture_timeoutSetByCaller_timesOut() throws Exception { + long timeoutMillis = 5000; + ListenableFuture<Void> listenableFuture = + profileTestCrossProfileTypeWhichNeedsContext + .other() + .timeout(timeoutMillis) + .listenableFutureMethodWhichNeverSetsTheValueWithDefaultTimeout(); + + scheduledExecutorService.advanceTimeBy(timeoutMillis + 1, TimeUnit.MILLISECONDS); + + assertFutureHasException(listenableFuture, UnavailableProfileException.class); + } + + @Test + public void other_listenableFuture_doesNotTimeoutAfterCompletion() throws Exception { + // We would expect an exception if the timeout continued after completion + ListenableFuture<Void> listenableFuture = + profileTestCrossProfileType.other().listenableFutureVoidMethod(); + + scheduledExecutorService.advanceTimeBy( + CrossProfileAnnotation.DEFAULT_TIMEOUT_MILLIS + 1, TimeUnit.MILLISECONDS); + + assertFutureDoesNotHaveException(listenableFuture, UnavailableProfileException.class); + } + + @Test + public void other_listenableFuture_doesNotTimeoutAfterException() throws Exception { + // We would expect an exception if the timeout continued after completion + ListenableFuture<Void> unusedFuture = + profileTestCrossProfileType + .other() + .listenableFutureVoidMethodWhichSetsIllegalStateException(); + + scheduledExecutorService.advanceTimeBy( + CrossProfileAnnotation.DEFAULT_TIMEOUT_MILLIS + 1, TimeUnit.MILLISECONDS); + + // We expect there would be an exception thrown due to setting the future twice if it timed out + // now + } + + @Test + public void other_listenableFuture_throwsException_exceptionIsWrapped() { + // The exception is only catchable when the connection is already established. + testUtilities.startConnectingAndWait(); + + try { + ListenableFuture<Void> unusedFuture = + profileTestCrossProfileType + .other() + .listenableFutureVoidMethodWhichThrowsRuntimeException(); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void other_listenableFuture_contextArgument_works() throws Exception { + ListenableFuture<Boolean> result = + profileTestCrossProfileType.other().futureIsContextArgumentPassed(); + + assertThat(result.get()).isTrue(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileManualAsyncTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileManualAsyncTest.java new file mode 100644 index 0000000..cac6278 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileManualAsyncTest.java @@ -0,0 +1,196 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestExceptionCallbackListener; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.TestStringCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.TestVoidCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.TestCrossProfileType; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +@LooperMode(LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class OtherProfileManualAsyncTest { + + private static final String STRING = "String"; + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestStringCallbackListenerImpl stringCallbackListener = + new TestStringCallbackListenerImpl(); + private final TestVoidCallbackListenerImpl voidCallbackListener = + new TestVoidCallbackListenerImpl(); + private final TestExceptionCallbackListener exceptionCallbackListener = + new TestExceptionCallbackListener(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + } + + @Test + public void other_async_manualConnection_isBound_callsMethod() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType + .other() + .asyncVoidMethod(voidCallbackListener, exceptionCallbackListener); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void other_async_manualConnection_isBound_firesCallback() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType + .other() + .asyncVoidMethod(voidCallbackListener, exceptionCallbackListener); + + assertThat(voidCallbackListener.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void other_async_manualConnection_isBound_unbundlesCorrectly() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType + .other() + .asyncIdentityStringMethod(STRING, stringCallbackListener, exceptionCallbackListener); + + assertThat(stringCallbackListener.stringCallbackValue).isEqualTo(STRING); + } + + @Test // This behaviour is expected right now but will change + public void other_async_manualConnection_isBound_blockingMethod_blocks() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType + .other() + .asyncVoidMethodWithDelay( + voidCallbackListener, /* secondsDelay= */ 5, exceptionCallbackListener); + + assertThat(voidCallbackListener.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void other_async_manualConnection_isBound_nonBlockingMethod_doesNotBlock() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType + .other() + .asyncVoidMethodWithNonBlockingDelay( + voidCallbackListener, /* secondsDelay= */ 5, exceptionCallbackListener); + + assertThat(stringCallbackListener.callbackMethodCalls).isEqualTo(0); + } + + @Test + public void other_async_manualConnection_isBound_nonBlockingMethod_doesCallback() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType + .other() + .asyncVoidMethodWithNonBlockingDelay( + voidCallbackListener, /* secondsDelay= */ 5, exceptionCallbackListener); + testUtilities.advanceTimeBySeconds(10); + + assertThat(voidCallbackListener.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void other_async_manualConnection_isNotBound_doesReturnUnavailableProfileException() { + testUtilities.turnOffWorkProfile(); + + profileTestCrossProfileType + .other() + .asyncVoidMethod(voidCallbackListener, exceptionCallbackListener); + + assertThat(exceptionCallbackListener.lastException) + .isInstanceOf(UnavailableProfileException.class); + } + + @Test + public void other_asyncMethod_manualConnection_isNotBound_binds() { + testUtilities.turnOnWorkProfile(); + + profileTestCrossProfileType + .other() + .asyncVoidMethod(voidCallbackListener, exceptionCallbackListener); + + assertThat(testProfileConnector.isConnected()).isTrue(); + } + + @Test + public void + other_async_manualConnection_connectionIsDroppedDuringCall_setUnavailableProfileException() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + profileTestCrossProfileType + .other() + .asyncMethodWhichNeverCallsBack(stringCallbackListener, exceptionCallbackListener); + testUtilities.advanceTimeBySeconds(5); + + testUtilities.turnOffWorkProfile(); + + assertThat(exceptionCallbackListener.lastException) + .isInstanceOf(UnavailableProfileException.class); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileManualListenableFutureTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileManualListenableFutureTest.java new file mode 100644 index 0000000..ae6aafb --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileManualListenableFutureTest.java @@ -0,0 +1,184 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.TestCrossProfileType; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.concurrent.ExecutionException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +@LooperMode(LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class OtherProfileManualListenableFutureTest { + + private static final String STRING = "String"; + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + } + + @Test + public void + other_listenableFuture_manualConnection_workProfileIsTurnedOff_doesSetUnavailableProfileExceptionImmediately() { + testUtilities.startConnectingAndWait(); + testUtilities.turnOffWorkProfile(); + + testUtilities.assertFutureHasException( + profileTestCrossProfileType.other().listenableFutureVoidMethod(), + UnavailableProfileException.class); + } + + @Test + public void other_listenableFuture_manualConnection_isBound_callsMethod() + throws ExecutionException, InterruptedException { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType.other().listenableFutureVoidMethod().get(); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void + other_listenableFuture_manualConnection_isNotBound_doesNotThrowUnavailableProfileException() { + testUtilities.turnOffWorkProfile(); + + ListenableFuture<Void> unusedFuture = + profileTestCrossProfileType.other().listenableFutureVoidMethod(); + } + + @Test + public void + other_listenableFuture_manualConnection_isNotBound_returnsThrowUnavailableProfileException() { + testUtilities.turnOffWorkProfile(); + + ListenableFuture<Void> future = + profileTestCrossProfileType.other().listenableFutureVoidMethod(); + + testUtilities.assertFutureHasException(future, UnavailableProfileException.class); + } + + @Test // This behaviour is expected right now but will change + public void other_listenableFuture_manualConnection_blockingMethod_blocks() { + ListenableFuture<Void> voidFuture = + profileTestCrossProfileType + .other() + .listenableFutureVoidMethodWithDelay(/* secondsDelay= */ 5); + + assertThat(voidFuture.isDone()).isTrue(); + } + + @Test + public void other_listenableFuture_manualConnection_setsFuture() + throws ExecutionException, InterruptedException { + + // This would throw an exception if it wasn't set + assertThat(profileTestCrossProfileType.other().listenableFutureVoidMethod().get()).isNull(); + } + + @Test + public void other_listenableFuture_manualConnection_setsException_isSet() { + testUtilities.assertFutureHasException( + profileTestCrossProfileType + .other() + .listenableFutureVoidMethodWhichSetsIllegalStateException(), + IllegalStateException.class); + } + + @Test + public void other_listenableFuture_manualConnection_passesParametersCorrectly() + throws ExecutionException, InterruptedException { + ListenableFuture<String> future = + profileTestCrossProfileType.other().listenableFutureIdentityStringMethod(STRING); + + assertThat(future.get()).isEqualTo(STRING); + } + + @Test + public void other_listenableFuture_manualConnection_nonblockingMethod_doesNotBlock() { + ListenableFuture<Void> future = + profileTestCrossProfileType + .other() + .listenableFutureVoidMethodWithNonBlockingDelay(/* secondsDelay= */ 5); + + assertThat(future.isDone()).isFalse(); + } + + @Test + public void other_listenableFuture_manualConnection_nonblockingMethod_doesCallback() { + ListenableFuture<Void> future = + profileTestCrossProfileType + .other() + .listenableFutureVoidMethodWithNonBlockingDelay(/* secondsDelay= */ 5); + testUtilities.advanceTimeBySeconds(10); + + assertThat(future.isDone()).isTrue(); + } + + @Test + public void + other_listenableFuture_manualConnection_connectionIsDroppedDuringCall_setUnavailableProfileException() { + ListenableFuture<Void> future = + profileTestCrossProfileType.other().listenableFutureMethodWhichNeverSetsTheValue(); + testUtilities.advanceTimeBySeconds(5); + + testUtilities.turnOffWorkProfile(); + + testUtilities.assertFutureHasException(future, UnavailableProfileException.class); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileSynchronousTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileSynchronousTest.java new file mode 100644 index 0000000..5b40c0f --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileSynchronousTest.java @@ -0,0 +1,164 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.testapp.NotReallySerializableObject; +import com.google.android.enterprise.connectedapps.testapp.ParcelableObject; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.common.util.concurrent.ListenableFuture; +import java.io.IOException; +import java.sql.SQLException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +@LooperMode(LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class OtherProfileSynchronousTest { + + private static final String STRING = "String"; + private static final ParcelableObject PARCELABLE_OBJECT = new ParcelableObject(""); + private static final NotReallySerializableObject NOT_REALLY_SERIALIZABLE_OBJECT = + new NotReallySerializableObject(PARCELABLE_OBJECT); + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testUtilities.startConnectingAndWait(); + } + + @Test + public void other_synchronous_isBound_callsMethod() throws UnavailableProfileException { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + assertThat(profileTestCrossProfileType.other().identityStringMethod(STRING)).isEqualTo(STRING); + } + + @Test + public void other_synchronous_isNotBound_throwsUnavailableProfileException() { + testUtilities.turnOffWorkProfile(); + + assertThrows( + UnavailableProfileException.class, + () -> profileTestCrossProfileType.other().identityStringMethod(STRING)); + } + + @Test + public void other_synchronous_isNotInitialised_throwsUnavailableProfileException() { + ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(TestProfileConnector.create(context)); + + assertThrows( + UnavailableProfileException.class, + () -> profileTestCrossProfileType.other().identityStringMethod(STRING)); + } + + @Test + public void + other_synchronous_isBound_automaticConnectionManagement_throwsUnavailableProfileException() { + testUtilities.turnOnWorkProfile(); + testProfileConnector.stopManualConnectionManagement(); + ListenableFuture<Void> ignored = + profileTestCrossProfileType.other().listenableFutureVoidMethod(); // Causes it to bind + + assertThrows( + UnavailableProfileException.class, + () -> profileTestCrossProfileType.other().voidMethod()); + } + + @Test + public void other_serializableObjectParameterIsNotReallySerializable_throwsException() { + assertThrows( + RuntimeException.class, + () -> + profileTestCrossProfileType + .other() + .identityNotReallySerializableObjectMethod(NOT_REALLY_SERIALIZABLE_OBJECT)); + } + + @Test + public void other_synchronous_contextArgument_works() throws Exception { + assertThat(profileTestCrossProfileType.other().isContextArgumentPassed()).isTrue(); + } + + @Test + public void other_synchronous_declaresButDoesNotThrowException_works() throws Exception { + assertThat( + profileTestCrossProfileType + .other() + .identityStringMethodDeclaresButDoesNotThrowIOException(STRING)) + .isEqualTo(STRING); + } + + @Test + public void other_synchronous_throwsException_works() { + assertThrows( + IOException.class, + () -> + profileTestCrossProfileType + .other() + .identityStringMethodThrowsIOException(STRING)); + } + + @Test + public void other_synchronous_declaresMultipleExceptions_throwsException_works() { + assertThrows( + SQLException.class, + () -> + profileTestCrossProfileType + .other() + .identityStringMethodDeclaresIOExceptionThrowsSQLException(STRING)); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/ProfilesTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/ProfilesTest.java new file mode 100644 index 0000000..4150353 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/ProfilesTest.java @@ -0,0 +1,148 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.Profile; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.TestCrossProfileType; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +@LooperMode(LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class ProfilesTest { + + private static final String STRING = "String"; + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + + private final Profile currentProfileIdentifier = testProfileConnector.utils().getCurrentProfile(); + private final Profile otherProfileIdentifier = testProfileConnector.utils().getOtherProfile(); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testUtilities.startConnectingAndWait(); + } + + @Test + public void profiles_isBound_resultContainsAllProfileResults() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + Map<Profile, String> result = + profileTestCrossProfileType + .profiles(currentProfileIdentifier, otherProfileIdentifier) + .identityStringMethod(STRING); + + assertThat(result.get(currentProfileIdentifier)).isEqualTo(STRING); + assertThat(result.get(otherProfileIdentifier)).isEqualTo(STRING); + } + + @Test + public void profiles_isNotBound_resultDoesNotContainOtherProfileResult() { + testUtilities.turnOffWorkProfile(); + + Map<Profile, String> result = + profileTestCrossProfileType + .profiles(currentProfileIdentifier, otherProfileIdentifier) + .identityStringMethod(STRING); + + assertThat(result.get(currentProfileIdentifier)).isEqualTo(STRING); + assertThat(result).doesNotContainKey(otherProfileIdentifier); + } + + @Test + public void profiles_passedCurrentProfile_runsOnlyOnCurrentProfile() { + Map<Profile, String> result = + profileTestCrossProfileType.profiles(currentProfileIdentifier).identityStringMethod(STRING); + + assertThat(result).hasSize(1); + assertThat(result).containsKey(currentProfileIdentifier); + } + + @Test + public void profiles_passedOtherProfile_runsOnlyOnOtherProfile() { + testUtilities.turnOnWorkProfile(); + + Map<Profile, String> result = + profileTestCrossProfileType.profiles(otherProfileIdentifier).identityStringMethod(STRING); + + assertThat(result).hasSize(1); + assertThat(result).containsKey(otherProfileIdentifier); + } + + @Test + public void profiles_passedMultipleOfSameProfile_runsOnlyOncePerProfile() { + profileTestCrossProfileType + .profiles(currentProfileIdentifier, currentProfileIdentifier) + .voidMethod(); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void suppliers_runningOnSecondaryProfile_runsOnlyOnce() { + testUtilities.setRunningOnPersonalProfile(); // Work profile is primary + profileTestCrossProfileType.suppliers().voidMethod(); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void suppliers_runningOnPrimaryProfile_runsOnEachProfile() { + testUtilities.setRunningOnWorkProfile(); // Work profile is primary + profileTestCrossProfileType.suppliers().voidMethod(); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(2); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/ProviderTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/ProviderTest.java new file mode 100644 index 0000000..e66c09a --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/ProviderTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import android.content.Context; +import android.os.Build.VERSION_CODES; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileTypeWhichNeedsContext; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class ProviderTest { + + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final Context context = ApplicationProvider.getApplicationContext(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + + @Before + public void setup() { + testUtilities.startConnectingAndWait(); + } + + @Test + public void provideWithoutContext_doesNotThrowException() { + ProfileTestCrossProfileType.create(testProfileConnector).current().voidMethod(); + } + + @Test + public void provideWithContext_doesNotThrowException() { + ProfileTestCrossProfileTypeWhichNeedsContext.create(testProfileConnector) + .current() + .voidMethod(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/StaticTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/StaticTest.java new file mode 100644 index 0000000..5ad5534 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/StaticTest.java @@ -0,0 +1,241 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.Profile; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestExceptionCallbackListener; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.TestStringCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.TestStringCallbackListenerMultiImpl; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.FakeTestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.FakeProfileNonInstantiableTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileNonInstantiableTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testing.annotations.CrossProfileTest; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Tests specific to static @CrossProfile methods. + * + * <p>These tests are located here rather than with e.g. {@link BothProfilesAsyncTest} etc. because + * they will be used to also test that static methods can be used without instantiating the type. + */ +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +@CrossProfileTest(configuration = TestApplication.class) +public final class StaticTest { + + private static final String STRING = "string"; + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final ProfileNonInstantiableTestCrossProfileType type = + ProfileNonInstantiableTestCrossProfileType.create(testProfileConnector); + + private final FakeTestProfileConnector fakeConnector = new FakeTestProfileConnector(context); + private final FakeProfileNonInstantiableTestCrossProfileType fakeType = + FakeProfileNonInstantiableTestCrossProfileType.builder().connector(fakeConnector).build(); + + private final TestStringCallbackListenerImpl stringCallbackListener = + new TestStringCallbackListenerImpl(); + private final TestStringCallbackListenerMultiImpl stringCallbackMultiListener = + new TestStringCallbackListenerMultiImpl(); + private final TestExceptionCallbackListener exceptionCallbackListener = + new TestExceptionCallbackListener(); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, TestProfileConnector.class.getName()); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testUtilities.startConnectingAndWait(); + } + + @Test + public void staticCrossProfileMethod_blocking_other_works() throws Exception { + assertThat(type.other().staticIdentityStringMethod(STRING)).isEqualTo(STRING); + } + + @Test + public void staticCrossProfileMethod_blocking_current_works() { + assertThat(type.current().staticIdentityStringMethod(STRING)).isEqualTo(STRING); + } + + @Test + public void staticCrossProfileMethod_blocking_both_works() { + Map<Profile, String> result = type.both().staticIdentityStringMethod(STRING); + + assertThat(result).containsKey(testProfileConnector.utils().getCurrentProfile()); + assertThat(result).containsKey(testProfileConnector.utils().getOtherProfile()); + } + + @Test + public void staticCrossProfileMethod_fake_blocking_other_works() throws Exception { + fakeConnector.turnOnWorkProfile(); + fakeConnector.startConnecting(); + + assertThat(fakeType.other().staticIdentityStringMethod(STRING)).isEqualTo(STRING); + } + + @Test + public void staticCrossProfileMethod_fake_blocking_current_works() { + assertThat(fakeType.current().staticIdentityStringMethod(STRING)).isEqualTo(STRING); + } + + @Test + public void staticCrossProfileMethod_fake_blocking_both_works() { + fakeConnector.turnOnWorkProfile(); + fakeConnector.startConnecting(); + + Map<Profile, String> result = fakeType.both().staticIdentityStringMethod(STRING); + + assertThat(result).containsKey(testProfileConnector.utils().getCurrentProfile()); + assertThat(result).containsKey(testProfileConnector.utils().getOtherProfile()); + } + + @Test + public void staticCrossProfileMethod_async_other_works() { + type.other() + .staticAsyncIdentityStringMethod(STRING, stringCallbackListener, exceptionCallbackListener); + + assertThat(stringCallbackListener.stringCallbackValue).isEqualTo(STRING); + } + + @Test + public void staticCrossProfileMethod_async_current_works() { + type.current().staticAsyncIdentityStringMethod(STRING, stringCallbackListener); + + assertThat(stringCallbackListener.stringCallbackValue).isEqualTo(STRING); + } + + @Test + public void staticCrossProfileMethod_async_both_works() { + type.both().staticAsyncIdentityStringMethod(STRING, stringCallbackMultiListener); + + Map<Profile, String> result = stringCallbackMultiListener.stringCallbackValues; + assertThat(result).containsKey(testProfileConnector.utils().getCurrentProfile()); + assertThat(result).containsKey(testProfileConnector.utils().getOtherProfile()); + } + + @Test + public void staticCrossProfileMethod_fake_async_other_works() { + fakeConnector.turnOnWorkProfile(); + + fakeType + .other() + .staticAsyncIdentityStringMethod(STRING, stringCallbackListener, exceptionCallbackListener); + + assertThat(stringCallbackListener.stringCallbackValue).isEqualTo(STRING); + } + + @Test + public void staticCrossProfileMethod_fake_async_current_works() { + fakeType.current().staticAsyncIdentityStringMethod(STRING, stringCallbackListener); + + assertThat(stringCallbackListener.stringCallbackValue).isEqualTo(STRING); + } + + @Test + public void staticCrossProfileMethod_fake_async_both_works() { + fakeConnector.turnOnWorkProfile(); + + fakeType.both().staticAsyncIdentityStringMethod(STRING, stringCallbackMultiListener); + + Map<Profile, String> result = stringCallbackMultiListener.stringCallbackValues; + assertThat(result).containsKey(testProfileConnector.utils().getCurrentProfile()); + assertThat(result).containsKey(testProfileConnector.utils().getOtherProfile()); + } + + @Test + public void staticCrossProfileMethod_future_other_works() throws Exception { + ListenableFuture<String> result = type.other().staticFutureIdentityStringMethod(STRING); + + assertThat(result.get()).isEqualTo(STRING); + } + + @Test + public void staticCrossProfileMethod_future_current_works() throws Exception { + ListenableFuture<String> result = type.current().staticFutureIdentityStringMethod(STRING); + + assertThat(result.get()).isEqualTo(STRING); + } + + @Test + public void staticCrossProfileMethod_future_both_works() throws Exception { + ListenableFuture<Map<Profile, String>> resultFuture = + type.both().staticFutureIdentityStringMethod(STRING); + + Map<Profile, String> result = resultFuture.get(); + assertThat(result).containsKey(testProfileConnector.utils().getCurrentProfile()); + assertThat(result).containsKey(testProfileConnector.utils().getOtherProfile()); + } + + @Test + public void staticCrossProfileMethod_fake_future_other_works() throws Exception { + fakeConnector.turnOnWorkProfile(); + + ListenableFuture<String> result = fakeType.other().staticFutureIdentityStringMethod(STRING); + + assertThat(result.get()).isEqualTo(STRING); + } + + @Test + public void staticCrossProfileMethod_fake_future_current_works() throws Exception { + ListenableFuture<String> result = fakeType.current().staticFutureIdentityStringMethod(STRING); + + assertThat(result.get()).isEqualTo(STRING); + } + + @Test + public void staticCrossProfileMethod_fake_future_both_works() throws Exception { + fakeConnector.turnOnWorkProfile(); + + ListenableFuture<Map<Profile, String>> resultFuture = + fakeType.both().staticFutureIdentityStringMethod(STRING); + + Map<Profile, String> result = resultFuture.get(); + assertThat(result).containsKey(testProfileConnector.utils().getCurrentProfile()); + assertThat(result).containsKey(testProfileConnector.utils().getOtherProfile()); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/TypesTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/TypesTest.java new file mode 100644 index 0000000..ff5d66c --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/TypesTest.java @@ -0,0 +1,647 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import android.app.Service; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import android.util.Pair; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestCustomWrapperCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.TestExceptionCallbackListener; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.testapp.CustomWrapper; +import com.google.android.enterprise.connectedapps.testapp.CustomWrapper2; +import com.google.android.enterprise.connectedapps.testapp.ParcelableObject; +import com.google.android.enterprise.connectedapps.testapp.SerializableObject; +import com.google.android.enterprise.connectedapps.testapp.SimpleFuture; +import com.google.android.enterprise.connectedapps.testapp.StringWrapper; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType_SingleSenderCanThrow; +import com.google.android.enterprise.connectedapps.testapp.types.TestCrossProfileType; +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.ParameterizedRobolectricTestRunner; +import org.robolectric.Robolectric; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +@LooperMode(LooperMode.Mode.LEGACY) +@RunWith(ParameterizedRobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class TypesTest { + + private static final String STRING = "string"; + private static final byte BYTE = 1; + private static final Byte BYTE_BOXED = 1; + private static final short SHORT = 1; + private static final Short SHORT_BOXED = 1; + private static final int INT = 1; + private static final Integer INTEGER = 1; + private static final long LONG = 1; + private static final Long LONG_BOXED = 1L; + private static final float FLOAT = 1; + private static final Float FLOAT_BOXED = 1f; + private static final double DOUBLE = 1; + private static final Double DOUBLE_BOXED = 1d; + private static final char CHAR = 1; + private static final Character CHARACTER = 1; + private static final boolean BOOLEAN = true; + private static final Boolean BOOLEAN_BOXED = true; + private static final ParcelableObject PARCELABLE = new ParcelableObject("test"); + private static final SerializableObject SERIALIZABLE = new SerializableObject("test"); + private static final List<String> listOfString = Collections.singletonList(STRING); + private static final List<List<String>> listOfListOfString = ImmutableList.of(listOfString); + private static final List<ParcelableObject> listOfParcelable = ImmutableList.of(PARCELABLE); + private static final List<SerializableObject> listOfSerializable = ImmutableList.of(SERIALIZABLE); + private static final ImmutableMap<String, String> IMMUTABLE_MAP_STRING_TO_STRING = + ImmutableMap.of(STRING, STRING); + private static final Set<String> setOfString = ImmutableSet.of(STRING); + private static final Collection<String> collectionOfString = ImmutableList.of(STRING); + // private static final TestProto PROTO = TestProto.newBuilder().setText(STRING).build(); + // private static final List<TestProto> listOfProto = ImmutableList.of(PROTO); + private static final String[] arrayOfString = new String[] {STRING}; + private static final Collection<String[]> collectionOfStringArray = + ImmutableList.of(arrayOfString); + private static final ParcelableObject[] arrayOfParcelable = new ParcelableObject[] {PARCELABLE}; + private static final SerializableObject[] arrayOfSerializable = + new SerializableObject[] {SERIALIZABLE}; + private static final Collection<ParcelableObject[]> collectionOfParcelableArray = + ImmutableList.of(arrayOfParcelable); + private static final Collection<SerializableObject[]> collectionOfSerializableArray = + ImmutableList.of(arrayOfSerializable); + // private static final TestProto[] arrayOfProto = new TestProto[] {PROTO}; + private static final String[] emptyStringArray = new String[] {}; + private static final CustomWrapper<String> CUSTOM_WRAPPER = new CustomWrapper<>(STRING); + private static final CustomWrapper2<String> CUSTOM_WRAPPER2 = new CustomWrapper2<>(STRING); + private static final StringWrapper STRING_WRAPPER = new StringWrapper(STRING); + private static final Optional<ParcelableObject> GUAVA_OPTIONAL = Optional.of(PARCELABLE); + private static final int[] BITMAP_PIXELS = {1, 2, 3, 4, 5, 6, 7, 8, 9}; + + private final Application context = ApplicationProvider.getApplicationContext(); + // Android type can't be static due to Robolectric + private final Pair<String, Integer> pair = new Pair<>(STRING, INTEGER); + private final Bitmap bitmap = Bitmap.createBitmap(BITMAP_PIXELS, 3, 3, Bitmap.Config.ARGB_8888); + + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + + private interface SenderProvider { + ProfileTestCrossProfileType_SingleSenderCanThrow provide( + Context context, TestProfileConnector testProfileConnector); + } + + @ParameterizedRobolectricTestRunner.Parameters(name = "{0}") + public static Collection<Object[]> data() { + + SenderProvider currentProfileSenderProvider = + (Context context, TestProfileConnector testProfileConnector) -> + (ProfileTestCrossProfileType_SingleSenderCanThrow) + ProfileTestCrossProfileType.create(testProfileConnector).current(); + SenderProvider otherProfileSenderProvider = + (Context context, TestProfileConnector testProfileConnector) -> + ProfileTestCrossProfileType.create(testProfileConnector).other(); + + return Arrays.asList( + new Object[][] { + {"CurrentProfile", currentProfileSenderProvider}, + {"OtherProfile", otherProfileSenderProvider}, + }); + } + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, TestProfileConnector.class.getName()); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testUtilities.startConnectingAndWait(); + } + + private SenderProvider senderProvider; + + public TypesTest(String profile, SenderProvider senderProvider) { + this.senderProvider = senderProvider; + } + + @Test + public void voidMethodWithNoArguments_callsMethod() throws UnavailableProfileException { + TestCrossProfileType.voidMethodCalls = 0; + + senderProvider.provide(context, testProfileConnector).voidMethod(); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void voidMethodWithArguments_callsMethod() throws UnavailableProfileException { + TestCrossProfileType.voidMethodCalls = 0; + + senderProvider.provide(context, testProfileConnector).voidMethod("argument"); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void stringReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).identityStringMethod(STRING)) + .isEqualTo(STRING); + } + + @Test + public void byteReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).identityByteMethod(BYTE)) + .isEqualTo(BYTE); + } + + @Test + public void boxedByteReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).identityByteMethod(BYTE_BOXED)) + .isEqualTo(BYTE_BOXED); + } + + @Test + public void shortReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).identityShortMethod(SHORT)) + .isEqualTo(SHORT); + } + + @Test + public void boxedShortReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat( + senderProvider.provide(context, testProfileConnector).identityShortMethod(SHORT_BOXED)) + .isEqualTo(SHORT_BOXED); + } + + @Test + public void intReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).identityIntMethod(INT)) + .isEqualTo(INT); + } + + @Test + public void integerReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).identityIntegerMethod(INTEGER)) + .isEqualTo(INTEGER); + } + + @Test + public void longReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).identityLongMethod(LONG)) + .isEqualTo(LONG); + } + + @Test + public void boxedLongReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).identityLongMethod(LONG_BOXED)) + .isEqualTo(LONG_BOXED); + } + + @Test + public void floatReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).identityFloatMethod(FLOAT)) + .isEqualTo(FLOAT); + } + + @Test + public void boxedFloatReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat( + senderProvider.provide(context, testProfileConnector).identityFloatMethod(FLOAT_BOXED)) + .isEqualTo(FLOAT_BOXED); + } + + @Test + public void doubleReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).identityDoubleMethod(DOUBLE)) + .isEqualTo(DOUBLE); + } + + @Test + public void boxedDoubleReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityDoubleMethod(DOUBLE_BOXED)) + .isEqualTo(DOUBLE_BOXED); + } + + @Test + public void charReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).identityCharMethod(CHAR)) + .isEqualTo(CHAR); + } + + @Test + public void characterReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityCharacterMethod(CHARACTER)) + .isEqualTo(CHARACTER); + } + + @Test + public void booleanReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).identityBooleanMethod(BOOLEAN)) + .isEqualTo(BOOLEAN); + } + + @Test + public void boxedBooleanReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityBooleanMethod(BOOLEAN_BOXED)) + .isEqualTo(BOOLEAN_BOXED); + } + + @Test + public void parcelableReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityParcelableMethod(PARCELABLE)) + .isEqualTo(PARCELABLE); + } + + @Test + public void serializableReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identitySerializableObjectMethod(SERIALIZABLE)) + .isEqualTo(SERIALIZABLE); + } + + @Test + public void parcelableWrapperOfParcelableTypeAndArgument_bothWork() + throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityParcelableWrapperOfParcelableMethod(listOfParcelable)) + .isEqualTo(listOfParcelable); + } + + @Test + public void parcelableWrapperOfSerializableTypeAndArgument_bothWork() + throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityParcelableWrapperOfSerializableMethod(listOfSerializable)) + .isEqualTo(listOfSerializable); + } + + @Test + public void parcelableWrapperOfParcelableWrapperTypeAndArgument_bothWork() + throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityParcelableWrapperOfParcelableWrapperMethod(listOfListOfString)) + .isEqualTo(listOfListOfString); + } + + @Test + public void listReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat( + senderProvider.provide(context, testProfileConnector).identityListMethod(listOfString)) + .isEqualTo(listOfString); + } + + @Test + public void mapReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityMapMethod(IMMUTABLE_MAP_STRING_TO_STRING)) + .isEqualTo(IMMUTABLE_MAP_STRING_TO_STRING); + } + + @Test + public void immutableMapReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityImmutableMapMethod(IMMUTABLE_MAP_STRING_TO_STRING)) + .isEqualTo(IMMUTABLE_MAP_STRING_TO_STRING); + } + + @Test + public void setReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).identitySetMethod(setOfString)) + .isEqualTo(setOfString); + } + + @Test + public void collectionReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityCollectionMethod(collectionOfString)) + .containsExactlyElementsIn(collectionOfString); + } + + @Test + public void arrayReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityStringArrayMethod(arrayOfString)) + .asList() + .containsExactlyElementsIn(arrayOfString); + } + + @Test + public void collectionOfArrayReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + ProfileTestCrossProfileType_SingleSenderCanThrow sender = + senderProvider.provide(context, testProfileConnector); + + List<String[]> originalAsList = new ArrayList<>(collectionOfStringArray); + List<String[]> resultAsList = + new ArrayList<>(sender.identityCollectionOfStringArrayMethod(collectionOfStringArray)); + + assertThat(sender.identityCollectionOfStringArrayMethod(collectionOfStringArray)) + .hasSize(collectionOfStringArray.size()); + for (int i = 0; i < collectionOfStringArray.size(); i++) { + assertThat(resultAsList.get(i)).asList().containsExactlyElementsIn(originalAsList.get(i)); + } + } + + @Test + public void parcelableArrayReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityParcelableObjectArrayMethod(arrayOfParcelable)) + .asList() + .containsExactlyElementsIn(arrayOfParcelable); + } + + @Test + public void serializableArrayReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identitySerializableObjectArrayMethod(arrayOfSerializable)) + .asList() + .containsExactlyElementsIn(arrayOfSerializable); + } + + @Test + public void collectionOfParcelableArrayReturnTypeAndArgument_bothWork() + throws UnavailableProfileException { + ProfileTestCrossProfileType_SingleSenderCanThrow sender = + senderProvider.provide(context, testProfileConnector); + + List<ParcelableObject[]> originalAsList = new ArrayList<>(collectionOfParcelableArray); + List<ParcelableObject[]> resultAsList = + new ArrayList<>( + sender.identityCollectionOfParcelableObjectArrayMethod(collectionOfParcelableArray)); + + assertThat(resultAsList).hasSize(collectionOfParcelableArray.size()); + for (int i = 0; i < collectionOfParcelableArray.size(); i++) { + assertThat(resultAsList.get(i)).asList().containsExactlyElementsIn(originalAsList.get(i)); + } + } + + @Test + public void collectionOfSerializableArrayReturnTypeAndArgument_bothWork() + throws UnavailableProfileException { + ProfileTestCrossProfileType_SingleSenderCanThrow sender = + senderProvider.provide(context, testProfileConnector); + + List<SerializableObject[]> originalAsList = new ArrayList<>(collectionOfSerializableArray); + List<SerializableObject[]> resultAsList = + new ArrayList<>( + sender.identityCollectionOfSerializableObjectArrayMethod( + collectionOfSerializableArray)); + + assertThat(resultAsList).hasSize(collectionOfSerializableArray.size()); + for (int i = 0; i < collectionOfSerializableArray.size(); i++) { + assertThat(resultAsList.get(i)).asList().containsExactlyElementsIn(originalAsList.get(i)); + } + } + + @Test + public void pairReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).identityPairMethod(pair)) + .isEqualTo(pair); + } + + @Test + public void optionalReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityGuavaOptionalMethod(GUAVA_OPTIONAL)) + .isEqualTo(GUAVA_OPTIONAL); + } + + // TODO: Disabled because use of Optional fails lint check. Re-enable when this is disabled. + // @Test + // public void optionalReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + // assertThat(senderProvider.provide(context, + // testProfileConnector).identityOptionalMethod(OPTIONAL_OF_STRING)).isEqualTo(OPTIONAL_OF_STRING); + // } + + @Test + public void voidObjectReturnType_works() throws UnavailableProfileException { + TestCrossProfileType.voidMethodCalls = 0; + + assertThat(senderProvider.provide(context, testProfileConnector).identityVoidMethod()) + .isEqualTo(null); + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void methodWhichReturnsNull_works() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).getNull()).isNull(); + } + + @Test + public void methodWhichReturnsNullCollection_works() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).getNullCollection()).isNull(); + } + + @Test + public void methodWhichReturnsNullList_works() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).getNullList()).isNull(); + } + + @Test + public void methodWhichReturnsNullMap_works() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).getNullMap()).isNull(); + } + + // @Test + // public void methodWhichReturnsNullOptional_works() throws UnavailableProfileException { + // assertThat(senderProvider.provide(context, testProfileConnector).getNullOptional()).isNull(); + // } + + @Test + public void methodWhichReturnsNullSet_works() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).getNullSet()).isNull(); + } + + // @Test + // public void methodWhichReturnsNullProto_works() throws UnavailableProfileException { + // assertThat(senderProvider.provide(context, testProfileConnector).getNullProto()).isNull(); + // } + + @Test + public void emptyArray_works() throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityStringArrayMethod(emptyStringArray)) + .asList() + .containsExactlyElementsIn(emptyStringArray); + } + + @Test + public void nullArray_works() throws UnavailableProfileException { + assertThat( + senderProvider.provide(context, testProfileConnector).identityStringArrayMethod(null)) + .isNull(); + } + + @Test + public void customParcelableWrapperDefinedOnTypeReturnTypeAndArgument_bothWork() + throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityCustomWrapper2Method(CUSTOM_WRAPPER2)) + .isEqualTo(CUSTOM_WRAPPER2); + } + + @Test + public void customParcelableWrapperReturnTypeAndArgument_bothWork() + throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityCustomWrapperMethod(CUSTOM_WRAPPER)) + .isEqualTo(CUSTOM_WRAPPER); + } + + @Test + public void customParcelableWrapperFutureReturnType_works() + throws ExecutionException, InterruptedException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .listenableFutureIdentityCustomWrapperMethod(CUSTOM_WRAPPER) + .get()) + .isEqualTo(CUSTOM_WRAPPER); + } + + @Test + public void customParcelableWrapperAsyncMethod_works() { + TestCustomWrapperCallbackListenerImpl callbackListener = + new TestCustomWrapperCallbackListenerImpl(); + TestExceptionCallbackListener exceptionListener = new TestExceptionCallbackListener(); + + senderProvider + .provide(context, testProfileConnector) + .asyncIdentityCustomWrapperMethod(CUSTOM_WRAPPER, callbackListener, exceptionListener); + + assertThat(callbackListener.customWrapperCallbackValue).isEqualTo(CUSTOM_WRAPPER); + } + + @Test + public void customFutureWrapper_works() { + SimpleFuture<String> future = + senderProvider + .provide(context, testProfileConnector) + .simpleFutureIdentityStringMethodWithNonBlockingDelay(STRING, /* secondsDelay= */ 5); + testUtilities.advanceTimeBySeconds(10); + + assertThat(future.get()).isEqualTo(STRING); + } + + @Test + public void parcelableWrapperWithoutGenericReturnTypeAndArgument_bothWork() + throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityStringWrapperMethod(STRING_WRAPPER)) + .isEqualTo(STRING_WRAPPER); + } + + @Test + public void bitmapReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + Bitmap returnBitmap = + senderProvider.provide(context, testProfileConnector).identityBitmapMethod(bitmap); + + assertThat(returnBitmap.getConfig()).isEqualTo(bitmap.getConfig()); + assertThat(returnBitmap.getWidth()).isEqualTo(bitmap.getWidth()); + assertThat(returnBitmap.getHeight()).isEqualTo(bitmap.getHeight()); + assertThat(getBitmapPixels(returnBitmap)).isEqualTo(BITMAP_PIXELS); + } + + @Test + public void nullBitmap_works() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).identityBitmapMethod(null)) + .isNull(); + } + + private static int[] getBitmapPixels(Bitmap bitmap) { + int[] pixels = new int[bitmap.getHeight() * bitmap.getWidth()]; + bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight()); + return pixels; + } + + @Test + public void contextArgument_works() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).isContextArgumentPassed()) + .isTrue(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/testing/AbstractFakeProfileConnectorTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/testing/AbstractFakeProfileConnectorTest.java new file mode 100644 index 0000000..f42d89b --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/testing/AbstractFakeProfileConnectorTest.java @@ -0,0 +1,407 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testing; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import android.content.Context; +import android.os.Build.VERSION_CODES; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.ConnectedAppsUtils; +import com.google.android.enterprise.connectedapps.TestAvailabilityListener; +import com.google.android.enterprise.connectedapps.TestConnectionListener; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector.ProfileType; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class AbstractFakeProfileConnectorTest { + static class FakeProfileConnector extends AbstractFakeProfileConnector { + FakeProfileConnector(Context context, ProfileType primaryProfileType) { + super(context, primaryProfileType); + } + } + + private final Context context = ApplicationProvider.getApplicationContext(); + private final FakeProfileConnector fakeProfileConnector = + new FakeProfileConnector(context, /* primaryProfileType= */ ProfileType.NONE); + private final TestAvailabilityListener availabilityListener = new TestAvailabilityListener(); + private final TestConnectionListener connectionListener = new TestConnectionListener(); + + @Test + public void startConnecting_connectionIsAvailable_isConnected() { + fakeProfileConnector.turnOnWorkProfile(); + + fakeProfileConnector.startConnecting(); + + assertThat(fakeProfileConnector.isConnected()).isTrue(); + } + + @Test + public void startConnecting_connectionIsAvailable_notifiesConnectionChanged() { + fakeProfileConnector.turnOnWorkProfile(); + fakeProfileConnector.registerConnectionListener(connectionListener); + + fakeProfileConnector.startConnecting(); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(1); + } + + @Test + public void startConnecting_unregisteredConnectionListener_doesNotNotifyConnectionChanged() { + fakeProfileConnector.turnOnWorkProfile(); + fakeProfileConnector.registerConnectionListener(connectionListener); + fakeProfileConnector.unregisterConnectionListener(connectionListener); + + fakeProfileConnector.startConnecting(); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(0); + } + + @Test + public void startConnecting_connectionIsNotAvailable_doesNotNotifyOfConnectionChanged() { + fakeProfileConnector.removeWorkProfile(); + fakeProfileConnector.registerConnectionListener(connectionListener); + + fakeProfileConnector.startConnecting(); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(0); + } + + @Test + public void connect_connectionIsAvailable_isConnected() throws Exception { + fakeProfileConnector.turnOnWorkProfile(); + + fakeProfileConnector.connect(); + + assertThat(fakeProfileConnector.isConnected()).isTrue(); + } + + @Test + public void connect_connectionIsAvailable_notifiesConnectionChanged() throws Exception { + fakeProfileConnector.turnOnWorkProfile(); + fakeProfileConnector.registerConnectionListener(connectionListener); + + fakeProfileConnector.connect(); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(1); + } + + @Test + public void connect_unregisteredConnectionListener_doesNotNotifyConnectionChanged() + throws Exception { + fakeProfileConnector.turnOnWorkProfile(); + fakeProfileConnector.registerConnectionListener(connectionListener); + fakeProfileConnector.unregisterConnectionListener(connectionListener); + + fakeProfileConnector.connect(); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(0); + } + + @Test + public void connect_connectionIsNotAvailable_throwsUnavailableProfileException() { + fakeProfileConnector.removeWorkProfile(); + fakeProfileConnector.registerConnectionListener(connectionListener); + + assertThrows(UnavailableProfileException.class, fakeProfileConnector::connect); + } + + @Test + public void turnOnWorkProfile_workProfileWasOff_notifiesAvailabilityChange() { + fakeProfileConnector.registerAvailabilityListener(availabilityListener); + + fakeProfileConnector.turnOnWorkProfile(); + + assertThat(availabilityListener.availabilityChangedCount()).isEqualTo(1); + } + + @Test + public void turnOnWorkProfile_workProfileWasOn_doesNotNotifyAvailabilityChange() { + fakeProfileConnector.turnOnWorkProfile(); + fakeProfileConnector.registerAvailabilityListener(availabilityListener); + + fakeProfileConnector.turnOnWorkProfile(); + + assertThat(availabilityListener.availabilityChangedCount()).isEqualTo(0); + } + + @Test + public void turnOffWorkProfile_workProfileWasOn_notifiesAvailabilityChange() { + fakeProfileConnector.turnOnWorkProfile(); + fakeProfileConnector.registerAvailabilityListener(availabilityListener); + + fakeProfileConnector.turnOffWorkProfile(); + + assertThat(availabilityListener.availabilityChangedCount()).isEqualTo(1); + } + + @Test + public void turnOffWorkProfile_workProfileWasOff_doesNotNotifyAvailabilityChange() { + fakeProfileConnector.turnOffWorkProfile(); + fakeProfileConnector.registerAvailabilityListener(availabilityListener); + + fakeProfileConnector.turnOffWorkProfile(); + + assertThat(availabilityListener.availabilityChangedCount()).isEqualTo(0); + } + + @Test + public void turnOffWorkProfile_wasConnected_notifiesConnectionChange() throws Exception { + fakeProfileConnector.turnOnWorkProfile(); + fakeProfileConnector.connect(); + fakeProfileConnector.registerConnectionListener(connectionListener); + + fakeProfileConnector.turnOffWorkProfile(); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(1); + } + + @Test + public void setRunningOnProfile_setsRunningOnProfile() { + fakeProfileConnector.setRunningOnProfile(ProfileType.WORK); + + assertThat(fakeProfileConnector.runningOnProfile()).isEqualTo(ProfileType.WORK); + } + + @Test + public void setRunningOnWorkProfile_startsWorkProfile() { + fakeProfileConnector.setRunningOnProfile(ProfileType.WORK); + fakeProfileConnector.setRunningOnProfile(ProfileType.PERSONAL); + + assertThat(fakeProfileConnector.isAvailable()).isTrue(); + } + + @Test + public void removeWorkProfile_workProfileBecomesUnavailable() { + fakeProfileConnector.turnOnWorkProfile(); + fakeProfileConnector.removeWorkProfile(); + + assertThat(fakeProfileConnector.isAvailable()).isFalse(); + } + + @Test + public void isConnected_isConnected_returnsTrue() throws Exception { + fakeProfileConnector.turnOnWorkProfile(); + + fakeProfileConnector.connect(); + + assertThat(fakeProfileConnector.isConnected()).isTrue(); + } + + @Test + public void isConnected_isNotConnected_returnsFalse() { + fakeProfileConnector.turnOnWorkProfile(); + + fakeProfileConnector.disconnect(); + + assertThat(fakeProfileConnector.isConnected()).isFalse(); + } + + @Test + public void getCurrentProfile_getOtherProfile_areDifferent() { + ConnectedAppsUtils utils = fakeProfileConnector.utils(); + assertThat(utils.getCurrentProfile()).isNotEqualTo(utils.getOtherProfile()); + } + + @Test + public void getWorkProfile_runningOnWorkProfile_returnsCurrent() { + ConnectedAppsUtils utils = fakeProfileConnector.utils(); + fakeProfileConnector.setRunningOnProfile(ProfileType.WORK); + + assertThat(utils.getWorkProfile()).isEqualTo(utils.getCurrentProfile()); + } + + @Test + public void getWorkProfile_runningOnPersonalProfile_returnsOther() { + ConnectedAppsUtils utils = fakeProfileConnector.utils(); + fakeProfileConnector.setRunningOnProfile(ProfileType.PERSONAL); + + assertThat(utils.getWorkProfile()).isEqualTo(utils.getOtherProfile()); + } + + @Test + public void getPersonalProfile_runningOnPersonalProfile_returnsCurrent() { + ConnectedAppsUtils utils = fakeProfileConnector.utils(); + fakeProfileConnector.setRunningOnProfile(ProfileType.PERSONAL); + + assertThat(utils.getPersonalProfile()).isEqualTo(utils.getCurrentProfile()); + } + + @Test + public void getPersonalProfile_runningOnWorkProfile_returnsOther() { + ConnectedAppsUtils utils = fakeProfileConnector.utils(); + fakeProfileConnector.setRunningOnProfile(ProfileType.WORK); + + assertThat(utils.getPersonalProfile()).isEqualTo(utils.getOtherProfile()); + } + + @Test + public void getPrimaryProfile_noPrimaryProfileSet_throwsIllegalStateException() { + FakeProfileConnector fakeProfileConnector = + new FakeProfileConnector(context, /* primaryProfileType= */ ProfileType.NONE); + ConnectedAppsUtils utils = fakeProfileConnector.utils(); + + assertThrows(IllegalStateException.class, () -> utils.getPrimaryProfile()); + } + + @Test + public void getSecondaryProfile_noPrimaryProfileSet_throwsIllegalStateException() { + FakeProfileConnector fakeProfileConnector = + new FakeProfileConnector(context, /* primaryProfileType= */ ProfileType.NONE); + ConnectedAppsUtils utils = fakeProfileConnector.utils(); + + assertThrows(IllegalStateException.class, () -> utils.getSecondaryProfile()); + } + + @Test + public void getPrimaryProfile_primaryProfileSetToWork_returnsWork() { + FakeProfileConnector fakeProfileConnector = + new FakeProfileConnector(context, /* primaryProfileType= */ ProfileType.WORK); + ConnectedAppsUtils utils = fakeProfileConnector.utils(); + + assertThat(utils.getPrimaryProfile()).isEqualTo(utils.getWorkProfile()); + } + + @Test + public void getPrimaryProfile_primaryProfileSetToPersonal_returnsPersonal() { + FakeProfileConnector fakeProfileConnector = + new FakeProfileConnector(context, /* primaryProfileType= */ ProfileType.PERSONAL); + ConnectedAppsUtils utils = fakeProfileConnector.utils(); + + assertThat(utils.getPrimaryProfile()).isEqualTo(utils.getPersonalProfile()); + } + + @Test + public void getSecondaryProfile_primaryProfileSetToWork_returnsPersonal() { + FakeProfileConnector fakeProfileConnector = + new FakeProfileConnector(context, /* primaryProfileType= */ ProfileType.WORK); + ConnectedAppsUtils utils = fakeProfileConnector.utils(); + + assertThat(utils.getSecondaryProfile()).isEqualTo(utils.getPersonalProfile()); + } + + @Test + public void getSecondaryProfile_primaryProfileSetToPersonal_returnsWork() { + FakeProfileConnector fakeProfileConnector = + new FakeProfileConnector(context, /* primaryProfileType= */ ProfileType.PERSONAL); + ConnectedAppsUtils utils = fakeProfileConnector.utils(); + + assertThat(utils.getSecondaryProfile()).isEqualTo(utils.getWorkProfile()); + } + + @Test + public void runningOnWork_runningOnWork_returnsTrue() { + fakeProfileConnector.setRunningOnProfile(ProfileType.WORK); + + assertThat(fakeProfileConnector.utils().runningOnWork()).isTrue(); + } + + @Test + public void runningOnWork_runningOnPersonal_returnsFalse() { + fakeProfileConnector.setRunningOnProfile(ProfileType.PERSONAL); + + assertThat(fakeProfileConnector.utils().runningOnWork()).isFalse(); + } + + @Test + public void runningOnPersonal_runningOnPersonal_returnsTrue() { + fakeProfileConnector.setRunningOnProfile(ProfileType.PERSONAL); + + assertThat(fakeProfileConnector.utils().runningOnPersonal()).isTrue(); + } + + @Test + public void runningOnPersonal_runningOnWork_returnsFalse() { + fakeProfileConnector.setRunningOnProfile(ProfileType.WORK); + + assertThat(fakeProfileConnector.utils().runningOnPersonal()).isFalse(); + } + + @Test + public void canMakeCrossProfileCalls_defaultsToTrue() { + assertThat(fakeProfileConnector.permissions().canMakeCrossProfileCalls()).isTrue(); + } + + @Test + public void canMakeCrossProfileCalls_setToFalse_returnsFalse() { + fakeProfileConnector.setHasPermissionToMakeCrossProfileCalls(false); + + assertThat(fakeProfileConnector.permissions().canMakeCrossProfileCalls()).isFalse(); + } + + @Test + public void canMakeCrossProfileCalls_setToTrue_returnsTrue() { + fakeProfileConnector.setHasPermissionToMakeCrossProfileCalls(false); + fakeProfileConnector.setHasPermissionToMakeCrossProfileCalls(true); + + assertThat(fakeProfileConnector.permissions().canMakeCrossProfileCalls()).isTrue(); + } + + @Test + public void isManuallyManagingConnection_returnsFalse() { + assertThat(fakeProfileConnector.isManuallyManagingConnection()).isFalse(); + } + + @Test + public void isManuallyManagingConnection_hasStartedManuallyConnecting_returnsTrue() { + fakeProfileConnector.startConnecting(); + + assertThat(fakeProfileConnector.isManuallyManagingConnection()).isTrue(); + } + + @Test + public void isManuallyManagingConnection_hasManuallyConnected_returnsTrue() throws Exception { + fakeProfileConnector.turnOnWorkProfile(); + fakeProfileConnector.connect(); + + assertThat(fakeProfileConnector.isManuallyManagingConnection()).isTrue(); + } + + @Test + public void isManuallyManagingConnection_hasCalledStopManualConnectionManagement_returnsFalse() { + fakeProfileConnector.startConnecting(); + + fakeProfileConnector.stopManualConnectionManagement(); + + assertThat(fakeProfileConnector.isManuallyManagingConnection()).isFalse(); + } + + @Test + public void timeoutConnection_isManuallyManagingConnection_doesNotDisconnect() throws Exception { + fakeProfileConnector.turnOnWorkProfile(); + fakeProfileConnector.connect(); + + fakeProfileConnector.timeoutConnection(); + + assertThat(fakeProfileConnector.isConnected()).isTrue(); + } + + @Test + public void timeoutConnection_isNotManuallyManagingConnection_disconnects() { + fakeProfileConnector.turnOnWorkProfile(); + fakeProfileConnector.stopManualConnectionManagement(); + + fakeProfileConnector.timeoutConnection(); + + assertThat(fakeProfileConnector.isConnected()).isFalse(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/testing/FakeCrossProfileTypeTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/testing/FakeCrossProfileTypeTest.java new file mode 100644 index 0000000..3599dd6 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/testing/FakeCrossProfileTypeTest.java @@ -0,0 +1,821 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testing; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.assertFutureHasException; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.fail; + +import android.content.Context; +import android.os.Build.VERSION_CODES; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.NonSimpleCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.Profile; +import com.google.android.enterprise.connectedapps.TestBooleanCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.TestBooleanCallbackListenerMultiImpl; +import com.google.android.enterprise.connectedapps.TestConnectionListener; +import com.google.android.enterprise.connectedapps.TestExceptionCallbackListener; +import com.google.android.enterprise.connectedapps.TestStringCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.TestVoidCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector.ProfileType; +import com.google.android.enterprise.connectedapps.exceptions.ProfileRuntimeException; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.testapp.CustomRuntimeException; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.FakeTestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.FakeProfileTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.TestCrossProfileType; +import com.google.android.enterprise.connectedapps.testing.annotations.CrossProfileTest; +import com.google.common.util.concurrent.ListenableFuture; +import java.io.IOException; +import java.sql.SQLException; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@CrossProfileTest(configuration = TestApplication.class) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class FakeCrossProfileTypeTest { + + private static final String STRING = "String"; + private static final String STRING2 = "String2"; + + private final Context context = ApplicationProvider.getApplicationContext(); + private final FakeTestProfileConnector connector = new FakeTestProfileConnector(context); + private final TestCrossProfileType personal = new TestCrossProfileType(); + private final TestCrossProfileType work = new TestCrossProfileType(); + + private final TestVoidCallbackListenerImpl voidCallbackListener = + new TestVoidCallbackListenerImpl(); + private final TestExceptionCallbackListener exceptionCallbackListener = + new TestExceptionCallbackListener(); + private final TestStringCallbackListenerImpl stringCallbackListener = + new TestStringCallbackListenerImpl(); + private final TestBooleanCallbackListenerImpl booleanCallbackListener = + new TestBooleanCallbackListenerImpl(); + private final TestBooleanCallbackListenerMultiImpl booleanMultiCallbackListener = + new TestBooleanCallbackListenerMultiImpl(); + private final NonSimpleCallbackListenerImpl nonSimpleCallbackListener = + new NonSimpleCallbackListenerImpl(); + private final TestConnectionListener connectionListener = new TestConnectionListener(); + + FakeProfileTestCrossProfileType fakeCrossProfileType = + FakeProfileTestCrossProfileType.builder() + .personal(personal) + .work(work) + .connector(connector) + .build(); + + @Before + public void setUp() { + connector.setRunningOnProfile(ProfileType.PERSONAL); + connector.turnOnWorkProfile(); + connector.startConnecting(); + connector.registerConnectionListener(connectionListener); + } + + @Test + public void personal_callsOnPersonal() throws UnavailableProfileException { + fakeCrossProfileType.personal().voidMethod(); + + assertThat(personal.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void personal_doesNotCallOnWork() throws UnavailableProfileException { + fakeCrossProfileType.personal().voidMethod(); + + assertThat(work.voidMethodInstanceCalls).isEqualTo(0); + } + + @Test + public void work_callsOnWork() throws UnavailableProfileException { + fakeCrossProfileType.work().voidMethod(); + + assertThat(work.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void work_doesNotCallOnPersonal() throws UnavailableProfileException { + fakeCrossProfileType.work().voidMethod(); + + assertThat(personal.voidMethodInstanceCalls).isEqualTo(0); + } + + @Test + public void current_runningOnPersonal_callsPersonal() { + connector.setRunningOnProfile(ProfileType.PERSONAL); + fakeCrossProfileType.current().voidMethod(); + + assertThat(personal.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void current_runningOnWork_callsWork() { + connector.setRunningOnProfile(ProfileType.WORK); + fakeCrossProfileType.current().voidMethod(); + + assertThat(work.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void other_runningOnPersonal_callsWork() throws UnavailableProfileException { + connector.setRunningOnProfile(ProfileType.PERSONAL); + fakeCrossProfileType.other().voidMethod(); + + assertThat(work.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void other_runningOnWork_callsPersonal() throws UnavailableProfileException { + connector.setRunningOnProfile(ProfileType.WORK); + fakeCrossProfileType.other().voidMethod(); + + assertThat(personal.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void both_callsBoth() { + fakeCrossProfileType.both().voidMethod(); + + assertThat(personal.voidMethodInstanceCalls).isEqualTo(1); + assertThat(work.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void primary_callsPrimary() throws UnavailableProfileException { + // Work is primary for TestProfileConnector + fakeCrossProfileType.primary().voidMethod(); + + assertThat(work.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void secondary_callsSecondary() throws UnavailableProfileException { + // Work is primary for TestProfileConnector + fakeCrossProfileType.secondary().voidMethod(); + + assertThat(personal.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void suppliers_runningOnPrimary_callsPrimaryAndSecondary() { + // Work is primary for TestProfileConnector + connector.setRunningOnProfile(ProfileType.WORK); + + fakeCrossProfileType.suppliers().voidMethod(); + + assertThat(personal.voidMethodInstanceCalls).isEqualTo(1); + assertThat(work.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void suppliers_runningOnSecondary_onlyCallsSecondary() { + // Work is primary for TestProfileConnector + connector.setRunningOnProfile(ProfileType.PERSONAL); + + fakeCrossProfileType.suppliers().voidMethod(); + + assertThat(personal.voidMethodInstanceCalls).isEqualTo(1); + assertThat(work.voidMethodInstanceCalls).isEqualTo(0); + } + + @Test + public void profile_specifiesCurrent_callsCurrent() throws UnavailableProfileException { + connector.setRunningOnProfile(ProfileType.PERSONAL); + + fakeCrossProfileType.profile(connector.utils().getCurrentProfile()).voidMethod(); + + assertThat(personal.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void profile_specifiesOther_callsOther() throws UnavailableProfileException { + connector.setRunningOnProfile(ProfileType.PERSONAL); + + fakeCrossProfileType.profile(connector.utils().getOtherProfile()).voidMethod(); + + assertThat(work.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void profile_specifiesPersonal_callsPersonal() throws UnavailableProfileException { + fakeCrossProfileType.profile(connector.utils().getPersonalProfile()).voidMethod(); + + assertThat(personal.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void profile_specifiesWork_callsWork() throws UnavailableProfileException { + fakeCrossProfileType.profile(connector.utils().getWorkProfile()).voidMethod(); + + assertThat(work.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void profile_specifiesPrimary_callsPrimary() throws UnavailableProfileException { + // Work is primary for TestProfileConnector + fakeCrossProfileType.profile(connector.utils().getPrimaryProfile()).voidMethod(); + + assertThat(work.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void profile_specifiesSecondary_callsSecondary() throws UnavailableProfileException { + // Work is primary for TestProfileConnector + fakeCrossProfileType.profile(connector.utils().getSecondaryProfile()).voidMethod(); + + assertThat(personal.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void profiles_callsSpecifiedProfiles() { + fakeCrossProfileType + .profiles(connector.utils().getPersonalProfile(), connector.utils().getWorkProfile()) + .voidMethod(); + + assertThat(personal.voidMethodInstanceCalls).isEqualTo(1); + assertThat(work.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void build_noPersonalSpecified_throwsIllegalStateException() { + assertThrows( + IllegalStateException.class, + () -> FakeProfileTestCrossProfileType.builder().connector(connector).work(work).build()); + } + + @Test + public void build_noWorkSpecified_throwsIllegalStateException() { + assertThrows( + IllegalStateException.class, + () -> + FakeProfileTestCrossProfileType.builder() + .connector(connector) + .personal(personal) + .build()); + } + + @Test + public void build_noConnectorSpecified_throwsIllegalStateException() { + assertThrows( + IllegalStateException.class, + () -> FakeProfileTestCrossProfileType.builder().personal(personal).work(work).build()); + } + + @Test + public void blockingCall_workProfileNotConnected_throwsUnavailableProfileException() { + connector.disconnect(); + + assertThrows(UnavailableProfileException.class, () -> fakeCrossProfileType.work().voidMethod()); + } + + @Test + public void blockingCall_notManuallyManagingConnection_throwsUnavailableProfileException() + throws Exception { + connector.stopManualConnectionManagement(); + connector.turnOnWorkProfile(); + fakeCrossProfileType.other().listenableFutureVoidMethod().get(); // Force connection + + assertThrows( + UnavailableProfileException.class, () -> fakeCrossProfileType.other().voidMethod()); + } + + @Test + public void asyncCall_workProfileAvailableButNotConnected_works() { + connector.turnOnWorkProfile(); + connector.disconnect(); + + fakeCrossProfileType.work().asyncVoidMethod(voidCallbackListener, exceptionCallbackListener); + + assertThat(voidCallbackListener.callbackMethodCalls).isEqualTo(1); + assertThat(exceptionCallbackListener.lastException()).isNull(); + } + + @Test + public void asyncCall_notConnected_connects() { + connector.turnOnWorkProfile(); + connector.disconnect(); + connectionListener.resetConnectionChangedCount(); + + fakeCrossProfileType.work().asyncVoidMethod(voidCallbackListener, exceptionCallbackListener); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(1); + assertThat(connector.isConnected()).isTrue(); + } + + @Test + public void asyncCall_notConnected_doesNotStartManualConnectionManagement() { + connector.turnOnWorkProfile(); + connector.stopManualConnectionManagement(); + connector.disconnect(); + + fakeCrossProfileType.work().asyncVoidMethod(voidCallbackListener, exceptionCallbackListener); + + assertThat(connector.isManuallyManagingConnection()).isFalse(); + } + + @Test + public void asyncCall_workProfileUnavailable_callsWithUnavailableProfileException() { + connector.removeWorkProfile(); + + fakeCrossProfileType.work().asyncVoidMethod(voidCallbackListener, exceptionCallbackListener); + + assertThat(voidCallbackListener.callbackMethodCalls).isEqualTo(0); + assertThat(exceptionCallbackListener.lastException()) + .isInstanceOf(UnavailableProfileException.class); + } + + @Test + public void futureCall_workProfileAvailableButNotConnected_works() + throws ExecutionException, InterruptedException { + connector.turnOnWorkProfile(); + connector.disconnect(); + + ListenableFuture<Void> future = fakeCrossProfileType.work().listenableFutureVoidMethod(); + + assertThat(future.get()).isNull(); + } + + @Test + public void futureCall_notConnected_connects() { + connector.turnOnWorkProfile(); + connector.disconnect(); + connectionListener.resetConnectionChangedCount(); + + ListenableFuture<Void> unusedFuture = fakeCrossProfileType.work().listenableFutureVoidMethod(); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(1); + assertThat(connector.isConnected()).isTrue(); + } + + @Test + public void futureCall_notConnected_doesNotStartManualConnectionManagement() { + connector.turnOnWorkProfile(); + connector.stopManualConnectionManagement(); + connector.disconnect(); + + ListenableFuture<Void> unusedFuture = fakeCrossProfileType.work().listenableFutureVoidMethod(); + + assertThat(connector.isManuallyManagingConnection()).isFalse(); + } + + @Test + public void futureCall_workProfileUnavailable_setsUnavailableProfileException() { + connector.removeWorkProfile(); + + ListenableFuture<Void> future = fakeCrossProfileType.work().listenableFutureVoidMethod(); + + assertFutureHasException(future, UnavailableProfileException.class); + } + + @Test + public void blockingCallOnBoth_notConnected_onlyCallsOnCurrent() { + connector.setRunningOnProfile(ProfileType.PERSONAL); + connector.turnOnWorkProfile(); + connector.disconnect(); + + fakeCrossProfileType.both().voidMethod(); + + assertThat(personal.voidMethodInstanceCalls).isEqualTo(1); + assertThat(work.voidMethodInstanceCalls).isEqualTo(0); + } + + @Test + public void asyncCallOnBoth_notAvailable_onlyCallsOnCurrent() { + connector.setRunningOnProfile(ProfileType.PERSONAL); + connector.turnOffWorkProfile(); + + fakeCrossProfileType + .both() + .asyncVoidMethod( + () -> { + // Ignored + }); + + assertThat(personal.voidMethodInstanceCalls).isEqualTo(1); + assertThat(work.voidMethodInstanceCalls).isEqualTo(0); + } + + @Test + public void futureCallOnBoth_notAvailable_onlyCallsOnCurrent() { + connector.setRunningOnProfile(ProfileType.PERSONAL); + connector.turnOffWorkProfile(); + + ListenableFuture<Map<Profile, Void>> unusedFuture = + fakeCrossProfileType.both().listenableFutureVoidMethod(); + + assertThat(personal.voidMethodInstanceCalls).isEqualTo(1); + assertThat(work.voidMethodInstanceCalls).isEqualTo(0); + } + + @Test + public void current_synchronous_throwsRuntimeException_runtimeExceptionIsThrown() { + assertThrows( + CustomRuntimeException.class, + () -> { + fakeCrossProfileType.current().methodWhichThrowsRuntimeException(); + }); + } + + @Test + public void other_synchronous_throwsRuntimeException_exceptionIsWrapped() + throws UnavailableProfileException { + try { + fakeCrossProfileType.other().methodWhichThrowsRuntimeException(); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void current_async_throwsRuntimeException_runtimeExceptionIsThrown() { + assertThrows( + CustomRuntimeException.class, + () -> { + fakeCrossProfileType + .current() + .asyncStringMethodWhichThrowsRuntimeException(/* callback= */ null); + }); + } + + @Test + public void other_async_throwsRuntimeException_exceptionIsWrapped() { + try { + fakeCrossProfileType + .other() + .asyncStringMethodWhichThrowsRuntimeException( + /* callback= */ null, /* exceptionCallback= */ null); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void current_future_throwsRuntimeException_runtimeExceptionIsThrown() { + assertThrows( + CustomRuntimeException.class, + () -> { + fakeCrossProfileType.current().listenableFutureVoidMethodWhichThrowsRuntimeException(); + }); + } + + @Test + public void other_future_throwsRuntimeException_exceptionIsWrapped() { + try { + fakeCrossProfileType.other().listenableFutureVoidMethodWhichThrowsRuntimeException(); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void both_synchronous_throwsRuntimeException_exceptionIsThrown() { + // Which one is thrown when both throw exceptions is not specified + try { + fakeCrossProfileType.both().methodWhichThrowsRuntimeException(); + fail(); + } catch (CustomRuntimeException expected) { + + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void both_async_throwsRuntimeException_exceptionIsThrown() { + // Which one is thrown when both throw exceptions is not specified + try { + fakeCrossProfileType + .both() + .asyncStringMethodWhichThrowsRuntimeException(/* callback= */ null); + fail(); + } catch (CustomRuntimeException expected) { + + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void both_future_throwsRuntimeException_exceptionIsThrown() { + // Which one is thrown when both throw exceptions is not specified + try { + fakeCrossProfileType.both().listenableFutureVoidMethodWhichThrowsRuntimeException(); + fail(); + } catch (CustomRuntimeException expected) { + + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void ifAvailable_synchronous_notConnected_returnsDefaultValue() { + connector.disconnect(); + + assertThat( + fakeCrossProfileType + .other() + .ifAvailable() + .identityStringMethod(STRING, /* defaultValue= */ STRING2)) + .isEqualTo(STRING2); + } + + @Test + public void ifAvailable_synchronousVoid_notConnected_doesNotCallMethod() { + connector.setRunningOnProfile(ProfileType.PERSONAL); + connector.disconnect(); + + fakeCrossProfileType.other().ifAvailable().voidMethod(); + + assertThat(work.voidMethodInstanceCalls).isEqualTo(0); + } + + @Test + public void ifAvailable_synchronous_connected_returnsCorrectValue() { + connector.startConnecting(); + + assertThat( + fakeCrossProfileType + .other() + .ifAvailable() + .identityStringMethod(STRING, /* defaultValue= */ STRING2)) + .isEqualTo(STRING); + } + + @Test + public void ifAvailable_synchronousVoid_connected_callsMethod() { + connector.startConnecting(); + connector.setRunningOnProfile(ProfileType.PERSONAL); + fakeCrossProfileType.other().ifAvailable().voidMethod(); + + assertThat(work.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void ifAvailable_callback_notAvailable_returnsDefaultValue() { + connector.turnOffWorkProfile(); + + fakeCrossProfileType + .other() + .ifAvailable() + .asyncIdentityStringMethod(STRING, stringCallbackListener, /* defaultValue= */ STRING2); + + assertThat(stringCallbackListener.stringCallbackValue).isEqualTo(STRING2); + } + + @Test + public void ifAvailable_voidCallback_notAvailable_callsback() { + connector.turnOffWorkProfile(); + + fakeCrossProfileType.other().ifAvailable().asyncVoidMethod(voidCallbackListener); + + assertThat(voidCallbackListener.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void ifAvailable_callback_available_returnsCorrectValue() { + connector.turnOnWorkProfile(); + + fakeCrossProfileType + .other() + .ifAvailable() + .asyncIdentityStringMethod(STRING, stringCallbackListener, /* defaultValue= */ STRING2); + + assertThat(stringCallbackListener.stringCallbackValue).isEqualTo(STRING); + } + + @Test + public void ifAvailable_voidCallback_available_callsMethod() { + connector.turnOnWorkProfile(); + + fakeCrossProfileType.other().ifAvailable().asyncVoidMethod(voidCallbackListener); + + assertThat(voidCallbackListener.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void ifAvailable_future_available_returnsCorrectValue() + throws ExecutionException, InterruptedException { + connector.turnOnWorkProfile(); + + ListenableFuture<String> future = + fakeCrossProfileType + .other() + .ifAvailable() + .listenableFutureIdentityStringMethod(STRING, STRING2); + + assertThat(future.get()).isEqualTo(STRING); + } + + @Test + public void ifAvailable_future_notAvailable_returnsDefaultValue() + throws ExecutionException, InterruptedException { + connector.turnOffWorkProfile(); + + ListenableFuture<String> future = + fakeCrossProfileType + .other() + .ifAvailable() + .listenableFutureIdentityStringMethod(STRING, STRING2); + + assertThat(future.get()).isEqualTo(STRING2); + } + + @Test + public void contextArgument_synchronous_currentProfile_works() { + assertThat(fakeCrossProfileType.current().isContextArgumentPassed()).isTrue(); + } + + @Test + public void contextArgument_synchronous_otherProfile_works() throws UnavailableProfileException { + assertThat(fakeCrossProfileType.other().isContextArgumentPassed()).isTrue(); + } + + @Test + public void contextArgument_synchronous_both_works() { + Map<Profile, Boolean> result = fakeCrossProfileType.both().isContextArgumentPassed(); + + assertThat(result.get(connector.utils().getCurrentProfile())).isTrue(); + assertThat(result.get(connector.utils().getOtherProfile())).isTrue(); + } + + @Test + public void contextArgument_async_currentProfile_works() { + fakeCrossProfileType.current().asyncIsContextArgumentPassed(booleanCallbackListener); + + assertThat(booleanCallbackListener.booleanCallbackValue).isTrue(); + } + + @Test + public void contextArgument_async_otherProfile_works() { + fakeCrossProfileType + .other() + .asyncIsContextArgumentPassed(booleanCallbackListener, exceptionCallbackListener); + + assertThat(booleanCallbackListener.booleanCallbackValue).isTrue(); + } + + @Test + public void contextArgument_async_both_works() { + fakeCrossProfileType.both().asyncIsContextArgumentPassed(booleanMultiCallbackListener); + + Map<Profile, Boolean> result = booleanMultiCallbackListener.booleanCallbackValues; + assertThat(result.get(connector.utils().getCurrentProfile())).isTrue(); + assertThat(result.get(connector.utils().getOtherProfile())).isTrue(); + } + + @Test + public void contextArgument_future_currentProfile_works() throws Exception { + ListenableFuture<Boolean> future = + fakeCrossProfileType.current().futureIsContextArgumentPassed(); + + assertThat(future.get()).isTrue(); + } + + @Test + public void contextArgument_future_otherProfile_works() throws Exception { + ListenableFuture<Boolean> future = fakeCrossProfileType.other().futureIsContextArgumentPassed(); + + assertThat(future.get()).isTrue(); + } + + @Test + public void contextArgument_future_both_works() throws Exception { + ListenableFuture<Map<Profile, Boolean>> resultFuture = + fakeCrossProfileType.both().futureIsContextArgumentPassed(); + + Map<Profile, Boolean> result = resultFuture.get(); + assertThat(result.get(connector.utils().getCurrentProfile())).isTrue(); + assertThat(result.get(connector.utils().getOtherProfile())).isTrue(); + } + + @Test + public void current_synchronous_declaresButDoesNotThrowException_works() throws Exception { + assertThat( + fakeCrossProfileType + .current() + .identityStringMethodDeclaresButDoesNotThrowIOException(STRING)) + .isEqualTo(STRING); + } + + @Test + public void current_synchronous_throwsException_works() { + assertThrows( + IOException.class, + () -> + fakeCrossProfileType + .current() + .identityStringMethodThrowsIOException(STRING)); + } + + @Test + public void current_synchronous_declaresMultipleExceptions_throwsException_works() { + assertThrows( + SQLException.class, + () -> + fakeCrossProfileType + .current() + .identityStringMethodDeclaresIOExceptionThrowsSQLException( + STRING)); + } + + @Test + public void other_synchronous_declaresButDoesNotThrowException_works() throws Exception { + assertThat( + fakeCrossProfileType + .other() + .identityStringMethodDeclaresButDoesNotThrowIOException(STRING)) + .isEqualTo(STRING); + } + + @Test + public void other_synchronous_throwsException_works() { + assertThrows( + IOException.class, + () -> + fakeCrossProfileType + .other() + .identityStringMethodThrowsIOException(STRING)); + } + + @Test + public void other_synchronous_declaresMultipleExceptions_throwsException_works() { + assertThrows( + SQLException.class, + () -> + fakeCrossProfileType + .other() + .identityStringMethodDeclaresIOExceptionThrowsSQLException(STRING)); + } + + @Test + public void current_async_nonSimpleCallback_works() { + nonSimpleCallbackListener.callbackMethodCalls = 0; + fakeCrossProfileType + .current() + .asyncMethodWithNonSimpleCallback(nonSimpleCallbackListener, STRING, STRING2); + + assertThat(nonSimpleCallbackListener.callbackMethodCalls).isEqualTo(1); + assertThat(nonSimpleCallbackListener.string1CallbackValue).isEqualTo(STRING); + assertThat(nonSimpleCallbackListener.string2CallbackValue).isEqualTo(STRING2); + } + + @Test + public void other_async_nonSimpleCallback_works() { + nonSimpleCallbackListener.callbackMethodCalls = 0; + fakeCrossProfileType + .other() + .asyncMethodWithNonSimpleCallback( + nonSimpleCallbackListener, STRING, STRING2, exceptionCallbackListener); + + assertThat(nonSimpleCallbackListener.callbackMethodCalls).isEqualTo(1); + assertThat(nonSimpleCallbackListener.string1CallbackValue).isEqualTo(STRING); + assertThat(nonSimpleCallbackListener.string2CallbackValue).isEqualTo(STRING2); + } + + @Test + public void current_async_nonSimpleCallback_secondMethod_works() { + fakeCrossProfileType + .current() + .asyncMethodWithNonSimpleCallbackCallsSecondMethod( + nonSimpleCallbackListener, STRING, STRING2); + + assertThat(nonSimpleCallbackListener.string3CallbackValue).isEqualTo(STRING); + assertThat(nonSimpleCallbackListener.string4CallbackValue).isEqualTo(STRING2); + } + + @Test + public void other_async_nonSimpleCallback_secondMethod_works() { + fakeCrossProfileType + .other() + .asyncMethodWithNonSimpleCallbackCallsSecondMethod( + nonSimpleCallbackListener, STRING, STRING2, exceptionCallbackListener); + + assertThat(nonSimpleCallbackListener.string3CallbackValue).isEqualTo(STRING); + assertThat(nonSimpleCallbackListener.string4CallbackValue).isEqualTo(STRING2); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/testing/GeneratedFakeProfileConnectorTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/testing/GeneratedFakeProfileConnectorTest.java new file mode 100644 index 0000000..187d95d --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/testing/GeneratedFakeProfileConnectorTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testing; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.os.Build.VERSION_CODES; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.FakeTestProfileConnector; +import com.google.android.enterprise.connectedapps.testing.annotations.CrossProfileTest; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Test specifics of a single generated fake for {@link + * com.google.android.enterprise.connectedapps.ProfileConnector} + * + * <p>More extensive tests of this functionality are in {@link AbstractFakeProfileConnectorTest}. + */ +@CrossProfileTest(configuration = TestApplication.class) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class GeneratedFakeProfileConnectorTest { + + private final Context context = ApplicationProvider.getApplicationContext(); + private final FakeTestProfileConnector fakeTestProfileConnector = + new FakeTestProfileConnector(context); + + @Test + public void getPrimaryProfile_equalsWorkProfile() { + // The TestProfileConnector's primary profile is set to work + assertThat(fakeTestProfileConnector.utils().getPrimaryProfile()) + .isEqualTo(fakeTestProfileConnector.utils().getWorkProfile()); + } +} diff --git a/tests/shared/additional_types/AndroidManifest.xml b/tests/shared/additional_types/AndroidManifest.xml new file mode 100644 index 0000000..3aea2f3 --- /dev/null +++ b/tests/shared/additional_types/AndroidManifest.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.google.android.enterprise.connectedapps.shared.additional_types"> + <application> + </application> +</manifest>
\ No newline at end of file diff --git a/tests/shared/additional_types/build.gradle b/tests/shared/additional_types/build.gradle new file mode 100644 index 0000000..342d739 --- /dev/null +++ b/tests/shared/additional_types/build.gradle @@ -0,0 +1,46 @@ +buildscript { + repositories { + mavenCentral() + } +} + +plugins { + id 'com.android.library' +} + +dependencies { + api project(path: ':connectedapps-testapp_basictypes') + api project(path: ':connectedapps-testapp_connector') + api project(path: ':connectedapps-testapp_wrappers') + + + implementation project(path: ':connectedapps') + implementation project(path: ':connectedapps-annotations') + implementation project(path: ':connectedapps-processor') + annotationProcessor project(path: ':connectedapps-processor') +} + +android { + defaultConfig { + compileSdkVersion 30 + minSdkVersion 26 + } + + testOptions.unitTests.includeAndroidResources = true + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + java.srcDirs = [file('../src/main/java')] + java.includes = [ + "com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileInterface.java", + "com/google/android/enterprise/connectedapps/testapp/types/TestInterfaceProvider.java", + ] + manifest.srcFile 'AndroidManifest.xml' + } + } +} diff --git a/tests/shared/app/AndroidManifest.xml b/tests/shared/app/AndroidManifest.xml new file mode 100644 index 0000000..2ea278d --- /dev/null +++ b/tests/shared/app/AndroidManifest.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.google.android.enterprise.connectedapps.shared.app"> + <application> + </application> +</manifest>
\ No newline at end of file diff --git a/tests/shared/app/build.gradle b/tests/shared/app/build.gradle new file mode 100644 index 0000000..8d92955 --- /dev/null +++ b/tests/shared/app/build.gradle @@ -0,0 +1,35 @@ +buildscript { + repositories { + mavenCentral() + } +} + +plugins { + id 'com.android.application' +} + +dependencies { + api project(path: ':connectedapps-testapp_additional_types') + api project(path: ':connectedapps-testapp_types') +} + +android { + defaultConfig { + compileSdkVersion 30 + minSdkVersion 26 + } + + testOptions.unitTests.includeAndroidResources = true + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + java.srcDirs = [] + manifest.srcFile 'AndroidManifest.xml' + } + } +} diff --git a/tests/shared/basictypes/AndroidManifest.xml b/tests/shared/basictypes/AndroidManifest.xml new file mode 100644 index 0000000..e774d40 --- /dev/null +++ b/tests/shared/basictypes/AndroidManifest.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.google.android.enterprise.connectedapps.shared.basictypes"> + <application> + </application> +</manifest>
\ No newline at end of file diff --git a/tests/shared/basictypes/build.gradle b/tests/shared/basictypes/build.gradle new file mode 100644 index 0000000..f00a2ac --- /dev/null +++ b/tests/shared/basictypes/build.gradle @@ -0,0 +1,55 @@ +buildscript { + repositories { + mavenCentral() + } +} + +plugins { + id 'com.android.library' +} + +dependencies { + api deps.guava + + implementation project(path: ':connectedapps') + implementation project(path: ':connectedapps-annotations') + implementation project(path: ':connectedapps-processor') + annotationProcessor project(path: ':connectedapps-processor') +} + +android { + defaultConfig { + compileSdkVersion 30 + minSdkVersion 26 + } + + testOptions.unitTests.includeAndroidResources = true + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + java.srcDirs = [file('../src/main/java')] + java.includes = [ + "com/google/android/enterprise/connectedapps/testapp/CustomRuntimeException.java", + "com/google/android/enterprise/connectedapps/testapp/CustomWrapper.java", + "com/google/android/enterprise/connectedapps/testapp/CustomWrapper2.java", + "com/google/android/enterprise/connectedapps/testapp/NonSimpleCallbackListener.java", + "com/google/android/enterprise/connectedapps/testapp/NotReallySerializableObject.java", + "com/google/android/enterprise/connectedapps/testapp/ParcelableObject.java", + "com/google/android/enterprise/connectedapps/testapp/SerializableObject.java", + "com/google/android/enterprise/connectedapps/testapp/SimpleFuture.java", + "com/google/android/enterprise/connectedapps/testapp/StringWrapper.java", + "com/google/android/enterprise/connectedapps/testapp/TestBooleanCallbackListener.java", + "com/google/android/enterprise/connectedapps/testapp/TestCustomWrapperCallbackListener.java", + "com/google/android/enterprise/connectedapps/testapp/TestNotReallySerializableObjectCallbackListener.java", + "com/google/android/enterprise/connectedapps/testapp/TestStringCallbackListener.java", + "com/google/android/enterprise/connectedapps/testapp/TestVoidCallbackListener.java", + ] + manifest.srcFile 'AndroidManifest.xml' + } + } +} diff --git a/tests/shared/build.gradle b/tests/shared/build.gradle new file mode 100644 index 0000000..4642f3b --- /dev/null +++ b/tests/shared/build.gradle @@ -0,0 +1,46 @@ +buildscript { + repositories { + mavenCentral() + } +} + +plugins { + id 'com.android.library' +} + +dependencies { + api deps.checkerFramework + api project(path: ':connectedapps-testapp') + implementation project(path: ':connectedapps-annotations') + implementation 'org.robolectric:robolectric:4.4' + implementation 'junit:junit:4.13.1' + implementation 'com.google.truth:truth:1.1.2' + implementation 'androidx.test:core:1.3.0' + implementation project(path: ':connectedapps') + implementation project(path: ':connectedapps-annotations') + implementation project(path: ':connectedapps-processor') + annotationProcessor project(path: ':connectedapps-processor') + implementation project(path: ':connectedapps-testing') + implementation project(path: ':connectedapps-testing-annotations') + implementation project(path: ':connectedapps-testapp_types') +} + +android { + defaultConfig { + compileSdkVersion 30 + minSdkVersion 26 + } + + testOptions.unitTests.includeAndroidResources = true + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + java.includes = ["com/google/android/enterprise/connectedapps/*.java"] + } + } +} diff --git a/tests/shared/configuration/AndroidManifest.xml b/tests/shared/configuration/AndroidManifest.xml new file mode 100644 index 0000000..9a8e3b2 --- /dev/null +++ b/tests/shared/configuration/AndroidManifest.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.google.android.enterprise.connectedapps.shared.configuration"> + <application> + </application> +</manifest>
\ No newline at end of file diff --git a/tests/shared/configuration/build.gradle b/tests/shared/configuration/build.gradle new file mode 100644 index 0000000..96b3ee9 --- /dev/null +++ b/tests/shared/configuration/build.gradle @@ -0,0 +1,44 @@ +buildscript { + repositories { + mavenCentral() + } +} + +plugins { + id 'com.android.library' +} + +dependencies { + api project(path: ':connectedapps-testapp_types_providers') + api project(path: ':connectedapps-testapp_types') + api project(path: ':connectedapps-testapp_additional_types') + + implementation project(path: ':connectedapps') + implementation project(path: ':connectedapps-annotations') + implementation project(path: ':connectedapps-processor') + annotationProcessor project(path: ':connectedapps-processor') +} + +android { + defaultConfig { + compileSdkVersion 30 + minSdkVersion 26 + } + + testOptions.unitTests.includeAndroidResources = true + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + java.srcDirs = [file('../src/main/java')] + java.includes = [ + "com/google/android/enterprise/connectedapps/testapp/configuration/TestApplication.java" + ] + manifest.srcFile 'AndroidManifest.xml' + } + } +} diff --git a/tests/shared/connector/AndroidManifest.xml b/tests/shared/connector/AndroidManifest.xml new file mode 100644 index 0000000..37f0eda --- /dev/null +++ b/tests/shared/connector/AndroidManifest.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.google.android.enterprise.connectedapps.shared.connector"> + <application> + </application> +</manifest>
\ No newline at end of file diff --git a/tests/shared/connector/build.gradle b/tests/shared/connector/build.gradle new file mode 100644 index 0000000..c24d502 --- /dev/null +++ b/tests/shared/connector/build.gradle @@ -0,0 +1,46 @@ +buildscript { + repositories { + mavenCentral() + } +} + +plugins { + id 'com.android.library' +} + +dependencies { + api project(path: ':connectedapps-testapp_wrappers') + + + implementation project(path: ':connectedapps') + implementation project(path: ':connectedapps-annotations') + implementation project(path: ':connectedapps-processor') + annotationProcessor project(path: ':connectedapps-processor') +} + +android { + defaultConfig { + compileSdkVersion 30 + minSdkVersion 26 + } + + testOptions.unitTests.includeAndroidResources = true + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + java.srcDirs = [file('../src/main/java')] + java.includes = [ + "com/google/android/enterprise/connectedapps/testapp/ConnectorSingleton.java", + "com/google/android/enterprise/connectedapps/testapp/connector/DirectBootAwareConnector.java", + "com/google/android/enterprise/connectedapps/testapp/connector/TestProfileConnector.java", + "com/google/android/enterprise/connectedapps/testapp/connector/TestProfileConnectorWithCustomServiceClass.java", + ] + manifest.srcFile 'AndroidManifest.xml' + } + } +} diff --git a/tests/shared/crossuser/AndroidManifest.xml b/tests/shared/crossuser/AndroidManifest.xml new file mode 100644 index 0000000..0e07eca --- /dev/null +++ b/tests/shared/crossuser/AndroidManifest.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.google.android.enterprise.connectedapps.shared.crossuser"> + <application> + </application> +</manifest>
\ No newline at end of file diff --git a/tests/shared/crossuser/build.gradle b/tests/shared/crossuser/build.gradle new file mode 100644 index 0000000..4cceaba --- /dev/null +++ b/tests/shared/crossuser/build.gradle @@ -0,0 +1,40 @@ +buildscript { + repositories { + mavenCentral() + } +} + +plugins { + id 'com.android.library' +} + +dependencies { + implementation project(path: ':connectedapps') + implementation project(path: ':connectedapps-annotations') + implementation project(path: ':connectedapps-processor') + annotationProcessor project(path: ':connectedapps-processor') +} + +android { + defaultConfig { + compileSdkVersion 30 + minSdkVersion 26 + } + + testOptions.unitTests.includeAndroidResources = true + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + java.srcDirs = [file('../src/main/java')] + java.includes = [ + "com/google/android/enterprise/connectedapps/testapp/crossuser/*.java" + ] + manifest.srcFile 'AndroidManifest.xml' + } + } +} diff --git a/tests/shared/src/main/AndroidManifest.xml b/tests/shared/src/main/AndroidManifest.xml new file mode 100644 index 0000000..cc92adf --- /dev/null +++ b/tests/shared/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.google.android.enterprise.connectedapps.shared"> + + <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" tools:ignore="ProtectedPermissions" /> + + <application> + </application> +</manifest>
\ No newline at end of file diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/NonSimpleCallbackListenerImpl.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/NonSimpleCallbackListenerImpl.java new file mode 100644 index 0000000..cdb1f6f --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/NonSimpleCallbackListenerImpl.java @@ -0,0 +1,41 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import com.google.android.enterprise.connectedapps.testapp.NonSimpleCallbackListener; + +public class NonSimpleCallbackListenerImpl implements NonSimpleCallbackListener { + + public int callbackMethodCalls = 0; + public String string1CallbackValue; + public String string2CallbackValue; + public String string3CallbackValue; + public String string4CallbackValue; + + @Override + public void callback(String string1, String string2) { + string1CallbackValue = string1; + string2CallbackValue = string2; + callbackMethodCalls++; + } + + @Override + public void callback2(String string3, String string4) { + string3CallbackValue = string3; + string4CallbackValue = string4; + callbackMethodCalls++; + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/SharedTestUtilities.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/SharedTestUtilities.java new file mode 100644 index 0000000..5ff3b0b --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/SharedTestUtilities.java @@ -0,0 +1,96 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; + +import android.os.UserHandle; +import com.google.common.util.concurrent.FluentFuture; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** Test utilities shared between Robolectric and Instrumented tests. */ +public final class SharedTestUtilities { + + public static final String INTERACT_ACROSS_USERS = "android.permission.INTERACT_ACROSS_USERS"; + public static final String INTERACT_ACROSS_USERS_FULL = + "android.permission.INTERACT_ACROSS_USERS_FULL"; + + private static final String OF_METHOD_NAME = "of"; + + /** Get the {@link UserHandle} for the given user ID. */ + public static UserHandle getUserHandleForUserId(int userId) { + try { + return (UserHandle) + UserHandle.class.getMethod(OF_METHOD_NAME, int.class).invoke(null, userId); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Error getting current user handle", e); + } + } + + public static @Nullable Throwable assertFutureHasException( + ListenableFuture<?> future, Class<? extends Throwable> throwable) { + AtomicReference<Throwable> thrown = new AtomicReference<>(); + try { + FluentFuture.from(future) + .catching( + throwable, + t -> { + // Expected + thrown.set(t); + return null; + }, + directExecutor()) + .get(); + } catch (InterruptedException | ExecutionException e) { + throw new AssertionError("Unhandled exception", e); + } + + assertThat(thrown.get()).isNotNull(); + return thrown.get(); + } + + public static void assertFutureDoesNotHaveException( + ListenableFuture<?> future, Class<? extends Throwable> throwable) { + AtomicBoolean didThrow = new AtomicBoolean(false); + try { + FluentFuture.from(future) + .catching( + throwable, + expected -> { + didThrow.set(true); + return null; + }, + directExecutor()) + .withTimeout(1, TimeUnit.SECONDS, Executors.newSingleThreadScheduledExecutor()) + .get(); + } catch (InterruptedException e) { + throw new AssertionError("Unhandled exception", e); + } catch (ExecutionException e) { + // This is called when the 1 second times out - which means nothing was thrown + } + + assertThat(didThrow.get()).isFalse(); + } + + private SharedTestUtilities() {} +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/StringUtilities.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/StringUtilities.java new file mode 100644 index 0000000..7b13028 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/StringUtilities.java @@ -0,0 +1,34 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import java.util.Random; + +public class StringUtilities { + private StringUtilities() {} + + private static final long RANDOM_SEED = 1; + + /** Generate a random String of the given length. */ + public static String randomString(int length) { + Random r = new Random(RANDOM_SEED); + char[] chars = new char[length]; + for (int i = 0; i < length; i++) { + chars[i] = (char) (r.nextInt(26) + 'a'); + } + return new String(chars); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestAvailabilityListener.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestAvailabilityListener.java new file mode 100644 index 0000000..0e78d3a --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestAvailabilityListener.java @@ -0,0 +1,55 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class TestAvailabilityListener implements AvailabilityListener { + + private static final long DEFAULT_TIMEOUT = 30; + private static final TimeUnit DEFAULT_UNIT = SECONDS; + + private int availabilityChangedCount = 0; + private CountDownLatch latch = new CountDownLatch(1); + + public int availabilityChangedCount() { + return availabilityChangedCount; + } + + public void reset() { + availabilityChangedCount = 0; + latch.countDown(); + latch = new CountDownLatch(1); + } + + public int awaitAvailabilityChange() throws InterruptedException { + return awaitAvailabilityChange(DEFAULT_TIMEOUT, DEFAULT_UNIT); + } + + public int awaitAvailabilityChange(long timeout, TimeUnit unit) throws InterruptedException { + latch.await(timeout, unit); + return availabilityChangedCount(); + } + + @Override + public void availabilityChanged() { + availabilityChangedCount++; + latch.countDown(); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestBooleanCallbackListenerImpl.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestBooleanCallbackListenerImpl.java new file mode 100644 index 0000000..2b88f3c --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestBooleanCallbackListenerImpl.java @@ -0,0 +1,30 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import com.google.android.enterprise.connectedapps.testapp.TestBooleanCallbackListener; + +public class TestBooleanCallbackListenerImpl implements TestBooleanCallbackListener { + + public int callbackMethodCalls = 0; + public boolean booleanCallbackValue; + + @Override + public void booleanCallback(boolean b) { + callbackMethodCalls++; + booleanCallbackValue = b; + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestBooleanCallbackListenerMultiImpl.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestBooleanCallbackListenerMultiImpl.java new file mode 100644 index 0000000..c6bba6e --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestBooleanCallbackListenerMultiImpl.java @@ -0,0 +1,30 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import com.google.android.enterprise.connectedapps.testapp.TestBooleanCallbackListener_Multi; +import java.util.Map; + +public class TestBooleanCallbackListenerMultiImpl implements TestBooleanCallbackListener_Multi { + public int numberOfResults = 0; + public Map<Profile, Boolean> booleanCallbackValues; + + @Override + public void booleanCallback(Map<Profile, Boolean> b) { + numberOfResults = b.size(); + booleanCallbackValues = b; + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestConnectionListener.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestConnectionListener.java new file mode 100644 index 0000000..1cc4309 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestConnectionListener.java @@ -0,0 +1,34 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +public class TestConnectionListener implements ConnectionListener { + + private int connectionChangedCount = 0; + + public int connectionChangedCount() { + return connectionChangedCount; + } + + public void resetConnectionChangedCount() { + connectionChangedCount = 0; + } + + @Override + public void connectionChanged() { + connectionChangedCount++; + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestCustomWrapperCallbackListenerImpl.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestCustomWrapperCallbackListenerImpl.java new file mode 100644 index 0000000..cb70b33 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestCustomWrapperCallbackListenerImpl.java @@ -0,0 +1,31 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import com.google.android.enterprise.connectedapps.testapp.CustomWrapper; +import com.google.android.enterprise.connectedapps.testapp.TestCustomWrapperCallbackListener; + +public class TestCustomWrapperCallbackListenerImpl implements TestCustomWrapperCallbackListener { + + public int callbackMethodCalls = 0; + public CustomWrapper<String> customWrapperCallbackValue; + + @Override + public void customWrapperCallback(CustomWrapper<String> c) { + callbackMethodCalls++; + customWrapperCallbackValue = c; + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestExceptionCallbackListener.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestExceptionCallbackListener.java new file mode 100644 index 0000000..40229cc --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestExceptionCallbackListener.java @@ -0,0 +1,36 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +public class TestExceptionCallbackListener implements ExceptionCallback { + + public int exceptionCalls = 0; + public Throwable lastException = null; + + public int exceptionCalls() { + return exceptionCalls; + } + + public Throwable lastException() { + return lastException; + } + + @Override + public void onException(Throwable throwable) { + lastException = throwable; + exceptionCalls++; + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestNotReallySerializableObjectCallbackListenerImpl.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestNotReallySerializableObjectCallbackListenerImpl.java new file mode 100644 index 0000000..6e939d1 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestNotReallySerializableObjectCallbackListenerImpl.java @@ -0,0 +1,31 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import com.google.android.enterprise.connectedapps.testapp.NotReallySerializableObject; +import com.google.android.enterprise.connectedapps.testapp.TestNotReallySerializableObjectCallbackListener; + +public class TestNotReallySerializableObjectCallbackListenerImpl + implements TestNotReallySerializableObjectCallbackListener { + public int callbackMethodCalls = 0; + public NotReallySerializableObject notReallySerializableObjectCallbackValue; + + @Override + public void callback(NotReallySerializableObject n) { + callbackMethodCalls++; + notReallySerializableObjectCallbackValue = n; + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestStringCallbackListenerImpl.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestStringCallbackListenerImpl.java new file mode 100644 index 0000000..4207e7b --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestStringCallbackListenerImpl.java @@ -0,0 +1,30 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import com.google.android.enterprise.connectedapps.testapp.TestStringCallbackListener; + +public class TestStringCallbackListenerImpl implements TestStringCallbackListener { + + public int callbackMethodCalls = 0; + public String stringCallbackValue; + + @Override + public void stringCallback(String s) { + callbackMethodCalls++; + stringCallbackValue = s; + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestStringCallbackListenerMultiImpl.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestStringCallbackListenerMultiImpl.java new file mode 100644 index 0000000..5f3043e --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestStringCallbackListenerMultiImpl.java @@ -0,0 +1,30 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import com.google.android.enterprise.connectedapps.testapp.TestStringCallbackListener_Multi; +import java.util.Map; + +public class TestStringCallbackListenerMultiImpl implements TestStringCallbackListener_Multi { + public int numberOfResults = 0; + public Map<Profile, String> stringCallbackValues; + + @Override + public void stringCallback(Map<Profile, String> s) { + numberOfResults = s.size(); + stringCallbackValues = s; + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestVoidCallbackListenerImpl.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestVoidCallbackListenerImpl.java new file mode 100644 index 0000000..d7f4b66 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestVoidCallbackListenerImpl.java @@ -0,0 +1,28 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import com.google.android.enterprise.connectedapps.testapp.TestVoidCallbackListener; + +public class TestVoidCallbackListenerImpl implements TestVoidCallbackListener { + + public int callbackMethodCalls = 0; + + @Override + public void callback() { + callbackMethodCalls++; + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestVoidCallbackListenerMultiImpl.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestVoidCallbackListenerMultiImpl.java new file mode 100644 index 0000000..90c4beb --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestVoidCallbackListenerMultiImpl.java @@ -0,0 +1,28 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import com.google.android.enterprise.connectedapps.testapp.TestVoidCallbackListener_Multi; + +public class TestVoidCallbackListenerMultiImpl implements TestVoidCallbackListener_Multi { + + public int callbackMethodCalls = 0; + + @Override + public void callback() { + callbackMethodCalls++; + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/ConnectorSingleton.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/ConnectorSingleton.java new file mode 100644 index 0000000..99c21bd --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/ConnectorSingleton.java @@ -0,0 +1,35 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp; + +import android.content.Context; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; + +/** Holder of a singleton {@link TestProfileConnector}. */ +public final class ConnectorSingleton { + private static TestProfileConnector connector; + + public static TestProfileConnector getConnector(Context context) { + if (connector == null) { + synchronized (ConnectorSingleton.class) { + connector = TestProfileConnector.create(context); + } + } + return connector; + } + + private ConnectorSingleton() {} +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/CustomRuntimeException.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/CustomRuntimeException.java new file mode 100644 index 0000000..22f187b --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/CustomRuntimeException.java @@ -0,0 +1,22 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp; + +public class CustomRuntimeException extends RuntimeException { + public CustomRuntimeException(String msg) { + super(msg); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/CustomWrapper.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/CustomWrapper.java new file mode 100644 index 0000000..a2b1b51 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/CustomWrapper.java @@ -0,0 +1,47 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp; + +import java.util.Objects; + +public class CustomWrapper<F> { + private final F value; + + public CustomWrapper(F value) { + this.value = value; + } + + public F value() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof CustomWrapper)) { + return false; + } + CustomWrapper<?> that = (CustomWrapper<?>) o; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/CustomWrapper2.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/CustomWrapper2.java new file mode 100644 index 0000000..add6d3b --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/CustomWrapper2.java @@ -0,0 +1,47 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp; + +import java.util.Objects; + +public class CustomWrapper2<F> { + private final F value; + + public CustomWrapper2(F value) { + this.value = value; + } + + public F value() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof CustomWrapper2)) { + return false; + } + CustomWrapper2<?> that = (CustomWrapper2<?>) o; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/NonSimpleCallbackListener.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/NonSimpleCallbackListener.java new file mode 100644 index 0000000..cc56283 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/NonSimpleCallbackListener.java @@ -0,0 +1,25 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileCallback; + +@CrossProfileCallback +public interface NonSimpleCallbackListener { + void callback(String string1, String string2); + + void callback2(String string3, String string4); +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/NotReallySerializableObject.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/NotReallySerializableObject.java new file mode 100644 index 0000000..d1d58c6 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/NotReallySerializableObject.java @@ -0,0 +1,27 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp; + +import java.io.Serializable; + +public class NotReallySerializableObject implements Serializable { + // ParcelableObject does not implement Serializable + private final ParcelableObject parcelableObject; + + public NotReallySerializableObject(ParcelableObject parcelableObject) { + this.parcelableObject = parcelableObject; + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/ParcelableObject.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/ParcelableObject.java new file mode 100644 index 0000000..f499df4 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/ParcelableObject.java @@ -0,0 +1,80 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp; + +import static com.google.common.base.Preconditions.checkNotNull; + +import android.os.Parcel; +import android.os.Parcelable; +import java.util.Objects; + +public final class ParcelableObject implements Parcelable { + + @SuppressWarnings("rawtypes") + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public ParcelableObject createFromParcel(Parcel in) { + return new ParcelableObject(in); + } + + @Override + public ParcelableObject[] newArray(int size) { + return new ParcelableObject[size]; + } + }; + + private final String value; + + public String value() { + return value; + } + + public ParcelableObject(Parcel in) { + this(in.readString()); + } + + public ParcelableObject(String value) { + this.value = checkNotNull(value); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(value); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ParcelableObject that = (ParcelableObject) o; + return value.equals(that.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/SerializableObject.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/SerializableObject.java new file mode 100644 index 0000000..d8fc9d8 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/SerializableObject.java @@ -0,0 +1,49 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp; + +import java.io.Serializable; +import java.util.Objects; + +public final class SerializableObject implements Serializable { + + private final String value; + + public String value() { + return value; + } + + public SerializableObject(String value) { + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SerializableObject that = (SerializableObject) o; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/SimpleFuture.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/SimpleFuture.java new file mode 100644 index 0000000..9c388ee --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/SimpleFuture.java @@ -0,0 +1,71 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp; + +import java.util.concurrent.CountDownLatch; + +/** A very simple implementation of the future pattern used to test custom future wrappers. */ +public class SimpleFuture<E> { + + public static interface Consumer<E> { + void accept(E value); + } + + private E value; + private Throwable thrown; + private final CountDownLatch countDownLatch = new CountDownLatch(1); + private Consumer<E> callback; + private Consumer<Throwable> exceptionCallback; + + public void set(E value) { + this.value = value; + countDownLatch.countDown(); + if (callback != null) { + callback.accept(value); + } + } + + public void setException(Throwable t) { + this.thrown = t; + countDownLatch.countDown(); + if (exceptionCallback != null) { + exceptionCallback.accept(thrown); + } + } + + public E get() { + try { + countDownLatch.await(); + } catch (InterruptedException e) { + return null; + } + if (thrown != null) { + throw new RuntimeException(thrown); + } + return value; + } + + public void setCallback(Consumer<E> callback, Consumer<Throwable> exceptionCallback) { + if (value != null) { + callback.accept(value); + } else if (thrown != null) { + exceptionCallback.accept(thrown); + } else { + this.callback = callback; + this.exceptionCallback = exceptionCallback; + } + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/StringWrapper.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/StringWrapper.java new file mode 100644 index 0000000..4828f91 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/StringWrapper.java @@ -0,0 +1,47 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp; + +import java.util.Objects; + +public class StringWrapper { + private final String value; + + public StringWrapper(String value) { + this.value = value; + } + + public String value() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof StringWrapper)) { + return false; + } + StringWrapper that = (StringWrapper) o; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestBooleanCallbackListener.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestBooleanCallbackListener.java new file mode 100644 index 0000000..9e82374 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestBooleanCallbackListener.java @@ -0,0 +1,23 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileCallback; + +@CrossProfileCallback +public interface TestBooleanCallbackListener { + void booleanCallback(boolean b); +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestCustomWrapperCallbackListener.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestCustomWrapperCallbackListener.java new file mode 100644 index 0000000..a6e3899 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestCustomWrapperCallbackListener.java @@ -0,0 +1,23 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileCallback; + +@CrossProfileCallback +public interface TestCustomWrapperCallbackListener { + void customWrapperCallback(CustomWrapper<String> c); +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestNotReallySerializableObjectCallbackListener.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestNotReallySerializableObjectCallbackListener.java new file mode 100644 index 0000000..f6085d7 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestNotReallySerializableObjectCallbackListener.java @@ -0,0 +1,23 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileCallback; + +@CrossProfileCallback +public interface TestNotReallySerializableObjectCallbackListener { + void callback(NotReallySerializableObject object); +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestStringCallbackListener.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestStringCallbackListener.java new file mode 100644 index 0000000..dc25651 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestStringCallbackListener.java @@ -0,0 +1,23 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileCallback; + +@CrossProfileCallback +public interface TestStringCallbackListener { + void stringCallback(String s); +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestVoidCallbackListener.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestVoidCallbackListener.java new file mode 100644 index 0000000..01d28b9 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestVoidCallbackListener.java @@ -0,0 +1,23 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileCallback; + +@CrossProfileCallback +public interface TestVoidCallbackListener { + void callback(); +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/configuration/TestApplication.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/configuration/TestApplication.java new file mode 100644 index 0000000..de1d8f5 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/configuration/TestApplication.java @@ -0,0 +1,35 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.configuration; + +import android.app.Service; +import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector_Service; +import com.google.android.enterprise.connectedapps.testapp.types.SeparateBuildTargetProvider; +import com.google.android.enterprise.connectedapps.testapp.types.TestInterfaceProvider; +import com.google.android.enterprise.connectedapps.testapp.types.TestProvider; + +@CrossProfileConfiguration(providers = { + TestProvider.class, SeparateBuildTargetProvider.class, TestInterfaceProvider.class}) +public abstract class TestApplication { + + // This is available so the test targets can access the generated Service class. + public static Class<? extends Service> getService() { + return TestProfileConnector_Service.class; + } + + private TestApplication() {} +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/connector/DirectBootAwareConnector.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/connector/DirectBootAwareConnector.java new file mode 100644 index 0000000..56dcf08 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/connector/DirectBootAwareConnector.java @@ -0,0 +1,38 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.connector; + +import android.content.Context; +import com.google.android.enterprise.connectedapps.ProfileConnector; +import com.google.android.enterprise.connectedapps.annotations.AvailabilityRestrictions; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector; +import com.google.android.enterprise.connectedapps.annotations.GeneratedProfileConnector; +import java.util.concurrent.ScheduledExecutorService; + +@GeneratedProfileConnector +@CustomProfileConnector(availabilityRestrictions = AvailabilityRestrictions.DIRECT_BOOT_AWARE) +public interface DirectBootAwareConnector extends ProfileConnector { + static DirectBootAwareConnector create(Context context) { + return GeneratedDirectBootAwareConnector.builder(context).build(); + } + + static DirectBootAwareConnector create( + Context context, ScheduledExecutorService scheduledExecutorService) { + return GeneratedDirectBootAwareConnector.builder(context) + .setScheduledExecutorService(scheduledExecutorService) + .build(); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/connector/TestProfileConnector.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/connector/TestProfileConnector.java new file mode 100644 index 0000000..296b820 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/connector/TestProfileConnector.java @@ -0,0 +1,43 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.connector; + +import android.content.Context; +import com.google.android.enterprise.connectedapps.ProfileConnector; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector.ProfileType; +import com.google.android.enterprise.connectedapps.annotations.GeneratedProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.wrappers.ParcelableCustomWrapper; +import com.google.android.enterprise.connectedapps.testapp.wrappers.SimpleFutureWrapper; +import java.util.concurrent.ScheduledExecutorService; + +@GeneratedProfileConnector +@CustomProfileConnector( + primaryProfile = ProfileType.WORK, + parcelableWrappers = {ParcelableCustomWrapper.class}, + futureWrappers = {SimpleFutureWrapper.class}) +public interface TestProfileConnector extends ProfileConnector { + static TestProfileConnector create(Context context) { + return GeneratedTestProfileConnector.builder(context).build(); + } + + static TestProfileConnector create( + Context context, ScheduledExecutorService scheduledExecutorService) { + return GeneratedTestProfileConnector.builder(context) + .setScheduledExecutorService(scheduledExecutorService) + .build(); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/connector/TestProfileConnectorWithCustomServiceClass.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/connector/TestProfileConnectorWithCustomServiceClass.java new file mode 100644 index 0000000..a5cfa6a --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/connector/TestProfileConnectorWithCustomServiceClass.java @@ -0,0 +1,33 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.connector; + +import android.content.Context; +import com.google.android.enterprise.connectedapps.ProfileConnector; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector; +import com.google.android.enterprise.connectedapps.annotations.GeneratedProfileConnector; +import java.util.concurrent.ScheduledExecutorService; + +@GeneratedProfileConnector +@CustomProfileConnector(serviceClassName = "com.google.CustomServiceClass") +public interface TestProfileConnectorWithCustomServiceClass extends ProfileConnector { + static TestProfileConnectorWithCustomServiceClass create( + Context context, ScheduledExecutorService scheduledExecutorService) { + return GeneratedTestProfileConnectorWithCustomServiceClass.builder(context) + .setScheduledExecutorService(scheduledExecutorService) + .build(); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserConfiguration.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserConfiguration.java new file mode 100644 index 0000000..f8ac4e8 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserConfiguration.java @@ -0,0 +1,31 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.crossuser; + +import android.app.Service; +import com.google.android.enterprise.connectedapps.annotations.CrossUserConfiguration; +import com.google.android.enterprise.connectedapps.annotations.CrossUserConfigurations; + +@CrossUserConfigurations(@CrossUserConfiguration(providers = TestCrossUserProvider.class)) +public abstract class TestCrossUserConfiguration { + + // This is available so the test targets can access the generated Service class. + public static Class<? extends Service> getService() { + return TestCrossUserConnector_Service.class; + } + + private TestCrossUserConfiguration() {} +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserConnector.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserConnector.java new file mode 100644 index 0000000..72c3a8c --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserConnector.java @@ -0,0 +1,38 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.crossuser; + +import android.content.Context; +import com.google.android.enterprise.connectedapps.ProfileConnector; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector.ProfileType; +import com.google.android.enterprise.connectedapps.annotations.GeneratedProfileConnector; +import java.util.concurrent.ScheduledExecutorService; + +@GeneratedProfileConnector +@CustomProfileConnector(primaryProfile = ProfileType.WORK) +public interface TestCrossUserConnector extends ProfileConnector { + static TestCrossUserConnector create(Context context) { + return GeneratedTestCrossUserConnector.builder(context).build(); + } + + static TestCrossUserConnector create( + Context context, ScheduledExecutorService scheduledExecutorService) { + return GeneratedTestCrossUserConnector.builder(context) + .setScheduledExecutorService(scheduledExecutorService) + .build(); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserProvider.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserProvider.java new file mode 100644 index 0000000..6c6e4c6 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserProvider.java @@ -0,0 +1,26 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.crossuser; + +import com.google.android.enterprise.connectedapps.annotations.CrossUserProvider; + +public class TestCrossUserProvider { + + @CrossUserProvider + public TestCrossUserType provideTestCrossUserType() { + return new TestCrossUserType(); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserStringCallbackListener.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserStringCallbackListener.java new file mode 100644 index 0000000..2ec080e --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserStringCallbackListener.java @@ -0,0 +1,23 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.crossuser; + +import com.google.android.enterprise.connectedapps.annotations.CrossUserCallback; + +@CrossUserCallback +public interface TestCrossUserStringCallbackListener { + void stringCallback(String s); +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserStringCallbackListenerImpl.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserStringCallbackListenerImpl.java new file mode 100644 index 0000000..482a154 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserStringCallbackListenerImpl.java @@ -0,0 +1,29 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.crossuser; + +public class TestCrossUserStringCallbackListenerImpl + implements TestCrossUserStringCallbackListener { + + public int callbackMethodCalls = 0; + public String stringCallbackValue; + + @Override + public void stringCallback(String s) { + callbackMethodCalls++; + stringCallbackValue = s; + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserType.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserType.java new file mode 100644 index 0000000..9be071d --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserType.java @@ -0,0 +1,27 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.crossuser; + +import com.google.android.enterprise.connectedapps.annotations.CrossUser; + +@CrossUser(connector = TestCrossUserConnector.class, timeoutMillis = 7000) +public class TestCrossUserType { + + @CrossUser + public void passString(String string, TestCrossUserStringCallbackListener callbackListener) { + callbackListener.stringCallback(string); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/NonInstantiableTestCrossProfileType.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/NonInstantiableTestCrossProfileType.java new file mode 100644 index 0000000..0fa102c --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/NonInstantiableTestCrossProfileType.java @@ -0,0 +1,42 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.types; + +import static com.google.common.util.concurrent.Futures.immediateFuture; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfile; +import com.google.android.enterprise.connectedapps.testapp.TestStringCallbackListener; +import com.google.common.util.concurrent.ListenableFuture; + +public final class NonInstantiableTestCrossProfileType { + private NonInstantiableTestCrossProfileType() {} + + @CrossProfile + public static String staticIdentityStringMethod(String s) { + return s; + } + + @CrossProfile + public static void staticAsyncIdentityStringMethod( + String s, TestStringCallbackListener callback) { + callback.stringCallback(s); + } + + @CrossProfile + public static ListenableFuture<String> staticFutureIdentityStringMethod(String s) { + return immediateFuture(s); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/SeparateBuildTargetProvider.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/SeparateBuildTargetProvider.java new file mode 100644 index 0000000..67d6003 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/SeparateBuildTargetProvider.java @@ -0,0 +1,28 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.types; + +import android.content.Context; +import com.google.android.enterprise.connectedapps.annotations.CrossProfileProvider; + +/** A provider which is in a separate build target to the type it provides. */ +public class SeparateBuildTargetProvider { + @CrossProfileProvider + public TestCrossProfileTypeWhichNeedsContext provideTestCrossProfileTypeWhichNeedsContext( + Context context) { + return new TestCrossProfileTypeWhichNeedsContext(context); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileInterface.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileInterface.java new file mode 100644 index 0000000..7dfa616 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileInterface.java @@ -0,0 +1,29 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.types; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfile; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import java.util.List; + +@CrossProfile(connector = TestProfileConnector.class) +public interface TestCrossProfileInterface { + + // This needs to be a type which has a parcelable wrapper to ensure that we generate duplicate + // parcelable wrappers in multiple targets and they are resolved correctly + @CrossProfile + List<String> identityListOfStringMethod(List<String> s); +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileType.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileType.java new file mode 100644 index 0000000..ddadc81 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileType.java @@ -0,0 +1,602 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.types; + +import static com.google.common.util.concurrent.Futures.immediateFuture; +import static com.google.common.util.concurrent.Futures.immediateVoidFuture; + +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Handler; +import android.util.Pair; +import com.google.android.enterprise.connectedapps.annotations.CrossProfile; +import com.google.android.enterprise.connectedapps.testapp.CustomRuntimeException; +import com.google.android.enterprise.connectedapps.testapp.CustomWrapper; +import com.google.android.enterprise.connectedapps.testapp.CustomWrapper2; +import com.google.android.enterprise.connectedapps.testapp.NonSimpleCallbackListener; +import com.google.android.enterprise.connectedapps.testapp.NotReallySerializableObject; +import com.google.android.enterprise.connectedapps.testapp.ParcelableObject; +import com.google.android.enterprise.connectedapps.testapp.SerializableObject; +import com.google.android.enterprise.connectedapps.testapp.SimpleFuture; +import com.google.android.enterprise.connectedapps.testapp.StringWrapper; +import com.google.android.enterprise.connectedapps.testapp.TestBooleanCallbackListener; +import com.google.android.enterprise.connectedapps.testapp.TestCustomWrapperCallbackListener; +import com.google.android.enterprise.connectedapps.testapp.TestNotReallySerializableObjectCallbackListener; +import com.google.android.enterprise.connectedapps.testapp.TestStringCallbackListener; +import com.google.android.enterprise.connectedapps.testapp.TestVoidCallbackListener; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.wrappers.ParcelableCustomWrapper2; +import com.google.android.enterprise.connectedapps.testapp.wrappers.ParcelableStringWrapper; +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import java.io.IOException; +import java.sql.SQLException; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +@CrossProfile( + connector = TestProfileConnector.class, + timeoutMillis = 7000, + parcelableWrappers = {ParcelableCustomWrapper2.class, ParcelableStringWrapper.class}) +public class TestCrossProfileType { + + public static int voidMethodCalls = 0; + public int voidMethodInstanceCalls; + + @CrossProfile + public void voidMethod() { + voidMethodCalls += 1; + voidMethodInstanceCalls += 1; + } + + @CrossProfile + public void voidMethod(String s) { + voidMethod(); + } + + @CrossProfile + public String methodWhichThrowsRuntimeException() { + throw new CustomRuntimeException("Exception"); + } + + @CrossProfile + public ListenableFuture<Void> listenableFutureVoidMethod() { + voidMethod(); + return immediateFuture(null); + } + + @CrossProfile + public ListenableFuture<Void> listenableFutureMethodWhichNeverSetsTheValue() { + return SettableFuture.create(); + } + + @CrossProfile // Timeout is inherited + public ListenableFuture<Void> listenableFutureMethodWhichNeverSetsTheValueWith7SecondTimeout() { + return SettableFuture.create(); + } + + @CrossProfile(timeoutMillis = 5000) + public ListenableFuture<Void> listenableFutureMethodWhichNeverSetsTheValueWith5SecondTimeout() { + return SettableFuture.create(); + } + + @CrossProfile + public ListenableFuture<Void> listenableFutureVoidMethodWhichThrowsRuntimeException() { + throw new CustomRuntimeException("Exception"); + } + + @CrossProfile + public ListenableFuture<Void> listenableFutureVoidMethodWhichSetsIllegalStateException() { + return Futures.immediateFailedFuture(new IllegalStateException("Illegal State")); + } + + @CrossProfile + public ListenableFuture<Void> listenableFutureVoidMethodWithDelay(int secondsDelay) { + try { + TimeUnit.SECONDS.sleep(secondsDelay); + } catch (InterruptedException e) { + throw new IllegalStateException("Error during delay"); + } + return listenableFutureVoidMethod(); + } + + @CrossProfile + public ListenableFuture<Void> listenableFutureVoidMethodWithNonBlockingDelay(int secondsDelay) { + SettableFuture<Void> v = SettableFuture.create(); + + new Handler() + .postDelayed( + () -> { + voidMethod(); + v.set(null); + }, + TimeUnit.SECONDS.toMillis(secondsDelay)); + return v; + } + + @CrossProfile + public ListenableFuture<String> listenableFutureIdentityStringMethodWithNonBlockingDelay( + String s, int secondsDelay) { + SettableFuture<String> v = SettableFuture.create(); + + new Handler().postDelayed(() -> v.set(s), TimeUnit.SECONDS.toMillis(secondsDelay)); + return v; + } + + @CrossProfile(timeoutMillis = 3000) + public ListenableFuture<String> + listenableFutureIdentityStringMethodWithNonBlockingDelayWith3SecondTimeout( + String s, int secondsDelay) { + SettableFuture<String> v = SettableFuture.create(); + + new Handler().postDelayed(() -> v.set(s), TimeUnit.SECONDS.toMillis(secondsDelay)); + return v; + } + + @CrossProfile + public void asyncStringMethodWhichThrowsRuntimeException(TestStringCallbackListener callback) { + throw new CustomRuntimeException("Exception"); + } + + @CrossProfile + public void asyncVoidMethodWhichCallsBackTwice(TestVoidCallbackListener callback) { + voidMethod(); + callback.callback(); + callback.callback(); + } + + @CrossProfile + public void asyncVoidMethod(TestVoidCallbackListener callback) { + voidMethod(); + callback.callback(); + } + + @CrossProfile + public void asyncMethodWhichNeverCallsBack(TestStringCallbackListener callback) {} + + @CrossProfile // Timeout is inherited + public void asyncMethodWhichNeverCallsBackWith7SecondTimeout( + TestStringCallbackListener callback) {} + + @CrossProfile(timeoutMillis = 5000) + public void asyncMethodWhichNeverCallsBackWith5SecondTimeout( + TestStringCallbackListener callback) {} + + @CrossProfile + public void asyncVoidMethodWithDelay(TestVoidCallbackListener callback, int secondsDelay) { + try { + TimeUnit.SECONDS.sleep(secondsDelay); + } catch (InterruptedException e) { + throw new IllegalStateException("Error during delay"); + } + asyncVoidMethod(callback); + } + + @CrossProfile + public void asyncVoidMethodWithNonBlockingDelay( + TestVoidCallbackListener callback, int secondsDelay) { + new Handler() + .postDelayed(() -> asyncVoidMethod(callback), TimeUnit.SECONDS.toMillis(secondsDelay)); + } + + @CrossProfile(timeoutMillis = 50000) + public void asyncVoidMethodWithNonBlockingDelayWith50SecondTimeout( + TestVoidCallbackListener callback, int secondsDelay) { + new Handler() + .postDelayed(() -> asyncVoidMethod(callback), TimeUnit.SECONDS.toMillis(secondsDelay)); + } + + @CrossProfile(timeoutMillis = 3000) + public void asyncIdentityStringMethodWithNonBlockingDelayWith3SecondTimeout( + String s, TestStringCallbackListener callback, int secondsDelay) { + new Handler() + .postDelayed( + () -> asyncIdentityStringMethod(s, callback), TimeUnit.SECONDS.toMillis(secondsDelay)); + } + + @CrossProfile + public void asyncIdentityStringMethodWithNonBlockingDelay( + String s, TestStringCallbackListener callback, int secondsDelay) { + new Handler() + .postDelayed( + () -> asyncIdentityStringMethod(s, callback), TimeUnit.SECONDS.toMillis(secondsDelay)); + } + + @CrossProfile + public Void identityVoidMethod() { + voidMethod(); + return null; + } + + @CrossProfile + public String getNull() { + return null; + } + + @CrossProfile + public Collection<String> getNullCollection() { + return null; + } + + @CrossProfile + public List<String> getNullList() { + return null; + } + + @CrossProfile + public Map<String, String> getNullMap() { + return null; + } + + // @CrossProfile + // public Optional<String> getNullOptional() { + // return null; + // } + + @CrossProfile + public Set<String> getNullSet() { + return null; + } + + // @CrossProfile + // public TestProto getNullProto() { + // return null; + // } + + @CrossProfile + public String identityStringMethod(String s) { + return s; + } + + @CrossProfile + public void asyncIdentityStringMethod(String s, TestStringCallbackListener callback) { + callback.stringCallback(s); + } + + @CrossProfile + public ListenableFuture<String> listenableFutureIdentityStringMethod(String s) { + return immediateFuture(s); + } + + @CrossProfile + public byte identityByteMethod(byte b) { + return b; + } + + @CrossProfile + public Byte identityByteMethod(Byte b) { + return b; + } + + @CrossProfile + public short identityShortMethod(short s) { + return s; + } + + @CrossProfile + public Short identityShortMethod(Short s) { + return s; + } + + @CrossProfile + public int identityIntMethod(int i) { + return i; + } + + @CrossProfile + public Integer identityIntegerMethod(Integer i) { + return i; + } + + @CrossProfile + public long identityLongMethod(long l) { + return l; + } + + @CrossProfile + public Long identityLongMethod(Long l) { + return l; + } + + @CrossProfile + public float identityFloatMethod(float f) { + return f; + } + + @CrossProfile + public Float identityFloatMethod(Float f) { + return f; + } + + @CrossProfile + public double identityDoubleMethod(double d) { + return d; + } + + @CrossProfile + public Double identityDoubleMethod(Double d) { + return d; + } + + @CrossProfile + public char identityCharMethod(char c) { + return c; + } + + @CrossProfile + public Character identityCharacterMethod(Character c) { + return c; + } + + @CrossProfile + public boolean identityBooleanMethod(boolean b) { + return b; + } + + @CrossProfile + public Boolean identityBooleanMethod(Boolean b) { + return b; + } + + @CrossProfile + public ParcelableObject identityParcelableMethod(ParcelableObject p) { + return p; + } + + @CrossProfile + public SerializableObject identitySerializableObjectMethod(SerializableObject s) { + return s; + } + + @CrossProfile + public List<String> identityListMethod(List<String> l) { + return l; + } + + @CrossProfile + public Map<String, String> identityMapMethod(Map<String, String> m) { + return m; + } + + @CrossProfile + public Set<String> identitySetMethod(Set<String> s) { + return s; + } + + // TODO: Disabled because use of Optional fails lint check. Re-enable when this is disabled. + // @CrossProfile + // public Optional<String> identityOptionalMethod(Optional<String> o) { + // return o; + // } + + @CrossProfile + public ImmutableMap<String, String> identityImmutableMapMethod(ImmutableMap<String, String> m) { + return m; + } + + // @CrossProfile + // public TestProto identityProtoMethod(TestProto p) { + // return p; + // } + + // @CrossProfile + // public List<TestProto> identityListOfProtoMethod(List<TestProto> l) { + // return l; + // } + + @CrossProfile + public Collection<String> identityCollectionMethod(Collection<String> c) { + return c; + } + + @CrossProfile + public List<ParcelableObject> identityParcelableWrapperOfParcelableMethod( + List<ParcelableObject> l) { + return l; + } + + @CrossProfile + public List<SerializableObject> identityParcelableWrapperOfSerializableMethod( + List<SerializableObject> l) { + return l; + } + + @CrossProfile + public List<List<String>> identityParcelableWrapperOfParcelableWrapperMethod( + List<List<String>> l) { + return l; + } + + @CrossProfile + public String[] identityStringArrayMethod(String[] s) { + return s; + } + + @CrossProfile + public ListenableFuture<String[]> asyncIdentityStringArrayMethod(String[] s) { + return immediateFuture(s); + } + + @CrossProfile + public Collection<String[]> identityCollectionOfStringArrayMethod(Collection<String[]> c) { + return c; + } + + @CrossProfile + public ParcelableObject[] identityParcelableObjectArrayMethod(ParcelableObject[] p) { + return p; + } + + @CrossProfile + public SerializableObject[] identitySerializableObjectArrayMethod(SerializableObject[] s) { + return s; + } + + @CrossProfile + public Collection<ParcelableObject[]> identityCollectionOfParcelableObjectArrayMethod( + Collection<ParcelableObject[]> c) { + return c; + } + + @CrossProfile + public Collection<SerializableObject[]> identityCollectionOfSerializableObjectArrayMethod( + Collection<SerializableObject[]> c) { + return c; + } + + // @CrossProfile + // public TestProto[] identityProtoArrayMethod(TestProto[] p) { + // return p; + // } + + @CrossProfile + public Pair<String, Integer> identityPairMethod(Pair<String, Integer> p) { + return p; + } + + @CrossProfile + public Optional<ParcelableObject> identityGuavaOptionalMethod(Optional<ParcelableObject> p) { + return p; + } + + @CrossProfile + public Bitmap identityBitmapMethod(Bitmap p) { + return p; + } + + @CrossProfile + public NotReallySerializableObject identityNotReallySerializableObjectMethod( + NotReallySerializableObject n) { + return n; + } + + @CrossProfile + public NotReallySerializableObject returnNotReallySerializableObjectMethod() { + return new NotReallySerializableObject(new ParcelableObject("")); + } + + @CrossProfile + public void asyncGetNotReallySerializableObjectMethod( + TestNotReallySerializableObjectCallbackListener callbackListener) { + callbackListener.callback(new NotReallySerializableObject(new ParcelableObject("TEST"))); + } + + @CrossProfile + public ListenableFuture<NotReallySerializableObject> + futureGetNotReallySerializableObjectMethod() { + return immediateFuture(new NotReallySerializableObject(new ParcelableObject("TEST"))); + } + + @CrossProfile + public CustomWrapper<String> identityCustomWrapperMethod(CustomWrapper<String> c) { + return c; + } + + @CrossProfile + public ListenableFuture<CustomWrapper<String>> listenableFutureIdentityCustomWrapperMethod( + CustomWrapper<String> c) { + return immediateFuture(c); + } + + @CrossProfile + public void asyncIdentityCustomWrapperMethod( + CustomWrapper<String> c, TestCustomWrapperCallbackListener callbackListener) { + callbackListener.customWrapperCallback(c); + } + + @CrossProfile + public CustomWrapper2<String> identityCustomWrapper2Method(CustomWrapper2<String> c) { + return c; + } + + @CrossProfile + public SimpleFuture<String> simpleFutureIdentityStringMethodWithNonBlockingDelay( + String s, int secondsDelay) { + SimpleFuture<String> future = new SimpleFuture<>(); + + new Handler().postDelayed(() -> future.set(s), TimeUnit.SECONDS.toMillis(secondsDelay)); + + return future; + } + + @CrossProfile + public StringWrapper identityStringWrapperMethod(StringWrapper s) { + return s; + } + + @CrossProfile + public int getUserId() { + return android.os.Process.myUid() / 100000; + } + + @CrossProfile + public ListenableFuture<Void> killApp() { + android.os.Process.killProcess(android.os.Process.myPid()); + return immediateVoidFuture(); + } + + @CrossProfile + public boolean isContextArgumentPassed(Context context) { + return context != null; + } + + @CrossProfile + public void asyncIsContextArgumentPassed( + Context contextArg, TestBooleanCallbackListener callback) { + callback.booleanCallback(isContextArgumentPassed(contextArg)); + } + + @CrossProfile + public ListenableFuture<Boolean> futureIsContextArgumentPassed(Context contextArg) { + return immediateFuture(isContextArgumentPassed(contextArg)); + } + + @CrossProfile + public String identityStringMethodDeclaresButDoesNotThrowIOException(String s) + throws IOException { + return s; + } + + @CrossProfile + public String identityStringMethodThrowsIOException(String s) + throws IOException { + throw new IOException("Requested to throw"); + } + + @CrossProfile + public String identityStringMethodDeclaresIOExceptionThrowsSQLException(String s) + throws IOException, SQLException { + throw new SQLException("Requested to throw"); + } + + @CrossProfile + public void asyncMethodWithNonSimpleCallback( + NonSimpleCallbackListener callback, String s1, String s2) { + callback.callback(s1, s2); + } + + @CrossProfile + public void asyncMethodWithNonSimpleCallbackCallsSecondMethod( + NonSimpleCallbackListener callback, String s1, String s2) { + callback.callback2(s1, s2); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileTypeWhichDoesNotSpecifyConnector.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileTypeWhichDoesNotSpecifyConnector.java new file mode 100644 index 0000000..0785ab0 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileTypeWhichDoesNotSpecifyConnector.java @@ -0,0 +1,26 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.types; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfile; + +public class TestCrossProfileTypeWhichDoesNotSpecifyConnector { + + @CrossProfile + public String identityStringMethod(String s) { + return s; + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileTypeWhichNeedsContext.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileTypeWhichNeedsContext.java new file mode 100644 index 0000000..f98a5b1 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileTypeWhichNeedsContext.java @@ -0,0 +1,159 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.types; + +import static com.google.common.util.concurrent.Futures.immediateFuture; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import com.google.android.enterprise.connectedapps.annotations.CrossProfile; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.testapp.ConnectorSingleton; +import com.google.android.enterprise.connectedapps.testapp.TestStringCallbackListener; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; + +@CrossProfile(connector = TestProfileConnector.class) +public class TestCrossProfileTypeWhichNeedsContext { + + private static final int TEN_SECONDS = 10000; + + public static int voidMethodCalls = 0; + + private final Context context; + + private final ProfileTestCrossProfileType profileTestCrossProfileType; + + public TestCrossProfileTypeWhichNeedsContext(Context context) { + this.context = context; + this.profileTestCrossProfileType = + ProfileTestCrossProfileType.create(ConnectorSingleton.getConnector(context)); + } + + @CrossProfile // Timeout is not specified on type or method so will be default + public ListenableFuture<Void> listenableFutureMethodWhichNeverSetsTheValueWithDefaultTimeout() { + return SettableFuture.create(); + } + + @CrossProfile // Timeout is not specified on type or method so will be default + public void asyncMethodWhichNeverCallsBackWithDefaultTimeout( + TestStringCallbackListener callback) {} + + @CrossProfile + public void voidMethod() { + voidMethodCalls += 1; + } + + @CrossProfile + public void connectToOtherProfile() { + // This, when called cross-profile, causes the other profile to create a connection back to the + // original profile + ConnectorSingleton.getConnector(context).startConnecting(); + } + + @CrossProfile + public boolean isConnectedToOtherProfile() { + return ConnectorSingleton.getConnector(context).isConnected(); + } + + @CrossProfile + public String methodWhichCallsIdentityStringMethodOnOtherProfile(String s) { + try { + return profileTestCrossProfileType.other().identityStringMethod(s); + } catch (UnavailableProfileException e) { + throw new RuntimeException("Cannot call back to other profile", e); + } + } + + @CrossProfile + public void asyncMethodWhichCallsIdentityStringMethodOnOtherProfile( + String s, TestStringCallbackListener callback) { + profileTestCrossProfileType + .other() + .asyncIdentityStringMethod( + s, + callback, + throwable -> { + throw new RuntimeException(throwable); + }); + } + + @CrossProfile + public ListenableFuture<String> + listenableFutureMethodWhichCallsIdentityStringMethodOnOtherProfile(String s) { + return profileTestCrossProfileType.other().listenableFutureIdentityStringMethod(s); + } + + @CrossProfile + public String identityStringMethodWhichDelays10SecondsOnWorkProfile(String s) { + if (ConnectorSingleton.getConnector(context).utils().runningOnWork()) { + try { + Thread.sleep(TEN_SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException("Sleep interrupted", e); + } + } + return s; + } + + @CrossProfile + public void asyncIdentityStringMethodWhichDelays10SecondsOnWorkProfile( + String s, TestStringCallbackListener callback) { + if (ConnectorSingleton.getConnector(context).utils().runningOnWork()) { + new Handler(Looper.getMainLooper()) + .postDelayed( + () -> { + callback.stringCallback(s); + }, + TEN_SECONDS); + } else { + callback.stringCallback(s); + } + } + + @CrossProfile + public ListenableFuture<String> futureIdentityStringMethodWhichDelays10SecondsOnWorkProfile( + String s) { + if (ConnectorSingleton.getConnector(context).utils().runningOnWork()) { + SettableFuture<String> future = SettableFuture.create(); + new Handler(Looper.getMainLooper()) + .postDelayed( + () -> { + future.set(s); + }, + TEN_SECONDS); + return future; + } + + return immediateFuture(s); + } + + @CrossProfile + public int getUserId() { + return android.os.Process.myUid() / 100000; + } + + @CrossProfile + public int getOtherUserId() { + try { + return profileTestCrossProfileType.other().getUserId(); + } catch (UnavailableProfileException e) { + throw new RuntimeException("Cannot call back to other profile", e); + } + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestInterfaceProvider.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestInterfaceProvider.java new file mode 100644 index 0000000..06c01ba --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestInterfaceProvider.java @@ -0,0 +1,26 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.types; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileProvider; + +public class TestInterfaceProvider { + + @CrossProfileProvider + public TestCrossProfileInterface provideCrossProfileInterface() { + return s -> s; + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestProvider.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestProvider.java new file mode 100644 index 0000000..29473a8 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestProvider.java @@ -0,0 +1,33 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.types; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileProvider; + +@CrossProfileProvider(staticTypes = {NonInstantiableTestCrossProfileType.class}) +public class TestProvider { + + @CrossProfileProvider + public TestCrossProfileType provideTestCrossProfileType() { + return new TestCrossProfileType(); + } + + @CrossProfileProvider + public TestCrossProfileTypeWhichDoesNotSpecifyConnector + provideTestCrossProfileTypeWhichDoesNotSpecifyConnector() { + return new TestCrossProfileTypeWhichDoesNotSpecifyConnector(); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/wrappers/ParcelableCustomWrapper.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/wrappers/ParcelableCustomWrapper.java new file mode 100644 index 0000000..a9545a2 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/wrappers/ParcelableCustomWrapper.java @@ -0,0 +1,112 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.wrappers; + +import android.os.Parcel; +import android.os.Parcelable; +import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.internal.BundlerType; +import com.google.android.enterprise.connectedapps.testapp.CustomWrapper; + +@CustomParcelableWrapper(originalType = CustomWrapper.class) +public class ParcelableCustomWrapper<E> implements Parcelable { + + private static final int NULL = -1; + private static final int NOT_NULL = 1; + + private final Bundler bundler; + private final BundlerType type; + private final CustomWrapper<E> customWrapper; + + /** + * Create a wrapper for a given {@link CustomWrapper}. + * + * <p>The passed in {@link Bundler} must be capable of bundling {@code F}. + */ + public static <F> ParcelableCustomWrapper<F> of( + Bundler bundler, BundlerType type, CustomWrapper<F> customWrapper) { + return new ParcelableCustomWrapper<>(bundler, type, customWrapper); + } + + public CustomWrapper<E> get() { + return customWrapper; + } + + private ParcelableCustomWrapper( + Bundler bundler, BundlerType type, CustomWrapper<E> customWrapper) { + if (bundler == null || type == null) { + throw new NullPointerException(); + } + this.bundler = bundler; + this.type = type; + this.customWrapper = customWrapper; + } + + private ParcelableCustomWrapper(Parcel in) { + bundler = in.readParcelable(Bundler.class.getClassLoader()); + + int presentValue = in.readInt(); + + if (presentValue == NULL) { + type = null; + customWrapper = null; + return; + } + + type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader()); + BundlerType valueType = type.typeArguments().get(0); + + @SuppressWarnings("unchecked") + E value = (E) bundler.readFromParcel(in, valueType); + + customWrapper = new CustomWrapper<>(value); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(bundler, flags); + + if (customWrapper == null) { + dest.writeInt(NULL); + return; + } + + dest.writeInt(NOT_NULL); + dest.writeParcelable(type, flags); + BundlerType valueType = type.typeArguments().get(0); + bundler.writeToParcel(dest, customWrapper.value(), valueType, flags); + } + + @Override + public int describeContents() { + return 0; + } + + @SuppressWarnings("rawtypes") + public static final Creator<ParcelableCustomWrapper> CREATOR = + new Creator<ParcelableCustomWrapper>() { + @Override + public ParcelableCustomWrapper createFromParcel(Parcel in) { + return new ParcelableCustomWrapper(in); + } + + @Override + public ParcelableCustomWrapper[] newArray(int size) { + return new ParcelableCustomWrapper[size]; + } + }; +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/wrappers/ParcelableCustomWrapper2.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/wrappers/ParcelableCustomWrapper2.java new file mode 100644 index 0000000..4721445 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/wrappers/ParcelableCustomWrapper2.java @@ -0,0 +1,113 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.wrappers; + +import android.os.Parcel; +import android.os.Parcelable; +import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.internal.BundlerType; +import com.google.android.enterprise.connectedapps.testapp.CustomWrapper; +import com.google.android.enterprise.connectedapps.testapp.CustomWrapper2; + +@CustomParcelableWrapper(originalType = CustomWrapper2.class) +public class ParcelableCustomWrapper2<E> implements Parcelable { + + private static final int NULL = -1; + private static final int NOT_NULL = 1; + + private final Bundler bundler; + private final BundlerType type; + private final CustomWrapper2<E> customWrapper; + + /** + * Create a wrapper for a given {@link CustomWrapper}. + * + * <p>The passed in {@link Bundler} must be capable of bundling {@code F}. + */ + public static <F> ParcelableCustomWrapper2<F> of( + Bundler bundler, BundlerType type, CustomWrapper2<F> customWrapper) { + return new ParcelableCustomWrapper2<>(bundler, type, customWrapper); + } + + public CustomWrapper2<E> get() { + return customWrapper; + } + + private ParcelableCustomWrapper2( + Bundler bundler, BundlerType type, CustomWrapper2<E> customWrapper) { + if (bundler == null || type == null) { + throw new NullPointerException(); + } + this.bundler = bundler; + this.type = type; + this.customWrapper = customWrapper; + } + + private ParcelableCustomWrapper2(Parcel in) { + bundler = in.readParcelable(Bundler.class.getClassLoader()); + + int presentValue = in.readInt(); + + if (presentValue == NULL) { + type = null; + customWrapper = null; + return; + } + + type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader()); + BundlerType valueType = type.typeArguments().get(0); + + @SuppressWarnings("unchecked") + E value = (E) bundler.readFromParcel(in, valueType); + + customWrapper = new CustomWrapper2<>(value); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(bundler, flags); + + if (customWrapper == null) { + dest.writeInt(NULL); + return; + } + + dest.writeInt(NOT_NULL); + dest.writeParcelable(type, flags); + BundlerType valueType = type.typeArguments().get(0); + bundler.writeToParcel(dest, customWrapper.value(), valueType, flags); + } + + @Override + public int describeContents() { + return 0; + } + + @SuppressWarnings("rawtypes") + public static final Creator<ParcelableCustomWrapper2> CREATOR = + new Creator<ParcelableCustomWrapper2>() { + @Override + public ParcelableCustomWrapper2 createFromParcel(Parcel in) { + return new ParcelableCustomWrapper2(in); + } + + @Override + public ParcelableCustomWrapper2[] newArray(int size) { + return new ParcelableCustomWrapper2[size]; + } + }; +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/wrappers/ParcelableStringWrapper.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/wrappers/ParcelableStringWrapper.java new file mode 100644 index 0000000..c7728f9 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/wrappers/ParcelableStringWrapper.java @@ -0,0 +1,93 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.wrappers; + +import android.os.Parcel; +import android.os.Parcelable; +import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.internal.BundlerType; +import com.google.android.enterprise.connectedapps.testapp.StringWrapper; + +@CustomParcelableWrapper(originalType = StringWrapper.class) +public class ParcelableStringWrapper implements Parcelable { + + private static final int NULL = -1; + private static final int NOT_NULL = 1; + + private final StringWrapper stringWrapper; + + /** + * Create a wrapper for a given {@link StringWrapper}. + * + * <p>The passed in {@link Bundler} and {@link BundlerType} are ignored. + */ + public static ParcelableStringWrapper of( + Bundler bundler, BundlerType type, StringWrapper stringWrapper) { + return new ParcelableStringWrapper(bundler, type, stringWrapper); + } + + public StringWrapper get() { + return stringWrapper; + } + + private ParcelableStringWrapper(Bundler bundler, BundlerType type, StringWrapper stringWrapper) { + // Ignore bundler and type as we aren't generic + this.stringWrapper = stringWrapper; + } + + private ParcelableStringWrapper(Parcel in) { + int presentValue = in.readInt(); + + if (presentValue == NULL) { + stringWrapper = null; + return; + } + + String value = in.readString(); + + stringWrapper = new StringWrapper(value); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + if (stringWrapper == null) { + dest.writeInt(NULL); + return; + } + + dest.writeInt(NOT_NULL); + dest.writeString(stringWrapper.value()); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator<ParcelableStringWrapper> CREATOR = + new Creator<ParcelableStringWrapper>() { + @Override + public ParcelableStringWrapper createFromParcel(Parcel in) { + return new ParcelableStringWrapper(in); + } + + @Override + public ParcelableStringWrapper[] newArray(int size) { + return new ParcelableStringWrapper[size]; + } + }; +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/wrappers/SimpleFutureWrapper.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/wrappers/SimpleFutureWrapper.java new file mode 100644 index 0000000..7ea8418 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/wrappers/SimpleFutureWrapper.java @@ -0,0 +1,83 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.wrappers; + +import com.google.android.enterprise.connectedapps.FutureWrapper; +import com.google.android.enterprise.connectedapps.Profile; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.internal.BundlerType; +import com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger; +import com.google.android.enterprise.connectedapps.internal.FutureResultWriter; +import com.google.android.enterprise.connectedapps.testapp.SimpleFuture; +import java.util.Map; + +/** Wrapper for adding support for {@link SimpleFuture} to the Connected Apps SDK. */ +@com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper( + originalType = SimpleFuture.class) +public final class SimpleFutureWrapper<E> extends FutureWrapper<E> { + + private final SimpleFuture<E> future = new SimpleFuture<>(); + + public static <E> SimpleFutureWrapper<E> create(Bundler bundler, BundlerType bundlerType) { + return new SimpleFutureWrapper<>(bundler, bundlerType); + } + + public static <E> SimpleFuture<E> immediateFailedFuture(Throwable t) { + SimpleFuture<E> future = new SimpleFuture<>(); + future.setException(t); + return future; + } + + private SimpleFutureWrapper(Bundler bundler, BundlerType bundlerType) { + super(bundler, bundlerType); + } + + public SimpleFuture<E> getFuture() { + return future; + } + + @Override + public void onResult(E result) { + future.set(result); + } + + @Override + public void onException(Throwable throwable) { + future.setException(throwable); + } + + public static <E> void writeFutureResult( + SimpleFuture<E> future, FutureResultWriter<E> resultWriter) { + + future.setCallback(resultWriter::onSuccess, resultWriter::onFailure); + } + + public static <E> SimpleFuture<Map<Profile, E>> groupResults( + Map<Profile, SimpleFuture<E>> results) { + SimpleFuture<Map<Profile, E>> m = new SimpleFuture<>(); + + CrossProfileCallbackMultiMerger<E> merger = + new CrossProfileCallbackMultiMerger<>(results.size(), m::set); + for (Map.Entry<Profile, SimpleFuture<E>> result : results.entrySet()) { + result + .getValue() + .setCallback( + (value) -> merger.onResult(result.getKey(), value), + (throwable) -> merger.missingResult(result.getKey())); + } + return m; + } +} diff --git a/tests/shared/src/main/proto/connectedappssdk/TestProto2.proto b/tests/shared/src/main/proto/connectedappssdk/TestProto2.proto new file mode 100644 index 0000000..da3cd1c --- /dev/null +++ b/tests/shared/src/main/proto/connectedappssdk/TestProto2.proto @@ -0,0 +1,22 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +syntax = "proto2"; + +package connectedappssdk; + +message TestProto2 { + optional string text = 1; +} diff --git a/tests/shared/testapp/AndroidManifest.xml b/tests/shared/testapp/AndroidManifest.xml new file mode 100644 index 0000000..6a9b2de --- /dev/null +++ b/tests/shared/testapp/AndroidManifest.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.google.android.enterprise.connectedapps.shared.testapp"> + <application> + </application> +</manifest>
\ No newline at end of file diff --git a/tests/shared/testapp/build.gradle b/tests/shared/testapp/build.gradle new file mode 100644 index 0000000..16da3a4 --- /dev/null +++ b/tests/shared/testapp/build.gradle @@ -0,0 +1,41 @@ +buildscript { + repositories { + mavenCentral() + } +} + +plugins { + id 'com.android.library' +} + +dependencies { + api project(path: ':connectedapps-testapp_additional_types') + api project(path: ':connectedapps-testapp_basictypes') + api project(path: ':connectedapps-testapp_configuration') + api project(path: ':connectedapps-testapp_connector') + api project(path: ':connectedapps-testapp_types') + api project(path: ':connectedapps-testapp_types_providers') + api project(path: ':connectedapps-testapp_wrappers') + api project(path: ':connectedapps-testapp_crossuser') +} + +android { + defaultConfig { + compileSdkVersion 30 + minSdkVersion 26 + } + + testOptions.unitTests.includeAndroidResources = true + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + java.srcDirs = [] + manifest.srcFile 'AndroidManifest.xml' + } + } +} diff --git a/tests/shared/types/AndroidManifest.xml b/tests/shared/types/AndroidManifest.xml new file mode 100644 index 0000000..edfbe25 --- /dev/null +++ b/tests/shared/types/AndroidManifest.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.google.android.enterprise.connectedapps.shared.types"> + <application> + </application> +</manifest>
\ No newline at end of file diff --git a/tests/shared/types/build.gradle b/tests/shared/types/build.gradle new file mode 100644 index 0000000..b7f77f8 --- /dev/null +++ b/tests/shared/types/build.gradle @@ -0,0 +1,49 @@ +buildscript { + repositories { + mavenCentral() + } +} + +plugins { + id 'com.android.library' +} + +dependencies { + api project(path: ':connectedapps-testapp_basictypes') + api project(path: ':connectedapps-testapp_connector') + api project(path: ':connectedapps-testapp_wrappers') + + + implementation project(path: ':connectedapps') + implementation project(path: ':connectedapps-annotations') + implementation project(path: ':connectedapps-processor') + annotationProcessor project(path: ':connectedapps-processor') +} + +android { + defaultConfig { + compileSdkVersion 30 + minSdkVersion 26 + } + + testOptions.unitTests.includeAndroidResources = true + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + java.srcDirs = [file('../src/main/java')] + java.includes = [ + "com/google/android/enterprise/connectedapps/testapp/types/NonInstantiableTestCrossProfileType.java", + "com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileType.java", + "com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileTypeWhichDoesNotSpecifyConnector.java", + "com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileTypeWhichNeedsContext.java", + "com/google/android/enterprise/connectedapps/testapp/types/TestProvider.java", + ] + manifest.srcFile 'AndroidManifest.xml' + } + } +} diff --git a/tests/shared/types_providers/AndroidManifest.xml b/tests/shared/types_providers/AndroidManifest.xml new file mode 100644 index 0000000..52d649a --- /dev/null +++ b/tests/shared/types_providers/AndroidManifest.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.google.android.enterprise.connectedapps.shared.types_providers"> + <application> + </application> +</manifest>
\ No newline at end of file diff --git a/tests/shared/types_providers/build.gradle b/tests/shared/types_providers/build.gradle new file mode 100644 index 0000000..9694b83 --- /dev/null +++ b/tests/shared/types_providers/build.gradle @@ -0,0 +1,44 @@ +buildscript { + repositories { + mavenCentral() + } +} + +plugins { + id 'com.android.library' +} + +dependencies { + api project(path: ':connectedapps-testapp_basictypes') + api project(path: ':connectedapps-testapp_connector') + api project(path: ':connectedapps-testapp_types') + + implementation project(path: ':connectedapps') + implementation project(path: ':connectedapps-annotations') + implementation project(path: ':connectedapps-processor') + annotationProcessor project(path: ':connectedapps-processor') +} + +android { + defaultConfig { + compileSdkVersion 30 + minSdkVersion 26 + } + + testOptions.unitTests.includeAndroidResources = true + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + java.srcDirs = [file('../src/main/java')] + java.includes = [ + "com/google/android/enterprise/connectedapps/testapp/types/SeparateBuildTargetProvider.java", + ] + manifest.srcFile 'AndroidManifest.xml' + } + } +} diff --git a/tests/shared/wrappers/AndroidManifest.xml b/tests/shared/wrappers/AndroidManifest.xml new file mode 100644 index 0000000..606e72a --- /dev/null +++ b/tests/shared/wrappers/AndroidManifest.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.google.android.enterprise.connectedapps.shared.wrappers"> + <application> + </application> +</manifest>
\ No newline at end of file diff --git a/tests/shared/wrappers/build.gradle b/tests/shared/wrappers/build.gradle new file mode 100644 index 0000000..d08e47f --- /dev/null +++ b/tests/shared/wrappers/build.gradle @@ -0,0 +1,45 @@ +buildscript { + repositories { + mavenCentral() + } +} + +plugins { + id 'com.android.library' +} + +dependencies { + api project(path: ':connectedapps-testapp_basictypes') + + implementation project(path: ':connectedapps') + implementation project(path: ':connectedapps-annotations') + implementation project(path: ':connectedapps-processor') + annotationProcessor project(path: ':connectedapps-processor') +} + +android { + defaultConfig { + compileSdkVersion 30 + minSdkVersion 26 + } + + testOptions.unitTests.includeAndroidResources = true + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + java.srcDirs = [file('../src/main/java')] + java.includes = [ + "com/google/android/enterprise/connectedapps/testapp/wrappers/ParcelableCustomWrapper.java", + "com/google/android/enterprise/connectedapps/testapp/wrappers/ParcelableCustomWrapper2.java", + "com/google/android/enterprise/connectedapps/testapp/wrappers/ParcelableStringWrapper.java", + "com/google/android/enterprise/connectedapps/testapp/wrappers/SimpleFutureWrapper.java", + ] + manifest.srcFile 'AndroidManifest.xml' + } + } +} |