refactor(appfunctions): migrate to AppFunctionServiceEntryPoint (#7340)

This commit is contained in:
James Rich authored and GitHub committed 2026-09-24 23:34:43 +00:00
1 parent 9962e401ed
commit df78aaafcd
9 files changed
+223 -208

No files matched your search

-2
View File
@@ -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") }
+7 -7
View File
@@ -32,13 +32,13 @@
<ID>ReturnCount:MlKitMessageTranslator.kt:MlKitMessageTranslator$override suspend fun downloadLanguageModels: DownloadResult</ID>
<ID>ReturnCount:MlKitMessageTranslator.kt:MlKitMessageTranslator$override suspend fun translate: TranslationResult</ID>
<ID>SwallowedException:MapViewModel.kt:MapViewModel$e: Exception</ID>
<ID>ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$@AppFunction(isDescribedByKDoc = true) suspend fun getChannelInfo: GetChannelInfoResponse</ID>
<ID>ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$@AppFunction(isDescribedByKDoc = true) suspend fun getDeviceStatus: GetDeviceStatusResponse</ID>
<ID>ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$@AppFunction(isDescribedByKDoc = true) suspend fun getMeshMetrics: GetMeshMetricsResponse</ID>
<ID>ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$@AppFunction(isDescribedByKDoc = true) suspend fun getNodeDetails: GetNodeDetailsResponse</ID>
<ID>ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$@AppFunction(isDescribedByKDoc = true) suspend fun getNodeList: GetNodeListResponse</ID>
<ID>ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$@AppFunction(isDescribedByKDoc = true) suspend fun getRecentMessages: GetRecentMessagesResponse</ID>
<ID>ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$@AppFunction(isDescribedByKDoc = true) suspend fun sendMessage: SendMessageResponse</ID>
<ID>ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$suspend fun getChannelInfo: GetChannelInfoResponse</ID>
<ID>ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$suspend fun getDeviceStatus: GetDeviceStatusResponse</ID>
<ID>ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$suspend fun getMeshMetrics: GetMeshMetricsResponse</ID>
<ID>ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$suspend fun getNodeDetails: GetNodeDetailsResponse</ID>
<ID>ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$suspend fun getNodeList: GetNodeListResponse</ID>
<ID>ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$suspend fun getRecentMessages: GetRecentMessagesResponse</ID>
<ID>ThrowsCount:MeshtasticAppFunctions.kt:MeshtasticAppFunctions$suspend fun sendMessage: SendMessageResponse</ID>
<ID>TooGenericExceptionCaught:FdroidMapOverlayRenderer.kt:FdroidMapOverlayRenderer$e: Exception</ID>
<ID>TooGenericExceptionCaught:GeminiNanoDocAssistant.kt:GeminiNanoDocAssistant$e: Exception</ID>
<ID>TooGenericExceptionCaught:MapView.kt:e: Exception</ID>
+3 -41
View File
@@ -58,56 +58,18 @@
</intent-filter>
</activity>
<!--
AppFunctions 1.0.0-alpha10 discontinued the appfunctions-service artifact whose
manifest used to contribute these two delegate services; apps must now declare
them (mirrors the alpha09 appfunctions-service AAR manifest, classes relocated
to the androidx.appfunctions package).
-->
<!-- Generated by the AppFunctions compiler from BaseMeshtasticAppFunctionService. -->
<service
android:name="androidx.appfunctions.PlatformAppFunctionService"
android:enabled="@bool/enablePlatformAppFunctionService"
android:name="org.meshtastic.app.ai.appfunctions.MeshtasticAppFunctionService"
android:exported="true"
android:permission="android.permission.BIND_APP_FUNCTION_SERVICE"
tools:targetApi="36">
<property
android:name="android.app.appfunctions"
android:value="app_functions.xml" />
<property
android:name="android.app.appfunctions.schema"
android:value="app_functions_schema.xsd" />
<property
android:name="android.app.appfunctions.v2"
android:value="app_functions_v2.xml" />
<intent-filter>
<action android:name="android.app.appfunctions.AppFunctionService" />
</intent-filter>
</service>
<!--
tools:ignore="Instantiatable": lint can't resolve this library-provided service's superclass chain
up to android.app.Service. ExtensionAppFunctionService extends the abstract
ExtensionsAppFunctionService, whose base is com.android.extensions.appfunctions.AppFunctionService —
a class supplied at runtime by the optional "com.android.extensions.appfunctions" <uses-library>
(declared android:required="false" in the appfunctions AAR), not by any compile/lint-classpath
artifact. Lint therefore can't prove it's a Service and false-positives. The sibling
PlatformAppFunctionService is fine because its chain ends in the compile-SDK
android.app.appfunctions.AppFunctionService, so the check stays active there and on our own services.
-->
<service
android:name="androidx.appfunctions.ExtensionAppFunctionService"
android:enabled="@bool/enableExtensionAppFunctionService"
android:exported="true"
android:permission="android.permission.BIND_APP_FUNCTION_SERVICE"
tools:ignore="Instantiatable">
<property
android:name="android.app.appfunctions"
android:value="app_functions.xml" />
<property
android:name="android.app.appfunctions.schema"
android:value="app_functions_schema.xsd" />
<property
android:name="android.app.appfunctions.v2"
android:value="app_functions_v2.xml" />
android:value="meshtastic_app_function_service.xml" />
<intent-filter>
<action android:name="android.app.appfunctions.AppFunctionService" />
</intent-filter>
@@ -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<AppFunctionStateSync>() }
}
override val appFunctionConfiguration: AppFunctionConfiguration
get() =
AppFunctionConfiguration.Builder()
.addEnclosingClassFactory(MeshtasticAppFunctions::class.java) {
getKoin().get<MeshtasticAppFunctions>()
}
.build()
}
@@ -134,7 +134,8 @@ class AppFunctionStateSync(
): List<Pair<String, Boolean>> =
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"
@@ -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 <https://www.gnu.org/licenses/>.
*/
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()
}
@@ -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()
@@ -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,
)
}
}
@@ -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<AppFunctionInvalidArgumentException> {
appFunctions.sendMessage(context, "Hello", "Al", null)
}
assertFailsWith<AppFunctionInvalidArgumentException> { 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<AppFunctionNotSupportedException> { appFunctions.sendMessage(context, "Hello", "Alice", null) }
assertFailsWith<AppFunctionNotSupportedException> { 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<AppFunctionElementNotFoundException> { appFunctions.getNodeDetails(context, "!unknown") }
assertFailsWith<AppFunctionElementNotFoundException> { 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)