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