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