aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-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
173 files changed, 24589 insertions, 0 deletions
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'
+ }
+ }
+}