aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJonathan Scott <scottjonathan@google.com>2021-04-15 23:52:57 +0100
committerJonathan Scott <scottjonathan@google.com>2021-04-15 23:53:22 +0100
commit7451f6c15e755236d0e1aef2e1ae40f01c2ea105 (patch)
treedd553a86ad2ab309954ebdc46e5592979c734942
parent98aadee05251dfff3611cd31000da37d00d943f2 (diff)
downloadconnectedappssdk-7451f6c15e755236d0e1aef2e1ae40f01c2ea105.tar.gz
Import platform/external/connectedappssdk
Test: N/A importing code - BUILD to follow Bug: 179354604 Change-Id: I6b9833d1b148f526643e6ba34bd09ac4a17f37cd
-rw-r--r--CONTRIBUTING.md33
-rw-r--r--LICENSE202
-rw-r--r--METADATA15
-rw-r--r--MODULE_LICENSE_APACHE20
-rw-r--r--OWNERS2
-rw-r--r--README.md10
-rw-r--r--annotations/build.gradle29
-rw-r--r--annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/AvailabilityRestrictions.java30
-rw-r--r--annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossProfile.java83
-rw-r--r--annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossProfileCallback.java42
-rw-r--r--annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossProfileConfiguration.java60
-rw-r--r--annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossProfileConfigurations.java28
-rw-r--r--annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossProfileProvider.java38
-rw-r--r--annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossUser.java83
-rw-r--r--annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossUserCallback.java42
-rw-r--r--annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossUserConfiguration.java61
-rw-r--r--annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossUserConfigurations.java29
-rw-r--r--annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CrossUserProvider.java38
-rw-r--r--annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CustomFutureWrapper.java32
-rw-r--r--annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CustomParcelableWrapper.java32
-rw-r--r--annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CustomProfileConnector.java73
-rw-r--r--annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/CustomUserConnector.java54
-rw-r--r--annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/GeneratedProfileConnector.java29
-rw-r--r--annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/GeneratedUserConnector.java29
-rw-r--r--annotations/src/main/java/com/google/android/enterprise/connectedapps/annotations/ParcelableWrapper.java32
-rw-r--r--build.gradle27
-rw-r--r--gradle.properties3
-rw-r--r--processor/build.gradle41
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/AlwaysThrowsGenerator.java221
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/BundlerGenerator.java270
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CodeGenerator.java80
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CommonClassNames.java124
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ConfigurationCodeGenerator.java59
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileCallbackCodeGenerator.java501
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileTypeCodeGenerator.java73
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CurrentProfileGenerator.java219
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DefaultProfileClassGenerator.java330
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DispatcherGenerator.java326
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/EarlyValidator.java1297
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeCrossProfileTypeGenerator.java477
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeOtherGenerator.java344
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeProfileConnectorGenerator.java100
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FutureWrappersGenerator.java126
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/GeneratorUtilities.java299
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/IfAvailableGenerator.java232
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InterfaceGenerator.java690
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalCrossProfileClassGenerator.java421
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalProviderClassGenerator.java186
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/LateValidator.java213
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/MultipleProfilesGenerator.java392
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/OtherProfileGenerator.java379
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ParcelableWrappersGenerator.java154
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/Processor.java415
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProcessorConfiguration.java30
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProfileConnectorCodeGenerator.java189
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProtoParcelableWrapperGenerator.java171
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProviderClassCodeGenerator.java53
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ServiceGenerator.java178
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/SupportedTypes.java844
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/TestCodeGenerator.java97
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/TypeUtils.java121
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/UserConnectorCodeGenerator.java178
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ValidationMessageFormatter.java49
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationClasses.java42
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationFinder.java271
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationInfoExtractor.java92
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationInvocationHandler.java49
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationNames.java37
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationPrinter.java52
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationStrings.java188
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileAnnotationInfoExtractor.java78
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileCallbackAnnotationInfoExtractor.java48
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileConfigurationAnnotationInfoExtractor.java69
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileConfigurationsAnnotationInfoExtractor.java69
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileProviderAnnotationInfoExtractor.java52
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileTestAnnotationInfoExtractor.java51
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileAnnotation.java36
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileCallbackAnnotation.java22
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileConfigurationAnnotation.java28
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileConfigurationsAnnotation.java24
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileProviderAnnotation.java22
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileTestAnnotation.java22
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/Context.java29
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileAnnotationInfo.java72
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileCallbackAnnotationInfo.java38
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileCallbackInterfaceInfo.java72
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationAnnotationInfo.java54
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationInfo.java119
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationsAnnotationInfo.java34
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileMethodInfo.java231
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileProviderAnnotationInfo.java33
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTestAnnotationInfo.java39
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTestInfo.java47
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTypeInfo.java171
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/FutureWrapper.java139
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/FutureWrapperAnnotationInfo.java45
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/GeneratorContext.java142
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ParcelableWrapper.java274
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ParcelableWrapperAnnotationInfo.java42
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ProfileConnectorInfo.java160
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ProviderClassInfo.java130
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/Type.java136
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/UserConnectorInfo.java150
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorContext.java120
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileConfigurationInfo.java125
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileTestInfo.java40
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileTypeInfo.java115
-rw-r--r--processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorProviderClassInfo.java51
-rw-r--r--processor/src/main/resources/futurewrappers/ListenableFutureWrapper.java122
-rw-r--r--processor/src/main/resources/parcelablewrappers/ParcelableArray.java118
-rw-r--r--processor/src/main/resources/parcelablewrappers/ParcelableBitmap.java101
-rw-r--r--processor/src/main/resources/parcelablewrappers/ParcelableCollection.java117
-rw-r--r--processor/src/main/resources/parcelablewrappers/ParcelableGuavaOptional.java120
-rw-r--r--processor/src/main/resources/parcelablewrappers/ParcelableImmutableMap.java129
-rw-r--r--processor/src/main/resources/parcelablewrappers/ParcelableList.java117
-rw-r--r--processor/src/main/resources/parcelablewrappers/ParcelableMap.java121
-rw-r--r--processor/src/main/resources/parcelablewrappers/ParcelableOptional.java120
-rw-r--r--processor/src/main/resources/parcelablewrappers/ParcelablePair.java115
-rw-r--r--processor/src/main/resources/parcelablewrappers/ParcelableSet.java116
-rw-r--r--proguard.pgcfg0
-rw-r--r--sdk/build.gradle71
-rw-r--r--sdk/src/main/AndroidManifest.xml3
-rw-r--r--sdk/src/main/aidl/com/google/android/enterprise/connectedapps/ICrossProfileCallback.aidl22
-rw-r--r--sdk/src/main/aidl/com/google/android/enterprise/connectedapps/ICrossProfileService.aidl41
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/AbstractProfileBinder.java138
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/AbstractProfileConnector.java251
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/AbstractUserConnector.java131
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/AvailabilityListener.java21
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/ConnectedAppsUtils.java87
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/ConnectedAppsUtilsImpl.java114
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/ConnectionBinder.java55
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/ConnectionListener.java21
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/CrossProfileConnector.java84
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/CrossProfileConnectorImpl.java23
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/CrossProfileSDKUtilities.java132
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/CrossProfileSender.java825
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/CrossUserConnector.java81
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/CrossUserConnectorImpl.java22
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/DefaultProfileBinder.java37
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/DpcProfileBinder.java95
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/ExceptionCallback.java21
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/FutureWrapper.java54
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/LocalCallback.java40
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/ParcelableWrapperUtils.java43
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/Permissions.java29
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/PermissionsImpl.java43
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/Profile.java69
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/ProfileConnector.java131
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/ReflectionUtilities.java96
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/UserConnector.java134
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/exceptions/MissingApiException.java27
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/exceptions/ProfileRuntimeException.java27
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/exceptions/UnavailableProfileException.java29
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/BackgroundExceptionThrower.java44
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/Bundler.java89
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/BundlerType.java100
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/ByteUtilities.java29
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/CallbackMergerExceptionCallback.java43
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/CrossProfileCallbackExceptionParcelCallSender.java58
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/CrossProfileCallbackMultiMerger.java97
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/CrossProfileCallbackParcelCallSender.java61
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/CrossProfileFutureResultWriter.java76
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/CrossProfileParcelCallSender.java64
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/FutureResultWriter.java25
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/IfAvailableFutureResultWriter.java53
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/MethodRunner.java25
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/ParcelCallReceiver.java152
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/ParcelCallSender.java225
-rw-r--r--sdk/src/main/java/com/google/android/enterprise/connectedapps/internal/ParcelUtilities.java31
-rw-r--r--settings.gradle42
-rw-r--r--testing/annotations/build.gradle29
-rw-r--r--testing/annotations/src/main/java/com/google/android/enterprise/connectedapps/testing/annotations/CrossProfileTest.java35
-rw-r--r--testing/annotations/src/main/java/com/google/android/enterprise/connectedapps/testing/annotations/CrossUserTest.java35
-rw-r--r--testing/sdk/build.gradle50
-rw-r--r--testing/sdk/src/AndroidManifest.xml36
-rw-r--r--testing/sdk/src/main/AndroidManifest.xml3
-rw-r--r--testing/sdk/src/main/java/com/google/android/enterprise/connectedapps/testing/AbstractFakeProfileConnector.java303
-rw-r--r--testing/sdk/src/main/java/com/google/android/enterprise/connectedapps/testing/BlockingPoll.java50
-rw-r--r--testing/sdk/src/main/java/com/google/android/enterprise/connectedapps/testing/DeviceAdminReceiver.java19
-rw-r--r--testing/sdk/src/main/java/com/google/android/enterprise/connectedapps/testing/FakeConnectedAppsUtils.java96
-rw-r--r--testing/sdk/src/main/java/com/google/android/enterprise/connectedapps/testing/FakePermissions.java32
-rw-r--r--testing/sdk/src/main/java/com/google/android/enterprise/connectedapps/testing/InstrumentedTestUtilities.java282
-rw-r--r--testing/sdk/src/main/java/com/google/android/enterprise/connectedapps/testing/ProfileAvailabilityPoll.java45
-rw-r--r--testing/sdk/src/main/res/xml/device_admin_receiver.xml19
-rw-r--r--tests/instrumented/src/AndroidManifest.xml36
-rw-r--r--tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/AvailabilityListenerTest.java102
-rw-r--r--tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/BothProfilesTest.java98
-rw-r--r--tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/ConnectTest.java112
-rw-r--r--tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/HappyPathEndToEndTest.java152
-rw-r--r--tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/InstrumentedTestUtilitiesTest.java97
-rw-r--r--tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/MessageSizeTest.java103
-rw-r--r--tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/NotInstalledInOtherUserTest.java63
-rw-r--r--tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/NotReallySerializableTest.java85
-rw-r--r--tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/SecondUserTest.java111
-rw-r--r--tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingBroadcastReceiver.java88
-rw-r--r--tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingCallbackListener.java39
-rw-r--r--tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingExceptionCallbackListener.java27
-rw-r--r--tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingStringCallbackListener.java27
-rw-r--r--tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingStringCallbackListenerMulti.java29
-rw-r--r--tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/InstrumentedTestUtilities.java353
-rw-r--r--tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/ServiceCall.java201
-rw-r--r--tests/processor/src/main/AndroidManifest.xml23
-rw-r--r--tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/AlwaysThrowsTest.java89
-rw-r--r--tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/BundlerTest.java73
-rw-r--r--tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileCallbackSupportedParameterTypeTest.java120
-rw-r--r--tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileCallbackTest.java847
-rw-r--r--tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileProviderClassTest.java321
-rw-r--r--tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileProviderTest.java337
-rw-r--r--tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileSupportedParameterTypeTest.java131
-rw-r--r--tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileSupportedReturnTypeTest.java170
-rw-r--r--tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileTest.java1698
-rw-r--r--tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileTestTest.java225
-rw-r--r--tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileTypeTest.java363
-rw-r--r--tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CustomFutureWrapperTest.java855
-rw-r--r--tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CustomParcelableWrapperTest.java799
-rw-r--r--tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CustomProfileConnectorTest.java160
-rw-r--r--tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CustomUserConnectorTest.java160
-rw-r--r--tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DefaultProfileClassTest.java73
-rw-r--r--tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DispatcherTest.java110
-rw-r--r--tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/GeneratedProfileConnectorTest.java152
-rw-r--r--tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/GeneratedUserConnectorTest.java152
-rw-r--r--tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/IfAvailableTest.java192
-rw-r--r--tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InterfaceTest.java560
-rw-r--r--tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalCrossProfileTypeTest.java103
-rw-r--r--tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalProviderClassTest.java145
-rw-r--r--tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/OtherProfileTest.java90
-rw-r--r--tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProcessorCrossProfileConfigurationTest.java463
-rw-r--r--tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProcessorCurrentProfileTest.java90
-rw-r--r--tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProcessorMultipleProfilesTest.java96
-rw-r--r--tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProfileInterfaceTest.java225
-rw-r--r--tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ServiceTest.java165
-rw-r--r--tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/TestUtilities.java599
-rw-r--r--tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ValidationMessageFormatterTest.java57
-rw-r--r--tests/processor/src/main/proto/connectedappssdk/TestProto.proto22
-rw-r--r--tests/robotests/src/test/AndroidManifest.xml30
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ConnectedAppsUtilsManagedPersonalProfileTest.java90
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ConnectedAppsUtilsTest.java183
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ConnectedAppsUtilsUnsupportedTest.java74
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/CrossProfileSenderTest.java499
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/DpcProfileBinderTest.java31
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/PermissionsTest.java106
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/PermissionsUnsupportedTest.java41
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ProfileConnectorTest.java254
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ProfileConnectorUnsupportedTest.java93
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ProfileTest.java76
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/RobolectricTestUtilities.java362
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/TestICrossProfileCallback.java63
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/TestScheduledExecutorService.java179
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/TestService.java102
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/TestStringCrossProfileCallback.java57
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/internal/ByteUtilitiesTest.java50
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/internal/CrossProfileCallbackMultiMergerTest.java197
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/internal/ParcelCallSenderTest.java125
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/AutomaticConnectionManagementTest.java239
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/AvailabilityListenerTest.java112
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesAsyncTest.java223
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesListenableFutureTest.java278
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesManualAsyncTest.java212
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesManualListenableFutureTest.java234
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesSynchronousTest.java121
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileCallbackReceiverTest.java51
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileCallbackSenderTest.java67
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileInterfaceTest.java86
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileServiceTest.java112
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileTypeProfileTest.java553
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileTypeUnsupportedTest.java301
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossUserTest.java81
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CurrentProfileTest.java413
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/IfAvailableTest.java187
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/ManualConnectionManagementTest.java82
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/MessageSizeTest.java130
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileAsyncTest.java342
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileListenableFutureTest.java297
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileManualAsyncTest.java196
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileManualListenableFutureTest.java184
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileSynchronousTest.java164
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/ProfilesTest.java148
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/ProviderTest.java60
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/StaticTest.java241
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/TypesTest.java647
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/testing/AbstractFakeProfileConnectorTest.java407
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/testing/FakeCrossProfileTypeTest.java821
-rw-r--r--tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/testing/GeneratedFakeProfileConnectorTest.java52
-rw-r--r--tests/shared/additional_types/AndroidManifest.xml24
-rw-r--r--tests/shared/additional_types/build.gradle46
-rw-r--r--tests/shared/app/AndroidManifest.xml24
-rw-r--r--tests/shared/app/build.gradle35
-rw-r--r--tests/shared/basictypes/AndroidManifest.xml24
-rw-r--r--tests/shared/basictypes/build.gradle55
-rw-r--r--tests/shared/build.gradle46
-rw-r--r--tests/shared/configuration/AndroidManifest.xml24
-rw-r--r--tests/shared/configuration/build.gradle44
-rw-r--r--tests/shared/connector/AndroidManifest.xml24
-rw-r--r--tests/shared/connector/build.gradle46
-rw-r--r--tests/shared/crossuser/AndroidManifest.xml24
-rw-r--r--tests/shared/crossuser/build.gradle40
-rw-r--r--tests/shared/src/main/AndroidManifest.xml27
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/NonSimpleCallbackListenerImpl.java41
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/SharedTestUtilities.java96
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/StringUtilities.java34
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestAvailabilityListener.java55
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestBooleanCallbackListenerImpl.java30
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestBooleanCallbackListenerMultiImpl.java30
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestConnectionListener.java34
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestCustomWrapperCallbackListenerImpl.java31
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestExceptionCallbackListener.java36
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestNotReallySerializableObjectCallbackListenerImpl.java31
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestStringCallbackListenerImpl.java30
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestStringCallbackListenerMultiImpl.java30
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestVoidCallbackListenerImpl.java28
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestVoidCallbackListenerMultiImpl.java28
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/ConnectorSingleton.java35
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/CustomRuntimeException.java22
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/CustomWrapper.java47
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/CustomWrapper2.java47
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/NonSimpleCallbackListener.java25
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/NotReallySerializableObject.java27
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/ParcelableObject.java80
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/SerializableObject.java49
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/SimpleFuture.java71
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/StringWrapper.java47
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestBooleanCallbackListener.java23
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestCustomWrapperCallbackListener.java23
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestNotReallySerializableObjectCallbackListener.java23
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestStringCallbackListener.java23
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestVoidCallbackListener.java23
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/configuration/TestApplication.java35
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/connector/DirectBootAwareConnector.java38
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/connector/TestProfileConnector.java43
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/connector/TestProfileConnectorWithCustomServiceClass.java33
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserConfiguration.java31
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserConnector.java38
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserProvider.java26
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserStringCallbackListener.java23
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserStringCallbackListenerImpl.java29
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserType.java27
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/NonInstantiableTestCrossProfileType.java42
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/SeparateBuildTargetProvider.java28
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileInterface.java29
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileType.java602
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileTypeWhichDoesNotSpecifyConnector.java26
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileTypeWhichNeedsContext.java159
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestInterfaceProvider.java26
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestProvider.java33
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/wrappers/ParcelableCustomWrapper.java112
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/wrappers/ParcelableCustomWrapper2.java113
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/wrappers/ParcelableStringWrapper.java93
-rw-r--r--tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/wrappers/SimpleFutureWrapper.java83
-rw-r--r--tests/shared/src/main/proto/connectedappssdk/TestProto2.proto22
-rw-r--r--tests/shared/testapp/AndroidManifest.xml24
-rw-r--r--tests/shared/testapp/build.gradle41
-rw-r--r--tests/shared/types/AndroidManifest.xml24
-rw-r--r--tests/shared/types/build.gradle49
-rw-r--r--tests/shared/types_providers/AndroidManifest.xml24
-rw-r--r--tests/shared/types_providers/build.gradle44
-rw-r--r--tests/shared/wrappers/AndroidManifest.xml24
-rw-r--r--tests/shared/wrappers/build.gradle45
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
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..7a4a3ea
--- /dev/null
+++ b/LICENSE
@@ -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
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..ce3438f
--- /dev/null
+++ b/OWNERS
@@ -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'
+ }
+ }
+}