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

+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()