Merge "Add AggregateAppFunctionInvoker" into androidx-main
diff --git a/appfunctions/appfunctions-runtime/build.gradle b/appfunctions/appfunctions-runtime/build.gradle
index a95d75a..146190f 100644
--- a/appfunctions/appfunctions-runtime/build.gradle
+++ b/appfunctions/appfunctions-runtime/build.gradle
@@ -36,8 +36,15 @@
implementation("androidx.annotation:annotation:1.9.0-rc01")
implementation project(":appfunctions:appfunctions-common")
+ // Test dependencies
testImplementation(libs.junit)
testImplementation(libs.truth)
+
+ androidTestImplementation(libs.testCore)
+ androidTestImplementation(libs.testRules)
+ androidTestImplementation(libs.junit)
+ androidTestImplementation(libs.truth)
+ androidTestImplementation(libs.kotlinCoroutinesTest)
}
android {
diff --git a/appfunctions/appfunctions-runtime/src/androidTest/java/androidx/appfunctions/internal/AggregateAppFunctionInvokerTest.kt b/appfunctions/appfunctions-runtime/src/androidTest/java/androidx/appfunctions/internal/AggregateAppFunctionInvokerTest.kt
new file mode 100644
index 0000000..1d63cd0
--- /dev/null
+++ b/appfunctions/appfunctions-runtime/src/androidTest/java/androidx/appfunctions/internal/AggregateAppFunctionInvokerTest.kt
@@ -0,0 +1,151 @@
+/*
+ * 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.internal
+
+import android.content.Context
+import android.content.pm.SigningInfo
+import androidx.appfunctions.AppFunctionContext
+import androidx.appfunctions.AppFunctionFunctionNotFoundException
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Assert
+import org.junit.Test
+
+class AggregateAppFunctionInvokerTest {
+
+ @Test
+ fun testEmptyAggregateInvoker() {
+ val aggregateInvoker =
+ object : AggregateAppFunctionInvoker() {
+ override val invokers: List<AppFunctionInvoker> = emptyList()
+ }
+
+ assertThat(aggregateInvoker.supportedFunctionIds).isEmpty()
+ Assert.assertThrows(AppFunctionFunctionNotFoundException::class.java) {
+ runBlocking {
+ aggregateInvoker.unsafeInvoke(
+ FakeAppFunctionContext,
+ "androidx.apfunctions.internal#test1",
+ mapOf()
+ )
+ }
+ }
+ }
+
+ @Test
+ fun testAggregateInvoker_nonExistFunction() {
+ val aggregateInvoker =
+ object : AggregateAppFunctionInvoker() {
+ override val invokers: List<AppFunctionInvoker> =
+ listOf(
+ Invoker1(),
+ Invoker2(),
+ )
+ }
+
+ assertThat(aggregateInvoker.supportedFunctionIds).hasSize(2)
+ assertThat(aggregateInvoker.supportedFunctionIds)
+ .containsNoneIn(listOf("androidx.appfunctions.internal#test0"))
+ Assert.assertThrows(AppFunctionFunctionNotFoundException::class.java) {
+ runBlocking {
+ aggregateInvoker.unsafeInvoke(
+ FakeAppFunctionContext,
+ "androidx.apfunctions.internal#test0",
+ mapOf()
+ )
+ }
+ }
+ }
+
+ @Test
+ fun testAggregateInvoker_existFunctions() {
+ val aggregateInvoker =
+ object : AggregateAppFunctionInvoker() {
+ override val invokers: List<AppFunctionInvoker> =
+ listOf(
+ Invoker1(),
+ Invoker2(),
+ )
+ }
+
+ val invokeTest1Result = runBlocking {
+ aggregateInvoker.unsafeInvoke(
+ FakeAppFunctionContext,
+ "androidx.appfunctions.internal#test1",
+ mapOf()
+ )
+ }
+ val invokeTest2Result = runBlocking {
+ aggregateInvoker.unsafeInvoke(
+ FakeAppFunctionContext,
+ "androidx.appfunctions.internal#test2",
+ mapOf()
+ )
+ }
+
+ assertThat(aggregateInvoker.supportedFunctionIds)
+ .containsExactly(
+ "androidx.appfunctions.internal#test1",
+ "androidx.appfunctions.internal#test2"
+ )
+ assertThat(invokeTest1Result).isEqualTo("Invoker1#test1")
+ assertThat(invokeTest2Result).isEqualTo("Invoker2#test2")
+ }
+
+ private class Invoker1 : AppFunctionInvoker {
+ override val supportedFunctionIds: Set<String> =
+ setOf("androidx.appfunctions.internal#test1")
+
+ override suspend fun unsafeInvoke(
+ appFunctionContext: AppFunctionContext,
+ functionIdentifier: String,
+ parameters: Map<String, Any?>
+ ): Any? {
+ return when (functionIdentifier) {
+ "androidx.appfunctions.internal#test1" -> "Invoker1#test1"
+ else -> throw IllegalArgumentException()
+ }
+ }
+ }
+
+ private class Invoker2 : AppFunctionInvoker {
+ override val supportedFunctionIds: Set<String> =
+ setOf("androidx.appfunctions.internal#test2")
+
+ override suspend fun unsafeInvoke(
+ appFunctionContext: AppFunctionContext,
+ functionIdentifier: String,
+ parameters: Map<String, Any?>
+ ): Any? {
+ return when (functionIdentifier) {
+ "androidx.appfunctions.internal#test2" -> "Invoker2#test2"
+ else -> throw IllegalArgumentException()
+ }
+ }
+ }
+
+ private object FakeAppFunctionContext : AppFunctionContext {
+ override val context: Context
+ get() = throw RuntimeException("Stub!")
+
+ override val callingPackageName: String
+ get() = throw RuntimeException("Stub!")
+
+ override val callingPackageSigningInfo: SigningInfo
+ get() = throw RuntimeException("Stub!")
+ }
+}
diff --git a/appfunctions/appfunctions-runtime/src/main/java/androidx/appfunctions/internal/AggregateAppFunctionInvoker.kt b/appfunctions/appfunctions-runtime/src/main/java/androidx/appfunctions/internal/AggregateAppFunctionInvoker.kt
new file mode 100644
index 0000000..df6be5a
--- /dev/null
+++ b/appfunctions/appfunctions-runtime/src/main/java/androidx/appfunctions/internal/AggregateAppFunctionInvoker.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.internal
+
+import androidx.annotation.RestrictTo
+import androidx.appfunctions.AppFunctionContext
+import androidx.appfunctions.AppFunctionFunctionNotFoundException
+
+/**
+ * An [AppFunctionInvoker] that will delegate [unsafeInvoke] to the implementation that supports the
+ * given function call request.
+ *
+ * AppFunction compiler will automatically generate the implementation of this class.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public abstract class AggregateAppFunctionInvoker : AppFunctionInvoker {
+
+ /** The list of [AppFunctionInvoker] instances that contribute to this aggregate. */
+ public abstract val invokers: List<AppFunctionInvoker>
+
+ final override val supportedFunctionIds: Set<String> by lazy {
+ // Empty collection can't be reduced
+ if (invokers.isEmpty()) return@lazy emptySet<String>()
+ invokers.map(AppFunctionInvoker::supportedFunctionIds).reduce { acc, ids -> acc + ids }
+ }
+
+ final override suspend fun unsafeInvoke(
+ appFunctionContext: AppFunctionContext,
+ functionIdentifier: String,
+ parameters: Map<String, Any?>
+ ): Any? {
+ for (invoker in invokers) {
+ if (invoker.supportedFunctionIds.contains(functionIdentifier)) {
+ return invoker.unsafeInvoke(appFunctionContext, functionIdentifier, parameters)
+ }
+ }
+ throw AppFunctionFunctionNotFoundException("Unable to find $functionIdentifier")
+ }
+}
diff --git a/appfunctions/appfunctions-runtime/src/main/java/androidx/appfunctions/internal/AppFunctionInvoker.kt b/appfunctions/appfunctions-runtime/src/main/java/androidx/appfunctions/internal/AppFunctionInvoker.kt
index 59c4bca..4567641 100644
--- a/appfunctions/appfunctions-runtime/src/main/java/androidx/appfunctions/internal/AppFunctionInvoker.kt
+++ b/appfunctions/appfunctions-runtime/src/main/java/androidx/appfunctions/internal/AppFunctionInvoker.kt
@@ -50,11 +50,10 @@
/**
* Invokes an AppFunction identified by [functionIdentifier], with [parameters].
*
- * @throws [androidx.appfunctions.AppFunctionException] with error code
- * [androidx.appfunctions.AppFunctionException.ERROR_FUNCTION_NOT_FOUND] if called with
- * invalid function identifier or code
- * [androidx.appfunctions.AppFunctionException.ERROR_INVALID_ARGUMENT] if called with invalid
- * parameters.
+ * @throws [androidx.appfunctions.AppFunctionFunctionNotFoundException] if [functionIdentifier]
+ * does not exist.
+ * @throws [androidx.appfunctions.AppFunctionInvalidArgumentException] if [parameters] is
+ * invalid.
*/
public suspend fun unsafeInvoke(
appFunctionContext: AppFunctionContext,