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