Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion ChatApp/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ dependencies {

// App functions
implementation(libs.androidx.appfunctions)
implementation(libs.androidx.appfunctions.service)
ksp(libs.androidx.appfunctions.compiler)

testImplementation(libs.junit)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class AppFunctionsViewModelTest {
fun toggleFunction_doesNotCrash() =
runTest {
// Use a potentially real ID from the app
Comment thread
keyboardsurfer marked this conversation as resolved.
viewModel.toggleFunction("com.example.chatapp.appfunctions.AppFunctions#searchContacts", true)
viewModel.toggleFunction(com.example.chatapp.appfunctions.AppFunctionsIds.SEARCH_CONTACTS_ID, true)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,6 @@ class AppFunctionInstrumentationTest {

@Inject lateinit var recipientsRepository: RecipientsRepository

@Inject lateinit var appFunctions: AppFunctions

private val context: Context = ApplicationProvider.getApplicationContext()
private val appFunctionManager: AppFunctionManager =
checkNotNull(AppFunctionManager.getInstance(context))
Expand Down Expand Up @@ -164,10 +162,10 @@ class AppFunctionInstrumentationTest {
.getAppFunctionDataList(
ExecuteAppFunctionResponse.Success.PROPERTY_RETURN_VALUE,
)
?.map { it.deserialize(AppFunctions.ContactSearchResult::class.java) },
?.map { it.deserialize(ContactSearchResult::class.java) },
)
.containsExactly(
AppFunctions.ContactSearchResult(endpointValue = "1", endpointType = "INDIVIDUAL", displayName = "Alice Smith"),
ContactSearchResult(endpointValue = "1", endpointType = "INDIVIDUAL", displayName = "Alice Smith"),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package com.example.chatapp.appfunctions
import android.content.Context
import android.net.Uri
import androidx.appfunctions.AppFunctionAppUnknownException
import androidx.appfunctions.AppFunctionContext
import androidx.appfunctions.AppFunctionElementNotFoundException
import androidx.appfunctions.AppFunctionInvalidArgumentException
import androidx.test.core.app.ApplicationProvider
Expand All @@ -38,12 +37,6 @@ import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class AppFunctionsTest {
private val testContext =
object : AppFunctionContext {
override val context: Context
get() = ApplicationProvider.getApplicationContext()
}

private class MockMessageRepository : MessageRepository {
var shouldFail = false
private val messages = MutableStateFlow<Map<String, List<DisplayMessage>>>(emptyMap())
Expand Down Expand Up @@ -85,19 +78,47 @@ class AppFunctionsTest {

private val callManager = CallManager(recipientsRepository)

private val appFunctions = AppFunctions(messageRepository, recipientsRepository, callManager)
private class TestChatAppFunctionService(
val testContext: Context,
messageRepo: MessageRepository,
recipientsRepo: RecipientsRepository,
callMgr: CallManager,
) : BaseChatAppFunctionService() {
init {
Comment thread
keyboardsurfer marked this conversation as resolved.
Outdated
attachBaseContext(testContext)
this.messageRepository = messageRepo
this.recipientsRepository = recipientsRepo
this.callManager = callMgr
}

override fun onExecuteFunction(
request: androidx.appfunctions.ExecuteAppFunctionRequest,
cancellationSignal: android.os.CancellationSignal,
callback: java.util.function.Consumer<androidx.appfunctions.ExecuteAppFunctionResponse>,
) {
// Not used in direct unit tests
}
}

private val appFunctions =
TestChatAppFunctionService(
ApplicationProvider.getApplicationContext(),
messageRepository,
recipientsRepository,
callManager,
)

@Test(expected = AppFunctionInvalidArgumentException::class)
fun searchContacts_returnsEmptyList() {
runBlocking {
appFunctions.searchContacts(testContext, "nonexistent", "INDIVIDUAL")
appFunctions.searchContacts("nonexistent", "INDIVIDUAL")
}
}

@Test
fun searchContacts_returnsMatches() {
runBlocking {
val contacts = appFunctions.searchContacts(testContext, "Alice", "INDIVIDUAL")
val contacts = appFunctions.searchContacts("Alice", "INDIVIDUAL")
Assert.assertEquals(1, contacts.size)
Assert.assertEquals("Alice Smith", contacts[0].displayName)
}
Expand All @@ -106,7 +127,7 @@ class AppFunctionsTest {
@Test
fun searchContacts_groups_returnsMatches() {
runBlocking {
val contacts = appFunctions.searchContacts(testContext, "Work", "GROUP")
val contacts = appFunctions.searchContacts("Work", "GROUP")
Assert.assertEquals(1, contacts.size)
Assert.assertEquals("Work Friends", contacts[0].displayName)
}
Expand All @@ -115,16 +136,15 @@ class AppFunctionsTest {
@Test
fun searchContacts_emptyQuery_returnsRecent() {
runBlocking {
val contacts = appFunctions.searchContacts(testContext, "", "INDIVIDUAL")
val contacts = appFunctions.searchContacts("", "INDIVIDUAL")
Assert.assertEquals(3, contacts.size)
}
}

@Test
fun searchContacts_anyType_returnsMatches() {
runBlocking {
// "searchAny" branch (when filterType is not INDIVIDUAL or GROUP)
val contacts = appFunctions.searchContacts(testContext, "Alice", "ANY")
val contacts = appFunctions.searchContacts("Alice", "ANY")
Assert.assertEquals(1, contacts.size)
Assert.assertEquals("Alice Smith", contacts[0].displayName)
}
Expand All @@ -133,9 +153,9 @@ class AppFunctionsTest {
@Test
fun send_validMessage_returnsSuccess() {
runTest {
val result = appFunctions.send(testContext, "1", "Hello")
val result = appFunctions.send("1", "Hello")
Assert.assertEquals(
AppFunctions.Result(
Result(
"Message ID",
"Message sent to: Alice Smith.",
),
Expand All @@ -149,13 +169,12 @@ class AppFunctionsTest {
runTest {
val result =
appFunctions.send(
testContext,
"1",
"Hello",
listOf(Uri.parse("content://media/1")),
)
Assert.assertEquals(
AppFunctions.Result(
Result(
"Message ID",
"Message sent to: Alice Smith.",
),
Expand All @@ -167,9 +186,9 @@ class AppFunctionsTest {
@Test
fun send_toGroup_success() {
runTest {
val result = appFunctions.send(testContext, "g1", "Hello")
val result = appFunctions.send("g1", "Hello")
Assert.assertEquals(
AppFunctions.Result(
Result(
"Message ID",
"Message sent to: Work Friends.",
),
Expand All @@ -181,39 +200,37 @@ class AppFunctionsTest {
@Test(expected = AppFunctionInvalidArgumentException::class)
fun send_emptyContent_fails() {
runTest {
appFunctions.send(testContext, "1", "")
appFunctions.send("1", "")
}
}

@Test(expected = AppFunctionElementNotFoundException::class)
fun send_invalidRecipient_fails() {
runTest {
appFunctions.send(testContext, "nonexistent_id", "Hello")
appFunctions.send("nonexistent_id", "Hello")
}
}

@Test(expected = AppFunctionAppUnknownException::class)
fun send_repositoryError_returnsError() {
runTest {
messageRepository.shouldFail = true
appFunctions.send(testContext, "1", "Hello")
appFunctions.send("1", "Hello")
}
}

@Test
fun makeCall_returnsPendingIntent() {
runBlocking {
val pendingIntent = appFunctions.makeCall(testContext, endpointValue = "1")
val pendingIntent = appFunctions.makeCall(endpointValue = "1")
Assert.assertNotNull(pendingIntent)
}
}


@Test(expected = AppFunctionElementNotFoundException::class)
fun makeCall_invalidEndpointValue_fails() {
runBlocking {
appFunctions.makeCall(testContext, endpointValue = "nonexistent_id")
appFunctions.makeCall(endpointValue = "nonexistent_id")
}
}

}
18 changes: 17 additions & 1 deletion ChatApp/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
-->

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:appfn="http://schemas.android.com/apk/androidx.appfunctions">
xmlns:appfn="http://schemas.android.com/apk/androidx.appfunctions"
xmlns:tools="http://schemas.android.com/tools">

<application
android:name=".ChatApplication"
Expand Down Expand Up @@ -49,6 +50,21 @@
android:host="com.example.chatapp" />
</intent-filter>
</activity>
<service
android:name="com.example.chatapp.appfunctions.ChatAppFunctionService"
android:permission="android.permission.BIND_APP_FUNCTION_SERVICE"
android:exported="true"
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="chat_app_function_service.xml" />
<intent-filter>
<action android:name="android.app.appfunctions.AppFunctionService" />
</intent-filter>
</service>
<property
android:name="android.app.appfunctions.app_metadata"
android:resource="@xml/app_metadata" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/
package com.example.chatapp.data

import com.example.chatapp.appfunctions.AppFunctions.Recipient
import com.example.chatapp.appfunctions.Recipient
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
*/
package com.example.chatapp.data

import com.example.chatapp.appfunctions.AppFunctions.ChatGroup
import com.example.chatapp.appfunctions.AppFunctions.ContactSearchResult
import com.example.chatapp.appfunctions.AppFunctions.Recipient
import com.example.chatapp.appfunctions.ChatGroup
import com.example.chatapp.appfunctions.ContactSearchResult
import com.example.chatapp.appfunctions.Recipient
import javax.inject.Inject
import javax.inject.Singleton

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.example.chatapp.RecipientsViewModel
import com.example.chatapp.appfunctions.AppFunctions.ChatGroup
import com.example.chatapp.appfunctions.AppFunctions.Recipient
import com.example.chatapp.appfunctions.ChatGroup
import com.example.chatapp.appfunctions.Recipient

/**
* A screen that displays a list of recipients/contacts.
Expand Down
14 changes: 7 additions & 7 deletions ChatApp/app/src/main/res/xml/app_metadata.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@
~ limitations under the License.
-->

<AppFunctionAppMetadata xmlns:appfn="http://schemas.android.com/apk/res-auto"
appfn:description="Provides tools for managing chat conversations, searching contacts, and initiating calls.
<AppFunctionAppMetadata xmlns:appfn="http://schemas.android.com/apk/androidx.appfunctions"
appfn:description="Provides functions for managing chat conversations, searching contacts, sending messages, and initiating voice calls.
Operational Patterns:
- Resolution of contacts via 'searchContacts' provides the required 'endpointValue' for sending messages or making calls.
- Passing a blank query to 'searchContacts' retrieves the most recent contacts.
- Always call 'searchContacts' first to resolve contact or group names to an 'endpointValue' before calling 'send' or 'makeCall'.
- Passing a blank query string to 'searchContacts' retrieves the most recently contacted entities.
Constraints:
- Message content cannot be empty or blank.
- Calls are initiated by unique identifier.
- Media attachments are supported via a list of URIs."
- Message content cannot be empty or blank when calling 'send'.
- Calls and messages must use valid endpointValue identifiers returned by 'searchContacts'.
- Image attachments are supported in 'send' via a list of content URIs."
appfn:displayDescription="@string/app_function_app_display_description"/>
3 changes: 1 addition & 2 deletions ChatApp/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[versions]
agp = "9.2.1"
appfunctions = "1.0.0-alpha09"
appfunctions = "1.0.0-alpha10"
kotlin = "2.3.21"
coreKtx = "1.18.0"
junit = "4.13.2"
Expand All @@ -27,7 +27,6 @@ ktlint = "1.2.1"
[libraries]
androidx-appfunctions = { module = "androidx.appfunctions:appfunctions", version.ref = "appfunctions" }
androidx-appfunctions-compiler = { module = "androidx.appfunctions:appfunctions-compiler", version.ref = "appfunctions" }
androidx-appfunctions-service = { module = "androidx.appfunctions:appfunctions-service", version.ref = "appfunctions" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
Expand Down
1 change: 0 additions & 1 deletion ChatApp/shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ dependencies {

// App functions
implementation(libs.androidx.appfunctions)
implementation(libs.androidx.appfunctions.service)
ksp(libs.androidx.appfunctions.compiler)

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,4 @@
package com.example.chatapp

import android.app.Application
import androidx.appfunctions.service.AppFunctionConfiguration
import com.example.chatapp.appfunctions.AppFunctions
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent

abstract class BaseChatApplication : Application(), AppFunctionConfiguration.Provider {
override val appFunctionConfiguration: AppFunctionConfiguration
get() {
val entryPoint = EntryPointAccessors.fromApplication(this, AppFunctionsEntryPoint::class.java)
val taskFunctions = entryPoint.getAppFunctions()

return AppFunctionConfiguration.Builder()
.addEnclosingClassFactory(AppFunctions::class.java) { taskFunctions }
.build()
}
}

@EntryPoint
@InstallIn(SingletonComponent::class)
interface AppFunctionsEntryPoint {
fun getAppFunctions(): AppFunctions
}
abstract class BaseChatApplication : Application()
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ package com.example.chatapp
import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.chatapp.appfunctions.AppFunctions.Recipient
import com.example.chatapp.appfunctions.Recipient
import com.example.chatapp.data.CallManager
import com.example.chatapp.data.DisplayMessage
import com.example.chatapp.data.MessageRepository
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
package com.example.chatapp

import androidx.lifecycle.ViewModel
import com.example.chatapp.appfunctions.AppFunctions.ChatGroup
import com.example.chatapp.appfunctions.AppFunctions.Recipient
import com.example.chatapp.appfunctions.ChatGroup
import com.example.chatapp.appfunctions.Recipient
import com.example.chatapp.data.DisplayMessage
import com.example.chatapp.data.MessageRepository
import com.example.chatapp.data.RecipientsRepository
Expand Down
Loading