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.core
18 
19 import android.content.Context
20 import android.os.Build
21 import androidx.`annotation`.RequiresApi
22 import androidx.appfunctions.core.AppFunctionMetadataTestHelper.FunctionIds.NOTES_SCHEMA_PRINT
23 import androidx.appfunctions.`internal`.readAll
24 import androidx.appfunctions.metadata.AppFunctionComponentsMetadata
25 import androidx.appfunctions.metadata.AppFunctionMetadata
26 import androidx.appfunctions.metadata.AppFunctionMetadataDocument
27 import androidx.appfunctions.metadata.AppFunctionPrimitiveTypeMetadata
28 import androidx.appfunctions.metadata.AppFunctionResponseMetadata
29 import androidx.appfunctions.metadata.AppFunctionSchemaMetadata
30 import androidx.appsearch.app.Features
31 import androidx.appsearch.app.GlobalSearchSession
32 import androidx.appsearch.app.SearchSpec
33 import androidx.appsearch.platformstorage.PlatformStorage
34 import androidx.concurrent.futures.await
35 import kotlinx.coroutines.delay
36 
37 @RequiresApi(Build.VERSION_CODES.S)
38 internal class AppFunctionMetadataTestHelper(private val context: Context) {
39     suspend fun awaitAppFunctionIndexed(functionIds: Set<String>) {
40         awaitRuntimeMetadataAvailable(functionIds)
41     }
42 
43     private suspend fun awaitRuntimeMetadataAvailable(functionIds: Set<String>) {
44         val notFoundIds = mutableSetOf<String>().apply { addAll(functionIds) }
45 
46         createSearchSession().use { session ->
47             var retry = 0
48             while (retry < RETRY_LIMIT) {
49                 val searchResults =
50                     session.search(
51                         "",
52                         SearchSpec.Builder()
53                             .addFilterNamespaces("app_functions_runtime")
54                             .addFilterPackageNames("android")
55                             .addFilterSchemas("AppFunctionRuntimeMetadata")
56                             .build(),
57                     )
58                 var nextPage = searchResults.nextPageAsync.await()
59                 while (notFoundIds.isNotEmpty() && nextPage.isNotEmpty()) {
60                     for (result in nextPage) {
61                         val functionId = result.genericDocument.getPropertyString("functionId")
62                         if (notFoundIds.contains(functionId)) {
63                             notFoundIds.remove(functionId)
64                         }
65                     }
66                     nextPage = searchResults.nextPageAsync.await()
67                 }
68                 if (notFoundIds.isEmpty()) {
69                     return
70                 }
71 
72                 delay(RETRY_DELAY_MS)
73                 retry += 1
74             }
75         }
76         throw IllegalStateException("AppSearch indexer fail")
77     }
78 
79     /** Checks if the legacy AppFunction indexer is available on the device. */
80     suspend fun isLegacyAppFunctionIndexerAvailable(): Boolean {
81         return createSearchSession().use {
82             // AppFunctions indexer was shipped with Mobile applications indexer.
83             it.features.isFeatureSupported(Features.INDEXER_MOBILE_APPLICATIONS)
84         }
85     }
86 
87     /**
88      * Checks if the dynamic indexer is available.
89      *
90      * Only works if the functions are already indexed. Use [awaitAppFunctionIndexed] for waiting.
91      */
92     suspend fun isDynamicIndexerAvailable(): Boolean =
93         // TODO - Check AppSearch version when new indexer is available in AppSearch.
94         createSearchSession().use { session ->
95             // Only check for current package functions.
96             val metadataDocument =
97                 session
98                     .search(
99                         "packageName:\"${context.packageName}\"",
100                         SearchSpec.Builder()
101                             .addFilterNamespaces("app_functions")
102                             .addFilterDocumentClasses(AppFunctionMetadataDocument::class.java)
103                             .addFilterPackageNames("android")
104                             .setVerbatimSearchEnabled(true)
105                             .setNumericSearchEnabled(true)
106                             .build()
107                     )
108                     .readAll { it.genericDocument }
109                     .filterNotNull()
110                     .first()
111 
112             // Check if one of the additional property i.e. response is available. Checking for
113             // response as all app functions will always have a response.
114             return metadataDocument.getPropertyDocument("response") != null
115         }
116 
117     private suspend fun createSearchSession(): GlobalSearchSession {
118         return PlatformStorage.createGlobalSearchSessionAsync(
119                 PlatformStorage.GlobalSearchContext.Builder(context).build()
120             )
121             .await()
122     }
123 
124     /** List of function ids defined in androidTest/res/xml/app_functions.xml */
125     object FunctionIds {
126         const val NO_SCHEMA_ENABLED_BY_DEFAULT =
127             "androidx.appfunctions.test#noSchema_enabledByDefault"
128         const val NO_SCHEMA_DISABLED_BY_DEFAULT =
129             "androidx.appfunctions.test#noSchema_disabledByDefault"
130         const val NO_SCHEMA_EXECUTION_SUCCEED =
131             "androidx.appfunctions.test#noSchema_executionSucceed"
132         const val NO_SCHEMA_EXECUTION_FAIL = "androidx.appfunctions.test#noSchema_executionFail"
133         const val NOTES_SCHEMA_PRINT = "androidx.appfunctions.test#notesSchema_print"
134         const val MEDIA_SCHEMA_PRINT = "androidx.appfunctions.test#mediaSchema_print"
135         const val MEDIA_SCHEMA2_PRINT = "androidx.appfunctions.test#mediaSchema2_print"
136     }
137 
138     object FunctionMetadata {
139         val NO_SCHEMA_EXECUTION_SUCCEED =
140             AppFunctionMetadata(
141                 id = FunctionIds.NO_SCHEMA_EXECUTION_SUCCEED,
142                 packageName = "androidx.appfunctions.runtime.test",
143                 isEnabled = true,
144                 schema = null,
145                 parameters = emptyList(),
146                 response =
147                     AppFunctionResponseMetadata(
148                         valueType =
149                             AppFunctionPrimitiveTypeMetadata(
150                                 type = AppFunctionPrimitiveTypeMetadata.TYPE_STRING,
151                                 isNullable = false
152                             )
153                     ),
154                 components = AppFunctionComponentsMetadata()
155             )
156 
157         val NO_SCHEMA_ENABLED_BY_DEFAULT =
158             AppFunctionMetadata(
159                 id = FunctionIds.NO_SCHEMA_ENABLED_BY_DEFAULT,
160                 packageName = "androidx.appfunctions.runtime.test",
161                 isEnabled = true,
162                 schema = null,
163                 parameters = emptyList(),
164                 response =
165                     AppFunctionResponseMetadata(
166                         valueType =
167                             AppFunctionPrimitiveTypeMetadata(
168                                 type = AppFunctionPrimitiveTypeMetadata.TYPE_UNIT,
169                                 isNullable = false
170                             )
171                     ),
172                 components = AppFunctionComponentsMetadata()
173             )
174 
175         val NO_SCHEMA_DISABLED_BY_DEFAULT =
176             AppFunctionMetadata(
177                 id = FunctionIds.NO_SCHEMA_DISABLED_BY_DEFAULT,
178                 packageName = "androidx.appfunctions.runtime.test",
179                 isEnabled = false,
180                 schema = null,
181                 parameters = emptyList(),
182                 response =
183                     AppFunctionResponseMetadata(
184                         valueType =
185                             AppFunctionPrimitiveTypeMetadata(
186                                 type = AppFunctionPrimitiveTypeMetadata.TYPE_UNIT,
187                                 isNullable = false
188                             )
189                     ),
190                 components = AppFunctionComponentsMetadata()
191             )
192 
193         val MEDIA_SCHEMA2_PRINT =
194             AppFunctionMetadata(
195                 id = FunctionIds.MEDIA_SCHEMA2_PRINT,
196                 packageName = "androidx.appfunctions.runtime.test",
197                 isEnabled = true,
198                 schema = AppFunctionSchemaMetadata(category = "media", name = "print", version = 2),
199                 parameters = emptyList(),
200                 response =
201                     AppFunctionResponseMetadata(
202                         valueType =
203                             AppFunctionPrimitiveTypeMetadata(
204                                 type = AppFunctionPrimitiveTypeMetadata.TYPE_UNIT,
205                                 isNullable = false
206                             )
207                     ),
208                 components = AppFunctionComponentsMetadata()
209             )
210 
211         val MEDIA_SCHEMA_PRINT =
212             AppFunctionMetadata(
213                 id = FunctionIds.MEDIA_SCHEMA_PRINT,
214                 packageName = "androidx.appfunctions.runtime.test",
215                 isEnabled = true,
216                 schema = AppFunctionSchemaMetadata(category = "media", name = "print", version = 1),
217                 parameters = emptyList(),
218                 response =
219                     AppFunctionResponseMetadata(
220                         valueType =
221                             AppFunctionPrimitiveTypeMetadata(
222                                 type = AppFunctionPrimitiveTypeMetadata.TYPE_UNIT,
223                                 isNullable = false
224                             )
225                     ),
226                 components = AppFunctionComponentsMetadata()
227             )
228 
229         val NOTES_SCHEMA_PRINT =
230             AppFunctionMetadata(
231                 id = FunctionIds.NOTES_SCHEMA_PRINT,
232                 packageName = "androidx.appfunctions.runtime.test",
233                 isEnabled = true,
234                 schema = AppFunctionSchemaMetadata(category = "notes", name = "print", version = 1),
235                 parameters = emptyList(),
236                 response =
237                     AppFunctionResponseMetadata(
238                         valueType =
239                             AppFunctionPrimitiveTypeMetadata(
240                                 type = AppFunctionPrimitiveTypeMetadata.TYPE_UNIT,
241                                 isNullable = false
242                             )
243                     ),
244                 components = AppFunctionComponentsMetadata()
245             )
246 
247         val NO_SCHEMA_EXECUTION_FAIL =
248             AppFunctionMetadata(
249                 id = FunctionIds.NO_SCHEMA_EXECUTION_FAIL,
250                 packageName = "androidx.appfunctions.runtime.test",
251                 isEnabled = true,
252                 schema = null,
253                 parameters = emptyList(),
254                 response =
255                     AppFunctionResponseMetadata(
256                         valueType =
257                             AppFunctionPrimitiveTypeMetadata(
258                                 type = AppFunctionPrimitiveTypeMetadata.TYPE_UNIT,
259                                 isNullable = false
260                             )
261                     ),
262                 components = AppFunctionComponentsMetadata()
263             )
264     }
265 
266     companion object {
267         private const val RETRY_LIMIT = 5
268         private const val RETRY_DELAY_MS = 500L
269     }
270 }
271