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