1 /* <lambda>null2 * 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 18 19 import android.Manifest 20 import android.app.PendingIntent 21 import android.app.UiAutomation 22 import android.content.BroadcastReceiver 23 import android.content.Context 24 import android.content.Intent 25 import android.content.IntentFilter 26 import android.content.pm.PackageInstaller 27 import android.os.Build 28 import androidx.appfunctions.core.AppFunctionMetadataTestHelper 29 import androidx.test.filters.SdkSuppress 30 import androidx.test.platform.app.InstrumentationRegistry 31 import com.google.common.truth.Truth.assertThat 32 import java.io.InputStream 33 import kotlin.coroutines.resumeWithException 34 import kotlinx.coroutines.CoroutineScope 35 import kotlinx.coroutines.Dispatchers 36 import kotlinx.coroutines.ExperimentalCoroutinesApi 37 import kotlinx.coroutines.delay 38 import kotlinx.coroutines.flow.SharingStarted 39 import kotlinx.coroutines.flow.first 40 import kotlinx.coroutines.flow.shareIn 41 import kotlinx.coroutines.flow.take 42 import kotlinx.coroutines.runBlocking 43 import kotlinx.coroutines.suspendCancellableCoroutine 44 import org.junit.After 45 import org.junit.Assume.assumeNotNull 46 import org.junit.Assume.assumeTrue 47 import org.junit.Before 48 import org.junit.Test 49 50 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) 51 class AppFunctionManagerCompatTest { 52 53 private val context: Context = InstrumentationRegistry.getInstrumentation().targetContext 54 55 private val metadataTestHelper: AppFunctionMetadataTestHelper = 56 AppFunctionMetadataTestHelper(context) 57 58 private lateinit var appFunctionManagerCompat: AppFunctionManagerCompat 59 60 private val uiAutomation: UiAutomation = 61 InstrumentationRegistry.getInstrumentation().uiAutomation 62 63 private val testFunctionIds = 64 setOf( 65 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_ENABLED_BY_DEFAULT, 66 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_DISABLED_BY_DEFAULT, 67 ) 68 69 @Before 70 fun setup() { 71 val appFunctionManagerCompatOrNull = AppFunctionManagerCompat.getInstance(context) 72 assumeNotNull(appFunctionManagerCompatOrNull) 73 appFunctionManagerCompat = checkNotNull(appFunctionManagerCompatOrNull) 74 75 uiAutomation.adoptShellPermissionIdentity( 76 Manifest.permission.INSTALL_PACKAGES, 77 "android.permission.EXECUTE_APP_FUNCTIONS", 78 ) 79 80 runBlocking { 81 metadataTestHelper.awaitAppFunctionIndexed(testFunctionIds) 82 83 // Reset all test ids 84 for (functionIds in testFunctionIds) { 85 appFunctionManagerCompat.setAppFunctionEnabled( 86 functionIds, 87 AppFunctionManagerCompat.Companion.APP_FUNCTION_STATE_DEFAULT 88 ) 89 } 90 } 91 } 92 93 @After 94 fun tearDown() { 95 uiAutomation.dropShellPermissionIdentity() 96 uiAutomation.executeShellCommand("pm uninstall $ADDITIONAL_APP_PACKAGE") 97 } 98 99 @Test 100 fun testSelfIsAppFunctionEnabled_defaultEnabledState() { 101 val isEnabled = runBlocking { 102 appFunctionManagerCompat.isAppFunctionEnabled( 103 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_ENABLED_BY_DEFAULT 104 ) 105 } 106 107 assertThat(isEnabled).isTrue() 108 } 109 110 @Test 111 fun testSelfIsAppFunctionEnabled_defaultDisabledState() { 112 val isEnabled = runBlocking { 113 appFunctionManagerCompat.isAppFunctionEnabled( 114 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_DISABLED_BY_DEFAULT 115 ) 116 } 117 118 assertThat(isEnabled).isFalse() 119 } 120 121 @Test 122 fun testIsAppFunctionEnabled_defaultEnabledState() { 123 val isEnabled = runBlocking { 124 appFunctionManagerCompat.isAppFunctionEnabled( 125 context.packageName, 126 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_ENABLED_BY_DEFAULT 127 ) 128 } 129 130 assertThat(isEnabled).isTrue() 131 } 132 133 @Test 134 fun testIsAppFunctionEnabled_defaultDisabledState() { 135 val isEnabled = runBlocking { 136 appFunctionManagerCompat.isAppFunctionEnabled( 137 context.packageName, 138 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_DISABLED_BY_DEFAULT 139 ) 140 } 141 142 assertThat(isEnabled).isFalse() 143 } 144 145 @Test 146 fun testSetAppFunctionEnabled_overrideToDisable() { 147 val isEnabled = runBlocking { 148 appFunctionManagerCompat.setAppFunctionEnabled( 149 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_ENABLED_BY_DEFAULT, 150 AppFunctionManagerCompat.APP_FUNCTION_STATE_DISABLED 151 ) 152 appFunctionManagerCompat.isAppFunctionEnabled( 153 context.packageName, 154 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_ENABLED_BY_DEFAULT 155 ) 156 } 157 158 assertThat(isEnabled).isFalse() 159 } 160 161 @Test 162 fun testSetAppFunctionEnabled_overrideToEnabled() { 163 val isEnabled = runBlocking { 164 appFunctionManagerCompat.setAppFunctionEnabled( 165 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_DISABLED_BY_DEFAULT, 166 AppFunctionManagerCompat.APP_FUNCTION_STATE_ENABLED 167 ) 168 appFunctionManagerCompat.isAppFunctionEnabled( 169 context.packageName, 170 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_DISABLED_BY_DEFAULT 171 ) 172 } 173 174 assertThat(isEnabled).isTrue() 175 } 176 177 @Test 178 fun testSetAppFunctionEnabled_resetToEnabled() { 179 val isEnabled = runBlocking { 180 appFunctionManagerCompat.setAppFunctionEnabled( 181 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_ENABLED_BY_DEFAULT, 182 AppFunctionManagerCompat.APP_FUNCTION_STATE_DISABLED 183 ) 184 appFunctionManagerCompat.setAppFunctionEnabled( 185 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_ENABLED_BY_DEFAULT, 186 AppFunctionManagerCompat.APP_FUNCTION_STATE_DEFAULT 187 ) 188 appFunctionManagerCompat.isAppFunctionEnabled( 189 context.packageName, 190 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_ENABLED_BY_DEFAULT 191 ) 192 } 193 194 assertThat(isEnabled).isTrue() 195 } 196 197 @Test 198 fun testSetAppFunctionEnabled_resetToDisabled() { 199 val isEnabled = runBlocking { 200 appFunctionManagerCompat.setAppFunctionEnabled( 201 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_DISABLED_BY_DEFAULT, 202 AppFunctionManagerCompat.APP_FUNCTION_STATE_ENABLED 203 ) 204 appFunctionManagerCompat.setAppFunctionEnabled( 205 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_DISABLED_BY_DEFAULT, 206 AppFunctionManagerCompat.APP_FUNCTION_STATE_DEFAULT 207 ) 208 appFunctionManagerCompat.isAppFunctionEnabled( 209 context.packageName, 210 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_DISABLED_BY_DEFAULT 211 ) 212 } 213 214 assertThat(isEnabled).isFalse() 215 } 216 217 @Test 218 fun testExecuteAppFunction_functionNotExist() { 219 val request = 220 ExecuteAppFunctionRequest( 221 targetPackageName = context.packageName, 222 functionIdentifier = "fakeFunctionId", 223 functionParameters = AppFunctionData.EMPTY, 224 ) 225 226 val response = runBlocking { appFunctionManagerCompat.executeAppFunction(request) } 227 228 assertThat(response).isInstanceOf(ExecuteAppFunctionResponse.Error::class.java) 229 assertThat((response as ExecuteAppFunctionResponse.Error).error) 230 .isInstanceOf(AppFunctionFunctionNotFoundException::class.java) 231 } 232 233 @Test 234 fun testExecuteAppFunction_functionSucceed() { 235 val request = 236 ExecuteAppFunctionRequest( 237 targetPackageName = context.packageName, 238 functionIdentifier = 239 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_EXECUTION_SUCCEED, 240 functionParameters = AppFunctionData.EMPTY, 241 ) 242 243 val response = runBlocking { appFunctionManagerCompat.executeAppFunction(request) } 244 245 assertThat(response).isInstanceOf(ExecuteAppFunctionResponse.Success::class.java) 246 assertThat( 247 (response as ExecuteAppFunctionResponse.Success) 248 .returnValue 249 .getString(ExecuteAppFunctionResponse.Success.PROPERTY_RETURN_VALUE) 250 ) 251 .isEqualTo("result") 252 } 253 254 @Test 255 fun testExecuteAppFunction_functionFail() { 256 val request = 257 ExecuteAppFunctionRequest( 258 targetPackageName = context.packageName, 259 functionIdentifier = 260 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_EXECUTION_FAIL, 261 functionParameters = AppFunctionData.EMPTY, 262 ) 263 264 val response = runBlocking { appFunctionManagerCompat.executeAppFunction(request) } 265 266 assertThat(response).isInstanceOf(ExecuteAppFunctionResponse.Error::class.java) 267 assertThat((response as ExecuteAppFunctionResponse.Error).error) 268 .isInstanceOf(AppFunctionInvalidArgumentException::class.java) 269 } 270 271 @Test 272 fun observeAppFunctions_emptyPackagesListInSearchSpec_noResults() = 273 runBlocking<Unit> { 274 val searchFunctionSpec = AppFunctionSearchSpec(packageNames = emptySet()) 275 276 assertThat(appFunctionManagerCompat.observeAppFunctions(searchFunctionSpec).first()) 277 .isEmpty() 278 } 279 280 @Test 281 fun observeAppFunctions_emptySchemaNameInSearchSpec_noResults() = 282 runBlocking<Unit> { 283 val searchFunctionSpec = AppFunctionSearchSpec(schemaName = "") 284 285 assertThat(appFunctionManagerCompat.observeAppFunctions(searchFunctionSpec).first()) 286 .isEmpty() 287 } 288 289 @Test 290 fun observeAppFunctions_emptySchemaCategoryInSearchSpec_noResults() = 291 runBlocking<Unit> { 292 val searchFunctionSpec = AppFunctionSearchSpec(schemaCategory = "") 293 294 assertThat(appFunctionManagerCompat.observeAppFunctions(searchFunctionSpec).first()) 295 .isEmpty() 296 } 297 298 @Test 299 fun observeAppFunctions_packageListNotSetInSpec_returnsAllAppFunctions() = 300 runBlocking<Unit> { 301 installApk(ADDITIONAL_APK_FILE) 302 val searchFunctionSpec = AppFunctionSearchSpec() 303 304 val appFunctions = 305 appFunctionManagerCompat.observeAppFunctions(searchFunctionSpec).first() 306 307 assertThat(appFunctions.map { it.id }) 308 .containsExactly( 309 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_ENABLED_BY_DEFAULT, 310 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_DISABLED_BY_DEFAULT, 311 AppFunctionMetadataTestHelper.FunctionIds.MEDIA_SCHEMA_PRINT, 312 AppFunctionMetadataTestHelper.FunctionIds.MEDIA_SCHEMA2_PRINT, 313 AppFunctionMetadataTestHelper.FunctionIds.NOTES_SCHEMA_PRINT, 314 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_EXECUTION_FAIL, 315 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_EXECUTION_SUCCEED, 316 ADDITIONAL_APP_FUNCTION_ID 317 ) 318 } 319 320 @Test 321 fun observeAppFunctions_multiplePackagesSetInSpec_returnsAppFunctionsFromBoth() = 322 runBlocking<Unit> { 323 installApk(ADDITIONAL_APK_FILE) 324 val searchFunctionSpec = 325 AppFunctionSearchSpec( 326 packageNames = setOf(context.packageName, ADDITIONAL_APP_PACKAGE) 327 ) 328 329 val appFunctions = 330 appFunctionManagerCompat.observeAppFunctions(searchFunctionSpec).first() 331 332 assertThat(appFunctions.map { it.id }) 333 .containsExactly( 334 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_ENABLED_BY_DEFAULT, 335 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_DISABLED_BY_DEFAULT, 336 AppFunctionMetadataTestHelper.FunctionIds.MEDIA_SCHEMA_PRINT, 337 AppFunctionMetadataTestHelper.FunctionIds.MEDIA_SCHEMA2_PRINT, 338 AppFunctionMetadataTestHelper.FunctionIds.NOTES_SCHEMA_PRINT, 339 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_EXECUTION_FAIL, 340 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_EXECUTION_SUCCEED, 341 ADDITIONAL_APP_FUNCTION_ID 342 ) 343 } 344 345 @Test 346 fun observeAppFunctions_packageListSetInSpec_returnsAppFunctionsInPackage() = 347 runBlocking<Unit> { 348 installApk(ADDITIONAL_APK_FILE) 349 val searchFunctionSpec = 350 AppFunctionSearchSpec(packageNames = setOf(context.packageName)) 351 352 val appFunctions = 353 appFunctionManagerCompat.observeAppFunctions(searchFunctionSpec).first() 354 355 // TODO: Populate other fields for legacy indexer. 356 assertThat(appFunctions.map { it.id }) 357 .containsExactly( 358 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_ENABLED_BY_DEFAULT, 359 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_DISABLED_BY_DEFAULT, 360 AppFunctionMetadataTestHelper.FunctionIds.MEDIA_SCHEMA_PRINT, 361 AppFunctionMetadataTestHelper.FunctionIds.MEDIA_SCHEMA2_PRINT, 362 AppFunctionMetadataTestHelper.FunctionIds.NOTES_SCHEMA_PRINT, 363 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_EXECUTION_FAIL, 364 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_EXECUTION_SUCCEED, 365 ) 366 // Only check for all fields when dynamic indexer is enabled. 367 assumeTrue(metadataTestHelper.isDynamicIndexerAvailable()) 368 assertThat(appFunctions) 369 .containsExactly( 370 AppFunctionMetadataTestHelper.FunctionMetadata.NO_SCHEMA_ENABLED_BY_DEFAULT, 371 AppFunctionMetadataTestHelper.FunctionMetadata.NO_SCHEMA_DISABLED_BY_DEFAULT, 372 AppFunctionMetadataTestHelper.FunctionMetadata.MEDIA_SCHEMA_PRINT, 373 AppFunctionMetadataTestHelper.FunctionMetadata.MEDIA_SCHEMA2_PRINT, 374 AppFunctionMetadataTestHelper.FunctionMetadata.NOTES_SCHEMA_PRINT, 375 AppFunctionMetadataTestHelper.FunctionMetadata.NO_SCHEMA_EXECUTION_FAIL, 376 AppFunctionMetadataTestHelper.FunctionMetadata.NO_SCHEMA_EXECUTION_SUCCEED, 377 ) 378 } 379 380 @Test 381 fun observeAppFunctions_schemaNameInSpec_returnsMatchingAppFunctions() = 382 runBlocking<Unit> { 383 val searchFunctionSpec = AppFunctionSearchSpec(schemaName = "print") 384 385 val appFunctions = 386 appFunctionManagerCompat.observeAppFunctions(searchFunctionSpec).first() 387 388 // TODO: Populate other fields for legacy indexer. 389 assertThat(appFunctions.map { it.id }) 390 .containsExactly( 391 AppFunctionMetadataTestHelper.FunctionIds.MEDIA_SCHEMA_PRINT, 392 AppFunctionMetadataTestHelper.FunctionIds.NOTES_SCHEMA_PRINT, 393 AppFunctionMetadataTestHelper.FunctionIds.MEDIA_SCHEMA2_PRINT 394 ) 395 assertThat(appFunctions.map { it.schema }) 396 .containsExactly( 397 AppFunctionMetadataTestHelper.FunctionMetadata.MEDIA_SCHEMA_PRINT.schema, 398 AppFunctionMetadataTestHelper.FunctionMetadata.NOTES_SCHEMA_PRINT.schema, 399 AppFunctionMetadataTestHelper.FunctionMetadata.MEDIA_SCHEMA2_PRINT.schema, 400 ) 401 // Only check for all fields when dynamic indexer is enabled. 402 assumeTrue(metadataTestHelper.isDynamicIndexerAvailable()) 403 assertThat(appFunctions) 404 .containsExactly( 405 AppFunctionMetadataTestHelper.FunctionMetadata.MEDIA_SCHEMA_PRINT, 406 AppFunctionMetadataTestHelper.FunctionMetadata.NOTES_SCHEMA_PRINT, 407 AppFunctionMetadataTestHelper.FunctionMetadata.MEDIA_SCHEMA2_PRINT, 408 ) 409 } 410 411 @Test 412 fun observeAppFunctions_schemaCategoryInSpec_returnsMatchingAppFunctions() = 413 runBlocking<Unit> { 414 val searchFunctionSpec = AppFunctionSearchSpec(schemaCategory = "media") 415 416 val appFunctions = 417 appFunctionManagerCompat.observeAppFunctions(searchFunctionSpec).first() 418 419 // TODO: Populate other fields for legacy indexer. 420 assertThat(appFunctions.map { it.id }) 421 .containsExactly( 422 AppFunctionMetadataTestHelper.FunctionIds.MEDIA_SCHEMA_PRINT, 423 AppFunctionMetadataTestHelper.FunctionIds.MEDIA_SCHEMA2_PRINT 424 ) 425 assertThat(appFunctions.map { it.schema }) 426 .containsExactly( 427 AppFunctionMetadataTestHelper.FunctionMetadata.MEDIA_SCHEMA_PRINT.schema, 428 AppFunctionMetadataTestHelper.FunctionMetadata.MEDIA_SCHEMA2_PRINT.schema, 429 ) 430 // Only check for all fields when dynamic indexer is enabled. 431 assumeTrue(metadataTestHelper.isDynamicIndexerAvailable()) 432 assertThat(appFunctions) 433 .containsExactly( 434 AppFunctionMetadataTestHelper.FunctionMetadata.MEDIA_SCHEMA_PRINT, 435 AppFunctionMetadataTestHelper.FunctionMetadata.MEDIA_SCHEMA2_PRINT 436 ) 437 } 438 439 @Test 440 fun observeAppFunctions_minSchemaVersionInSpec_returnsAppFunctionsWithSchemaVersionGreaterThanMin() = 441 runBlocking<Unit> { 442 val searchFunctionSpec = AppFunctionSearchSpec(minSchemaVersion = 2) 443 444 val appFunctions = 445 appFunctionManagerCompat.observeAppFunctions(searchFunctionSpec).first() 446 447 // TODO: Populate other fields for legacy indexer. 448 assertThat(appFunctions.map { it.id }) 449 .containsExactly(AppFunctionMetadataTestHelper.FunctionIds.MEDIA_SCHEMA2_PRINT) 450 assertThat(appFunctions.map { it.schema }) 451 .containsExactly( 452 AppFunctionMetadataTestHelper.FunctionMetadata.MEDIA_SCHEMA2_PRINT.schema 453 ) 454 // Only check for all fields when dynamic indexer is enabled. 455 assumeTrue(metadataTestHelper.isDynamicIndexerAvailable()) 456 assertThat(appFunctions) 457 .containsExactly(AppFunctionMetadataTestHelper.FunctionMetadata.MEDIA_SCHEMA2_PRINT) 458 } 459 460 @Test 461 fun observeAppFunctions_isDisabledInRuntime_returnsIsEnabledFalse() = 462 runBlocking<Unit> { 463 val functionIdToTest = 464 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_ENABLED_BY_DEFAULT 465 val searchFunctionSpec = AppFunctionSearchSpec() 466 appFunctionManagerCompat.setAppFunctionEnabled( 467 functionIdToTest, 468 AppFunctionManagerCompat.APP_FUNCTION_STATE_DISABLED 469 ) 470 471 val appFunctionMetadata = 472 appFunctionManagerCompat.observeAppFunctions(searchFunctionSpec).first().single { 473 it.id == functionIdToTest 474 } 475 476 assertThat(appFunctionMetadata.isEnabled).isFalse() 477 } 478 479 @Test 480 fun observeAppFunctions_isEnabledInRuntime_returnsIsEnabledTrue() = 481 runBlocking<Unit> { 482 val functionIdToTest = 483 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_DISABLED_BY_DEFAULT 484 val searchFunctionSpec = AppFunctionSearchSpec() 485 appFunctionManagerCompat.setAppFunctionEnabled( 486 functionIdToTest, 487 AppFunctionManagerCompat.APP_FUNCTION_STATE_ENABLED 488 ) 489 490 val appFunctionMetadata = 491 appFunctionManagerCompat.observeAppFunctions(searchFunctionSpec).first().single { 492 it.id == functionIdToTest 493 } 494 495 assertThat(appFunctionMetadata.isEnabled).isTrue() 496 } 497 498 @Test 499 fun observeAppFunctions_observeDocumentChanges_returnsListWithUpdatedValue() = 500 runBlocking<Unit> { 501 val functionIdToTest = 502 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_ENABLED_BY_DEFAULT 503 val searchFunctionSpec = 504 AppFunctionSearchSpec(packageNames = setOf(context.packageName)) 505 val appFunctionSearchFlow = 506 appFunctionManagerCompat.observeAppFunctions(searchFunctionSpec) 507 val emittedValues = 508 appFunctionSearchFlow.shareIn( 509 scope = CoroutineScope(Dispatchers.Default), 510 started = SharingStarted.Eagerly, 511 replay = 10, 512 ) 513 emittedValues.first() // Allow emitting initial value and registering callback. 514 515 // Modify the runtime document. 516 appFunctionManagerCompat.setAppFunctionEnabled( 517 functionIdToTest, 518 AppFunctionManagerCompat.APP_FUNCTION_STATE_DISABLED 519 ) 520 521 // Collect in a separate scope to avoid deadlock within the testcase. 522 runBlocking(Dispatchers.Default) { emittedValues.take(2).collect {} } 523 assertThat(emittedValues.replayCache).hasSize(2) 524 // Assert first result to be default value. 525 assertThat( 526 emittedValues.replayCache[0] 527 .single { 528 it.id == 529 AppFunctionMetadataTestHelper.FunctionIds 530 .NO_SCHEMA_ENABLED_BY_DEFAULT 531 } 532 .isEnabled 533 ) 534 .isEqualTo( 535 AppFunctionMetadataTestHelper.FunctionMetadata.NO_SCHEMA_ENABLED_BY_DEFAULT 536 .isEnabled 537 ) 538 // Assert next update has updated value. 539 assertThat( 540 emittedValues.replayCache[1] 541 .single { 542 it.id == 543 AppFunctionMetadataTestHelper.FunctionIds 544 .NO_SCHEMA_ENABLED_BY_DEFAULT 545 } 546 .isEnabled 547 ) 548 .isFalse() 549 } 550 551 @Test 552 fun observeAppFunctions_multipleUpdates_returnsUpdatesAfterDebouncing() = 553 runBlocking<Unit> { 554 val functionIdToTest = 555 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_ENABLED_BY_DEFAULT 556 val searchFunctionSpec = 557 AppFunctionSearchSpec(packageNames = setOf(context.packageName)) 558 val appFunctionSearchFlow = 559 appFunctionManagerCompat.observeAppFunctions(searchFunctionSpec) 560 val emittedValues = 561 appFunctionSearchFlow.shareIn( 562 scope = CoroutineScope(Dispatchers.Default), 563 started = SharingStarted.Eagerly, 564 replay = 10, 565 ) 566 emittedValues.first() // Allow emitting initial value and registering callback. 567 568 // Modify the runtime document twice. 569 appFunctionManagerCompat.setAppFunctionEnabled( 570 functionIdToTest, 571 AppFunctionManagerCompat.APP_FUNCTION_STATE_DISABLED 572 ) 573 appFunctionManagerCompat.setAppFunctionEnabled( 574 functionIdToTest, 575 AppFunctionManagerCompat.APP_FUNCTION_STATE_ENABLED 576 ) 577 578 // Collect in a separate scope to avoid deadlock within the testcase. 579 runBlocking(Dispatchers.Default) { emittedValues.take(2).collect {} } 580 // Only 2 updates are emitted. 581 assertThat(emittedValues.replayCache).hasSize(2) 582 assertThat(emittedValues.replayCache[1].single { it.id == functionIdToTest }.isEnabled) 583 .isTrue() 584 } 585 586 @Test 587 fun observeAppFunctions_multiplePackageInstall_onlyObservesSpecifiedPackageUpdate() = 588 runBlocking<Unit> { 589 val searchFunctionSpec = 590 AppFunctionSearchSpec(packageNames = setOf(context.packageName)) 591 val appFunctionSearchFlow = 592 appFunctionManagerCompat.observeAppFunctions(searchFunctionSpec) 593 val emittedValues = 594 appFunctionSearchFlow.shareIn( 595 scope = CoroutineScope(Dispatchers.Default), 596 started = SharingStarted.Eagerly, 597 replay = 10, 598 ) 599 emittedValues.first() // Allow emitting initial value and registering callback. 600 601 installApk(ADDITIONAL_APK_FILE) 602 delay(1000) // Avoid debounce 603 appFunctionManagerCompat.setAppFunctionEnabled( 604 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_ENABLED_BY_DEFAULT, 605 AppFunctionManagerCompat.APP_FUNCTION_STATE_DISABLED 606 ) 607 608 // Collect in a separate scope to avoid deadlock within the testcase. 609 runBlocking(Dispatchers.Default) { emittedValues.take(2).collect {} } 610 // Only 2 updates are emitted and update from other app is ignored. 611 assertThat(emittedValues.replayCache).hasSize(2) 612 assertThat( 613 emittedValues.replayCache[1] 614 .single { 615 it.id == 616 AppFunctionMetadataTestHelper.FunctionIds 617 .NO_SCHEMA_ENABLED_BY_DEFAULT 618 } 619 .isEnabled 620 ) 621 .isFalse() 622 } 623 624 @Test 625 fun observeAppFunctions_multiplePackagesInSpec_updatesEmittedForAllChanges() = 626 runBlocking<Unit> { 627 val searchFunctionSpec = 628 AppFunctionSearchSpec( 629 packageNames = setOf(context.packageName, ADDITIONAL_APP_PACKAGE) 630 ) 631 val appFunctionSearchFlow = 632 appFunctionManagerCompat.observeAppFunctions(searchFunctionSpec) 633 val emittedValues = 634 appFunctionSearchFlow.shareIn( 635 scope = CoroutineScope(Dispatchers.Default), 636 started = SharingStarted.Eagerly, 637 replay = 10, 638 ) 639 emittedValues.first() // Allow emitting initial value and registering callback. 640 641 installApk(ADDITIONAL_APK_FILE) 642 delay(1000) // Avoid debounce 643 appFunctionManagerCompat.setAppFunctionEnabled( 644 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_ENABLED_BY_DEFAULT, 645 AppFunctionManagerCompat.APP_FUNCTION_STATE_DISABLED 646 ) 647 648 // Collect in a separate scope to avoid deadlock within the testcase. 649 runBlocking(Dispatchers.Default) { emittedValues.take(3).collect {} } 650 assertThat(emittedValues.replayCache).hasSize(3) 651 // First result only contains functions from first package. 652 assertThat(emittedValues.replayCache[0].map { it.id }) 653 .containsExactly( 654 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_ENABLED_BY_DEFAULT, 655 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_DISABLED_BY_DEFAULT, 656 AppFunctionMetadataTestHelper.FunctionIds.MEDIA_SCHEMA_PRINT, 657 AppFunctionMetadataTestHelper.FunctionIds.MEDIA_SCHEMA2_PRINT, 658 AppFunctionMetadataTestHelper.FunctionIds.NOTES_SCHEMA_PRINT, 659 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_EXECUTION_FAIL, 660 AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_EXECUTION_SUCCEED, 661 ) 662 // Second result contains functionId from additional app install as well. 663 assertThat(emittedValues.replayCache[1].map { it.id }) 664 .contains(ADDITIONAL_APP_FUNCTION_ID) 665 // Third result has modified value of isEnabled from the original package. 666 assertThat( 667 emittedValues.replayCache[2] 668 .single { 669 it.id == 670 AppFunctionMetadataTestHelper.FunctionIds 671 .NO_SCHEMA_ENABLED_BY_DEFAULT 672 } 673 .isEnabled 674 ) 675 .isFalse() 676 } 677 678 private suspend fun installApk(apk: String) { 679 val installer = context.packageManager.packageInstaller 680 val sessionParams = 681 PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL) 682 683 val sessionId = installer.createSession(sessionParams) 684 685 installer.openSession(sessionId).use { session -> 686 session.openWrite("apk_install", 0, -1).use { outputStream -> 687 getResourceAsStream(apk).transferTo(outputStream) 688 } 689 690 assertThat(session.commitSession()).isTrue() 691 } 692 693 metadataTestHelper.awaitAppFunctionIndexed(setOf(ADDITIONAL_APP_FUNCTION_ID)) 694 } 695 696 fun getResourceAsStream(name: String): InputStream { 697 return checkNotNull(Thread.currentThread().contextClassLoader).getResourceAsStream(name) 698 } 699 700 @OptIn(ExperimentalCoroutinesApi::class) 701 private suspend fun PackageInstaller.Session.commitSession(): Boolean { 702 val action = "com.example.COMMIT_COMPLETE.${System.currentTimeMillis()}" 703 704 return suspendCancellableCoroutine { continuation -> 705 val receiver = 706 object : BroadcastReceiver() { 707 override fun onReceive(context: Context, intent: Intent) { 708 context.unregisterReceiver(this) 709 710 val status = 711 intent.getIntExtra( 712 PackageInstaller.EXTRA_STATUS, 713 PackageInstaller.STATUS_FAILURE 714 ) 715 val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) 716 717 if (status == PackageInstaller.STATUS_SUCCESS) { 718 continuation.resume(true) {} 719 } else { 720 continuation.resumeWithException( 721 Exception("Installation failed: $message") 722 ) 723 } 724 } 725 } 726 727 val filter = IntentFilter(action) 728 context.registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED) 729 730 val intent = Intent(action).setPackage(context.packageName) 731 val sender = 732 PendingIntent.getBroadcast( 733 context, 734 0, 735 intent, 736 PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE 737 ) 738 739 this.commit(sender.intentSender) 740 741 continuation.invokeOnCancellation { 742 // Unregister the receiver if the coroutine is cancelled 743 context.unregisterReceiver(receiver) 744 } 745 } 746 } 747 748 private companion object { 749 const val ADDITIONAL_APP_FUNCTION_ID = 750 "com.example.android.architecture.blueprints.todoapp#NoteFunctions_createNote" 751 const val ADDITIONAL_APK_FILE = "notes.apk" 752 const val ADDITIONAL_APP_PACKAGE = "com.google.android.app.notes" 753 } 754 } 755