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 android.app.appsearch.AppSearchResult.RESULT_SECURITY_ERROR; 20 import static android.app.appsearch.InternalSetSchemaResponse.newFailedSetSchemaResponse; 21 import static android.app.appsearch.InternalSetSchemaResponse.newSuccessfulSetSchemaResponse; 22 23 import static com.android.server.appsearch.external.localstorage.util.PrefixUtil.addPrefixToDocument; 24 import static com.android.server.appsearch.external.localstorage.util.PrefixUtil.createPrefix; 25 import static com.android.server.appsearch.external.localstorage.util.PrefixUtil.getDatabaseName; 26 import static com.android.server.appsearch.external.localstorage.util.PrefixUtil.getPackageName; 27 import static com.android.server.appsearch.external.localstorage.util.PrefixUtil.getPrefix; 28 import static com.android.server.appsearch.external.localstorage.util.PrefixUtil.removePrefixesFromDocument; 29 30 import android.annotation.NonNull; 31 import android.annotation.Nullable; 32 import android.annotation.WorkerThread; 33 import android.app.appsearch.AppSearchResult; 34 import android.app.appsearch.AppSearchSchema; 35 import android.app.appsearch.GenericDocument; 36 import android.app.appsearch.GetByDocumentIdRequest; 37 import android.app.appsearch.GetSchemaResponse; 38 import android.app.appsearch.InternalSetSchemaResponse; 39 import android.app.appsearch.InternalVisibilityConfig; 40 import android.app.appsearch.JoinSpec; 41 import android.app.appsearch.PackageIdentifier; 42 import android.app.appsearch.SchemaVisibilityConfig; 43 import android.app.appsearch.SearchResultPage; 44 import android.app.appsearch.SearchSpec; 45 import android.app.appsearch.SearchSuggestionResult; 46 import android.app.appsearch.SearchSuggestionSpec; 47 import android.app.appsearch.SetSchemaResponse; 48 import android.app.appsearch.StorageInfo; 49 import android.app.appsearch.exceptions.AppSearchException; 50 import android.app.appsearch.observer.ObserverCallback; 51 import android.app.appsearch.observer.ObserverSpec; 52 import android.app.appsearch.util.LogUtil; 53 import android.os.SystemClock; 54 import android.util.ArrayMap; 55 import android.util.ArraySet; 56 import android.util.Log; 57 58 import com.android.internal.annotations.GuardedBy; 59 import com.android.internal.annotations.VisibleForTesting; 60 import com.android.server.appsearch.external.localstorage.converter.GenericDocumentToProtoConverter; 61 import com.android.server.appsearch.external.localstorage.converter.ResultCodeToProtoConverter; 62 import com.android.server.appsearch.external.localstorage.converter.SchemaToProtoConverter; 63 import com.android.server.appsearch.external.localstorage.converter.SearchResultToProtoConverter; 64 import com.android.server.appsearch.external.localstorage.converter.SearchSpecToProtoConverter; 65 import com.android.server.appsearch.external.localstorage.converter.SearchSuggestionSpecToProtoConverter; 66 import com.android.server.appsearch.external.localstorage.converter.SetSchemaResponseToProtoConverter; 67 import com.android.server.appsearch.external.localstorage.converter.TypePropertyPathToProtoConverter; 68 import com.android.server.appsearch.external.localstorage.stats.InitializeStats; 69 import com.android.server.appsearch.external.localstorage.stats.OptimizeStats; 70 import com.android.server.appsearch.external.localstorage.stats.PutDocumentStats; 71 import com.android.server.appsearch.external.localstorage.stats.RemoveStats; 72 import com.android.server.appsearch.external.localstorage.stats.SearchStats; 73 import com.android.server.appsearch.external.localstorage.stats.SetSchemaStats; 74 import com.android.server.appsearch.external.localstorage.util.PrefixUtil; 75 import com.android.server.appsearch.external.localstorage.visibilitystore.CallerAccess; 76 import com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityChecker; 77 import com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityStore; 78 import com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityUtil; 79 80 import com.google.android.icing.IcingSearchEngine; 81 import com.google.android.icing.proto.DebugInfoProto; 82 import com.google.android.icing.proto.DebugInfoResultProto; 83 import com.google.android.icing.proto.DebugInfoVerbosity; 84 import com.google.android.icing.proto.DeleteByQueryResultProto; 85 import com.google.android.icing.proto.DeleteResultProto; 86 import com.google.android.icing.proto.DocumentProto; 87 import com.google.android.icing.proto.DocumentStorageInfoProto; 88 import com.google.android.icing.proto.GetAllNamespacesResultProto; 89 import com.google.android.icing.proto.GetOptimizeInfoResultProto; 90 import com.google.android.icing.proto.GetResultProto; 91 import com.google.android.icing.proto.GetResultSpecProto; 92 import com.google.android.icing.proto.GetSchemaResultProto; 93 import com.google.android.icing.proto.IcingSearchEngineOptions; 94 import com.google.android.icing.proto.InitializeResultProto; 95 import com.google.android.icing.proto.LogSeverity; 96 import com.google.android.icing.proto.NamespaceStorageInfoProto; 97 import com.google.android.icing.proto.OptimizeResultProto; 98 import com.google.android.icing.proto.PersistToDiskResultProto; 99 import com.google.android.icing.proto.PersistType; 100 import com.google.android.icing.proto.PropertyConfigProto; 101 import com.google.android.icing.proto.PutResultProto; 102 import com.google.android.icing.proto.ReportUsageResultProto; 103 import com.google.android.icing.proto.ResetResultProto; 104 import com.google.android.icing.proto.ResultSpecProto; 105 import com.google.android.icing.proto.SchemaProto; 106 import com.google.android.icing.proto.SchemaTypeConfigProto; 107 import com.google.android.icing.proto.ScoringSpecProto; 108 import com.google.android.icing.proto.SearchResultProto; 109 import com.google.android.icing.proto.SearchSpecProto; 110 import com.google.android.icing.proto.SetSchemaResultProto; 111 import com.google.android.icing.proto.StatusProto; 112 import com.google.android.icing.proto.StorageInfoProto; 113 import com.google.android.icing.proto.StorageInfoResultProto; 114 import com.google.android.icing.proto.SuggestionResponse; 115 import com.google.android.icing.proto.TypePropertyMask; 116 import com.google.android.icing.proto.UsageReport; 117 118 import java.io.Closeable; 119 import java.io.File; 120 import java.util.ArrayList; 121 import java.util.Collections; 122 import java.util.HashMap; 123 import java.util.List; 124 import java.util.Map; 125 import java.util.Objects; 126 import java.util.Set; 127 import java.util.concurrent.Executor; 128 import java.util.concurrent.locks.ReadWriteLock; 129 import java.util.concurrent.locks.ReentrantReadWriteLock; 130 131 /** 132 * Manages interaction with the native IcingSearchEngine and other components to implement AppSearch 133 * functionality. 134 * 135 * <p>Never create two instances using the same folder. 136 * 137 * <p>A single instance of {@link AppSearchImpl} can support all packages and databases. This is 138 * done by combining the package and database name into a unique prefix and prefixing the schemas 139 * and documents stored under that owner. Schemas and documents are physically saved together in 140 * {@link IcingSearchEngine}, but logically isolated: 141 * 142 * <ul> 143 * <li>Rewrite SchemaType in SchemaProto by adding the package-database prefix and save into 144 * SchemaTypes set in {@link #setSchema}. 145 * <li>Rewrite namespace and SchemaType in DocumentProto by adding package-database prefix and 146 * save to namespaces set in {@link #putDocument}. 147 * <li>Remove package-database prefix when retrieving documents in {@link #getDocument} and {@link 148 * #query}. 149 * <li>Rewrite filters in {@link SearchSpecProto} to have all namespaces and schema types of the 150 * queried database when user using empty filters in {@link #query}. 151 * </ul> 152 * 153 * <p>Methods in this class belong to two groups, the query group and the mutate group. 154 * 155 * <ul> 156 * <li>All methods are going to modify global parameters and data in Icing are executed under 157 * WRITE lock to keep thread safety. 158 * <li>All methods are going to access global parameters or query data from Icing are executed 159 * under READ lock to improve query performance. 160 * </ul> 161 * 162 * <p>This class is thread safe. 163 * 164 * @hide 165 */ 166 @WorkerThread 167 public final class AppSearchImpl implements Closeable { 168 private static final String TAG = "AppSearchImpl"; 169 170 /** A value 0 means that there're no more pages in the search results. */ 171 private static final long EMPTY_PAGE_TOKEN = 0; 172 173 @VisibleForTesting static final int CHECK_OPTIMIZE_INTERVAL = 100; 174 175 /** A GetResultSpec that uses projection to skip all properties. */ 176 private static final GetResultSpecProto GET_RESULT_SPEC_NO_PROPERTIES = 177 GetResultSpecProto.newBuilder() 178 .addTypePropertyMasks( 179 TypePropertyMask.newBuilder() 180 .setSchemaType( 181 GetByDocumentIdRequest.PROJECTION_SCHEMA_TYPE_WILDCARD)) 182 .build(); 183 184 private final ReadWriteLock mReadWriteLock = new ReentrantReadWriteLock(); 185 private final OptimizeStrategy mOptimizeStrategy; 186 private final AppSearchConfig mConfig; 187 188 @GuardedBy("mReadWriteLock") 189 @VisibleForTesting 190 final IcingSearchEngine mIcingSearchEngineLocked; 191 192 @GuardedBy("mReadWriteLock") 193 private final SchemaCache mSchemaCacheLocked = new SchemaCache(); 194 195 // This map contains namespaces for all package-database prefixes. All values in the map are 196 // prefixed with the package-database prefix. 197 // TODO(b/172360376): Check if this can be replaced with an ArrayMap 198 @GuardedBy("mReadWriteLock") 199 private final Map<String, Set<String>> mNamespaceMapLocked = new HashMap<>(); 200 201 /** Maps package name to active document count. */ 202 @GuardedBy("mReadWriteLock") 203 private final Map<String, Integer> mDocumentCountMapLocked = new ArrayMap<>(); 204 205 // Maps packages to the set of valid nextPageTokens that the package can manipulate. A token 206 // is unique and constant per query (i.e. the same token '123' is used to iterate through 207 // pages of search results). The tokens themselves are generated and tracked by 208 // IcingSearchEngine. IcingSearchEngine considers a token valid and won't be reused 209 // until we call invalidateNextPageToken on the token. 210 // 211 // Note that we synchronize on itself because the nextPageToken cache is checked at 212 // query-time, and queries are done in parallel with a read lock. Ideally, this would be 213 // guarded by the normal mReadWriteLock.writeLock, but ReentrantReadWriteLocks can't upgrade 214 // read to write locks. This lock should be acquired at the smallest scope possible. 215 // mReadWriteLock is a higher-level lock, so calls shouldn't be made out 216 // to any functions that grab the lock. 217 @GuardedBy("mNextPageTokensLocked") 218 private final Map<String, Set<Long>> mNextPageTokensLocked = new ArrayMap<>(); 219 220 private final ObserverManager mObserverManager = new ObserverManager(); 221 222 /** 223 * VisibilityStore will be used in {@link #setSchema} and {@link #getSchema} to store and query 224 * visibility information. But to create a {@link VisibilityStore}, it will call {@link 225 * #setSchema} and {@link #getSchema} to get the visibility schema. Make it nullable to avoid 226 * call it before we actually create it. 227 */ 228 @Nullable 229 @VisibleForTesting 230 @GuardedBy("mReadWriteLock") 231 final VisibilityStore mVisibilityStoreLocked; 232 233 @Nullable 234 @GuardedBy("mReadWriteLock") 235 private final VisibilityChecker mVisibilityCheckerLocked; 236 237 /** 238 * The counter to check when to call {@link #checkForOptimize}. The interval is {@link 239 * #CHECK_OPTIMIZE_INTERVAL}. 240 */ 241 @GuardedBy("mReadWriteLock") 242 private int mOptimizeIntervalCountLocked = 0; 243 244 /** Whether this instance has been closed, and therefore unusable. */ 245 @GuardedBy("mReadWriteLock") 246 private boolean mClosedLocked = false; 247 248 /** 249 * Creates and initializes an instance of {@link AppSearchImpl} which writes data to the given 250 * folder. 251 * 252 * <p>Clients can pass a {@link AppSearchLogger} here through their AppSearchSession, but it 253 * can't be saved inside {@link AppSearchImpl}, because the impl will be shared by all the 254 * sessions for the same package in JetPack. 255 * 256 * <p>Instead, logger instance needs to be passed to each individual method, like create, query 257 * and putDocument. 258 * 259 * @param initStatsBuilder collects stats for initialization if provided. 260 * @param visibilityChecker The {@link VisibilityChecker} that check whether the caller has 261 * access to aa specific schema. Pass null will lost that ability and global querier could 262 * only get their own data. 263 */ 264 @NonNull create( @onNull File icingDir, @NonNull AppSearchConfig config, @Nullable InitializeStats.Builder initStatsBuilder, @Nullable VisibilityChecker visibilityChecker, @NonNull OptimizeStrategy optimizeStrategy)265 public static AppSearchImpl create( 266 @NonNull File icingDir, 267 @NonNull AppSearchConfig config, 268 @Nullable InitializeStats.Builder initStatsBuilder, 269 @Nullable VisibilityChecker visibilityChecker, 270 @NonNull OptimizeStrategy optimizeStrategy) 271 throws AppSearchException { 272 return new AppSearchImpl( 273 icingDir, config, initStatsBuilder, optimizeStrategy, visibilityChecker); 274 } 275 276 /** 277 * @param initStatsBuilder collects stats for initialization if provided. 278 */ AppSearchImpl( @onNull File icingDir, @NonNull AppSearchConfig config, @Nullable InitializeStats.Builder initStatsBuilder, @NonNull OptimizeStrategy optimizeStrategy, @Nullable VisibilityChecker visibilityChecker)279 private AppSearchImpl( 280 @NonNull File icingDir, 281 @NonNull AppSearchConfig config, 282 @Nullable InitializeStats.Builder initStatsBuilder, 283 @NonNull OptimizeStrategy optimizeStrategy, 284 @Nullable VisibilityChecker visibilityChecker) 285 throws AppSearchException { 286 Objects.requireNonNull(icingDir); 287 mConfig = Objects.requireNonNull(config); 288 mOptimizeStrategy = Objects.requireNonNull(optimizeStrategy); 289 mVisibilityCheckerLocked = visibilityChecker; 290 291 mReadWriteLock.writeLock().lock(); 292 try { 293 // We synchronize here because we don't want to call IcingSearchEngine.initialize() more 294 // than once. It's unnecessary and can be a costly operation. 295 IcingSearchEngineOptions options = 296 IcingSearchEngineOptions.newBuilder() 297 .setBaseDir(icingDir.getAbsolutePath()) 298 .setMaxTokenLength(mConfig.getMaxTokenLength()) 299 .setIndexMergeSize(mConfig.getIndexMergeSize()) 300 .setDocumentStoreNamespaceIdFingerprint( 301 mConfig.getDocumentStoreNamespaceIdFingerprint()) 302 .setOptimizeRebuildIndexThreshold( 303 mConfig.getOptimizeRebuildIndexThreshold()) 304 .setCompressionLevel(mConfig.getCompressionLevel()) 305 .setAllowCircularSchemaDefinitions( 306 mConfig.getAllowCircularSchemaDefinitions()) 307 .setPreMappingFbv(mConfig.getUsePreMappingWithFileBackedVector()) 308 .setUsePersistentHashMap(mConfig.getUsePersistentHashMap()) 309 .setIntegerIndexBucketSplitThreshold( 310 mConfig.getIntegerIndexBucketSplitThreshold()) 311 .setLiteIndexSortAtIndexing(mConfig.getLiteIndexSortAtIndexing()) 312 .setLiteIndexSortSize(mConfig.getLiteIndexSortSize()) 313 .setUseNewQualifiedIdJoinIndex(mConfig.getUseNewQualifiedIdJoinIndex()) 314 .setBuildPropertyExistenceMetadataHits( 315 mConfig.getBuildPropertyExistenceMetadataHits()) 316 .build(); 317 LogUtil.piiTrace(TAG, "Constructing IcingSearchEngine, request", options); 318 mIcingSearchEngineLocked = new IcingSearchEngine(options); 319 LogUtil.piiTrace( 320 TAG, 321 "Constructing IcingSearchEngine, response", 322 Objects.hashCode(mIcingSearchEngineLocked)); 323 324 // The core initialization procedure. If any part of this fails, we bail into 325 // resetLocked(), deleting all data (but hopefully allowing AppSearchImpl to come up). 326 try { 327 LogUtil.piiTrace(TAG, "icingSearchEngine.initialize, request"); 328 InitializeResultProto initializeResultProto = mIcingSearchEngineLocked.initialize(); 329 LogUtil.piiTrace( 330 TAG, 331 "icingSearchEngine.initialize, response", 332 initializeResultProto.getStatus(), 333 initializeResultProto); 334 335 if (initStatsBuilder != null) { 336 initStatsBuilder 337 .setStatusCode( 338 statusProtoToResultCode(initializeResultProto.getStatus())) 339 // TODO(b/173532925) how to get DeSyncs value 340 .setHasDeSync(false); 341 AppSearchLoggerHelper.copyNativeStats( 342 initializeResultProto.getInitializeStats(), initStatsBuilder); 343 } 344 checkSuccess(initializeResultProto.getStatus()); 345 346 // Read all protos we need to construct AppSearchImpl's cache maps 347 long prepareSchemaAndNamespacesLatencyStartMillis = SystemClock.elapsedRealtime(); 348 SchemaProto schemaProto = getSchemaProtoLocked(); 349 350 LogUtil.piiTrace(TAG, "init:getAllNamespaces, request"); 351 GetAllNamespacesResultProto getAllNamespacesResultProto = 352 mIcingSearchEngineLocked.getAllNamespaces(); 353 LogUtil.piiTrace( 354 TAG, 355 "init:getAllNamespaces, response", 356 getAllNamespacesResultProto.getNamespacesCount(), 357 getAllNamespacesResultProto); 358 359 StorageInfoProto storageInfoProto = getRawStorageInfoProto(); 360 361 // Log the time it took to read the data that goes into the cache maps 362 if (initStatsBuilder != null) { 363 // In case there is some error for getAllNamespaces, we can still 364 // set the latency for preparation. 365 // If there is no error, the value will be overridden by the actual one later. 366 initStatsBuilder 367 .setStatusCode( 368 statusProtoToResultCode( 369 getAllNamespacesResultProto.getStatus())) 370 .setPrepareSchemaAndNamespacesLatencyMillis( 371 (int) 372 (SystemClock.elapsedRealtime() 373 - prepareSchemaAndNamespacesLatencyStartMillis)); 374 } 375 checkSuccess(getAllNamespacesResultProto.getStatus()); 376 377 // Populate schema map 378 List<SchemaTypeConfigProto> schemaProtoTypesList = schemaProto.getTypesList(); 379 for (int i = 0; i < schemaProtoTypesList.size(); i++) { 380 SchemaTypeConfigProto schema = schemaProtoTypesList.get(i); 381 String prefixedSchemaType = schema.getSchemaType(); 382 mSchemaCacheLocked.addToSchemaMap(getPrefix(prefixedSchemaType), schema); 383 } 384 385 // Populate schema parent-to-children map 386 mSchemaCacheLocked.rebuildSchemaParentToChildrenMap(); 387 388 // Populate namespace map 389 List<String> prefixedNamespaceList = 390 getAllNamespacesResultProto.getNamespacesList(); 391 for (int i = 0; i < prefixedNamespaceList.size(); i++) { 392 String prefixedNamespace = prefixedNamespaceList.get(i); 393 addToMap(mNamespaceMapLocked, getPrefix(prefixedNamespace), prefixedNamespace); 394 } 395 396 // Populate document count map 397 rebuildDocumentCountMapLocked(storageInfoProto); 398 399 // logging prepare_schema_and_namespaces latency 400 if (initStatsBuilder != null) { 401 initStatsBuilder.setPrepareSchemaAndNamespacesLatencyMillis( 402 (int) 403 (SystemClock.elapsedRealtime() 404 - prepareSchemaAndNamespacesLatencyStartMillis)); 405 } 406 407 LogUtil.piiTrace(TAG, "Init completed successfully"); 408 409 } catch (AppSearchException e) { 410 // Some error. Reset and see if it fixes it. 411 Log.e(TAG, "Error initializing, resetting IcingSearchEngine.", e); 412 if (initStatsBuilder != null) { 413 initStatsBuilder.setStatusCode(e.getResultCode()); 414 } 415 resetLocked(initStatsBuilder); 416 } 417 418 long prepareVisibilityStoreLatencyStartMillis = SystemClock.elapsedRealtime(); 419 mVisibilityStoreLocked = new VisibilityStore(this); 420 long prepareVisibilityStoreLatencyEndMillis = SystemClock.elapsedRealtime(); 421 if (initStatsBuilder != null) { 422 initStatsBuilder.setPrepareVisibilityStoreLatencyMillis( 423 (int) 424 (prepareVisibilityStoreLatencyEndMillis 425 - prepareVisibilityStoreLatencyStartMillis)); 426 } 427 } finally { 428 mReadWriteLock.writeLock().unlock(); 429 } 430 } 431 432 @GuardedBy("mReadWriteLock") throwIfClosedLocked()433 private void throwIfClosedLocked() { 434 if (mClosedLocked) { 435 throw new IllegalStateException("Trying to use a closed AppSearchImpl instance."); 436 } 437 } 438 439 /** 440 * Persists data to disk and closes the instance. 441 * 442 * <p>This instance is no longer usable after it's been closed. Call {@link #create} to create a 443 * new, usable instance. 444 */ 445 @Override close()446 public void close() { 447 mReadWriteLock.writeLock().lock(); 448 try { 449 if (mClosedLocked) { 450 return; 451 } 452 persistToDisk(PersistType.Code.FULL); 453 LogUtil.piiTrace(TAG, "icingSearchEngine.close, request"); 454 mIcingSearchEngineLocked.close(); 455 LogUtil.piiTrace(TAG, "icingSearchEngine.close, response"); 456 mClosedLocked = true; 457 } catch (AppSearchException e) { 458 Log.w(TAG, "Error when closing AppSearchImpl.", e); 459 } finally { 460 mReadWriteLock.writeLock().unlock(); 461 } 462 } 463 464 /** 465 * Updates the AppSearch schema for this app. 466 * 467 * <p>This method belongs to mutate group. 468 * 469 * @param packageName The package name that owns the schemas. 470 * @param databaseName The name of the database where this schema lives. 471 * @param schemas Schemas to set for this app. 472 * @param visibilityConfigs {@link InternalVisibilityConfig}s that contain all visibility 473 * setting information for those schemas has user custom settings. Other schemas in the list 474 * that don't has a {@link InternalVisibilityConfig} will be treated as having the default 475 * visibility, which is accessible by the system and no other packages. 476 * @param forceOverride Whether to force-apply the schema even if it is incompatible. Documents 477 * which do not comply with the new schema will be deleted. 478 * @param version The overall version number of the request. 479 * @param setSchemaStatsBuilder Builder for {@link SetSchemaStats} to hold stats for setSchema 480 * @return A success {@link InternalSetSchemaResponse} with a {@link SetSchemaResponse}. Or a 481 * failed {@link InternalSetSchemaResponse} if this call contains incompatible change. The 482 * {@link SetSchemaResponse} in the failed {@link InternalSetSchemaResponse} contains which 483 * type is incompatible. You need to check the status by {@link 484 * InternalSetSchemaResponse#isSuccess()}. 485 * @throws AppSearchException On IcingSearchEngine error. If the status code is 486 * FAILED_PRECONDITION for the incompatible change, the exception will be converted to the 487 * SetSchemaResponse. 488 */ 489 @NonNull setSchema( @onNull String packageName, @NonNull String databaseName, @NonNull List<AppSearchSchema> schemas, @NonNull List<InternalVisibilityConfig> visibilityConfigs, boolean forceOverride, int version, @Nullable SetSchemaStats.Builder setSchemaStatsBuilder)490 public InternalSetSchemaResponse setSchema( 491 @NonNull String packageName, 492 @NonNull String databaseName, 493 @NonNull List<AppSearchSchema> schemas, 494 @NonNull List<InternalVisibilityConfig> visibilityConfigs, 495 boolean forceOverride, 496 int version, 497 @Nullable SetSchemaStats.Builder setSchemaStatsBuilder) 498 throws AppSearchException { 499 long javaLockAcquisitionLatencyStartMillis = SystemClock.elapsedRealtime(); 500 mReadWriteLock.writeLock().lock(); 501 try { 502 throwIfClosedLocked(); 503 if (setSchemaStatsBuilder != null) { 504 setSchemaStatsBuilder.setJavaLockAcquisitionLatencyMillis( 505 (int) 506 (SystemClock.elapsedRealtime() 507 - javaLockAcquisitionLatencyStartMillis)); 508 } 509 if (mObserverManager.isPackageObserved(packageName)) { 510 return doSetSchemaWithChangeNotificationLocked( 511 packageName, 512 databaseName, 513 schemas, 514 visibilityConfigs, 515 forceOverride, 516 version, 517 setSchemaStatsBuilder); 518 } else { 519 return doSetSchemaNoChangeNotificationLocked( 520 packageName, 521 databaseName, 522 schemas, 523 visibilityConfigs, 524 forceOverride, 525 version, 526 setSchemaStatsBuilder); 527 } 528 } finally { 529 mReadWriteLock.writeLock().unlock(); 530 } 531 } 532 533 /** 534 * Updates the AppSearch schema for this app, dispatching change notifications. 535 * 536 * @see #setSchema 537 * @see #doSetSchemaNoChangeNotificationLocked 538 */ 539 @GuardedBy("mReadWriteLock") 540 @NonNull doSetSchemaWithChangeNotificationLocked( @onNull String packageName, @NonNull String databaseName, @NonNull List<AppSearchSchema> schemas, @NonNull List<InternalVisibilityConfig> visibilityConfigs, boolean forceOverride, int version, @Nullable SetSchemaStats.Builder setSchemaStatsBuilder)541 private InternalSetSchemaResponse doSetSchemaWithChangeNotificationLocked( 542 @NonNull String packageName, 543 @NonNull String databaseName, 544 @NonNull List<AppSearchSchema> schemas, 545 @NonNull List<InternalVisibilityConfig> visibilityConfigs, 546 boolean forceOverride, 547 int version, 548 @Nullable SetSchemaStats.Builder setSchemaStatsBuilder) 549 throws AppSearchException { 550 // First, capture the old state of the system. This includes the old schema as well as 551 // whether each registered observer can access each type. Once VisibilityStore is updated 552 // by the setSchema call, the information of which observers could see which types will be 553 // lost. 554 long getOldSchemaStartTimeMillis = SystemClock.elapsedRealtime(); 555 GetSchemaResponse oldSchema = 556 getSchema( 557 packageName, 558 databaseName, 559 // A CallerAccess object for internal use that has local access to this 560 // database. 561 new CallerAccess(/* callingPackageName= */ packageName)); 562 long getOldSchemaEndTimeMillis = SystemClock.elapsedRealtime(); 563 if (setSchemaStatsBuilder != null) { 564 setSchemaStatsBuilder 565 .setIsPackageObserved(true) 566 .setGetOldSchemaLatencyMillis( 567 (int) (getOldSchemaEndTimeMillis - getOldSchemaStartTimeMillis)); 568 } 569 570 long getOldSchemaObserverStartTimeMillis = SystemClock.elapsedRealtime(); 571 // Cache some lookup tables to help us work with the old schema 572 Set<AppSearchSchema> oldSchemaTypes = oldSchema.getSchemas(); 573 Map<String, AppSearchSchema> oldSchemaNameToType = new ArrayMap<>(oldSchemaTypes.size()); 574 // Maps unprefixed schema name to the set of listening packages that had visibility into 575 // that type under the old schema. 576 Map<String, Set<String>> oldSchemaNameToVisibleListeningPackage = 577 new ArrayMap<>(oldSchemaTypes.size()); 578 for (AppSearchSchema oldSchemaType : oldSchemaTypes) { 579 String oldSchemaName = oldSchemaType.getSchemaType(); 580 oldSchemaNameToType.put(oldSchemaName, oldSchemaType); 581 oldSchemaNameToVisibleListeningPackage.put( 582 oldSchemaName, 583 mObserverManager.getObserversForSchemaType( 584 packageName, 585 databaseName, 586 oldSchemaName, 587 mVisibilityStoreLocked, 588 mVisibilityCheckerLocked)); 589 } 590 int getOldSchemaObserverLatencyMillis = 591 (int) (SystemClock.elapsedRealtime() - getOldSchemaObserverStartTimeMillis); 592 593 // Apply the new schema 594 InternalSetSchemaResponse internalSetSchemaResponse = 595 doSetSchemaNoChangeNotificationLocked( 596 packageName, 597 databaseName, 598 schemas, 599 visibilityConfigs, 600 forceOverride, 601 version, 602 setSchemaStatsBuilder); 603 604 // This check is needed wherever setSchema is called to detect soft errors which do not 605 // throw an exception but also prevent the schema from actually being applied. 606 if (!internalSetSchemaResponse.isSuccess()) { 607 return internalSetSchemaResponse; 608 } 609 610 long getNewSchemaObserverStartTimeMillis = SystemClock.elapsedRealtime(); 611 // Cache some lookup tables to help us work with the new schema 612 Map<String, AppSearchSchema> newSchemaNameToType = new ArrayMap<>(schemas.size()); 613 // Maps unprefixed schema name to the set of listening packages that have visibility into 614 // that type under the new schema. 615 Map<String, Set<String>> newSchemaNameToVisibleListeningPackage = 616 new ArrayMap<>(schemas.size()); 617 for (AppSearchSchema newSchemaType : schemas) { 618 String newSchemaName = newSchemaType.getSchemaType(); 619 newSchemaNameToType.put(newSchemaName, newSchemaType); 620 newSchemaNameToVisibleListeningPackage.put( 621 newSchemaName, 622 mObserverManager.getObserversForSchemaType( 623 packageName, 624 databaseName, 625 newSchemaName, 626 mVisibilityStoreLocked, 627 mVisibilityCheckerLocked)); 628 } 629 long getNewSchemaObserverEndTimeMillis = SystemClock.elapsedRealtime(); 630 if (setSchemaStatsBuilder != null) { 631 setSchemaStatsBuilder.setGetObserverLatencyMillis( 632 getOldSchemaObserverLatencyMillis 633 + (int) 634 (getNewSchemaObserverEndTimeMillis 635 - getNewSchemaObserverStartTimeMillis)); 636 } 637 638 long preparingChangeNotificationStartTimeMillis = SystemClock.elapsedRealtime(); 639 // Create a unified set of all schema names mentioned in either the old or new schema. 640 Set<String> allSchemaNames = new ArraySet<>(oldSchemaNameToType.keySet()); 641 allSchemaNames.addAll(newSchemaNameToType.keySet()); 642 643 // Perform the diff between the old and new schema. 644 for (String schemaName : allSchemaNames) { 645 final AppSearchSchema contentBefore = oldSchemaNameToType.get(schemaName); 646 final AppSearchSchema contentAfter = newSchemaNameToType.get(schemaName); 647 648 final boolean existBefore = (contentBefore != null); 649 final boolean existAfter = (contentAfter != null); 650 651 // This should never happen 652 if (!existBefore && !existAfter) { 653 continue; 654 } 655 656 boolean contentsChanged = true; 657 if (contentBefore != null && contentBefore.equals(contentAfter)) { 658 contentsChanged = false; 659 } 660 661 Set<String> oldVisibleListeners = 662 oldSchemaNameToVisibleListeningPackage.get(schemaName); 663 Set<String> newVisibleListeners = 664 newSchemaNameToVisibleListeningPackage.get(schemaName); 665 Set<String> allListeningPackages = new ArraySet<>(oldVisibleListeners); 666 if (newVisibleListeners != null) { 667 allListeningPackages.addAll(newVisibleListeners); 668 } 669 670 // Now that we've computed the relationship between the old and new schema, we go 671 // observer by observer and consider the observer's own personal view of the schema. 672 for (String listeningPackageName : allListeningPackages) { 673 // Figure out the visibility 674 final boolean visibleBefore = 675 (existBefore 676 && oldVisibleListeners != null 677 && oldVisibleListeners.contains(listeningPackageName)); 678 final boolean visibleAfter = 679 (existAfter 680 && newVisibleListeners != null 681 && newVisibleListeners.contains(listeningPackageName)); 682 683 // Now go through the truth table of all the relevant flags. 684 // visibleBefore and visibleAfter take into account existBefore and existAfter, so 685 // we can stop worrying about existBefore and existAfter. 686 boolean sendNotification = false; 687 if (visibleBefore && visibleAfter && contentsChanged) { 688 sendNotification = true; // Type configuration was modified 689 } else if (!visibleBefore && visibleAfter) { 690 sendNotification = true; // Newly granted visibility or type was created 691 } else if (visibleBefore && !visibleAfter) { 692 sendNotification = true; // Revoked visibility or type was deleted 693 } else { 694 // No visibility before and no visibility after. Nothing to dispatch. 695 } 696 697 if (sendNotification) { 698 mObserverManager.onSchemaChange( 699 /* listeningPackageName= */ listeningPackageName, 700 /* targetPackageName= */ packageName, 701 /* databaseName= */ databaseName, 702 /* schemaName= */ schemaName); 703 } 704 } 705 } 706 if (setSchemaStatsBuilder != null) { 707 setSchemaStatsBuilder.setPreparingChangeNotificationLatencyMillis( 708 (int) 709 (SystemClock.elapsedRealtime() 710 - preparingChangeNotificationStartTimeMillis)); 711 } 712 713 return internalSetSchemaResponse; 714 } 715 716 /** 717 * Updates the AppSearch schema for this app, without dispatching change notifications. 718 * 719 * <p>This method can be used only when no one is observing {@code packageName}. 720 * 721 * @see #setSchema 722 * @see #doSetSchemaWithChangeNotificationLocked 723 */ 724 @GuardedBy("mReadWriteLock") 725 @NonNull doSetSchemaNoChangeNotificationLocked( @onNull String packageName, @NonNull String databaseName, @NonNull List<AppSearchSchema> schemas, @NonNull List<InternalVisibilityConfig> visibilityConfigs, boolean forceOverride, int version, @Nullable SetSchemaStats.Builder setSchemaStatsBuilder)726 private InternalSetSchemaResponse doSetSchemaNoChangeNotificationLocked( 727 @NonNull String packageName, 728 @NonNull String databaseName, 729 @NonNull List<AppSearchSchema> schemas, 730 @NonNull List<InternalVisibilityConfig> visibilityConfigs, 731 boolean forceOverride, 732 int version, 733 @Nullable SetSchemaStats.Builder setSchemaStatsBuilder) 734 throws AppSearchException { 735 long setRewriteSchemaLatencyStartTimeMillis = SystemClock.elapsedRealtime(); 736 SchemaProto.Builder existingSchemaBuilder = getSchemaProtoLocked().toBuilder(); 737 738 SchemaProto.Builder newSchemaBuilder = SchemaProto.newBuilder(); 739 for (int i = 0; i < schemas.size(); i++) { 740 AppSearchSchema schema = schemas.get(i); 741 SchemaTypeConfigProto schemaTypeProto = 742 SchemaToProtoConverter.toSchemaTypeConfigProto(schema, version); 743 newSchemaBuilder.addTypes(schemaTypeProto); 744 } 745 746 String prefix = createPrefix(packageName, databaseName); 747 // Combine the existing schema (which may have types from other prefixes) with this 748 // prefix's new schema. Modifies the existingSchemaBuilder. 749 RewrittenSchemaResults rewrittenSchemaResults = 750 rewriteSchema(prefix, existingSchemaBuilder, newSchemaBuilder.build()); 751 752 long rewriteSchemaEndTimeMillis = SystemClock.elapsedRealtime(); 753 if (setSchemaStatsBuilder != null) { 754 setSchemaStatsBuilder.setRewriteSchemaLatencyMillis( 755 (int) (rewriteSchemaEndTimeMillis - setRewriteSchemaLatencyStartTimeMillis)); 756 } 757 758 // Apply schema 759 long nativeLatencyStartTimeMillis = SystemClock.elapsedRealtime(); 760 SchemaProto finalSchema = existingSchemaBuilder.build(); 761 LogUtil.piiTrace(TAG, "setSchema, request", finalSchema.getTypesCount(), finalSchema); 762 SetSchemaResultProto setSchemaResultProto = 763 mIcingSearchEngineLocked.setSchema(finalSchema, forceOverride); 764 LogUtil.piiTrace( 765 TAG, "setSchema, response", setSchemaResultProto.getStatus(), setSchemaResultProto); 766 long nativeLatencyEndTimeMillis = SystemClock.elapsedRealtime(); 767 if (setSchemaStatsBuilder != null) { 768 setSchemaStatsBuilder 769 .setTotalNativeLatencyMillis( 770 (int) (nativeLatencyEndTimeMillis - nativeLatencyStartTimeMillis)) 771 .setStatusCode(statusProtoToResultCode(setSchemaResultProto.getStatus())); 772 AppSearchLoggerHelper.copyNativeStats(setSchemaResultProto, setSchemaStatsBuilder); 773 } 774 775 boolean isFailedPrecondition = 776 setSchemaResultProto.getStatus().getCode() == StatusProto.Code.FAILED_PRECONDITION; 777 // Determine whether it succeeded. 778 try { 779 checkSuccess(setSchemaResultProto.getStatus()); 780 } catch (AppSearchException e) { 781 // Swallow the exception for the incompatible change case. We will generate a failed 782 // InternalSetSchemaResponse for this case. 783 int deletedTypes = setSchemaResultProto.getDeletedSchemaTypesCount(); 784 int incompatibleTypes = setSchemaResultProto.getIncompatibleSchemaTypesCount(); 785 boolean isIncompatible = deletedTypes > 0 || incompatibleTypes > 0; 786 if (isFailedPrecondition && !forceOverride && isIncompatible) { 787 SetSchemaResponse setSchemaResponse = 788 SetSchemaResponseToProtoConverter.toSetSchemaResponse( 789 setSchemaResultProto, prefix); 790 String errorMessage = 791 "Schema is incompatible." 792 + "\n Deleted types: " 793 + setSchemaResponse.getDeletedTypes() 794 + "\n Incompatible types: " 795 + setSchemaResponse.getIncompatibleTypes(); 796 return newFailedSetSchemaResponse(setSchemaResponse, errorMessage); 797 } else { 798 throw e; 799 } 800 } 801 802 long saveVisibilitySettingStartTimeMillis = SystemClock.elapsedRealtime(); 803 // Update derived data structures. 804 for (SchemaTypeConfigProto schemaTypeConfigProto : 805 rewrittenSchemaResults.mRewrittenPrefixedTypes.values()) { 806 mSchemaCacheLocked.addToSchemaMap(prefix, schemaTypeConfigProto); 807 } 808 809 for (String schemaType : rewrittenSchemaResults.mDeletedPrefixedTypes) { 810 mSchemaCacheLocked.removeFromSchemaMap(prefix, schemaType); 811 } 812 813 mSchemaCacheLocked.rebuildSchemaParentToChildrenMapForPrefix(prefix); 814 815 // Since the constructor of VisibilityStore will set schema. Avoid call visibility 816 // store before we have already created it. 817 if (mVisibilityStoreLocked != null) { 818 // Add prefix to all visibility documents. 819 // Find out which Visibility document is deleted or changed to all-default settings. 820 // We need to remove them from Visibility Store. 821 Set<String> deprecatedVisibilityDocuments = 822 new ArraySet<>(rewrittenSchemaResults.mRewrittenPrefixedTypes.keySet()); 823 List<InternalVisibilityConfig> prefixedVisibilityConfigs = 824 new ArrayList<>(visibilityConfigs.size()); 825 for (int i = 0; i < visibilityConfigs.size(); i++) { 826 InternalVisibilityConfig visibilityConfig = visibilityConfigs.get(i); 827 // The VisibilityConfig is controlled by the client and it's untrusted but we 828 // make it safe by appending a prefix. 829 // We must control the package-database prefix. Therefore even if the client 830 // fake the id, they can only mess their own app. That's totally allowed and 831 // they can do this via the public API too. 832 // TODO(b/275592563): Move prefixing into VisibilityConfig.createVisibilityDocument 833 // and createVisibilityOverlay 834 String schemaType = visibilityConfig.getSchemaType(); 835 String prefixedSchemaType = prefix + schemaType; 836 prefixedVisibilityConfigs.add( 837 new InternalVisibilityConfig.Builder(visibilityConfig) 838 .setSchemaType(prefixedSchemaType) 839 .build()); 840 // This schema has visibility settings. We should keep it from the removal list. 841 deprecatedVisibilityDocuments.remove(visibilityConfig.getSchemaType()); 842 } 843 // Now deprecatedVisibilityDocuments contains those existing schemas that has 844 // all-default visibility settings, add deleted schemas. That's all we need to 845 // remove. 846 deprecatedVisibilityDocuments.addAll(rewrittenSchemaResults.mDeletedPrefixedTypes); 847 mVisibilityStoreLocked.removeVisibility(deprecatedVisibilityDocuments); 848 mVisibilityStoreLocked.setVisibility(prefixedVisibilityConfigs); 849 } 850 long saveVisibilitySettingEndTimeMillis = SystemClock.elapsedRealtime(); 851 if (setSchemaStatsBuilder != null) { 852 setSchemaStatsBuilder.setVisibilitySettingLatencyMillis( 853 (int) 854 (saveVisibilitySettingEndTimeMillis 855 - saveVisibilitySettingStartTimeMillis)); 856 } 857 858 long convertToResponseStartTimeMillis = SystemClock.elapsedRealtime(); 859 InternalSetSchemaResponse setSchemaResponse = 860 newSuccessfulSetSchemaResponse( 861 SetSchemaResponseToProtoConverter.toSetSchemaResponse( 862 setSchemaResultProto, prefix)); 863 long convertToResponseEndTimeMillis = SystemClock.elapsedRealtime(); 864 if (setSchemaStatsBuilder != null) { 865 setSchemaStatsBuilder.setConvertToResponseLatencyMillis( 866 (int) (convertToResponseEndTimeMillis - convertToResponseStartTimeMillis)); 867 } 868 return setSchemaResponse; 869 } 870 871 /** 872 * Retrieves the AppSearch schema for this package name, database. 873 * 874 * <p>This method belongs to query group. 875 * 876 * @param packageName Package that owns the requested {@link AppSearchSchema} instances. 877 * @param databaseName Database that owns the requested {@link AppSearchSchema} instances. 878 * @param callerAccess Visibility access info of the calling app 879 * @throws AppSearchException on IcingSearchEngine error. 880 */ 881 @NonNull getSchema( @onNull String packageName, @NonNull String databaseName, @NonNull CallerAccess callerAccess)882 public GetSchemaResponse getSchema( 883 @NonNull String packageName, 884 @NonNull String databaseName, 885 @NonNull CallerAccess callerAccess) 886 throws AppSearchException { 887 mReadWriteLock.readLock().lock(); 888 try { 889 throwIfClosedLocked(); 890 891 SchemaProto fullSchema = getSchemaProtoLocked(); 892 String prefix = createPrefix(packageName, databaseName); 893 GetSchemaResponse.Builder responseBuilder = new GetSchemaResponse.Builder(); 894 for (int i = 0; i < fullSchema.getTypesCount(); i++) { 895 // Check that this type belongs to the requested app and that the caller has 896 // access to it. 897 SchemaTypeConfigProto typeConfig = fullSchema.getTypes(i); 898 String prefixedSchemaType = typeConfig.getSchemaType(); 899 String typePrefix = getPrefix(prefixedSchemaType); 900 if (!prefix.equals(typePrefix)) { 901 // This schema type doesn't belong to the database we're querying for. 902 continue; 903 } 904 if (!VisibilityUtil.isSchemaSearchableByCaller( 905 callerAccess, 906 packageName, 907 prefixedSchemaType, 908 mVisibilityStoreLocked, 909 mVisibilityCheckerLocked)) { 910 // Caller doesn't have access to this type. 911 continue; 912 } 913 914 // Rewrite SchemaProto.types.schema_type 915 SchemaTypeConfigProto.Builder typeConfigBuilder = typeConfig.toBuilder(); 916 PrefixUtil.removePrefixesFromSchemaType(typeConfigBuilder); 917 AppSearchSchema schema = 918 SchemaToProtoConverter.toAppSearchSchema(typeConfigBuilder); 919 920 responseBuilder.setVersion(typeConfig.getVersion()); 921 responseBuilder.addSchema(schema); 922 923 // Populate visibility info. Since the constructor of VisibilityStore will get 924 // schema. Avoid call visibility store before we have already created it. 925 if (mVisibilityStoreLocked != null) { 926 String typeName = typeConfig.getSchemaType().substring(typePrefix.length()); 927 InternalVisibilityConfig visibilityConfig = 928 mVisibilityStoreLocked.getVisibility(prefixedSchemaType); 929 if (visibilityConfig != null) { 930 if (visibilityConfig.isNotDisplayedBySystem()) { 931 responseBuilder.addSchemaTypeNotDisplayedBySystem(typeName); 932 } 933 List<PackageIdentifier> packageIdentifiers = 934 visibilityConfig.getVisibilityConfig().getAllowedPackages(); 935 if (!packageIdentifiers.isEmpty()) { 936 responseBuilder.setSchemaTypeVisibleToPackages( 937 typeName, new ArraySet<>(packageIdentifiers)); 938 } 939 Set<Set<Integer>> visibleToPermissions = 940 visibilityConfig.getVisibilityConfig().getRequiredPermissions(); 941 if (!visibleToPermissions.isEmpty()) { 942 Set<Set<Integer>> visibleToPermissionsSet = 943 new ArraySet<>(visibleToPermissions.size()); 944 for (Set<Integer> permissionList : visibleToPermissions) { 945 visibleToPermissionsSet.add(new ArraySet<>(permissionList)); 946 } 947 948 responseBuilder.setRequiredPermissionsForSchemaTypeVisibility( 949 typeName, visibleToPermissionsSet); 950 } 951 952 // Check for Visibility properties from the overlay 953 PackageIdentifier publiclyVisibleFromPackage = 954 visibilityConfig 955 .getVisibilityConfig() 956 .getPubliclyVisibleTargetPackage(); 957 if (publiclyVisibleFromPackage != null) { 958 responseBuilder.setPubliclyVisibleSchema( 959 typeName, publiclyVisibleFromPackage); 960 } 961 Set<SchemaVisibilityConfig> visibleToConfigs = 962 visibilityConfig.getVisibleToConfigs(); 963 if (!visibleToConfigs.isEmpty()) { 964 responseBuilder.setSchemaTypeVisibleToConfigs( 965 typeName, visibleToConfigs); 966 } 967 } 968 } 969 } 970 return responseBuilder.build(); 971 972 } finally { 973 mReadWriteLock.readLock().unlock(); 974 } 975 } 976 977 /** 978 * Retrieves the list of namespaces with at least one document for this package name, database. 979 * 980 * <p>This method belongs to query group. 981 * 982 * @param packageName Package name that owns this schema 983 * @param databaseName The name of the database where this schema lives. 984 * @throws AppSearchException on IcingSearchEngine error. 985 */ 986 @NonNull getNamespaces(@onNull String packageName, @NonNull String databaseName)987 public List<String> getNamespaces(@NonNull String packageName, @NonNull String databaseName) 988 throws AppSearchException { 989 mReadWriteLock.readLock().lock(); 990 try { 991 throwIfClosedLocked(); 992 LogUtil.piiTrace(TAG, "getAllNamespaces, request"); 993 // We can't just use mNamespaceMap here because we have no way to prune namespaces from 994 // mNamespaceMap when they have no more documents (e.g. after setting schema to empty or 995 // using deleteByQuery). 996 GetAllNamespacesResultProto getAllNamespacesResultProto = 997 mIcingSearchEngineLocked.getAllNamespaces(); 998 LogUtil.piiTrace( 999 TAG, 1000 "getAllNamespaces, response", 1001 getAllNamespacesResultProto.getNamespacesCount(), 1002 getAllNamespacesResultProto); 1003 checkSuccess(getAllNamespacesResultProto.getStatus()); 1004 String prefix = createPrefix(packageName, databaseName); 1005 List<String> results = new ArrayList<>(); 1006 for (int i = 0; i < getAllNamespacesResultProto.getNamespacesCount(); i++) { 1007 String prefixedNamespace = getAllNamespacesResultProto.getNamespaces(i); 1008 if (prefixedNamespace.startsWith(prefix)) { 1009 results.add(prefixedNamespace.substring(prefix.length())); 1010 } 1011 } 1012 return results; 1013 } finally { 1014 mReadWriteLock.readLock().unlock(); 1015 } 1016 } 1017 1018 /** 1019 * Adds a document to the AppSearch index. 1020 * 1021 * <p>This method belongs to mutate group. 1022 * 1023 * @param packageName The package name that owns this document. 1024 * @param databaseName The databaseName this document resides in. 1025 * @param document The document to index. 1026 * @param sendChangeNotifications Whether to dispatch {@link 1027 * android.app.appsearch.observer.DocumentChangeInfo} messages to observers for this change. 1028 * @throws AppSearchException on IcingSearchEngine error. 1029 */ putDocument( @onNull String packageName, @NonNull String databaseName, @NonNull GenericDocument document, boolean sendChangeNotifications, @Nullable AppSearchLogger logger)1030 public void putDocument( 1031 @NonNull String packageName, 1032 @NonNull String databaseName, 1033 @NonNull GenericDocument document, 1034 boolean sendChangeNotifications, 1035 @Nullable AppSearchLogger logger) 1036 throws AppSearchException { 1037 PutDocumentStats.Builder pStatsBuilder = null; 1038 if (logger != null) { 1039 pStatsBuilder = new PutDocumentStats.Builder(packageName, databaseName); 1040 } 1041 long totalStartTimeMillis = SystemClock.elapsedRealtime(); 1042 1043 mReadWriteLock.writeLock().lock(); 1044 try { 1045 throwIfClosedLocked(); 1046 1047 // Generate Document Proto 1048 long generateDocumentProtoStartTimeMillis = SystemClock.elapsedRealtime(); 1049 DocumentProto.Builder documentBuilder = 1050 GenericDocumentToProtoConverter.toDocumentProto(document).toBuilder(); 1051 long generateDocumentProtoEndTimeMillis = SystemClock.elapsedRealtime(); 1052 1053 // Rewrite Document Type 1054 long rewriteDocumentTypeStartTimeMillis = SystemClock.elapsedRealtime(); 1055 String prefix = createPrefix(packageName, databaseName); 1056 addPrefixToDocument(documentBuilder, prefix); 1057 long rewriteDocumentTypeEndTimeMillis = SystemClock.elapsedRealtime(); 1058 DocumentProto finalDocument = documentBuilder.build(); 1059 1060 // Check limits 1061 int newDocumentCount = 1062 enforceLimitConfigLocked( 1063 packageName, finalDocument.getUri(), finalDocument.getSerializedSize()); 1064 1065 // Insert document 1066 LogUtil.piiTrace(TAG, "putDocument, request", finalDocument.getUri(), finalDocument); 1067 PutResultProto putResultProto = mIcingSearchEngineLocked.put(finalDocument); 1068 LogUtil.piiTrace( 1069 TAG, "putDocument, response", putResultProto.getStatus(), putResultProto); 1070 1071 // Logging stats 1072 if (pStatsBuilder != null) { 1073 pStatsBuilder 1074 .setStatusCode(statusProtoToResultCode(putResultProto.getStatus())) 1075 .setGenerateDocumentProtoLatencyMillis( 1076 (int) 1077 (generateDocumentProtoEndTimeMillis 1078 - generateDocumentProtoStartTimeMillis)) 1079 .setRewriteDocumentTypesLatencyMillis( 1080 (int) 1081 (rewriteDocumentTypeEndTimeMillis 1082 - rewriteDocumentTypeStartTimeMillis)); 1083 AppSearchLoggerHelper.copyNativeStats( 1084 putResultProto.getPutDocumentStats(), pStatsBuilder); 1085 } 1086 1087 checkSuccess(putResultProto.getStatus()); 1088 1089 // Only update caches if the document is successfully put to Icing. 1090 addToMap(mNamespaceMapLocked, prefix, finalDocument.getNamespace()); 1091 mDocumentCountMapLocked.put(packageName, newDocumentCount); 1092 1093 // Prepare notifications 1094 if (sendChangeNotifications) { 1095 mObserverManager.onDocumentChange( 1096 packageName, 1097 databaseName, 1098 document.getNamespace(), 1099 document.getSchemaType(), 1100 document.getId(), 1101 mVisibilityStoreLocked, 1102 mVisibilityCheckerLocked); 1103 } 1104 } finally { 1105 mReadWriteLock.writeLock().unlock(); 1106 1107 if (pStatsBuilder != null && logger != null) { 1108 long totalEndTimeMillis = SystemClock.elapsedRealtime(); 1109 pStatsBuilder.setTotalLatencyMillis( 1110 (int) (totalEndTimeMillis - totalStartTimeMillis)); 1111 logger.logStats(pStatsBuilder.build()); 1112 } 1113 } 1114 } 1115 1116 /** 1117 * Checks that a new document can be added to the given packageName with the given serialized 1118 * size without violating our {@link LimitConfig}. 1119 * 1120 * @return the new count of documents for the given package, including the new document. 1121 * @throws AppSearchException with a code of {@link AppSearchResult#RESULT_OUT_OF_SPACE} if the 1122 * limits are violated by the new document. 1123 */ 1124 @GuardedBy("mReadWriteLock") enforceLimitConfigLocked(String packageName, String newDocUri, int newDocSize)1125 private int enforceLimitConfigLocked(String packageName, String newDocUri, int newDocSize) 1126 throws AppSearchException { 1127 // Limits check: size of document 1128 if (newDocSize > mConfig.getMaxDocumentSizeBytes()) { 1129 throw new AppSearchException( 1130 AppSearchResult.RESULT_OUT_OF_SPACE, 1131 "Document \"" 1132 + newDocUri 1133 + "\" for package \"" 1134 + packageName 1135 + "\" serialized to " 1136 + newDocSize 1137 + " bytes, which exceeds " 1138 + "limit of " 1139 + mConfig.getMaxDocumentSizeBytes() 1140 + " bytes"); 1141 } 1142 1143 // Limits check: number of documents 1144 Integer oldDocumentCount = mDocumentCountMapLocked.get(packageName); 1145 int newDocumentCount; 1146 if (oldDocumentCount == null) { 1147 newDocumentCount = 1; 1148 } else { 1149 newDocumentCount = oldDocumentCount + 1; 1150 } 1151 if (newDocumentCount > mConfig.getMaxDocumentCount()) { 1152 // Our management of mDocumentCountMapLocked doesn't account for document 1153 // replacements, so our counter might have overcounted if the app has replaced docs. 1154 // Rebuild the counter from StorageInfo in case this is so. 1155 // TODO(b/170371356): If Icing lib exposes something in the result which says 1156 // whether the document was a replacement, we could subtract 1 again after the put 1157 // to keep the count accurate. That would allow us to remove this code. 1158 rebuildDocumentCountMapLocked(getRawStorageInfoProto()); 1159 oldDocumentCount = mDocumentCountMapLocked.get(packageName); 1160 if (oldDocumentCount == null) { 1161 newDocumentCount = 1; 1162 } else { 1163 newDocumentCount = oldDocumentCount + 1; 1164 } 1165 } 1166 if (newDocumentCount > mConfig.getMaxDocumentCount()) { 1167 // Now we really can't fit it in, even accounting for replacements. 1168 throw new AppSearchException( 1169 AppSearchResult.RESULT_OUT_OF_SPACE, 1170 "Package \"" 1171 + packageName 1172 + "\" exceeded limit of " 1173 + mConfig.getMaxDocumentCount() 1174 + " documents. Some documents " 1175 + "must be removed to index additional ones."); 1176 } 1177 1178 return newDocumentCount; 1179 } 1180 1181 /** 1182 * Retrieves a document from the AppSearch index by namespace and document ID from any 1183 * application the caller is allowed to view 1184 * 1185 * <p>This method will handle both Icing engine errors as well as permission errors by throwing 1186 * an obfuscated RESULT_NOT_FOUND exception. This is done so the caller doesn't receive 1187 * information on whether or not a file they are not allowed to access exists or not. This is 1188 * different from the behavior of {@link #getDocument}. 1189 * 1190 * @param packageName The package that owns this document. 1191 * @param databaseName The databaseName this document resides in. 1192 * @param namespace The namespace this document resides in. 1193 * @param id The ID of the document to get. 1194 * @param typePropertyPaths A map of schema type to a list of property paths to return in the 1195 * result. 1196 * @param callerAccess Visibility access info of the calling app 1197 * @return The Document contents 1198 * @throws AppSearchException on IcingSearchEngine error or invalid permissions 1199 */ 1200 @NonNull globalGetDocument( @onNull String packageName, @NonNull String databaseName, @NonNull String namespace, @NonNull String id, @NonNull Map<String, List<String>> typePropertyPaths, @NonNull CallerAccess callerAccess)1201 public GenericDocument globalGetDocument( 1202 @NonNull String packageName, 1203 @NonNull String databaseName, 1204 @NonNull String namespace, 1205 @NonNull String id, 1206 @NonNull Map<String, List<String>> typePropertyPaths, 1207 @NonNull CallerAccess callerAccess) 1208 throws AppSearchException { 1209 mReadWriteLock.readLock().lock(); 1210 try { 1211 throwIfClosedLocked(); 1212 // We retrieve the document before checking for access, as we do not know which 1213 // schema the document is under. Schema is required for checking access 1214 DocumentProto documentProto; 1215 try { 1216 documentProto = 1217 getDocumentProtoByIdLocked( 1218 packageName, databaseName, namespace, id, typePropertyPaths); 1219 1220 if (!VisibilityUtil.isSchemaSearchableByCaller( 1221 callerAccess, 1222 packageName, 1223 documentProto.getSchema(), 1224 mVisibilityStoreLocked, 1225 mVisibilityCheckerLocked)) { 1226 throw new AppSearchException(AppSearchResult.RESULT_NOT_FOUND); 1227 } 1228 } catch (AppSearchException e) { 1229 // Not passing cause in AppSearchException as that violates privacy guarantees as 1230 // user could differentiate between document not existing and not having access. 1231 throw new AppSearchException( 1232 AppSearchResult.RESULT_NOT_FOUND, 1233 "Document (" + namespace + ", " + id + ") not found."); 1234 } 1235 1236 DocumentProto.Builder documentBuilder = documentProto.toBuilder(); 1237 removePrefixesFromDocument(documentBuilder); 1238 String prefix = createPrefix(packageName, databaseName); 1239 Map<String, SchemaTypeConfigProto> schemaTypeMap = 1240 mSchemaCacheLocked.getSchemaMapForPrefix(prefix); 1241 return GenericDocumentToProtoConverter.toGenericDocument( 1242 documentBuilder.build(), prefix, schemaTypeMap, mConfig); 1243 } finally { 1244 mReadWriteLock.readLock().unlock(); 1245 } 1246 } 1247 1248 /** 1249 * Retrieves a document from the AppSearch index by namespace and document ID. 1250 * 1251 * <p>This method belongs to query group. 1252 * 1253 * @param packageName The package that owns this document. 1254 * @param databaseName The databaseName this document resides in. 1255 * @param namespace The namespace this document resides in. 1256 * @param id The ID of the document to get. 1257 * @param typePropertyPaths A map of schema type to a list of property paths to return in the 1258 * result. 1259 * @return The Document contents 1260 * @throws AppSearchException on IcingSearchEngine error. 1261 */ 1262 @NonNull getDocument( @onNull String packageName, @NonNull String databaseName, @NonNull String namespace, @NonNull String id, @NonNull Map<String, List<String>> typePropertyPaths)1263 public GenericDocument getDocument( 1264 @NonNull String packageName, 1265 @NonNull String databaseName, 1266 @NonNull String namespace, 1267 @NonNull String id, 1268 @NonNull Map<String, List<String>> typePropertyPaths) 1269 throws AppSearchException { 1270 mReadWriteLock.readLock().lock(); 1271 try { 1272 throwIfClosedLocked(); 1273 DocumentProto documentProto = 1274 getDocumentProtoByIdLocked( 1275 packageName, databaseName, namespace, id, typePropertyPaths); 1276 DocumentProto.Builder documentBuilder = documentProto.toBuilder(); 1277 removePrefixesFromDocument(documentBuilder); 1278 1279 String prefix = createPrefix(packageName, databaseName); 1280 // The schema type map cannot be null at this point. It could only be null if no 1281 // schema had ever been set for that prefix. Given we have retrieved a document from 1282 // the index, we know a schema had to have been set. 1283 Map<String, SchemaTypeConfigProto> schemaTypeMap = 1284 mSchemaCacheLocked.getSchemaMapForPrefix(prefix); 1285 return GenericDocumentToProtoConverter.toGenericDocument( 1286 documentBuilder.build(), prefix, schemaTypeMap, mConfig); 1287 } finally { 1288 mReadWriteLock.readLock().unlock(); 1289 } 1290 } 1291 1292 /** 1293 * Returns a DocumentProto from Icing. 1294 * 1295 * @param packageName The package that owns this document. 1296 * @param databaseName The databaseName this document resides in. 1297 * @param namespace The namespace this document resides in. 1298 * @param id The ID of the document to get. 1299 * @param typePropertyPaths A map of schema type to a list of property paths to return in the 1300 * result. 1301 * @return the DocumentProto object 1302 * @throws AppSearchException on IcingSearchEngine error 1303 */ 1304 @NonNull 1305 @GuardedBy("mReadWriteLock") 1306 // We only log getResultProto.toString() in fullPii trace for debugging. 1307 @SuppressWarnings("LiteProtoToString") getDocumentProtoByIdLocked( @onNull String packageName, @NonNull String databaseName, @NonNull String namespace, @NonNull String id, @NonNull Map<String, List<String>> typePropertyPaths)1308 private DocumentProto getDocumentProtoByIdLocked( 1309 @NonNull String packageName, 1310 @NonNull String databaseName, 1311 @NonNull String namespace, 1312 @NonNull String id, 1313 @NonNull Map<String, List<String>> typePropertyPaths) 1314 throws AppSearchException { 1315 String prefix = createPrefix(packageName, databaseName); 1316 List<TypePropertyMask.Builder> nonPrefixedPropertyMaskBuilders = 1317 TypePropertyPathToProtoConverter.toTypePropertyMaskBuilderList(typePropertyPaths); 1318 List<TypePropertyMask> prefixedPropertyMasks = 1319 new ArrayList<>(nonPrefixedPropertyMaskBuilders.size()); 1320 for (int i = 0; i < nonPrefixedPropertyMaskBuilders.size(); ++i) { 1321 String nonPrefixedType = nonPrefixedPropertyMaskBuilders.get(i).getSchemaType(); 1322 String prefixedType = 1323 nonPrefixedType.equals(GetByDocumentIdRequest.PROJECTION_SCHEMA_TYPE_WILDCARD) 1324 ? nonPrefixedType 1325 : prefix + nonPrefixedType; 1326 prefixedPropertyMasks.add( 1327 nonPrefixedPropertyMaskBuilders.get(i).setSchemaType(prefixedType).build()); 1328 } 1329 GetResultSpecProto getResultSpec = 1330 GetResultSpecProto.newBuilder() 1331 .addAllTypePropertyMasks(prefixedPropertyMasks) 1332 .build(); 1333 1334 String finalNamespace = createPrefix(packageName, databaseName) + namespace; 1335 if (LogUtil.isPiiTraceEnabled()) { 1336 LogUtil.piiTrace( 1337 TAG, "getDocument, request", finalNamespace + ", " + id + "," + getResultSpec); 1338 } 1339 GetResultProto getResultProto = 1340 mIcingSearchEngineLocked.get(finalNamespace, id, getResultSpec); 1341 LogUtil.piiTrace(TAG, "getDocument, response", getResultProto.getStatus(), getResultProto); 1342 checkSuccess(getResultProto.getStatus()); 1343 1344 return getResultProto.getDocument(); 1345 } 1346 1347 /** 1348 * Executes a query against the AppSearch index and returns results. 1349 * 1350 * <p>This method belongs to query group. 1351 * 1352 * @param packageName The package name that is performing the query. 1353 * @param databaseName The databaseName this query for. 1354 * @param queryExpression Query String to search. 1355 * @param searchSpec Spec for setting filters, raw query etc. 1356 * @param logger logger to collect query stats 1357 * @return The results of performing this search. It may contain an empty list of results if no 1358 * documents matched the query. 1359 * @throws AppSearchException on IcingSearchEngine error. 1360 */ 1361 @NonNull query( @onNull String packageName, @NonNull String databaseName, @NonNull String queryExpression, @NonNull SearchSpec searchSpec, @Nullable AppSearchLogger logger)1362 public SearchResultPage query( 1363 @NonNull String packageName, 1364 @NonNull String databaseName, 1365 @NonNull String queryExpression, 1366 @NonNull SearchSpec searchSpec, 1367 @Nullable AppSearchLogger logger) 1368 throws AppSearchException { 1369 long totalLatencyStartMillis = SystemClock.elapsedRealtime(); 1370 SearchStats.Builder sStatsBuilder = null; 1371 if (logger != null) { 1372 sStatsBuilder = 1373 new SearchStats.Builder(SearchStats.VISIBILITY_SCOPE_LOCAL, packageName) 1374 .setDatabase(databaseName) 1375 .setSearchSourceLogTag(searchSpec.getSearchSourceLogTag()); 1376 } 1377 1378 long javaLockAcquisitionLatencyStartMillis = SystemClock.elapsedRealtime(); 1379 mReadWriteLock.readLock().lock(); 1380 try { 1381 if (sStatsBuilder != null) { 1382 sStatsBuilder.setJavaLockAcquisitionLatencyMillis( 1383 (int) 1384 (SystemClock.elapsedRealtime() 1385 - javaLockAcquisitionLatencyStartMillis)); 1386 } 1387 throwIfClosedLocked(); 1388 1389 List<String> filterPackageNames = searchSpec.getFilterPackageNames(); 1390 if (!filterPackageNames.isEmpty() && !filterPackageNames.contains(packageName)) { 1391 // Client wanted to query over some packages that weren't its own. This isn't 1392 // allowed through local query so we can return early with no results. 1393 if (sStatsBuilder != null && logger != null) { 1394 sStatsBuilder.setStatusCode(AppSearchResult.RESULT_SECURITY_ERROR); 1395 } 1396 return new SearchResultPage(); 1397 } 1398 1399 String prefix = createPrefix(packageName, databaseName); 1400 SearchSpecToProtoConverter searchSpecToProtoConverter = 1401 new SearchSpecToProtoConverter( 1402 queryExpression, 1403 searchSpec, 1404 Collections.singleton(prefix), 1405 mNamespaceMapLocked, 1406 mSchemaCacheLocked, 1407 mConfig); 1408 if (searchSpecToProtoConverter.hasNothingToSearch()) { 1409 // there is nothing to search over given their search filters, so we can return an 1410 // empty SearchResult and skip sending request to Icing. 1411 return new SearchResultPage(); 1412 } 1413 1414 SearchResultPage searchResultPage = 1415 doQueryLocked(searchSpecToProtoConverter, sStatsBuilder); 1416 addNextPageToken(packageName, searchResultPage.getNextPageToken()); 1417 return searchResultPage; 1418 } finally { 1419 mReadWriteLock.readLock().unlock(); 1420 if (sStatsBuilder != null && logger != null) { 1421 sStatsBuilder.setTotalLatencyMillis( 1422 (int) (SystemClock.elapsedRealtime() - totalLatencyStartMillis)); 1423 logger.logStats(sStatsBuilder.build()); 1424 } 1425 } 1426 } 1427 1428 /** 1429 * Executes a global query, i.e. over all permitted prefixes, against the AppSearch index and 1430 * returns results. 1431 * 1432 * <p>This method belongs to query group. 1433 * 1434 * @param queryExpression Query String to search. 1435 * @param searchSpec Spec for setting filters, raw query etc. 1436 * @param callerAccess Visibility access info of the calling app 1437 * @param logger logger to collect globalQuery stats 1438 * @return The results of performing this search. It may contain an empty list of results if no 1439 * documents matched the query. 1440 * @throws AppSearchException on IcingSearchEngine error. 1441 */ 1442 @NonNull globalQuery( @onNull String queryExpression, @NonNull SearchSpec searchSpec, @NonNull CallerAccess callerAccess, @Nullable AppSearchLogger logger)1443 public SearchResultPage globalQuery( 1444 @NonNull String queryExpression, 1445 @NonNull SearchSpec searchSpec, 1446 @NonNull CallerAccess callerAccess, 1447 @Nullable AppSearchLogger logger) 1448 throws AppSearchException { 1449 long totalLatencyStartMillis = SystemClock.elapsedRealtime(); 1450 SearchStats.Builder sStatsBuilder = null; 1451 if (logger != null) { 1452 sStatsBuilder = 1453 new SearchStats.Builder( 1454 SearchStats.VISIBILITY_SCOPE_GLOBAL, 1455 callerAccess.getCallingPackageName()) 1456 .setSearchSourceLogTag(searchSpec.getSearchSourceLogTag()); 1457 } 1458 1459 long javaLockAcquisitionLatencyStartMillis = SystemClock.elapsedRealtime(); 1460 mReadWriteLock.readLock().lock(); 1461 try { 1462 if (sStatsBuilder != null) { 1463 sStatsBuilder.setJavaLockAcquisitionLatencyMillis( 1464 (int) 1465 (SystemClock.elapsedRealtime() 1466 - javaLockAcquisitionLatencyStartMillis)); 1467 } 1468 throwIfClosedLocked(); 1469 1470 long aclLatencyStartMillis = SystemClock.elapsedRealtime(); 1471 1472 // The two scenarios where we want to limit package filters are if the outer 1473 // SearchSpec has package filters and there is no JoinSpec, or if both outer and 1474 // nested SearchSpecs have package filters. If outer SearchSpec has no package 1475 // filters or the nested SearchSpec has no package filters, then we pass the key set of 1476 // mNamespaceMapLocked to the SearchSpecToProtoConverter, signifying that there is a 1477 // SearchSpec that wants to query every visible package. 1478 Set<String> packageFilters = new ArraySet<>(); 1479 if (!searchSpec.getFilterPackageNames().isEmpty()) { 1480 JoinSpec joinSpec = searchSpec.getJoinSpec(); 1481 if (joinSpec == null) { 1482 packageFilters.addAll(searchSpec.getFilterPackageNames()); 1483 } else if (!joinSpec.getNestedSearchSpec().getFilterPackageNames().isEmpty()) { 1484 packageFilters.addAll(searchSpec.getFilterPackageNames()); 1485 packageFilters.addAll(joinSpec.getNestedSearchSpec().getFilterPackageNames()); 1486 } 1487 } 1488 1489 // Convert package filters to prefix filters 1490 Set<String> prefixFilters = new ArraySet<>(); 1491 if (packageFilters.isEmpty()) { 1492 // Client didn't restrict their search over packages. Try to query over all 1493 // packages/prefixes 1494 prefixFilters = mNamespaceMapLocked.keySet(); 1495 } else { 1496 // Client did restrict their search over packages. Only include the prefixes that 1497 // belong to the specified packages. 1498 for (String prefix : mNamespaceMapLocked.keySet()) { 1499 String packageName = getPackageName(prefix); 1500 if (packageFilters.contains(packageName)) { 1501 prefixFilters.add(prefix); 1502 } 1503 } 1504 } 1505 SearchSpecToProtoConverter searchSpecToProtoConverter = 1506 new SearchSpecToProtoConverter( 1507 queryExpression, 1508 searchSpec, 1509 prefixFilters, 1510 mNamespaceMapLocked, 1511 mSchemaCacheLocked, 1512 mConfig); 1513 // Remove those inaccessible schemas. 1514 searchSpecToProtoConverter.removeInaccessibleSchemaFilter( 1515 callerAccess, mVisibilityStoreLocked, mVisibilityCheckerLocked); 1516 if (searchSpecToProtoConverter.hasNothingToSearch()) { 1517 // there is nothing to search over given their search filters, so we can return an 1518 // empty SearchResult and skip sending request to Icing. 1519 return new SearchResultPage(); 1520 } 1521 if (sStatsBuilder != null) { 1522 sStatsBuilder.setAclCheckLatencyMillis( 1523 (int) (SystemClock.elapsedRealtime() - aclLatencyStartMillis)); 1524 } 1525 SearchResultPage searchResultPage = 1526 doQueryLocked(searchSpecToProtoConverter, sStatsBuilder); 1527 addNextPageToken( 1528 callerAccess.getCallingPackageName(), searchResultPage.getNextPageToken()); 1529 return searchResultPage; 1530 } finally { 1531 mReadWriteLock.readLock().unlock(); 1532 1533 if (sStatsBuilder != null && logger != null) { 1534 sStatsBuilder.setTotalLatencyMillis( 1535 (int) (SystemClock.elapsedRealtime() - totalLatencyStartMillis)); 1536 logger.logStats(sStatsBuilder.build()); 1537 } 1538 } 1539 } 1540 1541 @GuardedBy("mReadWriteLock") doQueryLocked( @onNull SearchSpecToProtoConverter searchSpecToProtoConverter, @Nullable SearchStats.Builder sStatsBuilder)1542 private SearchResultPage doQueryLocked( 1543 @NonNull SearchSpecToProtoConverter searchSpecToProtoConverter, 1544 @Nullable SearchStats.Builder sStatsBuilder) 1545 throws AppSearchException { 1546 // Rewrite the given SearchSpec into SearchSpecProto, ResultSpecProto and ScoringSpecProto. 1547 // All processes are counted in rewriteSearchSpecLatencyMillis 1548 long rewriteSearchSpecLatencyStartMillis = SystemClock.elapsedRealtime(); 1549 SearchSpecProto finalSearchSpec = searchSpecToProtoConverter.toSearchSpecProto(); 1550 ResultSpecProto finalResultSpec = 1551 searchSpecToProtoConverter.toResultSpecProto( 1552 mNamespaceMapLocked, mSchemaCacheLocked); 1553 ScoringSpecProto scoringSpec = searchSpecToProtoConverter.toScoringSpecProto(); 1554 if (sStatsBuilder != null) { 1555 sStatsBuilder.setRewriteSearchSpecLatencyMillis( 1556 (int) (SystemClock.elapsedRealtime() - rewriteSearchSpecLatencyStartMillis)); 1557 } 1558 1559 // Send request to Icing. 1560 SearchResultProto searchResultProto = 1561 searchInIcingLocked(finalSearchSpec, finalResultSpec, scoringSpec, sStatsBuilder); 1562 1563 long rewriteSearchResultLatencyStartMillis = SystemClock.elapsedRealtime(); 1564 // Rewrite search result before we return. 1565 SearchResultPage searchResultPage = 1566 SearchResultToProtoConverter.toSearchResultPage( 1567 searchResultProto, mSchemaCacheLocked, mConfig); 1568 if (sStatsBuilder != null) { 1569 sStatsBuilder.setRewriteSearchResultLatencyMillis( 1570 (int) (SystemClock.elapsedRealtime() - rewriteSearchResultLatencyStartMillis)); 1571 } 1572 return searchResultPage; 1573 } 1574 1575 @GuardedBy("mReadWriteLock") 1576 // We only log searchSpec, scoringSpec and resultSpec in fullPii trace for debugging. 1577 @SuppressWarnings("LiteProtoToString") searchInIcingLocked( @onNull SearchSpecProto searchSpec, @NonNull ResultSpecProto resultSpec, @NonNull ScoringSpecProto scoringSpec, @Nullable SearchStats.Builder sStatsBuilder)1578 private SearchResultProto searchInIcingLocked( 1579 @NonNull SearchSpecProto searchSpec, 1580 @NonNull ResultSpecProto resultSpec, 1581 @NonNull ScoringSpecProto scoringSpec, 1582 @Nullable SearchStats.Builder sStatsBuilder) 1583 throws AppSearchException { 1584 if (LogUtil.isPiiTraceEnabled()) { 1585 LogUtil.piiTrace( 1586 TAG, 1587 "search, request", 1588 searchSpec.getQuery(), 1589 searchSpec + ", " + scoringSpec + ", " + resultSpec); 1590 } 1591 SearchResultProto searchResultProto = 1592 mIcingSearchEngineLocked.search(searchSpec, scoringSpec, resultSpec); 1593 LogUtil.piiTrace( 1594 TAG, "search, response", searchResultProto.getResultsCount(), searchResultProto); 1595 if (sStatsBuilder != null) { 1596 sStatsBuilder.setStatusCode(statusProtoToResultCode(searchResultProto.getStatus())); 1597 if (searchSpec.hasJoinSpec()) { 1598 sStatsBuilder.setJoinType( 1599 AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID); 1600 } 1601 AppSearchLoggerHelper.copyNativeStats(searchResultProto.getQueryStats(), sStatsBuilder); 1602 } 1603 checkSuccess(searchResultProto.getStatus()); 1604 return searchResultProto; 1605 } 1606 1607 /** 1608 * Generates suggestions based on the given search prefix. 1609 * 1610 * <p>This method belongs to query group. 1611 * 1612 * @param packageName The package name that is performing the query. 1613 * @param databaseName The databaseName this query for. 1614 * @param suggestionQueryExpression The non-empty query expression used to be completed. 1615 * @param searchSuggestionSpec Spec for setting filters. 1616 * @return a List of {@link SearchSuggestionResult}. The returned {@link SearchSuggestionResult} 1617 * are order by the number of {@link android.app.appsearch.SearchResult} you could get by 1618 * using that suggestion in {@link #query}. 1619 * @throws AppSearchException if the suggestionQueryExpression is empty. 1620 */ 1621 @NonNull searchSuggestion( @onNull String packageName, @NonNull String databaseName, @NonNull String suggestionQueryExpression, @NonNull SearchSuggestionSpec searchSuggestionSpec)1622 public List<SearchSuggestionResult> searchSuggestion( 1623 @NonNull String packageName, 1624 @NonNull String databaseName, 1625 @NonNull String suggestionQueryExpression, 1626 @NonNull SearchSuggestionSpec searchSuggestionSpec) 1627 throws AppSearchException { 1628 mReadWriteLock.readLock().lock(); 1629 try { 1630 throwIfClosedLocked(); 1631 if (suggestionQueryExpression.isEmpty()) { 1632 throw new AppSearchException( 1633 AppSearchResult.RESULT_INVALID_ARGUMENT, 1634 "suggestionQueryExpression cannot be empty."); 1635 } 1636 if (searchSuggestionSpec.getMaximumResultCount() > mConfig.getMaxSuggestionCount()) { 1637 throw new AppSearchException( 1638 AppSearchResult.RESULT_INVALID_ARGUMENT, 1639 "Trying to get " 1640 + searchSuggestionSpec.getMaximumResultCount() 1641 + " suggestion results, which exceeds limit of " 1642 + mConfig.getMaxSuggestionCount()); 1643 } 1644 1645 String prefix = createPrefix(packageName, databaseName); 1646 SearchSuggestionSpecToProtoConverter searchSuggestionSpecToProtoConverter = 1647 new SearchSuggestionSpecToProtoConverter( 1648 suggestionQueryExpression, 1649 searchSuggestionSpec, 1650 Collections.singleton(prefix), 1651 mNamespaceMapLocked, 1652 mSchemaCacheLocked); 1653 1654 if (searchSuggestionSpecToProtoConverter.hasNothingToSearch()) { 1655 // there is nothing to search over given their search filters, so we can return an 1656 // empty SearchResult and skip sending request to Icing. 1657 return new ArrayList<>(); 1658 } 1659 1660 SuggestionResponse response = 1661 mIcingSearchEngineLocked.searchSuggestions( 1662 searchSuggestionSpecToProtoConverter.toSearchSuggestionSpecProto()); 1663 1664 checkSuccess(response.getStatus()); 1665 List<SearchSuggestionResult> suggestions = 1666 new ArrayList<>(response.getSuggestionsCount()); 1667 for (int i = 0; i < response.getSuggestionsCount(); i++) { 1668 suggestions.add( 1669 new SearchSuggestionResult.Builder() 1670 .setSuggestedResult(response.getSuggestions(i).getQuery()) 1671 .build()); 1672 } 1673 return suggestions; 1674 } finally { 1675 mReadWriteLock.readLock().unlock(); 1676 } 1677 } 1678 1679 /** 1680 * Returns a mapping of package names to all the databases owned by that package. 1681 * 1682 * <p>This method is inefficient to call repeatedly. 1683 */ 1684 @NonNull getPackageToDatabases()1685 public Map<String, Set<String>> getPackageToDatabases() { 1686 mReadWriteLock.readLock().lock(); 1687 try { 1688 Map<String, Set<String>> packageToDatabases = new ArrayMap<>(); 1689 for (String prefix : mSchemaCacheLocked.getAllPrefixes()) { 1690 String packageName = getPackageName(prefix); 1691 1692 Set<String> databases = packageToDatabases.get(packageName); 1693 if (databases == null) { 1694 databases = new ArraySet<>(); 1695 packageToDatabases.put(packageName, databases); 1696 } 1697 1698 String databaseName = getDatabaseName(prefix); 1699 databases.add(databaseName); 1700 } 1701 1702 return packageToDatabases; 1703 } finally { 1704 mReadWriteLock.readLock().unlock(); 1705 } 1706 } 1707 1708 /** 1709 * Fetches the next page of results of a previously executed query. Results can be empty if 1710 * next-page token is invalid or all pages have been returned. 1711 * 1712 * <p>This method belongs to query group. 1713 * 1714 * @param packageName Package name of the caller. 1715 * @param nextPageToken The token of pre-loaded results of previously executed query. 1716 * @return The next page of results of previously executed query. 1717 * @throws AppSearchException on IcingSearchEngine error or if can't advance on nextPageToken. 1718 */ 1719 @NonNull getNextPage( @onNull String packageName, long nextPageToken, @Nullable SearchStats.Builder sStatsBuilder)1720 public SearchResultPage getNextPage( 1721 @NonNull String packageName, 1722 long nextPageToken, 1723 @Nullable SearchStats.Builder sStatsBuilder) 1724 throws AppSearchException { 1725 long totalLatencyStartMillis = SystemClock.elapsedRealtime(); 1726 1727 long javaLockAcquisitionLatencyStartMillis = SystemClock.elapsedRealtime(); 1728 mReadWriteLock.readLock().lock(); 1729 try { 1730 if (sStatsBuilder != null) { 1731 sStatsBuilder.setJavaLockAcquisitionLatencyMillis( 1732 (int) 1733 (SystemClock.elapsedRealtime() 1734 - javaLockAcquisitionLatencyStartMillis)); 1735 } 1736 throwIfClosedLocked(); 1737 1738 LogUtil.piiTrace(TAG, "getNextPage, request", nextPageToken); 1739 checkNextPageToken(packageName, nextPageToken); 1740 SearchResultProto searchResultProto = 1741 mIcingSearchEngineLocked.getNextPage(nextPageToken); 1742 1743 if (sStatsBuilder != null) { 1744 sStatsBuilder.setStatusCode(statusProtoToResultCode(searchResultProto.getStatus())); 1745 // Join query stats are handled by SearchResultsImpl, which has access to the 1746 // original SearchSpec. 1747 AppSearchLoggerHelper.copyNativeStats( 1748 searchResultProto.getQueryStats(), sStatsBuilder); 1749 } 1750 1751 LogUtil.piiTrace( 1752 TAG, 1753 "getNextPage, response", 1754 searchResultProto.getResultsCount(), 1755 searchResultProto); 1756 checkSuccess(searchResultProto.getStatus()); 1757 if (nextPageToken != EMPTY_PAGE_TOKEN 1758 && searchResultProto.getNextPageToken() == EMPTY_PAGE_TOKEN) { 1759 // At this point, we're guaranteed that this nextPageToken exists for this package, 1760 // otherwise checkNextPageToken would've thrown an exception. 1761 // Since the new token is 0, this is the last page. We should remove the old token 1762 // from our cache since it no longer refers to this query. 1763 synchronized (mNextPageTokensLocked) { 1764 Set<Long> nextPageTokensForPackage = 1765 Objects.requireNonNull(mNextPageTokensLocked.get(packageName)); 1766 nextPageTokensForPackage.remove(nextPageToken); 1767 } 1768 } 1769 long rewriteSearchResultLatencyStartMillis = SystemClock.elapsedRealtime(); 1770 // Rewrite search result before we return. 1771 SearchResultPage searchResultPage = 1772 SearchResultToProtoConverter.toSearchResultPage( 1773 searchResultProto, mSchemaCacheLocked, mConfig); 1774 if (sStatsBuilder != null) { 1775 sStatsBuilder.setRewriteSearchResultLatencyMillis( 1776 (int) 1777 (SystemClock.elapsedRealtime() 1778 - rewriteSearchResultLatencyStartMillis)); 1779 } 1780 return searchResultPage; 1781 } finally { 1782 mReadWriteLock.readLock().unlock(); 1783 if (sStatsBuilder != null) { 1784 sStatsBuilder.setTotalLatencyMillis( 1785 (int) (SystemClock.elapsedRealtime() - totalLatencyStartMillis)); 1786 } 1787 } 1788 } 1789 1790 /** 1791 * Invalidates the next-page token so that no more results of the related query can be returned. 1792 * 1793 * <p>This method belongs to query group. 1794 * 1795 * @param packageName Package name of the caller. 1796 * @param nextPageToken The token of pre-loaded results of previously executed query to be 1797 * Invalidated. 1798 * @throws AppSearchException if nextPageToken is unusable. 1799 */ invalidateNextPageToken(@onNull String packageName, long nextPageToken)1800 public void invalidateNextPageToken(@NonNull String packageName, long nextPageToken) 1801 throws AppSearchException { 1802 if (nextPageToken == EMPTY_PAGE_TOKEN) { 1803 // (b/208305352) Directly return here since we are no longer caching EMPTY_PAGE_TOKEN 1804 // in the cached token set. So no need to remove it anymore. 1805 return; 1806 } 1807 1808 mReadWriteLock.readLock().lock(); 1809 try { 1810 throwIfClosedLocked(); 1811 1812 LogUtil.piiTrace(TAG, "invalidateNextPageToken, request", nextPageToken); 1813 checkNextPageToken(packageName, nextPageToken); 1814 mIcingSearchEngineLocked.invalidateNextPageToken(nextPageToken); 1815 1816 synchronized (mNextPageTokensLocked) { 1817 Set<Long> tokens = mNextPageTokensLocked.get(packageName); 1818 if (tokens != null) { 1819 tokens.remove(nextPageToken); 1820 } else { 1821 Log.e( 1822 TAG, 1823 "Failed to invalidate token " 1824 + nextPageToken 1825 + ": tokens are not " 1826 + "cached."); 1827 } 1828 } 1829 } finally { 1830 mReadWriteLock.readLock().unlock(); 1831 } 1832 } 1833 1834 /** 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)1835 public void reportUsage( 1836 @NonNull String packageName, 1837 @NonNull String databaseName, 1838 @NonNull String namespace, 1839 @NonNull String documentId, 1840 long usageTimestampMillis, 1841 boolean systemUsage) 1842 throws AppSearchException { 1843 mReadWriteLock.writeLock().lock(); 1844 try { 1845 throwIfClosedLocked(); 1846 1847 String prefixedNamespace = createPrefix(packageName, databaseName) + namespace; 1848 UsageReport.UsageType usageType = 1849 systemUsage 1850 ? UsageReport.UsageType.USAGE_TYPE2 1851 : UsageReport.UsageType.USAGE_TYPE1; 1852 UsageReport report = 1853 UsageReport.newBuilder() 1854 .setDocumentNamespace(prefixedNamespace) 1855 .setDocumentUri(documentId) 1856 .setUsageTimestampMs(usageTimestampMillis) 1857 .setUsageType(usageType) 1858 .build(); 1859 1860 LogUtil.piiTrace(TAG, "reportUsage, request", report.getDocumentUri(), report); 1861 ReportUsageResultProto result = mIcingSearchEngineLocked.reportUsage(report); 1862 LogUtil.piiTrace(TAG, "reportUsage, response", result.getStatus(), result); 1863 checkSuccess(result.getStatus()); 1864 } finally { 1865 mReadWriteLock.writeLock().unlock(); 1866 } 1867 } 1868 1869 /** 1870 * Removes the given document by id. 1871 * 1872 * <p>This method belongs to mutate group. 1873 * 1874 * @param packageName The package name that owns the document. 1875 * @param databaseName The databaseName the document is in. 1876 * @param namespace Namespace of the document to remove. 1877 * @param documentId ID of the document to remove. 1878 * @param removeStatsBuilder builder for {@link RemoveStats} to hold stats for remove 1879 * @throws AppSearchException on IcingSearchEngine error. 1880 */ remove( @onNull String packageName, @NonNull String databaseName, @NonNull String namespace, @NonNull String documentId, @Nullable RemoveStats.Builder removeStatsBuilder)1881 public void remove( 1882 @NonNull String packageName, 1883 @NonNull String databaseName, 1884 @NonNull String namespace, 1885 @NonNull String documentId, 1886 @Nullable RemoveStats.Builder removeStatsBuilder) 1887 throws AppSearchException { 1888 long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime(); 1889 mReadWriteLock.writeLock().lock(); 1890 try { 1891 throwIfClosedLocked(); 1892 1893 String prefixedNamespace = createPrefix(packageName, databaseName) + namespace; 1894 String schemaType = null; 1895 if (mObserverManager.isPackageObserved(packageName)) { 1896 // Someone might be observing the type this document is under, but we have no way to 1897 // know its type without retrieving it. Do so now. 1898 // TODO(b/193494000): If Icing Lib can return information about the deleted 1899 // document's type we can remove this code. 1900 if (LogUtil.isPiiTraceEnabled()) { 1901 LogUtil.piiTrace( 1902 TAG, "removeById, getRequest", prefixedNamespace + ", " + documentId); 1903 } 1904 GetResultProto getResult = 1905 mIcingSearchEngineLocked.get( 1906 prefixedNamespace, documentId, GET_RESULT_SPEC_NO_PROPERTIES); 1907 LogUtil.piiTrace(TAG, "removeById, getResponse", getResult.getStatus(), getResult); 1908 checkSuccess(getResult.getStatus()); 1909 schemaType = PrefixUtil.removePrefix(getResult.getDocument().getSchema()); 1910 } 1911 1912 if (LogUtil.isPiiTraceEnabled()) { 1913 LogUtil.piiTrace(TAG, "removeById, request", prefixedNamespace + ", " + documentId); 1914 } 1915 DeleteResultProto deleteResultProto = 1916 mIcingSearchEngineLocked.delete(prefixedNamespace, documentId); 1917 LogUtil.piiTrace( 1918 TAG, "removeById, response", deleteResultProto.getStatus(), deleteResultProto); 1919 1920 if (removeStatsBuilder != null) { 1921 removeStatsBuilder.setStatusCode( 1922 statusProtoToResultCode(deleteResultProto.getStatus())); 1923 AppSearchLoggerHelper.copyNativeStats( 1924 deleteResultProto.getDeleteStats(), removeStatsBuilder); 1925 } 1926 checkSuccess(deleteResultProto.getStatus()); 1927 1928 // Update derived maps 1929 updateDocumentCountAfterRemovalLocked(packageName, /* numDocumentsDeleted= */ 1); 1930 1931 // Prepare notifications 1932 if (schemaType != null) { 1933 mObserverManager.onDocumentChange( 1934 packageName, 1935 databaseName, 1936 namespace, 1937 schemaType, 1938 documentId, 1939 mVisibilityStoreLocked, 1940 mVisibilityCheckerLocked); 1941 } 1942 } finally { 1943 mReadWriteLock.writeLock().unlock(); 1944 if (removeStatsBuilder != null) { 1945 removeStatsBuilder.setTotalLatencyMillis( 1946 (int) (SystemClock.elapsedRealtime() - totalLatencyStartTimeMillis)); 1947 } 1948 } 1949 } 1950 1951 /** 1952 * Removes documents by given query. 1953 * 1954 * <p>This method belongs to mutate group. 1955 * 1956 * <p>{@link SearchSpec} objects containing a {@link JoinSpec} are not allowed here. 1957 * 1958 * @param packageName The package name that owns the documents. 1959 * @param databaseName The databaseName the document is in. 1960 * @param queryExpression Query String to search. 1961 * @param searchSpec Defines what and how to remove 1962 * @param removeStatsBuilder builder for {@link RemoveStats} to hold stats for remove 1963 * @throws AppSearchException on IcingSearchEngine error. 1964 * @throws IllegalArgumentException if the {@link SearchSpec} contains a {@link JoinSpec}. 1965 */ removeByQuery( @onNull String packageName, @NonNull String databaseName, @NonNull String queryExpression, @NonNull SearchSpec searchSpec, @Nullable RemoveStats.Builder removeStatsBuilder)1966 public void removeByQuery( 1967 @NonNull String packageName, 1968 @NonNull String databaseName, 1969 @NonNull String queryExpression, 1970 @NonNull SearchSpec searchSpec, 1971 @Nullable RemoveStats.Builder removeStatsBuilder) 1972 throws AppSearchException { 1973 if (searchSpec.getJoinSpec() != null) { 1974 throw new IllegalArgumentException( 1975 "JoinSpec not allowed in removeByQuery, but " + "JoinSpec was provided"); 1976 } 1977 1978 long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime(); 1979 mReadWriteLock.writeLock().lock(); 1980 try { 1981 throwIfClosedLocked(); 1982 1983 List<String> filterPackageNames = searchSpec.getFilterPackageNames(); 1984 if (!filterPackageNames.isEmpty() && !filterPackageNames.contains(packageName)) { 1985 // We're only removing documents within the parameter `packageName`. If we're not 1986 // restricting our remove-query to this package name, then there's nothing for us to 1987 // remove. 1988 return; 1989 } 1990 1991 String prefix = createPrefix(packageName, databaseName); 1992 if (!mNamespaceMapLocked.containsKey(prefix)) { 1993 // The target database is empty so we can return early and skip sending request to 1994 // Icing. 1995 return; 1996 } 1997 1998 SearchSpecToProtoConverter searchSpecToProtoConverter = 1999 new SearchSpecToProtoConverter( 2000 queryExpression, 2001 searchSpec, 2002 Collections.singleton(prefix), 2003 mNamespaceMapLocked, 2004 mSchemaCacheLocked, 2005 mConfig); 2006 if (searchSpecToProtoConverter.hasNothingToSearch()) { 2007 // there is nothing to search over given their search filters, so we can return 2008 // early and skip sending request to Icing. 2009 return; 2010 } 2011 2012 SearchSpecProto finalSearchSpec = searchSpecToProtoConverter.toSearchSpecProto(); 2013 2014 Set<String> prefixedObservedSchemas = null; 2015 if (mObserverManager.isPackageObserved(packageName)) { 2016 prefixedObservedSchemas = new ArraySet<>(); 2017 List<String> prefixedTargetSchemaTypes = finalSearchSpec.getSchemaTypeFiltersList(); 2018 for (int i = 0; i < prefixedTargetSchemaTypes.size(); i++) { 2019 String prefixedType = prefixedTargetSchemaTypes.get(i); 2020 String shortTypeName = PrefixUtil.removePrefix(prefixedType); 2021 if (mObserverManager.isSchemaTypeObserved(packageName, shortTypeName)) { 2022 prefixedObservedSchemas.add(prefixedType); 2023 } 2024 } 2025 } 2026 2027 doRemoveByQueryLocked( 2028 packageName, finalSearchSpec, prefixedObservedSchemas, removeStatsBuilder); 2029 2030 } finally { 2031 mReadWriteLock.writeLock().unlock(); 2032 if (removeStatsBuilder != null) { 2033 removeStatsBuilder.setTotalLatencyMillis( 2034 (int) (SystemClock.elapsedRealtime() - totalLatencyStartTimeMillis)); 2035 } 2036 } 2037 } 2038 2039 /** 2040 * Executes removeByQuery. 2041 * 2042 * <p>Change notifications will be created if prefixedObservedSchemas is not null. 2043 * 2044 * @param packageName The package name that owns the documents. 2045 * @param finalSearchSpec The final search spec that has been written through {@link 2046 * SearchSpecToProtoConverter}. 2047 * @param prefixedObservedSchemas The set of prefixed schemas that have valid registered 2048 * observers. Only changes to schemas in this set will be queued. 2049 */ 2050 @GuardedBy("mReadWriteLock") doRemoveByQueryLocked( @onNull String packageName, @NonNull SearchSpecProto finalSearchSpec, @Nullable Set<String> prefixedObservedSchemas, @Nullable RemoveStats.Builder removeStatsBuilder)2051 private void doRemoveByQueryLocked( 2052 @NonNull String packageName, 2053 @NonNull SearchSpecProto finalSearchSpec, 2054 @Nullable Set<String> prefixedObservedSchemas, 2055 @Nullable RemoveStats.Builder removeStatsBuilder) 2056 throws AppSearchException { 2057 LogUtil.piiTrace(TAG, "removeByQuery, request", finalSearchSpec); 2058 boolean returnDeletedDocumentInfo = 2059 prefixedObservedSchemas != null && !prefixedObservedSchemas.isEmpty(); 2060 DeleteByQueryResultProto deleteResultProto = 2061 mIcingSearchEngineLocked.deleteByQuery(finalSearchSpec, returnDeletedDocumentInfo); 2062 LogUtil.piiTrace( 2063 TAG, "removeByQuery, response", deleteResultProto.getStatus(), deleteResultProto); 2064 2065 if (removeStatsBuilder != null) { 2066 removeStatsBuilder.setStatusCode( 2067 statusProtoToResultCode(deleteResultProto.getStatus())); 2068 // TODO(b/187206766) also log query stats here once IcingLib returns it 2069 AppSearchLoggerHelper.copyNativeStats( 2070 deleteResultProto.getDeleteByQueryStats(), removeStatsBuilder); 2071 } 2072 2073 // It seems that the caller wants to get success if the data matching the query is 2074 // not in the DB because it was not there or was successfully deleted. 2075 checkCodeOneOf( 2076 deleteResultProto.getStatus(), StatusProto.Code.OK, StatusProto.Code.NOT_FOUND); 2077 2078 // Update derived maps 2079 int numDocumentsDeleted = 2080 deleteResultProto.getDeleteByQueryStats().getNumDocumentsDeleted(); 2081 updateDocumentCountAfterRemovalLocked(packageName, numDocumentsDeleted); 2082 2083 if (prefixedObservedSchemas != null && !prefixedObservedSchemas.isEmpty()) { 2084 dispatchChangeNotificationsAfterRemoveByQueryLocked( 2085 packageName, deleteResultProto, prefixedObservedSchemas); 2086 } 2087 } 2088 2089 @GuardedBy("mReadWriteLock") updateDocumentCountAfterRemovalLocked( @onNull String packageName, int numDocumentsDeleted)2090 private void updateDocumentCountAfterRemovalLocked( 2091 @NonNull String packageName, int numDocumentsDeleted) { 2092 if (numDocumentsDeleted > 0) { 2093 Integer oldDocumentCount = mDocumentCountMapLocked.get(packageName); 2094 // This should always be true: how can we delete documents for a package without 2095 // having seen that package during init? This is just a safeguard. 2096 if (oldDocumentCount != null) { 2097 // This should always be >0; how can we remove more documents than we've indexed? 2098 // This is just a safeguard. 2099 int newDocumentCount = Math.max(oldDocumentCount - numDocumentsDeleted, 0); 2100 mDocumentCountMapLocked.put(packageName, newDocumentCount); 2101 } 2102 } 2103 } 2104 2105 @GuardedBy("mReadWriteLock") dispatchChangeNotificationsAfterRemoveByQueryLocked( @onNull String packageName, @NonNull DeleteByQueryResultProto deleteResultProto, @NonNull Set<String> prefixedObservedSchemas)2106 private void dispatchChangeNotificationsAfterRemoveByQueryLocked( 2107 @NonNull String packageName, 2108 @NonNull DeleteByQueryResultProto deleteResultProto, 2109 @NonNull Set<String> prefixedObservedSchemas) 2110 throws AppSearchException { 2111 for (int i = 0; i < deleteResultProto.getDeletedDocumentsCount(); ++i) { 2112 DeleteByQueryResultProto.DocumentGroupInfo group = 2113 deleteResultProto.getDeletedDocuments(i); 2114 if (!prefixedObservedSchemas.contains(group.getSchema())) { 2115 continue; 2116 } 2117 String databaseName = PrefixUtil.getDatabaseName(group.getNamespace()); 2118 String namespace = PrefixUtil.removePrefix(group.getNamespace()); 2119 String schemaType = PrefixUtil.removePrefix(group.getSchema()); 2120 for (int j = 0; j < group.getUrisCount(); ++j) { 2121 String uri = group.getUris(j); 2122 mObserverManager.onDocumentChange( 2123 packageName, 2124 databaseName, 2125 namespace, 2126 schemaType, 2127 uri, 2128 mVisibilityStoreLocked, 2129 mVisibilityCheckerLocked); 2130 } 2131 } 2132 } 2133 2134 /** Estimates the storage usage info for a specific package. */ 2135 @NonNull getStorageInfoForPackage(@onNull String packageName)2136 public StorageInfo getStorageInfoForPackage(@NonNull String packageName) 2137 throws AppSearchException { 2138 mReadWriteLock.readLock().lock(); 2139 try { 2140 throwIfClosedLocked(); 2141 2142 Map<String, Set<String>> packageToDatabases = getPackageToDatabases(); 2143 Set<String> databases = packageToDatabases.get(packageName); 2144 if (databases == null) { 2145 // Package doesn't exist, no storage info to report 2146 return new StorageInfo.Builder().build(); 2147 } 2148 2149 // Accumulate all the namespaces we're interested in. 2150 Set<String> wantedPrefixedNamespaces = new ArraySet<>(); 2151 for (String database : databases) { 2152 Set<String> prefixedNamespaces = 2153 mNamespaceMapLocked.get(createPrefix(packageName, database)); 2154 if (prefixedNamespaces != null) { 2155 wantedPrefixedNamespaces.addAll(prefixedNamespaces); 2156 } 2157 } 2158 if (wantedPrefixedNamespaces.isEmpty()) { 2159 return new StorageInfo.Builder().build(); 2160 } 2161 2162 return getStorageInfoForNamespaces(getRawStorageInfoProto(), wantedPrefixedNamespaces); 2163 } finally { 2164 mReadWriteLock.readLock().unlock(); 2165 } 2166 } 2167 2168 /** Estimates the storage usage info for a specific database in a package. */ 2169 @NonNull getStorageInfoForDatabase( @onNull String packageName, @NonNull String databaseName)2170 public StorageInfo getStorageInfoForDatabase( 2171 @NonNull String packageName, @NonNull String databaseName) throws AppSearchException { 2172 mReadWriteLock.readLock().lock(); 2173 try { 2174 throwIfClosedLocked(); 2175 2176 Map<String, Set<String>> packageToDatabases = getPackageToDatabases(); 2177 Set<String> databases = packageToDatabases.get(packageName); 2178 if (databases == null) { 2179 // Package doesn't exist, no storage info to report 2180 return new StorageInfo.Builder().build(); 2181 } 2182 if (!databases.contains(databaseName)) { 2183 // Database doesn't exist, no storage info to report 2184 return new StorageInfo.Builder().build(); 2185 } 2186 2187 Set<String> wantedPrefixedNamespaces = 2188 mNamespaceMapLocked.get(createPrefix(packageName, databaseName)); 2189 if (wantedPrefixedNamespaces == null || wantedPrefixedNamespaces.isEmpty()) { 2190 return new StorageInfo.Builder().build(); 2191 } 2192 2193 return getStorageInfoForNamespaces(getRawStorageInfoProto(), wantedPrefixedNamespaces); 2194 } finally { 2195 mReadWriteLock.readLock().unlock(); 2196 } 2197 } 2198 2199 /** 2200 * Returns the native storage info capsuled in {@link StorageInfoResultProto} directly from 2201 * IcingSearchEngine. 2202 */ 2203 @NonNull getRawStorageInfoProto()2204 public StorageInfoProto getRawStorageInfoProto() throws AppSearchException { 2205 mReadWriteLock.readLock().lock(); 2206 try { 2207 throwIfClosedLocked(); 2208 LogUtil.piiTrace(TAG, "getStorageInfo, request"); 2209 StorageInfoResultProto storageInfoResult = mIcingSearchEngineLocked.getStorageInfo(); 2210 LogUtil.piiTrace( 2211 TAG, 2212 "getStorageInfo, response", 2213 storageInfoResult.getStatus(), 2214 storageInfoResult); 2215 checkSuccess(storageInfoResult.getStatus()); 2216 return storageInfoResult.getStorageInfo(); 2217 } finally { 2218 mReadWriteLock.readLock().unlock(); 2219 } 2220 } 2221 2222 /** 2223 * Extracts and returns {@link StorageInfo} from {@link StorageInfoProto} based on prefixed 2224 * namespaces. 2225 */ 2226 @NonNull getStorageInfoForNamespaces( @onNull StorageInfoProto storageInfoProto, @NonNull Set<String> prefixedNamespaces)2227 private static StorageInfo getStorageInfoForNamespaces( 2228 @NonNull StorageInfoProto storageInfoProto, @NonNull Set<String> prefixedNamespaces) { 2229 if (!storageInfoProto.hasDocumentStorageInfo()) { 2230 return new StorageInfo.Builder().build(); 2231 } 2232 2233 long totalStorageSize = storageInfoProto.getTotalStorageSize(); 2234 DocumentStorageInfoProto documentStorageInfo = storageInfoProto.getDocumentStorageInfo(); 2235 int totalDocuments = 2236 documentStorageInfo.getNumAliveDocuments() 2237 + documentStorageInfo.getNumExpiredDocuments(); 2238 2239 if (totalStorageSize == 0 || totalDocuments == 0) { 2240 // Maybe we can exit early and also avoid a divide by 0 error. 2241 return new StorageInfo.Builder().build(); 2242 } 2243 2244 // Accumulate stats across the package's namespaces. 2245 int aliveDocuments = 0; 2246 int expiredDocuments = 0; 2247 int aliveNamespaces = 0; 2248 List<NamespaceStorageInfoProto> namespaceStorageInfos = 2249 documentStorageInfo.getNamespaceStorageInfoList(); 2250 for (int i = 0; i < namespaceStorageInfos.size(); i++) { 2251 NamespaceStorageInfoProto namespaceStorageInfo = namespaceStorageInfos.get(i); 2252 // The namespace from icing lib is already the prefixed format 2253 if (prefixedNamespaces.contains(namespaceStorageInfo.getNamespace())) { 2254 if (namespaceStorageInfo.getNumAliveDocuments() > 0) { 2255 aliveNamespaces++; 2256 aliveDocuments += namespaceStorageInfo.getNumAliveDocuments(); 2257 } 2258 expiredDocuments += namespaceStorageInfo.getNumExpiredDocuments(); 2259 } 2260 } 2261 int namespaceDocuments = aliveDocuments + expiredDocuments; 2262 2263 // Since we don't have the exact size of all the documents, we do an estimation. Note 2264 // that while the total storage takes into account schema, index, etc. in addition to 2265 // documents, we'll only calculate the percentage based on number of documents a 2266 // client has. 2267 return new StorageInfo.Builder() 2268 .setSizeBytes((long) (namespaceDocuments * 1.0 / totalDocuments * totalStorageSize)) 2269 .setAliveDocumentsCount(aliveDocuments) 2270 .setAliveNamespacesCount(aliveNamespaces) 2271 .build(); 2272 } 2273 2274 /** 2275 * Returns the native debug info capsuled in {@link DebugInfoResultProto} directly from 2276 * IcingSearchEngine. 2277 * 2278 * @param verbosity The verbosity of the debug info. {@link DebugInfoVerbosity.Code#BASIC} will 2279 * return the simplest debug information. {@link DebugInfoVerbosity.Code#DETAILED} will 2280 * return more detailed debug information as indicated in the comments in debug.proto 2281 */ 2282 @NonNull getRawDebugInfoProto(@onNull DebugInfoVerbosity.Code verbosity)2283 public DebugInfoProto getRawDebugInfoProto(@NonNull DebugInfoVerbosity.Code verbosity) 2284 throws AppSearchException { 2285 mReadWriteLock.readLock().lock(); 2286 try { 2287 throwIfClosedLocked(); 2288 LogUtil.piiTrace(TAG, "getDebugInfo, request"); 2289 DebugInfoResultProto debugInfoResult = mIcingSearchEngineLocked.getDebugInfo(verbosity); 2290 LogUtil.piiTrace( 2291 TAG, "getDebugInfo, response", debugInfoResult.getStatus(), debugInfoResult); 2292 checkSuccess(debugInfoResult.getStatus()); 2293 return debugInfoResult.getDebugInfo(); 2294 } finally { 2295 mReadWriteLock.readLock().unlock(); 2296 } 2297 } 2298 2299 /** 2300 * Persists all update/delete requests to the disk. 2301 * 2302 * <p>If the app crashes after a call to PersistToDisk with {@link PersistType.Code#FULL}, Icing 2303 * would be able to fully recover all data written up to this point without a costly recovery 2304 * process. 2305 * 2306 * <p>If the app crashes after a call to PersistToDisk with {@link PersistType.Code#LITE}, Icing 2307 * would trigger a costly recovery process in next initialization. After that, Icing would still 2308 * be able to recover all written data - excepting Usage data. Usage data is only guaranteed to 2309 * be safe after a call to PersistToDisk with {@link PersistType.Code#FULL} 2310 * 2311 * <p>If the app crashes after an update/delete request has been made, but before any call to 2312 * PersistToDisk, then all data in Icing will be lost. 2313 * 2314 * @param persistType the amount of data to persist. {@link PersistType.Code#LITE} will only 2315 * persist the minimal amount of data to ensure all data can be recovered. {@link 2316 * PersistType.Code#FULL} will persist all data necessary to prevent data loss without 2317 * needing data recovery. 2318 * @throws AppSearchException on any error that AppSearch persist data to disk. 2319 */ persistToDisk(@onNull PersistType.Code persistType)2320 public void persistToDisk(@NonNull PersistType.Code persistType) throws AppSearchException { 2321 mReadWriteLock.writeLock().lock(); 2322 try { 2323 throwIfClosedLocked(); 2324 2325 LogUtil.piiTrace(TAG, "persistToDisk, request", persistType); 2326 PersistToDiskResultProto persistToDiskResultProto = 2327 mIcingSearchEngineLocked.persistToDisk(persistType); 2328 LogUtil.piiTrace( 2329 TAG, 2330 "persistToDisk, response", 2331 persistToDiskResultProto.getStatus(), 2332 persistToDiskResultProto); 2333 checkSuccess(persistToDiskResultProto.getStatus()); 2334 } finally { 2335 mReadWriteLock.writeLock().unlock(); 2336 } 2337 } 2338 2339 /** 2340 * Remove all {@link AppSearchSchema}s and {@link GenericDocument}s under the given package. 2341 * 2342 * @param packageName The name of package to be removed. 2343 * @throws AppSearchException if we cannot remove the data. 2344 */ clearPackageData(@onNull String packageName)2345 public void clearPackageData(@NonNull String packageName) throws AppSearchException { 2346 mReadWriteLock.writeLock().lock(); 2347 try { 2348 throwIfClosedLocked(); 2349 if (LogUtil.DEBUG) { 2350 Log.d(TAG, "Clear data for package: " + packageName); 2351 } 2352 // TODO(b/193494000): We are calling getPackageToDatabases here and in several other 2353 // places within AppSearchImpl. This method is not efficient and does a lot of string 2354 // manipulation. We should find a way to cache the package to database map so it can 2355 // just be obtained from a local variable instead of being parsed out of the prefixed 2356 // map. 2357 Set<String> existingPackages = getPackageToDatabases().keySet(); 2358 if (existingPackages.contains(packageName)) { 2359 existingPackages.remove(packageName); 2360 prunePackageData(existingPackages); 2361 } 2362 } finally { 2363 mReadWriteLock.writeLock().unlock(); 2364 } 2365 } 2366 2367 /** 2368 * Remove all {@link AppSearchSchema}s and {@link GenericDocument}s that doesn't belong to any 2369 * of the given installed packages 2370 * 2371 * @param installedPackages The name of all installed package. 2372 * @throws AppSearchException if we cannot remove the data. 2373 */ prunePackageData(@onNull Set<String> installedPackages)2374 public void prunePackageData(@NonNull Set<String> installedPackages) throws AppSearchException { 2375 mReadWriteLock.writeLock().lock(); 2376 try { 2377 throwIfClosedLocked(); 2378 Map<String, Set<String>> packageToDatabases = getPackageToDatabases(); 2379 if (installedPackages.containsAll(packageToDatabases.keySet())) { 2380 // No package got removed. We are good. 2381 return; 2382 } 2383 2384 // Prune schema proto 2385 SchemaProto existingSchema = getSchemaProtoLocked(); 2386 SchemaProto.Builder newSchemaBuilder = SchemaProto.newBuilder(); 2387 for (int i = 0; i < existingSchema.getTypesCount(); i++) { 2388 String packageName = getPackageName(existingSchema.getTypes(i).getSchemaType()); 2389 if (installedPackages.contains(packageName)) { 2390 newSchemaBuilder.addTypes(existingSchema.getTypes(i)); 2391 } 2392 } 2393 2394 SchemaProto finalSchema = newSchemaBuilder.build(); 2395 2396 // Apply schema, set force override to true to remove all schemas and documents that 2397 // doesn't belong to any of these installed packages. 2398 LogUtil.piiTrace( 2399 TAG, 2400 "clearPackageData.setSchema, request", 2401 finalSchema.getTypesCount(), 2402 finalSchema); 2403 SetSchemaResultProto setSchemaResultProto = 2404 mIcingSearchEngineLocked.setSchema( 2405 finalSchema, /* ignoreErrorsAndDeleteDocuments= */ true); 2406 LogUtil.piiTrace( 2407 TAG, 2408 "clearPackageData.setSchema, response", 2409 setSchemaResultProto.getStatus(), 2410 setSchemaResultProto); 2411 2412 // Determine whether it succeeded. 2413 checkSuccess(setSchemaResultProto.getStatus()); 2414 2415 // Prune cached maps 2416 for (Map.Entry<String, Set<String>> entry : packageToDatabases.entrySet()) { 2417 String packageName = entry.getKey(); 2418 Set<String> databaseNames = entry.getValue(); 2419 if (!installedPackages.contains(packageName) && databaseNames != null) { 2420 mDocumentCountMapLocked.remove(packageName); 2421 synchronized (mNextPageTokensLocked) { 2422 mNextPageTokensLocked.remove(packageName); 2423 } 2424 for (String databaseName : databaseNames) { 2425 String removedPrefix = createPrefix(packageName, databaseName); 2426 Set<String> removedSchemas = mSchemaCacheLocked.removePrefix(removedPrefix); 2427 if (mVisibilityStoreLocked != null) { 2428 mVisibilityStoreLocked.removeVisibility(removedSchemas); 2429 } 2430 2431 mNamespaceMapLocked.remove(removedPrefix); 2432 } 2433 } 2434 } 2435 } finally { 2436 mReadWriteLock.writeLock().unlock(); 2437 } 2438 } 2439 2440 /** 2441 * Clears documents and schema across all packages and databaseNames. 2442 * 2443 * <p>This method belongs to mutate group. 2444 * 2445 * @throws AppSearchException on IcingSearchEngine error. 2446 */ 2447 @GuardedBy("mReadWriteLock") resetLocked(@ullable InitializeStats.Builder initStatsBuilder)2448 private void resetLocked(@Nullable InitializeStats.Builder initStatsBuilder) 2449 throws AppSearchException { 2450 LogUtil.piiTrace(TAG, "icingSearchEngine.reset, request"); 2451 ResetResultProto resetResultProto = mIcingSearchEngineLocked.reset(); 2452 LogUtil.piiTrace( 2453 TAG, 2454 "icingSearchEngine.reset, response", 2455 resetResultProto.getStatus(), 2456 resetResultProto); 2457 mOptimizeIntervalCountLocked = 0; 2458 mSchemaCacheLocked.clear(); 2459 mNamespaceMapLocked.clear(); 2460 mDocumentCountMapLocked.clear(); 2461 synchronized (mNextPageTokensLocked) { 2462 mNextPageTokensLocked.clear(); 2463 } 2464 if (initStatsBuilder != null) { 2465 initStatsBuilder 2466 .setHasReset(true) 2467 .setResetStatusCode(statusProtoToResultCode(resetResultProto.getStatus())); 2468 } 2469 2470 checkSuccess(resetResultProto.getStatus()); 2471 } 2472 2473 @GuardedBy("mReadWriteLock") rebuildDocumentCountMapLocked(@onNull StorageInfoProto storageInfoProto)2474 private void rebuildDocumentCountMapLocked(@NonNull StorageInfoProto storageInfoProto) { 2475 mDocumentCountMapLocked.clear(); 2476 List<NamespaceStorageInfoProto> namespaceStorageInfoProtoList = 2477 storageInfoProto.getDocumentStorageInfo().getNamespaceStorageInfoList(); 2478 for (int i = 0; i < namespaceStorageInfoProtoList.size(); i++) { 2479 NamespaceStorageInfoProto namespaceStorageInfoProto = 2480 namespaceStorageInfoProtoList.get(i); 2481 String packageName = getPackageName(namespaceStorageInfoProto.getNamespace()); 2482 Integer oldCount = mDocumentCountMapLocked.get(packageName); 2483 int newCount; 2484 if (oldCount == null) { 2485 newCount = namespaceStorageInfoProto.getNumAliveDocuments(); 2486 } else { 2487 newCount = oldCount + namespaceStorageInfoProto.getNumAliveDocuments(); 2488 } 2489 mDocumentCountMapLocked.put(packageName, newCount); 2490 } 2491 } 2492 2493 /** Wrapper around schema changes */ 2494 @VisibleForTesting 2495 static class RewrittenSchemaResults { 2496 // Any prefixed types that used to exist in the schema, but are deleted in the new one. 2497 final Set<String> mDeletedPrefixedTypes = new ArraySet<>(); 2498 2499 // Map of prefixed schema types to SchemaTypeConfigProtos that were part of the new schema. 2500 final Map<String, SchemaTypeConfigProto> mRewrittenPrefixedTypes = new ArrayMap<>(); 2501 } 2502 2503 /** 2504 * Rewrites all types mentioned in the given {@code newSchema} to prepend {@code prefix}. 2505 * Rewritten types will be added to the {@code existingSchema}. 2506 * 2507 * @param prefix The full prefix to prepend to the schema. 2508 * @param existingSchema A schema that may contain existing types from across all prefixes. Will 2509 * be mutated to contain the properly rewritten schema types from {@code newSchema}. 2510 * @param newSchema Schema with types to add to the {@code existingSchema}. 2511 * @return a RewrittenSchemaResults that contains all prefixed schema type names in the given 2512 * prefix as well as a set of schema types that were deleted. 2513 */ 2514 @VisibleForTesting rewriteSchema( @onNull String prefix, @NonNull SchemaProto.Builder existingSchema, @NonNull SchemaProto newSchema)2515 static RewrittenSchemaResults rewriteSchema( 2516 @NonNull String prefix, 2517 @NonNull SchemaProto.Builder existingSchema, 2518 @NonNull SchemaProto newSchema) 2519 throws AppSearchException { 2520 HashMap<String, SchemaTypeConfigProto> newTypesToProto = new HashMap<>(); 2521 // Rewrite the schema type to include the typePrefix. 2522 for (int typeIdx = 0; typeIdx < newSchema.getTypesCount(); typeIdx++) { 2523 SchemaTypeConfigProto.Builder typeConfigBuilder = 2524 newSchema.getTypes(typeIdx).toBuilder(); 2525 2526 // Rewrite SchemaProto.types.schema_type 2527 String newSchemaType = prefix + typeConfigBuilder.getSchemaType(); 2528 typeConfigBuilder.setSchemaType(newSchemaType); 2529 2530 // Rewrite SchemaProto.types.properties.schema_type 2531 for (int propertyIdx = 0; 2532 propertyIdx < typeConfigBuilder.getPropertiesCount(); 2533 propertyIdx++) { 2534 PropertyConfigProto.Builder propertyConfigBuilder = 2535 typeConfigBuilder.getProperties(propertyIdx).toBuilder(); 2536 if (!propertyConfigBuilder.getSchemaType().isEmpty()) { 2537 String newPropertySchemaType = prefix + propertyConfigBuilder.getSchemaType(); 2538 propertyConfigBuilder.setSchemaType(newPropertySchemaType); 2539 typeConfigBuilder.setProperties(propertyIdx, propertyConfigBuilder); 2540 } 2541 } 2542 2543 // Rewrite SchemaProto.types.parent_types 2544 for (int parentTypeIdx = 0; 2545 parentTypeIdx < typeConfigBuilder.getParentTypesCount(); 2546 parentTypeIdx++) { 2547 String newParentType = prefix + typeConfigBuilder.getParentTypes(parentTypeIdx); 2548 typeConfigBuilder.setParentTypes(parentTypeIdx, newParentType); 2549 } 2550 2551 newTypesToProto.put(newSchemaType, typeConfigBuilder.build()); 2552 } 2553 2554 // newTypesToProto is modified below, so we need a copy first 2555 RewrittenSchemaResults rewrittenSchemaResults = new RewrittenSchemaResults(); 2556 rewrittenSchemaResults.mRewrittenPrefixedTypes.putAll(newTypesToProto); 2557 2558 // Combine the existing schema (which may have types from other prefixes) with this 2559 // prefix's new schema. Modifies the existingSchemaBuilder. 2560 // Check if we need to replace any old schema types with the new ones. 2561 for (int i = 0; i < existingSchema.getTypesCount(); i++) { 2562 String schemaType = existingSchema.getTypes(i).getSchemaType(); 2563 SchemaTypeConfigProto newProto = newTypesToProto.remove(schemaType); 2564 if (newProto != null) { 2565 // Replacement 2566 existingSchema.setTypes(i, newProto); 2567 } else if (prefix.equals(getPrefix(schemaType))) { 2568 // All types existing before but not in newSchema should be removed. 2569 existingSchema.removeTypes(i); 2570 --i; 2571 rewrittenSchemaResults.mDeletedPrefixedTypes.add(schemaType); 2572 } 2573 } 2574 // We've been removing existing types from newTypesToProto, so everything that remains is 2575 // new. 2576 existingSchema.addAllTypes(newTypesToProto.values()); 2577 2578 return rewrittenSchemaResults; 2579 } 2580 2581 @VisibleForTesting 2582 @GuardedBy("mReadWriteLock") getSchemaProtoLocked()2583 SchemaProto getSchemaProtoLocked() throws AppSearchException { 2584 LogUtil.piiTrace(TAG, "getSchema, request"); 2585 GetSchemaResultProto schemaProto = mIcingSearchEngineLocked.getSchema(); 2586 LogUtil.piiTrace(TAG, "getSchema, response", schemaProto.getStatus(), schemaProto); 2587 // TODO(b/161935693) check GetSchemaResultProto is success or not. Call reset() if it's not. 2588 // TODO(b/161935693) only allow GetSchemaResultProto NOT_FOUND on first run 2589 checkCodeOneOf(schemaProto.getStatus(), StatusProto.Code.OK, StatusProto.Code.NOT_FOUND); 2590 return schemaProto.getSchema(); 2591 } 2592 addNextPageToken(String packageName, long nextPageToken)2593 private void addNextPageToken(String packageName, long nextPageToken) { 2594 if (nextPageToken == EMPTY_PAGE_TOKEN) { 2595 // There is no more pages. No need to add it. 2596 return; 2597 } 2598 synchronized (mNextPageTokensLocked) { 2599 Set<Long> tokens = mNextPageTokensLocked.get(packageName); 2600 if (tokens == null) { 2601 tokens = new ArraySet<>(); 2602 mNextPageTokensLocked.put(packageName, tokens); 2603 } 2604 tokens.add(nextPageToken); 2605 } 2606 } 2607 checkNextPageToken(String packageName, long nextPageToken)2608 private void checkNextPageToken(String packageName, long nextPageToken) 2609 throws AppSearchException { 2610 if (nextPageToken == EMPTY_PAGE_TOKEN) { 2611 // Swallow the check for empty page token, token = 0 means there is no more page and it 2612 // won't return anything from Icing. 2613 return; 2614 } 2615 synchronized (mNextPageTokensLocked) { 2616 Set<Long> nextPageTokens = mNextPageTokensLocked.get(packageName); 2617 if (nextPageTokens == null || !nextPageTokens.contains(nextPageToken)) { 2618 throw new AppSearchException( 2619 RESULT_SECURITY_ERROR, 2620 "Package \"" 2621 + packageName 2622 + "\" cannot use nextPageToken: " 2623 + nextPageToken); 2624 } 2625 } 2626 } 2627 2628 /** 2629 * Adds an {@link ObserverCallback} to monitor changes within the databases owned by {@code 2630 * targetPackageName} if they match the given {@link 2631 * android.app.appsearch.observer.ObserverSpec}. 2632 * 2633 * <p>If the data owned by {@code targetPackageName} is not visible to you, the registration 2634 * call will succeed but no notifications will be dispatched. Notifications could start flowing 2635 * later if {@code targetPackageName} changes its schema visibility settings. 2636 * 2637 * <p>If no package matching {@code targetPackageName} exists on the system, the registration 2638 * call will succeed but no notifications will be dispatched. Notifications could start flowing 2639 * later if {@code targetPackageName} is installed and starts indexing data. 2640 * 2641 * <p>Note that this method does not take the standard read/write lock that guards I/O, so it 2642 * will not queue behind I/O. Therefore it is safe to call from any thread including UI or 2643 * binder threads. 2644 * 2645 * @param listeningPackageAccess Visibility information about the app that wants to receive 2646 * notifications. 2647 * @param targetPackageName The package that owns the data the observer wants to be notified 2648 * for. 2649 * @param spec Describes the kind of data changes the observer should trigger for. 2650 * @param executor The executor on which to trigger the observer callback to deliver 2651 * notifications. 2652 * @param observer The callback to trigger on notifications. 2653 */ registerObserverCallback( @onNull CallerAccess listeningPackageAccess, @NonNull String targetPackageName, @NonNull ObserverSpec spec, @NonNull Executor executor, @NonNull ObserverCallback observer)2654 public void registerObserverCallback( 2655 @NonNull CallerAccess listeningPackageAccess, 2656 @NonNull String targetPackageName, 2657 @NonNull ObserverSpec spec, 2658 @NonNull Executor executor, 2659 @NonNull ObserverCallback observer) { 2660 // This method doesn't consult mSchemaMap or mNamespaceMap, and it will register 2661 // observers for types that don't exist. This is intentional because we notify for types 2662 // being created or removed. If we only registered observer for existing types, it would 2663 // be impossible to ever dispatch a notification of a type being added. 2664 mObserverManager.registerObserverCallback( 2665 listeningPackageAccess, targetPackageName, spec, executor, observer); 2666 } 2667 2668 /** 2669 * Removes an {@link ObserverCallback} from watching the databases owned by {@code 2670 * targetPackageName}. 2671 * 2672 * <p>All observers which compare equal to the given observer via {@link 2673 * ObserverCallback#equals} are removed. This may be 0, 1, or many observers. 2674 * 2675 * <p>Note that this method does not take the standard read/write lock that guards I/O, so it 2676 * will not queue behind I/O. Therefore it is safe to call from any thread including UI or 2677 * binder threads. 2678 */ unregisterObserverCallback( @onNull String targetPackageName, @NonNull ObserverCallback observer)2679 public void unregisterObserverCallback( 2680 @NonNull String targetPackageName, @NonNull ObserverCallback observer) { 2681 mObserverManager.unregisterObserverCallback(targetPackageName, observer); 2682 } 2683 2684 /** 2685 * Dispatches the pending change notifications one at a time. 2686 * 2687 * <p>The notifications are dispatched on the respective executors that were provided at the 2688 * time of observer registration. This method does not take the standard read/write lock that 2689 * guards I/O, so it is safe to call from any thread including UI or binder threads. 2690 * 2691 * <p>Exceptions thrown from notification dispatch are logged but otherwise suppressed. 2692 */ dispatchAndClearChangeNotifications()2693 public void dispatchAndClearChangeNotifications() { 2694 mObserverManager.dispatchAndClearPendingNotifications(); 2695 } 2696 addToMap( Map<String, Set<String>> map, String prefix, String prefixedValue)2697 private static void addToMap( 2698 Map<String, Set<String>> map, String prefix, String prefixedValue) { 2699 Set<String> values = map.get(prefix); 2700 if (values == null) { 2701 values = new ArraySet<>(); 2702 map.put(prefix, values); 2703 } 2704 values.add(prefixedValue); 2705 } 2706 2707 /** 2708 * Checks the given status code and throws an {@link AppSearchException} if code is an error. 2709 * 2710 * @throws AppSearchException on error codes. 2711 */ checkSuccess(StatusProto statusProto)2712 private static void checkSuccess(StatusProto statusProto) throws AppSearchException { 2713 checkCodeOneOf(statusProto, StatusProto.Code.OK); 2714 } 2715 2716 /** 2717 * Checks the given status code is one of the provided codes, and throws an {@link 2718 * AppSearchException} if it is not. 2719 */ checkCodeOneOf(StatusProto statusProto, StatusProto.Code... codes)2720 private static void checkCodeOneOf(StatusProto statusProto, StatusProto.Code... codes) 2721 throws AppSearchException { 2722 for (int i = 0; i < codes.length; i++) { 2723 if (codes[i] == statusProto.getCode()) { 2724 // Everything's good 2725 return; 2726 } 2727 } 2728 2729 if (statusProto.getCode() == StatusProto.Code.WARNING_DATA_LOSS) { 2730 // TODO: May want to propagate WARNING_DATA_LOSS up to AppSearchSession so they can 2731 // choose to log the error or potentially pass it on to clients. 2732 Log.w(TAG, "Encountered WARNING_DATA_LOSS: " + statusProto.getMessage()); 2733 return; 2734 } 2735 2736 throw new AppSearchException( 2737 ResultCodeToProtoConverter.toResultCode(statusProto.getCode()), 2738 statusProto.getMessage()); 2739 } 2740 2741 /** 2742 * Checks whether {@link IcingSearchEngine#optimize()} should be called to release resources. 2743 * 2744 * <p>This method should be only called after a mutation to local storage backend which deletes 2745 * a mass of data and could release lots resources after {@link IcingSearchEngine#optimize()}. 2746 * 2747 * <p>This method will trigger {@link IcingSearchEngine#getOptimizeInfo()} to check resources 2748 * that could be released for every {@link #CHECK_OPTIMIZE_INTERVAL} mutations. 2749 * 2750 * <p>{@link IcingSearchEngine#optimize()} should be called only if {@link 2751 * GetOptimizeInfoResultProto} shows there is enough resources could be released. 2752 * 2753 * @param mutationSize The number of how many mutations have been executed for current request. 2754 * An inside counter will accumulates it. Once the counter reaches {@link 2755 * #CHECK_OPTIMIZE_INTERVAL}, {@link IcingSearchEngine#getOptimizeInfo()} will be triggered 2756 * and the counter will be reset. 2757 */ checkForOptimize(int mutationSize, @Nullable OptimizeStats.Builder builder)2758 public void checkForOptimize(int mutationSize, @Nullable OptimizeStats.Builder builder) 2759 throws AppSearchException { 2760 mReadWriteLock.writeLock().lock(); 2761 try { 2762 mOptimizeIntervalCountLocked += mutationSize; 2763 if (mOptimizeIntervalCountLocked >= CHECK_OPTIMIZE_INTERVAL) { 2764 checkForOptimize(builder); 2765 } 2766 } finally { 2767 mReadWriteLock.writeLock().unlock(); 2768 } 2769 } 2770 2771 /** 2772 * Checks whether {@link IcingSearchEngine#optimize()} should be called to release resources. 2773 * 2774 * <p>This method will directly trigger {@link IcingSearchEngine#getOptimizeInfo()} to check 2775 * resources that could be released. 2776 * 2777 * <p>{@link IcingSearchEngine#optimize()} should be called only if {@link 2778 * OptimizeStrategy#shouldOptimize(GetOptimizeInfoResultProto)} return true. 2779 */ checkForOptimize(@ullable OptimizeStats.Builder builder)2780 public void checkForOptimize(@Nullable OptimizeStats.Builder builder) 2781 throws AppSearchException { 2782 mReadWriteLock.writeLock().lock(); 2783 try { 2784 GetOptimizeInfoResultProto optimizeInfo = getOptimizeInfoResultLocked(); 2785 checkSuccess(optimizeInfo.getStatus()); 2786 mOptimizeIntervalCountLocked = 0; 2787 if (mOptimizeStrategy.shouldOptimize(optimizeInfo)) { 2788 optimize(builder); 2789 } 2790 } finally { 2791 mReadWriteLock.writeLock().unlock(); 2792 } 2793 // TODO(b/147699081): Return OptimizeResultProto & log lost data detail once we add 2794 // a field to indicate lost_schema and lost_documents in OptimizeResultProto. 2795 // go/icing-library-apis. 2796 } 2797 2798 /** Triggers {@link IcingSearchEngine#optimize()} directly. */ optimize(@ullable OptimizeStats.Builder builder)2799 public void optimize(@Nullable OptimizeStats.Builder builder) throws AppSearchException { 2800 mReadWriteLock.writeLock().lock(); 2801 try { 2802 LogUtil.piiTrace(TAG, "optimize, request"); 2803 OptimizeResultProto optimizeResultProto = mIcingSearchEngineLocked.optimize(); 2804 LogUtil.piiTrace( 2805 TAG, 2806 "optimize, response", 2807 optimizeResultProto.getStatus(), 2808 optimizeResultProto); 2809 if (builder != null) { 2810 builder.setStatusCode(statusProtoToResultCode(optimizeResultProto.getStatus())); 2811 AppSearchLoggerHelper.copyNativeStats( 2812 optimizeResultProto.getOptimizeStats(), builder); 2813 } 2814 checkSuccess(optimizeResultProto.getStatus()); 2815 } finally { 2816 mReadWriteLock.writeLock().unlock(); 2817 } 2818 } 2819 2820 /** Sync the current Android logging level to Icing for the entire process. No lock required. */ syncLoggingLevelToIcing()2821 public static void syncLoggingLevelToIcing() { 2822 String icingTag = IcingSearchEngine.getLoggingTag(); 2823 if (icingTag == null) { 2824 Log.e(TAG, "Received null logging tag from Icing"); 2825 return; 2826 } 2827 if (LogUtil.DEBUG) { 2828 if (Log.isLoggable(icingTag, Log.VERBOSE)) { 2829 boolean unused = 2830 IcingSearchEngine.setLoggingLevel( 2831 LogSeverity.Code.VERBOSE, /* verbosity= */ (short) 1); 2832 return; 2833 } else if (Log.isLoggable(icingTag, Log.DEBUG)) { 2834 boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.DBG); 2835 return; 2836 } 2837 } 2838 if (LogUtil.INFO) { 2839 if (Log.isLoggable(icingTag, Log.INFO)) { 2840 boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.INFO); 2841 return; 2842 } 2843 } 2844 if (Log.isLoggable(icingTag, Log.WARN)) { 2845 boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.WARNING); 2846 } else if (Log.isLoggable(icingTag, Log.ERROR)) { 2847 boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.ERROR); 2848 } else { 2849 boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.FATAL); 2850 } 2851 } 2852 2853 @GuardedBy("mReadWriteLock") 2854 @VisibleForTesting getOptimizeInfoResultLocked()2855 GetOptimizeInfoResultProto getOptimizeInfoResultLocked() { 2856 LogUtil.piiTrace(TAG, "getOptimizeInfo, request"); 2857 GetOptimizeInfoResultProto result = mIcingSearchEngineLocked.getOptimizeInfo(); 2858 LogUtil.piiTrace(TAG, "getOptimizeInfo, response", result.getStatus(), result); 2859 return result; 2860 } 2861 2862 /** 2863 * Returns all prefixed schema types saved in AppSearch. 2864 * 2865 * <p>This method is inefficient to call repeatedly. 2866 */ 2867 @NonNull getAllPrefixedSchemaTypes()2868 public List<String> getAllPrefixedSchemaTypes() { 2869 mReadWriteLock.readLock().lock(); 2870 try { 2871 return mSchemaCacheLocked.getAllPrefixedSchemaTypes(); 2872 } finally { 2873 mReadWriteLock.readLock().unlock(); 2874 } 2875 } 2876 2877 /** 2878 * Converts an erroneous status code from the Icing status enums to the AppSearchResult enums. 2879 * 2880 * <p>Callers should ensure that the status code is not OK or WARNING_DATA_LOSS. 2881 * 2882 * @param statusProto StatusProto with error code to translate into an {@link AppSearchResult} 2883 * code. 2884 * @return {@link AppSearchResult} error code 2885 */ 2886 @AppSearchResult.ResultCode statusProtoToResultCode(@onNull StatusProto statusProto)2887 private static int statusProtoToResultCode(@NonNull StatusProto statusProto) { 2888 return ResultCodeToProtoConverter.toResultCode(statusProto.getCode()); 2889 } 2890 } 2891