1 /*
2  * Copyright 2025 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.appfunctions.integration.tests
18 
19 import android.net.Uri
20 import androidx.appfunctions.AppFunctionData
21 import androidx.appfunctions.AppFunctionFunctionNotFoundException
22 import androidx.appfunctions.AppFunctionInvalidArgumentException
23 import androidx.appfunctions.AppFunctionManagerCompat
24 import androidx.appfunctions.ExecuteAppFunctionRequest
25 import androidx.appfunctions.ExecuteAppFunctionResponse
26 import androidx.appfunctions.integration.tests.TestUtil.doBlocking
27 import androidx.appfunctions.integration.tests.TestUtil.retryAssert
28 import androidx.test.filters.LargeTest
29 import androidx.test.platform.app.InstrumentationRegistry
30 import com.google.common.truth.Truth.assertThat
31 import java.time.LocalDateTime
32 import kotlin.test.assertIs
33 import org.junit.After
34 import org.junit.Assume.assumeNotNull
35 import org.junit.Before
36 import org.junit.Ignore
37 import org.junit.Test
38 
39 @LargeTest
40 class IntegrationTest {
41     private val context = InstrumentationRegistry.getInstrumentation().context
42     private val targetContext = InstrumentationRegistry.getInstrumentation().targetContext
43     private lateinit var appFunctionManager: AppFunctionManagerCompat
44     private val uiAutomation = InstrumentationRegistry.getInstrumentation().uiAutomation
45 
46     @Before
<lambda>null47     fun setup() = doBlocking {
48         val appFunctionManagerCompatOrNull = AppFunctionManagerCompat.getInstance(targetContext)
49         assumeNotNull(appFunctionManagerCompatOrNull)
50         appFunctionManager = checkNotNull(appFunctionManagerCompatOrNull)
51 
52         uiAutomation.apply {
53             // This is needed because the test is running under the UID of
54             // "androidx.appfunctions.integration.testapp",
55             // while the app functions are defined under
56             // "androidx.appfunctions.integration.testapp.test"
57             adoptShellPermissionIdentity("android.permission.EXECUTE_APP_FUNCTIONS")
58             executeShellCommand(
59                 "device_config put appsearch max_allowed_app_function_doc_size_in_bytes $TEST_APP_FUNCTION_DOC_SIZE_LIMIT"
60             )
61         }
62         awaitAppFunctionsIndexed(FUNCTION_IDS)
63     }
64 
65     @After
tearDownnull66     fun tearDown() {
67         uiAutomation.dropShellPermissionIdentity()
68     }
69 
70     @Test
71     @Ignore("b/408436534")
<lambda>null72     fun executeAppFunction_success() = doBlocking {
73         val response =
74             appFunctionManager.executeAppFunction(
75                 request =
76                     ExecuteAppFunctionRequest(
77                         context.packageName,
78                         "TestFunctionsIds.ADD_ID",
79                         AppFunctionData.Builder("").setLong("num1", 1).setLong("num2", 2).build()
80                     )
81             )
82 
83         val successResponse = assertIs<ExecuteAppFunctionResponse.Success>(response)
84         assertThat(
85                 successResponse.returnValue.getLong(
86                     ExecuteAppFunctionResponse.Success.PROPERTY_RETURN_VALUE
87                 )
88             )
89             .isEqualTo(3)
90     }
91 
92     @Test
93     @Ignore("b/408436534")
<lambda>null94     fun executeAppFunction_voidReturnType_success() = doBlocking {
95         val response =
96             appFunctionManager.executeAppFunction(
97                 request =
98                     ExecuteAppFunctionRequest(
99                         context.packageName,
100                         "TestFunctionsIds.VOID_FUNCTION_ID",
101                         AppFunctionData.Builder("").build()
102                     )
103             )
104 
105         assertThat(response).isInstanceOf(ExecuteAppFunctionResponse.Success::class.java)
106     }
107 
108     @Test
109     @Ignore("b/408436534")
executeAppFunction_setFactory_successnull110     fun executeAppFunction_setFactory_success() = doBlocking {
111         // A factory is set to create the enclosing class of the function.
112         // See [TestApplication.appFunctionConfiguration].
113         var response =
114             appFunctionManager.executeAppFunction(
115                 request =
116                     ExecuteAppFunctionRequest(
117                         context.packageName,
118                         "TestFactoryIds.IS_CREATED_BY_FACTORY_ID",
119                         AppFunctionData.Builder("").build()
120                     )
121             )
122 
123         // If the enclosing class was created by the provided factory, the secondary constructor
124         // should be called and so the return value would be `true`.
125         val successResponse = assertIs<ExecuteAppFunctionResponse.Success>(response)
126         assertThat(
127                 successResponse.returnValue.getBoolean(
128                     ExecuteAppFunctionResponse.Success.PROPERTY_RETURN_VALUE
129                 )
130             )
131             .isEqualTo(true)
132     }
133 
134     @Test
135     @Ignore("b/408436534")
<lambda>null136     fun executeAppFunction_functionInLibraryModule_success() = doBlocking {
137         var response =
138             appFunctionManager.executeAppFunction(
139                 request =
140                     ExecuteAppFunctionRequest(
141                         context.packageName,
142                         "TestFunctions2Ids.CONCAT_ID",
143                         AppFunctionData.Builder("")
144                             .setString("str1", "log")
145                             .setString("str2", "cat")
146                             .build()
147                     )
148             )
149 
150         val successResponse = assertIs<ExecuteAppFunctionResponse.Success>(response)
151         assertThat(
152                 successResponse.returnValue.getString(
153                     ExecuteAppFunctionResponse.Success.PROPERTY_RETURN_VALUE
154                 )
155             )
156             .isEqualTo("logcat")
157     }
158 
159     @Test
160     @Ignore("b/408436534")
<lambda>null161     fun executeAppFunction_functionNotFound_fail() = doBlocking {
162         val response =
163             appFunctionManager.executeAppFunction(
164                 request =
165                     ExecuteAppFunctionRequest(
166                         context.packageName,
167                         "androidx.appfunctions.integration.testapp.TestFunctions#notExist",
168                         AppFunctionData.Builder("").build()
169                     )
170             )
171 
172         val errorResponse = assertIs<ExecuteAppFunctionResponse.Error>(response)
173         assertThat(errorResponse.error)
174             .isInstanceOf(AppFunctionFunctionNotFoundException::class.java)
175     }
176 
177     @Test
178     @Ignore("b/408436534")
<lambda>null179     fun executeAppFunction_appThrows_fail() = doBlocking {
180         val response =
181             appFunctionManager.executeAppFunction(
182                 request =
183                     ExecuteAppFunctionRequest(
184                         context.packageName,
185                         "TestFunctionsIds.DO_THROW_ID",
186                         AppFunctionData.Builder("").build()
187                     )
188             )
189 
190         assertThat(response).isInstanceOf(ExecuteAppFunctionResponse.Error::class.java)
191         val errorResponse = response as ExecuteAppFunctionResponse.Error
192         assertThat(errorResponse.error)
193             .isInstanceOf(AppFunctionInvalidArgumentException::class.java)
194     }
195 
196     @Test
197     @Ignore("b/408436534")
<lambda>null198     fun executeAppFunction_createNote() = doBlocking {
199         val response =
200             appFunctionManager.executeAppFunction(
201                 request =
202                     ExecuteAppFunctionRequest(
203                         context.packageName,
204                         "TestFunctionsIds.CREATE_NOTE_ID",
205                         AppFunctionData.Builder("")
206                             .setAppFunctionData(
207                                 "createNoteParams",
208                                 AppFunctionData.serialize(
209                                     CreateNoteParams(
210                                         title = "Test Title",
211                                         content = listOf("1", "2"),
212                                         owner = Owner("test"),
213                                         attachments =
214                                             listOf(Attachment("Uri1", Attachment("nested")))
215                                     ),
216                                     CreateNoteParams::class.java
217                                 )
218                             )
219                             .build()
220                     )
221             )
222 
223         val successResponse = assertIs<ExecuteAppFunctionResponse.Success>(response)
224         val expectedNote =
225             Note(
226                 title = "Test Title",
227                 content = listOf("1", "2"),
228                 owner = Owner("test"),
229                 attachments = listOf(Attachment("Uri1", Attachment("nested")))
230             )
231         assertThat(
232                 successResponse.returnValue
233                     .getAppFunctionData(ExecuteAppFunctionResponse.Success.PROPERTY_RETURN_VALUE)
234                     ?.deserialize(Note::class.java)
235             )
236             .isEqualTo(expectedNote)
237     }
238 
239     @Test
240     @Ignore("b/408436534")
<lambda>null241     fun executeAppFunction_createNote_withOpenableCapability_returnsNote() = doBlocking {
242         val response =
243             appFunctionManager.executeAppFunction(
244                 request =
245                     ExecuteAppFunctionRequest(
246                         context.packageName,
247                         "TestFunctionsIds.GET_OPENABLE_NOTE_ID",
248                         AppFunctionData.Builder("")
249                             .setAppFunctionData(
250                                 "createNoteParams",
251                                 AppFunctionData.serialize(
252                                     CreateNoteParams(
253                                         title = "Test Title",
254                                         content = listOf("1", "2"),
255                                         owner = Owner("test"),
256                                         attachments =
257                                             listOf(Attachment("Uri1", Attachment("nested")))
258                                     ),
259                                     CreateNoteParams::class.java
260                                 )
261                             )
262                             .build()
263                     )
264             )
265 
266         val successResponse = assertIs<ExecuteAppFunctionResponse.Success>(response)
267         val expectedNote =
268             Note(
269                 title = "Test Title",
270                 content = listOf("1", "2"),
271                 owner = Owner("test"),
272                 attachments = listOf(Attachment("Uri1", Attachment("nested")))
273             )
274         assertThat(
275                 successResponse.returnValue
276                     .getAppFunctionData(ExecuteAppFunctionResponse.Success.PROPERTY_RETURN_VALUE)
277                     ?.deserialize(Note::class.java)
278             )
279             .isEqualTo(expectedNote)
280     }
281 
282     @Test
283     @Ignore("b/408436534")
<lambda>null284     fun executeAppFunction_createNote_withOpenableCapability_returnsOpenableNote() = doBlocking {
285         val response =
286             appFunctionManager.executeAppFunction(
287                 request =
288                     ExecuteAppFunctionRequest(
289                         context.packageName,
290                         "TestFunctionsIds.GET_OPENABLE_NOTE_ID",
291                         AppFunctionData.Builder("")
292                             .setAppFunctionData(
293                                 "createNoteParams",
294                                 AppFunctionData.serialize(
295                                     CreateNoteParams(
296                                         title = "Test Title",
297                                         content = listOf("1", "2"),
298                                         owner = Owner("test"),
299                                         attachments =
300                                             listOf(Attachment("Uri1", Attachment("nested")))
301                                     ),
302                                     CreateNoteParams::class.java
303                                 )
304                             )
305                             .build()
306                     )
307             )
308 
309         val successResponse = assertIs<ExecuteAppFunctionResponse.Success>(response)
310         val expectedNote =
311             Note(
312                 title = "Test Title",
313                 content = listOf("1", "2"),
314                 owner = Owner("test"),
315                 attachments = listOf(Attachment("Uri1", Attachment("nested"))),
316             )
317         val openableNoteResult =
318             assertIs<OpenableNote>(
319                 successResponse.returnValue
320                     .getAppFunctionData(ExecuteAppFunctionResponse.Success.PROPERTY_RETURN_VALUE)
321                     ?.deserialize(OpenableNote::class.java)
322             )
323 
324         assertThat(openableNoteResult.title).isEqualTo(expectedNote.title)
325         assertThat(openableNoteResult.content).isEqualTo(expectedNote.content)
326         assertThat(openableNoteResult.owner).isEqualTo(expectedNote.owner)
327         assertThat(openableNoteResult.attachments).isEqualTo(expectedNote.attachments)
328         assertThat(openableNoteResult.intentToOpen).isNotNull()
329     }
330 
331     @Test
332     @Ignore("b/408436534")
<lambda>null333     fun executeAppFunction_serializableProxyParam_dateTime_success() = doBlocking {
334         val localDateTimeClass = DateTime(LocalDateTime.now())
335         val response =
336             appFunctionManager.executeAppFunction(
337                 request =
338                     ExecuteAppFunctionRequest(
339                         targetPackageName = context.packageName,
340                         functionIdentifier = "TestFunctionsIds.LOG_LOCAL_DATE_TIME_ID",
341                         functionParameters =
342                             AppFunctionData.Builder("")
343                                 .setAppFunctionData(
344                                     "dateTime",
345                                     AppFunctionData.serialize(
346                                         localDateTimeClass,
347                                         DateTime::class.java
348                                     )
349                                 )
350                                 .build()
351                     )
352             )
353 
354         assertIs<ExecuteAppFunctionResponse.Success>(response)
355     }
356 
357     @Test
358     @Ignore("b/408436534")
<lambda>null359     fun executeAppFunction_serializableProxyParam_androidUri_success() = doBlocking {
360         val androidUri = Uri.parse("https://www.google.com/")
361         val response =
362             appFunctionManager.executeAppFunction(
363                 request =
364                     ExecuteAppFunctionRequest(
365                         targetPackageName = context.packageName,
366                         functionIdentifier = "TestFunctions2Ids.LOG_URI_ID",
367                         functionParameters =
368                             AppFunctionData.Builder("")
369                                 .setAppFunctionData(
370                                     "androidUri",
371                                     AppFunctionData.serialize(androidUri, Uri::class.java)
372                                 )
373                                 .build()
374                     )
375             )
376 
377         assertIs<ExecuteAppFunctionResponse.Success>(response)
378     }
379 
380     @Test
381     @Ignore("b/408436534")
<lambda>null382     fun executeAppFunction_serializableProxyResponse_dateTime_success() = doBlocking {
383         val response =
384             appFunctionManager.executeAppFunction(
385                 request =
386                     ExecuteAppFunctionRequest(
387                         targetPackageName = context.packageName,
388                         functionIdentifier = "TestFunctionsIds.GET_LOCAL_DATE_ID",
389                         functionParameters = AppFunctionData.Builder("").build()
390                     )
391             )
392 
393         val successResponse = assertIs<ExecuteAppFunctionResponse.Success>(response)
394 
395         assertIs<LocalDateTime>(
396             successResponse.returnValue
397                 .getAppFunctionData(ExecuteAppFunctionResponse.Success.PROPERTY_RETURN_VALUE)
398                 ?.deserialize(DateTime::class.java)
399                 ?.localDateTime
400         )
401     }
402 
403     @Test
404     @Ignore("b/408436534")
<lambda>null405     fun executeAppFunction_serializableProxyResponse_androidUri_success() = doBlocking {
406         val response =
407             appFunctionManager.executeAppFunction(
408                 request =
409                     ExecuteAppFunctionRequest(
410                         targetPackageName = context.packageName,
411                         functionIdentifier = "TestFunctions2Ids.GET_URI_ID",
412                         functionParameters = AppFunctionData.Builder("").build()
413                     )
414             )
415 
416         val successResponse = assertIs<ExecuteAppFunctionResponse.Success>(response)
417 
418         val androidUriResult =
419             assertIs<Uri>(
420                 successResponse.returnValue
421                     .getAppFunctionData(ExecuteAppFunctionResponse.Success.PROPERTY_RETURN_VALUE)
422                     ?.deserialize(Uri::class.java)
423             )
424         assertThat(androidUriResult.toString()).isEqualTo("https://www.google.com/")
425     }
426 
427     @Test
428     @Ignore("b/408436534")
<lambda>null429     fun executeAppFunction_updateNote_success() = doBlocking {
430         val response =
431             appFunctionManager.executeAppFunction(
432                 request =
433                     ExecuteAppFunctionRequest(
434                         context.packageName,
435                         "TestFunctionsIds.UPDATE_NOTE_ID",
436                         AppFunctionData.Builder("")
437                             .setAppFunctionData(
438                                 "updateNoteParams",
439                                 AppFunctionData.serialize(
440                                     UpdateNoteParams(
441                                         title = SetField("NewTitle1"),
442                                         nullableTitle = SetField("NewTitle2"),
443                                         content = SetField(listOf("NewContent1")),
444                                         nullableContent = SetField(listOf("NewContent2"))
445                                     ),
446                                     UpdateNoteParams::class.java
447                                 )
448                             )
449                             .build()
450                     )
451             )
452 
453         val successResponse = assertIs<ExecuteAppFunctionResponse.Success>(response)
454         val expectedNote =
455             Note(
456                 title = "NewTitle1_NewTitle2",
457                 content = listOf("NewContent1", "NewContent2"),
458                 owner = Owner("test"),
459                 attachments = listOf()
460             )
461         assertThat(
462                 successResponse.returnValue
463                     .getAppFunctionData(ExecuteAppFunctionResponse.Success.PROPERTY_RETURN_VALUE)
464                     ?.deserialize(Note::class.java)
465             )
466             .isEqualTo(expectedNote)
467     }
468 
469     @Test
470     @Ignore("b/408436534")
<lambda>null471     fun executeAppFunction_updateNoteSetFieldNullContent_success() = doBlocking {
472         val response =
473             appFunctionManager.executeAppFunction(
474                 request =
475                     ExecuteAppFunctionRequest(
476                         context.packageName,
477                         "TestFunctionsIds.UPDATE_NOTE_ID",
478                         AppFunctionData.Builder("")
479                             .setAppFunctionData(
480                                 "updateNoteParams",
481                                 AppFunctionData.serialize(
482                                     UpdateNoteParams(
483                                         title = SetField("NewTitle1"),
484                                         nullableTitle = SetField(null),
485                                         content = SetField(listOf("NewContent1")),
486                                         nullableContent = SetField(null)
487                                     ),
488                                     UpdateNoteParams::class.java
489                                 )
490                             )
491                             .build()
492                     )
493             )
494 
495         val successResponse = assertIs<ExecuteAppFunctionResponse.Success>(response)
496         val expectedNote =
497             Note(
498                 title = "NewTitle1_DefaultTitle",
499                 content = listOf("NewContent1", "DefaultContent"),
500                 owner = Owner("test"),
501                 attachments = listOf()
502             )
503         assertThat(
504                 successResponse.returnValue
505                     .getAppFunctionData(ExecuteAppFunctionResponse.Success.PROPERTY_RETURN_VALUE)
506                     ?.deserialize(Note::class.java)
507             )
508             .isEqualTo(expectedNote)
509     }
510 
511     @Test
512     @Ignore("b/408436534")
<lambda>null513     fun executeAppFunction_updateNoteNullSetFields_success() = doBlocking {
514         val response =
515             appFunctionManager.executeAppFunction(
516                 request =
517                     ExecuteAppFunctionRequest(
518                         context.packageName,
519                         "TestFunctionsIds.UPDATE_NOTE_ID",
520                         AppFunctionData.Builder("")
521                             .setAppFunctionData(
522                                 "updateNoteParams",
523                                 AppFunctionData.serialize(
524                                     UpdateNoteParams(),
525                                     UpdateNoteParams::class.java
526                                 )
527                             )
528                             .build()
529                     )
530             )
531 
532         val successResponse = assertIs<ExecuteAppFunctionResponse.Success>(response)
533         val expectedNote =
534             Note(
535                 title = "DefaultTitle_DefaultTitle",
536                 content = listOf("DefaultContent", "DefaultContent"),
537                 owner = Owner("test"),
538                 attachments = listOf()
539             )
540         assertThat(
541                 successResponse.returnValue
542                     .getAppFunctionData(ExecuteAppFunctionResponse.Success.PROPERTY_RETURN_VALUE)
543                     ?.deserialize(Note::class.java)
544             )
545             .isEqualTo(expectedNote)
546     }
547 
awaitAppFunctionsIndexednull548     private suspend fun awaitAppFunctionsIndexed(expectedFunctionIds: Set<String>) {
549         retryAssert {
550             val functionIds = AppSearchMetadataHelper.collectSelfFunctionIds(context)
551             assertThat(functionIds).containsAtLeastElementsIn(expectedFunctionIds)
552         }
553     }
554 
555     private companion object {
556         const val TEST_APP_FUNCTION_DOC_SIZE_LIMIT = 512 * 1024 // 512kb
557 
558         val FUNCTION_IDS =
559             setOf<String>(
560                 //                TestFunctionsIds.ADD_ID,
561                 //                TestFunctionsIds.DO_THROW_ID,
562                 //                TestFunctionsIds.VOID_FUNCTION_ID,
563                 //                TestFunctionsIds.CREATE_NOTE_ID,
564                 //                TestFunctionsIds.UPDATE_NOTE_ID,
565                 //                TestFunctionsIds.LOG_LOCAL_DATE_TIME_ID,
566                 //                TestFunctionsIds.GET_LOCAL_DATE_ID,
567                 //                TestFunctionsIds.GET_OPENABLE_NOTE_ID,
568                 //                TestFactoryIds.IS_CREATED_BY_FACTORY_ID,
569                 //                TestFunctions2Ids.CONCAT_ID,
570                 //                TestFunctions2Ids.LOG_URI_ID,
571                 //                TestFunctions2Ids.GET_URI_ID,
572             )
573     }
574 }
575