1 /* 2 * Copyright 2020 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 com.android.server.appsearch.external.localstorage; 18 19 import static com.android.server.appsearch.external.localstorage.util.PrefixUtil.addPrefixToDocument; 20 import static com.android.server.appsearch.external.localstorage.util.PrefixUtil.createPrefix; 21 import static com.android.server.appsearch.external.localstorage.util.PrefixUtil.getDatabaseName; 22 import static com.android.server.appsearch.external.localstorage.util.PrefixUtil.getPackageName; 23 import static com.android.server.appsearch.external.localstorage.util.PrefixUtil.getPrefix; 24 import static com.android.server.appsearch.external.localstorage.util.PrefixUtil.removePrefix; 25 import static com.android.server.appsearch.external.localstorage.util.PrefixUtil.removePrefixesFromDocument; 26 27 import android.annotation.NonNull; 28 import android.annotation.Nullable; 29 import android.annotation.WorkerThread; 30 import android.app.appsearch.AppSearchResult; 31 import android.app.appsearch.AppSearchSchema; 32 import android.app.appsearch.GenericDocument; 33 import android.app.appsearch.GetByDocumentIdRequest; 34 import android.app.appsearch.GetSchemaResponse; 35 import android.app.appsearch.PackageIdentifier; 36 import android.app.appsearch.SearchResultPage; 37 import android.app.appsearch.SearchSpec; 38 import android.app.appsearch.SetSchemaResponse; 39 import android.app.appsearch.StorageInfo; 40 import android.app.appsearch.exceptions.AppSearchException; 41 import android.app.appsearch.util.LogUtil; 42 import android.os.Bundle; 43 import android.os.SystemClock; 44 import android.util.ArrayMap; 45 import android.util.ArraySet; 46 import android.util.Log; 47 48 import com.android.internal.annotations.GuardedBy; 49 import com.android.internal.annotations.VisibleForTesting; 50 import com.android.server.appsearch.external.localstorage.converter.GenericDocumentToProtoConverter; 51 import com.android.server.appsearch.external.localstorage.converter.ResultCodeToProtoConverter; 52 import com.android.server.appsearch.external.localstorage.converter.SchemaToProtoConverter; 53 import com.android.server.appsearch.external.localstorage.converter.SearchResultToProtoConverter; 54 import com.android.server.appsearch.external.localstorage.converter.SearchSpecToProtoConverter; 55 import com.android.server.appsearch.external.localstorage.converter.SetSchemaResponseToProtoConverter; 56 import com.android.server.appsearch.external.localstorage.converter.TypePropertyPathToProtoConverter; 57 import com.android.server.appsearch.external.localstorage.stats.InitializeStats; 58 import com.android.server.appsearch.external.localstorage.stats.OptimizeStats; 59 import com.android.server.appsearch.external.localstorage.stats.PutDocumentStats; 60 import com.android.server.appsearch.external.localstorage.stats.RemoveStats; 61 import com.android.server.appsearch.external.localstorage.stats.SearchStats; 62 import com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityStore; 63 64 import com.google.android.icing.IcingSearchEngine; 65 import com.google.android.icing.proto.DeleteByQueryResultProto; 66 import com.google.android.icing.proto.DeleteResultProto; 67 import com.google.android.icing.proto.DocumentProto; 68 import com.google.android.icing.proto.DocumentStorageInfoProto; 69 import com.google.android.icing.proto.GetAllNamespacesResultProto; 70 import com.google.android.icing.proto.GetOptimizeInfoResultProto; 71 import com.google.android.icing.proto.GetResultProto; 72 import com.google.android.icing.proto.GetResultSpecProto; 73 import com.google.android.icing.proto.GetSchemaResultProto; 74 import com.google.android.icing.proto.IcingSearchEngineOptions; 75 import com.google.android.icing.proto.InitializeResultProto; 76 import com.google.android.icing.proto.NamespaceStorageInfoProto; 77 import com.google.android.icing.proto.OptimizeResultProto; 78 import com.google.android.icing.proto.PersistToDiskResultProto; 79 import com.google.android.icing.proto.PersistType; 80 import com.google.android.icing.proto.PropertyConfigProto; 81 import com.google.android.icing.proto.PutResultProto; 82 import com.google.android.icing.proto.ReportUsageResultProto; 83 import com.google.android.icing.proto.ResetResultProto; 84 import com.google.android.icing.proto.ResultSpecProto; 85 import com.google.android.icing.proto.SchemaProto; 86 import com.google.android.icing.proto.SchemaTypeConfigProto; 87 import com.google.android.icing.proto.ScoringSpecProto; 88 import com.google.android.icing.proto.SearchResultProto; 89 import com.google.android.icing.proto.SearchSpecProto; 90 import com.google.android.icing.proto.SetSchemaResultProto; 91 import com.google.android.icing.proto.StatusProto; 92 import com.google.android.icing.proto.StorageInfoProto; 93 import com.google.android.icing.proto.StorageInfoResultProto; 94 import com.google.android.icing.proto.TypePropertyMask; 95 import com.google.android.icing.proto.UsageReport; 96 97 import java.io.Closeable; 98 import java.io.File; 99 import java.util.ArrayList; 100 import java.util.Collections; 101 import java.util.HashMap; 102 import java.util.Iterator; 103 import java.util.List; 104 import java.util.Map; 105 import java.util.Objects; 106 import java.util.Set; 107 import java.util.concurrent.locks.ReadWriteLock; 108 import java.util.concurrent.locks.ReentrantReadWriteLock; 109 110 /** 111 * Manages interaction with the native IcingSearchEngine and other components to implement AppSearch 112 * functionality. 113 * 114 * <p>Never create two instances using the same folder. 115 * 116 * <p>A single instance of {@link AppSearchImpl} can support all packages and databases. This is 117 * done by combining the package and database name into a unique prefix and prefixing the schemas 118 * and documents stored under that owner. Schemas and documents are physically saved together in 119 * {@link IcingSearchEngine}, but logically isolated: 120 * 121 * <ul> 122 * <li>Rewrite SchemaType in SchemaProto by adding the package-database prefix and save into 123 * SchemaTypes set in {@link #setSchema}. 124 * <li>Rewrite namespace and SchemaType in DocumentProto by adding package-database prefix and 125 * save to namespaces set in {@link #putDocument}. 126 * <li>Remove package-database prefix when retrieving documents in {@link #getDocument} and {@link 127 * #query}. 128 * <li>Rewrite filters in {@link SearchSpecProto} to have all namespaces and schema types of the 129 * queried database when user using empty filters in {@link #query}. 130 * </ul> 131 * 132 * <p>Methods in this class belong to two groups, the query group and the mutate group. 133 * 134 * <ul> 135 * <li>All methods are going to modify global parameters and data in Icing are executed under 136 * WRITE lock to keep thread safety. 137 * <li>All methods are going to access global parameters or query data from Icing are executed 138 * under READ lock to improve query performance. 139 * </ul> 140 * 141 * <p>This class is thread safe. 142 * 143 * @hide 144 */ 145 @WorkerThread 146 public final class AppSearchImpl implements Closeable { 147 private static final String TAG = "AppSearchImpl"; 148 149 /** A value 0 means that there're no more pages in the search results. */ 150 private static final long EMPTY_PAGE_TOKEN = 0; 151 152 @VisibleForTesting static final int CHECK_OPTIMIZE_INTERVAL = 100; 153 154 private final ReadWriteLock mReadWriteLock = new ReentrantReadWriteLock(); 155 private final LogUtil mLogUtil = new LogUtil(TAG); 156 private final OptimizeStrategy mOptimizeStrategy; 157 private final LimitConfig mLimitConfig; 158 159 @GuardedBy("mReadWriteLock") 160 @VisibleForTesting 161 final IcingSearchEngine mIcingSearchEngineLocked; 162 163 // This map contains schema types and SchemaTypeConfigProtos for all package-database 164 // prefixes. It maps each package-database prefix to an inner-map. The inner-map maps each 165 // prefixed schema type to its respective SchemaTypeConfigProto. 166 @GuardedBy("mReadWriteLock") 167 private final Map<String, Map<String, SchemaTypeConfigProto>> mSchemaMapLocked = 168 new ArrayMap<>(); 169 170 // This map contains namespaces for all package-database prefixes. All values in the map are 171 // prefixed with the package-database prefix. 172 // TODO(b/172360376): Check if this can be replaced with an ArrayMap 173 @GuardedBy("mReadWriteLock") 174 private final Map<String, Set<String>> mNamespaceMapLocked = new HashMap<>(); 175 176 /** Maps package name to active document count. */ 177 @GuardedBy("mReadWriteLock") 178 private final Map<String, Integer> mDocumentCountMapLocked = new ArrayMap<>(); 179 180 // Maps packages to the set of valid nextPageTokens that the package can manipulate. A token 181 // is unique and constant per query (i.e. the same token '123' is used to iterate through 182 // pages of search results). The tokens themselves are generated and tracked by 183 // IcingSearchEngine. IcingSearchEngine considers a token valid and won't be reused 184 // until we call invalidateNextPageToken on the token. 185 // 186 // Note that we synchronize on itself because the nextPageToken cache is checked at 187 // query-time, and queries are done in parallel with a read lock. Ideally, this would be 188 // guarded by the normal mReadWriteLock.writeLock, but ReentrantReadWriteLocks can't upgrade 189 // read to write locks. This lock should be acquired at the smallest scope possible. 190 // mReadWriteLock is a higher-level lock, so calls shouldn't be made out 191 // to any functions that grab the lock. 192 @GuardedBy("mNextPageTokensLocked") 193 private final Map<String, Set<Long>> mNextPageTokensLocked = new ArrayMap<>(); 194 195 /** 196 * The counter to check when to call {@link #checkForOptimize}. The interval is {@link 197 * #CHECK_OPTIMIZE_INTERVAL}. 198 */ 199 @GuardedBy("mReadWriteLock") 200 private int mOptimizeIntervalCountLocked = 0; 201 202 /** Whether this instance has been closed, and therefore unusable. */ 203 @GuardedBy("mReadWriteLock") 204 private boolean mClosedLocked = false; 205 206 /** 207 * Creates and initializes an instance of {@link AppSearchImpl} which writes data to the given 208 * folder. 209 * 210 * <p>Clients can pass a {@link AppSearchLogger} here through their AppSearchSession, but it 211 * can't be saved inside {@link AppSearchImpl}, because the impl will be shared by all the 212 * sessions for the same package in JetPack. 213 * 214 * <p>Instead, logger instance needs to be passed to each individual method, like create, query 215 * and putDocument. 216 * 217 * @param initStatsBuilder collects stats for initialization if provided. 218 */ 219 @NonNull create( @onNull File icingDir, @NonNull LimitConfig limitConfig, @Nullable InitializeStats.Builder initStatsBuilder, @NonNull OptimizeStrategy optimizeStrategy)220 public static AppSearchImpl create( 221 @NonNull File icingDir, 222 @NonNull LimitConfig limitConfig, 223 @Nullable InitializeStats.Builder initStatsBuilder, 224 @NonNull OptimizeStrategy optimizeStrategy) 225 throws AppSearchException { 226 return new AppSearchImpl(icingDir, limitConfig, initStatsBuilder, optimizeStrategy); 227 } 228 229 /** @param initStatsBuilder collects stats for initialization if provided. */ AppSearchImpl( @onNull File icingDir, @NonNull LimitConfig limitConfig, @Nullable InitializeStats.Builder initStatsBuilder, @NonNull OptimizeStrategy optimizeStrategy)230 private AppSearchImpl( 231 @NonNull File icingDir, 232 @NonNull LimitConfig limitConfig, 233 @Nullable InitializeStats.Builder initStatsBuilder, 234 @NonNull OptimizeStrategy optimizeStrategy) 235 throws AppSearchException { 236 Objects.requireNonNull(icingDir); 237 mLimitConfig = Objects.requireNonNull(limitConfig); 238 mOptimizeStrategy = Objects.requireNonNull(optimizeStrategy); 239 240 mReadWriteLock.writeLock().lock(); 241 try { 242 // We synchronize here because we don't want to call IcingSearchEngine.initialize() more 243 // than once. It's unnecessary and can be a costly operation. 244 IcingSearchEngineOptions options = 245 IcingSearchEngineOptions.newBuilder() 246 .setBaseDir(icingDir.getAbsolutePath()) 247 .build(); 248 mLogUtil.piiTrace("Constructing IcingSearchEngine, request", options); 249 mIcingSearchEngineLocked = new IcingSearchEngine(options); 250 mLogUtil.piiTrace( 251 "Constructing IcingSearchEngine, response", 252 Objects.hashCode(mIcingSearchEngineLocked)); 253 254 // The core initialization procedure. If any part of this fails, we bail into 255 // resetLocked(), deleting all data (but hopefully allowing AppSearchImpl to come up). 256 try { 257 mLogUtil.piiTrace("icingSearchEngine.initialize, request"); 258 InitializeResultProto initializeResultProto = mIcingSearchEngineLocked.initialize(); 259 mLogUtil.piiTrace( 260 "icingSearchEngine.initialize, response", 261 initializeResultProto.getStatus(), 262 initializeResultProto); 263 264 if (initStatsBuilder != null) { 265 initStatsBuilder 266 .setStatusCode( 267 statusProtoToResultCode(initializeResultProto.getStatus())) 268 // TODO(b/173532925) how to get DeSyncs value 269 .setHasDeSync(false); 270 AppSearchLoggerHelper.copyNativeStats( 271 initializeResultProto.getInitializeStats(), initStatsBuilder); 272 } 273 checkSuccess(initializeResultProto.getStatus()); 274 275 // Read all protos we need to construct AppSearchImpl's cache maps 276 long prepareSchemaAndNamespacesLatencyStartMillis = SystemClock.elapsedRealtime(); 277 SchemaProto schemaProto = getSchemaProtoLocked(); 278 279 mLogUtil.piiTrace("init:getAllNamespaces, request"); 280 GetAllNamespacesResultProto getAllNamespacesResultProto = 281 mIcingSearchEngineLocked.getAllNamespaces(); 282 mLogUtil.piiTrace( 283 "init:getAllNamespaces, response", 284 getAllNamespacesResultProto.getNamespacesCount(), 285 getAllNamespacesResultProto); 286 287 StorageInfoProto storageInfoProto = getRawStorageInfoProto(); 288 289 // Log the time it took to read the data that goes into the cache maps 290 if (initStatsBuilder != null) { 291 // In case there is some error for getAllNamespaces, we can still 292 // set the latency for preparation. 293 // If there is no error, the value will be overridden by the actual one later. 294 initStatsBuilder 295 .setStatusCode( 296 statusProtoToResultCode( 297 getAllNamespacesResultProto.getStatus())) 298 .setPrepareSchemaAndNamespacesLatencyMillis( 299 (int) 300 (SystemClock.elapsedRealtime() 301 - prepareSchemaAndNamespacesLatencyStartMillis)); 302 } 303 checkSuccess(getAllNamespacesResultProto.getStatus()); 304 305 // Populate schema map 306 List<SchemaTypeConfigProto> schemaProtoTypesList = schemaProto.getTypesList(); 307 for (int i = 0; i < schemaProtoTypesList.size(); i++) { 308 SchemaTypeConfigProto schema = schemaProtoTypesList.get(i); 309 String prefixedSchemaType = schema.getSchemaType(); 310 addToMap(mSchemaMapLocked, getPrefix(prefixedSchemaType), schema); 311 } 312 313 // Populate namespace map 314 List<String> prefixedNamespaceList = 315 getAllNamespacesResultProto.getNamespacesList(); 316 for (int i = 0; i < prefixedNamespaceList.size(); i++) { 317 String prefixedNamespace = prefixedNamespaceList.get(i); 318 addToMap(mNamespaceMapLocked, getPrefix(prefixedNamespace), prefixedNamespace); 319 } 320 321 // Populate document count map 322 rebuildDocumentCountMapLocked(storageInfoProto); 323 324 // logging prepare_schema_and_namespaces latency 325 if (initStatsBuilder != null) { 326 initStatsBuilder.setPrepareSchemaAndNamespacesLatencyMillis( 327 (int) 328 (SystemClock.elapsedRealtime() 329 - prepareSchemaAndNamespacesLatencyStartMillis)); 330 } 331 332 mLogUtil.piiTrace("Init completed successfully"); 333 334 } catch (AppSearchException e) { 335 // Some error. Reset and see if it fixes it. 336 Log.e(TAG, "Error initializing, resetting IcingSearchEngine.", e); 337 if (initStatsBuilder != null) { 338 initStatsBuilder.setStatusCode(e.getResultCode()); 339 } 340 resetLocked(initStatsBuilder); 341 } 342 343 } finally { 344 mReadWriteLock.writeLock().unlock(); 345 } 346 } 347 348 @GuardedBy("mReadWriteLock") throwIfClosedLocked()349 private void throwIfClosedLocked() { 350 if (mClosedLocked) { 351 throw new IllegalStateException("Trying to use a closed AppSearchImpl instance."); 352 } 353 } 354 355 /** 356 * Persists data to disk and closes the instance. 357 * 358 * <p>This instance is no longer usable after it's been closed. Call {@link #create} to create a 359 * new, usable instance. 360 */ 361 @Override close()362 public void close() { 363 mReadWriteLock.writeLock().lock(); 364 try { 365 if (mClosedLocked) { 366 return; 367 } 368 persistToDisk(PersistType.Code.FULL); 369 mLogUtil.piiTrace("icingSearchEngine.close, request"); 370 mIcingSearchEngineLocked.close(); 371 mLogUtil.piiTrace("icingSearchEngine.close, response"); 372 mClosedLocked = true; 373 } catch (AppSearchException e) { 374 Log.w(TAG, "Error when closing AppSearchImpl.", e); 375 } finally { 376 mReadWriteLock.writeLock().unlock(); 377 } 378 } 379 380 /** 381 * Updates the AppSearch schema for this app. 382 * 383 * <p>This method belongs to mutate group. 384 * 385 * @param packageName The package name that owns the schemas. 386 * @param databaseName The name of the database where this schema lives. 387 * @param schemas Schemas to set for this app. 388 * @param visibilityStore If set, {@code schemasNotDisplayedBySystem} and {@code 389 * schemasVisibleToPackages} will be saved here if the schema is successfully applied. 390 * @param schemasNotDisplayedBySystem Schema types that should not be surfaced on platform 391 * surfaces. 392 * @param schemasVisibleToPackages Schema types that are visible to the specified packages. 393 * @param forceOverride Whether to force-apply the schema even if it is incompatible. Documents 394 * which do not comply with the new schema will be deleted. 395 * @param version The overall version number of the request. 396 * @return The response contains deleted schema types and incompatible schema types of this 397 * call. 398 * @throws AppSearchException On IcingSearchEngine error. If the status code is 399 * FAILED_PRECONDITION for the incompatible change, the exception will be converted to the 400 * SetSchemaResponse. 401 */ 402 @NonNull setSchema( @onNull String packageName, @NonNull String databaseName, @NonNull List<AppSearchSchema> schemas, @Nullable VisibilityStore visibilityStore, @NonNull List<String> schemasNotDisplayedBySystem, @NonNull Map<String, List<PackageIdentifier>> schemasVisibleToPackages, boolean forceOverride, int version)403 public SetSchemaResponse setSchema( 404 @NonNull String packageName, 405 @NonNull String databaseName, 406 @NonNull List<AppSearchSchema> schemas, 407 @Nullable VisibilityStore visibilityStore, 408 @NonNull List<String> schemasNotDisplayedBySystem, 409 @NonNull Map<String, List<PackageIdentifier>> schemasVisibleToPackages, 410 boolean forceOverride, 411 int version) 412 throws AppSearchException { 413 mReadWriteLock.writeLock().lock(); 414 try { 415 throwIfClosedLocked(); 416 417 SchemaProto.Builder existingSchemaBuilder = getSchemaProtoLocked().toBuilder(); 418 419 SchemaProto.Builder newSchemaBuilder = SchemaProto.newBuilder(); 420 for (int i = 0; i < schemas.size(); i++) { 421 AppSearchSchema schema = schemas.get(i); 422 SchemaTypeConfigProto schemaTypeProto = 423 SchemaToProtoConverter.toSchemaTypeConfigProto(schema, version); 424 newSchemaBuilder.addTypes(schemaTypeProto); 425 } 426 427 String prefix = createPrefix(packageName, databaseName); 428 // Combine the existing schema (which may have types from other prefixes) with this 429 // prefix's new schema. Modifies the existingSchemaBuilder. 430 RewrittenSchemaResults rewrittenSchemaResults = 431 rewriteSchema(prefix, existingSchemaBuilder, newSchemaBuilder.build()); 432 433 // Apply schema 434 SchemaProto finalSchema = existingSchemaBuilder.build(); 435 mLogUtil.piiTrace("setSchema, request", finalSchema.getTypesCount(), finalSchema); 436 SetSchemaResultProto setSchemaResultProto = 437 mIcingSearchEngineLocked.setSchema(finalSchema, forceOverride); 438 mLogUtil.piiTrace( 439 "setSchema, response", setSchemaResultProto.getStatus(), setSchemaResultProto); 440 441 // Determine whether it succeeded. 442 try { 443 checkSuccess(setSchemaResultProto.getStatus()); 444 } catch (AppSearchException e) { 445 // Swallow the exception for the incompatible change case. We will propagate 446 // those deleted schemas and incompatible types to the SetSchemaResponse. 447 boolean isFailedPrecondition = 448 setSchemaResultProto.getStatus().getCode() 449 == StatusProto.Code.FAILED_PRECONDITION; 450 boolean isIncompatible = 451 setSchemaResultProto.getDeletedSchemaTypesCount() > 0 452 || setSchemaResultProto.getIncompatibleSchemaTypesCount() > 0; 453 if (isFailedPrecondition && isIncompatible) { 454 return SetSchemaResponseToProtoConverter.toSetSchemaResponse( 455 setSchemaResultProto, prefix); 456 } else { 457 throw e; 458 } 459 } 460 461 // Update derived data structures. 462 for (SchemaTypeConfigProto schemaTypeConfigProto : 463 rewrittenSchemaResults.mRewrittenPrefixedTypes.values()) { 464 addToMap(mSchemaMapLocked, prefix, schemaTypeConfigProto); 465 } 466 467 for (String schemaType : rewrittenSchemaResults.mDeletedPrefixedTypes) { 468 removeFromMap(mSchemaMapLocked, prefix, schemaType); 469 } 470 471 if (visibilityStore != null) { 472 Set<String> prefixedSchemasNotDisplayedBySystem = 473 new ArraySet<>(schemasNotDisplayedBySystem.size()); 474 for (int i = 0; i < schemasNotDisplayedBySystem.size(); i++) { 475 prefixedSchemasNotDisplayedBySystem.add( 476 prefix + schemasNotDisplayedBySystem.get(i)); 477 } 478 479 Map<String, List<PackageIdentifier>> prefixedSchemasVisibleToPackages = 480 new ArrayMap<>(schemasVisibleToPackages.size()); 481 for (Map.Entry<String, List<PackageIdentifier>> entry : 482 schemasVisibleToPackages.entrySet()) { 483 prefixedSchemasVisibleToPackages.put(prefix + entry.getKey(), entry.getValue()); 484 } 485 486 visibilityStore.setVisibility( 487 packageName, 488 databaseName, 489 prefixedSchemasNotDisplayedBySystem, 490 prefixedSchemasVisibleToPackages); 491 } 492 493 return SetSchemaResponseToProtoConverter.toSetSchemaResponse( 494 setSchemaResultProto, prefix); 495 } finally { 496 mReadWriteLock.writeLock().unlock(); 497 } 498 } 499 500 /** 501 * Retrieves the AppSearch schema for this package name, database. 502 * 503 * <p>This method belongs to query group. 504 * 505 * @param packageName Package name that owns this schema 506 * @param databaseName The name of the database where this schema lives. 507 * @throws AppSearchException on IcingSearchEngine error. 508 */ 509 @NonNull getSchema(@onNull String packageName, @NonNull String databaseName)510 public GetSchemaResponse getSchema(@NonNull String packageName, @NonNull String databaseName) 511 throws AppSearchException { 512 mReadWriteLock.readLock().lock(); 513 try { 514 throwIfClosedLocked(); 515 516 SchemaProto fullSchema = getSchemaProtoLocked(); 517 518 String prefix = createPrefix(packageName, databaseName); 519 GetSchemaResponse.Builder responseBuilder = new GetSchemaResponse.Builder(); 520 for (int i = 0; i < fullSchema.getTypesCount(); i++) { 521 String typePrefix = getPrefix(fullSchema.getTypes(i).getSchemaType()); 522 if (!prefix.equals(typePrefix)) { 523 continue; 524 } 525 // Rewrite SchemaProto.types.schema_type 526 SchemaTypeConfigProto.Builder typeConfigBuilder = 527 fullSchema.getTypes(i).toBuilder(); 528 String newSchemaType = typeConfigBuilder.getSchemaType().substring(prefix.length()); 529 typeConfigBuilder.setSchemaType(newSchemaType); 530 531 // Rewrite SchemaProto.types.properties.schema_type 532 for (int propertyIdx = 0; 533 propertyIdx < typeConfigBuilder.getPropertiesCount(); 534 propertyIdx++) { 535 PropertyConfigProto.Builder propertyConfigBuilder = 536 typeConfigBuilder.getProperties(propertyIdx).toBuilder(); 537 if (!propertyConfigBuilder.getSchemaType().isEmpty()) { 538 String newPropertySchemaType = 539 propertyConfigBuilder.getSchemaType().substring(prefix.length()); 540 propertyConfigBuilder.setSchemaType(newPropertySchemaType); 541 typeConfigBuilder.setProperties(propertyIdx, propertyConfigBuilder); 542 } 543 } 544 545 AppSearchSchema schema = 546 SchemaToProtoConverter.toAppSearchSchema(typeConfigBuilder); 547 548 // TODO(b/183050495) find a place to store the version for the database, rather 549 // than read from a schema. 550 responseBuilder.setVersion(fullSchema.getTypes(i).getVersion()); 551 responseBuilder.addSchema(schema); 552 } 553 return responseBuilder.build(); 554 } finally { 555 mReadWriteLock.readLock().unlock(); 556 } 557 } 558 559 /** 560 * Retrieves the list of namespaces with at least one document for this package name, database. 561 * 562 * <p>This method belongs to query group. 563 * 564 * @param packageName Package name that owns this schema 565 * @param databaseName The name of the database where this schema lives. 566 * @throws AppSearchException on IcingSearchEngine error. 567 */ 568 @NonNull getNamespaces(@onNull String packageName, @NonNull String databaseName)569 public List<String> getNamespaces(@NonNull String packageName, @NonNull String databaseName) 570 throws AppSearchException { 571 mReadWriteLock.readLock().lock(); 572 try { 573 throwIfClosedLocked(); 574 mLogUtil.piiTrace("getAllNamespaces, request"); 575 // We can't just use mNamespaceMap here because we have no way to prune namespaces from 576 // mNamespaceMap when they have no more documents (e.g. after setting schema to empty or 577 // using deleteByQuery). 578 GetAllNamespacesResultProto getAllNamespacesResultProto = 579 mIcingSearchEngineLocked.getAllNamespaces(); 580 mLogUtil.piiTrace( 581 "getAllNamespaces, response", 582 getAllNamespacesResultProto.getNamespacesCount(), 583 getAllNamespacesResultProto); 584 checkSuccess(getAllNamespacesResultProto.getStatus()); 585 String prefix = createPrefix(packageName, databaseName); 586 List<String> results = new ArrayList<>(); 587 for (int i = 0; i < getAllNamespacesResultProto.getNamespacesCount(); i++) { 588 String prefixedNamespace = getAllNamespacesResultProto.getNamespaces(i); 589 if (prefixedNamespace.startsWith(prefix)) { 590 results.add(prefixedNamespace.substring(prefix.length())); 591 } 592 } 593 return results; 594 } finally { 595 mReadWriteLock.readLock().unlock(); 596 } 597 } 598 599 /** 600 * Adds a document to the AppSearch index. 601 * 602 * <p>This method belongs to mutate group. 603 * 604 * @param packageName The package name that owns this document. 605 * @param databaseName The databaseName this document resides in. 606 * @param document The document to index. 607 * @throws AppSearchException on IcingSearchEngine error. 608 */ putDocument( @onNull String packageName, @NonNull String databaseName, @NonNull GenericDocument document, @Nullable AppSearchLogger logger)609 public void putDocument( 610 @NonNull String packageName, 611 @NonNull String databaseName, 612 @NonNull GenericDocument document, 613 @Nullable AppSearchLogger logger) 614 throws AppSearchException { 615 PutDocumentStats.Builder pStatsBuilder = null; 616 if (logger != null) { 617 pStatsBuilder = new PutDocumentStats.Builder(packageName, databaseName); 618 } 619 long totalStartTimeMillis = SystemClock.elapsedRealtime(); 620 621 mReadWriteLock.writeLock().lock(); 622 try { 623 throwIfClosedLocked(); 624 625 // Generate Document Proto 626 long generateDocumentProtoStartTimeMillis = SystemClock.elapsedRealtime(); 627 DocumentProto.Builder documentBuilder = 628 GenericDocumentToProtoConverter.toDocumentProto(document).toBuilder(); 629 long generateDocumentProtoEndTimeMillis = SystemClock.elapsedRealtime(); 630 631 // Rewrite Document Type 632 long rewriteDocumentTypeStartTimeMillis = SystemClock.elapsedRealtime(); 633 String prefix = createPrefix(packageName, databaseName); 634 addPrefixToDocument(documentBuilder, prefix); 635 long rewriteDocumentTypeEndTimeMillis = SystemClock.elapsedRealtime(); 636 DocumentProto finalDocument = documentBuilder.build(); 637 638 // Check limits 639 int newDocumentCount = 640 enforceLimitConfigLocked( 641 packageName, finalDocument.getUri(), finalDocument.getSerializedSize()); 642 643 // Insert document 644 mLogUtil.piiTrace("putDocument, request", finalDocument.getUri(), finalDocument); 645 PutResultProto putResultProto = mIcingSearchEngineLocked.put(finalDocument); 646 mLogUtil.piiTrace("putDocument, response", putResultProto.getStatus(), putResultProto); 647 648 // Update caches 649 addToMap(mNamespaceMapLocked, prefix, finalDocument.getNamespace()); 650 mDocumentCountMapLocked.put(packageName, newDocumentCount); 651 652 // Logging stats 653 if (pStatsBuilder != null) { 654 pStatsBuilder 655 .setStatusCode(statusProtoToResultCode(putResultProto.getStatus())) 656 .setGenerateDocumentProtoLatencyMillis( 657 (int) 658 (generateDocumentProtoEndTimeMillis 659 - generateDocumentProtoStartTimeMillis)) 660 .setRewriteDocumentTypesLatencyMillis( 661 (int) 662 (rewriteDocumentTypeEndTimeMillis 663 - rewriteDocumentTypeStartTimeMillis)); 664 AppSearchLoggerHelper.copyNativeStats( 665 putResultProto.getPutDocumentStats(), pStatsBuilder); 666 } 667 668 checkSuccess(putResultProto.getStatus()); 669 } finally { 670 mReadWriteLock.writeLock().unlock(); 671 672 if (logger != null) { 673 long totalEndTimeMillis = SystemClock.elapsedRealtime(); 674 pStatsBuilder.setTotalLatencyMillis( 675 (int) (totalEndTimeMillis - totalStartTimeMillis)); 676 logger.logStats(pStatsBuilder.build()); 677 } 678 } 679 } 680 681 /** 682 * Checks that a new document can be added to the given packageName with the given serialized 683 * size without violating our {@link LimitConfig}. 684 * 685 * @return the new count of documents for the given package, including the new document. 686 * @throws AppSearchException with a code of {@link AppSearchResult#RESULT_OUT_OF_SPACE} if the 687 * limits are violated by the new document. 688 */ 689 @GuardedBy("mReadWriteLock") enforceLimitConfigLocked(String packageName, String newDocUri, int newDocSize)690 private int enforceLimitConfigLocked(String packageName, String newDocUri, int newDocSize) 691 throws AppSearchException { 692 // Limits check: size of document 693 if (newDocSize > mLimitConfig.getMaxDocumentSizeBytes()) { 694 throw new AppSearchException( 695 AppSearchResult.RESULT_OUT_OF_SPACE, 696 "Document \"" 697 + newDocUri 698 + "\" for package \"" 699 + packageName 700 + "\" serialized to " 701 + newDocSize 702 + " bytes, which exceeds " 703 + "limit of " 704 + mLimitConfig.getMaxDocumentSizeBytes() 705 + " bytes"); 706 } 707 708 // Limits check: number of documents 709 Integer oldDocumentCount = mDocumentCountMapLocked.get(packageName); 710 int newDocumentCount; 711 if (oldDocumentCount == null) { 712 newDocumentCount = 1; 713 } else { 714 newDocumentCount = oldDocumentCount + 1; 715 } 716 if (newDocumentCount > mLimitConfig.getMaxDocumentCount()) { 717 // Our management of mDocumentCountMapLocked doesn't account for document 718 // replacements, so our counter might have overcounted if the app has replaced docs. 719 // Rebuild the counter from StorageInfo in case this is so. 720 // TODO(b/170371356): If Icing lib exposes something in the result which says 721 // whether the document was a replacement, we could subtract 1 again after the put 722 // to keep the count accurate. That would allow us to remove this code. 723 rebuildDocumentCountMapLocked(getRawStorageInfoProto()); 724 oldDocumentCount = mDocumentCountMapLocked.get(packageName); 725 if (oldDocumentCount == null) { 726 newDocumentCount = 1; 727 } else { 728 newDocumentCount = oldDocumentCount + 1; 729 } 730 } 731 if (newDocumentCount > mLimitConfig.getMaxDocumentCount()) { 732 // Now we really can't fit it in, even accounting for replacements. 733 throw new AppSearchException( 734 AppSearchResult.RESULT_OUT_OF_SPACE, 735 "Package \"" 736 + packageName 737 + "\" exceeded limit of " 738 + mLimitConfig.getMaxDocumentCount() 739 + " documents. Some documents " 740 + "must be removed to index additional ones."); 741 } 742 743 return newDocumentCount; 744 } 745 746 /** 747 * Retrieves a document from the AppSearch index by namespace and document ID. 748 * 749 * <p>This method belongs to query group. 750 * 751 * @param packageName The package that owns this document. 752 * @param databaseName The databaseName this document resides in. 753 * @param namespace The namespace this document resides in. 754 * @param id The ID of the document to get. 755 * @param typePropertyPaths A map of schema type to a list of property paths to return in the 756 * result. 757 * @return The Document contents 758 * @throws AppSearchException on IcingSearchEngine error. 759 */ 760 @NonNull getDocument( @onNull String packageName, @NonNull String databaseName, @NonNull String namespace, @NonNull String id, @NonNull Map<String, List<String>> typePropertyPaths)761 public GenericDocument getDocument( 762 @NonNull String packageName, 763 @NonNull String databaseName, 764 @NonNull String namespace, 765 @NonNull String id, 766 @NonNull Map<String, List<String>> typePropertyPaths) 767 throws AppSearchException { 768 mReadWriteLock.readLock().lock(); 769 try { 770 throwIfClosedLocked(); 771 String prefix = createPrefix(packageName, databaseName); 772 List<TypePropertyMask> nonPrefixedPropertyMasks = 773 TypePropertyPathToProtoConverter.toTypePropertyMaskList(typePropertyPaths); 774 List<TypePropertyMask> prefixedPropertyMasks = 775 new ArrayList<>(nonPrefixedPropertyMasks.size()); 776 for (int i = 0; i < nonPrefixedPropertyMasks.size(); ++i) { 777 TypePropertyMask typePropertyMask = nonPrefixedPropertyMasks.get(i); 778 String nonPrefixedType = typePropertyMask.getSchemaType(); 779 String prefixedType = 780 nonPrefixedType.equals( 781 GetByDocumentIdRequest.PROJECTION_SCHEMA_TYPE_WILDCARD) 782 ? nonPrefixedType 783 : prefix + nonPrefixedType; 784 prefixedPropertyMasks.add( 785 typePropertyMask.toBuilder().setSchemaType(prefixedType).build()); 786 } 787 GetResultSpecProto getResultSpec = 788 GetResultSpecProto.newBuilder() 789 .addAllTypePropertyMasks(prefixedPropertyMasks) 790 .build(); 791 792 String finalNamespace = createPrefix(packageName, databaseName) + namespace; 793 if (mLogUtil.isPiiTraceEnabled()) { 794 mLogUtil.piiTrace( 795 "getDocument, request", finalNamespace + ", " + id + "," + getResultSpec); 796 } 797 GetResultProto getResultProto = 798 mIcingSearchEngineLocked.get(finalNamespace, id, getResultSpec); 799 mLogUtil.piiTrace("getDocument, response", getResultProto.getStatus(), getResultProto); 800 checkSuccess(getResultProto.getStatus()); 801 802 // The schema type map cannot be null at this point. It could only be null if no 803 // schema had ever been set for that prefix. Given we have retrieved a document from 804 // the index, we know a schema had to have been set. 805 Map<String, SchemaTypeConfigProto> schemaTypeMap = mSchemaMapLocked.get(prefix); 806 DocumentProto.Builder documentBuilder = getResultProto.getDocument().toBuilder(); 807 removePrefixesFromDocument(documentBuilder); 808 return GenericDocumentToProtoConverter.toGenericDocument( 809 documentBuilder.build(), prefix, schemaTypeMap); 810 } finally { 811 mReadWriteLock.readLock().unlock(); 812 } 813 } 814 815 /** 816 * Executes a query against the AppSearch index and returns results. 817 * 818 * <p>This method belongs to query group. 819 * 820 * @param packageName The package name that is performing the query. 821 * @param databaseName The databaseName this query for. 822 * @param queryExpression Query String to search. 823 * @param searchSpec Spec for setting filters, raw query etc. 824 * @param logger logger to collect query stats 825 * @return The results of performing this search. It may contain an empty list of results if no 826 * documents matched the query. 827 * @throws AppSearchException on IcingSearchEngine error. 828 */ 829 @NonNull query( @onNull String packageName, @NonNull String databaseName, @NonNull String queryExpression, @NonNull SearchSpec searchSpec, @Nullable AppSearchLogger logger)830 public SearchResultPage query( 831 @NonNull String packageName, 832 @NonNull String databaseName, 833 @NonNull String queryExpression, 834 @NonNull SearchSpec searchSpec, 835 @Nullable AppSearchLogger logger) 836 throws AppSearchException { 837 long totalLatencyStartMillis = SystemClock.elapsedRealtime(); 838 SearchStats.Builder sStatsBuilder = null; 839 if (logger != null) { 840 sStatsBuilder = 841 new SearchStats.Builder(SearchStats.VISIBILITY_SCOPE_LOCAL, packageName) 842 .setDatabase(databaseName); 843 } 844 845 mReadWriteLock.readLock().lock(); 846 try { 847 throwIfClosedLocked(); 848 849 List<String> filterPackageNames = searchSpec.getFilterPackageNames(); 850 if (!filterPackageNames.isEmpty() && !filterPackageNames.contains(packageName)) { 851 // Client wanted to query over some packages that weren't its own. This isn't 852 // allowed through local query so we can return early with no results. 853 if (logger != null) { 854 sStatsBuilder.setStatusCode(AppSearchResult.RESULT_SECURITY_ERROR); 855 } 856 return new SearchResultPage(Bundle.EMPTY); 857 } 858 859 String prefix = createPrefix(packageName, databaseName); 860 Set<String> allowedPrefixedSchemas = getAllowedPrefixSchemasLocked(prefix, searchSpec); 861 862 SearchResultPage searchResultPage = 863 doQueryLocked( 864 Collections.singleton(createPrefix(packageName, databaseName)), 865 allowedPrefixedSchemas, 866 queryExpression, 867 searchSpec, 868 sStatsBuilder); 869 addNextPageToken(packageName, searchResultPage.getNextPageToken()); 870 return searchResultPage; 871 } finally { 872 mReadWriteLock.readLock().unlock(); 873 if (logger != null) { 874 sStatsBuilder.setTotalLatencyMillis( 875 (int) (SystemClock.elapsedRealtime() - totalLatencyStartMillis)); 876 logger.logStats(sStatsBuilder.build()); 877 } 878 } 879 } 880 881 /** 882 * Executes a global query, i.e. over all permitted prefixes, against the AppSearch index and 883 * returns results. 884 * 885 * <p>This method belongs to query group. 886 * 887 * @param queryExpression Query String to search. 888 * @param searchSpec Spec for setting filters, raw query etc. 889 * @param callerPackageName Package name of the caller, should belong to the {@code 890 * callerUserHandle}. 891 * @param visibilityStore Optional visibility store to obtain system and package visibility 892 * settings from 893 * @param callerUid UID of the client making the globalQuery call. 894 * @param callerHasSystemAccess Whether the caller has been positively identified as having 895 * access to schemas marked system surfaceable. 896 * @param logger logger to collect globalQuery stats 897 * @return The results of performing this search. It may contain an empty list of results if no 898 * documents matched the query. 899 * @throws AppSearchException on IcingSearchEngine error. 900 */ 901 @NonNull globalQuery( @onNull String queryExpression, @NonNull SearchSpec searchSpec, @NonNull String callerPackageName, @Nullable VisibilityStore visibilityStore, int callerUid, boolean callerHasSystemAccess, @Nullable AppSearchLogger logger)902 public SearchResultPage globalQuery( 903 @NonNull String queryExpression, 904 @NonNull SearchSpec searchSpec, 905 @NonNull String callerPackageName, 906 @Nullable VisibilityStore visibilityStore, 907 int callerUid, 908 boolean callerHasSystemAccess, 909 @Nullable AppSearchLogger logger) 910 throws AppSearchException { 911 long totalLatencyStartMillis = SystemClock.elapsedRealtime(); 912 SearchStats.Builder sStatsBuilder = null; 913 if (logger != null) { 914 sStatsBuilder = 915 new SearchStats.Builder(SearchStats.VISIBILITY_SCOPE_GLOBAL, callerPackageName); 916 } 917 918 mReadWriteLock.readLock().lock(); 919 try { 920 throwIfClosedLocked(); 921 922 // Convert package filters to prefix filters 923 Set<String> packageFilters = new ArraySet<>(searchSpec.getFilterPackageNames()); 924 Set<String> prefixFilters = new ArraySet<>(); 925 if (packageFilters.isEmpty()) { 926 // Client didn't restrict their search over packages. Try to query over all 927 // packages/prefixes 928 prefixFilters = mNamespaceMapLocked.keySet(); 929 } else { 930 // Client did restrict their search over packages. Only include the prefixes that 931 // belong to the specified packages. 932 for (String prefix : mNamespaceMapLocked.keySet()) { 933 String packageName = getPackageName(prefix); 934 if (packageFilters.contains(packageName)) { 935 prefixFilters.add(prefix); 936 } 937 } 938 } 939 940 // Convert schema filters to prefixed schema filters 941 ArraySet<String> prefixedSchemaFilters = new ArraySet<>(); 942 for (String prefix : prefixFilters) { 943 List<String> schemaFilters = searchSpec.getFilterSchemas(); 944 if (schemaFilters.isEmpty()) { 945 // Client didn't specify certain schemas to search over, check all schemas 946 prefixedSchemaFilters.addAll(mSchemaMapLocked.get(prefix).keySet()); 947 } else { 948 // Client specified some schemas to search over, check each one 949 for (int i = 0; i < schemaFilters.size(); i++) { 950 prefixedSchemaFilters.add(prefix + schemaFilters.get(i)); 951 } 952 } 953 } 954 955 // Remove the schemas the client is not allowed to search over 956 Iterator<String> prefixedSchemaIt = prefixedSchemaFilters.iterator(); 957 while (prefixedSchemaIt.hasNext()) { 958 String prefixedSchema = prefixedSchemaIt.next(); 959 String packageName = getPackageName(prefixedSchema); 960 961 boolean allow; 962 if (packageName.equals(callerPackageName)) { 963 // Callers can always retrieve their own data 964 allow = true; 965 } else if (visibilityStore == null) { 966 // If there's no visibility store, there's no extra access 967 allow = false; 968 } else { 969 String databaseName = getDatabaseName(prefixedSchema); 970 allow = 971 visibilityStore.isSchemaSearchableByCaller( 972 packageName, 973 databaseName, 974 prefixedSchema, 975 callerUid, 976 callerHasSystemAccess); 977 } 978 979 if (!allow) { 980 prefixedSchemaIt.remove(); 981 } 982 } 983 984 SearchResultPage searchResultPage = 985 doQueryLocked( 986 prefixFilters, 987 prefixedSchemaFilters, 988 queryExpression, 989 searchSpec, 990 sStatsBuilder); 991 addNextPageToken(callerPackageName, searchResultPage.getNextPageToken()); 992 return searchResultPage; 993 } finally { 994 mReadWriteLock.readLock().unlock(); 995 996 if (logger != null) { 997 sStatsBuilder.setTotalLatencyMillis( 998 (int) (SystemClock.elapsedRealtime() - totalLatencyStartMillis)); 999 logger.logStats(sStatsBuilder.build()); 1000 } 1001 } 1002 } 1003 1004 /** 1005 * Returns a mapping of package names to all the databases owned by that package. 1006 * 1007 * <p>This method is inefficient to call repeatedly. 1008 */ 1009 @NonNull getPackageToDatabases()1010 public Map<String, Set<String>> getPackageToDatabases() { 1011 mReadWriteLock.readLock().lock(); 1012 try { 1013 Map<String, Set<String>> packageToDatabases = new ArrayMap<>(); 1014 for (String prefix : mSchemaMapLocked.keySet()) { 1015 String packageName = getPackageName(prefix); 1016 1017 Set<String> databases = packageToDatabases.get(packageName); 1018 if (databases == null) { 1019 databases = new ArraySet<>(); 1020 packageToDatabases.put(packageName, databases); 1021 } 1022 1023 String databaseName = getDatabaseName(prefix); 1024 databases.add(databaseName); 1025 } 1026 1027 return packageToDatabases; 1028 } finally { 1029 mReadWriteLock.readLock().unlock(); 1030 } 1031 } 1032 1033 @GuardedBy("mReadWriteLock") doQueryLocked( @onNull Set<String> prefixes, @NonNull Set<String> allowedPrefixedSchemas, @NonNull String queryExpression, @NonNull SearchSpec searchSpec, @Nullable SearchStats.Builder sStatsBuilder)1034 private SearchResultPage doQueryLocked( 1035 @NonNull Set<String> prefixes, 1036 @NonNull Set<String> allowedPrefixedSchemas, 1037 @NonNull String queryExpression, 1038 @NonNull SearchSpec searchSpec, 1039 @Nullable SearchStats.Builder sStatsBuilder) 1040 throws AppSearchException { 1041 long rewriteSearchSpecLatencyStartMillis = SystemClock.elapsedRealtime(); 1042 1043 SearchSpecProto.Builder searchSpecBuilder = 1044 SearchSpecToProtoConverter.toSearchSpecProto(searchSpec).toBuilder() 1045 .setQuery(queryExpression); 1046 // rewriteSearchSpecForPrefixesLocked will return false if there is nothing to search 1047 // over given their search filters, so we can return an empty SearchResult and skip 1048 // sending request to Icing. 1049 if (!rewriteSearchSpecForPrefixesLocked( 1050 searchSpecBuilder, prefixes, allowedPrefixedSchemas)) { 1051 if (sStatsBuilder != null) { 1052 sStatsBuilder.setRewriteSearchSpecLatencyMillis( 1053 (int) 1054 (SystemClock.elapsedRealtime() 1055 - rewriteSearchSpecLatencyStartMillis)); 1056 } 1057 return new SearchResultPage(Bundle.EMPTY); 1058 } 1059 1060 // rewriteSearchSpec, rewriteResultSpec and convertScoringSpec are all counted in 1061 // rewriteSearchSpecLatencyMillis 1062 ResultSpecProto.Builder resultSpecBuilder = 1063 SearchSpecToProtoConverter.toResultSpecProto(searchSpec).toBuilder(); 1064 1065 int groupingType = searchSpec.getResultGroupingTypeFlags(); 1066 if ((groupingType & SearchSpec.GROUPING_TYPE_PER_PACKAGE) != 0 1067 && (groupingType & SearchSpec.GROUPING_TYPE_PER_NAMESPACE) != 0) { 1068 addPerPackagePerNamespaceResultGroupingsLocked( 1069 resultSpecBuilder, prefixes, searchSpec.getResultGroupingLimit()); 1070 } else if ((groupingType & SearchSpec.GROUPING_TYPE_PER_PACKAGE) != 0) { 1071 addPerPackageResultGroupingsLocked( 1072 resultSpecBuilder, prefixes, searchSpec.getResultGroupingLimit()); 1073 } else if ((groupingType & SearchSpec.GROUPING_TYPE_PER_NAMESPACE) != 0) { 1074 addPerNamespaceResultGroupingsLocked( 1075 resultSpecBuilder, prefixes, searchSpec.getResultGroupingLimit()); 1076 } 1077 1078 rewriteResultSpecForPrefixesLocked(resultSpecBuilder, prefixes, allowedPrefixedSchemas); 1079 ScoringSpecProto scoringSpec = SearchSpecToProtoConverter.toScoringSpecProto(searchSpec); 1080 SearchSpecProto finalSearchSpec = searchSpecBuilder.build(); 1081 ResultSpecProto finalResultSpec = resultSpecBuilder.build(); 1082 1083 long rewriteSearchSpecLatencyEndMillis = SystemClock.elapsedRealtime(); 1084 1085 if (mLogUtil.isPiiTraceEnabled()) { 1086 mLogUtil.piiTrace( 1087 "search, request", 1088 finalSearchSpec.getQuery(), 1089 finalSearchSpec + ", " + scoringSpec + ", " + finalResultSpec); 1090 } 1091 SearchResultProto searchResultProto = 1092 mIcingSearchEngineLocked.search(finalSearchSpec, scoringSpec, finalResultSpec); 1093 mLogUtil.piiTrace( 1094 "search, response", searchResultProto.getResultsCount(), searchResultProto); 1095 1096 if (sStatsBuilder != null) { 1097 sStatsBuilder 1098 .setStatusCode(statusProtoToResultCode(searchResultProto.getStatus())) 1099 .setRewriteSearchSpecLatencyMillis( 1100 (int) 1101 (rewriteSearchSpecLatencyEndMillis 1102 - rewriteSearchSpecLatencyStartMillis)); 1103 AppSearchLoggerHelper.copyNativeStats(searchResultProto.getQueryStats(), sStatsBuilder); 1104 } 1105 1106 checkSuccess(searchResultProto.getStatus()); 1107 1108 long rewriteSearchResultLatencyStartMillis = SystemClock.elapsedRealtime(); 1109 SearchResultPage resultPage = rewriteSearchResultProto(searchResultProto, mSchemaMapLocked); 1110 if (sStatsBuilder != null) { 1111 sStatsBuilder.setRewriteSearchResultLatencyMillis( 1112 (int) (SystemClock.elapsedRealtime() - rewriteSearchResultLatencyStartMillis)); 1113 } 1114 1115 return resultPage; 1116 } 1117 1118 /** 1119 * Fetches the next page of results of a previously executed query. Results can be empty if 1120 * next-page token is invalid or all pages have been returned. 1121 * 1122 * <p>This method belongs to query group. 1123 * 1124 * @param packageName Package name of the caller. 1125 * @param nextPageToken The token of pre-loaded results of previously executed query. 1126 * @return The next page of results of previously executed query. 1127 * @throws AppSearchException on IcingSearchEngine error or if can't advance on nextPageToken. 1128 */ 1129 @NonNull getNextPage(@onNull String packageName, long nextPageToken)1130 public SearchResultPage getNextPage(@NonNull String packageName, long nextPageToken) 1131 throws AppSearchException { 1132 mReadWriteLock.readLock().lock(); 1133 try { 1134 throwIfClosedLocked(); 1135 1136 mLogUtil.piiTrace("getNextPage, request", nextPageToken); 1137 checkNextPageToken(packageName, nextPageToken); 1138 SearchResultProto searchResultProto = 1139 mIcingSearchEngineLocked.getNextPage(nextPageToken); 1140 mLogUtil.piiTrace( 1141 "getNextPage, response", 1142 searchResultProto.getResultsCount(), 1143 searchResultProto); 1144 checkSuccess(searchResultProto.getStatus()); 1145 if (nextPageToken != EMPTY_PAGE_TOKEN 1146 && searchResultProto.getNextPageToken() == EMPTY_PAGE_TOKEN) { 1147 // At this point, we're guaranteed that this nextPageToken exists for this package, 1148 // otherwise checkNextPageToken would've thrown an exception. 1149 // Since the new token is 0, this is the last page. We should remove the old token 1150 // from our cache since it no longer refers to this query. 1151 synchronized (mNextPageTokensLocked) { 1152 mNextPageTokensLocked.get(packageName).remove(nextPageToken); 1153 } 1154 } 1155 return rewriteSearchResultProto(searchResultProto, mSchemaMapLocked); 1156 } finally { 1157 mReadWriteLock.readLock().unlock(); 1158 } 1159 } 1160 1161 /** 1162 * Invalidates the next-page token so that no more results of the related query can be returned. 1163 * 1164 * <p>This method belongs to query group. 1165 * 1166 * @param packageName Package name of the caller. 1167 * @param nextPageToken The token of pre-loaded results of previously executed query to be 1168 * Invalidated. 1169 * @throws AppSearchException if nextPageToken is unusable. 1170 */ invalidateNextPageToken(@onNull String packageName, long nextPageToken)1171 public void invalidateNextPageToken(@NonNull String packageName, long nextPageToken) 1172 throws AppSearchException { 1173 mReadWriteLock.readLock().lock(); 1174 try { 1175 throwIfClosedLocked(); 1176 1177 mLogUtil.piiTrace("invalidateNextPageToken, request", nextPageToken); 1178 checkNextPageToken(packageName, nextPageToken); 1179 mIcingSearchEngineLocked.invalidateNextPageToken(nextPageToken); 1180 1181 synchronized (mNextPageTokensLocked) { 1182 // At this point, we're guaranteed that this nextPageToken exists for this package, 1183 // otherwise checkNextPageToken would've thrown an exception. 1184 mNextPageTokensLocked.get(packageName).remove(nextPageToken); 1185 } 1186 } finally { 1187 mReadWriteLock.readLock().unlock(); 1188 } 1189 } 1190 1191 /** Reports a usage of the given document at the given timestamp. */ reportUsage( @onNull String packageName, @NonNull String databaseName, @NonNull String namespace, @NonNull String documentId, long usageTimestampMillis, boolean systemUsage)1192 public void reportUsage( 1193 @NonNull String packageName, 1194 @NonNull String databaseName, 1195 @NonNull String namespace, 1196 @NonNull String documentId, 1197 long usageTimestampMillis, 1198 boolean systemUsage) 1199 throws AppSearchException { 1200 mReadWriteLock.writeLock().lock(); 1201 try { 1202 throwIfClosedLocked(); 1203 1204 String prefixedNamespace = createPrefix(packageName, databaseName) + namespace; 1205 UsageReport.UsageType usageType = 1206 systemUsage 1207 ? UsageReport.UsageType.USAGE_TYPE2 1208 : UsageReport.UsageType.USAGE_TYPE1; 1209 UsageReport report = 1210 UsageReport.newBuilder() 1211 .setDocumentNamespace(prefixedNamespace) 1212 .setDocumentUri(documentId) 1213 .setUsageTimestampMs(usageTimestampMillis) 1214 .setUsageType(usageType) 1215 .build(); 1216 1217 mLogUtil.piiTrace("reportUsage, request", report.getDocumentUri(), report); 1218 ReportUsageResultProto result = mIcingSearchEngineLocked.reportUsage(report); 1219 mLogUtil.piiTrace("reportUsage, response", result.getStatus(), result); 1220 checkSuccess(result.getStatus()); 1221 } finally { 1222 mReadWriteLock.writeLock().unlock(); 1223 } 1224 } 1225 1226 /** 1227 * Removes the given document by id. 1228 * 1229 * <p>This method belongs to mutate group. 1230 * 1231 * @param packageName The package name that owns the document. 1232 * @param databaseName The databaseName the document is in. 1233 * @param namespace Namespace of the document to remove. 1234 * @param id ID of the document to remove. 1235 * @param removeStatsBuilder builder for {@link RemoveStats} to hold stats for remove 1236 * @throws AppSearchException on IcingSearchEngine error. 1237 */ remove( @onNull String packageName, @NonNull String databaseName, @NonNull String namespace, @NonNull String id, @Nullable RemoveStats.Builder removeStatsBuilder)1238 public void remove( 1239 @NonNull String packageName, 1240 @NonNull String databaseName, 1241 @NonNull String namespace, 1242 @NonNull String id, 1243 @Nullable RemoveStats.Builder removeStatsBuilder) 1244 throws AppSearchException { 1245 long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime(); 1246 mReadWriteLock.writeLock().lock(); 1247 try { 1248 throwIfClosedLocked(); 1249 1250 String prefixedNamespace = createPrefix(packageName, databaseName) + namespace; 1251 if (mLogUtil.isPiiTraceEnabled()) { 1252 mLogUtil.piiTrace("removeById, request", prefixedNamespace + ", " + id); 1253 } 1254 DeleteResultProto deleteResultProto = 1255 mIcingSearchEngineLocked.delete(prefixedNamespace, id); 1256 mLogUtil.piiTrace( 1257 "removeById, response", deleteResultProto.getStatus(), deleteResultProto); 1258 1259 if (removeStatsBuilder != null) { 1260 removeStatsBuilder.setStatusCode( 1261 statusProtoToResultCode(deleteResultProto.getStatus())); 1262 AppSearchLoggerHelper.copyNativeStats( 1263 deleteResultProto.getDeleteStats(), removeStatsBuilder); 1264 } 1265 checkSuccess(deleteResultProto.getStatus()); 1266 1267 // Update derived maps 1268 updateDocumentCountAfterRemovalLocked(packageName, /*numDocumentsDeleted=*/ 1); 1269 } finally { 1270 mReadWriteLock.writeLock().unlock(); 1271 if (removeStatsBuilder != null) { 1272 removeStatsBuilder.setTotalLatencyMillis( 1273 (int) (SystemClock.elapsedRealtime() - totalLatencyStartTimeMillis)); 1274 } 1275 } 1276 } 1277 1278 /** 1279 * Removes documents by given query. 1280 * 1281 * <p>This method belongs to mutate group. 1282 * 1283 * @param packageName The package name that owns the documents. 1284 * @param databaseName The databaseName the document is in. 1285 * @param queryExpression Query String to search. 1286 * @param searchSpec Defines what and how to remove 1287 * @param removeStatsBuilder builder for {@link RemoveStats} to hold stats for remove 1288 * @throws AppSearchException on IcingSearchEngine error. 1289 */ removeByQuery( @onNull String packageName, @NonNull String databaseName, @NonNull String queryExpression, @NonNull SearchSpec searchSpec, @Nullable RemoveStats.Builder removeStatsBuilder)1290 public void removeByQuery( 1291 @NonNull String packageName, 1292 @NonNull String databaseName, 1293 @NonNull String queryExpression, 1294 @NonNull SearchSpec searchSpec, 1295 @Nullable RemoveStats.Builder removeStatsBuilder) 1296 throws AppSearchException { 1297 long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime(); 1298 mReadWriteLock.writeLock().lock(); 1299 try { 1300 throwIfClosedLocked(); 1301 1302 List<String> filterPackageNames = searchSpec.getFilterPackageNames(); 1303 if (!filterPackageNames.isEmpty() && !filterPackageNames.contains(packageName)) { 1304 // We're only removing documents within the parameter `packageName`. If we're not 1305 // restricting our remove-query to this package name, then there's nothing for us to 1306 // remove. 1307 return; 1308 } 1309 1310 SearchSpecProto searchSpecProto = 1311 SearchSpecToProtoConverter.toSearchSpecProto(searchSpec); 1312 SearchSpecProto.Builder searchSpecBuilder = 1313 searchSpecProto.toBuilder().setQuery(queryExpression); 1314 1315 String prefix = createPrefix(packageName, databaseName); 1316 Set<String> allowedPrefixedSchemas = getAllowedPrefixSchemasLocked(prefix, searchSpec); 1317 1318 // rewriteSearchSpecForPrefixesLocked will return false if there is nothing to search 1319 // over given their search filters, so we can return early and skip sending request 1320 // to Icing. 1321 if (!rewriteSearchSpecForPrefixesLocked( 1322 searchSpecBuilder, Collections.singleton(prefix), allowedPrefixedSchemas)) { 1323 return; 1324 } 1325 SearchSpecProto finalSearchSpec = searchSpecBuilder.build(); 1326 mLogUtil.piiTrace("removeByQuery, request", finalSearchSpec); 1327 DeleteByQueryResultProto deleteResultProto = 1328 mIcingSearchEngineLocked.deleteByQuery(finalSearchSpec); 1329 mLogUtil.piiTrace( 1330 "removeByQuery, response", deleteResultProto.getStatus(), deleteResultProto); 1331 1332 if (removeStatsBuilder != null) { 1333 removeStatsBuilder.setStatusCode( 1334 statusProtoToResultCode(deleteResultProto.getStatus())); 1335 // TODO(b/187206766) also log query stats here once IcingLib returns it 1336 AppSearchLoggerHelper.copyNativeStats( 1337 deleteResultProto.getDeleteStats(), removeStatsBuilder); 1338 } 1339 1340 // It seems that the caller wants to get success if the data matching the query is 1341 // not in the DB because it was not there or was successfully deleted. 1342 checkCodeOneOf( 1343 deleteResultProto.getStatus(), StatusProto.Code.OK, StatusProto.Code.NOT_FOUND); 1344 1345 // Update derived maps 1346 int numDocumentsDeleted = deleteResultProto.getDeleteStats().getNumDocumentsDeleted(); 1347 updateDocumentCountAfterRemovalLocked(packageName, numDocumentsDeleted); 1348 } finally { 1349 mReadWriteLock.writeLock().unlock(); 1350 if (removeStatsBuilder != null) { 1351 removeStatsBuilder.setTotalLatencyMillis( 1352 (int) (SystemClock.elapsedRealtime() - totalLatencyStartTimeMillis)); 1353 } 1354 } 1355 } 1356 1357 @GuardedBy("mReadWriteLock") updateDocumentCountAfterRemovalLocked( @onNull String packageName, int numDocumentsDeleted)1358 private void updateDocumentCountAfterRemovalLocked( 1359 @NonNull String packageName, int numDocumentsDeleted) { 1360 if (numDocumentsDeleted > 0) { 1361 Integer oldDocumentCount = mDocumentCountMapLocked.get(packageName); 1362 // This should always be true: how can we delete documents for a package without 1363 // having seen that package during init? This is just a safeguard. 1364 if (oldDocumentCount != null) { 1365 // This should always be >0; how can we remove more documents than we've indexed? 1366 // This is just a safeguard. 1367 int newDocumentCount = Math.max(oldDocumentCount - numDocumentsDeleted, 0); 1368 mDocumentCountMapLocked.put(packageName, newDocumentCount); 1369 } 1370 } 1371 } 1372 1373 /** Estimates the storage usage info for a specific package. */ 1374 @NonNull getStorageInfoForPackage(@onNull String packageName)1375 public StorageInfo getStorageInfoForPackage(@NonNull String packageName) 1376 throws AppSearchException { 1377 mReadWriteLock.readLock().lock(); 1378 try { 1379 throwIfClosedLocked(); 1380 1381 Map<String, Set<String>> packageToDatabases = getPackageToDatabases(); 1382 Set<String> databases = packageToDatabases.get(packageName); 1383 if (databases == null) { 1384 // Package doesn't exist, no storage info to report 1385 return new StorageInfo.Builder().build(); 1386 } 1387 1388 // Accumulate all the namespaces we're interested in. 1389 Set<String> wantedPrefixedNamespaces = new ArraySet<>(); 1390 for (String database : databases) { 1391 Set<String> prefixedNamespaces = 1392 mNamespaceMapLocked.get(createPrefix(packageName, database)); 1393 if (prefixedNamespaces != null) { 1394 wantedPrefixedNamespaces.addAll(prefixedNamespaces); 1395 } 1396 } 1397 if (wantedPrefixedNamespaces.isEmpty()) { 1398 return new StorageInfo.Builder().build(); 1399 } 1400 1401 return getStorageInfoForNamespaces(getRawStorageInfoProto(), wantedPrefixedNamespaces); 1402 } finally { 1403 mReadWriteLock.readLock().unlock(); 1404 } 1405 } 1406 1407 /** Estimates the storage usage info for a specific database in a package. */ 1408 @NonNull getStorageInfoForDatabase( @onNull String packageName, @NonNull String databaseName)1409 public StorageInfo getStorageInfoForDatabase( 1410 @NonNull String packageName, @NonNull String databaseName) throws AppSearchException { 1411 mReadWriteLock.readLock().lock(); 1412 try { 1413 throwIfClosedLocked(); 1414 1415 Map<String, Set<String>> packageToDatabases = getPackageToDatabases(); 1416 Set<String> databases = packageToDatabases.get(packageName); 1417 if (databases == null) { 1418 // Package doesn't exist, no storage info to report 1419 return new StorageInfo.Builder().build(); 1420 } 1421 if (!databases.contains(databaseName)) { 1422 // Database doesn't exist, no storage info to report 1423 return new StorageInfo.Builder().build(); 1424 } 1425 1426 Set<String> wantedPrefixedNamespaces = 1427 mNamespaceMapLocked.get(createPrefix(packageName, databaseName)); 1428 if (wantedPrefixedNamespaces == null || wantedPrefixedNamespaces.isEmpty()) { 1429 return new StorageInfo.Builder().build(); 1430 } 1431 1432 return getStorageInfoForNamespaces(getRawStorageInfoProto(), wantedPrefixedNamespaces); 1433 } finally { 1434 mReadWriteLock.readLock().unlock(); 1435 } 1436 } 1437 1438 /** 1439 * Returns the native storage info capsuled in {@link StorageInfoResultProto} directly from 1440 * IcingSearchEngine. 1441 */ 1442 @NonNull getRawStorageInfoProto()1443 public StorageInfoProto getRawStorageInfoProto() throws AppSearchException { 1444 mReadWriteLock.readLock().lock(); 1445 try { 1446 throwIfClosedLocked(); 1447 mLogUtil.piiTrace("getStorageInfo, request"); 1448 StorageInfoResultProto storageInfoResult = mIcingSearchEngineLocked.getStorageInfo(); 1449 mLogUtil.piiTrace( 1450 "getStorageInfo, response", storageInfoResult.getStatus(), storageInfoResult); 1451 checkSuccess(storageInfoResult.getStatus()); 1452 return storageInfoResult.getStorageInfo(); 1453 } finally { 1454 mReadWriteLock.readLock().unlock(); 1455 } 1456 } 1457 1458 /** 1459 * Extracts and returns {@link StorageInfo} from {@link StorageInfoProto} based on prefixed 1460 * namespaces. 1461 */ 1462 @NonNull getStorageInfoForNamespaces( @onNull StorageInfoProto storageInfoProto, @NonNull Set<String> prefixedNamespaces)1463 private static StorageInfo getStorageInfoForNamespaces( 1464 @NonNull StorageInfoProto storageInfoProto, @NonNull Set<String> prefixedNamespaces) { 1465 if (!storageInfoProto.hasDocumentStorageInfo()) { 1466 return new StorageInfo.Builder().build(); 1467 } 1468 1469 long totalStorageSize = storageInfoProto.getTotalStorageSize(); 1470 DocumentStorageInfoProto documentStorageInfo = storageInfoProto.getDocumentStorageInfo(); 1471 int totalDocuments = 1472 documentStorageInfo.getNumAliveDocuments() 1473 + documentStorageInfo.getNumExpiredDocuments(); 1474 1475 if (totalStorageSize == 0 || totalDocuments == 0) { 1476 // Maybe we can exit early and also avoid a divide by 0 error. 1477 return new StorageInfo.Builder().build(); 1478 } 1479 1480 // Accumulate stats across the package's namespaces. 1481 int aliveDocuments = 0; 1482 int expiredDocuments = 0; 1483 int aliveNamespaces = 0; 1484 List<NamespaceStorageInfoProto> namespaceStorageInfos = 1485 documentStorageInfo.getNamespaceStorageInfoList(); 1486 for (int i = 0; i < namespaceStorageInfos.size(); i++) { 1487 NamespaceStorageInfoProto namespaceStorageInfo = namespaceStorageInfos.get(i); 1488 // The namespace from icing lib is already the prefixed format 1489 if (prefixedNamespaces.contains(namespaceStorageInfo.getNamespace())) { 1490 if (namespaceStorageInfo.getNumAliveDocuments() > 0) { 1491 aliveNamespaces++; 1492 aliveDocuments += namespaceStorageInfo.getNumAliveDocuments(); 1493 } 1494 expiredDocuments += namespaceStorageInfo.getNumExpiredDocuments(); 1495 } 1496 } 1497 int namespaceDocuments = aliveDocuments + expiredDocuments; 1498 1499 // Since we don't have the exact size of all the documents, we do an estimation. Note 1500 // that while the total storage takes into account schema, index, etc. in addition to 1501 // documents, we'll only calculate the percentage based on number of documents a 1502 // client has. 1503 return new StorageInfo.Builder() 1504 .setSizeBytes((long) (namespaceDocuments * 1.0 / totalDocuments * totalStorageSize)) 1505 .setAliveDocumentsCount(aliveDocuments) 1506 .setAliveNamespacesCount(aliveNamespaces) 1507 .build(); 1508 } 1509 1510 /** 1511 * Persists all update/delete requests to the disk. 1512 * 1513 * <p>If the app crashes after a call to PersistToDisk with {@link PersistType.Code#FULL}, Icing 1514 * would be able to fully recover all data written up to this point without a costly recovery 1515 * process. 1516 * 1517 * <p>If the app crashes after a call to PersistToDisk with {@link PersistType.Code#LITE}, Icing 1518 * would trigger a costly recovery process in next initialization. After that, Icing would still 1519 * be able to recover all written data - excepting Usage data. Usage data is only guaranteed to 1520 * be safe after a call to PersistToDisk with {@link PersistType.Code#FULL} 1521 * 1522 * <p>If the app crashes after an update/delete request has been made, but before any call to 1523 * PersistToDisk, then all data in Icing will be lost. 1524 * 1525 * @param persistType the amount of data to persist. {@link PersistType.Code#LITE} will only 1526 * persist the minimal amount of data to ensure all data can be recovered. {@link 1527 * PersistType.Code#FULL} will persist all data necessary to prevent data loss without 1528 * needing data recovery. 1529 * @throws AppSearchException on any error that AppSearch persist data to disk. 1530 */ persistToDisk(@onNull PersistType.Code persistType)1531 public void persistToDisk(@NonNull PersistType.Code persistType) throws AppSearchException { 1532 mReadWriteLock.writeLock().lock(); 1533 try { 1534 throwIfClosedLocked(); 1535 1536 mLogUtil.piiTrace("persistToDisk, request", persistType); 1537 PersistToDiskResultProto persistToDiskResultProto = 1538 mIcingSearchEngineLocked.persistToDisk(persistType); 1539 mLogUtil.piiTrace( 1540 "persistToDisk, response", 1541 persistToDiskResultProto.getStatus(), 1542 persistToDiskResultProto); 1543 checkSuccess(persistToDiskResultProto.getStatus()); 1544 } finally { 1545 mReadWriteLock.writeLock().unlock(); 1546 } 1547 } 1548 1549 /** 1550 * Remove all {@link AppSearchSchema}s and {@link GenericDocument}s under the given package. 1551 * 1552 * @param packageName The name of package to be removed. 1553 * @throws AppSearchException if we cannot remove the data. 1554 */ clearPackageData(@onNull String packageName)1555 public void clearPackageData(@NonNull String packageName) throws AppSearchException { 1556 mReadWriteLock.writeLock().lock(); 1557 try { 1558 throwIfClosedLocked(); 1559 Set<String> existingPackages = getPackageToDatabases().keySet(); 1560 if (existingPackages.contains(packageName)) { 1561 existingPackages.remove(packageName); 1562 prunePackageData(existingPackages); 1563 } 1564 } finally { 1565 mReadWriteLock.writeLock().unlock(); 1566 } 1567 } 1568 1569 /** 1570 * Remove all {@link AppSearchSchema}s and {@link GenericDocument}s that doesn't belong to any 1571 * of the given installed packages 1572 * 1573 * @param installedPackages The name of all installed package. 1574 * @throws AppSearchException if we cannot remove the data. 1575 */ prunePackageData(@onNull Set<String> installedPackages)1576 public void prunePackageData(@NonNull Set<String> installedPackages) throws AppSearchException { 1577 mReadWriteLock.writeLock().lock(); 1578 try { 1579 throwIfClosedLocked(); 1580 Map<String, Set<String>> packageToDatabases = getPackageToDatabases(); 1581 if (installedPackages.containsAll(packageToDatabases.keySet())) { 1582 // No package got removed. We are good. 1583 return; 1584 } 1585 1586 // Prune schema proto 1587 SchemaProto existingSchema = getSchemaProtoLocked(); 1588 SchemaProto.Builder newSchemaBuilder = SchemaProto.newBuilder(); 1589 for (int i = 0; i < existingSchema.getTypesCount(); i++) { 1590 String packageName = getPackageName(existingSchema.getTypes(i).getSchemaType()); 1591 if (installedPackages.contains(packageName)) { 1592 newSchemaBuilder.addTypes(existingSchema.getTypes(i)); 1593 } 1594 } 1595 1596 SchemaProto finalSchema = newSchemaBuilder.build(); 1597 1598 // Apply schema, set force override to true to remove all schemas and documents that 1599 // doesn't belong to any of these installed packages. 1600 mLogUtil.piiTrace( 1601 "clearPackageData.setSchema, request", 1602 finalSchema.getTypesCount(), 1603 finalSchema); 1604 SetSchemaResultProto setSchemaResultProto = 1605 mIcingSearchEngineLocked.setSchema( 1606 finalSchema, /*ignoreErrorsAndDeleteDocuments=*/ true); 1607 mLogUtil.piiTrace( 1608 "clearPackageData.setSchema, response", 1609 setSchemaResultProto.getStatus(), 1610 setSchemaResultProto); 1611 1612 // Determine whether it succeeded. 1613 checkSuccess(setSchemaResultProto.getStatus()); 1614 1615 // Prune cached maps 1616 for (Map.Entry<String, Set<String>> entry : packageToDatabases.entrySet()) { 1617 String packageName = entry.getKey(); 1618 Set<String> databaseNames = entry.getValue(); 1619 if (!installedPackages.contains(packageName) && databaseNames != null) { 1620 mDocumentCountMapLocked.remove(packageName); 1621 synchronized (mNextPageTokensLocked) { 1622 mNextPageTokensLocked.remove(packageName); 1623 } 1624 for (String databaseName : databaseNames) { 1625 String removedPrefix = createPrefix(packageName, databaseName); 1626 mSchemaMapLocked.remove(removedPrefix); 1627 mNamespaceMapLocked.remove(removedPrefix); 1628 } 1629 } 1630 } 1631 // TODO(b/145759910) clear visibility setting for package. 1632 } finally { 1633 mReadWriteLock.writeLock().unlock(); 1634 } 1635 } 1636 1637 /** 1638 * Clears documents and schema across all packages and databaseNames. 1639 * 1640 * <p>This method belongs to mutate group. 1641 * 1642 * @throws AppSearchException on IcingSearchEngine error. 1643 */ 1644 @GuardedBy("mReadWriteLock") resetLocked(@ullable InitializeStats.Builder initStatsBuilder)1645 private void resetLocked(@Nullable InitializeStats.Builder initStatsBuilder) 1646 throws AppSearchException { 1647 mLogUtil.piiTrace("icingSearchEngine.reset, request"); 1648 ResetResultProto resetResultProto = mIcingSearchEngineLocked.reset(); 1649 mLogUtil.piiTrace( 1650 "icingSearchEngine.reset, response", 1651 resetResultProto.getStatus(), 1652 resetResultProto); 1653 mOptimizeIntervalCountLocked = 0; 1654 mSchemaMapLocked.clear(); 1655 mNamespaceMapLocked.clear(); 1656 mDocumentCountMapLocked.clear(); 1657 synchronized (mNextPageTokensLocked) { 1658 mNextPageTokensLocked.clear(); 1659 } 1660 if (initStatsBuilder != null) { 1661 initStatsBuilder 1662 .setHasReset(true) 1663 .setResetStatusCode(statusProtoToResultCode(resetResultProto.getStatus())); 1664 } 1665 1666 checkSuccess(resetResultProto.getStatus()); 1667 } 1668 1669 @GuardedBy("mReadWriteLock") rebuildDocumentCountMapLocked(@onNull StorageInfoProto storageInfoProto)1670 private void rebuildDocumentCountMapLocked(@NonNull StorageInfoProto storageInfoProto) { 1671 mDocumentCountMapLocked.clear(); 1672 List<NamespaceStorageInfoProto> namespaceStorageInfoProtoList = 1673 storageInfoProto.getDocumentStorageInfo().getNamespaceStorageInfoList(); 1674 for (int i = 0; i < namespaceStorageInfoProtoList.size(); i++) { 1675 NamespaceStorageInfoProto namespaceStorageInfoProto = 1676 namespaceStorageInfoProtoList.get(i); 1677 String packageName = getPackageName(namespaceStorageInfoProto.getNamespace()); 1678 Integer oldCount = mDocumentCountMapLocked.get(packageName); 1679 int newCount; 1680 if (oldCount == null) { 1681 newCount = namespaceStorageInfoProto.getNumAliveDocuments(); 1682 } else { 1683 newCount = oldCount + namespaceStorageInfoProto.getNumAliveDocuments(); 1684 } 1685 mDocumentCountMapLocked.put(packageName, newCount); 1686 } 1687 } 1688 1689 /** Wrapper around schema changes */ 1690 @VisibleForTesting 1691 static class RewrittenSchemaResults { 1692 // Any prefixed types that used to exist in the schema, but are deleted in the new one. 1693 final Set<String> mDeletedPrefixedTypes = new ArraySet<>(); 1694 1695 // Map of prefixed schema types to SchemaTypeConfigProtos that were part of the new schema. 1696 final Map<String, SchemaTypeConfigProto> mRewrittenPrefixedTypes = new ArrayMap<>(); 1697 } 1698 1699 /** 1700 * Rewrites all types mentioned in the given {@code newSchema} to prepend {@code prefix}. 1701 * Rewritten types will be added to the {@code existingSchema}. 1702 * 1703 * @param prefix The full prefix to prepend to the schema. 1704 * @param existingSchema A schema that may contain existing types from across all prefixes. Will 1705 * be mutated to contain the properly rewritten schema types from {@code newSchema}. 1706 * @param newSchema Schema with types to add to the {@code existingSchema}. 1707 * @return a RewrittenSchemaResults that contains all prefixed schema type names in the given 1708 * prefix as well as a set of schema types that were deleted. 1709 */ 1710 @VisibleForTesting rewriteSchema( @onNull String prefix, @NonNull SchemaProto.Builder existingSchema, @NonNull SchemaProto newSchema)1711 static RewrittenSchemaResults rewriteSchema( 1712 @NonNull String prefix, 1713 @NonNull SchemaProto.Builder existingSchema, 1714 @NonNull SchemaProto newSchema) 1715 throws AppSearchException { 1716 HashMap<String, SchemaTypeConfigProto> newTypesToProto = new HashMap<>(); 1717 // Rewrite the schema type to include the typePrefix. 1718 for (int typeIdx = 0; typeIdx < newSchema.getTypesCount(); typeIdx++) { 1719 SchemaTypeConfigProto.Builder typeConfigBuilder = 1720 newSchema.getTypes(typeIdx).toBuilder(); 1721 1722 // Rewrite SchemaProto.types.schema_type 1723 String newSchemaType = prefix + typeConfigBuilder.getSchemaType(); 1724 typeConfigBuilder.setSchemaType(newSchemaType); 1725 1726 // Rewrite SchemaProto.types.properties.schema_type 1727 for (int propertyIdx = 0; 1728 propertyIdx < typeConfigBuilder.getPropertiesCount(); 1729 propertyIdx++) { 1730 PropertyConfigProto.Builder propertyConfigBuilder = 1731 typeConfigBuilder.getProperties(propertyIdx).toBuilder(); 1732 if (!propertyConfigBuilder.getSchemaType().isEmpty()) { 1733 String newPropertySchemaType = prefix + propertyConfigBuilder.getSchemaType(); 1734 propertyConfigBuilder.setSchemaType(newPropertySchemaType); 1735 typeConfigBuilder.setProperties(propertyIdx, propertyConfigBuilder); 1736 } 1737 } 1738 1739 newTypesToProto.put(newSchemaType, typeConfigBuilder.build()); 1740 } 1741 1742 // newTypesToProto is modified below, so we need a copy first 1743 RewrittenSchemaResults rewrittenSchemaResults = new RewrittenSchemaResults(); 1744 rewrittenSchemaResults.mRewrittenPrefixedTypes.putAll(newTypesToProto); 1745 1746 // Combine the existing schema (which may have types from other prefixes) with this 1747 // prefix's new schema. Modifies the existingSchemaBuilder. 1748 // Check if we need to replace any old schema types with the new ones. 1749 for (int i = 0; i < existingSchema.getTypesCount(); i++) { 1750 String schemaType = existingSchema.getTypes(i).getSchemaType(); 1751 SchemaTypeConfigProto newProto = newTypesToProto.remove(schemaType); 1752 if (newProto != null) { 1753 // Replacement 1754 existingSchema.setTypes(i, newProto); 1755 } else if (prefix.equals(getPrefix(schemaType))) { 1756 // All types existing before but not in newSchema should be removed. 1757 existingSchema.removeTypes(i); 1758 --i; 1759 rewrittenSchemaResults.mDeletedPrefixedTypes.add(schemaType); 1760 } 1761 } 1762 // We've been removing existing types from newTypesToProto, so everything that remains is 1763 // new. 1764 existingSchema.addAllTypes(newTypesToProto.values()); 1765 1766 return rewrittenSchemaResults; 1767 } 1768 1769 /** 1770 * Rewrites the search spec filters with {@code prefixes}. 1771 * 1772 * <p>This method should be only called in query methods and get the READ lock to keep thread 1773 * safety. 1774 * 1775 * @param searchSpecBuilder Client-provided SearchSpec 1776 * @param prefixes Prefixes that we should prepend to all our filters 1777 * @param allowedPrefixedSchemas Prefixed schemas that the client is allowed to query over. This 1778 * supersedes the schema filters that may exist on the {@code searchSpecBuilder}. 1779 * @return false if none there would be nothing to search over. 1780 */ 1781 @VisibleForTesting 1782 @GuardedBy("mReadWriteLock") rewriteSearchSpecForPrefixesLocked( @onNull SearchSpecProto.Builder searchSpecBuilder, @NonNull Set<String> prefixes, @NonNull Set<String> allowedPrefixedSchemas)1783 boolean rewriteSearchSpecForPrefixesLocked( 1784 @NonNull SearchSpecProto.Builder searchSpecBuilder, 1785 @NonNull Set<String> prefixes, 1786 @NonNull Set<String> allowedPrefixedSchemas) { 1787 // Create a copy since retainAll() modifies the original set. 1788 Set<String> existingPrefixes = new ArraySet<>(mNamespaceMapLocked.keySet()); 1789 existingPrefixes.retainAll(prefixes); 1790 1791 if (existingPrefixes.isEmpty()) { 1792 // None of the prefixes exist, empty query. 1793 return false; 1794 } 1795 1796 if (allowedPrefixedSchemas.isEmpty()) { 1797 // Not allowed to search over any schemas, empty query. 1798 return false; 1799 } 1800 1801 // Clear the schema type filters since we'll be rewriting them with the 1802 // allowedPrefixedSchemas. 1803 searchSpecBuilder.clearSchemaTypeFilters(); 1804 searchSpecBuilder.addAllSchemaTypeFilters(allowedPrefixedSchemas); 1805 1806 // Cache the namespaces before clearing everything. 1807 List<String> namespaceFilters = searchSpecBuilder.getNamespaceFiltersList(); 1808 searchSpecBuilder.clearNamespaceFilters(); 1809 1810 // Rewrite non-schema filters to include a prefix. 1811 for (String prefix : existingPrefixes) { 1812 // TODO(b/169883602): We currently grab every namespace for every prefix. We can 1813 // optimize this by checking if a prefix has any allowedSchemaTypes. If not, that 1814 // means we don't want to query over anything in that prefix anyways, so we don't 1815 // need to grab its namespaces either. 1816 1817 // Empty namespaces on the search spec means to query over all namespaces. 1818 Set<String> existingNamespaces = mNamespaceMapLocked.get(prefix); 1819 if (existingNamespaces != null) { 1820 if (namespaceFilters.isEmpty()) { 1821 // Include all namespaces 1822 searchSpecBuilder.addAllNamespaceFilters(existingNamespaces); 1823 } else { 1824 // Prefix the given namespaces. 1825 for (int i = 0; i < namespaceFilters.size(); i++) { 1826 String prefixedNamespace = prefix + namespaceFilters.get(i); 1827 if (existingNamespaces.contains(prefixedNamespace)) { 1828 searchSpecBuilder.addNamespaceFilters(prefixedNamespace); 1829 } 1830 } 1831 } 1832 } 1833 } 1834 1835 return true; 1836 } 1837 1838 /** 1839 * Returns the set of allowed prefixed schemas that the {@code prefix} can query while taking 1840 * into account the {@code searchSpec} schema filters. 1841 * 1842 * <p>This only checks intersection of schema filters on the search spec with those that the 1843 * prefix owns itself. This does not check global query permissions. 1844 */ 1845 @GuardedBy("mReadWriteLock") getAllowedPrefixSchemasLocked( @onNull String prefix, @NonNull SearchSpec searchSpec)1846 private Set<String> getAllowedPrefixSchemasLocked( 1847 @NonNull String prefix, @NonNull SearchSpec searchSpec) { 1848 Set<String> allowedPrefixedSchemas = new ArraySet<>(); 1849 1850 // Add all the schema filters the client specified. 1851 List<String> schemaFilters = searchSpec.getFilterSchemas(); 1852 for (int i = 0; i < schemaFilters.size(); i++) { 1853 allowedPrefixedSchemas.add(prefix + schemaFilters.get(i)); 1854 } 1855 1856 if (allowedPrefixedSchemas.isEmpty()) { 1857 // If the client didn't specify any schema filters, search over all of their schemas 1858 Map<String, SchemaTypeConfigProto> prefixedSchemaMap = mSchemaMapLocked.get(prefix); 1859 if (prefixedSchemaMap != null) { 1860 allowedPrefixedSchemas.addAll(prefixedSchemaMap.keySet()); 1861 } 1862 } 1863 return allowedPrefixedSchemas; 1864 } 1865 1866 /** 1867 * Rewrites the typePropertyMasks that exist in {@code prefixes}. 1868 * 1869 * <p>This method should be only called in query methods and get the READ lock to keep thread 1870 * safety. 1871 * 1872 * @param resultSpecBuilder ResultSpecs as specified by client 1873 * @param prefixes Prefixes that we should prepend to all our filters 1874 * @param allowedPrefixedSchemas Prefixed schemas that the client is allowed to query over. 1875 */ 1876 @VisibleForTesting 1877 @GuardedBy("mReadWriteLock") rewriteResultSpecForPrefixesLocked( @onNull ResultSpecProto.Builder resultSpecBuilder, @NonNull Set<String> prefixes, @NonNull Set<String> allowedPrefixedSchemas)1878 void rewriteResultSpecForPrefixesLocked( 1879 @NonNull ResultSpecProto.Builder resultSpecBuilder, 1880 @NonNull Set<String> prefixes, 1881 @NonNull Set<String> allowedPrefixedSchemas) { 1882 // Create a copy since retainAll() modifies the original set. 1883 Set<String> existingPrefixes = new ArraySet<>(mNamespaceMapLocked.keySet()); 1884 existingPrefixes.retainAll(prefixes); 1885 1886 List<TypePropertyMask> prefixedTypePropertyMasks = new ArrayList<>(); 1887 // Rewrite filters to include a database prefix. 1888 for (String prefix : existingPrefixes) { 1889 // Qualify the given schema types 1890 for (TypePropertyMask typePropertyMask : resultSpecBuilder.getTypePropertyMasksList()) { 1891 String unprefixedType = typePropertyMask.getSchemaType(); 1892 boolean isWildcard = 1893 unprefixedType.equals(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD); 1894 String prefixedType = isWildcard ? unprefixedType : prefix + unprefixedType; 1895 if (isWildcard || allowedPrefixedSchemas.contains(prefixedType)) { 1896 prefixedTypePropertyMasks.add( 1897 typePropertyMask.toBuilder().setSchemaType(prefixedType).build()); 1898 } 1899 } 1900 } 1901 resultSpecBuilder 1902 .clearTypePropertyMasks() 1903 .addAllTypePropertyMasks(prefixedTypePropertyMasks); 1904 } 1905 1906 /** 1907 * Adds result groupings for each namespace in each package being queried for. 1908 * 1909 * <p>This method should be only called in query methods and get the READ lock to keep thread 1910 * safety. 1911 * 1912 * @param resultSpecBuilder ResultSpecs as specified by client 1913 * @param prefixes Prefixes that we should prepend to all our filters 1914 * @param maxNumResults The maximum number of results for each grouping to support. 1915 */ 1916 @GuardedBy("mReadWriteLock") addPerPackagePerNamespaceResultGroupingsLocked( @onNull ResultSpecProto.Builder resultSpecBuilder, @NonNull Set<String> prefixes, int maxNumResults)1917 private void addPerPackagePerNamespaceResultGroupingsLocked( 1918 @NonNull ResultSpecProto.Builder resultSpecBuilder, 1919 @NonNull Set<String> prefixes, 1920 int maxNumResults) { 1921 Set<String> existingPrefixes = new ArraySet<>(mNamespaceMapLocked.keySet()); 1922 existingPrefixes.retainAll(prefixes); 1923 1924 // Create a map for package+namespace to prefixedNamespaces. This is NOT necessarily the 1925 // same as the list of namespaces. If one package has multiple databases, each with the same 1926 // namespace, then those should be grouped together. 1927 Map<String, List<String>> packageAndNamespaceToNamespaces = new ArrayMap<>(); 1928 for (String prefix : existingPrefixes) { 1929 Set<String> prefixedNamespaces = mNamespaceMapLocked.get(prefix); 1930 if (prefixedNamespaces == null) { 1931 continue; 1932 } 1933 String packageName = getPackageName(prefix); 1934 // Create a new prefix without the database name. This will allow us to group namespaces 1935 // that have the same name and package but a different database name together. 1936 String emptyDatabasePrefix = createPrefix(packageName, /*databaseName*/ ""); 1937 for (String prefixedNamespace : prefixedNamespaces) { 1938 String namespace; 1939 try { 1940 namespace = removePrefix(prefixedNamespace); 1941 } catch (AppSearchException e) { 1942 // This should never happen. Skip this namespace if it does. 1943 Log.e(TAG, "Prefixed namespace " + prefixedNamespace + " is malformed."); 1944 continue; 1945 } 1946 String emptyDatabasePrefixedNamespace = emptyDatabasePrefix + namespace; 1947 List<String> namespaceList = 1948 packageAndNamespaceToNamespaces.get(emptyDatabasePrefixedNamespace); 1949 if (namespaceList == null) { 1950 namespaceList = new ArrayList<>(); 1951 packageAndNamespaceToNamespaces.put( 1952 emptyDatabasePrefixedNamespace, namespaceList); 1953 } 1954 namespaceList.add(prefixedNamespace); 1955 } 1956 } 1957 1958 for (List<String> namespaces : packageAndNamespaceToNamespaces.values()) { 1959 resultSpecBuilder.addResultGroupings( 1960 ResultSpecProto.ResultGrouping.newBuilder() 1961 .addAllNamespaces(namespaces) 1962 .setMaxResults(maxNumResults)); 1963 } 1964 } 1965 1966 /** 1967 * Adds result groupings for each package being queried for. 1968 * 1969 * <p>This method should be only called in query methods and get the READ lock to keep thread 1970 * safety. 1971 * 1972 * @param resultSpecBuilder ResultSpecs as specified by client 1973 * @param prefixes Prefixes that we should prepend to all our filters 1974 * @param maxNumResults The maximum number of results for each grouping to support. 1975 */ 1976 @GuardedBy("mReadWriteLock") addPerPackageResultGroupingsLocked( @onNull ResultSpecProto.Builder resultSpecBuilder, @NonNull Set<String> prefixes, int maxNumResults)1977 private void addPerPackageResultGroupingsLocked( 1978 @NonNull ResultSpecProto.Builder resultSpecBuilder, 1979 @NonNull Set<String> prefixes, 1980 int maxNumResults) { 1981 Set<String> existingPrefixes = new ArraySet<>(mNamespaceMapLocked.keySet()); 1982 existingPrefixes.retainAll(prefixes); 1983 1984 // Build up a map of package to namespaces. 1985 Map<String, List<String>> packageToNamespacesMap = new ArrayMap<>(); 1986 for (String prefix : existingPrefixes) { 1987 Set<String> prefixedNamespaces = mNamespaceMapLocked.get(prefix); 1988 if (prefixedNamespaces == null) { 1989 continue; 1990 } 1991 String packageName = getPackageName(prefix); 1992 List<String> packageNamespaceList = packageToNamespacesMap.get(packageName); 1993 if (packageNamespaceList == null) { 1994 packageNamespaceList = new ArrayList<>(); 1995 packageToNamespacesMap.put(packageName, packageNamespaceList); 1996 } 1997 packageNamespaceList.addAll(prefixedNamespaces); 1998 } 1999 2000 for (List<String> prefixedNamespaces : packageToNamespacesMap.values()) { 2001 resultSpecBuilder.addResultGroupings( 2002 ResultSpecProto.ResultGrouping.newBuilder() 2003 .addAllNamespaces(prefixedNamespaces) 2004 .setMaxResults(maxNumResults)); 2005 } 2006 } 2007 2008 /** 2009 * Adds result groupings for each namespace being queried for. 2010 * 2011 * <p>This method should be only called in query methods and get the READ lock to keep thread 2012 * safety. 2013 * 2014 * @param resultSpecBuilder ResultSpecs as specified by client 2015 * @param prefixes Prefixes that we should prepend to all our filters 2016 * @param maxNumResults The maximum number of results for each grouping to support. 2017 */ 2018 @GuardedBy("mReadWriteLock") addPerNamespaceResultGroupingsLocked( @onNull ResultSpecProto.Builder resultSpecBuilder, @NonNull Set<String> prefixes, int maxNumResults)2019 private void addPerNamespaceResultGroupingsLocked( 2020 @NonNull ResultSpecProto.Builder resultSpecBuilder, 2021 @NonNull Set<String> prefixes, 2022 int maxNumResults) { 2023 Set<String> existingPrefixes = new ArraySet<>(mNamespaceMapLocked.keySet()); 2024 existingPrefixes.retainAll(prefixes); 2025 2026 // Create a map of namespace to prefixedNamespaces. This is NOT necessarily the 2027 // same as the list of namespaces. If a namespace exists under different packages and/or 2028 // different databases, they should still be grouped together. 2029 Map<String, List<String>> namespaceToPrefixedNamespaces = new ArrayMap<>(); 2030 for (String prefix : existingPrefixes) { 2031 Set<String> prefixedNamespaces = mNamespaceMapLocked.get(prefix); 2032 if (prefixedNamespaces == null) { 2033 continue; 2034 } 2035 for (String prefixedNamespace : prefixedNamespaces) { 2036 String namespace; 2037 try { 2038 namespace = removePrefix(prefixedNamespace); 2039 } catch (AppSearchException e) { 2040 // This should never happen. Skip this namespace if it does. 2041 Log.e(TAG, "Prefixed namespace " + prefixedNamespace + " is malformed."); 2042 continue; 2043 } 2044 List<String> groupedPrefixedNamespaces = 2045 namespaceToPrefixedNamespaces.get(namespace); 2046 if (groupedPrefixedNamespaces == null) { 2047 groupedPrefixedNamespaces = new ArrayList<>(); 2048 namespaceToPrefixedNamespaces.put(namespace, groupedPrefixedNamespaces); 2049 } 2050 groupedPrefixedNamespaces.add(prefixedNamespace); 2051 } 2052 } 2053 2054 for (List<String> namespaces : namespaceToPrefixedNamespaces.values()) { 2055 resultSpecBuilder.addResultGroupings( 2056 ResultSpecProto.ResultGrouping.newBuilder() 2057 .addAllNamespaces(namespaces) 2058 .setMaxResults(maxNumResults)); 2059 } 2060 } 2061 2062 @VisibleForTesting 2063 @GuardedBy("mReadWriteLock") getSchemaProtoLocked()2064 SchemaProto getSchemaProtoLocked() throws AppSearchException { 2065 mLogUtil.piiTrace("getSchema, request"); 2066 GetSchemaResultProto schemaProto = mIcingSearchEngineLocked.getSchema(); 2067 mLogUtil.piiTrace("getSchema, response", schemaProto.getStatus(), schemaProto); 2068 // TODO(b/161935693) check GetSchemaResultProto is success or not. Call reset() if it's not. 2069 // TODO(b/161935693) only allow GetSchemaResultProto NOT_FOUND on first run 2070 checkCodeOneOf(schemaProto.getStatus(), StatusProto.Code.OK, StatusProto.Code.NOT_FOUND); 2071 return schemaProto.getSchema(); 2072 } 2073 addNextPageToken(String packageName, long nextPageToken)2074 private void addNextPageToken(String packageName, long nextPageToken) { 2075 if (nextPageToken == EMPTY_PAGE_TOKEN) { 2076 // There is no more pages. No need to add it. 2077 return; 2078 } 2079 synchronized (mNextPageTokensLocked) { 2080 Set<Long> tokens = mNextPageTokensLocked.get(packageName); 2081 if (tokens == null) { 2082 tokens = new ArraySet<>(); 2083 mNextPageTokensLocked.put(packageName, tokens); 2084 } 2085 tokens.add(nextPageToken); 2086 } 2087 } 2088 checkNextPageToken(String packageName, long nextPageToken)2089 private void checkNextPageToken(String packageName, long nextPageToken) 2090 throws AppSearchException { 2091 if (nextPageToken == EMPTY_PAGE_TOKEN) { 2092 // Swallow the check for empty page token, token = 0 means there is no more page and it 2093 // won't return anything from Icing. 2094 return; 2095 } 2096 synchronized (mNextPageTokensLocked) { 2097 Set<Long> nextPageTokens = mNextPageTokensLocked.get(packageName); 2098 if (nextPageTokens == null || !nextPageTokens.contains(nextPageToken)) { 2099 throw new AppSearchException( 2100 AppSearchResult.RESULT_SECURITY_ERROR, 2101 "Package \"" 2102 + packageName 2103 + "\" cannot use nextPageToken: " 2104 + nextPageToken); 2105 } 2106 } 2107 } 2108 addToMap( Map<String, Set<String>> map, String prefix, String prefixedValue)2109 private static void addToMap( 2110 Map<String, Set<String>> map, String prefix, String prefixedValue) { 2111 Set<String> values = map.get(prefix); 2112 if (values == null) { 2113 values = new ArraySet<>(); 2114 map.put(prefix, values); 2115 } 2116 values.add(prefixedValue); 2117 } 2118 addToMap( Map<String, Map<String, SchemaTypeConfigProto>> map, String prefix, SchemaTypeConfigProto schemaTypeConfigProto)2119 private static void addToMap( 2120 Map<String, Map<String, SchemaTypeConfigProto>> map, 2121 String prefix, 2122 SchemaTypeConfigProto schemaTypeConfigProto) { 2123 Map<String, SchemaTypeConfigProto> schemaTypeMap = map.get(prefix); 2124 if (schemaTypeMap == null) { 2125 schemaTypeMap = new ArrayMap<>(); 2126 map.put(prefix, schemaTypeMap); 2127 } 2128 schemaTypeMap.put(schemaTypeConfigProto.getSchemaType(), schemaTypeConfigProto); 2129 } 2130 removeFromMap( Map<String, Map<String, SchemaTypeConfigProto>> map, String prefix, String schemaType)2131 private static void removeFromMap( 2132 Map<String, Map<String, SchemaTypeConfigProto>> map, String prefix, String schemaType) { 2133 Map<String, SchemaTypeConfigProto> schemaTypeMap = map.get(prefix); 2134 if (schemaTypeMap != null) { 2135 schemaTypeMap.remove(schemaType); 2136 } 2137 } 2138 2139 /** 2140 * Checks the given status code and throws an {@link AppSearchException} if code is an error. 2141 * 2142 * @throws AppSearchException on error codes. 2143 */ checkSuccess(StatusProto statusProto)2144 private static void checkSuccess(StatusProto statusProto) throws AppSearchException { 2145 checkCodeOneOf(statusProto, StatusProto.Code.OK); 2146 } 2147 2148 /** 2149 * Checks the given status code is one of the provided codes, and throws an {@link 2150 * AppSearchException} if it is not. 2151 */ checkCodeOneOf(StatusProto statusProto, StatusProto.Code... codes)2152 private static void checkCodeOneOf(StatusProto statusProto, StatusProto.Code... codes) 2153 throws AppSearchException { 2154 for (int i = 0; i < codes.length; i++) { 2155 if (codes[i] == statusProto.getCode()) { 2156 // Everything's good 2157 return; 2158 } 2159 } 2160 2161 if (statusProto.getCode() == StatusProto.Code.WARNING_DATA_LOSS) { 2162 // TODO: May want to propagate WARNING_DATA_LOSS up to AppSearchSession so they can 2163 // choose to log the error or potentially pass it on to clients. 2164 Log.w(TAG, "Encountered WARNING_DATA_LOSS: " + statusProto.getMessage()); 2165 return; 2166 } 2167 2168 throw new AppSearchException( 2169 ResultCodeToProtoConverter.toResultCode(statusProto.getCode()), 2170 statusProto.getMessage()); 2171 } 2172 2173 /** 2174 * Checks whether {@link IcingSearchEngine#optimize()} should be called to release resources. 2175 * 2176 * <p>This method should be only called after a mutation to local storage backend which deletes 2177 * a mass of data and could release lots resources after {@link IcingSearchEngine#optimize()}. 2178 * 2179 * <p>This method will trigger {@link IcingSearchEngine#getOptimizeInfo()} to check resources 2180 * that could be released for every {@link #CHECK_OPTIMIZE_INTERVAL} mutations. 2181 * 2182 * <p>{@link IcingSearchEngine#optimize()} should be called only if {@link 2183 * GetOptimizeInfoResultProto} shows there is enough resources could be released. 2184 * 2185 * @param mutationSize The number of how many mutations have been executed for current request. 2186 * An inside counter will accumulates it. Once the counter reaches {@link 2187 * #CHECK_OPTIMIZE_INTERVAL}, {@link IcingSearchEngine#getOptimizeInfo()} will be triggered 2188 * and the counter will be reset. 2189 */ checkForOptimize(int mutationSize, @Nullable OptimizeStats.Builder builder)2190 public void checkForOptimize(int mutationSize, @Nullable OptimizeStats.Builder builder) 2191 throws AppSearchException { 2192 mReadWriteLock.writeLock().lock(); 2193 try { 2194 mOptimizeIntervalCountLocked += mutationSize; 2195 if (mOptimizeIntervalCountLocked >= CHECK_OPTIMIZE_INTERVAL) { 2196 checkForOptimize(builder); 2197 } 2198 } finally { 2199 mReadWriteLock.writeLock().unlock(); 2200 } 2201 } 2202 2203 /** 2204 * Checks whether {@link IcingSearchEngine#optimize()} should be called to release resources. 2205 * 2206 * <p>This method will directly trigger {@link IcingSearchEngine#getOptimizeInfo()} to check 2207 * resources that could be released. 2208 * 2209 * <p>{@link IcingSearchEngine#optimize()} should be called only if {@link 2210 * OptimizeStrategy#shouldOptimize(GetOptimizeInfoResultProto)} return true. 2211 */ checkForOptimize(@ullable OptimizeStats.Builder builder)2212 public void checkForOptimize(@Nullable OptimizeStats.Builder builder) 2213 throws AppSearchException { 2214 mReadWriteLock.writeLock().lock(); 2215 try { 2216 GetOptimizeInfoResultProto optimizeInfo = getOptimizeInfoResultLocked(); 2217 checkSuccess(optimizeInfo.getStatus()); 2218 mOptimizeIntervalCountLocked = 0; 2219 if (mOptimizeStrategy.shouldOptimize(optimizeInfo)) { 2220 optimize(builder); 2221 } 2222 } finally { 2223 mReadWriteLock.writeLock().unlock(); 2224 } 2225 // TODO(b/147699081): Return OptimizeResultProto & log lost data detail once we add 2226 // a field to indicate lost_schema and lost_documents in OptimizeResultProto. 2227 // go/icing-library-apis. 2228 } 2229 2230 /** Triggers {@link IcingSearchEngine#optimize()} directly. */ optimize(@ullable OptimizeStats.Builder builder)2231 public void optimize(@Nullable OptimizeStats.Builder builder) throws AppSearchException { 2232 mReadWriteLock.writeLock().lock(); 2233 try { 2234 mLogUtil.piiTrace("optimize, request"); 2235 OptimizeResultProto optimizeResultProto = mIcingSearchEngineLocked.optimize(); 2236 mLogUtil.piiTrace( 2237 "optimize, response", optimizeResultProto.getStatus(), optimizeResultProto); 2238 if (builder != null) { 2239 builder.setStatusCode(statusProtoToResultCode(optimizeResultProto.getStatus())); 2240 AppSearchLoggerHelper.copyNativeStats( 2241 optimizeResultProto.getOptimizeStats(), builder); 2242 } 2243 checkSuccess(optimizeResultProto.getStatus()); 2244 } finally { 2245 mReadWriteLock.writeLock().unlock(); 2246 } 2247 } 2248 2249 /** Remove the rewritten schema types from any result documents. */ 2250 @NonNull 2251 @VisibleForTesting rewriteSearchResultProto( @onNull SearchResultProto searchResultProto, @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap)2252 static SearchResultPage rewriteSearchResultProto( 2253 @NonNull SearchResultProto searchResultProto, 2254 @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap) 2255 throws AppSearchException { 2256 // Parallel array of package names for each document search result. 2257 List<String> packageNames = new ArrayList<>(searchResultProto.getResultsCount()); 2258 2259 // Parallel array of database names for each document search result. 2260 List<String> databaseNames = new ArrayList<>(searchResultProto.getResultsCount()); 2261 2262 SearchResultProto.Builder resultsBuilder = searchResultProto.toBuilder(); 2263 for (int i = 0; i < searchResultProto.getResultsCount(); i++) { 2264 SearchResultProto.ResultProto.Builder resultBuilder = 2265 searchResultProto.getResults(i).toBuilder(); 2266 DocumentProto.Builder documentBuilder = resultBuilder.getDocument().toBuilder(); 2267 String prefix = removePrefixesFromDocument(documentBuilder); 2268 packageNames.add(getPackageName(prefix)); 2269 databaseNames.add(getDatabaseName(prefix)); 2270 resultBuilder.setDocument(documentBuilder); 2271 resultsBuilder.setResults(i, resultBuilder); 2272 } 2273 return SearchResultToProtoConverter.toSearchResultPage( 2274 resultsBuilder, packageNames, databaseNames, schemaMap); 2275 } 2276 2277 @GuardedBy("mReadWriteLock") 2278 @VisibleForTesting getOptimizeInfoResultLocked()2279 GetOptimizeInfoResultProto getOptimizeInfoResultLocked() { 2280 mLogUtil.piiTrace("getOptimizeInfo, request"); 2281 GetOptimizeInfoResultProto result = mIcingSearchEngineLocked.getOptimizeInfo(); 2282 mLogUtil.piiTrace("getOptimizeInfo, response", result.getStatus(), result); 2283 return result; 2284 } 2285 2286 /** 2287 * Converts an erroneous status code from the Icing status enums to the AppSearchResult enums. 2288 * 2289 * <p>Callers should ensure that the status code is not OK or WARNING_DATA_LOSS. 2290 * 2291 * @param statusProto StatusProto with error code to translate into an {@link AppSearchResult} 2292 * code. 2293 * @return {@link AppSearchResult} error code 2294 */ statusProtoToResultCode( @onNull StatusProto statusProto)2295 private static @AppSearchResult.ResultCode int statusProtoToResultCode( 2296 @NonNull StatusProto statusProto) { 2297 return ResultCodeToProtoConverter.toResultCode(statusProto.getCode()); 2298 } 2299 } 2300