From df78aaafcdcf7e0954b080455f46af32a8c8f5a4 Mon Sep 17 00:00:00 2001
From: James Rich <2199651+jamesarich@users.noreply.github.com>
Date: Thu, 24 Sep 2026 23:34:43 +0000
Subject: [PATCH] refactor(appfunctions): migrate to
AppFunctionServiceEntryPoint (#7340)
---
androidApp/build.gradle.kts | 2 -
androidApp/detekt-baseline.xml | 14 +-
androidApp/src/google/AndroidManifest.xml | 44 +----
.../app/GoogleMeshUtilApplication.kt | 21 +--
.../ai/appfunctions/AppFunctionStateSync.kt | 3 +-
.../BaseMeshtasticAppFunctionService.kt | 155 ++++++++++++++++++
.../ai/appfunctions/MeshtasticAppFunctions.kt | 133 ++-------------
.../appfunctions/AppFunctionStateSyncTest.kt | 31 ++++
.../MeshtasticAppFunctionsTest.kt | 28 ++--
9 files changed, 223 insertions(+), 208 deletions(-)
create mode 100644 androidApp/src/google/kotlin/org/meshtastic/app/ai/appfunctions/BaseMeshtasticAppFunctionService.kt
diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts
index 0d432101b4..04adf1252e 100644
--- a/androidApp/build.gradle.kts
+++ b/androidApp/build.gradle.kts
@@ -197,8 +197,6 @@ secrets {
propertiesFileName = "secrets.properties"
}
-ksp { arg("appfunctions:aggregateAppFunctions", "true") }
-
androidComponents {
onVariants(selector().withBuildType("debug")) { variant ->
variant.flavorName?.let { flavor -> variant.applicationId.set("com.geeksville.mesh.$flavor.debug") }
diff --git a/androidApp/detekt-baseline.xml b/androidApp/detekt-baseline.xml
index 0f71fbcb08..c1fd804fb9 100644
--- a/androidApp/detekt-baseline.xml
+++ b/androidApp/detekt-baseline.xml
@@ -32,13 +32,13 @@
ReturnCount:MlKitMessageTranslator.kt:MlKitMessageTranslator$override suspend fun downloadLanguageModels: DownloadResult
ReturnCount:MlKitMessageTranslator.kt:MlKitMessageTranslator$override suspend fun translate: TranslationResult
SwallowedException:MapViewModel.kt:MapViewModel$e: Exception
- ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$@AppFunction(isDescribedByKDoc = true) suspend fun getChannelInfo: GetChannelInfoResponse
- ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$@AppFunction(isDescribedByKDoc = true) suspend fun getDeviceStatus: GetDeviceStatusResponse
- ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$@AppFunction(isDescribedByKDoc = true) suspend fun getMeshMetrics: GetMeshMetricsResponse
- ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$@AppFunction(isDescribedByKDoc = true) suspend fun getNodeDetails: GetNodeDetailsResponse
- ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$@AppFunction(isDescribedByKDoc = true) suspend fun getNodeList: GetNodeListResponse
- ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$@AppFunction(isDescribedByKDoc = true) suspend fun getRecentMessages: GetRecentMessagesResponse
- ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$@AppFunction(isDescribedByKDoc = true) suspend fun sendMessage: SendMessageResponse
+ ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$suspend fun getChannelInfo: GetChannelInfoResponse
+ ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$suspend fun getDeviceStatus: GetDeviceStatusResponse
+ ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$suspend fun getMeshMetrics: GetMeshMetricsResponse
+ ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$suspend fun getNodeDetails: GetNodeDetailsResponse
+ ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$suspend fun getNodeList: GetNodeListResponse
+ ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$suspend fun getRecentMessages: GetRecentMessagesResponse
+ ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$suspend fun sendMessage: SendMessageResponse
TooGenericExceptionCaught:FdroidMapOverlayRenderer.kt:FdroidMapOverlayRenderer$e: Exception
TooGenericExceptionCaught:GeminiNanoDocAssistant.kt:GeminiNanoDocAssistant$e: Exception
TooGenericExceptionCaught:MapView.kt:e: Exception
diff --git a/androidApp/src/google/AndroidManifest.xml b/androidApp/src/google/AndroidManifest.xml
index 5a5890ac93..0e4fa20883 100644
--- a/androidApp/src/google/AndroidManifest.xml
+++ b/androidApp/src/google/AndroidManifest.xml
@@ -58,56 +58,18 @@
-
+
-
-
-
-
-
-
-
-
-
-
+ android:value="meshtastic_app_function_service.xml" />
diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/GoogleMeshUtilApplication.kt b/androidApp/src/google/kotlin/org/meshtastic/app/GoogleMeshUtilApplication.kt
index 379127456d..5645e9a986 100644
--- a/androidApp/src/google/kotlin/org/meshtastic/app/GoogleMeshUtilApplication.kt
+++ b/androidApp/src/google/kotlin/org/meshtastic/app/GoogleMeshUtilApplication.kt
@@ -16,21 +16,12 @@
*/
package org.meshtastic.app
-import androidx.appfunctions.AppFunctionConfiguration
import kotlinx.coroutines.launch
import org.koin.java.KoinJavaComponent.getKoin
import org.meshtastic.app.ai.appfunctions.AppFunctionStateSync
-import org.meshtastic.app.ai.appfunctions.MeshtasticAppFunctions
-/**
- * Google flavor Application subclass that configures App Functions.
- *
- * Registers a custom factory so the AppFunctions runtime can instantiate [MeshtasticAppFunctions] with its Koin-managed
- * dependencies.
- */
-class GoogleMeshUtilApplication :
- MeshUtilApplication(),
- AppFunctionConfiguration.Provider {
+/** Google flavor Application subclass that starts the App Functions enabled-state sync. */
+class GoogleMeshUtilApplication : MeshUtilApplication() {
override fun onCreate() {
super.onCreate()
@@ -40,12 +31,4 @@ class GoogleMeshUtilApplication :
// Off-main: construction forces the AppFunctionsPrefs subgraph and fires AppSearch binder calls.
applicationScope.launch { getKoin().get() }
}
-
- override val appFunctionConfiguration: AppFunctionConfiguration
- get() =
- AppFunctionConfiguration.Builder()
- .addEnclosingClassFactory(MeshtasticAppFunctions::class.java) {
- getKoin().get()
- }
- .build()
}
diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/ai/appfunctions/AppFunctionStateSync.kt b/androidApp/src/google/kotlin/org/meshtastic/app/ai/appfunctions/AppFunctionStateSync.kt
index 09c533b91c..d710e0f1d5 100644
--- a/androidApp/src/google/kotlin/org/meshtastic/app/ai/appfunctions/AppFunctionStateSync.kt
+++ b/androidApp/src/google/kotlin/org/meshtastic/app/ai/appfunctions/AppFunctionStateSync.kt
@@ -134,7 +134,8 @@ class AppFunctionStateSync(
): List> =
if (actual == null) desired else desired.filter { (id, enabled) -> actual[id] != enabled }
- private const val CLASS_PREFIX = "org.meshtastic.app.ai.appfunctions.MeshtasticAppFunctions#"
+ // Mirrors the generated MeshtasticAppFunctionService.FUNCTION_ID_* constants, which are API 36 only.
+ private const val CLASS_PREFIX = "org.meshtastic.app.ai.appfunctions.BaseMeshtasticAppFunctionService#"
const val SEND_MESSAGE_ID = "${CLASS_PREFIX}sendMessage"
const val GET_MESH_STATUS_ID = "${CLASS_PREFIX}getMeshStatus"
diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/ai/appfunctions/BaseMeshtasticAppFunctionService.kt b/androidApp/src/google/kotlin/org/meshtastic/app/ai/appfunctions/BaseMeshtasticAppFunctionService.kt
new file mode 100644
index 0000000000..7e44a3d309
--- /dev/null
+++ b/androidApp/src/google/kotlin/org/meshtastic/app/ai/appfunctions/BaseMeshtasticAppFunctionService.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.meshtastic.app.ai.appfunctions
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.appfunctions.AppFunction
+import androidx.appfunctions.AppFunctionIntValueConstraint
+import androidx.appfunctions.AppFunctionService
+import androidx.appfunctions.AppFunctionServiceEntryPoint
+import org.koin.android.ext.android.inject
+import org.meshtastic.core.data.ai.AiFunctionProvider
+
+/**
+ * Exposes Meshtastic mesh networking capabilities to system AI assistants via the Android App Functions API. Functions
+ * declared here are discoverable by the system and can be invoked by AI agents such as Gemini.
+ *
+ * KSP generates the concrete [MeshtasticAppFunctionService] and its `meshtastic_app_function_service.xml` asset; the
+ * KDoc on each function is the description agents read.
+ */
+@RequiresApi(Build.VERSION_CODES.BAKLAVA)
+@AppFunctionServiceEntryPoint(
+ serviceName = "MeshtasticAppFunctionService",
+ appFunctionXmlFileName = "meshtastic_app_function_service",
+)
+abstract class BaseMeshtasticAppFunctionService : AppFunctionService() {
+
+ private val appFunctions: MeshtasticAppFunctions by inject()
+
+ /**
+ * Send a text message over the Meshtastic mesh radio network.
+ *
+ * Messages are transmitted to nearby mesh nodes using LoRa radio. The mesh network is ideal for off-grid
+ * communications where cellular service is unavailable.
+ *
+ * @param text The message text to send (max 228 UTF-8 bytes — the mesh payload left after protobuf framing).
+ * @param recipientName Optional name of a specific node to send a direct message to. If omitted, the message is
+ * broadcast to all nodes on the specified channel.
+ * @param channelName Optional channel name to broadcast on. If omitted, uses the primary channel. Ignored when
+ * recipientName is specified.
+ * @return A [SendMessageResponse] with the message ID, channel, and timestamp.
+ */
+ @AppFunction(isDescribedByKDoc = true)
+ suspend fun sendMessage(
+ text: String,
+ recipientName: String? = null,
+ channelName: String? = null,
+ ): SendMessageResponse = appFunctions.sendMessage(text, recipientName, channelName)
+
+ /**
+ * Get the current status of the Meshtastic mesh network.
+ *
+ * Returns connection state, number of online nodes, total known nodes, the connected device's battery level, and
+ * the local node name.
+ *
+ * @return A [MeshStatusResponse] with the current mesh network status.
+ */
+ @AppFunction(isDescribedByKDoc = true)
+ suspend fun getMeshStatus(): MeshStatusResponse = appFunctions.getMeshStatus()
+
+ /**
+ * List all nodes currently visible on the Meshtastic mesh network.
+ *
+ * Returns detailed information about each node including name, battery level, and last heard time. Nodes are sorted
+ * by most recently heard first.
+ *
+ * @return A list of nodes with their current status and metrics.
+ */
+ @AppFunction(isDescribedByKDoc = true)
+ suspend fun getNodeList(): GetNodeListResponse = appFunctions.getNodeList()
+
+ /**
+ * List all available Meshtastic mesh channels and their configurations.
+ *
+ * Returns details about each channel including name, index, primary status, and uplink/downlink settings.
+ *
+ * @return A list of channels with their current configuration.
+ */
+ @AppFunction(isDescribedByKDoc = true)
+ suspend fun getChannelInfo(): GetChannelInfoResponse = appFunctions.getChannelInfo()
+
+ /**
+ * Get the status and metrics of the local Meshtastic radio device.
+ *
+ * Returns hardware model, firmware version, battery level, charging status, and current radio state.
+ *
+ * @return Device status with current metrics and configuration.
+ */
+ @AppFunction(isDescribedByKDoc = true)
+ suspend fun getDeviceStatus(): GetDeviceStatusResponse = appFunctions.getDeviceStatus()
+
+ /**
+ * Retrieve detailed telemetry and status for a specific mesh node.
+ *
+ * Returns per-node metrics including battery level, signal strength, hardware model, and location data.
+ *
+ * @param nodeId The target node ID (e.g., '!abc12345' or user ID).
+ * @return A [GetNodeDetailsResponse] with detailed node information.
+ */
+ @AppFunction(isDescribedByKDoc = true)
+ suspend fun getNodeDetails(nodeId: String): GetNodeDetailsResponse = appFunctions.getNodeDetails(nodeId)
+
+ /**
+ * Retrieve aggregate network metrics and statistics for the entire mesh.
+ *
+ * Returns mesh-wide analytics including total node count, online nodes, average battery level, and health score.
+ *
+ * @return A [GetMeshMetricsResponse] with mesh-wide statistics.
+ */
+ @AppFunction(isDescribedByKDoc = true)
+ suspend fun getMeshMetrics(): GetMeshMetricsResponse = appFunctions.getMeshMetrics()
+
+ /**
+ * Retrieve recent messages received over the Meshtastic mesh radio network.
+ *
+ * Returns a list of recent messages from the local message history. Messages are stored locally and do not require
+ * an active mesh connection. Useful for catching up on conversations or reviewing recent communications.
+ *
+ * @param contactName Optional name of a node or channel to filter messages from. If omitted, returns messages from
+ * all contacts sorted by most recent.
+ * @param limit Maximum number of messages to return: 1, 5, 10, 20 or 50. Defaults to 20.
+ * @return A [GetRecentMessagesResponse] containing the list of recent messages.
+ */
+ @AppFunction(isDescribedByKDoc = true)
+ suspend fun getRecentMessages(
+ contactName: String? = null,
+ @AppFunctionIntValueConstraint(enumValues = [1, 5, 10, 20, 50])
+ limit: Int = AiFunctionProvider.DEFAULT_MESSAGE_LIMIT,
+ ): GetRecentMessagesResponse = appFunctions.getRecentMessages(contactName, limit)
+
+ /**
+ * Get a summary of unread messages across all Meshtastic mesh contacts.
+ *
+ * Returns the total unread count and a per-contact breakdown showing who sent unread messages, how many are unread,
+ * and a preview of the last message. Muted contacts are excluded. Does not require an active mesh connection.
+ *
+ * @return A [GetUnreadSummaryResponse] with the total unread count and per-contact details.
+ */
+ @AppFunction(isDescribedByKDoc = true)
+ suspend fun getUnreadSummary(): GetUnreadSummaryResponse = appFunctions.getUnreadSummary()
+}
diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/ai/appfunctions/MeshtasticAppFunctions.kt b/androidApp/src/google/kotlin/org/meshtastic/app/ai/appfunctions/MeshtasticAppFunctions.kt
index 46463302ba..0097575bbb 100644
--- a/androidApp/src/google/kotlin/org/meshtastic/app/ai/appfunctions/MeshtasticAppFunctions.kt
+++ b/androidApp/src/google/kotlin/org/meshtastic/app/ai/appfunctions/MeshtasticAppFunctions.kt
@@ -16,10 +16,7 @@
*/
package org.meshtastic.app.ai.appfunctions
-import androidx.appfunctions.AppFunction
-import androidx.appfunctions.AppFunctionContext
import androidx.appfunctions.AppFunctionElementNotFoundException
-import androidx.appfunctions.AppFunctionIntValueConstraint
import androidx.appfunctions.AppFunctionInvalidArgumentException
import androidx.appfunctions.AppFunctionNotSupportedException
import kotlinx.coroutines.TimeoutCancellationException
@@ -27,35 +24,12 @@ import org.meshtastic.core.data.ai.AiFunctionProvider
import org.meshtastic.core.data.ai.SendMessageResult
/**
- * Exposes Meshtastic mesh networking capabilities to system AI assistants via the Android App Functions API. Functions
- * declared here are discoverable by the system and can be invoked by AI agents such as Gemini.
+ * Maps [AiFunctionProvider] results to App Functions responses and exceptions. [BaseMeshtasticAppFunctionService]
+ * declares the functions agents see and delegates each one here.
*/
-// The AppFunctions calling convention requires every @AppFunction to take AppFunctionContext as
-// its first parameter, even when the implementation never reads it.
-@Suppress("UnusedParameter")
class MeshtasticAppFunctions(private val provider: AiFunctionProvider) {
- /**
- * Send a text message over the Meshtastic mesh radio network.
- *
- * Messages are transmitted to nearby mesh nodes using LoRa radio. The mesh network is ideal for off-grid
- * communications where cellular service is unavailable.
- *
- * @param context The app function invocation context provided by the system.
- * @param text The message text to send (max 228 UTF-8 bytes — the mesh payload left after protobuf framing).
- * @param recipientName Optional name of a specific node to send a direct message to. If omitted, the message is
- * broadcast to all nodes on the specified channel.
- * @param channelName Optional channel name to broadcast on. If omitted, uses the primary channel. Ignored when
- * recipientName is specified.
- * @return A [SendMessageResponse] with the message ID, channel, and timestamp.
- */
- @AppFunction(isDescribedByKDoc = true)
- suspend fun sendMessage(
- context: AppFunctionContext,
- text: String,
- recipientName: String? = null,
- channelName: String? = null,
- ): SendMessageResponse {
+ suspend fun sendMessage(text: String, recipientName: String?, channelName: String?): SendMessageResponse {
val result =
try {
provider.sendMessage(text, recipientName, channelName)
@@ -91,17 +65,7 @@ class MeshtasticAppFunctions(private val provider: AiFunctionProvider) {
}
}
- /**
- * Get the current status of the Meshtastic mesh network.
- *
- * Returns connection state, number of online nodes, total known nodes, the connected device's battery level, and
- * the local node name.
- *
- * @param context The app function invocation context provided by the system.
- * @return A [MeshStatusResponse] with the current mesh network status.
- */
- @AppFunction(isDescribedByKDoc = true)
- suspend fun getMeshStatus(context: AppFunctionContext): MeshStatusResponse {
+ suspend fun getMeshStatus(): MeshStatusResponse {
val status =
try {
provider.getMeshStatus()
@@ -120,17 +84,7 @@ class MeshtasticAppFunctions(private val provider: AiFunctionProvider) {
)
}
- /**
- * List all nodes currently visible on the Meshtastic mesh network.
- *
- * Returns detailed information about each node including name, battery level, and last heard time. Nodes are sorted
- * by most recently heard first.
- *
- * @param context The app function invocation context provided by the system.
- * @return A list of nodes with their current status and metrics.
- */
- @AppFunction(isDescribedByKDoc = true)
- suspend fun getNodeList(context: AppFunctionContext): GetNodeListResponse {
+ suspend fun getNodeList(): GetNodeListResponse {
val result =
try {
provider.getNodeList()
@@ -163,16 +117,7 @@ class MeshtasticAppFunctions(private val provider: AiFunctionProvider) {
}
}
- /**
- * List all available Meshtastic mesh channels and their configurations.
- *
- * Returns details about each channel including name, index, primary status, and uplink/downlink settings.
- *
- * @param context The app function invocation context provided by the system.
- * @return A list of channels with their current configuration.
- */
- @AppFunction(isDescribedByKDoc = true)
- suspend fun getChannelInfo(context: AppFunctionContext): GetChannelInfoResponse {
+ suspend fun getChannelInfo(): GetChannelInfoResponse {
val result =
try {
provider.getChannelInfo()
@@ -205,16 +150,7 @@ class MeshtasticAppFunctions(private val provider: AiFunctionProvider) {
}
}
- /**
- * Get the status and metrics of the local Meshtastic radio device.
- *
- * Returns hardware model, firmware version, battery level, charging status, and current radio state.
- *
- * @param context The app function invocation context provided by the system.
- * @return Device status with current metrics and configuration.
- */
- @AppFunction(isDescribedByKDoc = true)
- suspend fun getDeviceStatus(context: AppFunctionContext): GetDeviceStatusResponse {
+ suspend fun getDeviceStatus(): GetDeviceStatusResponse {
val result =
try {
provider.getDeviceStatus()
@@ -243,17 +179,7 @@ class MeshtasticAppFunctions(private val provider: AiFunctionProvider) {
}
}
- /**
- * Retrieve detailed telemetry and status for a specific mesh node.
- *
- * Returns per-node metrics including battery level, signal strength, hardware model, and location data.
- *
- * @param context The app function invocation context provided by the system.
- * @param nodeId The target node ID (e.g., '!abc12345' or user ID).
- * @return A [GetNodeDetailsResponse] with detailed node information.
- */
- @AppFunction(isDescribedByKDoc = true)
- suspend fun getNodeDetails(context: AppFunctionContext, nodeId: String): GetNodeDetailsResponse {
+ suspend fun getNodeDetails(nodeId: String): GetNodeDetailsResponse {
val result =
try {
provider.getNodeDetails(nodeId)
@@ -294,16 +220,7 @@ class MeshtasticAppFunctions(private val provider: AiFunctionProvider) {
}
}
- /**
- * Retrieve aggregate network metrics and statistics for the entire mesh.
- *
- * Returns mesh-wide analytics including total node count, online nodes, average battery level, and health score.
- *
- * @param context The app function invocation context provided by the system.
- * @return A [GetMeshMetricsResponse] with mesh-wide statistics.
- */
- @AppFunction(isDescribedByKDoc = true)
- suspend fun getMeshMetrics(context: AppFunctionContext): GetMeshMetricsResponse {
+ suspend fun getMeshMetrics(): GetMeshMetricsResponse {
val result =
try {
provider.getMeshMetrics()
@@ -332,25 +249,7 @@ class MeshtasticAppFunctions(private val provider: AiFunctionProvider) {
}
}
- /**
- * Retrieve recent messages received over the Meshtastic mesh radio network.
- *
- * Returns a list of recent messages from the local message history. Messages are stored locally and do not require
- * an active mesh connection. Useful for catching up on conversations or reviewing recent communications.
- *
- * @param context The app function invocation context provided by the system.
- * @param contactName Optional name of a node or channel to filter messages from. If omitted, returns messages from
- * all contacts sorted by most recent.
- * @param limit Maximum number of messages to return (1–50). Defaults to 20.
- * @return A [GetRecentMessagesResponse] containing the list of recent messages.
- */
- @AppFunction(isDescribedByKDoc = true)
- suspend fun getRecentMessages(
- context: AppFunctionContext,
- contactName: String? = null,
- @AppFunctionIntValueConstraint(enumValues = [1, 5, 10, 20, 50])
- limit: Int = AiFunctionProvider.DEFAULT_MESSAGE_LIMIT,
- ): GetRecentMessagesResponse {
+ suspend fun getRecentMessages(contactName: String?, limit: Int): GetRecentMessagesResponse {
val result =
try {
provider.getRecentMessages(contactName, limit)
@@ -381,17 +280,7 @@ class MeshtasticAppFunctions(private val provider: AiFunctionProvider) {
}
}
- /**
- * Get a summary of unread messages across all Meshtastic mesh contacts.
- *
- * Returns the total unread count and a per-contact breakdown showing who sent unread messages, how many are unread,
- * and a preview of the last message. Muted contacts are excluded. Does not require an active mesh connection.
- *
- * @param context The app function invocation context provided by the system.
- * @return A [GetUnreadSummaryResponse] with the total unread count and per-contact details.
- */
- @AppFunction(isDescribedByKDoc = true)
- suspend fun getUnreadSummary(context: AppFunctionContext): GetUnreadSummaryResponse {
+ suspend fun getUnreadSummary(): GetUnreadSummaryResponse {
val result =
try {
provider.getUnreadSummary()
diff --git a/androidApp/src/testGoogle/kotlin/org/meshtastic/app/ai/appfunctions/AppFunctionStateSyncTest.kt b/androidApp/src/testGoogle/kotlin/org/meshtastic/app/ai/appfunctions/AppFunctionStateSyncTest.kt
index 6a8e8e0f21..e44e667325 100644
--- a/androidApp/src/testGoogle/kotlin/org/meshtastic/app/ai/appfunctions/AppFunctionStateSyncTest.kt
+++ b/androidApp/src/testGoogle/kotlin/org/meshtastic/app/ai/appfunctions/AppFunctionStateSyncTest.kt
@@ -51,4 +51,35 @@ class AppFunctionStateSyncTest {
assertEquals(listOf(AppFunctionStateSync.GET_NODE_LIST_ID to false), pendingWrites(desired, actual))
}
+
+ @Test
+ fun `toggle ids match the generated function ids`() {
+ assertEquals(MeshtasticAppFunctionService.FUNCTION_ID_SEND_MESSAGE, AppFunctionStateSync.SEND_MESSAGE_ID)
+ assertEquals(MeshtasticAppFunctionService.FUNCTION_ID_GET_MESH_STATUS, AppFunctionStateSync.GET_MESH_STATUS_ID)
+ assertEquals(MeshtasticAppFunctionService.FUNCTION_ID_GET_NODE_LIST, AppFunctionStateSync.GET_NODE_LIST_ID)
+ assertEquals(
+ MeshtasticAppFunctionService.FUNCTION_ID_GET_CHANNEL_INFO,
+ AppFunctionStateSync.GET_CHANNEL_INFO_ID,
+ )
+ assertEquals(
+ MeshtasticAppFunctionService.FUNCTION_ID_GET_DEVICE_STATUS,
+ AppFunctionStateSync.GET_DEVICE_STATUS_ID,
+ )
+ assertEquals(
+ MeshtasticAppFunctionService.FUNCTION_ID_GET_NODE_DETAILS,
+ AppFunctionStateSync.GET_NODE_DETAILS_ID,
+ )
+ assertEquals(
+ MeshtasticAppFunctionService.FUNCTION_ID_GET_MESH_METRICS,
+ AppFunctionStateSync.GET_MESH_METRICS_ID,
+ )
+ assertEquals(
+ MeshtasticAppFunctionService.FUNCTION_ID_GET_RECENT_MESSAGES,
+ AppFunctionStateSync.GET_RECENT_MESSAGES_ID,
+ )
+ assertEquals(
+ MeshtasticAppFunctionService.FUNCTION_ID_GET_UNREAD_SUMMARY,
+ AppFunctionStateSync.GET_UNREAD_SUMMARY_ID,
+ )
+ }
}
diff --git a/androidApp/src/testGoogle/kotlin/org/meshtastic/app/ai/appfunctions/MeshtasticAppFunctionsTest.kt b/androidApp/src/testGoogle/kotlin/org/meshtastic/app/ai/appfunctions/MeshtasticAppFunctionsTest.kt
index edc7bb3479..17c146fc4a 100644
--- a/androidApp/src/testGoogle/kotlin/org/meshtastic/app/ai/appfunctions/MeshtasticAppFunctionsTest.kt
+++ b/androidApp/src/testGoogle/kotlin/org/meshtastic/app/ai/appfunctions/MeshtasticAppFunctionsTest.kt
@@ -16,7 +16,6 @@
*/
package org.meshtastic.app.ai.appfunctions
-import androidx.appfunctions.AppFunctionContext
import androidx.appfunctions.AppFunctionElementNotFoundException
import androidx.appfunctions.AppFunctionInvalidArgumentException
import androidx.appfunctions.AppFunctionNotSupportedException
@@ -56,7 +55,6 @@ import kotlin.test.assertTrue
class MeshtasticAppFunctionsTest {
private val provider: AiFunctionProvider = mock(MockMode.autofill)
- private val context: AppFunctionContext = mock(MockMode.autofill)
private val appFunctions = MeshtasticAppFunctions(provider)
@Test
@@ -64,7 +62,7 @@ class MeshtasticAppFunctionsTest {
everySuspend { provider.sendMessage("Hello", "Alice", null) } returns
SendMessageResult.Success(messageId = 1234, channel = "Primary", timestamp = 1700000000L)
- val response = appFunctions.sendMessage(context, "Hello", "Alice", null)
+ val response = appFunctions.sendMessage("Hello", "Alice", null)
assertEquals(1234, response.messageId)
assertEquals("Primary", response.channel)
@@ -77,9 +75,7 @@ class MeshtasticAppFunctionsTest {
SendMessageResult.AmbiguousName(listOf("Alice", "Albert"))
val exception =
- assertFailsWith {
- appFunctions.sendMessage(context, "Hello", "Al", null)
- }
+ assertFailsWith { appFunctions.sendMessage("Hello", "Al", null) }
assertTrue(exception.message!!.contains("Multiple nodes match that name"))
}
@@ -88,7 +84,7 @@ class MeshtasticAppFunctionsTest {
everySuspend { provider.sendMessage("Hello", "Alice", null) } returns
SendMessageResult.NotConnected("Not connected")
- assertFailsWith { appFunctions.sendMessage(context, "Hello", "Alice", null) }
+ assertFailsWith { appFunctions.sendMessage("Hello", "Alice", null) }
}
@Test
@@ -102,7 +98,7 @@ class MeshtasticAppFunctionsTest {
localNodeName = "MyNode",
)
- val response = appFunctions.getMeshStatus(context)
+ val response = appFunctions.getMeshStatus()
assertEquals("CONNECTED", response.connectionState)
assertEquals(5, response.onlineNodeCount)
@@ -120,7 +116,7 @@ class MeshtasticAppFunctionsTest {
)
everySuspend { provider.getNodeList() } returns GetNodeListResult.Success(nodes)
- val response = appFunctions.getNodeList(context)
+ val response = appFunctions.getNodeList()
assertEquals(2, response.nodes.size)
assertEquals("1", response.nodes[0].id)
@@ -152,7 +148,7 @@ class MeshtasticAppFunctionsTest {
)
everySuspend { provider.getChannelInfo() } returns GetChannelInfoResult.Success(channels)
- val response = appFunctions.getChannelInfo(context)
+ val response = appFunctions.getChannelInfo()
assertEquals(2, response.channels.size)
assertEquals("Primary", response.channels[0].name)
@@ -174,7 +170,7 @@ class MeshtasticAppFunctionsTest {
)
everySuspend { provider.getDeviceStatus() } returns GetDeviceStatusResult.Success(device)
- val response = appFunctions.getDeviceStatus(context)
+ val response = appFunctions.getDeviceStatus()
assertEquals("T-Beam", response.model)
assertEquals("2.3.15", response.firmwareVersion)
@@ -207,7 +203,7 @@ class MeshtasticAppFunctionsTest {
)
everySuspend { provider.getNodeDetails("!abc12345") } returns GetNodeDetailsResult.Success(nodeDetails)
- val response = appFunctions.getNodeDetails(context, "!abc12345")
+ val response = appFunctions.getNodeDetails("!abc12345")
assertEquals("!abc12345", response.id)
assertEquals("TestNode", response.name)
@@ -220,7 +216,7 @@ class MeshtasticAppFunctionsTest {
fun getNodeDetails_notFound() = runTest {
everySuspend { provider.getNodeDetails("!unknown") } returns GetNodeDetailsResult.NotFound("Node not found")
- assertFailsWith { appFunctions.getNodeDetails(context, "!unknown") }
+ assertFailsWith { appFunctions.getNodeDetails("!unknown") }
}
@Test
@@ -237,7 +233,7 @@ class MeshtasticAppFunctionsTest {
)
everySuspend { provider.getMeshMetrics() } returns GetMeshMetricsResult.Success(metrics)
- val response = appFunctions.getMeshMetrics(context)
+ val response = appFunctions.getMeshMetrics()
assertEquals(12, response.totalNodeCount)
assertEquals(4, response.onlineNodeCount)
@@ -260,7 +256,7 @@ class MeshtasticAppFunctionsTest {
)
everySuspend { provider.getRecentMessages(null, 10) } returns GetRecentMessagesResult.Success(messages)
- val response = appFunctions.getRecentMessages(context, null, 10)
+ val response = appFunctions.getRecentMessages(null, 10)
assertEquals(1, response.messages.size)
assertEquals("Alice", response.messages[0].senderName)
@@ -284,7 +280,7 @@ class MeshtasticAppFunctionsTest {
)
everySuspend { provider.getUnreadSummary() } returns GetUnreadSummaryResult.Success(summary)
- val response = appFunctions.getUnreadSummary(context)
+ val response = appFunctions.getUnreadSummary()
assertEquals(3, response.totalUnreadCount)
assertEquals(1, response.contacts.size)