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