Description
Expose Foliary's task capabilities as Android App Functions (androidx.appfunctions) so system surfaces (Google Assistant, Google Home) can invoke them. Implement an AppFunctionService in the Android module that exposes four functions backed by TaskRepository:
getTodayTasks — tasks due today and overdue tasks; maps 1:1 to TaskRepository.findTodayTasks()
findById — a task by its UUID; maps to TaskRepository.findById()
search — case-insensitive free-text search over task titles and descriptions; no backing implementation exists, so this requires a new TaskDao query and TaskRepository method (with tests)
createTask — persists a new task via TaskRepository.save(); the full Task must be constructed with defaults, mirroring the existing logic in CreateTaskViewModel.onSaveClick (UUID via UuidProvider, Priority.MEDIUM, creation date via TimeProvider)
Technical and design context:
- App Functions requires adding the first-party
androidx.appfunctions:appfunctions runtime + compiler dependency (used version previously: 1.0.0-alpha10) to gradle/libs.versions.toml — a new dependency, needs approval.
- The service and its generated subclass must be registered in the Android manifest with the
android.permission.BIND_APP_FUNCTION_SERVICE permission, the android.app.appfunctions.AppFunctionService intent filter, the android.app.appfunctions.schema (XSD) and android.app.appfunctions.v2 inventory XML properties, plus uses-library com.android.extensions.appfunctions with required="false".
- Koin currently starts inside Compose composition (
App.kt), so the system-instantiated service cannot rely on that graph; it needs its own Koin module/wiring in the android module.
- Expose tasks through stable
@AppFunctionSerializable function models (id, title, description, dates, priority, url, location) mapped from the Room Task entity — do not expose Room entities directly.
- minSdk is 31 while App Functions services are
@RequiresApi(36); behavior on older devices must be gated at runtime and must not affect normal app behavior.
- Edge cases: malformed/unknown UUID input to
findById must yield an error result, not a crash; createTask with a blank title must fail with an error and persist nothing; empty search results must return an empty list; nulls (description, due date, location) must map cleanly to the serializable models.
- Reference material: a previous uncommitted implementation (package
dev.appoutlet.foliary.android.appfunction with TaskFunctionService, TaskFunctionModel, LocationFunctionModel, TaskFunctionModelMapper, AppFunctionModule) was deleted from source but its full schema and inventory survive in android/build/ KSP and dex backups — use them as reference.
Scope
In scope:
- App Functions runtime + compiler dependency in
gradle/libs.versions.toml
- Serializable function models and entity-to-model mapper
AppFunctionService exposing the four functions with correct schema/inventory metadata
- Manifest service registration and schema resources
- Backing
search capability: TaskDao query + TaskRepository method
- Unit tests: mapper/service logic, DAO and repository search, create-task validation
- Koin wiring for the system-instantiated service
Out of scope:
- Device-level verification with an actual system surface (Assistant/Home) — manual QA item
- Any UI changes or user-facing strings
- Functions beyond the four listed
Acceptance criteria
Description
Expose Foliary's task capabilities as Android App Functions (androidx.appfunctions) so system surfaces (Google Assistant, Google Home) can invoke them. Implement an
AppFunctionServicein the Android module that exposes four functions backed byTaskRepository:getTodayTasks— tasks due today and overdue tasks; maps 1:1 toTaskRepository.findTodayTasks()findById— a task by its UUID; maps toTaskRepository.findById()search— case-insensitive free-text search over task titles and descriptions; no backing implementation exists, so this requires a newTaskDaoquery andTaskRepositorymethod (with tests)createTask— persists a new task viaTaskRepository.save(); the fullTaskmust be constructed with defaults, mirroring the existing logic inCreateTaskViewModel.onSaveClick(UUID viaUuidProvider,Priority.MEDIUM, creation date viaTimeProvider)Technical and design context:
androidx.appfunctions:appfunctionsruntime + compiler dependency (used version previously:1.0.0-alpha10) togradle/libs.versions.toml— a new dependency, needs approval.android.permission.BIND_APP_FUNCTION_SERVICEpermission, theandroid.app.appfunctions.AppFunctionServiceintent filter, theandroid.app.appfunctions.schema(XSD) andandroid.app.appfunctions.v2inventory XML properties, plususes-library com.android.extensions.appfunctionswithrequired="false".App.kt), so the system-instantiated service cannot rely on that graph; it needs its own Koin module/wiring in the android module.@AppFunctionSerializablefunction models (id, title, description, dates, priority, url, location) mapped from the RoomTaskentity — do not expose Room entities directly.@RequiresApi(36); behavior on older devices must be gated at runtime and must not affect normal app behavior.findByIdmust yield an error result, not a crash;createTaskwith a blank title must fail with an error and persist nothing; empty search results must return an empty list; nulls (description, due date, location) must map cleanly to the serializable models.dev.appoutlet.foliary.android.appfunctionwithTaskFunctionService,TaskFunctionModel,LocationFunctionModel,TaskFunctionModelMapper,AppFunctionModule) was deleted from source but its full schema and inventory survive inandroid/build/KSP and dex backups — use them as reference.Scope
In scope:
gradle/libs.versions.tomlAppFunctionServiceexposing the four functions with correct schema/inventory metadatasearchcapability:TaskDaoquery +TaskRepositorymethodOut of scope:
Acceptance criteria
WHEN the
getTodayTasksapp function is invokedTHEN it returns all tasks due today and overdue tasks, mapped to the public function models
WHEN the
findByIdapp function is invoked with that UUIDTHEN it returns the task
WHEN the
findByIdapp function is invokedTHEN it returns an error result and the app does not crash
WHEN the
searchapp function is invoked with that stringTHEN it returns the matching tasks regardless of letter case, and an empty list when there are no matches
createTaskinvocation with a valid title and optional fieldsWHEN the function executes
THEN a new task is persisted through
TaskRepository.save()and its UUID is returnedcreateTaskinvocation with a blank titleWHEN the function executes
THEN it returns an error result and no task is persisted
WHEN the build succeeds
THEN the merged manifest contains the service with
BIND_APP_FUNCTION_SERVICE, the schema/inventory properties, and theuses-librarydeclarationWHEN the app is used normally
THEN behavior is unchanged and no crash or service registration occurs