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