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