Merge "Expose AppFunctionTestRule as an API to be used in Robolectric tests." into androidx-main
diff --git a/appfunctions/appfunctions-testing/api/current.txt b/appfunctions/appfunctions-testing/api/current.txt
index e6f50d0..1a2a9a2 100644
--- a/appfunctions/appfunctions-testing/api/current.txt
+++ b/appfunctions/appfunctions-testing/api/current.txt
@@ -1 +1,10 @@
// Signature format: 4.0
+package androidx.appfunctions.testing {
+
+ @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class AppFunctionTestRule implements org.junit.rules.TestRule {
+ ctor public AppFunctionTestRule(android.content.Context context);
+ method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement? base, org.junit.runner.Description? description);
+ }
+
+}
+
diff --git a/appfunctions/appfunctions-testing/api/restricted_current.txt b/appfunctions/appfunctions-testing/api/restricted_current.txt
index e6f50d0..1a2a9a2 100644
--- a/appfunctions/appfunctions-testing/api/restricted_current.txt
+++ b/appfunctions/appfunctions-testing/api/restricted_current.txt
@@ -1 +1,10 @@
// Signature format: 4.0
+package androidx.appfunctions.testing {
+
+ @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class AppFunctionTestRule implements org.junit.rules.TestRule {
+ ctor public AppFunctionTestRule(android.content.Context context);
+ method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement? base, org.junit.runner.Description? description);
+ }
+
+}
+
diff --git a/appfunctions/appfunctions-testing/build.gradle b/appfunctions/appfunctions-testing/build.gradle
index ab3decc..3100a8f 100644
--- a/appfunctions/appfunctions-testing/build.gradle
+++ b/appfunctions/appfunctions-testing/build.gradle
@@ -35,6 +35,7 @@
implementation(project(":appfunctions:appfunctions"))
implementation(project(":appfunctions:appfunctions-service"))
+ implementation("junit:junit:4.13.2")
// Internal dependencies
implementation("androidx.annotation:annotation:1.9.0-rc01")
diff --git a/appfunctions/appfunctions-testing/src/main/java/androidx/appfunctions/testing/AppFunctionTestEnvironment.kt b/appfunctions/appfunctions-testing/src/main/java/androidx/appfunctions/testing/AppFunctionTestEnvironment.kt
deleted file mode 100644
index a2d666c..0000000
--- a/appfunctions/appfunctions-testing/src/main/java/androidx/appfunctions/testing/AppFunctionTestEnvironment.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright 2025 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.appfunctions.testing
-
-import android.content.Context
-import android.os.Build
-import androidx.annotation.RequiresApi
-import androidx.appfunctions.AppFunctionManagerCompat
-import androidx.appfunctions.testing.internal.FakeAppFunctionManagerApi
-import androidx.appfunctions.testing.internal.FakeAppFunctionReader
-
-/**
- * Test environment for testing AppFunction API(s).
- *
- * Prefer using real setup if possible and only rely on this environment for unit or robolectric
- * tests to simulate cross app interactions via app functions.
- *
- * Functions defined via [androidx.appfunctions.service.AppFunction] annotation alongside test code
- * will already be registered to this environment on initialization, if `appfunctions-compiler` is
- * applied.
- */
-@RequiresApi(Build.VERSION_CODES.S)
-internal class AppFunctionTestEnvironment(private val context: Context) {
- // TODO: b/418017070 - Dynamic registration and changing app function enabled state API(s).
- // TODO: b/418017070 - Support function execution.
-
- private val appFunctionReader = FakeAppFunctionReader(context)
- private val appFunctionManagerApi = FakeAppFunctionManagerApi(context, appFunctionReader)
-
- /**
- * Returns an [AppFunctionManagerCompat] instance that can be used to interact with AppFunctions
- * registered in this environment.
- */
- internal fun getAppFunctionManagerCompat(): AppFunctionManagerCompat =
- AppFunctionManagerCompat(context, appFunctionReader, appFunctionManagerApi)
-}
diff --git a/appfunctions/appfunctions-testing/src/main/java/androidx/appfunctions/testing/AppFunctionTestRule.kt b/appfunctions/appfunctions-testing/src/main/java/androidx/appfunctions/testing/AppFunctionTestRule.kt
new file mode 100644
index 0000000..1b08ac7
--- /dev/null
+++ b/appfunctions/appfunctions-testing/src/main/java/androidx/appfunctions/testing/AppFunctionTestRule.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appfunctions.testing
+
+import android.content.Context
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.appfunctions.AppFunctionManagerCompat
+import androidx.appfunctions.testing.internal.FakeAppFunctionManagerApi
+import androidx.appfunctions.testing.internal.FakeAppFunctionReader
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * A JUnit TestRule for setting up an environment to exercise AppFunction APIs in unit or
+ * Robolectric tests.
+ *
+ * Prefer using the real system setup whenever possible. This rule should only be used for local
+ * tests that simulate cross-app interactions via AppFunctions.
+ *
+ * Any functions annotated with [androidx.appfunctions.service.AppFunction] in test code will be
+ * automatically registered in this environment during initialization, provided the
+ * `appfunctions-compiler` is applied to the test configuration.
+ */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+public class AppFunctionTestRule(private val context: Context) : TestRule {
+ // TODO: b/418017070 - Dynamic registration and changing app function enabled state API(s).
+ // TODO: b/418017070 - Support function execution.
+ // TODO: b/425327400 - Move to use Robolectric shadows
+
+ override fun apply(base: Statement?, description: Description?): Statement =
+ object : Statement() {
+ override fun evaluate() {
+ val appFunctionReader = FakeAppFunctionReader(context)
+ val appFunctionManagerApi = FakeAppFunctionManagerApi(context, appFunctionReader)
+ AppFunctionManagerCompat.setAppFunctionReader(appFunctionReader)
+ AppFunctionManagerCompat.setAppFunctionManagerApi(appFunctionManagerApi)
+ AppFunctionManagerCompat.setSkipExtensionLibraryCheck(true)
+
+ base?.evaluate()
+ }
+ }
+}
diff --git a/appfunctions/appfunctions-testing/src/test/java/androidx/appfunctions/testing/AppFunctionTestEnvironmentTest.kt b/appfunctions/appfunctions-testing/src/test/java/androidx/appfunctions/testing/AppFunctionTestRuleTest.kt
similarity index 90%
rename from appfunctions/appfunctions-testing/src/test/java/androidx/appfunctions/testing/AppFunctionTestEnvironmentTest.kt
rename to appfunctions/appfunctions-testing/src/test/java/androidx/appfunctions/testing/AppFunctionTestRuleTest.kt
index 3f9eb0d..4d4bace 100644
--- a/appfunctions/appfunctions-testing/src/test/java/androidx/appfunctions/testing/AppFunctionTestEnvironmentTest.kt
+++ b/appfunctions/appfunctions-testing/src/test/java/androidx/appfunctions/testing/AppFunctionTestRuleTest.kt
@@ -26,6 +26,7 @@
import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat
import kotlin.test.assertIs
+import kotlin.test.assertNotNull
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
@@ -34,6 +35,8 @@
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@@ -42,17 +45,22 @@
@RunWith(RobolectricTestRunner::class)
@Config(minSdk = Build.VERSION_CODES.TIRAMISU)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
-class AppFunctionTestEnvironmentTest {
+class AppFunctionTestRuleTest {
private val context = InstrumentationRegistry.getInstrumentation().context
private val targetContext = InstrumentationRegistry.getInstrumentation().targetContext
- private val appFunctionTestEnvironment = AppFunctionTestEnvironment(targetContext)
+ @get:Rule val appFunctionTestRule = AppFunctionTestRule(targetContext)
+
+ private lateinit var appFunctionManagerCompat: AppFunctionManagerCompat
+
+ @Before
+ fun setUp() {
+ appFunctionManagerCompat = assertNotNull(AppFunctionManagerCompat.getInstance(context))
+ }
@Test
fun returnedAppFunctionManagerCompat_observeApi_returnsAllAppFunctions() =
runBlocking<Unit> {
- val appFunctionManagerCompat = appFunctionTestEnvironment.getAppFunctionManagerCompat()
-
val results =
appFunctionManagerCompat
.observeAppFunctions(
@@ -68,7 +76,6 @@
fun returnedAppFunctionManagerCompat_observeApi_returnsNewValueOnUpdate() =
runBlocking<Unit> {
val functionIdToTest = "androidx.appfunctions.testing.TestFunctions#disabledByDefault"
- val appFunctionManagerCompat = appFunctionTestEnvironment.getAppFunctionManagerCompat()
val appFunctionSearchFlow =
appFunctionManagerCompat.observeAppFunctions(
AppFunctionSearchSpec(packageNames = setOf(context.packageName))
@@ -102,7 +109,6 @@
fun returnedAppFunctionManagerCompat_currentPackage_enabledByDefault_modified_success() =
runBlocking<Unit> {
val functionId = "androidx.appfunctions.testing.TestFunctions#enabledByDefault"
- val appFunctionManagerCompat = appFunctionTestEnvironment.getAppFunctionManagerCompat()
assertThat(appFunctionManagerCompat.isAppFunctionEnabled(functionId)).isTrue()
appFunctionManagerCompat.setAppFunctionEnabled(
@@ -117,7 +123,6 @@
fun returnedAppFunctionManagerCompat_currentPackage_disabledByDefault_modified_success() =
runBlocking<Unit> {
val functionId = "androidx.appfunctions.testing.TestFunctions#disabledByDefault"
- val appFunctionManagerCompat = appFunctionTestEnvironment.getAppFunctionManagerCompat()
assertThat(appFunctionManagerCompat.isAppFunctionEnabled(functionId)).isFalse()
appFunctionManagerCompat.setAppFunctionEnabled(
@@ -131,7 +136,6 @@
@Test
fun executeAppFunction_success() =
runBlocking<Unit> {
- val appFunctionManagerCompat = appFunctionTestEnvironment.getAppFunctionManagerCompat()
val response =
appFunctionManagerCompat.executeAppFunction(
request =
@@ -158,7 +162,6 @@
fun returnedAppFunctionManagerCompat_currentPackage_disabledByDefault_modifiedAndRestoredToDefault_success() =
runBlocking<Unit> {
val functionId = "androidx.appfunctions.testing.TestFunctions#disabledByDefault"
- val appFunctionManagerCompat = appFunctionTestEnvironment.getAppFunctionManagerCompat()
assertThat(appFunctionManagerCompat.isAppFunctionEnabled(functionId)).isFalse()
appFunctionManagerCompat.setAppFunctionEnabled(
diff --git a/appfunctions/appfunctions/src/main/java/androidx/appfunctions/AppFunctionManagerCompat.kt b/appfunctions/appfunctions/src/main/java/androidx/appfunctions/AppFunctionManagerCompat.kt
index ce07423..70cabe0 100644
--- a/appfunctions/appfunctions/src/main/java/androidx/appfunctions/AppFunctionManagerCompat.kt
+++ b/appfunctions/appfunctions/src/main/java/androidx/appfunctions/AppFunctionManagerCompat.kt
@@ -22,6 +22,8 @@
import androidx.annotation.IntDef
import androidx.annotation.RequiresPermission
import androidx.annotation.RestrictTo
+import androidx.appfunctions.AppFunctionManagerCompat.Companion.getInstance
+import androidx.appfunctions.AppFunctionManagerCompat.Companion.isExtensionLibraryAvailable
import androidx.appfunctions.internal.AppFunctionManagerApi
import androidx.appfunctions.internal.AppFunctionReader
import androidx.appfunctions.internal.AppSearchAppFunctionReader
@@ -212,6 +214,43 @@
/** The version shared across all schema defined in the legacy SDK. */
private const val LEGACY_SDK_GLOBAL_SCHEMA_VERSION = 1L
+ private var _appFunctionReader: AppFunctionReader? = null
+ private var _appFunctionManagerApi: AppFunctionManagerApi? = null
+
+ private var _skipExtensionLibraryCheck = false
+
+ /**
+ * Allows overriding the [AppFunctionReader] used for constructing
+ * [AppFunctionManagerCompat] instance in [getInstance] with a different implementation.
+ *
+ * Only meant to be used internally by `AppFunctionTestRule`.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public fun setAppFunctionReader(appFunctionReader: AppFunctionReader) {
+ _appFunctionReader = appFunctionReader
+ }
+
+ /**
+ * Allows overriding the [AppFunctionManagerApi] used for constructing
+ * [AppFunctionManagerCompat] instance in [getInstance] with a different implementation.
+ *
+ * Only meant to be used internally by `AppFunctionTestRule`.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public fun setAppFunctionManagerApi(appFunctionManagerApi: AppFunctionManagerApi) {
+ _appFunctionManagerApi = appFunctionManagerApi
+ }
+
+ /**
+ * Allows skipping [isExtensionLibraryAvailable] check in [getInstance].
+ *
+ * Only meant to be used internally by `AppFunctionTestRule`.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public fun setSkipExtensionLibraryCheck(skipExtensionLibraryCheck: Boolean) {
+ _skipExtensionLibraryCheck = skipExtensionLibraryCheck
+ }
+
/**
* Checks whether the AppFunction extension library is available.
*
@@ -219,6 +258,8 @@
* otherwise.
*/
private fun isExtensionLibraryAvailable(): Boolean {
+ if (_skipExtensionLibraryCheck) return true
+
return try {
Class.forName("com.android.extensions.appfunctions.AppFunctionManager")
true
@@ -244,11 +285,12 @@
Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA -> {
AppFunctionManagerCompat(
context,
- AppSearchAppFunctionReader(
- context,
- Dependencies.schemaAppFunctionInventory,
- ),
- PlatformAppFunctionManagerApi(context),
+ _appFunctionReader
+ ?: AppSearchAppFunctionReader(
+ context,
+ Dependencies.schemaAppFunctionInventory,
+ ),
+ _appFunctionManagerApi ?: PlatformAppFunctionManagerApi(context),
Dependencies.translatorSelector,
)
}
@@ -256,11 +298,12 @@
isExtensionLibraryAvailable() -> {
AppFunctionManagerCompat(
context,
- AppSearchAppFunctionReader(
- context,
- Dependencies.schemaAppFunctionInventory,
- ),
- ExtensionAppFunctionManagerApi(context),
+ _appFunctionReader
+ ?: AppSearchAppFunctionReader(
+ context,
+ Dependencies.schemaAppFunctionInventory,
+ ),
+ _appFunctionManagerApi ?: ExtensionAppFunctionManagerApi(context),
Dependencies.translatorSelector,
)
}