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.internal 18 19 import android.content.Context 20 import android.os.Build 21 import android.util.Log 22 import androidx.annotation.RequiresApi 23 import androidx.appfunctions.AppFunctionFunctionNotFoundException 24 import androidx.appfunctions.AppFunctionSearchSpec 25 import androidx.appfunctions.metadata.AppFunctionComponentsMetadata 26 import androidx.appfunctions.metadata.AppFunctionMetadata 27 import androidx.appfunctions.metadata.AppFunctionMetadataDocument 28 import androidx.appfunctions.metadata.AppFunctionPrimitiveTypeMetadata 29 import androidx.appfunctions.metadata.AppFunctionResponseMetadata 30 import androidx.appfunctions.metadata.AppFunctionRuntimeMetadata 31 import androidx.appfunctions.metadata.AppFunctionSchemaMetadata 32 import androidx.appsearch.app.GenericDocument 33 import androidx.appsearch.app.GetByDocumentIdRequest 34 import androidx.appsearch.app.GlobalSearchSession 35 import androidx.appsearch.app.JoinSpec 36 import androidx.appsearch.app.SearchResult 37 import androidx.appsearch.app.SearchSpec 38 import androidx.appsearch.observer.DocumentChangeInfo 39 import androidx.appsearch.observer.ObserverCallback 40 import androidx.appsearch.observer.ObserverSpec 41 import androidx.appsearch.observer.SchemaChangeInfo 42 import com.android.extensions.appfunctions.AppFunctionManager 43 import kotlin.time.Duration.Companion.seconds 44 import kotlinx.coroutines.FlowPreview 45 import kotlinx.coroutines.asExecutor 46 import kotlinx.coroutines.channels.Channel 47 import kotlinx.coroutines.channels.awaitClose 48 import kotlinx.coroutines.flow.Flow 49 import kotlinx.coroutines.flow.callbackFlow 50 import kotlinx.coroutines.flow.debounce 51 import kotlinx.coroutines.flow.flow 52 import kotlinx.coroutines.flow.receiveAsFlow 53 import kotlinx.coroutines.guava.await 54 import kotlinx.coroutines.launch 55 56 /** 57 * A class responsible for reading and searching for app functions based on a search specification. 58 * 59 * It searches for AppFunction documents using the [GlobalSearchSession] and converts them into 60 * [AppFunctionMetadata] objects. 61 * 62 * @param context The context of the application, used for session creation. 63 */ 64 @RequiresApi(Build.VERSION_CODES.S) 65 internal class AppSearchAppFunctionReader(private val context: Context) : AppFunctionReader { 66 67 @OptIn(FlowPreview::class) 68 override fun searchAppFunctions( 69 searchFunctionSpec: AppFunctionSearchSpec 70 ): Flow<List<AppFunctionMetadata>> { 71 if (searchFunctionSpec.packageNames?.isEmpty() == true) { 72 return flow { emit(emptyList()) } 73 } 74 75 return callbackFlow { 76 val session = createSearchSession(context) 77 78 // Perform initial search immediately 79 send(performSearch(session, searchFunctionSpec)) 80 81 val appSearchChannelObserver = AppSearchChannelObserver() 82 // Register the observer callback 83 session.registerObserverCallback( 84 SYSTEM_PACKAGE_NAME, 85 buildObserverSpec(searchFunctionSpec.packageNames ?: emptySet()), 86 Dispatchers.Worker.asExecutor(), 87 appSearchChannelObserver 88 ) 89 90 // Coroutine to react to updates from the observer 91 val observerJob = launch { 92 appSearchChannelObserver.observe().debounce(OBSERVER_DEBOUNCE_MILLIS).collect { 93 // TODO(b/403264749): Check if we can skip the running a full search again by 94 // caching the results. 95 send(performSearch(session, searchFunctionSpec)) 96 } 97 } 98 99 // Clean up when collection stops 100 awaitClose { 101 observerJob.cancel() 102 appSearchChannelObserver.close() 103 session.unregisterObserverCallback(SYSTEM_PACKAGE_NAME, appSearchChannelObserver) 104 session.close() 105 } 106 } 107 } 108 109 private class AppSearchChannelObserver : ObserverCallback { 110 private val updateChannel = Channel<Unit>(Channel.RENDEZVOUS) 111 112 override fun onSchemaChanged(changeInfo: SchemaChangeInfo) { 113 updateChannel.trySend(Unit) 114 } 115 116 override fun onDocumentChanged(changeInfo: DocumentChangeInfo) { 117 updateChannel.trySend(Unit) 118 } 119 120 fun observe(): Flow<Unit> = updateChannel.receiveAsFlow() 121 122 fun close() { 123 updateChannel.close() 124 } 125 } 126 127 private fun buildObserverSpec(packageNames: Set<String>) = 128 ObserverSpec.Builder() 129 .addFilterSchemas( 130 packageNames.flatMap { 131 listOf("AppFunctionStaticMetadata-$it", "AppFunctionRuntimeMetadata-$it") 132 } 133 ) 134 .build() 135 136 private suspend fun performSearch( 137 session: GlobalSearchSession, 138 searchFunctionSpec: AppFunctionSearchSpec, 139 ): List<AppFunctionMetadata> { 140 val joinSpec = 141 JoinSpec.Builder(AppFunctionRuntimeMetadata.STATIC_METADATA_JOIN_PROPERTY) 142 .setNestedSearch("", RUNTIME_SEARCH_SPEC) 143 .build() 144 145 val staticSearchSpec = 146 SearchSpec.Builder() 147 .addFilterNamespaces(APP_FUNCTIONS_NAMESPACE) 148 .addFilterDocumentClasses(AppFunctionMetadataDocument::class.java) 149 .addFilterPackageNames(SYSTEM_PACKAGE_NAME) 150 .setJoinSpec(joinSpec) 151 .setVerbatimSearchEnabled(true) 152 .setNumericSearchEnabled(true) 153 .setListFilterQueryLanguageEnabled(true) 154 .build() 155 return session 156 .search(searchFunctionSpec.toStaticMetadataAppSearchQuery(), staticSearchSpec) 157 .readAll(::convertSearchResultToAppFunctionMetadata) 158 .filterNotNull() 159 } 160 161 private fun convertSearchResultToAppFunctionMetadata( 162 searchResult: SearchResult 163 ): AppFunctionMetadata? { 164 165 // This is different from document id which for uniqueness is computed as packageName + "/" 166 // + functionId. 167 val functionId = checkNotNull(searchResult.genericDocument.getPropertyString("functionId")) 168 val packageName = 169 checkNotNull(searchResult.genericDocument.getPropertyString("packageName")) 170 171 // TODO: Handle failures and log instead of throwing. 172 val staticMetadataDocument = 173 searchResult.genericDocument.toDocumentClass(AppFunctionMetadataDocument::class.java) 174 val runtimeMetadataDocument = 175 searchResult.joinedResults 176 .single() 177 .genericDocument 178 .toDocumentClass(AppFunctionRuntimeMetadata::class.java) 179 180 return AppFunctionMetadata( 181 id = functionId, 182 packageName = packageName, 183 isEnabled = computeEffectivelyEnabled(staticMetadataDocument, runtimeMetadataDocument), 184 schema = buildSchemaMetadataFromGdForLegacyIndexer(searchResult.genericDocument), 185 // TODO: Populate them separately for legacy indexer. 186 parameters = 187 // Since this is a list type it can be null for cases where an app function has no 188 // parameters. 189 if (staticMetadataDocument.response != null) { 190 staticMetadataDocument.parameters?.map { it.toAppFunctionParameterMetadata() } 191 ?: emptyList() 192 } else { 193 // TODO - Populate for legacy indexer 194 emptyList() 195 }, 196 response = 197 staticMetadataDocument.response?.toAppFunctionResponseMetadata() 198 ?: AppFunctionResponseMetadata( 199 valueType = 200 AppFunctionPrimitiveTypeMetadata( 201 type = AppFunctionPrimitiveTypeMetadata.TYPE_UNIT, 202 isNullable = false 203 ) 204 ), 205 components = 206 staticMetadataDocument.components?.toAppFunctionComponentsMetadata() 207 ?: AppFunctionComponentsMetadata(), 208 ) 209 } 210 211 private fun computeEffectivelyEnabled( 212 staticMetadata: AppFunctionMetadataDocument, 213 runtimeMetadata: AppFunctionRuntimeMetadata, 214 ): Boolean = 215 when (runtimeMetadata.enabled.toInt()) { 216 AppFunctionManager.APP_FUNCTION_STATE_ENABLED -> true 217 AppFunctionManager.APP_FUNCTION_STATE_DISABLED -> false 218 AppFunctionManager.APP_FUNCTION_STATE_DEFAULT -> staticMetadata.isEnabledByDefault 219 else -> 220 throw IllegalStateException( 221 "Unknown AppFunction state: ${runtimeMetadata.enabled}." 222 ) 223 } 224 225 private fun buildSchemaMetadataFromGdForLegacyIndexer( 226 document: GenericDocument 227 ): AppFunctionSchemaMetadata? { 228 val schemaName = document.getPropertyString("schemaName") 229 val schemaCategory = document.getPropertyString("schemaCategory") 230 val schemaVersion = document.getPropertyLong("schemaVersion") 231 232 if (schemaName == null || schemaCategory == null || schemaVersion == 0L) { 233 if (schemaName != null || schemaCategory != null || schemaVersion != 0L) { 234 Log.e( 235 AppFunctionReader::class.simpleName, 236 "Unexpected state: schemaName=$schemaName, schemaCategory=$schemaCategory, schemaVersion=$schemaVersion" 237 ) 238 } 239 return null 240 } 241 242 return AppFunctionSchemaMetadata( 243 name = schemaName, 244 category = schemaCategory, 245 version = schemaVersion 246 ) 247 } 248 249 /** 250 * Returns the [AppFunctionSchemaMetadata] of the given app function. Returns null if the 251 * function is not implementing a predefined schema. 252 * 253 * @throws AppFunctionFunctionNotFoundException if the function does not exist. 254 */ 255 override suspend fun getAppFunctionSchemaMetadata( 256 functionId: String, 257 packageName: String 258 ): AppFunctionSchemaMetadata? { 259 val documentId = getAppFunctionId(packageName, functionId) 260 val result = 261 createSearchSession(context = context) 262 .use { session -> 263 session.getByDocumentIdAsync( 264 SYSTEM_PACKAGE_NAME, 265 APP_FUNCTIONS_STATIC_DATABASE_NAME, 266 GetByDocumentIdRequest.Builder(APP_FUNCTIONS_NAMESPACE) 267 .addIds(documentId) 268 .build() 269 ) 270 } 271 .await() 272 val genericDocument = 273 result.successes[documentId] 274 ?: throw AppFunctionFunctionNotFoundException( 275 "Function with ID = $documentId is not available" 276 ) 277 return buildSchemaMetadataFromGdForLegacyIndexer(genericDocument) 278 } 279 280 private fun getAppFunctionId(packageName: String, functionId: String) = 281 "$packageName/$functionId" 282 283 private companion object { 284 const val SYSTEM_PACKAGE_NAME = "android" 285 const val APP_FUNCTIONS_NAMESPACE = "app_functions" 286 const val APP_FUNCTIONS_RUNTIME_NAMESPACE = "app_functions_runtime" 287 const val APP_FUNCTIONS_STATIC_DATABASE_NAME = "apps-db" 288 289 val OBSERVER_DEBOUNCE_MILLIS = 1.seconds 290 291 val RUNTIME_SEARCH_SPEC = 292 SearchSpec.Builder() 293 .addFilterNamespaces(APP_FUNCTIONS_RUNTIME_NAMESPACE) 294 .addFilterDocumentClasses(AppFunctionRuntimeMetadata::class.java) 295 .addFilterPackageNames(SYSTEM_PACKAGE_NAME) 296 .setVerbatimSearchEnabled(true) 297 .build() 298 } 299 } 300