Add AggregateAppFunctionInventory abstract class

Bug: 374924489
Test: ./gradlew :appfunctions:appfunctions-runtime:test
Change-Id: I948c1c484dda6b35d94f6d0bf330ae77ee1d88cf
diff --git a/appfunctions/appfunctions-runtime/build.gradle b/appfunctions/appfunctions-runtime/build.gradle
index 3b37e08..a95d75a 100644
--- a/appfunctions/appfunctions-runtime/build.gradle
+++ b/appfunctions/appfunctions-runtime/build.gradle
@@ -35,6 +35,9 @@
     // Internal dependencies
     implementation("androidx.annotation:annotation:1.9.0-rc01")
     implementation project(":appfunctions:appfunctions-common")
+
+    testImplementation(libs.junit)
+    testImplementation(libs.truth)
 }
 
 android {
diff --git a/appfunctions/appfunctions-runtime/src/main/java/androidx/appfunctions/internal/AggregateAppFunctionInventory.kt b/appfunctions/appfunctions-runtime/src/main/java/androidx/appfunctions/internal/AggregateAppFunctionInventory.kt
new file mode 100644
index 0000000..6f792de
--- /dev/null
+++ b/appfunctions/appfunctions-runtime/src/main/java/androidx/appfunctions/internal/AggregateAppFunctionInventory.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.metadata.AppFunctionMetadata
+
+/**
+ * An [AppFunctionInventory] that aggregates the function metadata from multiple
+ * [AppFunctionInventory] instances.
+ *
+ * AppFunction compiler will automatically generate the implementation of this class to access all
+ * generated [AppFunctionMetadata] exposed by the application.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public abstract class AggregateAppFunctionInventory : AppFunctionInventory {
+
+    /** The list of [AppFunctionInventory] instances that contribute to this aggregate. */
+    public abstract val inventories: List<AppFunctionInventory>
+
+    final override val functionIdToMetadataMap: Map<String, AppFunctionMetadata> by lazy {
+        // Empty collection can't be reduced
+        if (inventories.isEmpty()) return@lazy emptyMap()
+        inventories.map(AppFunctionInventory::functionIdToMetadataMap).reduce { acc, map ->
+            acc + map
+        }
+    }
+}
diff --git a/appfunctions/appfunctions-runtime/src/test/java/androidx/appfunctions/internal/AggregateAppFunctionInventoryTest.kt b/appfunctions/appfunctions-runtime/src/test/java/androidx/appfunctions/internal/AggregateAppFunctionInventoryTest.kt
new file mode 100644
index 0000000..da0da07
--- /dev/null
+++ b/appfunctions/appfunctions-runtime/src/test/java/androidx/appfunctions/internal/AggregateAppFunctionInventoryTest.kt
@@ -0,0 +1,116 @@
+/*
+ * 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.appfunctions.metadata.AppFunctionComponentsMetadata
+import androidx.appfunctions.metadata.AppFunctionDataTypeMetadata
+import androidx.appfunctions.metadata.AppFunctionMetadata
+import androidx.appfunctions.metadata.AppFunctionResponseMetadata
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class AggregateAppFunctionInventoryTest {
+
+    @Test
+    fun testEmpty() {
+        val aggregateInventory =
+            object : AggregateAppFunctionInventory() {
+                override val inventories: List<AppFunctionInventory> = emptyList()
+            }
+
+        assertThat(aggregateInventory.functionIdToMetadataMap).hasSize(0)
+    }
+
+    @Test
+    fun testUniqueInventories() {
+        val aggregateInventory =
+            object : AggregateAppFunctionInventory() {
+                override val inventories: List<AppFunctionInventory> =
+                    listOf(Inventory1(), Inventory2())
+            }
+
+        assertThat(aggregateInventory.functionIdToMetadataMap).hasSize(2)
+        assertThat(aggregateInventory.functionIdToMetadataMap.keys)
+            .containsExactly(
+                "androix.appfunctions.internal#test1",
+                "androix.appfunctions.internal#test2"
+            )
+    }
+
+    @Test
+    fun testDuplicatedInventories() {
+        val aggregateInventory =
+            object : AggregateAppFunctionInventory() {
+                override val inventories: List<AppFunctionInventory> =
+                    listOf(Inventory1(), Inventory1())
+            }
+
+        assertThat(aggregateInventory.functionIdToMetadataMap).hasSize(1)
+        assertThat(aggregateInventory.functionIdToMetadataMap.keys)
+            .containsExactly(
+                "androix.appfunctions.internal#test1",
+            )
+    }
+
+    private class Inventory1 : AppFunctionInventory {
+        override val functionIdToMetadataMap: Map<String, AppFunctionMetadata> =
+            mapOf(
+                "androix.appfunctions.internal#test1" to
+                    AppFunctionMetadata(
+                        id = "androix.appfunctions.internal#test1",
+                        isEnabledByDefault = false,
+                        isRestrictToTrustedCaller = false,
+                        displayNameRes = 0,
+                        schema = null,
+                        parameters = emptyList(),
+                        response =
+                            AppFunctionResponseMetadata(
+                                isNullable = false,
+                                dataType =
+                                    AppFunctionDataTypeMetadata(
+                                        type = AppFunctionDataTypeMetadata.UNIT
+                                    )
+                            ),
+                        components = AppFunctionComponentsMetadata(dataTypes = emptyList())
+                    )
+            )
+    }
+
+    private class Inventory2 : AppFunctionInventory {
+        override val functionIdToMetadataMap: Map<String, AppFunctionMetadata> =
+            mapOf(
+                "androix.appfunctions.internal#test2" to
+                    AppFunctionMetadata(
+                        id = "androix.appfunctions.internal#test2",
+                        isEnabledByDefault = false,
+                        isRestrictToTrustedCaller = false,
+                        displayNameRes = 0,
+                        schema = null,
+                        parameters = emptyList(),
+                        response =
+                            AppFunctionResponseMetadata(
+                                isNullable = false,
+                                dataType =
+                                    AppFunctionDataTypeMetadata(
+                                        type = AppFunctionDataTypeMetadata.UNIT
+                                    )
+                            ),
+                        components = AppFunctionComponentsMetadata(dataTypes = emptyList())
+                    )
+            )
+    }
+}