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 androidx.appsearch.localstorage; 18 19 import static androidx.appsearch.app.AppSearchResult.RESULT_SECURITY_ERROR; 20 import static androidx.appsearch.app.AppSearchResult.throwableToFailedResult; 21 import static androidx.appsearch.app.InternalSetSchemaResponse.newFailedSetSchemaResponse; 22 import static androidx.appsearch.app.InternalSetSchemaResponse.newSuccessfulSetSchemaResponse; 23 import static androidx.appsearch.localstorage.util.PrefixUtil.addPrefixToDocument; 24 import static androidx.appsearch.localstorage.util.PrefixUtil.createPrefix; 25 import static androidx.appsearch.localstorage.util.PrefixUtil.getDatabaseName; 26 import static androidx.appsearch.localstorage.util.PrefixUtil.getPackageName; 27 import static androidx.appsearch.localstorage.util.PrefixUtil.getPrefix; 28 import static androidx.appsearch.localstorage.util.PrefixUtil.removePrefixesFromDocument; 29 30 import android.os.ParcelFileDescriptor; 31 import android.os.SystemClock; 32 import android.util.Log; 33 34 import androidx.annotation.GuardedBy; 35 import androidx.annotation.OptIn; 36 import androidx.annotation.RestrictTo; 37 import androidx.annotation.VisibleForTesting; 38 import androidx.annotation.WorkerThread; 39 import androidx.appsearch.app.AppSearchBatchResult; 40 import androidx.appsearch.app.AppSearchBlobHandle; 41 import androidx.appsearch.app.AppSearchResult; 42 import androidx.appsearch.app.AppSearchSchema; 43 import androidx.appsearch.app.ExperimentalAppSearchApi; 44 import androidx.appsearch.app.GenericDocument; 45 import androidx.appsearch.app.GetByDocumentIdRequest; 46 import androidx.appsearch.app.GetSchemaResponse; 47 import androidx.appsearch.app.InternalSetSchemaResponse; 48 import androidx.appsearch.app.InternalVisibilityConfig; 49 import androidx.appsearch.app.JoinSpec; 50 import androidx.appsearch.app.PackageIdentifier; 51 import androidx.appsearch.app.SchemaVisibilityConfig; 52 import androidx.appsearch.app.SearchResultPage; 53 import androidx.appsearch.app.SearchSpec; 54 import androidx.appsearch.app.SearchSuggestionResult; 55 import androidx.appsearch.app.SearchSuggestionSpec; 56 import androidx.appsearch.app.SetSchemaResponse; 57 import androidx.appsearch.app.StorageInfo; 58 import androidx.appsearch.exceptions.AppSearchException; 59 import androidx.appsearch.flags.Flags; 60 import androidx.appsearch.localstorage.converter.BlobHandleToProtoConverter; 61 import androidx.appsearch.localstorage.converter.GenericDocumentToProtoConverter; 62 import androidx.appsearch.localstorage.converter.ResultCodeToProtoConverter; 63 import androidx.appsearch.localstorage.converter.SchemaToProtoConverter; 64 import androidx.appsearch.localstorage.converter.SearchResultToProtoConverter; 65 import androidx.appsearch.localstorage.converter.SearchSpecToProtoConverter; 66 import androidx.appsearch.localstorage.converter.SearchSuggestionSpecToProtoConverter; 67 import androidx.appsearch.localstorage.converter.SetSchemaResponseToProtoConverter; 68 import androidx.appsearch.localstorage.converter.TypePropertyPathToProtoConverter; 69 import androidx.appsearch.localstorage.stats.InitializeStats; 70 import androidx.appsearch.localstorage.stats.OptimizeStats; 71 import androidx.appsearch.localstorage.stats.PutDocumentStats; 72 import androidx.appsearch.localstorage.stats.RemoveStats; 73 import androidx.appsearch.localstorage.stats.SearchStats; 74 import androidx.appsearch.localstorage.stats.SetSchemaStats; 75 import androidx.appsearch.localstorage.util.PrefixUtil; 76 import androidx.appsearch.localstorage.visibilitystore.CallerAccess; 77 import androidx.appsearch.localstorage.visibilitystore.VisibilityChecker; 78 import androidx.appsearch.localstorage.visibilitystore.VisibilityStore; 79 import androidx.appsearch.localstorage.visibilitystore.VisibilityUtil; 80 import androidx.appsearch.observer.ObserverCallback; 81 import androidx.appsearch.observer.ObserverSpec; 82 import androidx.appsearch.util.LogUtil; 83 import androidx.collection.ArrayMap; 84 import androidx.collection.ArraySet; 85 import androidx.core.util.ObjectsCompat; 86 import androidx.core.util.Preconditions; 87 88 import com.google.android.icing.IcingSearchEngine; 89 import com.google.android.icing.IcingSearchEngineInterface; 90 import com.google.android.icing.proto.BatchGetResultProto; 91 import com.google.android.icing.proto.BatchPutResultProto; 92 import com.google.android.icing.proto.BlobProto; 93 import com.google.android.icing.proto.DebugInfoProto; 94 import com.google.android.icing.proto.DebugInfoResultProto; 95 import com.google.android.icing.proto.DebugInfoVerbosity; 96 import com.google.android.icing.proto.DeleteByQueryResultProto; 97 import com.google.android.icing.proto.DeleteResultProto; 98 import com.google.android.icing.proto.DocumentProto; 99 import com.google.android.icing.proto.DocumentStorageInfoProto; 100 import com.google.android.icing.proto.GetAllNamespacesResultProto; 101 import com.google.android.icing.proto.GetOptimizeInfoResultProto; 102 import com.google.android.icing.proto.GetResultProto; 103 import com.google.android.icing.proto.GetResultSpecProto; 104 import com.google.android.icing.proto.GetSchemaResultProto; 105 import com.google.android.icing.proto.IcingSearchEngineOptions; 106 import com.google.android.icing.proto.InitializeResultProto; 107 import com.google.android.icing.proto.LogSeverity; 108 import com.google.android.icing.proto.NamespaceBlobStorageInfoProto; 109 import com.google.android.icing.proto.NamespaceStorageInfoProto; 110 import com.google.android.icing.proto.OptimizeResultProto; 111 import com.google.android.icing.proto.PersistToDiskResultProto; 112 import com.google.android.icing.proto.PersistType; 113 import com.google.android.icing.proto.PropertyConfigProto; 114 import com.google.android.icing.proto.PropertyProto; 115 import com.google.android.icing.proto.PutDocumentRequest; 116 import com.google.android.icing.proto.PutResultProto; 117 import com.google.android.icing.proto.ReportUsageResultProto; 118 import com.google.android.icing.proto.ResetResultProto; 119 import com.google.android.icing.proto.ResultSpecProto; 120 import com.google.android.icing.proto.SchemaProto; 121 import com.google.android.icing.proto.SchemaTypeConfigProto; 122 import com.google.android.icing.proto.ScoringSpecProto; 123 import com.google.android.icing.proto.SearchResultProto; 124 import com.google.android.icing.proto.SearchSpecProto; 125 import com.google.android.icing.proto.SetSchemaResultProto; 126 import com.google.android.icing.proto.StatusProto; 127 import com.google.android.icing.proto.StorageInfoProto; 128 import com.google.android.icing.proto.StorageInfoResultProto; 129 import com.google.android.icing.proto.SuggestionResponse; 130 import com.google.android.icing.proto.TypePropertyMask; 131 import com.google.android.icing.proto.UsageReport; 132 133 import org.jspecify.annotations.NonNull; 134 import org.jspecify.annotations.Nullable; 135 136 import java.io.Closeable; 137 import java.io.File; 138 import java.io.IOException; 139 import java.io.InputStream; 140 import java.security.DigestInputStream; 141 import java.security.MessageDigest; 142 import java.security.NoSuchAlgorithmException; 143 import java.util.ArrayList; 144 import java.util.Arrays; 145 import java.util.Collections; 146 import java.util.HashMap; 147 import java.util.List; 148 import java.util.Map; 149 import java.util.Set; 150 import java.util.concurrent.Executor; 151 import java.util.concurrent.locks.ReadWriteLock; 152 import java.util.concurrent.locks.ReentrantReadWriteLock; 153 154 /** 155 * Manages interaction with the native IcingSearchEngine and other components to implement AppSearch 156 * functionality. 157 * 158 * <p>Never create two instances using the same folder. 159 * 160 * <p>A single instance of {@link AppSearchImpl} can support all packages and databases. 161 * This is done by combining the package and database name into a unique prefix and 162 * prefixing the schemas and documents stored under that owner. Schemas and documents are 163 * physically saved together in {@link IcingSearchEngine}, but logically isolated: 164 * <ul> 165 * <li>Rewrite SchemaType in SchemaProto by adding the package-database prefix and save into 166 * SchemaTypes set in {@link #setSchema}. 167 * <li>Rewrite namespace and SchemaType in DocumentProto by adding package-database prefix and 168 * save to namespaces set in {@link #putDocument}. 169 * <li>Remove package-database prefix when retrieving documents in {@link #getDocument} and 170 * {@link #query}. 171 * <li>Rewrite filters in {@link SearchSpecProto} to have all namespaces and schema types of 172 * the queried database when user using empty filters in {@link #query}. 173 * </ul> 174 * 175 * <p>Methods in this class belong to two groups, the query group and the mutate group. 176 * <ul> 177 * <li>All methods are going to modify global parameters and data in Icing are executed under 178 * WRITE lock to keep thread safety. 179 * <li>All methods are going to access global parameters or query data from Icing are executed 180 * under READ lock to improve query performance. 181 * </ul> 182 * 183 * <p>This class is thread safe. 184 * 185 * @exportToFramework:hide 186 */ 187 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 188 @WorkerThread 189 public final class AppSearchImpl implements Closeable { 190 private static final String TAG = "AppSearchImpl"; 191 192 /** A value 0 means that there're no more pages in the search results. */ 193 private static final long EMPTY_PAGE_TOKEN = 0; 194 @VisibleForTesting 195 static final int CHECK_OPTIMIZE_INTERVAL = 100; 196 197 /** A GetResultSpec that uses projection to skip all properties. */ 198 private static final GetResultSpecProto GET_RESULT_SPEC_NO_PROPERTIES = 199 GetResultSpecProto.newBuilder().addTypePropertyMasks( 200 TypePropertyMask.newBuilder().setSchemaType( 201 GetByDocumentIdRequest.PROJECTION_SCHEMA_TYPE_WILDCARD)).build(); 202 203 private final ReadWriteLock mReadWriteLock = new ReentrantReadWriteLock(); 204 private final OptimizeStrategy mOptimizeStrategy; 205 private final AppSearchConfig mConfig; 206 private final File mBlobFilesDir; 207 208 @GuardedBy("mReadWriteLock") 209 @VisibleForTesting 210 final IcingSearchEngineInterface mIcingSearchEngineLocked; 211 private final boolean mIsVMEnabled; 212 213 @GuardedBy("mReadWriteLock") 214 private final SchemaCache mSchemaCacheLocked = new SchemaCache(); 215 216 @GuardedBy("mReadWriteLock") 217 private final NamespaceCache mNamespaceCacheLocked = new NamespaceCache(); 218 219 // Marked as volatile because a new instance may be assigned during resetLocked. 220 @GuardedBy("mReadWriteLock") 221 private volatile DocumentLimiter mDocumentLimiterLocked; 222 223 // Maps packages to the set of valid nextPageTokens that the package can manipulate. A token 224 // is unique and constant per query (i.e. the same token '123' is used to iterate through 225 // pages of search results). The tokens themselves are generated and tracked by 226 // IcingSearchEngine. IcingSearchEngine considers a token valid and won't be reused 227 // until we call invalidateNextPageToken on the token. 228 // 229 // Note that we synchronize on itself because the nextPageToken cache is checked at 230 // query-time, and queries are done in parallel with a read lock. Ideally, this would be 231 // guarded by the normal mReadWriteLock.writeLock, but ReentrantReadWriteLocks can't upgrade 232 // read to write locks. This lock should be acquired at the smallest scope possible. 233 // mReadWriteLock is a higher-level lock, so calls shouldn't be made out 234 // to any functions that grab the lock. 235 @GuardedBy("mNextPageTokensLocked") 236 private final Map<String, Set<Long>> mNextPageTokensLocked = new ArrayMap<>(); 237 238 private final ObserverManager mObserverManager = new ObserverManager(); 239 240 /** 241 * VisibilityStore will be used in {@link #setSchema} and {@link #getSchema} to store and query 242 * visibility information. But to create a {@link VisibilityStore}, it will call 243 * {@link #setSchema} and {@link #getSchema} to get the visibility schema. Make it nullable to 244 * avoid call it before we actually create it. 245 */ 246 @VisibleForTesting 247 @GuardedBy("mReadWriteLock") 248 final @Nullable VisibilityStore mDocumentVisibilityStoreLocked; 249 250 @VisibleForTesting 251 @GuardedBy("mReadWriteLock") 252 final @Nullable VisibilityStore mBlobVisibilityStoreLocked; 253 254 @GuardedBy("mReadWriteLock") 255 private final @Nullable VisibilityChecker mVisibilityCheckerLocked; 256 257 /** 258 * The counter to check when to call {@link #checkForOptimize}. The 259 * interval is 260 * {@link #CHECK_OPTIMIZE_INTERVAL}. 261 */ 262 @GuardedBy("mReadWriteLock") 263 private int mOptimizeIntervalCountLocked = 0; 264 265 @ExperimentalAppSearchApi 266 private final @Nullable RevocableFileDescriptorStore mRevocableFileDescriptorStore; 267 268 /** Whether this instance has been closed, and therefore unusable. */ 269 @GuardedBy("mReadWriteLock") 270 private boolean mClosedLocked = false; 271 272 /** 273 * Creates and initializes an instance of {@link AppSearchImpl} which writes data to the given 274 * folder. 275 * 276 * <p>Clients can pass a {@link AppSearchLogger} here through their AppSearchSession, but it 277 * can't be saved inside {@link AppSearchImpl}, because the impl will be shared by all the 278 * sessions for the same package in JetPack. 279 * 280 * <p>Instead, logger instance needs to be passed to each individual method, like create, query 281 * and putDocument. 282 * 283 * @param initStatsBuilder collects stats for initialization if provided. 284 * @param visibilityChecker The {@link VisibilityChecker} that check whether the caller has 285 * access to aa specific schema. Pass null will lost that ability and 286 * global querier could only get their own data. 287 * @param icingSearchEngine the underlying icing instance to use. If not provided, a new {@link 288 * IcingSearchEngine} instance will be created and used. 289 */ 290 @OptIn(markerClass = ExperimentalAppSearchApi.class) create( @onNull File icingDir, @NonNull AppSearchConfig config, InitializeStats.@Nullable Builder initStatsBuilder, @Nullable VisibilityChecker visibilityChecker, @Nullable RevocableFileDescriptorStore revocableFileDescriptorStore, @Nullable IcingSearchEngineInterface icingSearchEngine, @NonNull OptimizeStrategy optimizeStrategy)291 public static @NonNull AppSearchImpl create( 292 @NonNull File icingDir, 293 @NonNull AppSearchConfig config, 294 InitializeStats.@Nullable Builder initStatsBuilder, 295 @Nullable VisibilityChecker visibilityChecker, 296 @Nullable RevocableFileDescriptorStore revocableFileDescriptorStore, 297 @Nullable IcingSearchEngineInterface icingSearchEngine, 298 @NonNull OptimizeStrategy optimizeStrategy) 299 throws AppSearchException { 300 return new AppSearchImpl(icingDir, config, initStatsBuilder, visibilityChecker, 301 revocableFileDescriptorStore, icingSearchEngine, optimizeStrategy); 302 } 303 304 /** 305 * @param initStatsBuilder collects stats for initialization if provided. 306 */ 307 @OptIn(markerClass = ExperimentalAppSearchApi.class) AppSearchImpl( @onNull File icingDir, @NonNull AppSearchConfig config, InitializeStats.@Nullable Builder initStatsBuilder, @Nullable VisibilityChecker visibilityChecker, @Nullable RevocableFileDescriptorStore revocableFileDescriptorStore, @Nullable IcingSearchEngineInterface icingSearchEngine, @NonNull OptimizeStrategy optimizeStrategy)308 private AppSearchImpl( 309 @NonNull File icingDir, 310 @NonNull AppSearchConfig config, 311 InitializeStats.@Nullable Builder initStatsBuilder, 312 @Nullable VisibilityChecker visibilityChecker, 313 @Nullable RevocableFileDescriptorStore revocableFileDescriptorStore, 314 @Nullable IcingSearchEngineInterface icingSearchEngine, 315 @NonNull OptimizeStrategy optimizeStrategy) 316 throws AppSearchException { 317 Preconditions.checkNotNull(icingDir); 318 // This directory stores blob files. It is the same directory that Icing used to manage 319 // blob files when Flags.enableAppSearchManageBlobFiles() was false. After the rollout of 320 // this flag, AppSearch will continue to manage blob files in this same directory within 321 // Icing's directory. The location remains unchanged to ensure that the flag does not 322 // introduce any behavioral changes. 323 mBlobFilesDir = new File(icingDir, "blob_dir/blob_files"); 324 mConfig = Preconditions.checkNotNull(config); 325 mOptimizeStrategy = Preconditions.checkNotNull(optimizeStrategy); 326 mVisibilityCheckerLocked = visibilityChecker; 327 mRevocableFileDescriptorStore = revocableFileDescriptorStore; 328 329 mReadWriteLock.writeLock().lock(); 330 try { 331 // We synchronize here because we don't want to call IcingSearchEngine.initialize() more 332 // than once. It's unnecessary and can be a costly operation. 333 if (icingSearchEngine == null) { 334 mIsVMEnabled = false; 335 IcingSearchEngineOptions options = mConfig.toIcingSearchEngineOptions( 336 icingDir.getAbsolutePath(), mIsVMEnabled); 337 LogUtil.piiTrace(TAG, "Constructing IcingSearchEngine, request", options); 338 mIcingSearchEngineLocked = new IcingSearchEngine(options); 339 LogUtil.piiTrace( 340 TAG, 341 "Constructing IcingSearchEngine, response", 342 ObjectsCompat.hashCode(mIcingSearchEngineLocked)); 343 } else { 344 mIcingSearchEngineLocked = icingSearchEngine; 345 mIsVMEnabled = true; 346 } 347 348 // The core initialization procedure. If any part of this fails, we bail into 349 // resetLocked(), deleting all data (but hopefully allowing AppSearchImpl to come up). 350 try { 351 LogUtil.piiTrace(TAG, "icingSearchEngine.initialize, request"); 352 InitializeResultProto initializeResultProto = mIcingSearchEngineLocked.initialize(); 353 LogUtil.piiTrace( 354 TAG, 355 "icingSearchEngine.initialize, response", 356 initializeResultProto.getStatus(), 357 initializeResultProto); 358 359 if (initStatsBuilder != null) { 360 initStatsBuilder 361 .setStatusCode( 362 statusProtoToResultCode(initializeResultProto.getStatus())) 363 // TODO(b/173532925) how to get DeSyncs value 364 .setHasDeSync(false) 365 .setLaunchVMEnabled(mIsVMEnabled); 366 AppSearchLoggerHelper.copyNativeStats( 367 initializeResultProto.getInitializeStats(), initStatsBuilder); 368 } 369 checkSuccess(initializeResultProto.getStatus()); 370 371 if (Flags.enableAppSearchManageBlobFiles() && !mBlobFilesDir.exists() 372 && !mBlobFilesDir.mkdirs()) { 373 throw new AppSearchException(AppSearchResult.RESULT_IO_ERROR, 374 "Cannot create the blob file directory: " 375 + mBlobFilesDir.getAbsolutePath()); 376 } 377 378 // Read all protos we need to construct AppSearchImpl's cache maps 379 long prepareSchemaAndNamespacesLatencyStartMillis = SystemClock.elapsedRealtime(); 380 SchemaProto schemaProto = getSchemaProtoLocked(); 381 382 LogUtil.piiTrace(TAG, "init:getAllNamespaces, request"); 383 GetAllNamespacesResultProto getAllNamespacesResultProto = 384 mIcingSearchEngineLocked.getAllNamespaces(); 385 LogUtil.piiTrace( 386 TAG, 387 "init:getAllNamespaces, response", 388 getAllNamespacesResultProto.getNamespacesCount(), 389 getAllNamespacesResultProto); 390 391 StorageInfoProto storageInfoProto = getRawStorageInfoProto(); 392 393 // Log the time it took to read the data that goes into the cache maps 394 if (initStatsBuilder != null) { 395 // In case there is some error for getAllNamespaces, we can still 396 // set the latency for preparation. 397 // If there is no error, the value will be overridden by the actual one later. 398 initStatsBuilder.setStatusCode( 399 statusProtoToResultCode(getAllNamespacesResultProto.getStatus())) 400 .setPrepareSchemaAndNamespacesLatencyMillis( 401 (int) (SystemClock.elapsedRealtime() 402 - prepareSchemaAndNamespacesLatencyStartMillis)); 403 } 404 checkSuccess(getAllNamespacesResultProto.getStatus()); 405 406 // Populate schema map 407 List<SchemaTypeConfigProto> schemaProtoTypesList = schemaProto.getTypesList(); 408 for (int i = 0; i < schemaProtoTypesList.size(); i++) { 409 SchemaTypeConfigProto schema = schemaProtoTypesList.get(i); 410 String prefixedSchemaType = schema.getSchemaType(); 411 mSchemaCacheLocked.addToSchemaMap(getPrefix(prefixedSchemaType), schema); 412 } 413 414 // Populate schema parent-to-children map 415 mSchemaCacheLocked.rebuildCache(); 416 417 // Populate namespace map 418 List<String> prefixedNamespaceList = 419 getAllNamespacesResultProto.getNamespacesList(); 420 for (int i = 0; i < prefixedNamespaceList.size(); i++) { 421 String prefixedNamespace = prefixedNamespaceList.get(i); 422 mNamespaceCacheLocked.addToDocumentNamespaceMap( 423 getPrefix(prefixedNamespace), prefixedNamespace); 424 } 425 426 // Populate blob namespace map 427 if (mRevocableFileDescriptorStore != null) { 428 List<NamespaceBlobStorageInfoProto> namespaceBlobStorageInfoProto = 429 storageInfoProto.getNamespaceBlobStorageInfoList(); 430 for (int i = 0; i < namespaceBlobStorageInfoProto.size(); i++) { 431 String prefixedNamespace = namespaceBlobStorageInfoProto.get( 432 i).getNamespace(); 433 mNamespaceCacheLocked.addToBlobNamespaceMap( 434 getPrefix(prefixedNamespace), prefixedNamespace); 435 } 436 } 437 438 // Populate document count map 439 mDocumentLimiterLocked = 440 new DocumentLimiter( 441 mConfig.getDocumentCountLimitStartThreshold(), 442 mConfig.getPerPackageDocumentCountLimit(), 443 storageInfoProto.getDocumentStorageInfo() 444 .getNamespaceStorageInfoList()); 445 446 // logging prepare_schema_and_namespaces latency 447 if (initStatsBuilder != null) { 448 initStatsBuilder.setPrepareSchemaAndNamespacesLatencyMillis( 449 (int) (SystemClock.elapsedRealtime() 450 - prepareSchemaAndNamespacesLatencyStartMillis)); 451 } 452 453 LogUtil.piiTrace(TAG, "Init completed successfully"); 454 455 } catch (AppSearchException e) { 456 // Some error. Reset and see if it fixes it. 457 Log.e(TAG, "Error initializing, resetting IcingSearchEngine.", e); 458 if (initStatsBuilder != null) { 459 initStatsBuilder.setStatusCode(e.getResultCode()); 460 } 461 resetLocked(initStatsBuilder); 462 } 463 464 // AppSearchImpl core parameters are initialized and we should be able to build 465 // VisibilityStores based on that. We shouldn't wipe out everything if we only failed to 466 // build VisibilityStores. 467 long prepareVisibilityStoreLatencyStartMillis = SystemClock.elapsedRealtime(); 468 mDocumentVisibilityStoreLocked = VisibilityStore.createDocumentVisibilityStore(this); 469 if (mRevocableFileDescriptorStore != null) { 470 mBlobVisibilityStoreLocked = VisibilityStore.createBlobVisibilityStore(this); 471 } else { 472 mBlobVisibilityStoreLocked = null; 473 } 474 long prepareVisibilityStoreLatencyEndMillis = SystemClock.elapsedRealtime(); 475 if (initStatsBuilder != null) { 476 initStatsBuilder.setPrepareVisibilityStoreLatencyMillis((int) 477 (prepareVisibilityStoreLatencyEndMillis 478 - prepareVisibilityStoreLatencyStartMillis)); 479 } 480 } finally { 481 mReadWriteLock.writeLock().unlock(); 482 } 483 } 484 485 @GuardedBy("mReadWriteLock") throwIfClosedLocked()486 private void throwIfClosedLocked() { 487 if (mClosedLocked) { 488 throw new IllegalStateException("Trying to use a closed AppSearchImpl instance."); 489 } 490 } 491 492 /** 493 * Persists data to disk and closes the instance. 494 * 495 * <p>This instance is no longer usable after it's been closed. Call {@link #create} to 496 * create a new, usable instance. 497 */ 498 @Override 499 @OptIn(markerClass = ExperimentalAppSearchApi.class) close()500 public void close() { 501 mReadWriteLock.writeLock().lock(); 502 try { 503 if (mClosedLocked) { 504 return; 505 } 506 persistToDisk(PersistType.Code.FULL); 507 LogUtil.piiTrace(TAG, "icingSearchEngine.close, request"); 508 mIcingSearchEngineLocked.close(); 509 LogUtil.piiTrace(TAG, "icingSearchEngine.close, response"); 510 if (mRevocableFileDescriptorStore != null) { 511 mRevocableFileDescriptorStore.revokeAll(); 512 } 513 mClosedLocked = true; 514 } catch (AppSearchException | IOException e) { 515 Log.w(TAG, "Error when closing AppSearchImpl.", e); 516 } finally { 517 mReadWriteLock.writeLock().unlock(); 518 } 519 } 520 521 /** 522 * Returns the instance of AppSearchConfig used by this instance of AppSearchImpl. 523 */ getConfig()524 public @NonNull AppSearchConfig getConfig() { 525 return mConfig; 526 } 527 528 /** Returns whether pVM is enabled in this AppSearchImpl instance. */ isVMEnabled()529 public boolean isVMEnabled() { 530 return mIsVMEnabled; 531 } 532 533 /** 534 * Updates the AppSearch schema for this app. 535 * 536 * <p>This method belongs to mutate group. 537 * 538 * @param packageName The package name that owns the schemas. 539 * @param databaseName The name of the database where this schema lives. 540 * @param schemas Schemas to set for this app. 541 * @param visibilityConfigs {@link InternalVisibilityConfig}s that contain all 542 * visibility setting information for those schemas 543 * has user custom settings. Other schemas in the list 544 * that don't has a {@link InternalVisibilityConfig} 545 * will be treated as having the default visibility, 546 * which is accessible by the system and no other packages. 547 * @param forceOverride Whether to force-apply the schema even if it is 548 * incompatible. Documents 549 * which do not comply with the new schema will be deleted. 550 * @param version The overall version number of the request. 551 * @param setSchemaStatsBuilder Builder for {@link SetSchemaStats} to hold stats for 552 * setSchema 553 * @return A success {@link InternalSetSchemaResponse} with a {@link SetSchemaResponse}. Or a 554 * failed {@link InternalSetSchemaResponse} if this call contains incompatible change. The 555 * {@link SetSchemaResponse} in the failed {@link InternalSetSchemaResponse} contains which type 556 * is incompatible. You need to check the status by 557 * {@link InternalSetSchemaResponse#isSuccess()}. 558 * 559 * @throws AppSearchException On IcingSearchEngine error. If the status code is 560 * FAILED_PRECONDITION for the incompatible change, the 561 * exception will be converted to the SetSchemaResponse. 562 */ setSchema( @onNull String packageName, @NonNull String databaseName, @NonNull List<AppSearchSchema> schemas, @NonNull List<InternalVisibilityConfig> visibilityConfigs, boolean forceOverride, int version, SetSchemaStats.@Nullable Builder setSchemaStatsBuilder)563 public @NonNull InternalSetSchemaResponse setSchema( 564 @NonNull String packageName, 565 @NonNull String databaseName, 566 @NonNull List<AppSearchSchema> schemas, 567 @NonNull List<InternalVisibilityConfig> visibilityConfigs, 568 boolean forceOverride, 569 int version, 570 SetSchemaStats.@Nullable Builder setSchemaStatsBuilder) throws AppSearchException { 571 long javaLockAcquisitionLatencyStartMillis = SystemClock.elapsedRealtime(); 572 mReadWriteLock.writeLock().lock(); 573 try { 574 throwIfClosedLocked(); 575 if (setSchemaStatsBuilder != null) { 576 setSchemaStatsBuilder.setJavaLockAcquisitionLatencyMillis( 577 (int) (SystemClock.elapsedRealtime() 578 - javaLockAcquisitionLatencyStartMillis)) 579 .setLaunchVMEnabled(mIsVMEnabled); 580 } 581 if (mObserverManager.isPackageObserved(packageName)) { 582 return doSetSchemaWithChangeNotificationLocked( 583 packageName, 584 databaseName, 585 schemas, 586 visibilityConfigs, 587 forceOverride, 588 version, 589 setSchemaStatsBuilder); 590 } else { 591 return doSetSchemaNoChangeNotificationLocked( 592 packageName, 593 databaseName, 594 schemas, 595 visibilityConfigs, 596 forceOverride, 597 version, 598 setSchemaStatsBuilder); 599 } 600 } finally { 601 mReadWriteLock.writeLock().unlock(); 602 } 603 } 604 605 /** 606 * Updates the AppSearch schema for this app, dispatching change notifications. 607 * 608 * @see #setSchema 609 * @see #doSetSchemaNoChangeNotificationLocked 610 */ 611 @GuardedBy("mReadWriteLock") doSetSchemaWithChangeNotificationLocked( @onNull String packageName, @NonNull String databaseName, @NonNull List<AppSearchSchema> schemas, @NonNull List<InternalVisibilityConfig> visibilityConfigs, boolean forceOverride, int version, SetSchemaStats.@Nullable Builder setSchemaStatsBuilder)612 private @NonNull InternalSetSchemaResponse doSetSchemaWithChangeNotificationLocked( 613 @NonNull String packageName, 614 @NonNull String databaseName, 615 @NonNull List<AppSearchSchema> schemas, 616 @NonNull List<InternalVisibilityConfig> visibilityConfigs, 617 boolean forceOverride, 618 int version, 619 SetSchemaStats.@Nullable Builder setSchemaStatsBuilder) throws AppSearchException { 620 // First, capture the old state of the system. This includes the old schema as well as 621 // whether each registered observer can access each type. Once VisibilityStore is updated 622 // by the setSchema call, the information of which observers could see which types will be 623 // lost. 624 long getOldSchemaStartTimeMillis = SystemClock.elapsedRealtime(); 625 GetSchemaResponse oldSchema = getSchema( 626 packageName, 627 databaseName, 628 // A CallerAccess object for internal use that has local access to this database. 629 new CallerAccess(/*callingPackageName=*/packageName)); 630 long getOldSchemaEndTimeMillis = SystemClock.elapsedRealtime(); 631 if (setSchemaStatsBuilder != null) { 632 setSchemaStatsBuilder.setIsPackageObserved(true) 633 .setGetOldSchemaLatencyMillis( 634 (int) (getOldSchemaEndTimeMillis - getOldSchemaStartTimeMillis)); 635 } 636 637 long getOldSchemaObserverStartTimeMillis = SystemClock.elapsedRealtime(); 638 // Cache some lookup tables to help us work with the old schema 639 Set<AppSearchSchema> oldSchemaTypes = oldSchema.getSchemas(); 640 Map<String, AppSearchSchema> oldSchemaNameToType = new ArrayMap<>(oldSchemaTypes.size()); 641 // Maps unprefixed schema name to the set of listening packages that had visibility into 642 // that type under the old schema. 643 Map<String, Set<String>> oldSchemaNameToVisibleListeningPackage = 644 new ArrayMap<>(oldSchemaTypes.size()); 645 for (AppSearchSchema oldSchemaType : oldSchemaTypes) { 646 String oldSchemaName = oldSchemaType.getSchemaType(); 647 oldSchemaNameToType.put(oldSchemaName, oldSchemaType); 648 oldSchemaNameToVisibleListeningPackage.put( 649 oldSchemaName, 650 mObserverManager.getObserversForSchemaType( 651 packageName, 652 databaseName, 653 oldSchemaName, 654 mDocumentVisibilityStoreLocked, 655 mVisibilityCheckerLocked)); 656 } 657 int getOldSchemaObserverLatencyMillis = 658 (int) (SystemClock.elapsedRealtime() - getOldSchemaObserverStartTimeMillis); 659 660 // Apply the new schema 661 InternalSetSchemaResponse internalSetSchemaResponse = doSetSchemaNoChangeNotificationLocked( 662 packageName, 663 databaseName, 664 schemas, 665 visibilityConfigs, 666 forceOverride, 667 version, 668 setSchemaStatsBuilder); 669 670 // This check is needed wherever setSchema is called to detect soft errors which do not 671 // throw an exception but also prevent the schema from actually being applied. 672 if (!internalSetSchemaResponse.isSuccess()) { 673 return internalSetSchemaResponse; 674 } 675 676 long getNewSchemaObserverStartTimeMillis = SystemClock.elapsedRealtime(); 677 // Cache some lookup tables to help us work with the new schema 678 Map<String, AppSearchSchema> newSchemaNameToType = new ArrayMap<>(schemas.size()); 679 // Maps unprefixed schema name to the set of listening packages that have visibility into 680 // that type under the new schema. 681 Map<String, Set<String>> newSchemaNameToVisibleListeningPackage = 682 new ArrayMap<>(schemas.size()); 683 for (AppSearchSchema newSchemaType : schemas) { 684 String newSchemaName = newSchemaType.getSchemaType(); 685 newSchemaNameToType.put(newSchemaName, newSchemaType); 686 newSchemaNameToVisibleListeningPackage.put( 687 newSchemaName, 688 mObserverManager.getObserversForSchemaType( 689 packageName, 690 databaseName, 691 newSchemaName, 692 mDocumentVisibilityStoreLocked, 693 mVisibilityCheckerLocked)); 694 } 695 long getNewSchemaObserverEndTimeMillis = SystemClock.elapsedRealtime(); 696 if (setSchemaStatsBuilder != null) { 697 setSchemaStatsBuilder.setGetObserverLatencyMillis(getOldSchemaObserverLatencyMillis 698 + (int) (getNewSchemaObserverEndTimeMillis 699 - getNewSchemaObserverStartTimeMillis)); 700 } 701 702 long preparingChangeNotificationStartTimeMillis = SystemClock.elapsedRealtime(); 703 // Create a unified set of all schema names mentioned in either the old or new schema. 704 Set<String> allSchemaNames = new ArraySet<>(oldSchemaNameToType.keySet()); 705 allSchemaNames.addAll(newSchemaNameToType.keySet()); 706 707 // Perform the diff between the old and new schema. 708 for (String schemaName : allSchemaNames) { 709 final AppSearchSchema contentBefore = oldSchemaNameToType.get(schemaName); 710 final AppSearchSchema contentAfter = newSchemaNameToType.get(schemaName); 711 712 final boolean existBefore = (contentBefore != null); 713 final boolean existAfter = (contentAfter != null); 714 715 // This should never happen 716 if (!existBefore && !existAfter) { 717 continue; 718 } 719 720 boolean contentsChanged = true; 721 if (contentBefore != null 722 && contentBefore.equals(contentAfter)) { 723 contentsChanged = false; 724 } 725 726 Set<String> oldVisibleListeners = 727 oldSchemaNameToVisibleListeningPackage.get(schemaName); 728 Set<String> newVisibleListeners = 729 newSchemaNameToVisibleListeningPackage.get(schemaName); 730 Set<String> allListeningPackages = new ArraySet<>(oldVisibleListeners); 731 if (newVisibleListeners != null) { 732 allListeningPackages.addAll(newVisibleListeners); 733 } 734 735 // Now that we've computed the relationship between the old and new schema, we go 736 // observer by observer and consider the observer's own personal view of the schema. 737 for (String listeningPackageName : allListeningPackages) { 738 // Figure out the visibility 739 final boolean visibleBefore = ( 740 existBefore 741 && oldVisibleListeners != null 742 && oldVisibleListeners.contains(listeningPackageName)); 743 final boolean visibleAfter = ( 744 existAfter 745 && newVisibleListeners != null 746 && newVisibleListeners.contains(listeningPackageName)); 747 748 // Now go through the truth table of all the relevant flags. 749 // visibleBefore and visibleAfter take into account existBefore and existAfter, so 750 // we can stop worrying about existBefore and existAfter. 751 boolean sendNotification = false; 752 if (visibleBefore && visibleAfter && contentsChanged) { 753 sendNotification = true; // Type configuration was modified 754 } else if (!visibleBefore && visibleAfter) { 755 sendNotification = true; // Newly granted visibility or type was created 756 } else if (visibleBefore && !visibleAfter) { 757 sendNotification = true; // Revoked visibility or type was deleted 758 } else { 759 // No visibility before and no visibility after. Nothing to dispatch. 760 } 761 762 if (sendNotification) { 763 mObserverManager.onSchemaChange( 764 /*listeningPackageName=*/listeningPackageName, 765 /*targetPackageName=*/packageName, 766 /*databaseName=*/databaseName, 767 /*schemaName=*/schemaName); 768 } 769 } 770 } 771 if (setSchemaStatsBuilder != null) { 772 setSchemaStatsBuilder.setPreparingChangeNotificationLatencyMillis( 773 (int) (SystemClock.elapsedRealtime() 774 - preparingChangeNotificationStartTimeMillis)); 775 } 776 777 return internalSetSchemaResponse; 778 } 779 780 /** 781 * Updates the AppSearch schema for this app, without dispatching change notifications. 782 * 783 * <p>This method can be used only when no one is observing {@code packageName}. 784 * 785 * @see #setSchema 786 * @see #doSetSchemaWithChangeNotificationLocked 787 */ 788 @GuardedBy("mReadWriteLock") doSetSchemaNoChangeNotificationLocked( @onNull String packageName, @NonNull String databaseName, @NonNull List<AppSearchSchema> schemas, @NonNull List<InternalVisibilityConfig> visibilityConfigs, boolean forceOverride, int version, SetSchemaStats.@Nullable Builder setSchemaStatsBuilder)789 private @NonNull InternalSetSchemaResponse doSetSchemaNoChangeNotificationLocked( 790 @NonNull String packageName, 791 @NonNull String databaseName, 792 @NonNull List<AppSearchSchema> schemas, 793 @NonNull List<InternalVisibilityConfig> visibilityConfigs, 794 boolean forceOverride, 795 int version, 796 SetSchemaStats.@Nullable Builder setSchemaStatsBuilder) throws AppSearchException { 797 long setRewriteSchemaLatencyStartTimeMillis = SystemClock.elapsedRealtime(); 798 SchemaProto.Builder existingSchemaBuilder = getSchemaProtoLocked().toBuilder(); 799 800 SchemaProto.Builder newSchemaBuilder = SchemaProto.newBuilder(); 801 for (int i = 0; i < schemas.size(); i++) { 802 AppSearchSchema schema = schemas.get(i); 803 SchemaTypeConfigProto schemaTypeProto = 804 SchemaToProtoConverter.toSchemaTypeConfigProto(schema, version); 805 newSchemaBuilder.addTypes(schemaTypeProto); 806 } 807 808 String prefix = createPrefix(packageName, databaseName); 809 // Combine the existing schema (which may have types from other prefixes) with this 810 // prefix's new schema. Modifies the existingSchemaBuilder. 811 RewrittenSchemaResults rewrittenSchemaResults = rewriteSchema(prefix, 812 existingSchemaBuilder, 813 newSchemaBuilder.build()); 814 815 long rewriteSchemaEndTimeMillis = SystemClock.elapsedRealtime(); 816 if (setSchemaStatsBuilder != null) { 817 setSchemaStatsBuilder.setRewriteSchemaLatencyMillis( 818 (int) (rewriteSchemaEndTimeMillis - setRewriteSchemaLatencyStartTimeMillis)); 819 } 820 821 // Apply schema 822 long nativeLatencyStartTimeMillis = SystemClock.elapsedRealtime(); 823 SchemaProto finalSchema = existingSchemaBuilder.build(); 824 LogUtil.piiTrace(TAG, "setSchema, request", finalSchema.getTypesCount(), finalSchema); 825 SetSchemaResultProto setSchemaResultProto = 826 mIcingSearchEngineLocked.setSchema(finalSchema, forceOverride); 827 LogUtil.piiTrace( 828 TAG, "setSchema, response", setSchemaResultProto.getStatus(), setSchemaResultProto); 829 long nativeLatencyEndTimeMillis = SystemClock.elapsedRealtime(); 830 if (setSchemaStatsBuilder != null) { 831 setSchemaStatsBuilder 832 .setTotalNativeLatencyMillis( 833 (int) (nativeLatencyEndTimeMillis - nativeLatencyStartTimeMillis)) 834 .setStatusCode(statusProtoToResultCode( 835 setSchemaResultProto.getStatus())); 836 AppSearchLoggerHelper.copyNativeStats(setSchemaResultProto, 837 setSchemaStatsBuilder); 838 } 839 840 boolean isFailedPrecondition = setSchemaResultProto.getStatus().getCode() 841 == StatusProto.Code.FAILED_PRECONDITION; 842 // Determine whether it succeeded. 843 try { 844 checkSuccess(setSchemaResultProto.getStatus()); 845 } catch (AppSearchException e) { 846 // Swallow the exception for the incompatible change case. We will generate a failed 847 // InternalSetSchemaResponse for this case. 848 int deletedTypes = setSchemaResultProto.getDeletedSchemaTypesCount(); 849 int incompatibleTypes = setSchemaResultProto.getIncompatibleSchemaTypesCount(); 850 boolean isIncompatible = deletedTypes > 0 || incompatibleTypes > 0; 851 if (isFailedPrecondition && !forceOverride && isIncompatible) { 852 SetSchemaResponse setSchemaResponse = SetSchemaResponseToProtoConverter 853 .toSetSchemaResponse(setSchemaResultProto, prefix); 854 String errorMessage = "Schema is incompatible." 855 + "\n Deleted types: " + setSchemaResponse.getDeletedTypes() 856 + "\n Incompatible types: " + setSchemaResponse.getIncompatibleTypes(); 857 return newFailedSetSchemaResponse(setSchemaResponse, errorMessage); 858 } else { 859 throw e; 860 } 861 } 862 863 long saveVisibilitySettingStartTimeMillis = SystemClock.elapsedRealtime(); 864 // Update derived data structures. 865 for (SchemaTypeConfigProto schemaTypeConfigProto : 866 rewrittenSchemaResults.mRewrittenPrefixedTypes.values()) { 867 mSchemaCacheLocked.addToSchemaMap(prefix, schemaTypeConfigProto); 868 } 869 870 for (String schemaType : rewrittenSchemaResults.mDeletedPrefixedTypes) { 871 mSchemaCacheLocked.removeFromSchemaMap(prefix, schemaType); 872 } 873 874 mSchemaCacheLocked.rebuildCacheForPrefix(prefix); 875 876 // Since the constructor of VisibilityStore will set schema. Avoid call visibility 877 // store before we have already created it. 878 if (mDocumentVisibilityStoreLocked != null) { 879 // Add prefix to all visibility documents. 880 // Find out which Visibility document is deleted or changed to all-default settings. 881 // We need to remove them from Visibility Store. 882 Set<String> deprecatedVisibilityDocuments = 883 new ArraySet<>(rewrittenSchemaResults.mRewrittenPrefixedTypes.keySet()); 884 List<InternalVisibilityConfig> prefixedVisibilityConfigs = rewriteVisibilityConfigs( 885 prefix, visibilityConfigs, deprecatedVisibilityDocuments); 886 // Now deprecatedVisibilityDocuments contains those existing schemas that has 887 // all-default visibility settings, add deleted schemas. That's all we need to 888 // remove. 889 deprecatedVisibilityDocuments.addAll(rewrittenSchemaResults.mDeletedPrefixedTypes); 890 mDocumentVisibilityStoreLocked.removeVisibility(deprecatedVisibilityDocuments); 891 mDocumentVisibilityStoreLocked.setVisibility(prefixedVisibilityConfigs); 892 } 893 long saveVisibilitySettingEndTimeMillis = SystemClock.elapsedRealtime(); 894 if (setSchemaStatsBuilder != null) { 895 setSchemaStatsBuilder.setVisibilitySettingLatencyMillis( 896 (int) (saveVisibilitySettingEndTimeMillis 897 - saveVisibilitySettingStartTimeMillis)); 898 } 899 900 long convertToResponseStartTimeMillis = SystemClock.elapsedRealtime(); 901 InternalSetSchemaResponse setSchemaResponse = newSuccessfulSetSchemaResponse( 902 SetSchemaResponseToProtoConverter 903 .toSetSchemaResponse(setSchemaResultProto, prefix)); 904 long convertToResponseEndTimeMillis = SystemClock.elapsedRealtime(); 905 if (setSchemaStatsBuilder != null) { 906 setSchemaStatsBuilder.setConvertToResponseLatencyMillis( 907 (int) (convertToResponseEndTimeMillis 908 - convertToResponseStartTimeMillis)); 909 } 910 return setSchemaResponse; 911 } 912 913 /** 914 * Retrieves the AppSearch schema for this package name, database. 915 * 916 * <p>This method belongs to query group. 917 * 918 * @param packageName Package that owns the requested {@link AppSearchSchema} instances. 919 * @param databaseName Database that owns the requested {@link AppSearchSchema} instances. 920 * @param callerAccess Visibility access info of the calling app 921 * @throws AppSearchException on IcingSearchEngine error. 922 */ getSchema( @onNull String packageName, @NonNull String databaseName, @NonNull CallerAccess callerAccess)923 public @NonNull GetSchemaResponse getSchema( 924 @NonNull String packageName, 925 @NonNull String databaseName, 926 @NonNull CallerAccess callerAccess) 927 throws AppSearchException { 928 mReadWriteLock.readLock().lock(); 929 try { 930 throwIfClosedLocked(); 931 932 SchemaProto fullSchema = getSchemaProtoLocked(); 933 String prefix = createPrefix(packageName, databaseName); 934 GetSchemaResponse.Builder responseBuilder = new GetSchemaResponse.Builder(); 935 for (int i = 0; i < fullSchema.getTypesCount(); i++) { 936 // Check that this type belongs to the requested app and that the caller has 937 // access to it. 938 SchemaTypeConfigProto typeConfig = fullSchema.getTypes(i); 939 String prefixedSchemaType = typeConfig.getSchemaType(); 940 String typePrefix = getPrefix(prefixedSchemaType); 941 if (!prefix.equals(typePrefix)) { 942 // This schema type doesn't belong to the database we're querying for. 943 continue; 944 } 945 if (!VisibilityUtil.isSchemaSearchableByCaller( 946 callerAccess, 947 packageName, 948 prefixedSchemaType, 949 mDocumentVisibilityStoreLocked, 950 mVisibilityCheckerLocked)) { 951 // Caller doesn't have access to this type. 952 continue; 953 } 954 955 // Rewrite SchemaProto.types.schema_type 956 SchemaTypeConfigProto.Builder typeConfigBuilder = typeConfig.toBuilder(); 957 PrefixUtil.removePrefixesFromSchemaType(typeConfigBuilder); 958 AppSearchSchema schema = SchemaToProtoConverter.toAppSearchSchema( 959 typeConfigBuilder); 960 961 responseBuilder.setVersion(typeConfig.getVersion()); 962 responseBuilder.addSchema(schema); 963 964 // Populate visibility info. Since the constructor of VisibilityStore will get 965 // schema. Avoid call visibility store before we have already created it. 966 if (mDocumentVisibilityStoreLocked != null) { 967 String typeName = typeConfig.getSchemaType().substring(typePrefix.length()); 968 InternalVisibilityConfig visibilityConfig = 969 mDocumentVisibilityStoreLocked.getVisibility(prefixedSchemaType); 970 if (visibilityConfig != null) { 971 if (visibilityConfig.isNotDisplayedBySystem()) { 972 responseBuilder.addSchemaTypeNotDisplayedBySystem(typeName); 973 } 974 List<PackageIdentifier> packageIdentifiers = 975 visibilityConfig.getVisibilityConfig().getAllowedPackages(); 976 if (!packageIdentifiers.isEmpty()) { 977 responseBuilder.setSchemaTypeVisibleToPackages(typeName, 978 new ArraySet<>(packageIdentifiers)); 979 } 980 Set<Set<Integer>> visibleToPermissions = 981 visibilityConfig.getVisibilityConfig().getRequiredPermissions(); 982 if (!visibleToPermissions.isEmpty()) { 983 Set<Set<Integer>> visibleToPermissionsSet = 984 new ArraySet<>(visibleToPermissions.size()); 985 for (Set<Integer> permissionList : visibleToPermissions) { 986 visibleToPermissionsSet.add(new ArraySet<>(permissionList)); 987 } 988 989 responseBuilder.setRequiredPermissionsForSchemaTypeVisibility(typeName, 990 visibleToPermissionsSet); 991 } 992 993 // Check for Visibility properties from the overlay 994 PackageIdentifier publiclyVisibleFromPackage = 995 visibilityConfig.getVisibilityConfig() 996 .getPubliclyVisibleTargetPackage(); 997 if (publiclyVisibleFromPackage != null) { 998 responseBuilder.setPubliclyVisibleSchema( 999 typeName, publiclyVisibleFromPackage); 1000 } 1001 Set<SchemaVisibilityConfig> visibleToConfigs = 1002 visibilityConfig.getVisibleToConfigs(); 1003 if (!visibleToConfigs.isEmpty()) { 1004 responseBuilder.setSchemaTypeVisibleToConfigs( 1005 typeName, visibleToConfigs); 1006 } 1007 } 1008 } 1009 } 1010 return responseBuilder.build(); 1011 1012 } finally { 1013 mReadWriteLock.readLock().unlock(); 1014 } 1015 } 1016 1017 /** 1018 * Retrieves the list of namespaces with at least one document for this package name, database. 1019 * 1020 * <p>This method belongs to query group. 1021 * 1022 * @param packageName Package name that owns this schema 1023 * @param databaseName The name of the database where this schema lives. 1024 * @throws AppSearchException on IcingSearchEngine error. 1025 */ getNamespaces( @onNull String packageName, @NonNull String databaseName)1026 public @NonNull List<String> getNamespaces( 1027 @NonNull String packageName, @NonNull String databaseName) throws AppSearchException { 1028 mReadWriteLock.readLock().lock(); 1029 try { 1030 throwIfClosedLocked(); 1031 LogUtil.piiTrace(TAG, "getAllNamespaces, request"); 1032 // We can't just use mNamespaceMap here because we have no way to prune namespaces from 1033 // mNamespaceMap when they have no more documents (e.g. after setting schema to empty or 1034 // using deleteByQuery). 1035 GetAllNamespacesResultProto getAllNamespacesResultProto = 1036 mIcingSearchEngineLocked.getAllNamespaces(); 1037 LogUtil.piiTrace( 1038 TAG, 1039 "getAllNamespaces, response", 1040 getAllNamespacesResultProto.getNamespacesCount(), 1041 getAllNamespacesResultProto); 1042 checkSuccess(getAllNamespacesResultProto.getStatus()); 1043 String prefix = createPrefix(packageName, databaseName); 1044 List<String> results = new ArrayList<>(); 1045 for (int i = 0; i < getAllNamespacesResultProto.getNamespacesCount(); i++) { 1046 String prefixedNamespace = getAllNamespacesResultProto.getNamespaces(i); 1047 if (prefixedNamespace.startsWith(prefix)) { 1048 results.add(prefixedNamespace.substring(prefix.length())); 1049 } 1050 } 1051 return results; 1052 } finally { 1053 mReadWriteLock.readLock().unlock(); 1054 } 1055 } 1056 1057 1058 /** 1059 * Adds a list of documents to the AppSearch index. 1060 * 1061 * <p>This method belongs to mutate group. 1062 * 1063 * @param packageName The package name that owns this document. 1064 * @param databaseName The databaseName this document resides in. 1065 * @param documents A list of documents to index. 1066 * @param batchResultBuilder The builder for returning the batch result. 1067 * @param sendChangeNotifications Whether to dispatch 1068 * {@link androidx.appsearch.observer.DocumentChangeInfo} 1069 * messages to observers for this change. 1070 * @param persistType The persist type used to call PersistToDisk inside Icing at 1071 * the end of the Put request. If UNKNOWN, PersistToDisk will not 1072 * be called. See also {@link #persistToDisk(PersistType.Code)}. 1073 * @throws AppSearchException on IcingSearchEngine error. 1074 */ batchPutDocuments( @onNull String packageName, @NonNull String databaseName, @NonNull List<GenericDocument> documents, AppSearchBatchResult.@Nullable Builder<String, Void> batchResultBuilder, boolean sendChangeNotifications, @Nullable AppSearchLogger logger, PersistType.@NonNull Code persistType)1075 public void batchPutDocuments( 1076 @NonNull String packageName, 1077 @NonNull String databaseName, 1078 @NonNull List<GenericDocument> documents, 1079 AppSearchBatchResult.@Nullable Builder<String, Void> batchResultBuilder, 1080 boolean sendChangeNotifications, 1081 @Nullable AppSearchLogger logger, 1082 PersistType.@NonNull Code persistType) throws AppSearchException { 1083 // All the stats we want to print. This may not be necessary, 1084 // but just to keep the behavior same as before. 1085 // Use list instead of map as same id can appear more than once. 1086 List<PutDocumentStats.Builder> allStatsList = new ArrayList<>(); 1087 List<PutDocumentStats.Builder> statsNotFilteredOut = new ArrayList<>(); 1088 long totalStartTimeMillis = SystemClock.elapsedRealtime(); 1089 1090 mReadWriteLock.writeLock().lock(); 1091 try { 1092 throwIfClosedLocked(); 1093 1094 String prefix = createPrefix(packageName, databaseName); 1095 List<PutDocumentRequest.Builder> requestBuilderList = new ArrayList<>(); 1096 // This is to make sure the batching size is at least getMaxDocumentSizeBytes. 1097 // Otherwise one valid size doc may not fit into a batch. 1098 int maxBufferedBytes = Integer.max(mConfig.getMaxByteLimitForBatchPut(), 1099 mConfig.getMaxDocumentSizeBytes()); 1100 int currentTotalBytes = 0; 1101 PutDocumentRequest.Builder currentBatchBuilder = 1102 PutDocumentRequest.newBuilder().setPersistType(PersistType.Code.UNKNOWN); 1103 for (int i = 0; i < documents.size(); ++i) { 1104 String docId = documents.get(i).getId(); 1105 PutDocumentStats.Builder pStatsBuilder = 1106 new PutDocumentStats.Builder(packageName, databaseName) 1107 .setLaunchVMEnabled(mIsVMEnabled); 1108 // Previously we always log even if we reach the limit. To keep the behavior 1109 // same as before, we will save all the stats created. 1110 allStatsList.add(pStatsBuilder); 1111 1112 long generateDocumentProtoStartTimeMillis = 0; 1113 long generateDocumentProtoEndTimeMillis = 0; 1114 long rewriteDocumentTypeStartTimeMillis = 0; 1115 long rewriteDocumentTypeEndTimeMillis = 0; 1116 try { 1117 // Generate Document Proto 1118 generateDocumentProtoStartTimeMillis = SystemClock.elapsedRealtime(); 1119 DocumentProto.Builder documentBuilder = 1120 GenericDocumentToProtoConverter.toDocumentProto(documents.get(i)) 1121 .toBuilder(); 1122 generateDocumentProtoEndTimeMillis = SystemClock.elapsedRealtime(); 1123 1124 // Rewrite Document Type 1125 rewriteDocumentTypeStartTimeMillis = SystemClock.elapsedRealtime(); 1126 addPrefixToDocument(documentBuilder, prefix); 1127 rewriteDocumentTypeEndTimeMillis = SystemClock.elapsedRealtime(); 1128 DocumentProto finalDocument = documentBuilder.build(); 1129 1130 // Check limits 1131 int serializedSizeBytes = finalDocument.getSerializedSize(); 1132 enforceLimitConfigLocked(packageName, docId, serializedSizeBytes); 1133 1134 // to see if we want to finish the current batch and build a PutRequestProto. 1135 // based on how we calculate maxBufferedBytes, serializedSizeBytes is guaranteed 1136 // to be smaller or same as maxBufferedBytes. 1137 if (serializedSizeBytes > maxBufferedBytes - currentTotalBytes) { 1138 // Time to finish the current batch. 1139 requestBuilderList.add(currentBatchBuilder); 1140 1141 // reset everything for next batch 1142 currentBatchBuilder = 1143 PutDocumentRequest.newBuilder().setPersistType( 1144 PersistType.Code.UNKNOWN); 1145 currentTotalBytes = 0; 1146 } 1147 1148 currentTotalBytes += serializedSizeBytes; 1149 currentBatchBuilder.addDocuments(finalDocument); 1150 statsNotFilteredOut.add(pStatsBuilder); 1151 1152 } catch (Throwable t) { 1153 if (batchResultBuilder != null) { 1154 batchResultBuilder.setResult(docId, throwableToFailedResult(t)); 1155 } 1156 } finally { 1157 // 1158 // At this point, the doc has either been put in the requestProto, or error 1159 // result be put in batchResultBuilder. 1160 // 1161 1162 pStatsBuilder 1163 .setGenerateDocumentProtoLatencyMillis( 1164 (int) 1165 (generateDocumentProtoEndTimeMillis 1166 - generateDocumentProtoStartTimeMillis)) 1167 .setRewriteDocumentTypesLatencyMillis( 1168 (int) 1169 (rewriteDocumentTypeEndTimeMillis 1170 - rewriteDocumentTypeStartTimeMillis)); 1171 } 1172 } 1173 1174 // We have to "flush" the last batch. Since this is the last batch, we set the 1175 // persistType passed in here. 1176 requestBuilderList.add(currentBatchBuilder.setPersistType(persistType)); 1177 1178 // Put documents 1179 int statsIndex = 0; 1180 for (int requestIndex = 0; requestIndex < requestBuilderList.size(); ++requestIndex) { 1181 PutDocumentRequest requestProto = requestBuilderList.get(requestIndex).build(); 1182 LogUtil.piiTrace( 1183 TAG, 1184 "batchPutDocument, request", 1185 requestProto.getDocumentsCount(), 1186 requestProto); 1187 BatchPutResultProto batchPutResultProto = 1188 mIcingSearchEngineLocked.batchPut(requestProto); 1189 // TODO(b/394875109) We can provide a better debug information for fast trace here. 1190 LogUtil.piiTrace( 1191 TAG, "batchPutDocument", 1192 /* fastTraceObj= */ null, 1193 batchPutResultProto); 1194 1195 List<PutResultProto> putResultProtoList = 1196 batchPutResultProto.getPutResultProtosList(); 1197 for (int i = 0; i < putResultProtoList.size(); ++i, ++statsIndex) { 1198 PutResultProto putResultProto = putResultProtoList.get(i); 1199 String docId = putResultProto.getUri(); 1200 try { 1201 if (statsIndex <= statsNotFilteredOut.size()) { 1202 PutDocumentStats.Builder pStatsBuilder = 1203 statsNotFilteredOut.get(statsIndex); 1204 pStatsBuilder.setStatusCode( 1205 statusProtoToResultCode(putResultProto.getStatus())); 1206 AppSearchLoggerHelper.copyNativeStats( 1207 putResultProto.getPutDocumentStats(), pStatsBuilder); 1208 } else { 1209 // since it is just stats, we just log the debug message if 1210 // something goes wrong. 1211 LogUtil.piiTrace(TAG, "batchPutDocument", 1212 "index out of boundary for stats", 1213 statsNotFilteredOut); 1214 } 1215 1216 // If it is a failure, it will throw and the catch section will 1217 // set generated result 1218 checkSuccess(putResultProto.getStatus()); 1219 if (batchResultBuilder != null) { 1220 batchResultBuilder.setSuccess(docId, /* value= */ null); 1221 } 1222 1223 // Don't need to check the index here, as request doc list size should 1224 // definitely be bigger than response doc list size. 1225 DocumentProto documentProto = requestProto.getDocuments(i); 1226 if (!docId.equals(documentProto.getUri())) { 1227 // This shouldn't happen if native code implemented correctly. 1228 // Have a check here just in case something unexpected happens. 1229 Log.w(TAG, "id mismatch between request and response for batchPut"); 1230 continue; 1231 } 1232 1233 // Only update caches if the document is successfully put to Icing. 1234 // Prefixed namespace needed here. 1235 mNamespaceCacheLocked.addToDocumentNamespaceMap( 1236 prefix, documentProto.getNamespace()); 1237 if (!Flags.enableDocumentLimiterReplaceTracking() 1238 || !putResultProto.getWasReplacement()) { 1239 // If the document was a replacement, then there is no need to report it 1240 // because the number of documents has not changed. We only need to 1241 // report "true" additions to the DocumentLimiter. 1242 // Although a replacement document will consume a document id, 1243 // the limit is only intended to apply to "living" documents. 1244 // It is the responsibility of AppSearch's optimization task to reclaim 1245 // space when needed. 1246 mDocumentLimiterLocked.reportDocumentAdded( 1247 packageName, 1248 () -> 1249 getRawStorageInfoProto() 1250 .getDocumentStorageInfo() 1251 .getNamespaceStorageInfoList()); 1252 } 1253 // Prepare notifications 1254 if (sendChangeNotifications) { 1255 mObserverManager.onDocumentChange( 1256 packageName, 1257 databaseName, 1258 PrefixUtil.removePrefix(documentProto.getNamespace()), 1259 PrefixUtil.removePrefix(documentProto.getSchema()), 1260 documentProto.getUri(), 1261 mDocumentVisibilityStoreLocked, 1262 mVisibilityCheckerLocked); 1263 } 1264 } catch (Throwable t) { 1265 if (batchResultBuilder != null) { 1266 batchResultBuilder.setResult(docId, throwableToFailedResult(t)); 1267 } 1268 } 1269 } 1270 // As we only set "not unknown" persistType for the last request, 1271 // this should ONLY be checked for last request. 1272 if (requestProto.getPersistType() != PersistType.Code.UNKNOWN) { 1273 checkSuccess(batchPutResultProto.getPersistToDiskResultProto().getStatus()); 1274 } 1275 } 1276 } finally { 1277 mReadWriteLock.writeLock().unlock(); 1278 1279 if (logger != null && !allStatsList.isEmpty()) { 1280 // This seems broken and no easy way to get accurate number. 1281 int avgTotalLatencyMs = 1282 (int) ((SystemClock.elapsedRealtime() - totalStartTimeMillis) 1283 / allStatsList.size()); 1284 for (int i = 0; i < allStatsList.size(); ++i) { 1285 PutDocumentStats.Builder pStatsBuilder = allStatsList.get(i); 1286 pStatsBuilder.setTotalLatencyMillis(avgTotalLatencyMs); 1287 logger.logStats(pStatsBuilder.build()); 1288 } 1289 } 1290 } 1291 } 1292 1293 /** 1294 * Adds a document to the AppSearch index. 1295 * 1296 * <p>This method belongs to mutate group. 1297 * 1298 * @param packageName The package name that owns this document. 1299 * @param databaseName The databaseName this document resides in. 1300 * @param document The document to index. 1301 * @param sendChangeNotifications Whether to dispatch 1302 * {@link androidx.appsearch.observer.DocumentChangeInfo} 1303 * messages to observers for this change. 1304 * @throws AppSearchException on IcingSearchEngine error. 1305 * 1306 * @deprecated use {@link #batchPutDocuments(String, String, List, 1307 * AppSearchBatchResult.Builder, boolean, AppSearchLogger)} 1308 */ 1309 // TODO(b/394875109) keep this for now to make code sync easier. 1310 @Deprecated putDocument( @onNull String packageName, @NonNull String databaseName, @NonNull GenericDocument document, boolean sendChangeNotifications, @Nullable AppSearchLogger logger)1311 public void putDocument( 1312 @NonNull String packageName, 1313 @NonNull String databaseName, 1314 @NonNull GenericDocument document, 1315 boolean sendChangeNotifications, 1316 @Nullable AppSearchLogger logger) 1317 throws AppSearchException { 1318 PutDocumentStats.Builder pStatsBuilder = null; 1319 if (logger != null) { 1320 pStatsBuilder = new PutDocumentStats.Builder(packageName, databaseName) 1321 .setLaunchVMEnabled(mIsVMEnabled); 1322 } 1323 long totalStartTimeMillis = SystemClock.elapsedRealtime(); 1324 1325 mReadWriteLock.writeLock().lock(); 1326 try { 1327 throwIfClosedLocked(); 1328 1329 // Generate Document Proto 1330 long generateDocumentProtoStartTimeMillis = SystemClock.elapsedRealtime(); 1331 DocumentProto.Builder documentBuilder = GenericDocumentToProtoConverter.toDocumentProto( 1332 document).toBuilder(); 1333 long generateDocumentProtoEndTimeMillis = SystemClock.elapsedRealtime(); 1334 1335 // Rewrite Document Type 1336 long rewriteDocumentTypeStartTimeMillis = SystemClock.elapsedRealtime(); 1337 String prefix = createPrefix(packageName, databaseName); 1338 addPrefixToDocument(documentBuilder, prefix); 1339 long rewriteDocumentTypeEndTimeMillis = SystemClock.elapsedRealtime(); 1340 DocumentProto finalDocument = documentBuilder.build(); 1341 1342 // Check limits 1343 enforceLimitConfigLocked( 1344 packageName, finalDocument.getUri(), finalDocument.getSerializedSize()); 1345 1346 // Insert document 1347 LogUtil.piiTrace(TAG, "putDocument, request", finalDocument.getUri(), finalDocument); 1348 PutResultProto putResultProto = mIcingSearchEngineLocked.put(finalDocument); 1349 LogUtil.piiTrace( 1350 TAG, "putDocument, response", putResultProto.getStatus(), putResultProto); 1351 1352 // Logging stats 1353 if (pStatsBuilder != null) { 1354 pStatsBuilder 1355 .setStatusCode(statusProtoToResultCode(putResultProto.getStatus())) 1356 .setGenerateDocumentProtoLatencyMillis( 1357 (int) (generateDocumentProtoEndTimeMillis 1358 - generateDocumentProtoStartTimeMillis)) 1359 .setRewriteDocumentTypesLatencyMillis( 1360 (int) (rewriteDocumentTypeEndTimeMillis 1361 - rewriteDocumentTypeStartTimeMillis)); 1362 AppSearchLoggerHelper.copyNativeStats(putResultProto.getPutDocumentStats(), 1363 pStatsBuilder); 1364 } 1365 1366 checkSuccess(putResultProto.getStatus()); 1367 1368 // Only update caches if the document is successfully put to Icing. 1369 1370 mNamespaceCacheLocked.addToDocumentNamespaceMap(prefix, finalDocument.getNamespace()); 1371 if (!Flags.enableDocumentLimiterReplaceTracking() 1372 || !putResultProto.getWasReplacement()) { 1373 // If the document was a replacement, then there is no need to report it because the 1374 // number of documents has not changed. We only need to report "true" additions to 1375 // the DocumentLimiter. 1376 // Although a replacement document will consume a document id, the limit is only 1377 // intended to apply to "living" documents. It is the responsibility of AppSearch's 1378 // optimization task to reclaim space when needed. 1379 mDocumentLimiterLocked.reportDocumentAdded( 1380 packageName, 1381 () -> getRawStorageInfoProto().getDocumentStorageInfo() 1382 .getNamespaceStorageInfoList()); 1383 } 1384 1385 // Prepare notifications 1386 if (sendChangeNotifications) { 1387 mObserverManager.onDocumentChange( 1388 packageName, 1389 databaseName, 1390 document.getNamespace(), 1391 document.getSchemaType(), 1392 document.getId(), 1393 mDocumentVisibilityStoreLocked, 1394 mVisibilityCheckerLocked); 1395 } 1396 } finally { 1397 mReadWriteLock.writeLock().unlock(); 1398 1399 if (pStatsBuilder != null && logger != null) { 1400 long totalEndTimeMillis = SystemClock.elapsedRealtime(); 1401 pStatsBuilder.setTotalLatencyMillis( 1402 (int) (totalEndTimeMillis - totalStartTimeMillis)); 1403 logger.logStats(pStatsBuilder.build()); 1404 } 1405 } 1406 } 1407 1408 /** 1409 * Gets the {@link ParcelFileDescriptor} for write purpose of the given 1410 * {@link AppSearchBlobHandle}. 1411 * 1412 * <p> Only one opened {@link ParcelFileDescriptor} is allowed for each 1413 * {@link AppSearchBlobHandle}. The same {@link ParcelFileDescriptor} will be returned if it is 1414 * not closed by caller. 1415 * 1416 * @param packageName The package name that owns this blob. 1417 * @param databaseName The databaseName this blob resides in. 1418 * @param handle The {@link AppSearchBlobHandle} represent the blob. 1419 */ 1420 @ExperimentalAppSearchApi openWriteBlob( @onNull String packageName, @NonNull String databaseName, @NonNull AppSearchBlobHandle handle)1421 public @NonNull ParcelFileDescriptor openWriteBlob( 1422 @NonNull String packageName, 1423 @NonNull String databaseName, 1424 @NonNull AppSearchBlobHandle handle) 1425 throws AppSearchException, IOException { 1426 if (mRevocableFileDescriptorStore == null) { 1427 throw new UnsupportedOperationException( 1428 "BLOB_STORAGE is not available on this AppSearch implementation."); 1429 } 1430 mReadWriteLock.writeLock().lock(); 1431 try { 1432 throwIfClosedLocked(); 1433 verifyCallingBlobHandle(packageName, databaseName, handle); 1434 ParcelFileDescriptor pfd = mRevocableFileDescriptorStore 1435 .getOpenedRevocableFileDescriptorForWrite(packageName, handle); 1436 if (pfd != null) { 1437 // There is already an opened pfd for write with same blob handle, just return the 1438 // already opened one. 1439 return pfd; 1440 } 1441 mRevocableFileDescriptorStore.checkBlobStoreLimit(packageName); 1442 PropertyProto.BlobHandleProto blobHandleProto = 1443 BlobHandleToProtoConverter.toBlobHandleProto(handle); 1444 BlobProto result = mIcingSearchEngineLocked.openWriteBlob(blobHandleProto); 1445 pfd = retrieveFileDescriptorLocked(result, 1446 ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_READ_WRITE); 1447 mNamespaceCacheLocked.addToBlobNamespaceMap(createPrefix(packageName, databaseName), 1448 blobHandleProto.getNamespace()); 1449 1450 return mRevocableFileDescriptorStore.wrapToRevocableFileDescriptor( 1451 packageName, handle, pfd, ParcelFileDescriptor.MODE_READ_WRITE); 1452 } finally { 1453 mReadWriteLock.writeLock().unlock(); 1454 } 1455 } 1456 1457 /** 1458 * Remove and delete the blob file of given {@link AppSearchBlobHandle} from AppSearch 1459 * storage. 1460 * 1461 * <p> This method will delete pending blob or committed blobs. Remove blobs that have reference 1462 * documents linked to it will make those reference document has nothing to read. 1463 * 1464 * @param packageName The package name that owns this blob. 1465 * @param databaseName The databaseName this blob resides in. 1466 * @param handle The {@link AppSearchBlobHandle} represent the blob. 1467 */ 1468 @ExperimentalAppSearchApi removeBlob( @onNull String packageName, @NonNull String databaseName, @NonNull AppSearchBlobHandle handle)1469 public void removeBlob( 1470 @NonNull String packageName, 1471 @NonNull String databaseName, 1472 @NonNull AppSearchBlobHandle handle) 1473 throws AppSearchException, IOException { 1474 if (mRevocableFileDescriptorStore == null) { 1475 throw new UnsupportedOperationException( 1476 "BLOB_STORAGE is not available on this AppSearch implementation."); 1477 } 1478 mReadWriteLock.writeLock().lock(); 1479 try { 1480 throwIfClosedLocked(); 1481 verifyCallingBlobHandle(packageName, databaseName, handle); 1482 1483 BlobProto result = mIcingSearchEngineLocked.removeBlob( 1484 BlobHandleToProtoConverter.toBlobHandleProto(handle)); 1485 1486 checkSuccess(result.getStatus()); 1487 if (Flags.enableAppSearchManageBlobFiles()) { 1488 File blobFileToRemove = new File(mBlobFilesDir, result.getFileName()); 1489 if (!blobFileToRemove.delete()) { 1490 throw new AppSearchException(AppSearchResult.RESULT_IO_ERROR, 1491 "Cannot delete the blob file: " + blobFileToRemove.getName()); 1492 } 1493 } 1494 mRevocableFileDescriptorStore.revokeFdForWrite(packageName, handle); 1495 } finally { 1496 mReadWriteLock.writeLock().unlock(); 1497 } 1498 } 1499 1500 /** 1501 * Verifies the integrity of a blob file by comparing its SHA-256 digest with the expected 1502 * digest. 1503 * 1504 * <p>This method is used when AppSearch manages blob files directly. It opens the blob file 1505 * associated with the given {@link AppSearchBlobHandle}, calculates its SHA-256 digest, and 1506 * compares it with the digest provided in the handle. If the file does not exist or the 1507 * calculated digest does not match the expected digest, the blob is considered invalid and is 1508 * removed. 1509 * 1510 * @param handle The {@link AppSearchBlobHandle} representing the blob to verify. 1511 * @throws AppSearchException if the blob file does not exist, the calculated digest does not 1512 * match the expected digest, or if there is an error removing the 1513 * invalid blob. 1514 * @throws IOException if there is an error opening or reading the blob file. 1515 */ 1516 @GuardedBy("mReadWriteLock") 1517 @OptIn(markerClass = ExperimentalAppSearchApi.class) verifyBlobIntegrityLocked(@onNull AppSearchBlobHandle handle)1518 private void verifyBlobIntegrityLocked(@NonNull AppSearchBlobHandle handle) 1519 throws AppSearchException, IOException { 1520 // Since the blob has not yet been committed, we open the blob for *write* again to 1521 // get the file name. 1522 BlobProto result = mIcingSearchEngineLocked.openWriteBlob( 1523 BlobHandleToProtoConverter.toBlobHandleProto(handle)); 1524 checkSuccess(result.getStatus()); 1525 File blobFile = new File(mBlobFilesDir, result.getFileName()); 1526 boolean fileExists = blobFile.exists(); 1527 boolean digestMatches = false; 1528 1529 if (fileExists) { 1530 // Read the file to check the digest. 1531 byte[] digest; 1532 ParcelFileDescriptor pfd = ParcelFileDescriptor.open(blobFile, 1533 ParcelFileDescriptor.MODE_READ_ONLY); 1534 try (InputStream inputStream = 1535 new ParcelFileDescriptor.AutoCloseInputStream(pfd); 1536 DigestInputStream digestInputStream = 1537 new DigestInputStream(inputStream, MessageDigest.getInstance("SHA-256"))) { 1538 byte[] buffer = new byte[8192]; 1539 while (digestInputStream.read(buffer) != -1) { 1540 // pass 1541 } 1542 digest = digestInputStream.getMessageDigest().digest(); 1543 } catch (NoSuchAlgorithmException e) { 1544 throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR, 1545 "Failed to get MessageDigest for SHA-256.", e); 1546 } 1547 digestMatches = Arrays.equals(digest, handle.getSha256Digest()); 1548 } 1549 1550 // If the file does not exist or the digest is wrong, delete the blob and throw 1551 // an exception. 1552 if (!fileExists || !digestMatches) { 1553 BlobProto removeResult = mIcingSearchEngineLocked.removeBlob( 1554 BlobHandleToProtoConverter.toBlobHandleProto(handle)); 1555 checkSuccess(removeResult.getStatus()); 1556 1557 if (!fileExists) { 1558 throw new AppSearchException(AppSearchResult.RESULT_NOT_FOUND, 1559 "Cannot find the blob for handle: " + handle); 1560 } else { 1561 File blobFileToRemove = new File(mBlobFilesDir, removeResult.getFileName()); 1562 if (!blobFileToRemove.delete()) { 1563 throw new AppSearchException(AppSearchResult.RESULT_IO_ERROR, 1564 "Cannot delete the blob file: " + blobFileToRemove.getName()); 1565 } 1566 throw new AppSearchException(AppSearchResult.RESULT_INVALID_ARGUMENT, 1567 "The blob content doesn't match to the digest."); 1568 } 1569 } 1570 } 1571 1572 /** 1573 * Commits and seals the blob represented by the given {@link AppSearchBlobHandle}. 1574 * 1575 * <p>After this call, the blob is readable via {@link #openReadBlob}. And any rewrite is not 1576 * allowed. 1577 * 1578 * @param packageName The package name that owns this blob. 1579 * @param databaseName The databaseName this blob resides in. 1580 * @param handle The {@link AppSearchBlobHandle} represent the blob. 1581 */ 1582 @ExperimentalAppSearchApi commitBlob( @onNull String packageName, @NonNull String databaseName, @NonNull AppSearchBlobHandle handle)1583 public void commitBlob( 1584 @NonNull String packageName, 1585 @NonNull String databaseName, 1586 @NonNull AppSearchBlobHandle handle) 1587 throws AppSearchException, IOException { 1588 if (mRevocableFileDescriptorStore == null) { 1589 throw new UnsupportedOperationException( 1590 "BLOB_STORAGE is not available on this AppSearch implementation."); 1591 } 1592 mReadWriteLock.writeLock().lock(); 1593 try { 1594 throwIfClosedLocked(); 1595 verifyCallingBlobHandle(packageName, databaseName, handle); 1596 1597 // If AppSearch manages blob files, it is responsible for verifying the digest of the 1598 // blob file. 1599 if (Flags.enableAppSearchManageBlobFiles()) { 1600 verifyBlobIntegrityLocked(handle); 1601 } 1602 1603 BlobProto result = mIcingSearchEngineLocked.commitBlob( 1604 BlobHandleToProtoConverter.toBlobHandleProto(handle)); 1605 1606 checkSuccess(result.getStatus()); 1607 // The blob is committed and sealed, revoke the sent pfd for writing. 1608 mRevocableFileDescriptorStore.revokeFdForWrite(packageName, handle); 1609 } finally { 1610 mReadWriteLock.writeLock().unlock(); 1611 } 1612 } 1613 1614 /** 1615 * Gets the {@link ParcelFileDescriptor} for read only purpose of the given 1616 * {@link AppSearchBlobHandle}. 1617 * 1618 * <p>The target must be committed via {@link #commitBlob}; 1619 * 1620 * @param packageName The package name that owns this blob. 1621 * @param databaseName The databaseName this blob resides in. 1622 * @param handle The {@link AppSearchBlobHandle} represent the blob. 1623 */ 1624 @ExperimentalAppSearchApi openReadBlob( @onNull String packageName, @NonNull String databaseName, @NonNull AppSearchBlobHandle handle)1625 public @NonNull ParcelFileDescriptor openReadBlob( 1626 @NonNull String packageName, 1627 @NonNull String databaseName, 1628 @NonNull AppSearchBlobHandle handle) 1629 throws AppSearchException, IOException { 1630 if (mRevocableFileDescriptorStore == null) { 1631 throw new UnsupportedOperationException( 1632 "BLOB_STORAGE is not available on this AppSearch implementation."); 1633 } 1634 1635 mReadWriteLock.readLock().lock(); 1636 try { 1637 throwIfClosedLocked(); 1638 verifyCallingBlobHandle(packageName, databaseName, handle); 1639 mRevocableFileDescriptorStore.checkBlobStoreLimit(packageName); 1640 BlobProto result = mIcingSearchEngineLocked.openReadBlob( 1641 BlobHandleToProtoConverter.toBlobHandleProto(handle)); 1642 ParcelFileDescriptor pfd = retrieveFileDescriptorLocked(result, 1643 ParcelFileDescriptor.MODE_READ_ONLY); 1644 1645 // We do NOT need to look up the revocable file descriptor for read, skip passing the 1646 // blob handle key. 1647 return mRevocableFileDescriptorStore.wrapToRevocableFileDescriptor( 1648 packageName, /*blobHandle=*/null, pfd, ParcelFileDescriptor.MODE_READ_ONLY); 1649 } finally { 1650 mReadWriteLock.readLock().unlock(); 1651 } 1652 } 1653 1654 /** 1655 * Gets the {@link ParcelFileDescriptor} for read only purpose of the given 1656 * {@link AppSearchBlobHandle}. 1657 * 1658 * <p>The target must be committed via {@link #commitBlob}; 1659 * 1660 * @param handle The {@link AppSearchBlobHandle} represent the blob. 1661 */ 1662 @ExperimentalAppSearchApi globalOpenReadBlob(@onNull AppSearchBlobHandle handle, @NonNull CallerAccess access)1663 public @NonNull ParcelFileDescriptor globalOpenReadBlob(@NonNull AppSearchBlobHandle handle, 1664 @NonNull CallerAccess access) 1665 throws AppSearchException, IOException { 1666 if (mRevocableFileDescriptorStore == null) { 1667 throw new UnsupportedOperationException( 1668 "BLOB_STORAGE is not available on this AppSearch implementation."); 1669 } 1670 1671 mReadWriteLock.readLock().lock(); 1672 try { 1673 throwIfClosedLocked(); 1674 mRevocableFileDescriptorStore.checkBlobStoreLimit(access.getCallingPackageName()); 1675 String prefixedNamespace = 1676 createPrefix(handle.getPackageName(), handle.getDatabaseName()) 1677 + handle.getNamespace(); 1678 PropertyProto.BlobHandleProto blobHandleProto = 1679 BlobHandleToProtoConverter.toBlobHandleProto(handle); 1680 // We are using namespace to check blob's visibility. 1681 if (!VisibilityUtil.isSchemaSearchableByCaller( 1682 access, 1683 handle.getPackageName(), 1684 prefixedNamespace, 1685 mBlobVisibilityStoreLocked, 1686 mVisibilityCheckerLocked)) { 1687 // Caller doesn't have access to this namespace. 1688 throw new AppSearchException(AppSearchResult.RESULT_NOT_FOUND, 1689 "Cannot find the blob for handle: " 1690 + blobHandleProto.getDigest().toStringUtf8()); 1691 } 1692 1693 BlobProto result = mIcingSearchEngineLocked.openReadBlob(blobHandleProto); 1694 ParcelFileDescriptor pfd = retrieveFileDescriptorLocked(result, 1695 ParcelFileDescriptor.MODE_READ_ONLY); 1696 1697 // We do NOT need to look up the revocable file descriptor for read, skip passing the 1698 // blob handle key. 1699 return mRevocableFileDescriptorStore.wrapToRevocableFileDescriptor( 1700 access.getCallingPackageName(), 1701 /*blobHandle=*/null, 1702 pfd, 1703 ParcelFileDescriptor.MODE_READ_ONLY); 1704 } finally { 1705 mReadWriteLock.readLock().unlock(); 1706 } 1707 } 1708 1709 /** 1710 * Updates the visibility configuration for a specified namespace within a blob storage. 1711 * 1712 * <p>This method configures the visibility blob namespaces in the given specific database. 1713 * 1714 * <p>After applying the new visibility configurations, the method identifies and removes any 1715 * existing visibility settings that do not included in the new visibility configurations from 1716 * the visibility store. 1717 * 1718 * @param packageName The package name that owns these blobs. 1719 * @param databaseName The databaseName these blobs resides in. 1720 * @param visibilityConfigs a list of {@link InternalVisibilityConfig} objects representing the 1721 * visibility configurations to be set for the specified namespace. 1722 * @throws AppSearchException if an error occurs while updating the visibility configurations. 1723 * This could happen if the database is closed or in an invalid 1724 * state. 1725 */ 1726 @ExperimentalAppSearchApi setBlobNamespaceVisibility( @onNull String packageName, @NonNull String databaseName, @NonNull List<InternalVisibilityConfig> visibilityConfigs)1727 public void setBlobNamespaceVisibility( 1728 @NonNull String packageName, 1729 @NonNull String databaseName, 1730 @NonNull List<InternalVisibilityConfig> visibilityConfigs) throws AppSearchException { 1731 mReadWriteLock.writeLock().lock(); 1732 try { 1733 throwIfClosedLocked(); 1734 if (mBlobVisibilityStoreLocked != null) { 1735 String prefix = PrefixUtil.createPrefix(packageName, databaseName); 1736 Set<String> removedVisibilityConfigs = 1737 mNamespaceCacheLocked.getPrefixedBlobNamespaces(prefix); 1738 if (removedVisibilityConfigs == null) { 1739 removedVisibilityConfigs = new ArraySet<>(); 1740 } else { 1741 // wrap it to allow rewriteVisibilityConfigs modify it. 1742 removedVisibilityConfigs = new ArraySet<>(removedVisibilityConfigs); 1743 } 1744 List<InternalVisibilityConfig> prefixedVisibilityConfigs = rewriteVisibilityConfigs( 1745 prefix, visibilityConfigs, removedVisibilityConfigs); 1746 for (int i = 0; i < prefixedVisibilityConfigs.size(); i++) { 1747 // We are using schema type to represent blob's namespace in 1748 // InternalVisibilityConfig. 1749 mNamespaceCacheLocked.addToBlobNamespaceMap(prefix, 1750 prefixedVisibilityConfigs.get(i).getSchemaType()); 1751 } 1752 // Now removedVisibilityConfigs contains those existing schemas that has 1753 // all-default visibility settings, add deleted schemas. That's all we need to 1754 // remove. 1755 mBlobVisibilityStoreLocked.setVisibility(prefixedVisibilityConfigs); 1756 mBlobVisibilityStoreLocked.removeVisibility(removedVisibilityConfigs); 1757 } else { 1758 throw new UnsupportedOperationException( 1759 "BLOB_STORAGE is not available on this AppSearch implementation."); 1760 } 1761 } finally { 1762 mReadWriteLock.writeLock().unlock(); 1763 } 1764 } 1765 1766 /** 1767 * Retrieves the {@link ParcelFileDescriptor} from a {@link BlobProto}. 1768 * 1769 * <p>This method handles retrieving the actual file descriptor from the provided 1770 * {@link BlobProto}, taking into account whether AppSearch manages blob files directly. 1771 * If AppSearch manages blob files ({@code Flags.enableAppSearchManageBlobFiles()} is true), 1772 * it opens the file using the file name from the {@link BlobProto}. Otherwise, it retrieves 1773 * the file descriptor directly from the {@link BlobProto}. 1774 * 1775 * @return The {@link ParcelFileDescriptor} for the blob. 1776 * @throws AppSearchException if the {@link BlobProto}'s status indicates an error. 1777 * @throws IOException if there is an error opening the file, such as the file not 1778 * being found. 1779 */ 1780 @GuardedBy("mReadWriteLock") retrieveFileDescriptorLocked( BlobProto blobProto, int mode)1781 private ParcelFileDescriptor retrieveFileDescriptorLocked( 1782 BlobProto blobProto, int mode) throws AppSearchException, IOException { 1783 checkSuccess(blobProto.getStatus()); 1784 if (Flags.enableAppSearchManageBlobFiles()) { 1785 File blobFile = new File(mBlobFilesDir, blobProto.getFileName()); 1786 return ParcelFileDescriptor.open(blobFile, mode); 1787 } else { 1788 return ParcelFileDescriptor.adoptFd(blobProto.getFileDescriptor()); 1789 } 1790 } 1791 1792 /** 1793 * Checks that a new document can be added to the given packageName with the given serialized 1794 * size without violating our {@link LimitConfig}. 1795 * 1796 * @throws AppSearchException with a code of {@link AppSearchResult#RESULT_OUT_OF_SPACE} if the 1797 * limits are violated by the new document. 1798 */ 1799 @GuardedBy("mReadWriteLock") enforceLimitConfigLocked(String packageName, String newDocUri, int newDocSize)1800 private void enforceLimitConfigLocked(String packageName, String newDocUri, int newDocSize) 1801 throws AppSearchException { 1802 // Limits check: size of document 1803 if (newDocSize > mConfig.getMaxDocumentSizeBytes()) { 1804 throw new AppSearchException( 1805 AppSearchResult.RESULT_OUT_OF_SPACE, 1806 "Document \"" + newDocUri + "\" for package \"" + packageName 1807 + "\" serialized to " + newDocSize + " bytes, which exceeds " 1808 + "limit of " + mConfig.getMaxDocumentSizeBytes() + " bytes"); 1809 } 1810 1811 mDocumentLimiterLocked.enforceDocumentCountLimit( 1812 packageName, 1813 () -> getRawStorageInfoProto().getDocumentStorageInfo() 1814 .getNamespaceStorageInfoList()); 1815 } 1816 1817 /** 1818 * Retrieves a document from the AppSearch index by namespace and document ID from any 1819 * application the caller is allowed to view 1820 * 1821 * <p>This method will handle both Icing engine errors as well as permission errors by 1822 * throwing an obfuscated RESULT_NOT_FOUND exception. This is done so the caller doesn't 1823 * receive information on whether or not a file they are not allowed to access exists or not. 1824 * This is different from the behavior of {@link #getDocument}. 1825 * 1826 * @param packageName The package that owns this document. 1827 * @param databaseName The databaseName this document resides in. 1828 * @param namespace The namespace this document resides in. 1829 * @param id The ID of the document to get. 1830 * @param typePropertyPaths A map of schema type to a list of property paths to return in the 1831 * result. 1832 * @param callerAccess Visibility access info of the calling app 1833 * @return The Document contents 1834 * @throws AppSearchException on IcingSearchEngine error or invalid permissions 1835 */ globalGetDocument( @onNull String packageName, @NonNull String databaseName, @NonNull String namespace, @NonNull String id, @NonNull Map<String, List<String>> typePropertyPaths, @NonNull CallerAccess callerAccess)1836 public @NonNull GenericDocument globalGetDocument( 1837 @NonNull String packageName, 1838 @NonNull String databaseName, 1839 @NonNull String namespace, 1840 @NonNull String id, 1841 @NonNull Map<String, List<String>> typePropertyPaths, 1842 @NonNull CallerAccess callerAccess) throws AppSearchException { 1843 mReadWriteLock.readLock().lock(); 1844 try { 1845 throwIfClosedLocked(); 1846 // We retrieve the document before checking for access, as we do not know which 1847 // schema the document is under. Schema is required for checking access 1848 DocumentProto documentProto; 1849 try { 1850 documentProto = getDocumentProtoByIdLocked(packageName, databaseName, 1851 namespace, id, typePropertyPaths); 1852 1853 if (!VisibilityUtil.isSchemaSearchableByCaller( 1854 callerAccess, 1855 packageName, 1856 documentProto.getSchema(), 1857 mDocumentVisibilityStoreLocked, 1858 mVisibilityCheckerLocked)) { 1859 throw new AppSearchException(AppSearchResult.RESULT_NOT_FOUND); 1860 } 1861 } catch (AppSearchException e) { 1862 // Not passing cause in AppSearchException as that violates privacy guarantees as 1863 // user could differentiate between document not existing and not having access. 1864 throw new AppSearchException(AppSearchResult.RESULT_NOT_FOUND, 1865 "Document (" + namespace + ", " + id + ") not found."); 1866 } 1867 1868 DocumentProto.Builder documentBuilder = documentProto.toBuilder(); 1869 removePrefixesFromDocument(documentBuilder); 1870 String prefix = createPrefix(packageName, databaseName); 1871 return GenericDocumentToProtoConverter.toGenericDocument(documentBuilder.build(), 1872 prefix, mSchemaCacheLocked, mConfig); 1873 } finally { 1874 mReadWriteLock.readLock().unlock(); 1875 } 1876 } 1877 1878 /** 1879 * Retrieves a document from the AppSearch index by namespace and document ID. 1880 * 1881 * <p>This method belongs to query group. 1882 * 1883 * @param packageName The package that owns this document. 1884 * @param databaseName The databaseName this document resides in. 1885 * @param namespace The namespace this document resides in. 1886 * @param id The ID of the document to get. 1887 * @param typePropertyPaths A map of schema type to a list of property paths to return in the 1888 * result. 1889 * @return The Document contents 1890 * @throws AppSearchException on IcingSearchEngine error. 1891 */ getDocument( @onNull String packageName, @NonNull String databaseName, @NonNull String namespace, @NonNull String id, @NonNull Map<String, List<String>> typePropertyPaths)1892 public @NonNull GenericDocument getDocument( 1893 @NonNull String packageName, 1894 @NonNull String databaseName, 1895 @NonNull String namespace, 1896 @NonNull String id, 1897 @NonNull Map<String, List<String>> typePropertyPaths) throws AppSearchException { 1898 mReadWriteLock.readLock().lock(); 1899 try { 1900 throwIfClosedLocked(); 1901 DocumentProto documentProto = getDocumentProtoByIdLocked(packageName, databaseName, 1902 namespace, id, typePropertyPaths); 1903 DocumentProto.Builder documentBuilder = documentProto.toBuilder(); 1904 removePrefixesFromDocument(documentBuilder); 1905 1906 String prefix = createPrefix(packageName, databaseName); 1907 // The schema type map cannot be null at this point. It could only be null if no 1908 // schema had ever been set for that prefix. Given we have retrieved a document from 1909 // the index, we know a schema had to have been set. 1910 return GenericDocumentToProtoConverter.toGenericDocument(documentBuilder.build(), 1911 prefix, mSchemaCacheLocked, mConfig); 1912 } finally { 1913 mReadWriteLock.readLock().unlock(); 1914 } 1915 } 1916 1917 /** 1918 * Retrieves a list document from the AppSearch index by namespace and document ID. 1919 * 1920 * <p>This method belongs to query group. 1921 * 1922 * @param packageName The package that owns this document. 1923 * @param databaseName The databaseName this document resides in. 1924 * @param request The request configuration for BatchGet. 1925 * @param callerAccess The information about caller. Visibility will be checked if 1926 * it is not NULL. 1927 * 1928 * @return The Document contents in a {@link AppSearchBatchResult}. 1929 */ batchGetDocuments( @onNull String packageName, @NonNull String databaseName, @NonNull GetByDocumentIdRequest request, @Nullable CallerAccess callerAccess)1930 public @NonNull AppSearchBatchResult<String, GenericDocument> batchGetDocuments( 1931 @NonNull String packageName, 1932 @NonNull String databaseName, 1933 @NonNull GetByDocumentIdRequest request, 1934 @Nullable CallerAccess callerAccess) { 1935 AppSearchBatchResult.Builder<String, GenericDocument> resultBuilder = 1936 new AppSearchBatchResult.Builder<>(); 1937 1938 // If the id list is empty, we can just return directly. 1939 if (request.getIds().isEmpty()) { 1940 return resultBuilder.build(); 1941 } 1942 1943 mReadWriteLock.readLock().lock(); 1944 try { 1945 throwIfClosedLocked(); 1946 1947 BatchGetResultProto batchGetResultProto = batchGetDocumentProtoByIdLocked( 1948 packageName, databaseName, request); 1949 1950 for (int i = 0; i < batchGetResultProto.getGetResultProtosCount(); ++i) { 1951 GetResultProto getResultProto = batchGetResultProto.getGetResultProtos(i); 1952 String id = getResultProto.getUri(); 1953 try { 1954 checkSuccess(getResultProto.getStatus()); 1955 1956 // Check if the schema is visible to the caller. This is only done if 1957 // callerAccess is not null. 1958 // TODO(b/404643381) We can cache the results and use those if we have seen 1959 // the same schema before. 1960 if (callerAccess != null 1961 && !VisibilityUtil.isSchemaSearchableByCaller( 1962 callerAccess, 1963 packageName, 1964 getResultProto.getDocument().getSchema(), 1965 mDocumentVisibilityStoreLocked, 1966 mVisibilityCheckerLocked)) { 1967 throw new AppSearchException(AppSearchResult.RESULT_NOT_FOUND); 1968 } 1969 1970 DocumentProto.Builder documentBuilder = 1971 getResultProto.getDocument().toBuilder(); 1972 removePrefixesFromDocument(documentBuilder); 1973 String prefix = createPrefix(packageName, databaseName); 1974 // The schema type map cannot be null at this point. It could only be null if no 1975 // schema had ever been set for that prefix. Given we have retrieved a document 1976 // from the index, we know a schema had to have been set. 1977 GenericDocument doc = GenericDocumentToProtoConverter.toGenericDocument( 1978 documentBuilder.build(), prefix, mSchemaCacheLocked, mConfig); 1979 1980 resultBuilder.setSuccess(id, doc); 1981 } catch (Throwable t) { 1982 resultBuilder.setResult(id, throwableToFailedResult(t)); 1983 } 1984 } 1985 1986 return resultBuilder.build(); 1987 } finally { 1988 mReadWriteLock.readLock().unlock(); 1989 } 1990 } 1991 createGetResultSpecProto( @onNull String packageName, @NonNull String databaseName, @NonNull String namespace, @NonNull Map<String, List<String>> typePropertyPaths, @Nullable Set<String> ids)1992 private GetResultSpecProto createGetResultSpecProto( 1993 @NonNull String packageName, 1994 @NonNull String databaseName, 1995 @NonNull String namespace, 1996 @NonNull Map<String, List<String>> typePropertyPaths, 1997 @Nullable Set<String> ids) { 1998 String prefix = createPrefix(packageName, databaseName); 1999 List<TypePropertyMask.Builder> nonPrefixedPropertyMaskBuilders = 2000 TypePropertyPathToProtoConverter 2001 .toTypePropertyMaskBuilderList(typePropertyPaths); 2002 List<TypePropertyMask> prefixedPropertyMasks = 2003 new ArrayList<>(nonPrefixedPropertyMaskBuilders.size()); 2004 for (int i = 0; i < nonPrefixedPropertyMaskBuilders.size(); ++i) { 2005 String nonPrefixedType = nonPrefixedPropertyMaskBuilders.get(i).getSchemaType(); 2006 String prefixedType = nonPrefixedType; 2007 if (!nonPrefixedType.equals( 2008 GetByDocumentIdRequest.PROJECTION_SCHEMA_TYPE_WILDCARD)) { 2009 // Append prefix if it is not a wildcard. 2010 prefixedType = prefix + nonPrefixedType; 2011 } 2012 prefixedPropertyMasks.add( 2013 nonPrefixedPropertyMaskBuilders.get(i).setSchemaType(prefixedType).build()); 2014 } 2015 2016 GetResultSpecProto.Builder resultSpecProtoBuilder = GetResultSpecProto.newBuilder() 2017 .setNamespaceRequested(namespace) 2018 .addAllTypePropertyMasks(prefixedPropertyMasks); 2019 2020 // For old getDocumentProtoByIdLocked, we don't need to set the ids in the request. 2021 // So we don't pass the ids in from there. 2022 if (ids != null && !ids.isEmpty()) { 2023 resultSpecProtoBuilder.addAllIds(ids); 2024 } 2025 2026 return resultSpecProtoBuilder.build(); 2027 } 2028 2029 /** 2030 * Returns a DocumentProto from Icing. 2031 * 2032 * @param packageName The package that owns this document. 2033 * @param databaseName The databaseName this document resides in. 2034 * @param namespace The namespace this document resides in. 2035 * @param id The ID of the document to get. 2036 * @param typePropertyPaths A map of schema type to a list of property paths to return in the 2037 * result. 2038 * @return the DocumentProto object 2039 * @throws AppSearchException on IcingSearchEngine error 2040 */ 2041 @GuardedBy("mReadWriteLock") 2042 // We only log getResultProto.toString() in fullPii trace for debugging. 2043 @SuppressWarnings("LiteProtoToString") getDocumentProtoByIdLocked( @onNull String packageName, @NonNull String databaseName, @NonNull String namespace, @NonNull String id, @NonNull Map<String, List<String>> typePropertyPaths)2044 private @NonNull DocumentProto getDocumentProtoByIdLocked( 2045 @NonNull String packageName, 2046 @NonNull String databaseName, 2047 @NonNull String namespace, 2048 @NonNull String id, 2049 @NonNull Map<String, List<String>> typePropertyPaths) 2050 throws AppSearchException { 2051 String finalNamespace = createPrefix(packageName, databaseName) + namespace; 2052 GetResultSpecProto getResultSpec = createGetResultSpecProto( 2053 packageName, databaseName, finalNamespace, typePropertyPaths, /*ids=*/ null); 2054 2055 if (LogUtil.isPiiTraceEnabled()) { 2056 LogUtil.piiTrace( 2057 TAG, "getDocument, request", finalNamespace + ", " + id + "," + getResultSpec); 2058 } 2059 GetResultProto getResultProto = 2060 mIcingSearchEngineLocked.get(finalNamespace, id, getResultSpec); 2061 LogUtil.piiTrace(TAG, "getDocument, response", getResultProto.getStatus(), getResultProto); 2062 checkSuccess(getResultProto.getStatus()); 2063 2064 return getResultProto.getDocument(); 2065 } 2066 2067 2068 /* 2069 * Returns a BatchGetResultProto from Icing. It contains GetResultProto for each id. 2070 */ 2071 @GuardedBy("mReadWriteLock") 2072 @SuppressWarnings("LiteProtoToString") batchGetDocumentProtoByIdLocked( @onNull String packageName, @NonNull String databaseName, @NonNull GetByDocumentIdRequest request)2073 private @NonNull BatchGetResultProto batchGetDocumentProtoByIdLocked( 2074 @NonNull String packageName, 2075 @NonNull String databaseName, 2076 @NonNull GetByDocumentIdRequest request) { 2077 String finalNamespace = createPrefix(packageName, databaseName) + request.getNamespace(); 2078 GetResultSpecProto getResultSpec = createGetResultSpecProto( 2079 packageName, databaseName, finalNamespace, 2080 request.getProjections(), request.getIds()); 2081 2082 LogUtil.piiTrace( 2083 TAG, "getDocument, request", getResultSpec); 2084 BatchGetResultProto batchGetResultProto = 2085 mIcingSearchEngineLocked.batchGet(getResultSpec); 2086 LogUtil.piiTrace(TAG, "getDocument, response", 2087 batchGetResultProto.getStatus(), 2088 batchGetResultProto); 2089 2090 return batchGetResultProto; 2091 } 2092 2093 /** 2094 * Executes a query against the AppSearch index and returns results. 2095 * 2096 * <p>This method belongs to query group. 2097 * 2098 * @param packageName The package name that is performing the query. 2099 * @param databaseName The databaseName this query for. 2100 * @param queryExpression Query String to search. 2101 * @param searchSpec Spec for setting filters, raw query etc. 2102 * @param logger logger to collect query stats 2103 * @return The results of performing this search. It may contain an empty list of results if 2104 * no documents matched the query. 2105 * @throws AppSearchException on IcingSearchEngine error. 2106 */ query( @onNull String packageName, @NonNull String databaseName, @NonNull String queryExpression, @NonNull SearchSpec searchSpec, @Nullable AppSearchLogger logger)2107 public @NonNull SearchResultPage query( 2108 @NonNull String packageName, 2109 @NonNull String databaseName, 2110 @NonNull String queryExpression, 2111 @NonNull SearchSpec searchSpec, 2112 @Nullable AppSearchLogger logger) throws AppSearchException { 2113 long totalLatencyStartMillis = SystemClock.elapsedRealtime(); 2114 SearchStats.Builder sStatsBuilder = null; 2115 if (logger != null) { 2116 sStatsBuilder = 2117 new SearchStats.Builder(SearchStats.VISIBILITY_SCOPE_LOCAL, packageName) 2118 .setDatabase(databaseName) 2119 .setSearchSourceLogTag(searchSpec.getSearchSourceLogTag()) 2120 .setLaunchVMEnabled(mIsVMEnabled); 2121 } 2122 2123 long javaLockAcquisitionLatencyStartMillis = SystemClock.elapsedRealtime(); 2124 mReadWriteLock.readLock().lock(); 2125 try { 2126 if (sStatsBuilder != null) { 2127 sStatsBuilder.setJavaLockAcquisitionLatencyMillis( 2128 (int) (SystemClock.elapsedRealtime() 2129 - javaLockAcquisitionLatencyStartMillis)); 2130 } 2131 throwIfClosedLocked(); 2132 2133 List<String> filterPackageNames = searchSpec.getFilterPackageNames(); 2134 if (!filterPackageNames.isEmpty() && !filterPackageNames.contains(packageName)) { 2135 // Client wanted to query over some packages that weren't its own. This isn't 2136 // allowed through local query so we can return early with no results. 2137 if (sStatsBuilder != null && logger != null) { 2138 sStatsBuilder.setStatusCode(AppSearchResult.RESULT_SECURITY_ERROR); 2139 } 2140 return new SearchResultPage(); 2141 } 2142 2143 String prefix = createPrefix(packageName, databaseName); 2144 SearchSpecToProtoConverter searchSpecToProtoConverter = 2145 new SearchSpecToProtoConverter(queryExpression, searchSpec, 2146 Collections.singleton(prefix), mNamespaceCacheLocked, 2147 mSchemaCacheLocked, mConfig); 2148 if (searchSpecToProtoConverter.hasNothingToSearch()) { 2149 // there is nothing to search over given their search filters, so we can return an 2150 // empty SearchResult and skip sending request to Icing. 2151 return new SearchResultPage(); 2152 } 2153 2154 SearchResultPage searchResultPage = 2155 doQueryLocked( 2156 searchSpecToProtoConverter, 2157 sStatsBuilder); 2158 addNextPageToken(packageName, searchResultPage.getNextPageToken()); 2159 return searchResultPage; 2160 } finally { 2161 mReadWriteLock.readLock().unlock(); 2162 if (sStatsBuilder != null && logger != null) { 2163 sStatsBuilder.setTotalLatencyMillis( 2164 (int) (SystemClock.elapsedRealtime() - totalLatencyStartMillis)); 2165 logger.logStats(sStatsBuilder.build()); 2166 } 2167 } 2168 } 2169 2170 /** 2171 * Executes a global query, i.e. over all permitted prefixes, against the AppSearch index and 2172 * returns results. 2173 * 2174 * <p>This method belongs to query group. 2175 * 2176 * @param queryExpression Query String to search. 2177 * @param searchSpec Spec for setting filters, raw query etc. 2178 * @param callerAccess Visibility access info of the calling app 2179 * @param logger logger to collect globalQuery stats 2180 * @return The results of performing this search. It may contain an empty list of results if 2181 * no documents matched the query. 2182 * @throws AppSearchException on IcingSearchEngine error. 2183 */ globalQuery( @onNull String queryExpression, @NonNull SearchSpec searchSpec, @NonNull CallerAccess callerAccess, @Nullable AppSearchLogger logger)2184 public @NonNull SearchResultPage globalQuery( 2185 @NonNull String queryExpression, 2186 @NonNull SearchSpec searchSpec, 2187 @NonNull CallerAccess callerAccess, 2188 @Nullable AppSearchLogger logger) throws AppSearchException { 2189 long totalLatencyStartMillis = SystemClock.elapsedRealtime(); 2190 SearchStats.Builder sStatsBuilder = null; 2191 if (logger != null) { 2192 sStatsBuilder = 2193 new SearchStats.Builder( 2194 SearchStats.VISIBILITY_SCOPE_GLOBAL, 2195 callerAccess.getCallingPackageName()) 2196 .setSearchSourceLogTag(searchSpec.getSearchSourceLogTag()) 2197 .setLaunchVMEnabled(mIsVMEnabled); 2198 } 2199 2200 long javaLockAcquisitionLatencyStartMillis = SystemClock.elapsedRealtime(); 2201 mReadWriteLock.readLock().lock(); 2202 try { 2203 if (sStatsBuilder != null) { 2204 sStatsBuilder.setJavaLockAcquisitionLatencyMillis( 2205 (int) (SystemClock.elapsedRealtime() 2206 - javaLockAcquisitionLatencyStartMillis)); 2207 } 2208 throwIfClosedLocked(); 2209 2210 long aclLatencyStartMillis = SystemClock.elapsedRealtime(); 2211 2212 // The two scenarios where we want to limit package filters are if the outer 2213 // SearchSpec has package filters and there is no JoinSpec, or if both outer and 2214 // nested SearchSpecs have package filters. If outer SearchSpec has no package 2215 // filters or the nested SearchSpec has no package filters, then we pass the key set of 2216 // documentNamespace map of mNamespaceCachedLocked to the SearchSpecToProtoConverter, 2217 // signifying that there is a SearchSpec that wants to query every visible package. 2218 Set<String> packageFilters = new ArraySet<>(); 2219 if (!searchSpec.getFilterPackageNames().isEmpty()) { 2220 JoinSpec joinSpec = searchSpec.getJoinSpec(); 2221 if (joinSpec == null) { 2222 packageFilters.addAll(searchSpec.getFilterPackageNames()); 2223 } else if (!joinSpec.getNestedSearchSpec() 2224 .getFilterPackageNames().isEmpty()) { 2225 packageFilters.addAll(searchSpec.getFilterPackageNames()); 2226 packageFilters.addAll(joinSpec.getNestedSearchSpec().getFilterPackageNames()); 2227 } 2228 } 2229 2230 // Convert package filters to prefix filters 2231 Set<String> prefixFilters = new ArraySet<>(); 2232 if (packageFilters.isEmpty()) { 2233 // Client didn't restrict their search over packages. Try to query over all 2234 // packages/prefixes 2235 prefixFilters = mNamespaceCacheLocked.getAllDocumentPrefixes(); 2236 } else { 2237 // Client did restrict their search over packages. Only include the prefixes that 2238 // belong to the specified packages. 2239 for (String prefix : mNamespaceCacheLocked.getAllDocumentPrefixes()) { 2240 String packageName = getPackageName(prefix); 2241 if (packageFilters.contains(packageName)) { 2242 prefixFilters.add(prefix); 2243 } 2244 } 2245 } 2246 SearchSpecToProtoConverter searchSpecToProtoConverter = 2247 new SearchSpecToProtoConverter(queryExpression, searchSpec, prefixFilters, 2248 mNamespaceCacheLocked, mSchemaCacheLocked, mConfig); 2249 // Remove those inaccessible schemas. 2250 searchSpecToProtoConverter.removeInaccessibleSchemaFilter( 2251 callerAccess, mDocumentVisibilityStoreLocked, mVisibilityCheckerLocked); 2252 if (searchSpecToProtoConverter.hasNothingToSearch()) { 2253 // there is nothing to search over given their search filters, so we can return an 2254 // empty SearchResult and skip sending request to Icing. 2255 return new SearchResultPage(); 2256 } 2257 if (sStatsBuilder != null) { 2258 sStatsBuilder.setAclCheckLatencyMillis( 2259 (int) (SystemClock.elapsedRealtime() - aclLatencyStartMillis)); 2260 } 2261 SearchResultPage searchResultPage = 2262 doQueryLocked( 2263 searchSpecToProtoConverter, 2264 sStatsBuilder); 2265 addNextPageToken( 2266 callerAccess.getCallingPackageName(), searchResultPage.getNextPageToken()); 2267 return searchResultPage; 2268 } finally { 2269 mReadWriteLock.readLock().unlock(); 2270 2271 if (sStatsBuilder != null && logger != null) { 2272 sStatsBuilder.setTotalLatencyMillis( 2273 (int) (SystemClock.elapsedRealtime() - totalLatencyStartMillis)); 2274 logger.logStats(sStatsBuilder.build()); 2275 } 2276 } 2277 } 2278 2279 @GuardedBy("mReadWriteLock") doQueryLocked( @onNull SearchSpecToProtoConverter searchSpecToProtoConverter, SearchStats.@Nullable Builder sStatsBuilder)2280 private SearchResultPage doQueryLocked( 2281 @NonNull SearchSpecToProtoConverter searchSpecToProtoConverter, 2282 SearchStats.@Nullable Builder sStatsBuilder) 2283 throws AppSearchException { 2284 // Rewrite the given SearchSpec into SearchSpecProto, ResultSpecProto and ScoringSpecProto. 2285 // All processes are counted in rewriteSearchSpecLatencyMillis 2286 long rewriteSearchSpecLatencyStartMillis = SystemClock.elapsedRealtime(); 2287 SearchSpecProto finalSearchSpec = searchSpecToProtoConverter.toSearchSpecProto(); 2288 ResultSpecProto finalResultSpec = searchSpecToProtoConverter.toResultSpecProto( 2289 mNamespaceCacheLocked, mSchemaCacheLocked); 2290 ScoringSpecProto scoringSpec = searchSpecToProtoConverter.toScoringSpecProto(); 2291 if (sStatsBuilder != null) { 2292 sStatsBuilder.setRewriteSearchSpecLatencyMillis((int) 2293 (SystemClock.elapsedRealtime() - rewriteSearchSpecLatencyStartMillis)); 2294 } 2295 2296 // Send request to Icing. 2297 SearchResultProto searchResultProto = searchInIcingLocked( 2298 finalSearchSpec, finalResultSpec, scoringSpec, sStatsBuilder); 2299 2300 long rewriteSearchResultLatencyStartMillis = SystemClock.elapsedRealtime(); 2301 // Rewrite search result before we return. 2302 SearchResultPage searchResultPage = SearchResultToProtoConverter 2303 .toSearchResultPage(searchResultProto, mSchemaCacheLocked, mConfig); 2304 if (sStatsBuilder != null) { 2305 sStatsBuilder.setRewriteSearchResultLatencyMillis( 2306 (int) (SystemClock.elapsedRealtime() 2307 - rewriteSearchResultLatencyStartMillis)); 2308 } 2309 return searchResultPage; 2310 } 2311 2312 @GuardedBy("mReadWriteLock") 2313 // We only log searchSpec, scoringSpec and resultSpec in fullPii trace for debugging. 2314 @SuppressWarnings("LiteProtoToString") searchInIcingLocked( @onNull SearchSpecProto searchSpec, @NonNull ResultSpecProto resultSpec, @NonNull ScoringSpecProto scoringSpec, SearchStats.@Nullable Builder sStatsBuilder)2315 private SearchResultProto searchInIcingLocked( 2316 @NonNull SearchSpecProto searchSpec, 2317 @NonNull ResultSpecProto resultSpec, 2318 @NonNull ScoringSpecProto scoringSpec, 2319 SearchStats.@Nullable Builder sStatsBuilder) throws AppSearchException { 2320 if (LogUtil.isPiiTraceEnabled()) { 2321 LogUtil.piiTrace( 2322 TAG, 2323 "search, request", 2324 searchSpec.getQuery(), 2325 searchSpec + ", " + scoringSpec + ", " + resultSpec); 2326 } 2327 SearchResultProto searchResultProto = mIcingSearchEngineLocked.search( 2328 searchSpec, scoringSpec, resultSpec); 2329 LogUtil.piiTrace( 2330 TAG, "search, response", searchResultProto.getResultsCount(), searchResultProto); 2331 if (sStatsBuilder != null) { 2332 sStatsBuilder.setStatusCode(statusProtoToResultCode(searchResultProto.getStatus())); 2333 if (searchSpec.hasJoinSpec()) { 2334 sStatsBuilder.setJoinType(AppSearchSchema.StringPropertyConfig 2335 .JOINABLE_VALUE_TYPE_QUALIFIED_ID); 2336 } 2337 AppSearchLoggerHelper.copyNativeStats(searchResultProto.getQueryStats(), sStatsBuilder); 2338 } 2339 checkSuccess(searchResultProto.getStatus()); 2340 return searchResultProto; 2341 } 2342 2343 /** 2344 * Generates suggestions based on the given search prefix. 2345 * 2346 * <p>This method belongs to query group. 2347 * 2348 * @param packageName The package name that is performing the query. 2349 * @param databaseName The databaseName this query for. 2350 * @param suggestionQueryExpression The non-empty query expression used to be completed. 2351 * @param searchSuggestionSpec Spec for setting filters. 2352 * @return a List of {@link SearchSuggestionResult}. The returned {@link SearchSuggestionResult} 2353 * are order by the number of {@link androidx.appsearch.app.SearchResult} you could get 2354 * by using that suggestion in {@link #query}. 2355 * @throws AppSearchException if the suggestionQueryExpression is empty. 2356 */ searchSuggestion( @onNull String packageName, @NonNull String databaseName, @NonNull String suggestionQueryExpression, @NonNull SearchSuggestionSpec searchSuggestionSpec)2357 public @NonNull List<SearchSuggestionResult> searchSuggestion( 2358 @NonNull String packageName, 2359 @NonNull String databaseName, 2360 @NonNull String suggestionQueryExpression, 2361 @NonNull SearchSuggestionSpec searchSuggestionSpec) throws AppSearchException { 2362 mReadWriteLock.readLock().lock(); 2363 try { 2364 throwIfClosedLocked(); 2365 if (suggestionQueryExpression.isEmpty()) { 2366 throw new AppSearchException( 2367 AppSearchResult.RESULT_INVALID_ARGUMENT, 2368 "suggestionQueryExpression cannot be empty."); 2369 } 2370 if (searchSuggestionSpec.getMaximumResultCount() 2371 > mConfig.getMaxSuggestionCount()) { 2372 throw new AppSearchException( 2373 AppSearchResult.RESULT_INVALID_ARGUMENT, 2374 "Trying to get " + searchSuggestionSpec.getMaximumResultCount() 2375 + " suggestion results, which exceeds limit of " 2376 + mConfig.getMaxSuggestionCount()); 2377 } 2378 2379 String prefix = createPrefix(packageName, databaseName); 2380 SearchSuggestionSpecToProtoConverter searchSuggestionSpecToProtoConverter = 2381 new SearchSuggestionSpecToProtoConverter(suggestionQueryExpression, 2382 searchSuggestionSpec, 2383 Collections.singleton(prefix), 2384 mNamespaceCacheLocked, 2385 mSchemaCacheLocked); 2386 2387 if (searchSuggestionSpecToProtoConverter.hasNothingToSearch()) { 2388 // there is nothing to search over given their search filters, so we can return an 2389 // empty SearchResult and skip sending request to Icing. 2390 return new ArrayList<>(); 2391 } 2392 2393 SuggestionResponse response = mIcingSearchEngineLocked.searchSuggestions( 2394 searchSuggestionSpecToProtoConverter.toSearchSuggestionSpecProto()); 2395 2396 checkSuccess(response.getStatus()); 2397 List<SearchSuggestionResult> suggestions = 2398 new ArrayList<>(response.getSuggestionsCount()); 2399 for (int i = 0; i < response.getSuggestionsCount(); i++) { 2400 suggestions.add(new SearchSuggestionResult.Builder() 2401 .setSuggestedResult(response.getSuggestions(i).getQuery()) 2402 .build()); 2403 } 2404 return suggestions; 2405 } finally { 2406 mReadWriteLock.readLock().unlock(); 2407 } 2408 } 2409 2410 /** 2411 * Returns a mapping of package names to all the databases owned by that package. 2412 * 2413 * <p>This method is inefficient to call repeatedly. 2414 */ getPackageToDatabases()2415 public @NonNull Map<String, Set<String>> getPackageToDatabases() { 2416 mReadWriteLock.readLock().lock(); 2417 try { 2418 Map<String, Set<String>> packageToDatabases = new ArrayMap<>(); 2419 for (String prefix : mSchemaCacheLocked.getAllPrefixes()) { 2420 String packageName = getPackageName(prefix); 2421 2422 Set<String> databases = packageToDatabases.get(packageName); 2423 if (databases == null) { 2424 databases = new ArraySet<>(); 2425 packageToDatabases.put(packageName, databases); 2426 } 2427 2428 String databaseName = getDatabaseName(prefix); 2429 databases.add(databaseName); 2430 } 2431 2432 return packageToDatabases; 2433 } finally { 2434 mReadWriteLock.readLock().unlock(); 2435 } 2436 } 2437 2438 /** 2439 * Fetches the next page of results of a previously executed query. Results can be empty if 2440 * next-page token is invalid or all pages have been returned. 2441 * 2442 * <p>This method belongs to query group. 2443 * 2444 * @param packageName Package name of the caller. 2445 * @param nextPageToken The token of pre-loaded results of previously executed query. 2446 * @return The next page of results of previously executed query. 2447 * @throws AppSearchException on IcingSearchEngine error or if can't advance on nextPageToken. 2448 */ getNextPage(@onNull String packageName, long nextPageToken, SearchStats.@Nullable Builder sStatsBuilder)2449 public @NonNull SearchResultPage getNextPage(@NonNull String packageName, long nextPageToken, 2450 SearchStats.@Nullable Builder sStatsBuilder) 2451 throws AppSearchException { 2452 long totalLatencyStartMillis = SystemClock.elapsedRealtime(); 2453 2454 long javaLockAcquisitionLatencyStartMillis = SystemClock.elapsedRealtime(); 2455 mReadWriteLock.readLock().lock(); 2456 try { 2457 if (sStatsBuilder != null) { 2458 sStatsBuilder.setJavaLockAcquisitionLatencyMillis( 2459 (int) (SystemClock.elapsedRealtime() 2460 - javaLockAcquisitionLatencyStartMillis)) 2461 .setLaunchVMEnabled(mIsVMEnabled); 2462 } 2463 throwIfClosedLocked(); 2464 2465 LogUtil.piiTrace(TAG, "getNextPage, request", nextPageToken); 2466 checkNextPageToken(packageName, nextPageToken); 2467 SearchResultProto searchResultProto = mIcingSearchEngineLocked.getNextPage( 2468 nextPageToken); 2469 2470 if (sStatsBuilder != null) { 2471 sStatsBuilder.setStatusCode(statusProtoToResultCode(searchResultProto.getStatus())); 2472 // Join query stats are handled by SearchResultsImpl, which has access to the 2473 // original SearchSpec. 2474 AppSearchLoggerHelper.copyNativeStats(searchResultProto.getQueryStats(), 2475 sStatsBuilder); 2476 } 2477 2478 LogUtil.piiTrace( 2479 TAG, 2480 "getNextPage, response", 2481 searchResultProto.getResultsCount(), 2482 searchResultProto); 2483 checkSuccess(searchResultProto.getStatus()); 2484 if (nextPageToken != EMPTY_PAGE_TOKEN 2485 && searchResultProto.getNextPageToken() == EMPTY_PAGE_TOKEN) { 2486 // At this point, we're guaranteed that this nextPageToken exists for this package, 2487 // otherwise checkNextPageToken would've thrown an exception. 2488 // Since the new token is 0, this is the last page. We should remove the old token 2489 // from our cache since it no longer refers to this query. 2490 synchronized (mNextPageTokensLocked) { 2491 Set<Long> nextPageTokensForPackage = 2492 Preconditions.checkNotNull(mNextPageTokensLocked.get(packageName)); 2493 nextPageTokensForPackage.remove(nextPageToken); 2494 } 2495 } 2496 long rewriteSearchResultLatencyStartMillis = SystemClock.elapsedRealtime(); 2497 // Rewrite search result before we return. 2498 SearchResultPage searchResultPage = SearchResultToProtoConverter 2499 .toSearchResultPage(searchResultProto, mSchemaCacheLocked, mConfig); 2500 if (sStatsBuilder != null) { 2501 sStatsBuilder.setRewriteSearchResultLatencyMillis( 2502 (int) (SystemClock.elapsedRealtime() 2503 - rewriteSearchResultLatencyStartMillis)); 2504 } 2505 return searchResultPage; 2506 } finally { 2507 mReadWriteLock.readLock().unlock(); 2508 if (sStatsBuilder != null) { 2509 sStatsBuilder.setTotalLatencyMillis( 2510 (int) (SystemClock.elapsedRealtime() - totalLatencyStartMillis)); 2511 } 2512 } 2513 } 2514 2515 /** 2516 * Invalidates the next-page token so that no more results of the related query can be returned. 2517 * 2518 * <p>This method belongs to query group. 2519 * 2520 * @param packageName Package name of the caller. 2521 * @param nextPageToken The token of pre-loaded results of previously executed query to be 2522 * Invalidated. 2523 * @throws AppSearchException if nextPageToken is unusable. 2524 */ invalidateNextPageToken(@onNull String packageName, long nextPageToken)2525 public void invalidateNextPageToken(@NonNull String packageName, long nextPageToken) 2526 throws AppSearchException { 2527 if (nextPageToken == EMPTY_PAGE_TOKEN) { 2528 // (b/208305352) Directly return here since we are no longer caching EMPTY_PAGE_TOKEN 2529 // in the cached token set. So no need to remove it anymore. 2530 return; 2531 } 2532 2533 mReadWriteLock.readLock().lock(); 2534 try { 2535 throwIfClosedLocked(); 2536 2537 LogUtil.piiTrace(TAG, "invalidateNextPageToken, request", nextPageToken); 2538 checkNextPageToken(packageName, nextPageToken); 2539 mIcingSearchEngineLocked.invalidateNextPageToken(nextPageToken); 2540 2541 synchronized (mNextPageTokensLocked) { 2542 Set<Long> tokens = mNextPageTokensLocked.get(packageName); 2543 if (tokens != null) { 2544 tokens.remove(nextPageToken); 2545 } else { 2546 Log.e(TAG, "Failed to invalidate token " + nextPageToken + ": tokens are not " 2547 + "cached."); 2548 } 2549 } 2550 } finally { 2551 mReadWriteLock.readLock().unlock(); 2552 } 2553 } 2554 2555 /** 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)2556 public void reportUsage( 2557 @NonNull String packageName, 2558 @NonNull String databaseName, 2559 @NonNull String namespace, 2560 @NonNull String documentId, 2561 long usageTimestampMillis, 2562 boolean systemUsage) throws AppSearchException { 2563 mReadWriteLock.writeLock().lock(); 2564 try { 2565 throwIfClosedLocked(); 2566 2567 String prefixedNamespace = createPrefix(packageName, databaseName) + namespace; 2568 UsageReport.UsageType usageType = systemUsage 2569 ? UsageReport.UsageType.USAGE_TYPE2 : UsageReport.UsageType.USAGE_TYPE1; 2570 UsageReport report = UsageReport.newBuilder() 2571 .setDocumentNamespace(prefixedNamespace) 2572 .setDocumentUri(documentId) 2573 .setUsageTimestampMs(usageTimestampMillis) 2574 .setUsageType(usageType) 2575 .build(); 2576 2577 LogUtil.piiTrace(TAG, "reportUsage, request", report.getDocumentUri(), report); 2578 ReportUsageResultProto result = mIcingSearchEngineLocked.reportUsage(report); 2579 LogUtil.piiTrace(TAG, "reportUsage, response", result.getStatus(), result); 2580 checkSuccess(result.getStatus()); 2581 } finally { 2582 mReadWriteLock.writeLock().unlock(); 2583 } 2584 } 2585 2586 /** 2587 * Removes the given document by id. 2588 * 2589 * <p>This method belongs to mutate group. 2590 * 2591 * @param packageName The package name that owns the document. 2592 * @param databaseName The databaseName the document is in. 2593 * @param namespace Namespace of the document to remove. 2594 * @param documentId ID of the document to remove. 2595 * @param removeStatsBuilder builder for {@link RemoveStats} to hold stats for remove 2596 * @throws AppSearchException on IcingSearchEngine error. 2597 */ remove( @onNull String packageName, @NonNull String databaseName, @NonNull String namespace, @NonNull String documentId, RemoveStats.@Nullable Builder removeStatsBuilder)2598 public void remove( 2599 @NonNull String packageName, 2600 @NonNull String databaseName, 2601 @NonNull String namespace, 2602 @NonNull String documentId, 2603 RemoveStats.@Nullable Builder removeStatsBuilder) throws AppSearchException { 2604 long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime(); 2605 mReadWriteLock.writeLock().lock(); 2606 try { 2607 throwIfClosedLocked(); 2608 2609 String prefixedNamespace = createPrefix(packageName, databaseName) + namespace; 2610 String schemaType = null; 2611 if (mObserverManager.isPackageObserved(packageName)) { 2612 // Someone might be observing the type this document is under, but we have no way to 2613 // know its type without retrieving it. Do so now. 2614 // TODO(b/193494000): If Icing Lib can return information about the deleted 2615 // document's type we can remove this code. 2616 if (LogUtil.isPiiTraceEnabled()) { 2617 LogUtil.piiTrace( 2618 TAG, "removeById, getRequest", prefixedNamespace + ", " + documentId); 2619 } 2620 GetResultProto getResult = mIcingSearchEngineLocked.get( 2621 prefixedNamespace, documentId, GET_RESULT_SPEC_NO_PROPERTIES); 2622 LogUtil.piiTrace(TAG, "removeById, getResponse", getResult.getStatus(), getResult); 2623 checkSuccess(getResult.getStatus()); 2624 schemaType = PrefixUtil.removePrefix(getResult.getDocument().getSchema()); 2625 } 2626 2627 if (LogUtil.isPiiTraceEnabled()) { 2628 LogUtil.piiTrace(TAG, "removeById, request", prefixedNamespace + ", " + documentId); 2629 } 2630 DeleteResultProto deleteResultProto = 2631 mIcingSearchEngineLocked.delete(prefixedNamespace, documentId); 2632 LogUtil.piiTrace( 2633 TAG, "removeById, response", deleteResultProto.getStatus(), deleteResultProto); 2634 2635 if (removeStatsBuilder != null) { 2636 removeStatsBuilder.setStatusCode(statusProtoToResultCode( 2637 deleteResultProto.getStatus())) 2638 .setLaunchVMEnabled(mIsVMEnabled); 2639 AppSearchLoggerHelper.copyNativeStats(deleteResultProto.getDeleteStats(), 2640 removeStatsBuilder); 2641 } 2642 checkSuccess(deleteResultProto.getStatus()); 2643 2644 // Update derived maps 2645 mDocumentLimiterLocked.reportDocumentsRemoved(packageName, /*numDocumentsDeleted=*/1); 2646 2647 // Prepare notifications 2648 if (schemaType != null) { 2649 mObserverManager.onDocumentChange( 2650 packageName, 2651 databaseName, 2652 namespace, 2653 schemaType, 2654 documentId, 2655 mDocumentVisibilityStoreLocked, 2656 mVisibilityCheckerLocked); 2657 } 2658 } finally { 2659 mReadWriteLock.writeLock().unlock(); 2660 if (removeStatsBuilder != null) { 2661 removeStatsBuilder.setTotalLatencyMillis( 2662 (int) (SystemClock.elapsedRealtime() - totalLatencyStartTimeMillis)); 2663 } 2664 } 2665 } 2666 2667 /** 2668 * Removes documents by given query. 2669 * 2670 * <p>This method belongs to mutate group. 2671 * 2672 * <p> {@link SearchSpec} objects containing a {@link JoinSpec} are not allowed here. 2673 * 2674 * @param packageName The package name that owns the documents. 2675 * @param databaseName The databaseName the document is in. 2676 * @param queryExpression Query String to search. 2677 * @param searchSpec Defines what and how to remove 2678 * @param removeStatsBuilder builder for {@link RemoveStats} to hold stats for remove 2679 * @throws AppSearchException on IcingSearchEngine error. 2680 * @throws IllegalArgumentException if the {@link SearchSpec} contains a {@link JoinSpec}. 2681 */ removeByQuery(@onNull String packageName, @NonNull String databaseName, @NonNull String queryExpression, @NonNull SearchSpec searchSpec, RemoveStats.@Nullable Builder removeStatsBuilder)2682 public void removeByQuery(@NonNull String packageName, @NonNull String databaseName, 2683 @NonNull String queryExpression, 2684 @NonNull SearchSpec searchSpec, 2685 RemoveStats.@Nullable Builder removeStatsBuilder) 2686 throws AppSearchException { 2687 if (searchSpec.getJoinSpec() != null) { 2688 throw new IllegalArgumentException("JoinSpec not allowed in removeByQuery, but " 2689 + "JoinSpec was provided"); 2690 } 2691 2692 long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime(); 2693 mReadWriteLock.writeLock().lock(); 2694 try { 2695 throwIfClosedLocked(); 2696 2697 List<String> filterPackageNames = searchSpec.getFilterPackageNames(); 2698 if (!filterPackageNames.isEmpty() && !filterPackageNames.contains(packageName)) { 2699 // We're only removing documents within the parameter `packageName`. If we're not 2700 // restricting our remove-query to this package name, then there's nothing for us to 2701 // remove. 2702 return; 2703 } 2704 2705 String prefix = createPrefix(packageName, databaseName); 2706 if (!mNamespaceCacheLocked.getAllDocumentPrefixes().contains(prefix)) { 2707 // The target database is empty so we can return early and skip sending request to 2708 // Icing. 2709 return; 2710 } 2711 2712 SearchSpecToProtoConverter searchSpecToProtoConverter = 2713 new SearchSpecToProtoConverter(queryExpression, searchSpec, 2714 Collections.singleton(prefix), mNamespaceCacheLocked, 2715 mSchemaCacheLocked, mConfig); 2716 if (searchSpecToProtoConverter.hasNothingToSearch()) { 2717 // there is nothing to search over given their search filters, so we can return 2718 // early and skip sending request to Icing. 2719 return; 2720 } 2721 2722 SearchSpecProto finalSearchSpec = searchSpecToProtoConverter.toSearchSpecProto(); 2723 2724 Set<String> prefixedObservedSchemas = null; 2725 if (mObserverManager.isPackageObserved(packageName)) { 2726 prefixedObservedSchemas = new ArraySet<>(); 2727 List<String> prefixedTargetSchemaTypes = 2728 finalSearchSpec.getSchemaTypeFiltersList(); 2729 for (int i = 0; i < prefixedTargetSchemaTypes.size(); i++) { 2730 String prefixedType = prefixedTargetSchemaTypes.get(i); 2731 String shortTypeName = PrefixUtil.removePrefix(prefixedType); 2732 if (mObserverManager.isSchemaTypeObserved(packageName, shortTypeName)) { 2733 prefixedObservedSchemas.add(prefixedType); 2734 } 2735 } 2736 } 2737 2738 doRemoveByQueryLocked( 2739 packageName, finalSearchSpec, prefixedObservedSchemas, removeStatsBuilder); 2740 2741 } finally { 2742 mReadWriteLock.writeLock().unlock(); 2743 if (removeStatsBuilder != null) { 2744 removeStatsBuilder.setTotalLatencyMillis( 2745 (int) (SystemClock.elapsedRealtime() - totalLatencyStartTimeMillis)) 2746 .setLaunchVMEnabled(mIsVMEnabled); 2747 } 2748 } 2749 } 2750 2751 /** 2752 * Executes removeByQuery. 2753 * 2754 * <p>Change notifications will be created if prefixedObservedSchemas is not null. 2755 * 2756 * @param packageName The package name that owns the documents. 2757 * @param finalSearchSpec The final search spec that has been written through 2758 * {@link SearchSpecToProtoConverter}. 2759 * @param prefixedObservedSchemas The set of prefixed schemas that have valid registered 2760 * observers. Only changes to schemas in this set will be queued. 2761 */ 2762 @GuardedBy("mReadWriteLock") doRemoveByQueryLocked( @onNull String packageName, @NonNull SearchSpecProto finalSearchSpec, @Nullable Set<String> prefixedObservedSchemas, RemoveStats.@Nullable Builder removeStatsBuilder)2763 private void doRemoveByQueryLocked( 2764 @NonNull String packageName, 2765 @NonNull SearchSpecProto finalSearchSpec, 2766 @Nullable Set<String> prefixedObservedSchemas, 2767 RemoveStats.@Nullable Builder removeStatsBuilder) throws AppSearchException { 2768 LogUtil.piiTrace(TAG, "removeByQuery, request", finalSearchSpec); 2769 boolean returnDeletedDocumentInfo = 2770 prefixedObservedSchemas != null && !prefixedObservedSchemas.isEmpty(); 2771 DeleteByQueryResultProto deleteResultProto = 2772 mIcingSearchEngineLocked.deleteByQuery(finalSearchSpec, 2773 returnDeletedDocumentInfo); 2774 LogUtil.piiTrace( 2775 TAG, "removeByQuery, response", deleteResultProto.getStatus(), deleteResultProto); 2776 2777 if (removeStatsBuilder != null) { 2778 removeStatsBuilder.setStatusCode(statusProtoToResultCode( 2779 deleteResultProto.getStatus())); 2780 // TODO(b/187206766) also log query stats here once IcingLib returns it 2781 AppSearchLoggerHelper.copyNativeStats(deleteResultProto.getDeleteByQueryStats(), 2782 removeStatsBuilder); 2783 } 2784 2785 // It seems that the caller wants to get success if the data matching the query is 2786 // not in the DB because it was not there or was successfully deleted. 2787 checkCodeOneOf(deleteResultProto.getStatus(), 2788 StatusProto.Code.OK, StatusProto.Code.NOT_FOUND); 2789 2790 // Update derived maps 2791 int numDocumentsDeleted = 2792 deleteResultProto.getDeleteByQueryStats().getNumDocumentsDeleted(); 2793 mDocumentLimiterLocked.reportDocumentsRemoved(packageName, numDocumentsDeleted); 2794 2795 if (prefixedObservedSchemas != null && !prefixedObservedSchemas.isEmpty()) { 2796 dispatchChangeNotificationsAfterRemoveByQueryLocked(packageName, 2797 deleteResultProto, prefixedObservedSchemas); 2798 } 2799 } 2800 2801 @GuardedBy("mReadWriteLock") dispatchChangeNotificationsAfterRemoveByQueryLocked( @onNull String packageName, @NonNull DeleteByQueryResultProto deleteResultProto, @NonNull Set<String> prefixedObservedSchemas )2802 private void dispatchChangeNotificationsAfterRemoveByQueryLocked( 2803 @NonNull String packageName, 2804 @NonNull DeleteByQueryResultProto deleteResultProto, 2805 @NonNull Set<String> prefixedObservedSchemas 2806 ) throws AppSearchException { 2807 for (int i = 0; i < deleteResultProto.getDeletedDocumentsCount(); ++i) { 2808 DeleteByQueryResultProto.DocumentGroupInfo group = 2809 deleteResultProto.getDeletedDocuments(i); 2810 if (!prefixedObservedSchemas.contains(group.getSchema())) { 2811 continue; 2812 } 2813 String databaseName = PrefixUtil.getDatabaseName(group.getNamespace()); 2814 String namespace = PrefixUtil.removePrefix(group.getNamespace()); 2815 String schemaType = PrefixUtil.removePrefix(group.getSchema()); 2816 for (int j = 0; j < group.getUrisCount(); ++j) { 2817 String uri = group.getUris(j); 2818 mObserverManager.onDocumentChange( 2819 packageName, 2820 databaseName, 2821 namespace, 2822 schemaType, 2823 uri, 2824 mDocumentVisibilityStoreLocked, 2825 mVisibilityCheckerLocked); 2826 } 2827 } 2828 } 2829 2830 /** Estimates the total storage usage info data size for a specific set of packages. */ 2831 @ExperimentalAppSearchApi getStorageInfoForPackages(@onNull Set<String> packageNames)2832 public @NonNull StorageInfo getStorageInfoForPackages(@NonNull Set<String> packageNames) 2833 throws AppSearchException { 2834 mReadWriteLock.readLock().lock(); 2835 try { 2836 throwIfClosedLocked(); 2837 2838 StorageInfo.Builder storageInfoBuilder = new StorageInfo.Builder(); 2839 // read document storage info and set to storageInfoBuilder 2840 Set<String> wantedPrefixedDocumentNamespaces = 2841 mNamespaceCacheLocked.getAllPrefixedDocumentNamespaceForPackages(packageNames); 2842 Set<String> wantedPrefixedBlobNamespaces = 2843 mNamespaceCacheLocked.getAllPrefixedBlobNamespaceForPackages(packageNames); 2844 if (wantedPrefixedDocumentNamespaces.isEmpty() 2845 && wantedPrefixedBlobNamespaces.isEmpty()) { 2846 return storageInfoBuilder.build(); 2847 } 2848 StorageInfoProto storageInfoProto = getRawStorageInfoProto(); 2849 2850 if (Flags.enableBlobStore() && !wantedPrefixedBlobNamespaces.isEmpty()) { 2851 getBlobStorageInfoForNamespaces( 2852 storageInfoProto, wantedPrefixedBlobNamespaces, storageInfoBuilder); 2853 } 2854 if (!wantedPrefixedDocumentNamespaces.isEmpty()) { 2855 getDocumentStorageInfoForNamespaces( 2856 storageInfoProto, wantedPrefixedDocumentNamespaces, storageInfoBuilder); 2857 } 2858 return storageInfoBuilder.build(); 2859 } finally { 2860 mReadWriteLock.readLock().unlock(); 2861 } 2862 } 2863 2864 /** Estimates the storage usage info for a specific database in a package. */ 2865 @ExperimentalAppSearchApi getStorageInfoForDatabase(@onNull String packageName, @NonNull String databaseName)2866 public @NonNull StorageInfo getStorageInfoForDatabase(@NonNull String packageName, 2867 @NonNull String databaseName) 2868 throws AppSearchException { 2869 mReadWriteLock.readLock().lock(); 2870 try { 2871 throwIfClosedLocked(); 2872 2873 StorageInfo.Builder storageInfoBuilder = new StorageInfo.Builder(); 2874 String prefix = createPrefix(packageName, databaseName); 2875 if (Flags.enableBlobStore()) { 2876 // read blob storage info and set to storageInfoBuilder 2877 StorageInfoProto storageInfoProto = getRawStorageInfoProto(); 2878 getBlobStorageInfoForPrefix(storageInfoProto, prefix, storageInfoBuilder); 2879 // read document storage info and set to storageInfoBuilder 2880 Set<String> wantedPrefixedDocumentNamespaces = 2881 mNamespaceCacheLocked.getPrefixedDocumentNamespaces(prefix); 2882 if (wantedPrefixedDocumentNamespaces == null 2883 || wantedPrefixedDocumentNamespaces.isEmpty()) { 2884 return storageInfoBuilder.build(); 2885 } 2886 getDocumentStorageInfoForNamespaces(storageInfoProto, 2887 wantedPrefixedDocumentNamespaces, storageInfoBuilder); 2888 } else { 2889 Map<String, Set<String>> packageToDatabases = getPackageToDatabases(); 2890 Set<String> databases = packageToDatabases.get(packageName); 2891 if (databases == null) { 2892 // Package doesn't exist, no storage info to report 2893 return storageInfoBuilder.build(); 2894 } 2895 if (!databases.contains(databaseName)) { 2896 // Database doesn't exist, no storage info to report 2897 return storageInfoBuilder.build(); 2898 } 2899 2900 Set<String> wantedPrefixedDocumentNamespaces = 2901 mNamespaceCacheLocked.getPrefixedDocumentNamespaces(prefix); 2902 if (wantedPrefixedDocumentNamespaces == null 2903 || wantedPrefixedDocumentNamespaces.isEmpty()) { 2904 return storageInfoBuilder.build(); 2905 } 2906 getDocumentStorageInfoForNamespaces(getRawStorageInfoProto(), 2907 wantedPrefixedDocumentNamespaces, storageInfoBuilder); 2908 } 2909 return storageInfoBuilder.build(); 2910 } finally { 2911 mReadWriteLock.readLock().unlock(); 2912 } 2913 } 2914 2915 /** 2916 * Returns the native storage info capsuled in {@link StorageInfoResultProto} directly from 2917 * IcingSearchEngine. 2918 */ getRawStorageInfoProto()2919 public @NonNull StorageInfoProto getRawStorageInfoProto() throws AppSearchException { 2920 mReadWriteLock.readLock().lock(); 2921 try { 2922 throwIfClosedLocked(); 2923 LogUtil.piiTrace(TAG, "getStorageInfo, request"); 2924 StorageInfoResultProto storageInfoResult = mIcingSearchEngineLocked.getStorageInfo(); 2925 LogUtil.piiTrace( 2926 TAG, 2927 "getStorageInfo, response", storageInfoResult.getStatus(), storageInfoResult); 2928 checkSuccess(storageInfoResult.getStatus()); 2929 return storageInfoResult.getStorageInfo(); 2930 } finally { 2931 mReadWriteLock.readLock().unlock(); 2932 } 2933 } 2934 2935 /** 2936 * Extracts and returns {@link StorageInfo} from {@link StorageInfoProto} based on 2937 * prefixed namespaces. 2938 * 2939 * @param storageInfoProto The source {@link StorageInfoProto} containing storage information 2940 * to be analyzed. 2941 * @param prefixedNamespaces A set of prefixed namespaces that the storage information will be 2942 * filtered against. Only namespaces in this set will be included 2943 * in the analysis. 2944 * @param storageInfoBuilder The {@link StorageInfo.Builder} used to and build the resulting 2945 * {@link StorageInfo}. This builder will be modified with calculated 2946 * values. 2947 */ getDocumentStorageInfoForNamespaces( @onNull StorageInfoProto storageInfoProto, @NonNull Set<String> prefixedNamespaces, StorageInfo.@NonNull Builder storageInfoBuilder)2948 private static void getDocumentStorageInfoForNamespaces( 2949 @NonNull StorageInfoProto storageInfoProto, 2950 @NonNull Set<String> prefixedNamespaces, 2951 StorageInfo.@NonNull Builder storageInfoBuilder) { 2952 if (!storageInfoProto.hasDocumentStorageInfo()) { 2953 return; 2954 } 2955 2956 long totalStorageSize = storageInfoProto.getTotalStorageSize(); 2957 DocumentStorageInfoProto documentStorageInfo = 2958 storageInfoProto.getDocumentStorageInfo(); 2959 int totalDocuments = 2960 documentStorageInfo.getNumAliveDocuments() 2961 + documentStorageInfo.getNumExpiredDocuments(); 2962 2963 if (totalStorageSize == 0 || totalDocuments == 0) { 2964 // Maybe we can exit early and also avoid a divide by 0 error. 2965 return; 2966 } 2967 2968 // Accumulate stats across the package's namespaces. 2969 int aliveDocuments = 0; 2970 int expiredDocuments = 0; 2971 int aliveNamespaces = 0; 2972 List<NamespaceStorageInfoProto> namespaceStorageInfos = 2973 documentStorageInfo.getNamespaceStorageInfoList(); 2974 for (int i = 0; i < namespaceStorageInfos.size(); i++) { 2975 NamespaceStorageInfoProto namespaceStorageInfo = namespaceStorageInfos.get(i); 2976 // The namespace from icing lib is already the prefixed format 2977 if (prefixedNamespaces.contains(namespaceStorageInfo.getNamespace())) { 2978 if (namespaceStorageInfo.getNumAliveDocuments() > 0) { 2979 aliveNamespaces++; 2980 aliveDocuments += namespaceStorageInfo.getNumAliveDocuments(); 2981 } 2982 expiredDocuments += namespaceStorageInfo.getNumExpiredDocuments(); 2983 } 2984 } 2985 int namespaceDocuments = aliveDocuments + expiredDocuments; 2986 2987 // Since we don't have the exact size of all the documents, we do an estimation. Note 2988 // that while the total storage takes into account schema, index, etc. in addition to 2989 // documents, we'll only calculate the percentage based on number of documents a 2990 // client has. 2991 storageInfoBuilder 2992 .setSizeBytes((long) (namespaceDocuments * 1.0 / totalDocuments * totalStorageSize)) 2993 .setAliveDocumentsCount(aliveDocuments) 2994 .setAliveNamespacesCount(aliveNamespaces); 2995 } 2996 2997 /** 2998 * Extracts and returns blob storage information from {@link StorageInfoProto} based on 2999 * a namespace prefix. 3000 * 3001 * @param storageInfoProto The source {@link StorageInfoProto} containing blob storage 3002 * information to be analyzed. 3003 * @param prefix The prefix to match namespaces against. Only blob storage for 3004 * namespaces starting with this prefix will be included. 3005 * @param storageInfoBuilder The {@link StorageInfo.Builder} used to and build the resulting 3006 * {@link StorageInfo}. This builder will be modified with calculated 3007 * values. 3008 */ 3009 @ExperimentalAppSearchApi getBlobStorageInfoForPrefix( @onNull StorageInfoProto storageInfoProto, @NonNull String prefix, StorageInfo.@NonNull Builder storageInfoBuilder)3010 private void getBlobStorageInfoForPrefix( 3011 @NonNull StorageInfoProto storageInfoProto, 3012 @NonNull String prefix, 3013 StorageInfo.@NonNull Builder storageInfoBuilder) { 3014 Set<String> prefixedNamespaces = new ArraySet<>(); 3015 List<NamespaceBlobStorageInfoProto> blobStorageInfoProtos = 3016 storageInfoProto.getNamespaceBlobStorageInfoList(); 3017 for (int i = 0; i < blobStorageInfoProtos.size(); i++) { 3018 String prefixedNamespace = blobStorageInfoProtos.get(i).getNamespace(); 3019 if (prefixedNamespace.startsWith(prefix)) { 3020 prefixedNamespaces.add(prefixedNamespace); 3021 } 3022 } 3023 getBlobStorageInfoForNamespaces(storageInfoProto, prefixedNamespaces, storageInfoBuilder); 3024 } 3025 3026 /** 3027 * Extracts and returns blob storage information from {@link StorageInfoProto} based on prefixed 3028 * namespaces. 3029 * 3030 * @param storageInfoProto The source {@link StorageInfoProto} containing blob storage 3031 * information to be analyzed. 3032 * @param prefixedNamespaces A set of prefixed namespaces that the blob storage information will 3033 * be filtered against. Only namespaces in this set will be 3034 * included in the analysis. 3035 * @param storageInfoBuilder The {@link StorageInfo.Builder} used to and build the resulting 3036 * {@link StorageInfo}. This builder will be modified with 3037 * calculated values. 3038 */ 3039 @ExperimentalAppSearchApi getBlobStorageInfoForNamespaces( @onNull StorageInfoProto storageInfoProto, @NonNull Set<String> prefixedNamespaces, StorageInfo.@NonNull Builder storageInfoBuilder)3040 private void getBlobStorageInfoForNamespaces( 3041 @NonNull StorageInfoProto storageInfoProto, 3042 @NonNull Set<String> prefixedNamespaces, 3043 StorageInfo.@NonNull Builder storageInfoBuilder) { 3044 if (storageInfoProto.getNamespaceBlobStorageInfoCount() == 0) { 3045 return; 3046 } 3047 List<NamespaceBlobStorageInfoProto> blobStorageInfoProtos = 3048 storageInfoProto.getNamespaceBlobStorageInfoList(); 3049 long blobSizeBytes = 0; 3050 int blobCount = 0; 3051 for (int i = 0; i < blobStorageInfoProtos.size(); i++) { 3052 NamespaceBlobStorageInfoProto blobStorageInfoProto = blobStorageInfoProtos.get(i); 3053 if (prefixedNamespaces.contains(blobStorageInfoProto.getNamespace())) { 3054 if (Flags.enableAppSearchManageBlobFiles()) { 3055 List<String> blobFileNames = blobStorageInfoProto.getBlobFileNamesList(); 3056 for (int j = 0; j < blobFileNames.size(); j++) { 3057 File blobFile = new File(mBlobFilesDir, blobFileNames.get(j)); 3058 blobSizeBytes += blobFile.length(); 3059 } 3060 blobCount += blobFileNames.size(); 3061 } else { 3062 blobSizeBytes += blobStorageInfoProto.getBlobSize(); 3063 blobCount += blobStorageInfoProto.getNumBlobs(); 3064 } 3065 } 3066 } 3067 storageInfoBuilder.setBlobsCount(blobCount).setBlobsSizeBytes(blobSizeBytes); 3068 } 3069 3070 /** 3071 * Returns the native debug info capsuled in {@link DebugInfoResultProto} directly from 3072 * IcingSearchEngine. 3073 * 3074 * @param verbosity The verbosity of the debug info. {@link DebugInfoVerbosity.Code#BASIC} 3075 * will return the simplest debug information. 3076 * {@link DebugInfoVerbosity.Code#DETAILED} will return more detailed 3077 * debug information as indicated in the comments in debug.proto 3078 */ getRawDebugInfoProto(DebugInfoVerbosity.@onNull Code verbosity)3079 public @NonNull DebugInfoProto getRawDebugInfoProto(DebugInfoVerbosity.@NonNull Code verbosity) 3080 throws AppSearchException { 3081 mReadWriteLock.readLock().lock(); 3082 try { 3083 throwIfClosedLocked(); 3084 LogUtil.piiTrace(TAG, "getDebugInfo, request"); 3085 DebugInfoResultProto debugInfoResult = mIcingSearchEngineLocked.getDebugInfo( 3086 verbosity); 3087 LogUtil.piiTrace(TAG, "getDebugInfo, response", debugInfoResult.getStatus(), 3088 debugInfoResult); 3089 checkSuccess(debugInfoResult.getStatus()); 3090 return debugInfoResult.getDebugInfo(); 3091 } finally { 3092 mReadWriteLock.readLock().unlock(); 3093 } 3094 } 3095 3096 /** 3097 * Persists all update/delete requests to the disk. 3098 * 3099 * <p>If the app crashes after a call to PersistToDisk with {@link PersistType.Code#FULL}, Icing 3100 * would be able to fully recover all data written up to this point without a costly recovery 3101 * process. 3102 * 3103 * <p>If the app crashes after a call to PersistToDisk with {@link PersistType.Code#LITE}, Icing 3104 * would trigger a costly recovery process in next initialization. After that, Icing would still 3105 * be able to recover all written data - excepting Usage data. Usage data is only guaranteed 3106 * to be safe after a call to PersistToDisk with {@link PersistType.Code#FULL} 3107 * 3108 * <p>If the app crashes after an update/delete request has been made, but before any call to 3109 * PersistToDisk, then all data in Icing will be lost. 3110 * 3111 * @param persistType the amount of data to persist. {@link PersistType.Code#LITE} will only 3112 * persist the minimal amount of data to ensure all data can be recovered. 3113 * {@link PersistType.Code#FULL} will persist all data necessary to 3114 * prevent data loss without needing data recovery. 3115 * @throws AppSearchException on any error that AppSearch persist data to disk. 3116 */ persistToDisk(PersistType.@onNull Code persistType)3117 public void persistToDisk(PersistType.@NonNull Code persistType) throws AppSearchException { 3118 mReadWriteLock.writeLock().lock(); 3119 try { 3120 throwIfClosedLocked(); 3121 3122 LogUtil.piiTrace(TAG, "persistToDisk, request", persistType); 3123 PersistToDiskResultProto persistToDiskResultProto = 3124 mIcingSearchEngineLocked.persistToDisk(persistType); 3125 LogUtil.piiTrace( 3126 TAG, 3127 "persistToDisk, response", 3128 persistToDiskResultProto.getStatus(), 3129 persistToDiskResultProto); 3130 checkSuccess(persistToDiskResultProto.getStatus()); 3131 } finally { 3132 mReadWriteLock.writeLock().unlock(); 3133 } 3134 } 3135 3136 /** 3137 * Remove all {@link AppSearchSchema}s and {@link GenericDocument}s under the given package. 3138 * 3139 * @param packageName The name of package to be removed. 3140 * @throws AppSearchException if we cannot remove the data. 3141 */ 3142 @OptIn(markerClass = ExperimentalAppSearchApi.class) clearPackageData(@onNull String packageName)3143 public void clearPackageData(@NonNull String packageName) throws AppSearchException, 3144 IOException { 3145 mReadWriteLock.writeLock().lock(); 3146 try { 3147 throwIfClosedLocked(); 3148 if (LogUtil.DEBUG) { 3149 Log.d(TAG, "Clear data for package: " + packageName); 3150 } 3151 // TODO(b/193494000): We are calling getPackageToDatabases here and in several other 3152 // places within AppSearchImpl. This method is not efficient and does a lot of string 3153 // manipulation. We should find a way to cache the package to database map so it can 3154 // just be obtained from a local variable instead of being parsed out of the prefixed 3155 // map. 3156 Set<String> existingPackages = getPackageToDatabases().keySet(); 3157 if (existingPackages.contains(packageName)) { 3158 existingPackages.remove(packageName); 3159 prunePackageData(existingPackages); 3160 } 3161 if (mRevocableFileDescriptorStore != null) { 3162 mRevocableFileDescriptorStore.revokeForPackage(packageName); 3163 } 3164 } finally { 3165 mReadWriteLock.writeLock().unlock(); 3166 } 3167 } 3168 3169 /** 3170 * Remove all {@link AppSearchSchema}s and {@link GenericDocument}s that doesn't belong to any 3171 * of the given installed packages 3172 * 3173 * @param installedPackages The name of all installed package. 3174 * @throws AppSearchException if we cannot remove the data. 3175 */ prunePackageData(@onNull Set<String> installedPackages)3176 public void prunePackageData(@NonNull Set<String> installedPackages) throws AppSearchException { 3177 mReadWriteLock.writeLock().lock(); 3178 try { 3179 throwIfClosedLocked(); 3180 Map<String, Set<String>> packageToDatabases = getPackageToDatabases(); 3181 if (installedPackages.containsAll(packageToDatabases.keySet())) { 3182 // No package got removed. We are good. 3183 return; 3184 } 3185 3186 // Prune schema proto 3187 SchemaProto existingSchema = getSchemaProtoLocked(); 3188 SchemaProto.Builder newSchemaBuilder = SchemaProto.newBuilder(); 3189 for (int i = 0; i < existingSchema.getTypesCount(); i++) { 3190 String packageName = getPackageName(existingSchema.getTypes(i).getSchemaType()); 3191 if (installedPackages.contains(packageName)) { 3192 newSchemaBuilder.addTypes(existingSchema.getTypes(i)); 3193 } 3194 } 3195 3196 SchemaProto finalSchema = newSchemaBuilder.build(); 3197 3198 // Apply schema, set force override to true to remove all schemas and documents that 3199 // doesn't belong to any of these installed packages. 3200 LogUtil.piiTrace( 3201 TAG, 3202 "clearPackageData.setSchema, request", 3203 finalSchema.getTypesCount(), 3204 finalSchema); 3205 SetSchemaResultProto setSchemaResultProto = mIcingSearchEngineLocked.setSchema( 3206 finalSchema, /*ignoreErrorsAndDeleteDocuments=*/ true); 3207 LogUtil.piiTrace( 3208 TAG, 3209 "clearPackageData.setSchema, response", 3210 setSchemaResultProto.getStatus(), 3211 setSchemaResultProto); 3212 3213 // Determine whether it succeeded. 3214 checkSuccess(setSchemaResultProto.getStatus()); 3215 3216 // Prune cached maps 3217 for (Map.Entry<String, Set<String>> entry : packageToDatabases.entrySet()) { 3218 String packageName = entry.getKey(); 3219 Set<String> databaseNames = entry.getValue(); 3220 if (!installedPackages.contains(packageName) && databaseNames != null) { 3221 mDocumentLimiterLocked.reportPackageRemoved(packageName); 3222 synchronized (mNextPageTokensLocked) { 3223 mNextPageTokensLocked.remove(packageName); 3224 } 3225 for (String databaseName : databaseNames) { 3226 String removedPrefix = createPrefix(packageName, databaseName); 3227 Set<String> removedSchemas = mSchemaCacheLocked.removePrefix(removedPrefix); 3228 if (mDocumentVisibilityStoreLocked != null) { 3229 mDocumentVisibilityStoreLocked.removeVisibility(removedSchemas); 3230 } 3231 3232 mNamespaceCacheLocked.removeDocumentNamespaces(removedPrefix); 3233 } 3234 } 3235 } 3236 } finally { 3237 mReadWriteLock.writeLock().unlock(); 3238 } 3239 } 3240 3241 /** 3242 * Clears documents and schema across all packages and databaseNames. 3243 * 3244 * <p>This method belongs to mutate group. 3245 * 3246 * @throws AppSearchException on IcingSearchEngine error. 3247 */ 3248 @GuardedBy("mReadWriteLock") resetLocked(InitializeStats.@ullable Builder initStatsBuilder)3249 private void resetLocked(InitializeStats.@Nullable Builder initStatsBuilder) 3250 throws AppSearchException { 3251 LogUtil.piiTrace(TAG, "icingSearchEngine.reset, request"); 3252 ResetResultProto resetResultProto = mIcingSearchEngineLocked.reset(); 3253 LogUtil.piiTrace( 3254 TAG, 3255 "icingSearchEngine.reset, response", 3256 resetResultProto.getStatus(), 3257 resetResultProto); 3258 mOptimizeIntervalCountLocked = 0; 3259 mSchemaCacheLocked.clear(); 3260 mNamespaceCacheLocked.clear(); 3261 3262 // We just reset the index. So there is no need to retrieve the actual storage info. We know 3263 // that there are no actual namespaces. 3264 List<NamespaceStorageInfoProto> emptyNamespaceInfos = Collections.emptyList(); 3265 mDocumentLimiterLocked = 3266 new DocumentLimiter( 3267 mConfig.getDocumentCountLimitStartThreshold(), 3268 mConfig.getPerPackageDocumentCountLimit(), emptyNamespaceInfos); 3269 synchronized (mNextPageTokensLocked) { 3270 mNextPageTokensLocked.clear(); 3271 } 3272 if (initStatsBuilder != null) { 3273 initStatsBuilder 3274 .setHasReset(true) 3275 .setResetStatusCode(statusProtoToResultCode(resetResultProto.getStatus())); 3276 } 3277 3278 checkSuccess(resetResultProto.getStatus()); 3279 } 3280 3281 /** Wrapper around schema changes */ 3282 @VisibleForTesting 3283 static class RewrittenSchemaResults { 3284 // Any prefixed types that used to exist in the schema, but are deleted in the new one. 3285 final Set<String> mDeletedPrefixedTypes = new ArraySet<>(); 3286 3287 // Map of prefixed schema types to SchemaTypeConfigProtos that were part of the new schema. 3288 final Map<String, SchemaTypeConfigProto> mRewrittenPrefixedTypes = new ArrayMap<>(); 3289 } 3290 3291 /** 3292 * Rewrites all types mentioned in the given {@code newSchema} to prepend {@code prefix}. 3293 * Rewritten types will be added to the {@code existingSchema}. 3294 * 3295 * @param prefix The full prefix to prepend to the schema. 3296 * @param existingSchema A schema that may contain existing types from across all prefixes. 3297 * Will be mutated to contain the properly rewritten schema 3298 * types from {@code newSchema}. 3299 * @param newSchema Schema with types to add to the {@code existingSchema}. 3300 * @return a RewrittenSchemaResults that contains all prefixed schema type names in the given 3301 * prefix as well as a set of schema types that were deleted. 3302 */ 3303 @VisibleForTesting rewriteSchema(@onNull String prefix, SchemaProto.@NonNull Builder existingSchema, @NonNull SchemaProto newSchema)3304 static RewrittenSchemaResults rewriteSchema(@NonNull String prefix, 3305 SchemaProto.@NonNull Builder existingSchema, 3306 @NonNull SchemaProto newSchema) throws AppSearchException { 3307 HashMap<String, SchemaTypeConfigProto> newTypesToProto = new HashMap<>(); 3308 // Rewrite the schema type to include the typePrefix. 3309 for (int typeIdx = 0; typeIdx < newSchema.getTypesCount(); typeIdx++) { 3310 SchemaTypeConfigProto.Builder typeConfigBuilder = 3311 newSchema.getTypes(typeIdx).toBuilder(); 3312 3313 // Rewrite SchemaProto.types.schema_type 3314 String newSchemaType = prefix + typeConfigBuilder.getSchemaType(); 3315 typeConfigBuilder.setSchemaType(newSchemaType); 3316 3317 // Rewrite SchemaProto.types.properties.schema_type 3318 for (int propertyIdx = 0; 3319 propertyIdx < typeConfigBuilder.getPropertiesCount(); 3320 propertyIdx++) { 3321 PropertyConfigProto.Builder propertyConfigBuilder = 3322 typeConfigBuilder.getProperties(propertyIdx).toBuilder(); 3323 if (!propertyConfigBuilder.getSchemaType().isEmpty()) { 3324 String newPropertySchemaType = 3325 prefix + propertyConfigBuilder.getSchemaType(); 3326 propertyConfigBuilder.setSchemaType(newPropertySchemaType); 3327 typeConfigBuilder.setProperties(propertyIdx, propertyConfigBuilder); 3328 } 3329 } 3330 3331 // Rewrite SchemaProto.types.parent_types 3332 for (int parentTypeIdx = 0; parentTypeIdx < typeConfigBuilder.getParentTypesCount(); 3333 parentTypeIdx++) { 3334 String newParentType = prefix + typeConfigBuilder.getParentTypes(parentTypeIdx); 3335 typeConfigBuilder.setParentTypes(parentTypeIdx, newParentType); 3336 } 3337 3338 newTypesToProto.put(newSchemaType, typeConfigBuilder.build()); 3339 } 3340 3341 // newTypesToProto is modified below, so we need a copy first 3342 RewrittenSchemaResults rewrittenSchemaResults = new RewrittenSchemaResults(); 3343 rewrittenSchemaResults.mRewrittenPrefixedTypes.putAll(newTypesToProto); 3344 3345 // Combine the existing schema (which may have types from other prefixes) with this 3346 // prefix's new schema. Modifies the existingSchemaBuilder. 3347 // Check if we need to replace any old schema types with the new ones. 3348 for (int i = 0; i < existingSchema.getTypesCount(); i++) { 3349 String schemaType = existingSchema.getTypes(i).getSchemaType(); 3350 SchemaTypeConfigProto newProto = newTypesToProto.remove(schemaType); 3351 if (newProto != null) { 3352 // Replacement 3353 existingSchema.setTypes(i, newProto); 3354 } else if (prefix.equals(getPrefix(schemaType))) { 3355 // All types existing before but not in newSchema should be removed. 3356 existingSchema.removeTypes(i); 3357 --i; 3358 rewrittenSchemaResults.mDeletedPrefixedTypes.add(schemaType); 3359 } 3360 } 3361 // We've been removing existing types from newTypesToProto, so everything that remains is 3362 // new. 3363 existingSchema.addAllTypes(newTypesToProto.values()); 3364 3365 return rewrittenSchemaResults; 3366 } 3367 3368 /** 3369 * Rewrite the {@link InternalVisibilityConfig} to add given prefix in the schemaType of the 3370 * given List of {@link InternalVisibilityConfig} 3371 * 3372 * @param prefix The full prefix to prepend to the visibilityConfigs. 3373 * @param visibilityConfigs The visibility configs that need to add prefix 3374 * @param removedVisibilityConfigs The removed configs that is not included in the given 3375 * visibilityConfigs. 3376 * @return The List of {@link InternalVisibilityConfig} that contains prefixed in its schema 3377 * types. 3378 */ rewriteVisibilityConfigs(@onNull String prefix, @NonNull List<InternalVisibilityConfig> visibilityConfigs, @NonNull Set<String> removedVisibilityConfigs)3379 private List<InternalVisibilityConfig> rewriteVisibilityConfigs(@NonNull String prefix, 3380 @NonNull List<InternalVisibilityConfig> visibilityConfigs, 3381 @NonNull Set<String> removedVisibilityConfigs) { 3382 List<InternalVisibilityConfig> prefixedVisibilityConfigs = 3383 new ArrayList<>(visibilityConfigs.size()); 3384 for (int i = 0; i < visibilityConfigs.size(); i++) { 3385 InternalVisibilityConfig visibilityConfig = visibilityConfigs.get(i); 3386 // The VisibilityConfig is controlled by the client and it's untrusted but we 3387 // make it safe by appending a prefix. 3388 // We must control the package-database prefix. Therefore even if the client 3389 // fake the id, they can only mess their own app. That's totally allowed and 3390 // they can do this via the public API too. 3391 // TODO(b/275592563): Move prefixing into VisibilityConfig 3392 // .createVisibilityDocument and createVisibilityOverlay 3393 String namespace = visibilityConfig.getSchemaType(); 3394 String prefixedNamespace = prefix + namespace; 3395 prefixedVisibilityConfigs.add( 3396 new InternalVisibilityConfig.Builder(visibilityConfig) 3397 .setSchemaType(prefixedNamespace) 3398 .build()); 3399 // This schema has visibility settings. We should keep it from the removal list. 3400 removedVisibilityConfigs.remove(prefixedNamespace); 3401 } 3402 return prefixedVisibilityConfigs; 3403 } 3404 3405 @VisibleForTesting 3406 @GuardedBy("mReadWriteLock") getSchemaProtoLocked()3407 SchemaProto getSchemaProtoLocked() throws AppSearchException { 3408 LogUtil.piiTrace(TAG, "getSchema, request"); 3409 GetSchemaResultProto schemaProto = mIcingSearchEngineLocked.getSchema(); 3410 LogUtil.piiTrace(TAG, "getSchema, response", schemaProto.getStatus(), schemaProto); 3411 // TODO(b/161935693) check GetSchemaResultProto is success or not. Call reset() if it's not. 3412 // TODO(b/161935693) only allow GetSchemaResultProto NOT_FOUND on first run 3413 checkCodeOneOf(schemaProto.getStatus(), StatusProto.Code.OK, StatusProto.Code.NOT_FOUND); 3414 return schemaProto.getSchema(); 3415 } 3416 addNextPageToken(String packageName, long nextPageToken)3417 private void addNextPageToken(String packageName, long nextPageToken) { 3418 if (nextPageToken == EMPTY_PAGE_TOKEN) { 3419 // There is no more pages. No need to add it. 3420 return; 3421 } 3422 synchronized (mNextPageTokensLocked) { 3423 Set<Long> tokens = mNextPageTokensLocked.get(packageName); 3424 if (tokens == null) { 3425 tokens = new ArraySet<>(); 3426 mNextPageTokensLocked.put(packageName, tokens); 3427 } 3428 tokens.add(nextPageToken); 3429 } 3430 } 3431 checkNextPageToken(String packageName, long nextPageToken)3432 private void checkNextPageToken(String packageName, long nextPageToken) 3433 throws AppSearchException { 3434 if (nextPageToken == EMPTY_PAGE_TOKEN) { 3435 // Swallow the check for empty page token, token = 0 means there is no more page and it 3436 // won't return anything from Icing. 3437 return; 3438 } 3439 synchronized (mNextPageTokensLocked) { 3440 Set<Long> nextPageTokens = mNextPageTokensLocked.get(packageName); 3441 if (nextPageTokens == null || !nextPageTokens.contains(nextPageToken)) { 3442 throw new AppSearchException(RESULT_SECURITY_ERROR, 3443 "Package \"" + packageName + "\" cannot use nextPageToken: " 3444 + nextPageToken); 3445 } 3446 } 3447 } 3448 3449 /** 3450 * Adds an {@link ObserverCallback} to monitor changes within the databases owned by 3451 * {@code targetPackageName} if they match the given 3452 * {@link androidx.appsearch.observer.ObserverSpec}. 3453 * 3454 * <p>If the data owned by {@code targetPackageName} is not visible to you, the registration 3455 * call will succeed but no notifications will be dispatched. Notifications could start flowing 3456 * later if {@code targetPackageName} changes its schema visibility settings. 3457 * 3458 * <p>If no package matching {@code targetPackageName} exists on the system, the registration 3459 * call will succeed but no notifications will be dispatched. Notifications could start flowing 3460 * later if {@code targetPackageName} is installed and starts indexing data. 3461 * 3462 * <p>Note that this method does not take the standard read/write lock that guards I/O, so it 3463 * will not queue behind I/O. Therefore it is safe to call from any thread including UI or 3464 * binder threads. 3465 * 3466 * @param listeningPackageAccess Visibility information about the app that wants to receive 3467 * notifications. 3468 * @param targetPackageName The package that owns the data the observer wants to be 3469 * notified for. 3470 * @param spec Describes the kind of data changes the observer should trigger 3471 * for. 3472 * @param executor The executor on which to trigger the observer callback to 3473 * deliver notifications. 3474 * @param observer The callback to trigger on notifications. 3475 */ registerObserverCallback( @onNull CallerAccess listeningPackageAccess, @NonNull String targetPackageName, @NonNull ObserverSpec spec, @NonNull Executor executor, @NonNull ObserverCallback observer)3476 public void registerObserverCallback( 3477 @NonNull CallerAccess listeningPackageAccess, 3478 @NonNull String targetPackageName, 3479 @NonNull ObserverSpec spec, 3480 @NonNull Executor executor, 3481 @NonNull ObserverCallback observer) { 3482 // This method doesn't consult mSchemaMap or mNamespaceMap, and it will register 3483 // observers for types that don't exist. This is intentional because we notify for types 3484 // being created or removed. If we only registered observer for existing types, it would 3485 // be impossible to ever dispatch a notification of a type being added. 3486 mObserverManager.registerObserverCallback( 3487 listeningPackageAccess, targetPackageName, spec, executor, observer); 3488 } 3489 3490 /** 3491 * Removes an {@link ObserverCallback} from watching the databases owned by 3492 * {@code targetPackageName}. 3493 * 3494 * <p>All observers which compare equal to the given observer via 3495 * {@link ObserverCallback#equals} are removed. This may be 0, 1, or many observers. 3496 * 3497 * <p>Note that this method does not take the standard read/write lock that guards I/O, so it 3498 * will not queue behind I/O. Therefore it is safe to call from any thread including UI or 3499 * binder threads. 3500 */ unregisterObserverCallback( @onNull String targetPackageName, @NonNull ObserverCallback observer)3501 public void unregisterObserverCallback( 3502 @NonNull String targetPackageName, @NonNull ObserverCallback observer) { 3503 mObserverManager.unregisterObserverCallback(targetPackageName, observer); 3504 } 3505 3506 /** 3507 * Dispatches the pending change notifications one at a time. 3508 * 3509 * <p>The notifications are dispatched on the respective executors that were provided at the 3510 * time of observer registration. This method does not take the standard read/write lock that 3511 * guards I/O, so it is safe to call from any thread including UI or binder threads. 3512 * 3513 * <p>Exceptions thrown from notification dispatch are logged but otherwise suppressed. 3514 */ dispatchAndClearChangeNotifications()3515 public void dispatchAndClearChangeNotifications() { 3516 mObserverManager.dispatchAndClearPendingNotifications(); 3517 } 3518 3519 /** 3520 * Checks the given status code and throws an {@link AppSearchException} if code is an error. 3521 * 3522 * @throws AppSearchException on error codes. 3523 */ checkSuccess(StatusProto statusProto)3524 private static void checkSuccess(StatusProto statusProto) throws AppSearchException { 3525 checkCodeOneOf(statusProto, StatusProto.Code.OK); 3526 } 3527 3528 /** 3529 * Checks the given status code is one of the provided codes, and throws an 3530 * {@link AppSearchException} if it is not. 3531 */ checkCodeOneOf(StatusProto statusProto, StatusProto.Code... codes)3532 private static void checkCodeOneOf(StatusProto statusProto, StatusProto.Code... codes) 3533 throws AppSearchException { 3534 for (int i = 0; i < codes.length; i++) { 3535 if (codes[i] == statusProto.getCode()) { 3536 // Everything's good 3537 return; 3538 } 3539 } 3540 3541 if (statusProto.getCode() == StatusProto.Code.WARNING_DATA_LOSS) { 3542 // TODO: May want to propagate WARNING_DATA_LOSS up to AppSearchSession so they can 3543 // choose to log the error or potentially pass it on to clients. 3544 Log.w(TAG, "Encountered WARNING_DATA_LOSS: " + statusProto.getMessage()); 3545 return; 3546 } 3547 3548 throw new AppSearchException( 3549 ResultCodeToProtoConverter.toResultCode(statusProto.getCode()), 3550 statusProto.getMessage()); 3551 } 3552 3553 /** 3554 * Checks whether {@link IcingSearchEngine#optimize()} should be called to release resources. 3555 * 3556 * <p>This method should be only called after a mutation to local storage backend which 3557 * deletes a mass of data and could release lots resources after 3558 * {@link IcingSearchEngine#optimize()}. 3559 * 3560 * <p>This method will trigger {@link IcingSearchEngine#getOptimizeInfo()} to check 3561 * resources that could be released for every {@link #CHECK_OPTIMIZE_INTERVAL} mutations. 3562 * 3563 * <p>{@link IcingSearchEngine#optimize()} should be called only if 3564 * {@link GetOptimizeInfoResultProto} shows there is enough resources could be released. 3565 * 3566 * @param mutationSize The number of how many mutations have been executed for current request. 3567 * An inside counter will accumulates it. Once the counter reaches 3568 * {@link #CHECK_OPTIMIZE_INTERVAL}, 3569 * {@link IcingSearchEngine#getOptimizeInfo()} will be triggered and the 3570 * counter will be reset. 3571 */ checkForOptimize(int mutationSize, OptimizeStats.@Nullable Builder builder)3572 public void checkForOptimize(int mutationSize, OptimizeStats.@Nullable Builder builder) 3573 throws AppSearchException { 3574 mReadWriteLock.writeLock().lock(); 3575 try { 3576 mOptimizeIntervalCountLocked += mutationSize; 3577 if (mOptimizeIntervalCountLocked >= CHECK_OPTIMIZE_INTERVAL) { 3578 checkForOptimize(builder); 3579 } 3580 } finally { 3581 mReadWriteLock.writeLock().unlock(); 3582 } 3583 } 3584 3585 /** 3586 * Checks whether {@link IcingSearchEngine#optimize()} should be called to release resources. 3587 * 3588 * <p>This method will directly trigger {@link IcingSearchEngine#getOptimizeInfo()} to check 3589 * resources that could be released. 3590 * 3591 * <p>{@link IcingSearchEngine#optimize()} should be called only if 3592 * {@link OptimizeStrategy#shouldOptimize(GetOptimizeInfoResultProto)} return true. 3593 */ checkForOptimize(OptimizeStats.@ullable Builder builder)3594 public void checkForOptimize(OptimizeStats.@Nullable Builder builder) 3595 throws AppSearchException { 3596 mReadWriteLock.writeLock().lock(); 3597 try { 3598 GetOptimizeInfoResultProto optimizeInfo = getOptimizeInfoResultLocked(); 3599 checkSuccess(optimizeInfo.getStatus()); 3600 mOptimizeIntervalCountLocked = 0; 3601 if (mOptimizeStrategy.shouldOptimize(optimizeInfo)) { 3602 optimize(builder); 3603 } 3604 } finally { 3605 mReadWriteLock.writeLock().unlock(); 3606 } 3607 // TODO(b/147699081): Return OptimizeResultProto & log lost data detail once we add 3608 // a field to indicate lost_schema and lost_documents in OptimizeResultProto. 3609 // go/icing-library-apis. 3610 } 3611 3612 /** Triggers {@link IcingSearchEngine#optimize()} directly. */ optimize(OptimizeStats.@ullable Builder builder)3613 public void optimize(OptimizeStats.@Nullable Builder builder) throws AppSearchException { 3614 mReadWriteLock.writeLock().lock(); 3615 try { 3616 LogUtil.piiTrace(TAG, "optimize, request"); 3617 OptimizeResultProto optimizeResultProto = mIcingSearchEngineLocked.optimize(); 3618 LogUtil.piiTrace( 3619 TAG, 3620 "optimize, response", optimizeResultProto.getStatus(), optimizeResultProto); 3621 if (builder != null) { 3622 builder.setStatusCode(statusProtoToResultCode(optimizeResultProto.getStatus())) 3623 .setLaunchVMEnabled(mIsVMEnabled); 3624 AppSearchLoggerHelper.copyNativeStats(optimizeResultProto.getOptimizeStats(), 3625 builder); 3626 } 3627 checkSuccess(optimizeResultProto.getStatus()); 3628 3629 // If AppSearch manages blob files, remove the optimized blob files. 3630 if (Flags.enableAppSearchManageBlobFiles()) { 3631 List<String> blobFileNamesToRemove = 3632 optimizeResultProto.getBlobFileNamesToRemoveList(); 3633 for (int i = 0; i < blobFileNamesToRemove.size(); i++) { 3634 File blobFileToRemove = new File(mBlobFilesDir, blobFileNamesToRemove.get(i)); 3635 if (!blobFileToRemove.delete()) { 3636 Log.e(TAG, "Cannot delete the optimized blob file: " 3637 + blobFileToRemove.getName()); 3638 } 3639 } 3640 } 3641 } finally { 3642 mReadWriteLock.writeLock().unlock(); 3643 } 3644 } 3645 3646 /** 3647 * Sync the current Android logging level to Icing for the entire process. No lock required. 3648 */ syncLoggingLevelToIcing()3649 public static void syncLoggingLevelToIcing() { 3650 String icingTag = IcingSearchEngine.getLoggingTag(); 3651 if (icingTag == null) { 3652 Log.e(TAG, "Received null logging tag from Icing"); 3653 return; 3654 } 3655 if (LogUtil.DEBUG) { 3656 if (Log.isLoggable(icingTag, Log.VERBOSE)) { 3657 boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.VERBOSE, 3658 /*verbosity=*/ (short) 1); 3659 return; 3660 } else if (Log.isLoggable(icingTag, Log.DEBUG)) { 3661 boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.DBG); 3662 return; 3663 } 3664 } 3665 if (LogUtil.INFO) { 3666 if (Log.isLoggable(icingTag, Log.INFO)) { 3667 boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.INFO); 3668 return; 3669 } 3670 } 3671 if (Log.isLoggable(icingTag, Log.WARN)) { 3672 boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.WARNING); 3673 } else if (Log.isLoggable(icingTag, Log.ERROR)) { 3674 boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.ERROR); 3675 } else { 3676 boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.FATAL); 3677 } 3678 } 3679 3680 @GuardedBy("mReadWriteLock") 3681 @VisibleForTesting getOptimizeInfoResultLocked()3682 GetOptimizeInfoResultProto getOptimizeInfoResultLocked() { 3683 LogUtil.piiTrace(TAG, "getOptimizeInfo, request"); 3684 GetOptimizeInfoResultProto result = mIcingSearchEngineLocked.getOptimizeInfo(); 3685 LogUtil.piiTrace(TAG, "getOptimizeInfo, response", result.getStatus(), result); 3686 return result; 3687 } 3688 3689 /** 3690 * Returns all prefixed schema types saved in AppSearch. 3691 * 3692 * <p>This method is inefficient to call repeatedly. 3693 */ getAllPrefixedSchemaTypes()3694 public @NonNull List<String> getAllPrefixedSchemaTypes() { 3695 mReadWriteLock.readLock().lock(); 3696 try { 3697 return mSchemaCacheLocked.getAllPrefixedSchemaTypes(); 3698 } finally { 3699 mReadWriteLock.readLock().unlock(); 3700 } 3701 } 3702 3703 /** 3704 * Returns all prefixed blob namespaces saved in AppSearch. 3705 * 3706 * <p>This method is inefficient to call repeatedly. 3707 */ getAllPrefixedBlobNamespaces()3708 public @NonNull List<String> getAllPrefixedBlobNamespaces() { 3709 mReadWriteLock.readLock().lock(); 3710 try { 3711 return mNamespaceCacheLocked.getAllPrefixedBlobNamespaces(); 3712 } finally { 3713 mReadWriteLock.readLock().unlock(); 3714 } 3715 } 3716 3717 /** 3718 * Converts an erroneous status code from the Icing status enums to the AppSearchResult enums. 3719 * 3720 * <p>Callers should ensure that the status code is not OK or WARNING_DATA_LOSS. 3721 * 3722 * @param statusProto StatusProto with error code to translate into an 3723 * {@link AppSearchResult} code. 3724 * @return {@link AppSearchResult} error code 3725 */ statusProtoToResultCode( @onNull StatusProto statusProto)3726 @AppSearchResult.ResultCode private static int statusProtoToResultCode( 3727 @NonNull StatusProto statusProto) { 3728 return ResultCodeToProtoConverter.toResultCode(statusProto.getCode()); 3729 } 3730 3731 @ExperimentalAppSearchApi verifyCallingBlobHandle(@onNull String callingPackageName, @NonNull String callingDatabaseName, @NonNull AppSearchBlobHandle blobHandle)3732 private static void verifyCallingBlobHandle(@NonNull String callingPackageName, 3733 @NonNull String callingDatabaseName, @NonNull AppSearchBlobHandle blobHandle) 3734 throws AppSearchException { 3735 if (!blobHandle.getPackageName().equals(callingPackageName)) { 3736 throw new AppSearchException(AppSearchResult.RESULT_INVALID_ARGUMENT, 3737 "Blob package doesn't match calling package, calling package: " 3738 + callingPackageName + ", blob package: " 3739 + blobHandle.getPackageName()); 3740 } 3741 if (!blobHandle.getDatabaseName().equals(callingDatabaseName)) { 3742 throw new AppSearchException(AppSearchResult.RESULT_INVALID_ARGUMENT, 3743 "Blob database doesn't match calling database, calling database: " 3744 + callingDatabaseName + ", blob database: " 3745 + blobHandle.getDatabaseName()); 3746 } 3747 } 3748 3749 } 3750