In Agentic interaction using AppFunctions I showed how Be nice publishes createAppPair for agents, using Jetpack appfunctions 1.0.0-alpha08. That setup leaned on a library-merged PlatformAppFunctionService, an AppFunctionContext parameter, and aggregate XML named app_functions.xml / app_functions_v2.xml. When I bumped toward what Android Studio was suggesting as of mid July, Sync failed:
Could not find androidx.appfunctions:appfunctions-service:1.0.0-alpha10.
On 1 July 2026, alpha10 stopped publishing appfunctions-service (Maven still lists it through alpha09). The Add the AppFunctions API guide now expects you to host an @AppFunctionServiceEntryPoint AppFunctionService. Spoiler: that is the real change. The rest of this post is how that landed in Be nice, plus an important update to the earlier article that is easy to miss if you only chase compile errors.
Correction first
In the earlier post I wrote that agents would pass package names for app1 and app2. That was wrong for Be nice. The implementation matches display names, case-insensitively, against installed apps. Package names are a convenient engineer habit; they are a poor agent habit when the user said “Clock” and “Contacts”. KDoc and app-level metadata have to say what the code accepts — agents lean on that text harder than on your mental model of the APK.
What changed in the model
Before alpha10 you annotated a helper class and let the service AAR merge a stock entry point into the manifest. Starting with alpha10:
-
@AppFunctionlives inandroidx.appfunctions(notandroidx.appfunctions.service) - It is only legal on methods of an abstract
AppFunctionServicemarked@AppFunctionServiceEntryPoint - KSP generates the concrete subclass named in
serviceNameand the XML named inappFunctionXmlFileName - You declare that generated service yourself; nothing merges a stock one anymore
- There is no
AppFunctionContextparameter; the service is aContext - Drop
appfunctions:aggregateAppFunctions. Grepping for emptyapp_functions.xml/app_functions_v2.xmlis no longer the success signal
Alpha09 still shipped the service artifact. Jumping from the first article to today skips that soft landing and hits alpha10.
Dependencies
[versions]
appfunctions = "1.0.0-alpha10"
[libraries]
androidx-appfunctions = { group = "androidx.appfunctions", name = "appfunctions", version.ref = "appfunctions" }
androidx-appfunctions-compiler = { group = "androidx.appfunctions", name = "appfunctions-compiler", version.ref = "appfunctions" }
dependencies {
implementation(libs.androidx.appfunctions)
ksp(libs.androidx.appfunctions.compiler)
}
compileSdk stays at 36 or higher (Be nice is on 37). I still keep each merge*Assets task depending on the matching ksp*Kotlin task. Alpha10 still emits XML under KSP-generated assets — benice_app_functions.xml for me — and on my AGP/KSP combo packaging was flaky without that edge. Same class of problem as in the first article; new file name.
The host in Be nice
@RequiresApi(Build.VERSION_CODES.BAKLAVA)
@AppFunctionServiceEntryPoint(
serviceName = "BeNiceAppFunctionService",
appFunctionXmlFileName = "benice_app_functions",
)
abstract class BeNiceFunctions : AppFunctionService() {
/**
* Launches two installed apps together in split screen (an app pair).
*
* @param app1 Name of the first app in the pair.
* @param app2 Name of the second app in the pair.
* @return A localized message describing success or failure.
*/
@AppFunction(isDescribedByKDoc = true)
suspend fun createAppPair(
app1: String,
app2: String
): String = withContext(Dispatchers.IO) {
val success = performPairing(applicationContext, app1, app2)
if (success) {
getString(R.string.pair_created_success, app1, app2)
} else {
getString(R.string.pair_created_failure)
}
}
}
AppFunctionService is API 36 (Baklava), so the abstract host carries @RequiresApi. Where the old code used context.context, I use applicationContext / getString on the service.
I still use suspend. The docs also say AppFunctions run on the UI thread unless you move work elsewhere. Resolving installed apps and publishing a dynamic shortcut is not UI-thread work; withContext(Dispatchers.IO) is not optional once the body does real I/O.
isDescribedByKDoc = true still folds KDoc into agent metadata. Treat that block as public API surface for a model, not as a comment for the next human on your team.
Official snippets often put @AndroidEntryPoint and @Inject on the abstract service. I did not. KSP already generates a subclass (BeNiceAppFunctionService). Hilt would want to subclass the same type for injection. Stacking two code generators on one service is a fight I do not need for two collaborators. A small @EntryPoint plus EntryPointAccessors.fromApplication for InstalledAppsManager and ShortcutsRepository keeps the host boring.
Manifest and agent-facing metadata
Point at the generated class and at the XML name you chose:
<service
android:name=".appfunctions.BeNiceAppFunctionService"
android:exported="true"
android:permission="android.permission.BIND_APP_FUNCTION_SERVICE"
tools:targetApi="36">
<property
android:name="android.app.appfunctions.schema"
android:value="app_functions_schema.xsd" />
<property
android:name="android.app.appfunctions.v2"
android:value="benice_app_functions.xml" />
<intent-filter>
<action android:name="android.app.appfunctions.AppFunctionService" />
</intent-filter>
</service>
The v2 property value is appFunctionXmlFileName plus .xml. Wrong name, silent packaging miss; especially if you still grep for app_functions_v2.xml.
Under <application> the guide also wants app-level metadata. That is where I put the display-name contract in writing:
<property
android:name="android.app.appfunctions.app_metadata"
android:resource="@xml/app_metadata" />
<AppFunctionAppMetadata xmlns:appfn="http://schemas.android.com/apk/androidx.appfunctions"
appfn:description="This app creates app pairs that launch two installed apps together in split screen.
Operational Patterns:
- Use 'createAppPair' with the display names of two installed apps.
Constraints:
- Both app names must match installed apps (case-insensitive)."
appfn:displayDescription="@string/appfunctions_display_description" />
description is for agents; displayDescription is for people. The resolver can return null if the property is absent. I would not ship without it: this is how you stop an agent from inventing package-name parameters your code never accepted.
Packaging
After a debug build, the generated assets directory should show benice_app_functions.xml, not the old aggregate pair. If empty app_functions.xml / app_functions_v2.xml are still what you find, the aggregate KSP flag is probably still on.
unzip -l app/build/outputs/apk/debug/app-debug.apk | grep -E 'benice_app_functions|app_functions'
You want benice_app_functions.xml in the APK. Once that is true, install and check the function on device.
Checking the app functions
After install, the same adb path from the first article still works. On a debug build of Be nice the package id now carries the .debug suffix; release stays de.thomaskuenneth.benice. Grep with the shared prefix either way:
adb shell cmd app_function list-app-functions | grep -F de.thomaskuenneth.benice
You want the stable id de.thomaskuenneth.benice.appfunctions.BeNiceFunctions#createAppPair. Do not take the first something#something match blindly — the current list dump also contains other # fragments, and a wrong id comes back as App function not found.
Execute with AppSearch-style --parameters (each string as a one-element JSON array). Use display names, not package names:
adb shell "cmd app_function execute-app-function \
--package de.thomaskuenneth.benice.debug \
--function 'de.thomaskuenneth.benice.appfunctions.BeNiceFunctions#createAppPair' \
--parameters '{\"app1\":[\"Settings\"],\"app2\":[\"Clock\"]}'"
That returned the localized success string on my API 37 emulator. The same call with com.foo / com.bar still runs the function, but resolves to the failure string — which is what you want if the names are wrong. Swap --package to de.thomaskuenneth.benice for a release install. Some API 36 images still lack the shell until you pick a revision that includes it (I needed 36.1 last time).
Order of work
- Bump to alpha10; remove
appfunctions-serviceandaggregateAppFunctions - Move
@AppFunctiononto an@AppFunctionServiceEntryPointAppFunctionService; dropAppFunctionContext - Declare the generated service and
app_metadatain the manifest - Confirm the new XML is in the APK; re-list with
adbon a capable image - Keep
suspend; put blocking work on a background dispatcher - Re-read your KDoc and app metadata as if you were the agent
If you are already on alpha09, Android Studio documents a migration skill for the hop. From alpha08 you touch the same surfaces, and you also leave behind anything that assumed the service AAR and aggregate assets.
Closing
The first article is still useful for motivation, R8 notes, and the habit of unzipping the APK. Its dependency list, AppFunctionContext signature, aggregate greps, and the package-name claim for Be nice are not. Day to day I treat the Add the AppFunctions API guide and the alpha10 release notes as source of truth — and I re-read them on every bump. The gap between alpha08 and alpha10 is the kind of gap alphas are for: the library stopped hosting your functions, so your app has to.
References
- Agentic interaction using AppFunctions
- Add the AppFunctions API
-
appfunctions release notes (
1.0.0-alpha10) - Overview of AppFunctions
- Official snippets:
AppFunctionApiSnippets.kt
Top comments (0)