• 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 com.android.server.appsearch.external.localstorage.util.PrefixUtil.addPrefixToDocument;
20 import static com.android.server.appsearch.external.localstorage.util.PrefixUtil.createPrefix;
21 import static com.android.server.appsearch.external.localstorage.util.PrefixUtil.getDatabaseName;
22 import static com.android.server.appsearch.external.localstorage.util.PrefixUtil.getPackageName;
23 import static com.android.server.appsearch.external.localstorage.util.PrefixUtil.getPrefix;
24 import static com.android.server.appsearch.external.localstorage.util.PrefixUtil.removePrefix;
25 import static com.android.server.appsearch.external.localstorage.util.PrefixUtil.removePrefixesFromDocument;
26 
27 import android.annotation.NonNull;
28 import android.annotation.Nullable;
29 import android.annotation.WorkerThread;
30 import android.app.appsearch.AppSearchResult;
31 import android.app.appsearch.AppSearchSchema;
32 import android.app.appsearch.GenericDocument;
33 import android.app.appsearch.GetByDocumentIdRequest;
34 import android.app.appsearch.GetSchemaResponse;
35 import android.app.appsearch.PackageIdentifier;
36 import android.app.appsearch.SearchResultPage;
37 import android.app.appsearch.SearchSpec;
38 import android.app.appsearch.SetSchemaResponse;
39 import android.app.appsearch.StorageInfo;
40 import android.app.appsearch.exceptions.AppSearchException;
41 import android.app.appsearch.util.LogUtil;
42 import android.os.Bundle;
43 import android.os.SystemClock;
44 import android.util.ArrayMap;
45 import android.util.ArraySet;
46 import android.util.Log;
47 
48 import com.android.internal.annotations.GuardedBy;
49 import com.android.internal.annotations.VisibleForTesting;
50 import com.android.server.appsearch.external.localstorage.converter.GenericDocumentToProtoConverter;
51 import com.android.server.appsearch.external.localstorage.converter.ResultCodeToProtoConverter;
52 import com.android.server.appsearch.external.localstorage.converter.SchemaToProtoConverter;
53 import com.android.server.appsearch.external.localstorage.converter.SearchResultToProtoConverter;
54 import com.android.server.appsearch.external.localstorage.converter.SearchSpecToProtoConverter;
55 import com.android.server.appsearch.external.localstorage.converter.SetSchemaResponseToProtoConverter;
56 import com.android.server.appsearch.external.localstorage.converter.TypePropertyPathToProtoConverter;
57 import com.android.server.appsearch.external.localstorage.stats.InitializeStats;
58 import com.android.server.appsearch.external.localstorage.stats.OptimizeStats;
59 import com.android.server.appsearch.external.localstorage.stats.PutDocumentStats;
60 import com.android.server.appsearch.external.localstorage.stats.RemoveStats;
61 import com.android.server.appsearch.external.localstorage.stats.SearchStats;
62 import com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityStore;
63 
64 import com.google.android.icing.IcingSearchEngine;
65 import com.google.android.icing.proto.DeleteByQueryResultProto;
66 import com.google.android.icing.proto.DeleteResultProto;
67 import com.google.android.icing.proto.DocumentProto;
68 import com.google.android.icing.proto.DocumentStorageInfoProto;
69 import com.google.android.icing.proto.GetAllNamespacesResultProto;
70 import com.google.android.icing.proto.GetOptimizeInfoResultProto;
71 import com.google.android.icing.proto.GetResultProto;
72 import com.google.android.icing.proto.GetResultSpecProto;
73 import com.google.android.icing.proto.GetSchemaResultProto;
74 import com.google.android.icing.proto.IcingSearchEngineOptions;
75 import com.google.android.icing.proto.InitializeResultProto;
76 import com.google.android.icing.proto.NamespaceStorageInfoProto;
77 import com.google.android.icing.proto.OptimizeResultProto;
78 import com.google.android.icing.proto.PersistToDiskResultProto;
79 import com.google.android.icing.proto.PersistType;
80 import com.google.android.icing.proto.PropertyConfigProto;
81 import com.google.android.icing.proto.PutResultProto;
82 import com.google.android.icing.proto.ReportUsageResultProto;
83 import com.google.android.icing.proto.ResetResultProto;
84 import com.google.android.icing.proto.ResultSpecProto;
85 import com.google.android.icing.proto.SchemaProto;
86 import com.google.android.icing.proto.SchemaTypeConfigProto;
87 import com.google.android.icing.proto.ScoringSpecProto;
88 import com.google.android.icing.proto.SearchResultProto;
89 import com.google.android.icing.proto.SearchSpecProto;
90 import com.google.android.icing.proto.SetSchemaResultProto;
91 import com.google.android.icing.proto.StatusProto;
92 import com.google.android.icing.proto.StorageInfoProto;
93 import com.google.android.icing.proto.StorageInfoResultProto;
94 import com.google.android.icing.proto.TypePropertyMask;
95 import com.google.android.icing.proto.UsageReport;
96 
97 import java.io.Closeable;
98 import java.io.File;
99 import java.util.ArrayList;
100 import java.util.Collections;
101 import java.util.HashMap;
102 import java.util.Iterator;
103 import java.util.List;
104 import java.util.Map;
105 import java.util.Objects;
106 import java.util.Set;
107 import java.util.concurrent.locks.ReadWriteLock;
108 import java.util.concurrent.locks.ReentrantReadWriteLock;
109 
110 /**
111  * Manages interaction with the native IcingSearchEngine and other components to implement AppSearch
112  * functionality.
113  *
114  * <p>Never create two instances using the same folder.
115  *
116  * <p>A single instance of {@link AppSearchImpl} can support all packages and databases. This is
117  * done by combining the package and database name into a unique prefix and prefixing the schemas
118  * and documents stored under that owner. Schemas and documents are physically saved together in
119  * {@link IcingSearchEngine}, but logically isolated:
120  *
121  * <ul>
122  *   <li>Rewrite SchemaType in SchemaProto by adding the package-database prefix and save into
123  *       SchemaTypes set in {@link #setSchema}.
124  *   <li>Rewrite namespace and SchemaType in DocumentProto by adding package-database prefix and
125  *       save to namespaces set in {@link #putDocument}.
126  *   <li>Remove package-database prefix when retrieving documents in {@link #getDocument} and {@link
127  *       #query}.
128  *   <li>Rewrite filters in {@link SearchSpecProto} to have all namespaces and schema types of the
129  *       queried database when user using empty filters in {@link #query}.
130  * </ul>
131  *
132  * <p>Methods in this class belong to two groups, the query group and the mutate group.
133  *
134  * <ul>
135  *   <li>All methods are going to modify global parameters and data in Icing are executed under
136  *       WRITE lock to keep thread safety.
137  *   <li>All methods are going to access global parameters or query data from Icing are executed
138  *       under READ lock to improve query performance.
139  * </ul>
140  *
141  * <p>This class is thread safe.
142  *
143  * @hide
144  */
145 @WorkerThread
146 public final class AppSearchImpl implements Closeable {
147     private static final String TAG = "AppSearchImpl";
148 
149     /** A value 0 means that there're no more pages in the search results. */
150     private static final long EMPTY_PAGE_TOKEN = 0;
151 
152     @VisibleForTesting static final int CHECK_OPTIMIZE_INTERVAL = 100;
153 
154     private final ReadWriteLock mReadWriteLock = new ReentrantReadWriteLock();
155     private final LogUtil mLogUtil = new LogUtil(TAG);
156     private final OptimizeStrategy mOptimizeStrategy;
157     private final LimitConfig mLimitConfig;
158 
159     @GuardedBy("mReadWriteLock")
160     @VisibleForTesting
161     final IcingSearchEngine mIcingSearchEngineLocked;
162 
163     // This map contains schema types and SchemaTypeConfigProtos for all package-database
164     // prefixes. It maps each package-database prefix to an inner-map. The inner-map maps each
165     // prefixed schema type to its respective SchemaTypeConfigProto.
166     @GuardedBy("mReadWriteLock")
167     private final Map<String, Map<String, SchemaTypeConfigProto>> mSchemaMapLocked =
168             new ArrayMap<>();
169 
170     // This map contains namespaces for all package-database prefixes. All values in the map are
171     // prefixed with the package-database prefix.
172     // TODO(b/172360376): Check if this can be replaced with an ArrayMap
173     @GuardedBy("mReadWriteLock")
174     private final Map<String, Set<String>> mNamespaceMapLocked = new HashMap<>();
175 
176     /** Maps package name to active document count. */
177     @GuardedBy("mReadWriteLock")
178     private final Map<String, Integer> mDocumentCountMapLocked = new ArrayMap<>();
179 
180     // Maps packages to the set of valid nextPageTokens that the package can manipulate. A token
181     // is unique and constant per query (i.e. the same token '123' is used to iterate through
182     // pages of search results). The tokens themselves are generated and tracked by
183     // IcingSearchEngine. IcingSearchEngine considers a token valid and won't be reused
184     // until we call invalidateNextPageToken on the token.
185     //
186     // Note that we synchronize on itself because the nextPageToken cache is checked at
187     // query-time, and queries are done in parallel with a read lock. Ideally, this would be
188     // guarded by the normal mReadWriteLock.writeLock, but ReentrantReadWriteLocks can't upgrade
189     // read to write locks. This lock should be acquired at the smallest scope possible.
190     // mReadWriteLock is a higher-level lock, so calls shouldn't be made out
191     // to any functions that grab the lock.
192     @GuardedBy("mNextPageTokensLocked")
193     private final Map<String, Set<Long>> mNextPageTokensLocked = new ArrayMap<>();
194 
195     /**
196      * The counter to check when to call {@link #checkForOptimize}. The interval is {@link
197      * #CHECK_OPTIMIZE_INTERVAL}.
198      */
199     @GuardedBy("mReadWriteLock")
200     private int mOptimizeIntervalCountLocked = 0;
201 
202     /** Whether this instance has been closed, and therefore unusable. */
203     @GuardedBy("mReadWriteLock")
204     private boolean mClosedLocked = false;
205 
206     /**
207      * Creates and initializes an instance of {@link AppSearchImpl} which writes data to the given
208      * folder.
209      *
210      * <p>Clients can pass a {@link AppSearchLogger} here through their AppSearchSession, but it
211      * can't be saved inside {@link AppSearchImpl}, because the impl will be shared by all the
212      * sessions for the same package in JetPack.
213      *
214      * <p>Instead, logger instance needs to be passed to each individual method, like create, query
215      * and putDocument.
216      *
217      * @param initStatsBuilder collects stats for initialization if provided.
218      */
219     @NonNull
create( @onNull File icingDir, @NonNull LimitConfig limitConfig, @Nullable InitializeStats.Builder initStatsBuilder, @NonNull OptimizeStrategy optimizeStrategy)220     public static AppSearchImpl create(
221             @NonNull File icingDir,
222             @NonNull LimitConfig limitConfig,
223             @Nullable InitializeStats.Builder initStatsBuilder,
224             @NonNull OptimizeStrategy optimizeStrategy)
225             throws AppSearchException {
226         return new AppSearchImpl(icingDir, limitConfig, initStatsBuilder, optimizeStrategy);
227     }
228 
229     /** @param initStatsBuilder collects stats for initialization if provided. */
AppSearchImpl( @onNull File icingDir, @NonNull LimitConfig limitConfig, @Nullable InitializeStats.Builder initStatsBuilder, @NonNull OptimizeStrategy optimizeStrategy)230     private AppSearchImpl(
231             @NonNull File icingDir,
232             @NonNull LimitConfig limitConfig,
233             @Nullable InitializeStats.Builder initStatsBuilder,
234             @NonNull OptimizeStrategy optimizeStrategy)
235             throws AppSearchException {
236         Objects.requireNonNull(icingDir);
237         mLimitConfig = Objects.requireNonNull(limitConfig);
238         mOptimizeStrategy = Objects.requireNonNull(optimizeStrategy);
239 
240         mReadWriteLock.writeLock().lock();
241         try {
242             // We synchronize here because we don't want to call IcingSearchEngine.initialize() more
243             // than once. It's unnecessary and can be a costly operation.
244             IcingSearchEngineOptions options =
245                     IcingSearchEngineOptions.newBuilder()
246                             .setBaseDir(icingDir.getAbsolutePath())
247                             .build();
248             mLogUtil.piiTrace("Constructing IcingSearchEngine, request", options);
249             mIcingSearchEngineLocked = new IcingSearchEngine(options);
250             mLogUtil.piiTrace(
251                     "Constructing IcingSearchEngine, response",
252                     Objects.hashCode(mIcingSearchEngineLocked));
253 
254             // The core initialization procedure. If any part of this fails, we bail into
255             // resetLocked(), deleting all data (but hopefully allowing AppSearchImpl to come up).
256             try {
257                 mLogUtil.piiTrace("icingSearchEngine.initialize, request");
258                 InitializeResultProto initializeResultProto = mIcingSearchEngineLocked.initialize();
259                 mLogUtil.piiTrace(
260                         "icingSearchEngine.initialize, response",
261                         initializeResultProto.getStatus(),
262                         initializeResultProto);
263 
264                 if (initStatsBuilder != null) {
265                     initStatsBuilder
266                             .setStatusCode(
267                                     statusProtoToResultCode(initializeResultProto.getStatus()))
268                             // TODO(b/173532925) how to get DeSyncs value
269                             .setHasDeSync(false);
270                     AppSearchLoggerHelper.copyNativeStats(
271                             initializeResultProto.getInitializeStats(), initStatsBuilder);
272                 }
273                 checkSuccess(initializeResultProto.getStatus());
274 
275                 // Read all protos we need to construct AppSearchImpl's cache maps
276                 long prepareSchemaAndNamespacesLatencyStartMillis = SystemClock.elapsedRealtime();
277                 SchemaProto schemaProto = getSchemaProtoLocked();
278 
279                 mLogUtil.piiTrace("init:getAllNamespaces, request");
280                 GetAllNamespacesResultProto getAllNamespacesResultProto =
281                         mIcingSearchEngineLocked.getAllNamespaces();
282                 mLogUtil.piiTrace(
283                         "init:getAllNamespaces, response",
284                         getAllNamespacesResultProto.getNamespacesCount(),
285                         getAllNamespacesResultProto);
286 
287                 StorageInfoProto storageInfoProto = getRawStorageInfoProto();
288 
289                 // Log the time it took to read the data that goes into the cache maps
290                 if (initStatsBuilder != null) {
291                     // In case there is some error for getAllNamespaces, we can still
292                     // set the latency for preparation.
293                     // If there is no error, the value will be overridden by the actual one later.
294                     initStatsBuilder
295                             .setStatusCode(
296                                     statusProtoToResultCode(
297                                             getAllNamespacesResultProto.getStatus()))
298                             .setPrepareSchemaAndNamespacesLatencyMillis(
299                                     (int)
300                                             (SystemClock.elapsedRealtime()
301                                                     - prepareSchemaAndNamespacesLatencyStartMillis));
302                 }
303                 checkSuccess(getAllNamespacesResultProto.getStatus());
304 
305                 // Populate schema map
306                 List<SchemaTypeConfigProto> schemaProtoTypesList = schemaProto.getTypesList();
307                 for (int i = 0; i < schemaProtoTypesList.size(); i++) {
308                     SchemaTypeConfigProto schema = schemaProtoTypesList.get(i);
309                     String prefixedSchemaType = schema.getSchemaType();
310                     addToMap(mSchemaMapLocked, getPrefix(prefixedSchemaType), schema);
311                 }
312 
313                 // Populate namespace map
314                 List<String> prefixedNamespaceList =
315                         getAllNamespacesResultProto.getNamespacesList();
316                 for (int i = 0; i < prefixedNamespaceList.size(); i++) {
317                     String prefixedNamespace = prefixedNamespaceList.get(i);
318                     addToMap(mNamespaceMapLocked, getPrefix(prefixedNamespace), prefixedNamespace);
319                 }
320 
321                 // Populate document count map
322                 rebuildDocumentCountMapLocked(storageInfoProto);
323 
324                 // logging prepare_schema_and_namespaces latency
325                 if (initStatsBuilder != null) {
326                     initStatsBuilder.setPrepareSchemaAndNamespacesLatencyMillis(
327                             (int)
328                                     (SystemClock.elapsedRealtime()
329                                             - prepareSchemaAndNamespacesLatencyStartMillis));
330                 }
331 
332                 mLogUtil.piiTrace("Init completed successfully");
333 
334             } catch (AppSearchException e) {
335                 // Some error. Reset and see if it fixes it.
336                 Log.e(TAG, "Error initializing, resetting IcingSearchEngine.", e);
337                 if (initStatsBuilder != null) {
338                     initStatsBuilder.setStatusCode(e.getResultCode());
339                 }
340                 resetLocked(initStatsBuilder);
341             }
342 
343         } finally {
344             mReadWriteLock.writeLock().unlock();
345         }
346     }
347 
348     @GuardedBy("mReadWriteLock")
throwIfClosedLocked()349     private void throwIfClosedLocked() {
350         if (mClosedLocked) {
351             throw new IllegalStateException("Trying to use a closed AppSearchImpl instance.");
352         }
353     }
354 
355     /**
356      * Persists data to disk and closes the instance.
357      *
358      * <p>This instance is no longer usable after it's been closed. Call {@link #create} to create a
359      * new, usable instance.
360      */
361     @Override
close()362     public void close() {
363         mReadWriteLock.writeLock().lock();
364         try {
365             if (mClosedLocked) {
366                 return;
367             }
368             persistToDisk(PersistType.Code.FULL);
369             mLogUtil.piiTrace("icingSearchEngine.close, request");
370             mIcingSearchEngineLocked.close();
371             mLogUtil.piiTrace("icingSearchEngine.close, response");
372             mClosedLocked = true;
373         } catch (AppSearchException e) {
374             Log.w(TAG, "Error when closing AppSearchImpl.", e);
375         } finally {
376             mReadWriteLock.writeLock().unlock();
377         }
378     }
379 
380     /**
381      * Updates the AppSearch schema for this app.
382      *
383      * <p>This method belongs to mutate group.
384      *
385      * @param packageName The package name that owns the schemas.
386      * @param databaseName The name of the database where this schema lives.
387      * @param schemas Schemas to set for this app.
388      * @param visibilityStore If set, {@code schemasNotDisplayedBySystem} and {@code
389      *     schemasVisibleToPackages} will be saved here if the schema is successfully applied.
390      * @param schemasNotDisplayedBySystem Schema types that should not be surfaced on platform
391      *     surfaces.
392      * @param schemasVisibleToPackages Schema types that are visible to the specified packages.
393      * @param forceOverride Whether to force-apply the schema even if it is incompatible. Documents
394      *     which do not comply with the new schema will be deleted.
395      * @param version The overall version number of the request.
396      * @return The response contains deleted schema types and incompatible schema types of this
397      *     call.
398      * @throws AppSearchException On IcingSearchEngine error. If the status code is
399      *     FAILED_PRECONDITION for the incompatible change, the exception will be converted to the
400      *     SetSchemaResponse.
401      */
402     @NonNull
setSchema( @onNull String packageName, @NonNull String databaseName, @NonNull List<AppSearchSchema> schemas, @Nullable VisibilityStore visibilityStore, @NonNull List<String> schemasNotDisplayedBySystem, @NonNull Map<String, List<PackageIdentifier>> schemasVisibleToPackages, boolean forceOverride, int version)403     public SetSchemaResponse setSchema(
404             @NonNull String packageName,
405             @NonNull String databaseName,
406             @NonNull List<AppSearchSchema> schemas,
407             @Nullable VisibilityStore visibilityStore,
408             @NonNull List<String> schemasNotDisplayedBySystem,
409             @NonNull Map<String, List<PackageIdentifier>> schemasVisibleToPackages,
410             boolean forceOverride,
411             int version)
412             throws AppSearchException {
413         mReadWriteLock.writeLock().lock();
414         try {
415             throwIfClosedLocked();
416 
417             SchemaProto.Builder existingSchemaBuilder = getSchemaProtoLocked().toBuilder();
418 
419             SchemaProto.Builder newSchemaBuilder = SchemaProto.newBuilder();
420             for (int i = 0; i < schemas.size(); i++) {
421                 AppSearchSchema schema = schemas.get(i);
422                 SchemaTypeConfigProto schemaTypeProto =
423                         SchemaToProtoConverter.toSchemaTypeConfigProto(schema, version);
424                 newSchemaBuilder.addTypes(schemaTypeProto);
425             }
426 
427             String prefix = createPrefix(packageName, databaseName);
428             // Combine the existing schema (which may have types from other prefixes) with this
429             // prefix's new schema. Modifies the existingSchemaBuilder.
430             RewrittenSchemaResults rewrittenSchemaResults =
431                     rewriteSchema(prefix, existingSchemaBuilder, newSchemaBuilder.build());
432 
433             // Apply schema
434             SchemaProto finalSchema = existingSchemaBuilder.build();
435             mLogUtil.piiTrace("setSchema, request", finalSchema.getTypesCount(), finalSchema);
436             SetSchemaResultProto setSchemaResultProto =
437                     mIcingSearchEngineLocked.setSchema(finalSchema, forceOverride);
438             mLogUtil.piiTrace(
439                     "setSchema, response", setSchemaResultProto.getStatus(), setSchemaResultProto);
440 
441             // Determine whether it succeeded.
442             try {
443                 checkSuccess(setSchemaResultProto.getStatus());
444             } catch (AppSearchException e) {
445                 // Swallow the exception for the incompatible change case. We will propagate
446                 // those deleted schemas and incompatible types to the SetSchemaResponse.
447                 boolean isFailedPrecondition =
448                         setSchemaResultProto.getStatus().getCode()
449                                 == StatusProto.Code.FAILED_PRECONDITION;
450                 boolean isIncompatible =
451                         setSchemaResultProto.getDeletedSchemaTypesCount() > 0
452                                 || setSchemaResultProto.getIncompatibleSchemaTypesCount() > 0;
453                 if (isFailedPrecondition && isIncompatible) {
454                     return SetSchemaResponseToProtoConverter.toSetSchemaResponse(
455                             setSchemaResultProto, prefix);
456                 } else {
457                     throw e;
458                 }
459             }
460 
461             // Update derived data structures.
462             for (SchemaTypeConfigProto schemaTypeConfigProto :
463                     rewrittenSchemaResults.mRewrittenPrefixedTypes.values()) {
464                 addToMap(mSchemaMapLocked, prefix, schemaTypeConfigProto);
465             }
466 
467             for (String schemaType : rewrittenSchemaResults.mDeletedPrefixedTypes) {
468                 removeFromMap(mSchemaMapLocked, prefix, schemaType);
469             }
470 
471             if (visibilityStore != null) {
472                 Set<String> prefixedSchemasNotDisplayedBySystem =
473                         new ArraySet<>(schemasNotDisplayedBySystem.size());
474                 for (int i = 0; i < schemasNotDisplayedBySystem.size(); i++) {
475                     prefixedSchemasNotDisplayedBySystem.add(
476                             prefix + schemasNotDisplayedBySystem.get(i));
477                 }
478 
479                 Map<String, List<PackageIdentifier>> prefixedSchemasVisibleToPackages =
480                         new ArrayMap<>(schemasVisibleToPackages.size());
481                 for (Map.Entry<String, List<PackageIdentifier>> entry :
482                         schemasVisibleToPackages.entrySet()) {
483                     prefixedSchemasVisibleToPackages.put(prefix + entry.getKey(), entry.getValue());
484                 }
485 
486                 visibilityStore.setVisibility(
487                         packageName,
488                         databaseName,
489                         prefixedSchemasNotDisplayedBySystem,
490                         prefixedSchemasVisibleToPackages);
491             }
492 
493             return SetSchemaResponseToProtoConverter.toSetSchemaResponse(
494                     setSchemaResultProto, prefix);
495         } finally {
496             mReadWriteLock.writeLock().unlock();
497         }
498     }
499 
500     /**
501      * Retrieves the AppSearch schema for this package name, database.
502      *
503      * <p>This method belongs to query group.
504      *
505      * @param packageName Package name that owns this schema
506      * @param databaseName The name of the database where this schema lives.
507      * @throws AppSearchException on IcingSearchEngine error.
508      */
509     @NonNull
getSchema(@onNull String packageName, @NonNull String databaseName)510     public GetSchemaResponse getSchema(@NonNull String packageName, @NonNull String databaseName)
511             throws AppSearchException {
512         mReadWriteLock.readLock().lock();
513         try {
514             throwIfClosedLocked();
515 
516             SchemaProto fullSchema = getSchemaProtoLocked();
517 
518             String prefix = createPrefix(packageName, databaseName);
519             GetSchemaResponse.Builder responseBuilder = new GetSchemaResponse.Builder();
520             for (int i = 0; i < fullSchema.getTypesCount(); i++) {
521                 String typePrefix = getPrefix(fullSchema.getTypes(i).getSchemaType());
522                 if (!prefix.equals(typePrefix)) {
523                     continue;
524                 }
525                 // Rewrite SchemaProto.types.schema_type
526                 SchemaTypeConfigProto.Builder typeConfigBuilder =
527                         fullSchema.getTypes(i).toBuilder();
528                 String newSchemaType = typeConfigBuilder.getSchemaType().substring(prefix.length());
529                 typeConfigBuilder.setSchemaType(newSchemaType);
530 
531                 // Rewrite SchemaProto.types.properties.schema_type
532                 for (int propertyIdx = 0;
533                         propertyIdx < typeConfigBuilder.getPropertiesCount();
534                         propertyIdx++) {
535                     PropertyConfigProto.Builder propertyConfigBuilder =
536                             typeConfigBuilder.getProperties(propertyIdx).toBuilder();
537                     if (!propertyConfigBuilder.getSchemaType().isEmpty()) {
538                         String newPropertySchemaType =
539                                 propertyConfigBuilder.getSchemaType().substring(prefix.length());
540                         propertyConfigBuilder.setSchemaType(newPropertySchemaType);
541                         typeConfigBuilder.setProperties(propertyIdx, propertyConfigBuilder);
542                     }
543                 }
544 
545                 AppSearchSchema schema =
546                         SchemaToProtoConverter.toAppSearchSchema(typeConfigBuilder);
547 
548                 // TODO(b/183050495) find a place to store the version for the database, rather
549                 // than read from a schema.
550                 responseBuilder.setVersion(fullSchema.getTypes(i).getVersion());
551                 responseBuilder.addSchema(schema);
552             }
553             return responseBuilder.build();
554         } finally {
555             mReadWriteLock.readLock().unlock();
556         }
557     }
558 
559     /**
560      * Retrieves the list of namespaces with at least one document for this package name, database.
561      *
562      * <p>This method belongs to query group.
563      *
564      * @param packageName Package name that owns this schema
565      * @param databaseName The name of the database where this schema lives.
566      * @throws AppSearchException on IcingSearchEngine error.
567      */
568     @NonNull
getNamespaces(@onNull String packageName, @NonNull String databaseName)569     public List<String> getNamespaces(@NonNull String packageName, @NonNull String databaseName)
570             throws AppSearchException {
571         mReadWriteLock.readLock().lock();
572         try {
573             throwIfClosedLocked();
574             mLogUtil.piiTrace("getAllNamespaces, request");
575             // We can't just use mNamespaceMap here because we have no way to prune namespaces from
576             // mNamespaceMap when they have no more documents (e.g. after setting schema to empty or
577             // using deleteByQuery).
578             GetAllNamespacesResultProto getAllNamespacesResultProto =
579                     mIcingSearchEngineLocked.getAllNamespaces();
580             mLogUtil.piiTrace(
581                     "getAllNamespaces, response",
582                     getAllNamespacesResultProto.getNamespacesCount(),
583                     getAllNamespacesResultProto);
584             checkSuccess(getAllNamespacesResultProto.getStatus());
585             String prefix = createPrefix(packageName, databaseName);
586             List<String> results = new ArrayList<>();
587             for (int i = 0; i < getAllNamespacesResultProto.getNamespacesCount(); i++) {
588                 String prefixedNamespace = getAllNamespacesResultProto.getNamespaces(i);
589                 if (prefixedNamespace.startsWith(prefix)) {
590                     results.add(prefixedNamespace.substring(prefix.length()));
591                 }
592             }
593             return results;
594         } finally {
595             mReadWriteLock.readLock().unlock();
596         }
597     }
598 
599     /**
600      * Adds a document to the AppSearch index.
601      *
602      * <p>This method belongs to mutate group.
603      *
604      * @param packageName The package name that owns this document.
605      * @param databaseName The databaseName this document resides in.
606      * @param document The document to index.
607      * @throws AppSearchException on IcingSearchEngine error.
608      */
putDocument( @onNull String packageName, @NonNull String databaseName, @NonNull GenericDocument document, @Nullable AppSearchLogger logger)609     public void putDocument(
610             @NonNull String packageName,
611             @NonNull String databaseName,
612             @NonNull GenericDocument document,
613             @Nullable AppSearchLogger logger)
614             throws AppSearchException {
615         PutDocumentStats.Builder pStatsBuilder = null;
616         if (logger != null) {
617             pStatsBuilder = new PutDocumentStats.Builder(packageName, databaseName);
618         }
619         long totalStartTimeMillis = SystemClock.elapsedRealtime();
620 
621         mReadWriteLock.writeLock().lock();
622         try {
623             throwIfClosedLocked();
624 
625             // Generate Document Proto
626             long generateDocumentProtoStartTimeMillis = SystemClock.elapsedRealtime();
627             DocumentProto.Builder documentBuilder =
628                     GenericDocumentToProtoConverter.toDocumentProto(document).toBuilder();
629             long generateDocumentProtoEndTimeMillis = SystemClock.elapsedRealtime();
630 
631             // Rewrite Document Type
632             long rewriteDocumentTypeStartTimeMillis = SystemClock.elapsedRealtime();
633             String prefix = createPrefix(packageName, databaseName);
634             addPrefixToDocument(documentBuilder, prefix);
635             long rewriteDocumentTypeEndTimeMillis = SystemClock.elapsedRealtime();
636             DocumentProto finalDocument = documentBuilder.build();
637 
638             // Check limits
639             int newDocumentCount =
640                     enforceLimitConfigLocked(
641                             packageName, finalDocument.getUri(), finalDocument.getSerializedSize());
642 
643             // Insert document
644             mLogUtil.piiTrace("putDocument, request", finalDocument.getUri(), finalDocument);
645             PutResultProto putResultProto = mIcingSearchEngineLocked.put(finalDocument);
646             mLogUtil.piiTrace("putDocument, response", putResultProto.getStatus(), putResultProto);
647 
648             // Update caches
649             addToMap(mNamespaceMapLocked, prefix, finalDocument.getNamespace());
650             mDocumentCountMapLocked.put(packageName, newDocumentCount);
651 
652             // Logging stats
653             if (pStatsBuilder != null) {
654                 pStatsBuilder
655                         .setStatusCode(statusProtoToResultCode(putResultProto.getStatus()))
656                         .setGenerateDocumentProtoLatencyMillis(
657                                 (int)
658                                         (generateDocumentProtoEndTimeMillis
659                                                 - generateDocumentProtoStartTimeMillis))
660                         .setRewriteDocumentTypesLatencyMillis(
661                                 (int)
662                                         (rewriteDocumentTypeEndTimeMillis
663                                                 - rewriteDocumentTypeStartTimeMillis));
664                 AppSearchLoggerHelper.copyNativeStats(
665                         putResultProto.getPutDocumentStats(), pStatsBuilder);
666             }
667 
668             checkSuccess(putResultProto.getStatus());
669         } finally {
670             mReadWriteLock.writeLock().unlock();
671 
672             if (logger != null) {
673                 long totalEndTimeMillis = SystemClock.elapsedRealtime();
674                 pStatsBuilder.setTotalLatencyMillis(
675                         (int) (totalEndTimeMillis - totalStartTimeMillis));
676                 logger.logStats(pStatsBuilder.build());
677             }
678         }
679     }
680 
681     /**
682      * Checks that a new document can be added to the given packageName with the given serialized
683      * size without violating our {@link LimitConfig}.
684      *
685      * @return the new count of documents for the given package, including the new document.
686      * @throws AppSearchException with a code of {@link AppSearchResult#RESULT_OUT_OF_SPACE} if the
687      *     limits are violated by the new document.
688      */
689     @GuardedBy("mReadWriteLock")
enforceLimitConfigLocked(String packageName, String newDocUri, int newDocSize)690     private int enforceLimitConfigLocked(String packageName, String newDocUri, int newDocSize)
691             throws AppSearchException {
692         // Limits check: size of document
693         if (newDocSize > mLimitConfig.getMaxDocumentSizeBytes()) {
694             throw new AppSearchException(
695                     AppSearchResult.RESULT_OUT_OF_SPACE,
696                     "Document \""
697                             + newDocUri
698                             + "\" for package \""
699                             + packageName
700                             + "\" serialized to "
701                             + newDocSize
702                             + " bytes, which exceeds "
703                             + "limit of "
704                             + mLimitConfig.getMaxDocumentSizeBytes()
705                             + " bytes");
706         }
707 
708         // Limits check: number of documents
709         Integer oldDocumentCount = mDocumentCountMapLocked.get(packageName);
710         int newDocumentCount;
711         if (oldDocumentCount == null) {
712             newDocumentCount = 1;
713         } else {
714             newDocumentCount = oldDocumentCount + 1;
715         }
716         if (newDocumentCount > mLimitConfig.getMaxDocumentCount()) {
717             // Our management of mDocumentCountMapLocked doesn't account for document
718             // replacements, so our counter might have overcounted if the app has replaced docs.
719             // Rebuild the counter from StorageInfo in case this is so.
720             // TODO(b/170371356):  If Icing lib exposes something in the result which says
721             //  whether the document was a replacement, we could subtract 1 again after the put
722             //  to keep the count accurate. That would allow us to remove this code.
723             rebuildDocumentCountMapLocked(getRawStorageInfoProto());
724             oldDocumentCount = mDocumentCountMapLocked.get(packageName);
725             if (oldDocumentCount == null) {
726                 newDocumentCount = 1;
727             } else {
728                 newDocumentCount = oldDocumentCount + 1;
729             }
730         }
731         if (newDocumentCount > mLimitConfig.getMaxDocumentCount()) {
732             // Now we really can't fit it in, even accounting for replacements.
733             throw new AppSearchException(
734                     AppSearchResult.RESULT_OUT_OF_SPACE,
735                     "Package \""
736                             + packageName
737                             + "\" exceeded limit of "
738                             + mLimitConfig.getMaxDocumentCount()
739                             + " documents. Some documents "
740                             + "must be removed to index additional ones.");
741         }
742 
743         return newDocumentCount;
744     }
745 
746     /**
747      * Retrieves a document from the AppSearch index by namespace and document ID.
748      *
749      * <p>This method belongs to query group.
750      *
751      * @param packageName The package that owns this document.
752      * @param databaseName The databaseName this document resides in.
753      * @param namespace The namespace this document resides in.
754      * @param id The ID of the document to get.
755      * @param typePropertyPaths A map of schema type to a list of property paths to return in the
756      *     result.
757      * @return The Document contents
758      * @throws AppSearchException on IcingSearchEngine error.
759      */
760     @NonNull
getDocument( @onNull String packageName, @NonNull String databaseName, @NonNull String namespace, @NonNull String id, @NonNull Map<String, List<String>> typePropertyPaths)761     public GenericDocument getDocument(
762             @NonNull String packageName,
763             @NonNull String databaseName,
764             @NonNull String namespace,
765             @NonNull String id,
766             @NonNull Map<String, List<String>> typePropertyPaths)
767             throws AppSearchException {
768         mReadWriteLock.readLock().lock();
769         try {
770             throwIfClosedLocked();
771             String prefix = createPrefix(packageName, databaseName);
772             List<TypePropertyMask> nonPrefixedPropertyMasks =
773                     TypePropertyPathToProtoConverter.toTypePropertyMaskList(typePropertyPaths);
774             List<TypePropertyMask> prefixedPropertyMasks =
775                     new ArrayList<>(nonPrefixedPropertyMasks.size());
776             for (int i = 0; i < nonPrefixedPropertyMasks.size(); ++i) {
777                 TypePropertyMask typePropertyMask = nonPrefixedPropertyMasks.get(i);
778                 String nonPrefixedType = typePropertyMask.getSchemaType();
779                 String prefixedType =
780                         nonPrefixedType.equals(
781                                         GetByDocumentIdRequest.PROJECTION_SCHEMA_TYPE_WILDCARD)
782                                 ? nonPrefixedType
783                                 : prefix + nonPrefixedType;
784                 prefixedPropertyMasks.add(
785                         typePropertyMask.toBuilder().setSchemaType(prefixedType).build());
786             }
787             GetResultSpecProto getResultSpec =
788                     GetResultSpecProto.newBuilder()
789                             .addAllTypePropertyMasks(prefixedPropertyMasks)
790                             .build();
791 
792             String finalNamespace = createPrefix(packageName, databaseName) + namespace;
793             if (mLogUtil.isPiiTraceEnabled()) {
794                 mLogUtil.piiTrace(
795                         "getDocument, request", finalNamespace + ", " + id + "," + getResultSpec);
796             }
797             GetResultProto getResultProto =
798                     mIcingSearchEngineLocked.get(finalNamespace, id, getResultSpec);
799             mLogUtil.piiTrace("getDocument, response", getResultProto.getStatus(), getResultProto);
800             checkSuccess(getResultProto.getStatus());
801 
802             // The schema type map cannot be null at this point. It could only be null if no
803             // schema had ever been set for that prefix. Given we have retrieved a document from
804             // the index, we know a schema had to have been set.
805             Map<String, SchemaTypeConfigProto> schemaTypeMap = mSchemaMapLocked.get(prefix);
806             DocumentProto.Builder documentBuilder = getResultProto.getDocument().toBuilder();
807             removePrefixesFromDocument(documentBuilder);
808             return GenericDocumentToProtoConverter.toGenericDocument(
809                     documentBuilder.build(), prefix, schemaTypeMap);
810         } finally {
811             mReadWriteLock.readLock().unlock();
812         }
813     }
814 
815     /**
816      * Executes a query against the AppSearch index and returns results.
817      *
818      * <p>This method belongs to query group.
819      *
820      * @param packageName The package name that is performing the query.
821      * @param databaseName The databaseName this query for.
822      * @param queryExpression Query String to search.
823      * @param searchSpec Spec for setting filters, raw query etc.
824      * @param logger logger to collect query stats
825      * @return The results of performing this search. It may contain an empty list of results if no
826      *     documents matched the query.
827      * @throws AppSearchException on IcingSearchEngine error.
828      */
829     @NonNull
query( @onNull String packageName, @NonNull String databaseName, @NonNull String queryExpression, @NonNull SearchSpec searchSpec, @Nullable AppSearchLogger logger)830     public SearchResultPage query(
831             @NonNull String packageName,
832             @NonNull String databaseName,
833             @NonNull String queryExpression,
834             @NonNull SearchSpec searchSpec,
835             @Nullable AppSearchLogger logger)
836             throws AppSearchException {
837         long totalLatencyStartMillis = SystemClock.elapsedRealtime();
838         SearchStats.Builder sStatsBuilder = null;
839         if (logger != null) {
840             sStatsBuilder =
841                     new SearchStats.Builder(SearchStats.VISIBILITY_SCOPE_LOCAL, packageName)
842                             .setDatabase(databaseName);
843         }
844 
845         mReadWriteLock.readLock().lock();
846         try {
847             throwIfClosedLocked();
848 
849             List<String> filterPackageNames = searchSpec.getFilterPackageNames();
850             if (!filterPackageNames.isEmpty() && !filterPackageNames.contains(packageName)) {
851                 // Client wanted to query over some packages that weren't its own. This isn't
852                 // allowed through local query so we can return early with no results.
853                 if (logger != null) {
854                     sStatsBuilder.setStatusCode(AppSearchResult.RESULT_SECURITY_ERROR);
855                 }
856                 return new SearchResultPage(Bundle.EMPTY);
857             }
858 
859             String prefix = createPrefix(packageName, databaseName);
860             Set<String> allowedPrefixedSchemas = getAllowedPrefixSchemasLocked(prefix, searchSpec);
861 
862             SearchResultPage searchResultPage =
863                     doQueryLocked(
864                             Collections.singleton(createPrefix(packageName, databaseName)),
865                             allowedPrefixedSchemas,
866                             queryExpression,
867                             searchSpec,
868                             sStatsBuilder);
869             addNextPageToken(packageName, searchResultPage.getNextPageToken());
870             return searchResultPage;
871         } finally {
872             mReadWriteLock.readLock().unlock();
873             if (logger != null) {
874                 sStatsBuilder.setTotalLatencyMillis(
875                         (int) (SystemClock.elapsedRealtime() - totalLatencyStartMillis));
876                 logger.logStats(sStatsBuilder.build());
877             }
878         }
879     }
880 
881     /**
882      * Executes a global query, i.e. over all permitted prefixes, against the AppSearch index and
883      * returns results.
884      *
885      * <p>This method belongs to query group.
886      *
887      * @param queryExpression Query String to search.
888      * @param searchSpec Spec for setting filters, raw query etc.
889      * @param callerPackageName Package name of the caller, should belong to the {@code
890      *     callerUserHandle}.
891      * @param visibilityStore Optional visibility store to obtain system and package visibility
892      *     settings from
893      * @param callerUid UID of the client making the globalQuery call.
894      * @param callerHasSystemAccess Whether the caller has been positively identified as having
895      *     access to schemas marked system surfaceable.
896      * @param logger logger to collect globalQuery stats
897      * @return The results of performing this search. It may contain an empty list of results if no
898      *     documents matched the query.
899      * @throws AppSearchException on IcingSearchEngine error.
900      */
901     @NonNull
globalQuery( @onNull String queryExpression, @NonNull SearchSpec searchSpec, @NonNull String callerPackageName, @Nullable VisibilityStore visibilityStore, int callerUid, boolean callerHasSystemAccess, @Nullable AppSearchLogger logger)902     public SearchResultPage globalQuery(
903             @NonNull String queryExpression,
904             @NonNull SearchSpec searchSpec,
905             @NonNull String callerPackageName,
906             @Nullable VisibilityStore visibilityStore,
907             int callerUid,
908             boolean callerHasSystemAccess,
909             @Nullable AppSearchLogger logger)
910             throws AppSearchException {
911         long totalLatencyStartMillis = SystemClock.elapsedRealtime();
912         SearchStats.Builder sStatsBuilder = null;
913         if (logger != null) {
914             sStatsBuilder =
915                     new SearchStats.Builder(SearchStats.VISIBILITY_SCOPE_GLOBAL, callerPackageName);
916         }
917 
918         mReadWriteLock.readLock().lock();
919         try {
920             throwIfClosedLocked();
921 
922             // Convert package filters to prefix filters
923             Set<String> packageFilters = new ArraySet<>(searchSpec.getFilterPackageNames());
924             Set<String> prefixFilters = new ArraySet<>();
925             if (packageFilters.isEmpty()) {
926                 // Client didn't restrict their search over packages. Try to query over all
927                 // packages/prefixes
928                 prefixFilters = mNamespaceMapLocked.keySet();
929             } else {
930                 // Client did restrict their search over packages. Only include the prefixes that
931                 // belong to the specified packages.
932                 for (String prefix : mNamespaceMapLocked.keySet()) {
933                     String packageName = getPackageName(prefix);
934                     if (packageFilters.contains(packageName)) {
935                         prefixFilters.add(prefix);
936                     }
937                 }
938             }
939 
940             // Convert schema filters to prefixed schema filters
941             ArraySet<String> prefixedSchemaFilters = new ArraySet<>();
942             for (String prefix : prefixFilters) {
943                 List<String> schemaFilters = searchSpec.getFilterSchemas();
944                 if (schemaFilters.isEmpty()) {
945                     // Client didn't specify certain schemas to search over, check all schemas
946                     prefixedSchemaFilters.addAll(mSchemaMapLocked.get(prefix).keySet());
947                 } else {
948                     // Client specified some schemas to search over, check each one
949                     for (int i = 0; i < schemaFilters.size(); i++) {
950                         prefixedSchemaFilters.add(prefix + schemaFilters.get(i));
951                     }
952                 }
953             }
954 
955             // Remove the schemas the client is not allowed to search over
956             Iterator<String> prefixedSchemaIt = prefixedSchemaFilters.iterator();
957             while (prefixedSchemaIt.hasNext()) {
958                 String prefixedSchema = prefixedSchemaIt.next();
959                 String packageName = getPackageName(prefixedSchema);
960 
961                 boolean allow;
962                 if (packageName.equals(callerPackageName)) {
963                     // Callers can always retrieve their own data
964                     allow = true;
965                 } else if (visibilityStore == null) {
966                     // If there's no visibility store, there's no extra access
967                     allow = false;
968                 } else {
969                     String databaseName = getDatabaseName(prefixedSchema);
970                     allow =
971                             visibilityStore.isSchemaSearchableByCaller(
972                                     packageName,
973                                     databaseName,
974                                     prefixedSchema,
975                                     callerUid,
976                                     callerHasSystemAccess);
977                 }
978 
979                 if (!allow) {
980                     prefixedSchemaIt.remove();
981                 }
982             }
983 
984             SearchResultPage searchResultPage =
985                     doQueryLocked(
986                             prefixFilters,
987                             prefixedSchemaFilters,
988                             queryExpression,
989                             searchSpec,
990                             sStatsBuilder);
991             addNextPageToken(callerPackageName, searchResultPage.getNextPageToken());
992             return searchResultPage;
993         } finally {
994             mReadWriteLock.readLock().unlock();
995 
996             if (logger != null) {
997                 sStatsBuilder.setTotalLatencyMillis(
998                         (int) (SystemClock.elapsedRealtime() - totalLatencyStartMillis));
999                 logger.logStats(sStatsBuilder.build());
1000             }
1001         }
1002     }
1003 
1004     /**
1005      * Returns a mapping of package names to all the databases owned by that package.
1006      *
1007      * <p>This method is inefficient to call repeatedly.
1008      */
1009     @NonNull
getPackageToDatabases()1010     public Map<String, Set<String>> getPackageToDatabases() {
1011         mReadWriteLock.readLock().lock();
1012         try {
1013             Map<String, Set<String>> packageToDatabases = new ArrayMap<>();
1014             for (String prefix : mSchemaMapLocked.keySet()) {
1015                 String packageName = getPackageName(prefix);
1016 
1017                 Set<String> databases = packageToDatabases.get(packageName);
1018                 if (databases == null) {
1019                     databases = new ArraySet<>();
1020                     packageToDatabases.put(packageName, databases);
1021                 }
1022 
1023                 String databaseName = getDatabaseName(prefix);
1024                 databases.add(databaseName);
1025             }
1026 
1027             return packageToDatabases;
1028         } finally {
1029             mReadWriteLock.readLock().unlock();
1030         }
1031     }
1032 
1033     @GuardedBy("mReadWriteLock")
doQueryLocked( @onNull Set<String> prefixes, @NonNull Set<String> allowedPrefixedSchemas, @NonNull String queryExpression, @NonNull SearchSpec searchSpec, @Nullable SearchStats.Builder sStatsBuilder)1034     private SearchResultPage doQueryLocked(
1035             @NonNull Set<String> prefixes,
1036             @NonNull Set<String> allowedPrefixedSchemas,
1037             @NonNull String queryExpression,
1038             @NonNull SearchSpec searchSpec,
1039             @Nullable SearchStats.Builder sStatsBuilder)
1040             throws AppSearchException {
1041         long rewriteSearchSpecLatencyStartMillis = SystemClock.elapsedRealtime();
1042 
1043         SearchSpecProto.Builder searchSpecBuilder =
1044                 SearchSpecToProtoConverter.toSearchSpecProto(searchSpec).toBuilder()
1045                         .setQuery(queryExpression);
1046         // rewriteSearchSpecForPrefixesLocked will return false if there is nothing to search
1047         // over given their search filters, so we can return an empty SearchResult and skip
1048         // sending request to Icing.
1049         if (!rewriteSearchSpecForPrefixesLocked(
1050                 searchSpecBuilder, prefixes, allowedPrefixedSchemas)) {
1051             if (sStatsBuilder != null) {
1052                 sStatsBuilder.setRewriteSearchSpecLatencyMillis(
1053                         (int)
1054                                 (SystemClock.elapsedRealtime()
1055                                         - rewriteSearchSpecLatencyStartMillis));
1056             }
1057             return new SearchResultPage(Bundle.EMPTY);
1058         }
1059 
1060         // rewriteSearchSpec, rewriteResultSpec and convertScoringSpec are all counted in
1061         // rewriteSearchSpecLatencyMillis
1062         ResultSpecProto.Builder resultSpecBuilder =
1063                 SearchSpecToProtoConverter.toResultSpecProto(searchSpec).toBuilder();
1064 
1065         int groupingType = searchSpec.getResultGroupingTypeFlags();
1066         if ((groupingType & SearchSpec.GROUPING_TYPE_PER_PACKAGE) != 0
1067                 && (groupingType & SearchSpec.GROUPING_TYPE_PER_NAMESPACE) != 0) {
1068             addPerPackagePerNamespaceResultGroupingsLocked(
1069                     resultSpecBuilder, prefixes, searchSpec.getResultGroupingLimit());
1070         } else if ((groupingType & SearchSpec.GROUPING_TYPE_PER_PACKAGE) != 0) {
1071             addPerPackageResultGroupingsLocked(
1072                     resultSpecBuilder, prefixes, searchSpec.getResultGroupingLimit());
1073         } else if ((groupingType & SearchSpec.GROUPING_TYPE_PER_NAMESPACE) != 0) {
1074             addPerNamespaceResultGroupingsLocked(
1075                     resultSpecBuilder, prefixes, searchSpec.getResultGroupingLimit());
1076         }
1077 
1078         rewriteResultSpecForPrefixesLocked(resultSpecBuilder, prefixes, allowedPrefixedSchemas);
1079         ScoringSpecProto scoringSpec = SearchSpecToProtoConverter.toScoringSpecProto(searchSpec);
1080         SearchSpecProto finalSearchSpec = searchSpecBuilder.build();
1081         ResultSpecProto finalResultSpec = resultSpecBuilder.build();
1082 
1083         long rewriteSearchSpecLatencyEndMillis = SystemClock.elapsedRealtime();
1084 
1085         if (mLogUtil.isPiiTraceEnabled()) {
1086             mLogUtil.piiTrace(
1087                     "search, request",
1088                     finalSearchSpec.getQuery(),
1089                     finalSearchSpec + ", " + scoringSpec + ", " + finalResultSpec);
1090         }
1091         SearchResultProto searchResultProto =
1092                 mIcingSearchEngineLocked.search(finalSearchSpec, scoringSpec, finalResultSpec);
1093         mLogUtil.piiTrace(
1094                 "search, response", searchResultProto.getResultsCount(), searchResultProto);
1095 
1096         if (sStatsBuilder != null) {
1097             sStatsBuilder
1098                     .setStatusCode(statusProtoToResultCode(searchResultProto.getStatus()))
1099                     .setRewriteSearchSpecLatencyMillis(
1100                             (int)
1101                                     (rewriteSearchSpecLatencyEndMillis
1102                                             - rewriteSearchSpecLatencyStartMillis));
1103             AppSearchLoggerHelper.copyNativeStats(searchResultProto.getQueryStats(), sStatsBuilder);
1104         }
1105 
1106         checkSuccess(searchResultProto.getStatus());
1107 
1108         long rewriteSearchResultLatencyStartMillis = SystemClock.elapsedRealtime();
1109         SearchResultPage resultPage = rewriteSearchResultProto(searchResultProto, mSchemaMapLocked);
1110         if (sStatsBuilder != null) {
1111             sStatsBuilder.setRewriteSearchResultLatencyMillis(
1112                     (int) (SystemClock.elapsedRealtime() - rewriteSearchResultLatencyStartMillis));
1113         }
1114 
1115         return resultPage;
1116     }
1117 
1118     /**
1119      * Fetches the next page of results of a previously executed query. Results can be empty if
1120      * next-page token is invalid or all pages have been returned.
1121      *
1122      * <p>This method belongs to query group.
1123      *
1124      * @param packageName Package name of the caller.
1125      * @param nextPageToken The token of pre-loaded results of previously executed query.
1126      * @return The next page of results of previously executed query.
1127      * @throws AppSearchException on IcingSearchEngine error or if can't advance on nextPageToken.
1128      */
1129     @NonNull
getNextPage(@onNull String packageName, long nextPageToken)1130     public SearchResultPage getNextPage(@NonNull String packageName, long nextPageToken)
1131             throws AppSearchException {
1132         mReadWriteLock.readLock().lock();
1133         try {
1134             throwIfClosedLocked();
1135 
1136             mLogUtil.piiTrace("getNextPage, request", nextPageToken);
1137             checkNextPageToken(packageName, nextPageToken);
1138             SearchResultProto searchResultProto =
1139                     mIcingSearchEngineLocked.getNextPage(nextPageToken);
1140             mLogUtil.piiTrace(
1141                     "getNextPage, response",
1142                     searchResultProto.getResultsCount(),
1143                     searchResultProto);
1144             checkSuccess(searchResultProto.getStatus());
1145             if (nextPageToken != EMPTY_PAGE_TOKEN
1146                     && searchResultProto.getNextPageToken() == EMPTY_PAGE_TOKEN) {
1147                 // At this point, we're guaranteed that this nextPageToken exists for this package,
1148                 // otherwise checkNextPageToken would've thrown an exception.
1149                 // Since the new token is 0, this is the last page. We should remove the old token
1150                 // from our cache since it no longer refers to this query.
1151                 synchronized (mNextPageTokensLocked) {
1152                     mNextPageTokensLocked.get(packageName).remove(nextPageToken);
1153                 }
1154             }
1155             return rewriteSearchResultProto(searchResultProto, mSchemaMapLocked);
1156         } finally {
1157             mReadWriteLock.readLock().unlock();
1158         }
1159     }
1160 
1161     /**
1162      * Invalidates the next-page token so that no more results of the related query can be returned.
1163      *
1164      * <p>This method belongs to query group.
1165      *
1166      * @param packageName Package name of the caller.
1167      * @param nextPageToken The token of pre-loaded results of previously executed query to be
1168      *     Invalidated.
1169      * @throws AppSearchException if nextPageToken is unusable.
1170      */
invalidateNextPageToken(@onNull String packageName, long nextPageToken)1171     public void invalidateNextPageToken(@NonNull String packageName, long nextPageToken)
1172             throws AppSearchException {
1173         mReadWriteLock.readLock().lock();
1174         try {
1175             throwIfClosedLocked();
1176 
1177             mLogUtil.piiTrace("invalidateNextPageToken, request", nextPageToken);
1178             checkNextPageToken(packageName, nextPageToken);
1179             mIcingSearchEngineLocked.invalidateNextPageToken(nextPageToken);
1180 
1181             synchronized (mNextPageTokensLocked) {
1182                 // At this point, we're guaranteed that this nextPageToken exists for this package,
1183                 // otherwise checkNextPageToken would've thrown an exception.
1184                 mNextPageTokensLocked.get(packageName).remove(nextPageToken);
1185             }
1186         } finally {
1187             mReadWriteLock.readLock().unlock();
1188         }
1189     }
1190 
1191     /** 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)1192     public void reportUsage(
1193             @NonNull String packageName,
1194             @NonNull String databaseName,
1195             @NonNull String namespace,
1196             @NonNull String documentId,
1197             long usageTimestampMillis,
1198             boolean systemUsage)
1199             throws AppSearchException {
1200         mReadWriteLock.writeLock().lock();
1201         try {
1202             throwIfClosedLocked();
1203 
1204             String prefixedNamespace = createPrefix(packageName, databaseName) + namespace;
1205             UsageReport.UsageType usageType =
1206                     systemUsage
1207                             ? UsageReport.UsageType.USAGE_TYPE2
1208                             : UsageReport.UsageType.USAGE_TYPE1;
1209             UsageReport report =
1210                     UsageReport.newBuilder()
1211                             .setDocumentNamespace(prefixedNamespace)
1212                             .setDocumentUri(documentId)
1213                             .setUsageTimestampMs(usageTimestampMillis)
1214                             .setUsageType(usageType)
1215                             .build();
1216 
1217             mLogUtil.piiTrace("reportUsage, request", report.getDocumentUri(), report);
1218             ReportUsageResultProto result = mIcingSearchEngineLocked.reportUsage(report);
1219             mLogUtil.piiTrace("reportUsage, response", result.getStatus(), result);
1220             checkSuccess(result.getStatus());
1221         } finally {
1222             mReadWriteLock.writeLock().unlock();
1223         }
1224     }
1225 
1226     /**
1227      * Removes the given document by id.
1228      *
1229      * <p>This method belongs to mutate group.
1230      *
1231      * @param packageName The package name that owns the document.
1232      * @param databaseName The databaseName the document is in.
1233      * @param namespace Namespace of the document to remove.
1234      * @param id ID of the document to remove.
1235      * @param removeStatsBuilder builder for {@link RemoveStats} to hold stats for remove
1236      * @throws AppSearchException on IcingSearchEngine error.
1237      */
remove( @onNull String packageName, @NonNull String databaseName, @NonNull String namespace, @NonNull String id, @Nullable RemoveStats.Builder removeStatsBuilder)1238     public void remove(
1239             @NonNull String packageName,
1240             @NonNull String databaseName,
1241             @NonNull String namespace,
1242             @NonNull String id,
1243             @Nullable RemoveStats.Builder removeStatsBuilder)
1244             throws AppSearchException {
1245         long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
1246         mReadWriteLock.writeLock().lock();
1247         try {
1248             throwIfClosedLocked();
1249 
1250             String prefixedNamespace = createPrefix(packageName, databaseName) + namespace;
1251             if (mLogUtil.isPiiTraceEnabled()) {
1252                 mLogUtil.piiTrace("removeById, request", prefixedNamespace + ", " + id);
1253             }
1254             DeleteResultProto deleteResultProto =
1255                     mIcingSearchEngineLocked.delete(prefixedNamespace, id);
1256             mLogUtil.piiTrace(
1257                     "removeById, response", deleteResultProto.getStatus(), deleteResultProto);
1258 
1259             if (removeStatsBuilder != null) {
1260                 removeStatsBuilder.setStatusCode(
1261                         statusProtoToResultCode(deleteResultProto.getStatus()));
1262                 AppSearchLoggerHelper.copyNativeStats(
1263                         deleteResultProto.getDeleteStats(), removeStatsBuilder);
1264             }
1265             checkSuccess(deleteResultProto.getStatus());
1266 
1267             // Update derived maps
1268             updateDocumentCountAfterRemovalLocked(packageName, /*numDocumentsDeleted=*/ 1);
1269         } finally {
1270             mReadWriteLock.writeLock().unlock();
1271             if (removeStatsBuilder != null) {
1272                 removeStatsBuilder.setTotalLatencyMillis(
1273                         (int) (SystemClock.elapsedRealtime() - totalLatencyStartTimeMillis));
1274             }
1275         }
1276     }
1277 
1278     /**
1279      * Removes documents by given query.
1280      *
1281      * <p>This method belongs to mutate group.
1282      *
1283      * @param packageName The package name that owns the documents.
1284      * @param databaseName The databaseName the document is in.
1285      * @param queryExpression Query String to search.
1286      * @param searchSpec Defines what and how to remove
1287      * @param removeStatsBuilder builder for {@link RemoveStats} to hold stats for remove
1288      * @throws AppSearchException on IcingSearchEngine error.
1289      */
removeByQuery( @onNull String packageName, @NonNull String databaseName, @NonNull String queryExpression, @NonNull SearchSpec searchSpec, @Nullable RemoveStats.Builder removeStatsBuilder)1290     public void removeByQuery(
1291             @NonNull String packageName,
1292             @NonNull String databaseName,
1293             @NonNull String queryExpression,
1294             @NonNull SearchSpec searchSpec,
1295             @Nullable RemoveStats.Builder removeStatsBuilder)
1296             throws AppSearchException {
1297         long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
1298         mReadWriteLock.writeLock().lock();
1299         try {
1300             throwIfClosedLocked();
1301 
1302             List<String> filterPackageNames = searchSpec.getFilterPackageNames();
1303             if (!filterPackageNames.isEmpty() && !filterPackageNames.contains(packageName)) {
1304                 // We're only removing documents within the parameter `packageName`. If we're not
1305                 // restricting our remove-query to this package name, then there's nothing for us to
1306                 // remove.
1307                 return;
1308             }
1309 
1310             SearchSpecProto searchSpecProto =
1311                     SearchSpecToProtoConverter.toSearchSpecProto(searchSpec);
1312             SearchSpecProto.Builder searchSpecBuilder =
1313                     searchSpecProto.toBuilder().setQuery(queryExpression);
1314 
1315             String prefix = createPrefix(packageName, databaseName);
1316             Set<String> allowedPrefixedSchemas = getAllowedPrefixSchemasLocked(prefix, searchSpec);
1317 
1318             // rewriteSearchSpecForPrefixesLocked will return false if there is nothing to search
1319             // over given their search filters, so we can return early and skip sending request
1320             // to Icing.
1321             if (!rewriteSearchSpecForPrefixesLocked(
1322                     searchSpecBuilder, Collections.singleton(prefix), allowedPrefixedSchemas)) {
1323                 return;
1324             }
1325             SearchSpecProto finalSearchSpec = searchSpecBuilder.build();
1326             mLogUtil.piiTrace("removeByQuery, request", finalSearchSpec);
1327             DeleteByQueryResultProto deleteResultProto =
1328                     mIcingSearchEngineLocked.deleteByQuery(finalSearchSpec);
1329             mLogUtil.piiTrace(
1330                     "removeByQuery, response", deleteResultProto.getStatus(), deleteResultProto);
1331 
1332             if (removeStatsBuilder != null) {
1333                 removeStatsBuilder.setStatusCode(
1334                         statusProtoToResultCode(deleteResultProto.getStatus()));
1335                 // TODO(b/187206766) also log query stats here once IcingLib returns it
1336                 AppSearchLoggerHelper.copyNativeStats(
1337                         deleteResultProto.getDeleteStats(), removeStatsBuilder);
1338             }
1339 
1340             // It seems that the caller wants to get success if the data matching the query is
1341             // not in the DB because it was not there or was successfully deleted.
1342             checkCodeOneOf(
1343                     deleteResultProto.getStatus(), StatusProto.Code.OK, StatusProto.Code.NOT_FOUND);
1344 
1345             // Update derived maps
1346             int numDocumentsDeleted = deleteResultProto.getDeleteStats().getNumDocumentsDeleted();
1347             updateDocumentCountAfterRemovalLocked(packageName, numDocumentsDeleted);
1348         } finally {
1349             mReadWriteLock.writeLock().unlock();
1350             if (removeStatsBuilder != null) {
1351                 removeStatsBuilder.setTotalLatencyMillis(
1352                         (int) (SystemClock.elapsedRealtime() - totalLatencyStartTimeMillis));
1353             }
1354         }
1355     }
1356 
1357     @GuardedBy("mReadWriteLock")
updateDocumentCountAfterRemovalLocked( @onNull String packageName, int numDocumentsDeleted)1358     private void updateDocumentCountAfterRemovalLocked(
1359             @NonNull String packageName, int numDocumentsDeleted) {
1360         if (numDocumentsDeleted > 0) {
1361             Integer oldDocumentCount = mDocumentCountMapLocked.get(packageName);
1362             // This should always be true: how can we delete documents for a package without
1363             // having seen that package during init? This is just a safeguard.
1364             if (oldDocumentCount != null) {
1365                 // This should always be >0; how can we remove more documents than we've indexed?
1366                 // This is just a safeguard.
1367                 int newDocumentCount = Math.max(oldDocumentCount - numDocumentsDeleted, 0);
1368                 mDocumentCountMapLocked.put(packageName, newDocumentCount);
1369             }
1370         }
1371     }
1372 
1373     /** Estimates the storage usage info for a specific package. */
1374     @NonNull
getStorageInfoForPackage(@onNull String packageName)1375     public StorageInfo getStorageInfoForPackage(@NonNull String packageName)
1376             throws AppSearchException {
1377         mReadWriteLock.readLock().lock();
1378         try {
1379             throwIfClosedLocked();
1380 
1381             Map<String, Set<String>> packageToDatabases = getPackageToDatabases();
1382             Set<String> databases = packageToDatabases.get(packageName);
1383             if (databases == null) {
1384                 // Package doesn't exist, no storage info to report
1385                 return new StorageInfo.Builder().build();
1386             }
1387 
1388             // Accumulate all the namespaces we're interested in.
1389             Set<String> wantedPrefixedNamespaces = new ArraySet<>();
1390             for (String database : databases) {
1391                 Set<String> prefixedNamespaces =
1392                         mNamespaceMapLocked.get(createPrefix(packageName, database));
1393                 if (prefixedNamespaces != null) {
1394                     wantedPrefixedNamespaces.addAll(prefixedNamespaces);
1395                 }
1396             }
1397             if (wantedPrefixedNamespaces.isEmpty()) {
1398                 return new StorageInfo.Builder().build();
1399             }
1400 
1401             return getStorageInfoForNamespaces(getRawStorageInfoProto(), wantedPrefixedNamespaces);
1402         } finally {
1403             mReadWriteLock.readLock().unlock();
1404         }
1405     }
1406 
1407     /** Estimates the storage usage info for a specific database in a package. */
1408     @NonNull
getStorageInfoForDatabase( @onNull String packageName, @NonNull String databaseName)1409     public StorageInfo getStorageInfoForDatabase(
1410             @NonNull String packageName, @NonNull String databaseName) throws AppSearchException {
1411         mReadWriteLock.readLock().lock();
1412         try {
1413             throwIfClosedLocked();
1414 
1415             Map<String, Set<String>> packageToDatabases = getPackageToDatabases();
1416             Set<String> databases = packageToDatabases.get(packageName);
1417             if (databases == null) {
1418                 // Package doesn't exist, no storage info to report
1419                 return new StorageInfo.Builder().build();
1420             }
1421             if (!databases.contains(databaseName)) {
1422                 // Database doesn't exist, no storage info to report
1423                 return new StorageInfo.Builder().build();
1424             }
1425 
1426             Set<String> wantedPrefixedNamespaces =
1427                     mNamespaceMapLocked.get(createPrefix(packageName, databaseName));
1428             if (wantedPrefixedNamespaces == null || wantedPrefixedNamespaces.isEmpty()) {
1429                 return new StorageInfo.Builder().build();
1430             }
1431 
1432             return getStorageInfoForNamespaces(getRawStorageInfoProto(), wantedPrefixedNamespaces);
1433         } finally {
1434             mReadWriteLock.readLock().unlock();
1435         }
1436     }
1437 
1438     /**
1439      * Returns the native storage info capsuled in {@link StorageInfoResultProto} directly from
1440      * IcingSearchEngine.
1441      */
1442     @NonNull
getRawStorageInfoProto()1443     public StorageInfoProto getRawStorageInfoProto() throws AppSearchException {
1444         mReadWriteLock.readLock().lock();
1445         try {
1446             throwIfClosedLocked();
1447             mLogUtil.piiTrace("getStorageInfo, request");
1448             StorageInfoResultProto storageInfoResult = mIcingSearchEngineLocked.getStorageInfo();
1449             mLogUtil.piiTrace(
1450                     "getStorageInfo, response", storageInfoResult.getStatus(), storageInfoResult);
1451             checkSuccess(storageInfoResult.getStatus());
1452             return storageInfoResult.getStorageInfo();
1453         } finally {
1454             mReadWriteLock.readLock().unlock();
1455         }
1456     }
1457 
1458     /**
1459      * Extracts and returns {@link StorageInfo} from {@link StorageInfoProto} based on prefixed
1460      * namespaces.
1461      */
1462     @NonNull
getStorageInfoForNamespaces( @onNull StorageInfoProto storageInfoProto, @NonNull Set<String> prefixedNamespaces)1463     private static StorageInfo getStorageInfoForNamespaces(
1464             @NonNull StorageInfoProto storageInfoProto, @NonNull Set<String> prefixedNamespaces) {
1465         if (!storageInfoProto.hasDocumentStorageInfo()) {
1466             return new StorageInfo.Builder().build();
1467         }
1468 
1469         long totalStorageSize = storageInfoProto.getTotalStorageSize();
1470         DocumentStorageInfoProto documentStorageInfo = storageInfoProto.getDocumentStorageInfo();
1471         int totalDocuments =
1472                 documentStorageInfo.getNumAliveDocuments()
1473                         + documentStorageInfo.getNumExpiredDocuments();
1474 
1475         if (totalStorageSize == 0 || totalDocuments == 0) {
1476             // Maybe we can exit early and also avoid a divide by 0 error.
1477             return new StorageInfo.Builder().build();
1478         }
1479 
1480         // Accumulate stats across the package's namespaces.
1481         int aliveDocuments = 0;
1482         int expiredDocuments = 0;
1483         int aliveNamespaces = 0;
1484         List<NamespaceStorageInfoProto> namespaceStorageInfos =
1485                 documentStorageInfo.getNamespaceStorageInfoList();
1486         for (int i = 0; i < namespaceStorageInfos.size(); i++) {
1487             NamespaceStorageInfoProto namespaceStorageInfo = namespaceStorageInfos.get(i);
1488             // The namespace from icing lib is already the prefixed format
1489             if (prefixedNamespaces.contains(namespaceStorageInfo.getNamespace())) {
1490                 if (namespaceStorageInfo.getNumAliveDocuments() > 0) {
1491                     aliveNamespaces++;
1492                     aliveDocuments += namespaceStorageInfo.getNumAliveDocuments();
1493                 }
1494                 expiredDocuments += namespaceStorageInfo.getNumExpiredDocuments();
1495             }
1496         }
1497         int namespaceDocuments = aliveDocuments + expiredDocuments;
1498 
1499         // Since we don't have the exact size of all the documents, we do an estimation. Note
1500         // that while the total storage takes into account schema, index, etc. in addition to
1501         // documents, we'll only calculate the percentage based on number of documents a
1502         // client has.
1503         return new StorageInfo.Builder()
1504                 .setSizeBytes((long) (namespaceDocuments * 1.0 / totalDocuments * totalStorageSize))
1505                 .setAliveDocumentsCount(aliveDocuments)
1506                 .setAliveNamespacesCount(aliveNamespaces)
1507                 .build();
1508     }
1509 
1510     /**
1511      * Persists all update/delete requests to the disk.
1512      *
1513      * <p>If the app crashes after a call to PersistToDisk with {@link PersistType.Code#FULL}, Icing
1514      * would be able to fully recover all data written up to this point without a costly recovery
1515      * process.
1516      *
1517      * <p>If the app crashes after a call to PersistToDisk with {@link PersistType.Code#LITE}, Icing
1518      * would trigger a costly recovery process in next initialization. After that, Icing would still
1519      * be able to recover all written data - excepting Usage data. Usage data is only guaranteed to
1520      * be safe after a call to PersistToDisk with {@link PersistType.Code#FULL}
1521      *
1522      * <p>If the app crashes after an update/delete request has been made, but before any call to
1523      * PersistToDisk, then all data in Icing will be lost.
1524      *
1525      * @param persistType the amount of data to persist. {@link PersistType.Code#LITE} will only
1526      *     persist the minimal amount of data to ensure all data can be recovered. {@link
1527      *     PersistType.Code#FULL} will persist all data necessary to prevent data loss without
1528      *     needing data recovery.
1529      * @throws AppSearchException on any error that AppSearch persist data to disk.
1530      */
persistToDisk(@onNull PersistType.Code persistType)1531     public void persistToDisk(@NonNull PersistType.Code persistType) throws AppSearchException {
1532         mReadWriteLock.writeLock().lock();
1533         try {
1534             throwIfClosedLocked();
1535 
1536             mLogUtil.piiTrace("persistToDisk, request", persistType);
1537             PersistToDiskResultProto persistToDiskResultProto =
1538                     mIcingSearchEngineLocked.persistToDisk(persistType);
1539             mLogUtil.piiTrace(
1540                     "persistToDisk, response",
1541                     persistToDiskResultProto.getStatus(),
1542                     persistToDiskResultProto);
1543             checkSuccess(persistToDiskResultProto.getStatus());
1544         } finally {
1545             mReadWriteLock.writeLock().unlock();
1546         }
1547     }
1548 
1549     /**
1550      * Remove all {@link AppSearchSchema}s and {@link GenericDocument}s under the given package.
1551      *
1552      * @param packageName The name of package to be removed.
1553      * @throws AppSearchException if we cannot remove the data.
1554      */
clearPackageData(@onNull String packageName)1555     public void clearPackageData(@NonNull String packageName) throws AppSearchException {
1556         mReadWriteLock.writeLock().lock();
1557         try {
1558             throwIfClosedLocked();
1559             Set<String> existingPackages = getPackageToDatabases().keySet();
1560             if (existingPackages.contains(packageName)) {
1561                 existingPackages.remove(packageName);
1562                 prunePackageData(existingPackages);
1563             }
1564         } finally {
1565             mReadWriteLock.writeLock().unlock();
1566         }
1567     }
1568 
1569     /**
1570      * Remove all {@link AppSearchSchema}s and {@link GenericDocument}s that doesn't belong to any
1571      * of the given installed packages
1572      *
1573      * @param installedPackages The name of all installed package.
1574      * @throws AppSearchException if we cannot remove the data.
1575      */
prunePackageData(@onNull Set<String> installedPackages)1576     public void prunePackageData(@NonNull Set<String> installedPackages) throws AppSearchException {
1577         mReadWriteLock.writeLock().lock();
1578         try {
1579             throwIfClosedLocked();
1580             Map<String, Set<String>> packageToDatabases = getPackageToDatabases();
1581             if (installedPackages.containsAll(packageToDatabases.keySet())) {
1582                 // No package got removed. We are good.
1583                 return;
1584             }
1585 
1586             // Prune schema proto
1587             SchemaProto existingSchema = getSchemaProtoLocked();
1588             SchemaProto.Builder newSchemaBuilder = SchemaProto.newBuilder();
1589             for (int i = 0; i < existingSchema.getTypesCount(); i++) {
1590                 String packageName = getPackageName(existingSchema.getTypes(i).getSchemaType());
1591                 if (installedPackages.contains(packageName)) {
1592                     newSchemaBuilder.addTypes(existingSchema.getTypes(i));
1593                 }
1594             }
1595 
1596             SchemaProto finalSchema = newSchemaBuilder.build();
1597 
1598             // Apply schema, set force override to true to remove all schemas and documents that
1599             // doesn't belong to any of these installed packages.
1600             mLogUtil.piiTrace(
1601                     "clearPackageData.setSchema, request",
1602                     finalSchema.getTypesCount(),
1603                     finalSchema);
1604             SetSchemaResultProto setSchemaResultProto =
1605                     mIcingSearchEngineLocked.setSchema(
1606                             finalSchema, /*ignoreErrorsAndDeleteDocuments=*/ true);
1607             mLogUtil.piiTrace(
1608                     "clearPackageData.setSchema, response",
1609                     setSchemaResultProto.getStatus(),
1610                     setSchemaResultProto);
1611 
1612             // Determine whether it succeeded.
1613             checkSuccess(setSchemaResultProto.getStatus());
1614 
1615             // Prune cached maps
1616             for (Map.Entry<String, Set<String>> entry : packageToDatabases.entrySet()) {
1617                 String packageName = entry.getKey();
1618                 Set<String> databaseNames = entry.getValue();
1619                 if (!installedPackages.contains(packageName) && databaseNames != null) {
1620                     mDocumentCountMapLocked.remove(packageName);
1621                     synchronized (mNextPageTokensLocked) {
1622                         mNextPageTokensLocked.remove(packageName);
1623                     }
1624                     for (String databaseName : databaseNames) {
1625                         String removedPrefix = createPrefix(packageName, databaseName);
1626                         mSchemaMapLocked.remove(removedPrefix);
1627                         mNamespaceMapLocked.remove(removedPrefix);
1628                     }
1629                 }
1630             }
1631             // TODO(b/145759910) clear visibility setting for package.
1632         } finally {
1633             mReadWriteLock.writeLock().unlock();
1634         }
1635     }
1636 
1637     /**
1638      * Clears documents and schema across all packages and databaseNames.
1639      *
1640      * <p>This method belongs to mutate group.
1641      *
1642      * @throws AppSearchException on IcingSearchEngine error.
1643      */
1644     @GuardedBy("mReadWriteLock")
resetLocked(@ullable InitializeStats.Builder initStatsBuilder)1645     private void resetLocked(@Nullable InitializeStats.Builder initStatsBuilder)
1646             throws AppSearchException {
1647         mLogUtil.piiTrace("icingSearchEngine.reset, request");
1648         ResetResultProto resetResultProto = mIcingSearchEngineLocked.reset();
1649         mLogUtil.piiTrace(
1650                 "icingSearchEngine.reset, response",
1651                 resetResultProto.getStatus(),
1652                 resetResultProto);
1653         mOptimizeIntervalCountLocked = 0;
1654         mSchemaMapLocked.clear();
1655         mNamespaceMapLocked.clear();
1656         mDocumentCountMapLocked.clear();
1657         synchronized (mNextPageTokensLocked) {
1658             mNextPageTokensLocked.clear();
1659         }
1660         if (initStatsBuilder != null) {
1661             initStatsBuilder
1662                     .setHasReset(true)
1663                     .setResetStatusCode(statusProtoToResultCode(resetResultProto.getStatus()));
1664         }
1665 
1666         checkSuccess(resetResultProto.getStatus());
1667     }
1668 
1669     @GuardedBy("mReadWriteLock")
rebuildDocumentCountMapLocked(@onNull StorageInfoProto storageInfoProto)1670     private void rebuildDocumentCountMapLocked(@NonNull StorageInfoProto storageInfoProto) {
1671         mDocumentCountMapLocked.clear();
1672         List<NamespaceStorageInfoProto> namespaceStorageInfoProtoList =
1673                 storageInfoProto.getDocumentStorageInfo().getNamespaceStorageInfoList();
1674         for (int i = 0; i < namespaceStorageInfoProtoList.size(); i++) {
1675             NamespaceStorageInfoProto namespaceStorageInfoProto =
1676                     namespaceStorageInfoProtoList.get(i);
1677             String packageName = getPackageName(namespaceStorageInfoProto.getNamespace());
1678             Integer oldCount = mDocumentCountMapLocked.get(packageName);
1679             int newCount;
1680             if (oldCount == null) {
1681                 newCount = namespaceStorageInfoProto.getNumAliveDocuments();
1682             } else {
1683                 newCount = oldCount + namespaceStorageInfoProto.getNumAliveDocuments();
1684             }
1685             mDocumentCountMapLocked.put(packageName, newCount);
1686         }
1687     }
1688 
1689     /** Wrapper around schema changes */
1690     @VisibleForTesting
1691     static class RewrittenSchemaResults {
1692         // Any prefixed types that used to exist in the schema, but are deleted in the new one.
1693         final Set<String> mDeletedPrefixedTypes = new ArraySet<>();
1694 
1695         // Map of prefixed schema types to SchemaTypeConfigProtos that were part of the new schema.
1696         final Map<String, SchemaTypeConfigProto> mRewrittenPrefixedTypes = new ArrayMap<>();
1697     }
1698 
1699     /**
1700      * Rewrites all types mentioned in the given {@code newSchema} to prepend {@code prefix}.
1701      * Rewritten types will be added to the {@code existingSchema}.
1702      *
1703      * @param prefix The full prefix to prepend to the schema.
1704      * @param existingSchema A schema that may contain existing types from across all prefixes. Will
1705      *     be mutated to contain the properly rewritten schema types from {@code newSchema}.
1706      * @param newSchema Schema with types to add to the {@code existingSchema}.
1707      * @return a RewrittenSchemaResults that contains all prefixed schema type names in the given
1708      *     prefix as well as a set of schema types that were deleted.
1709      */
1710     @VisibleForTesting
rewriteSchema( @onNull String prefix, @NonNull SchemaProto.Builder existingSchema, @NonNull SchemaProto newSchema)1711     static RewrittenSchemaResults rewriteSchema(
1712             @NonNull String prefix,
1713             @NonNull SchemaProto.Builder existingSchema,
1714             @NonNull SchemaProto newSchema)
1715             throws AppSearchException {
1716         HashMap<String, SchemaTypeConfigProto> newTypesToProto = new HashMap<>();
1717         // Rewrite the schema type to include the typePrefix.
1718         for (int typeIdx = 0; typeIdx < newSchema.getTypesCount(); typeIdx++) {
1719             SchemaTypeConfigProto.Builder typeConfigBuilder =
1720                     newSchema.getTypes(typeIdx).toBuilder();
1721 
1722             // Rewrite SchemaProto.types.schema_type
1723             String newSchemaType = prefix + typeConfigBuilder.getSchemaType();
1724             typeConfigBuilder.setSchemaType(newSchemaType);
1725 
1726             // Rewrite SchemaProto.types.properties.schema_type
1727             for (int propertyIdx = 0;
1728                     propertyIdx < typeConfigBuilder.getPropertiesCount();
1729                     propertyIdx++) {
1730                 PropertyConfigProto.Builder propertyConfigBuilder =
1731                         typeConfigBuilder.getProperties(propertyIdx).toBuilder();
1732                 if (!propertyConfigBuilder.getSchemaType().isEmpty()) {
1733                     String newPropertySchemaType = prefix + propertyConfigBuilder.getSchemaType();
1734                     propertyConfigBuilder.setSchemaType(newPropertySchemaType);
1735                     typeConfigBuilder.setProperties(propertyIdx, propertyConfigBuilder);
1736                 }
1737             }
1738 
1739             newTypesToProto.put(newSchemaType, typeConfigBuilder.build());
1740         }
1741 
1742         // newTypesToProto is modified below, so we need a copy first
1743         RewrittenSchemaResults rewrittenSchemaResults = new RewrittenSchemaResults();
1744         rewrittenSchemaResults.mRewrittenPrefixedTypes.putAll(newTypesToProto);
1745 
1746         // Combine the existing schema (which may have types from other prefixes) with this
1747         // prefix's new schema. Modifies the existingSchemaBuilder.
1748         // Check if we need to replace any old schema types with the new ones.
1749         for (int i = 0; i < existingSchema.getTypesCount(); i++) {
1750             String schemaType = existingSchema.getTypes(i).getSchemaType();
1751             SchemaTypeConfigProto newProto = newTypesToProto.remove(schemaType);
1752             if (newProto != null) {
1753                 // Replacement
1754                 existingSchema.setTypes(i, newProto);
1755             } else if (prefix.equals(getPrefix(schemaType))) {
1756                 // All types existing before but not in newSchema should be removed.
1757                 existingSchema.removeTypes(i);
1758                 --i;
1759                 rewrittenSchemaResults.mDeletedPrefixedTypes.add(schemaType);
1760             }
1761         }
1762         // We've been removing existing types from newTypesToProto, so everything that remains is
1763         // new.
1764         existingSchema.addAllTypes(newTypesToProto.values());
1765 
1766         return rewrittenSchemaResults;
1767     }
1768 
1769     /**
1770      * Rewrites the search spec filters with {@code prefixes}.
1771      *
1772      * <p>This method should be only called in query methods and get the READ lock to keep thread
1773      * safety.
1774      *
1775      * @param searchSpecBuilder Client-provided SearchSpec
1776      * @param prefixes Prefixes that we should prepend to all our filters
1777      * @param allowedPrefixedSchemas Prefixed schemas that the client is allowed to query over. This
1778      *     supersedes the schema filters that may exist on the {@code searchSpecBuilder}.
1779      * @return false if none there would be nothing to search over.
1780      */
1781     @VisibleForTesting
1782     @GuardedBy("mReadWriteLock")
rewriteSearchSpecForPrefixesLocked( @onNull SearchSpecProto.Builder searchSpecBuilder, @NonNull Set<String> prefixes, @NonNull Set<String> allowedPrefixedSchemas)1783     boolean rewriteSearchSpecForPrefixesLocked(
1784             @NonNull SearchSpecProto.Builder searchSpecBuilder,
1785             @NonNull Set<String> prefixes,
1786             @NonNull Set<String> allowedPrefixedSchemas) {
1787         // Create a copy since retainAll() modifies the original set.
1788         Set<String> existingPrefixes = new ArraySet<>(mNamespaceMapLocked.keySet());
1789         existingPrefixes.retainAll(prefixes);
1790 
1791         if (existingPrefixes.isEmpty()) {
1792             // None of the prefixes exist, empty query.
1793             return false;
1794         }
1795 
1796         if (allowedPrefixedSchemas.isEmpty()) {
1797             // Not allowed to search over any schemas, empty query.
1798             return false;
1799         }
1800 
1801         // Clear the schema type filters since we'll be rewriting them with the
1802         // allowedPrefixedSchemas.
1803         searchSpecBuilder.clearSchemaTypeFilters();
1804         searchSpecBuilder.addAllSchemaTypeFilters(allowedPrefixedSchemas);
1805 
1806         // Cache the namespaces before clearing everything.
1807         List<String> namespaceFilters = searchSpecBuilder.getNamespaceFiltersList();
1808         searchSpecBuilder.clearNamespaceFilters();
1809 
1810         // Rewrite non-schema filters to include a prefix.
1811         for (String prefix : existingPrefixes) {
1812             // TODO(b/169883602): We currently grab every namespace for every prefix. We can
1813             //  optimize this by checking if a prefix has any allowedSchemaTypes. If not, that
1814             //  means we don't want to query over anything in that prefix anyways, so we don't
1815             //  need to grab its namespaces either.
1816 
1817             // Empty namespaces on the search spec means to query over all namespaces.
1818             Set<String> existingNamespaces = mNamespaceMapLocked.get(prefix);
1819             if (existingNamespaces != null) {
1820                 if (namespaceFilters.isEmpty()) {
1821                     // Include all namespaces
1822                     searchSpecBuilder.addAllNamespaceFilters(existingNamespaces);
1823                 } else {
1824                     // Prefix the given namespaces.
1825                     for (int i = 0; i < namespaceFilters.size(); i++) {
1826                         String prefixedNamespace = prefix + namespaceFilters.get(i);
1827                         if (existingNamespaces.contains(prefixedNamespace)) {
1828                             searchSpecBuilder.addNamespaceFilters(prefixedNamespace);
1829                         }
1830                     }
1831                 }
1832             }
1833         }
1834 
1835         return true;
1836     }
1837 
1838     /**
1839      * Returns the set of allowed prefixed schemas that the {@code prefix} can query while taking
1840      * into account the {@code searchSpec} schema filters.
1841      *
1842      * <p>This only checks intersection of schema filters on the search spec with those that the
1843      * prefix owns itself. This does not check global query permissions.
1844      */
1845     @GuardedBy("mReadWriteLock")
getAllowedPrefixSchemasLocked( @onNull String prefix, @NonNull SearchSpec searchSpec)1846     private Set<String> getAllowedPrefixSchemasLocked(
1847             @NonNull String prefix, @NonNull SearchSpec searchSpec) {
1848         Set<String> allowedPrefixedSchemas = new ArraySet<>();
1849 
1850         // Add all the schema filters the client specified.
1851         List<String> schemaFilters = searchSpec.getFilterSchemas();
1852         for (int i = 0; i < schemaFilters.size(); i++) {
1853             allowedPrefixedSchemas.add(prefix + schemaFilters.get(i));
1854         }
1855 
1856         if (allowedPrefixedSchemas.isEmpty()) {
1857             // If the client didn't specify any schema filters, search over all of their schemas
1858             Map<String, SchemaTypeConfigProto> prefixedSchemaMap = mSchemaMapLocked.get(prefix);
1859             if (prefixedSchemaMap != null) {
1860                 allowedPrefixedSchemas.addAll(prefixedSchemaMap.keySet());
1861             }
1862         }
1863         return allowedPrefixedSchemas;
1864     }
1865 
1866     /**
1867      * Rewrites the typePropertyMasks that exist in {@code prefixes}.
1868      *
1869      * <p>This method should be only called in query methods and get the READ lock to keep thread
1870      * safety.
1871      *
1872      * @param resultSpecBuilder ResultSpecs as specified by client
1873      * @param prefixes Prefixes that we should prepend to all our filters
1874      * @param allowedPrefixedSchemas Prefixed schemas that the client is allowed to query over.
1875      */
1876     @VisibleForTesting
1877     @GuardedBy("mReadWriteLock")
rewriteResultSpecForPrefixesLocked( @onNull ResultSpecProto.Builder resultSpecBuilder, @NonNull Set<String> prefixes, @NonNull Set<String> allowedPrefixedSchemas)1878     void rewriteResultSpecForPrefixesLocked(
1879             @NonNull ResultSpecProto.Builder resultSpecBuilder,
1880             @NonNull Set<String> prefixes,
1881             @NonNull Set<String> allowedPrefixedSchemas) {
1882         // Create a copy since retainAll() modifies the original set.
1883         Set<String> existingPrefixes = new ArraySet<>(mNamespaceMapLocked.keySet());
1884         existingPrefixes.retainAll(prefixes);
1885 
1886         List<TypePropertyMask> prefixedTypePropertyMasks = new ArrayList<>();
1887         // Rewrite filters to include a database prefix.
1888         for (String prefix : existingPrefixes) {
1889             // Qualify the given schema types
1890             for (TypePropertyMask typePropertyMask : resultSpecBuilder.getTypePropertyMasksList()) {
1891                 String unprefixedType = typePropertyMask.getSchemaType();
1892                 boolean isWildcard =
1893                         unprefixedType.equals(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD);
1894                 String prefixedType = isWildcard ? unprefixedType : prefix + unprefixedType;
1895                 if (isWildcard || allowedPrefixedSchemas.contains(prefixedType)) {
1896                     prefixedTypePropertyMasks.add(
1897                             typePropertyMask.toBuilder().setSchemaType(prefixedType).build());
1898                 }
1899             }
1900         }
1901         resultSpecBuilder
1902                 .clearTypePropertyMasks()
1903                 .addAllTypePropertyMasks(prefixedTypePropertyMasks);
1904     }
1905 
1906     /**
1907      * Adds result groupings for each namespace in each package being queried for.
1908      *
1909      * <p>This method should be only called in query methods and get the READ lock to keep thread
1910      * safety.
1911      *
1912      * @param resultSpecBuilder ResultSpecs as specified by client
1913      * @param prefixes Prefixes that we should prepend to all our filters
1914      * @param maxNumResults The maximum number of results for each grouping to support.
1915      */
1916     @GuardedBy("mReadWriteLock")
addPerPackagePerNamespaceResultGroupingsLocked( @onNull ResultSpecProto.Builder resultSpecBuilder, @NonNull Set<String> prefixes, int maxNumResults)1917     private void addPerPackagePerNamespaceResultGroupingsLocked(
1918             @NonNull ResultSpecProto.Builder resultSpecBuilder,
1919             @NonNull Set<String> prefixes,
1920             int maxNumResults) {
1921         Set<String> existingPrefixes = new ArraySet<>(mNamespaceMapLocked.keySet());
1922         existingPrefixes.retainAll(prefixes);
1923 
1924         // Create a map for package+namespace to prefixedNamespaces. This is NOT necessarily the
1925         // same as the list of namespaces. If one package has multiple databases, each with the same
1926         // namespace, then those should be grouped together.
1927         Map<String, List<String>> packageAndNamespaceToNamespaces = new ArrayMap<>();
1928         for (String prefix : existingPrefixes) {
1929             Set<String> prefixedNamespaces = mNamespaceMapLocked.get(prefix);
1930             if (prefixedNamespaces == null) {
1931                 continue;
1932             }
1933             String packageName = getPackageName(prefix);
1934             // Create a new prefix without the database name. This will allow us to group namespaces
1935             // that have the same name and package but a different database name together.
1936             String emptyDatabasePrefix = createPrefix(packageName, /*databaseName*/ "");
1937             for (String prefixedNamespace : prefixedNamespaces) {
1938                 String namespace;
1939                 try {
1940                     namespace = removePrefix(prefixedNamespace);
1941                 } catch (AppSearchException e) {
1942                     // This should never happen. Skip this namespace if it does.
1943                     Log.e(TAG, "Prefixed namespace " + prefixedNamespace + " is malformed.");
1944                     continue;
1945                 }
1946                 String emptyDatabasePrefixedNamespace = emptyDatabasePrefix + namespace;
1947                 List<String> namespaceList =
1948                         packageAndNamespaceToNamespaces.get(emptyDatabasePrefixedNamespace);
1949                 if (namespaceList == null) {
1950                     namespaceList = new ArrayList<>();
1951                     packageAndNamespaceToNamespaces.put(
1952                             emptyDatabasePrefixedNamespace, namespaceList);
1953                 }
1954                 namespaceList.add(prefixedNamespace);
1955             }
1956         }
1957 
1958         for (List<String> namespaces : packageAndNamespaceToNamespaces.values()) {
1959             resultSpecBuilder.addResultGroupings(
1960                     ResultSpecProto.ResultGrouping.newBuilder()
1961                             .addAllNamespaces(namespaces)
1962                             .setMaxResults(maxNumResults));
1963         }
1964     }
1965 
1966     /**
1967      * Adds result groupings for each package being queried for.
1968      *
1969      * <p>This method should be only called in query methods and get the READ lock to keep thread
1970      * safety.
1971      *
1972      * @param resultSpecBuilder ResultSpecs as specified by client
1973      * @param prefixes Prefixes that we should prepend to all our filters
1974      * @param maxNumResults The maximum number of results for each grouping to support.
1975      */
1976     @GuardedBy("mReadWriteLock")
addPerPackageResultGroupingsLocked( @onNull ResultSpecProto.Builder resultSpecBuilder, @NonNull Set<String> prefixes, int maxNumResults)1977     private void addPerPackageResultGroupingsLocked(
1978             @NonNull ResultSpecProto.Builder resultSpecBuilder,
1979             @NonNull Set<String> prefixes,
1980             int maxNumResults) {
1981         Set<String> existingPrefixes = new ArraySet<>(mNamespaceMapLocked.keySet());
1982         existingPrefixes.retainAll(prefixes);
1983 
1984         // Build up a map of package to namespaces.
1985         Map<String, List<String>> packageToNamespacesMap = new ArrayMap<>();
1986         for (String prefix : existingPrefixes) {
1987             Set<String> prefixedNamespaces = mNamespaceMapLocked.get(prefix);
1988             if (prefixedNamespaces == null) {
1989                 continue;
1990             }
1991             String packageName = getPackageName(prefix);
1992             List<String> packageNamespaceList = packageToNamespacesMap.get(packageName);
1993             if (packageNamespaceList == null) {
1994                 packageNamespaceList = new ArrayList<>();
1995                 packageToNamespacesMap.put(packageName, packageNamespaceList);
1996             }
1997             packageNamespaceList.addAll(prefixedNamespaces);
1998         }
1999 
2000         for (List<String> prefixedNamespaces : packageToNamespacesMap.values()) {
2001             resultSpecBuilder.addResultGroupings(
2002                     ResultSpecProto.ResultGrouping.newBuilder()
2003                             .addAllNamespaces(prefixedNamespaces)
2004                             .setMaxResults(maxNumResults));
2005         }
2006     }
2007 
2008     /**
2009      * Adds result groupings for each namespace being queried for.
2010      *
2011      * <p>This method should be only called in query methods and get the READ lock to keep thread
2012      * safety.
2013      *
2014      * @param resultSpecBuilder ResultSpecs as specified by client
2015      * @param prefixes Prefixes that we should prepend to all our filters
2016      * @param maxNumResults The maximum number of results for each grouping to support.
2017      */
2018     @GuardedBy("mReadWriteLock")
addPerNamespaceResultGroupingsLocked( @onNull ResultSpecProto.Builder resultSpecBuilder, @NonNull Set<String> prefixes, int maxNumResults)2019     private void addPerNamespaceResultGroupingsLocked(
2020             @NonNull ResultSpecProto.Builder resultSpecBuilder,
2021             @NonNull Set<String> prefixes,
2022             int maxNumResults) {
2023         Set<String> existingPrefixes = new ArraySet<>(mNamespaceMapLocked.keySet());
2024         existingPrefixes.retainAll(prefixes);
2025 
2026         // Create a map of namespace to prefixedNamespaces. This is NOT necessarily the
2027         // same as the list of namespaces. If a namespace exists under different packages and/or
2028         // different databases, they should still be grouped together.
2029         Map<String, List<String>> namespaceToPrefixedNamespaces = new ArrayMap<>();
2030         for (String prefix : existingPrefixes) {
2031             Set<String> prefixedNamespaces = mNamespaceMapLocked.get(prefix);
2032             if (prefixedNamespaces == null) {
2033                 continue;
2034             }
2035             for (String prefixedNamespace : prefixedNamespaces) {
2036                 String namespace;
2037                 try {
2038                     namespace = removePrefix(prefixedNamespace);
2039                 } catch (AppSearchException e) {
2040                     // This should never happen. Skip this namespace if it does.
2041                     Log.e(TAG, "Prefixed namespace " + prefixedNamespace + " is malformed.");
2042                     continue;
2043                 }
2044                 List<String> groupedPrefixedNamespaces =
2045                         namespaceToPrefixedNamespaces.get(namespace);
2046                 if (groupedPrefixedNamespaces == null) {
2047                     groupedPrefixedNamespaces = new ArrayList<>();
2048                     namespaceToPrefixedNamespaces.put(namespace, groupedPrefixedNamespaces);
2049                 }
2050                 groupedPrefixedNamespaces.add(prefixedNamespace);
2051             }
2052         }
2053 
2054         for (List<String> namespaces : namespaceToPrefixedNamespaces.values()) {
2055             resultSpecBuilder.addResultGroupings(
2056                     ResultSpecProto.ResultGrouping.newBuilder()
2057                             .addAllNamespaces(namespaces)
2058                             .setMaxResults(maxNumResults));
2059         }
2060     }
2061 
2062     @VisibleForTesting
2063     @GuardedBy("mReadWriteLock")
getSchemaProtoLocked()2064     SchemaProto getSchemaProtoLocked() throws AppSearchException {
2065         mLogUtil.piiTrace("getSchema, request");
2066         GetSchemaResultProto schemaProto = mIcingSearchEngineLocked.getSchema();
2067         mLogUtil.piiTrace("getSchema, response", schemaProto.getStatus(), schemaProto);
2068         // TODO(b/161935693) check GetSchemaResultProto is success or not. Call reset() if it's not.
2069         // TODO(b/161935693) only allow GetSchemaResultProto NOT_FOUND on first run
2070         checkCodeOneOf(schemaProto.getStatus(), StatusProto.Code.OK, StatusProto.Code.NOT_FOUND);
2071         return schemaProto.getSchema();
2072     }
2073 
addNextPageToken(String packageName, long nextPageToken)2074     private void addNextPageToken(String packageName, long nextPageToken) {
2075         if (nextPageToken == EMPTY_PAGE_TOKEN) {
2076             // There is no more pages. No need to add it.
2077             return;
2078         }
2079         synchronized (mNextPageTokensLocked) {
2080             Set<Long> tokens = mNextPageTokensLocked.get(packageName);
2081             if (tokens == null) {
2082                 tokens = new ArraySet<>();
2083                 mNextPageTokensLocked.put(packageName, tokens);
2084             }
2085             tokens.add(nextPageToken);
2086         }
2087     }
2088 
checkNextPageToken(String packageName, long nextPageToken)2089     private void checkNextPageToken(String packageName, long nextPageToken)
2090             throws AppSearchException {
2091         if (nextPageToken == EMPTY_PAGE_TOKEN) {
2092             // Swallow the check for empty page token, token = 0 means there is no more page and it
2093             // won't return anything from Icing.
2094             return;
2095         }
2096         synchronized (mNextPageTokensLocked) {
2097             Set<Long> nextPageTokens = mNextPageTokensLocked.get(packageName);
2098             if (nextPageTokens == null || !nextPageTokens.contains(nextPageToken)) {
2099                 throw new AppSearchException(
2100                         AppSearchResult.RESULT_SECURITY_ERROR,
2101                         "Package \""
2102                                 + packageName
2103                                 + "\" cannot use nextPageToken: "
2104                                 + nextPageToken);
2105             }
2106         }
2107     }
2108 
addToMap( Map<String, Set<String>> map, String prefix, String prefixedValue)2109     private static void addToMap(
2110             Map<String, Set<String>> map, String prefix, String prefixedValue) {
2111         Set<String> values = map.get(prefix);
2112         if (values == null) {
2113             values = new ArraySet<>();
2114             map.put(prefix, values);
2115         }
2116         values.add(prefixedValue);
2117     }
2118 
addToMap( Map<String, Map<String, SchemaTypeConfigProto>> map, String prefix, SchemaTypeConfigProto schemaTypeConfigProto)2119     private static void addToMap(
2120             Map<String, Map<String, SchemaTypeConfigProto>> map,
2121             String prefix,
2122             SchemaTypeConfigProto schemaTypeConfigProto) {
2123         Map<String, SchemaTypeConfigProto> schemaTypeMap = map.get(prefix);
2124         if (schemaTypeMap == null) {
2125             schemaTypeMap = new ArrayMap<>();
2126             map.put(prefix, schemaTypeMap);
2127         }
2128         schemaTypeMap.put(schemaTypeConfigProto.getSchemaType(), schemaTypeConfigProto);
2129     }
2130 
removeFromMap( Map<String, Map<String, SchemaTypeConfigProto>> map, String prefix, String schemaType)2131     private static void removeFromMap(
2132             Map<String, Map<String, SchemaTypeConfigProto>> map, String prefix, String schemaType) {
2133         Map<String, SchemaTypeConfigProto> schemaTypeMap = map.get(prefix);
2134         if (schemaTypeMap != null) {
2135             schemaTypeMap.remove(schemaType);
2136         }
2137     }
2138 
2139     /**
2140      * Checks the given status code and throws an {@link AppSearchException} if code is an error.
2141      *
2142      * @throws AppSearchException on error codes.
2143      */
checkSuccess(StatusProto statusProto)2144     private static void checkSuccess(StatusProto statusProto) throws AppSearchException {
2145         checkCodeOneOf(statusProto, StatusProto.Code.OK);
2146     }
2147 
2148     /**
2149      * Checks the given status code is one of the provided codes, and throws an {@link
2150      * AppSearchException} if it is not.
2151      */
checkCodeOneOf(StatusProto statusProto, StatusProto.Code... codes)2152     private static void checkCodeOneOf(StatusProto statusProto, StatusProto.Code... codes)
2153             throws AppSearchException {
2154         for (int i = 0; i < codes.length; i++) {
2155             if (codes[i] == statusProto.getCode()) {
2156                 // Everything's good
2157                 return;
2158             }
2159         }
2160 
2161         if (statusProto.getCode() == StatusProto.Code.WARNING_DATA_LOSS) {
2162             // TODO: May want to propagate WARNING_DATA_LOSS up to AppSearchSession so they can
2163             //  choose to log the error or potentially pass it on to clients.
2164             Log.w(TAG, "Encountered WARNING_DATA_LOSS: " + statusProto.getMessage());
2165             return;
2166         }
2167 
2168         throw new AppSearchException(
2169                 ResultCodeToProtoConverter.toResultCode(statusProto.getCode()),
2170                 statusProto.getMessage());
2171     }
2172 
2173     /**
2174      * Checks whether {@link IcingSearchEngine#optimize()} should be called to release resources.
2175      *
2176      * <p>This method should be only called after a mutation to local storage backend which deletes
2177      * a mass of data and could release lots resources after {@link IcingSearchEngine#optimize()}.
2178      *
2179      * <p>This method will trigger {@link IcingSearchEngine#getOptimizeInfo()} to check resources
2180      * that could be released for every {@link #CHECK_OPTIMIZE_INTERVAL} mutations.
2181      *
2182      * <p>{@link IcingSearchEngine#optimize()} should be called only if {@link
2183      * GetOptimizeInfoResultProto} shows there is enough resources could be released.
2184      *
2185      * @param mutationSize The number of how many mutations have been executed for current request.
2186      *     An inside counter will accumulates it. Once the counter reaches {@link
2187      *     #CHECK_OPTIMIZE_INTERVAL}, {@link IcingSearchEngine#getOptimizeInfo()} will be triggered
2188      *     and the counter will be reset.
2189      */
checkForOptimize(int mutationSize, @Nullable OptimizeStats.Builder builder)2190     public void checkForOptimize(int mutationSize, @Nullable OptimizeStats.Builder builder)
2191             throws AppSearchException {
2192         mReadWriteLock.writeLock().lock();
2193         try {
2194             mOptimizeIntervalCountLocked += mutationSize;
2195             if (mOptimizeIntervalCountLocked >= CHECK_OPTIMIZE_INTERVAL) {
2196                 checkForOptimize(builder);
2197             }
2198         } finally {
2199             mReadWriteLock.writeLock().unlock();
2200         }
2201     }
2202 
2203     /**
2204      * Checks whether {@link IcingSearchEngine#optimize()} should be called to release resources.
2205      *
2206      * <p>This method will directly trigger {@link IcingSearchEngine#getOptimizeInfo()} to check
2207      * resources that could be released.
2208      *
2209      * <p>{@link IcingSearchEngine#optimize()} should be called only if {@link
2210      * OptimizeStrategy#shouldOptimize(GetOptimizeInfoResultProto)} return true.
2211      */
checkForOptimize(@ullable OptimizeStats.Builder builder)2212     public void checkForOptimize(@Nullable OptimizeStats.Builder builder)
2213             throws AppSearchException {
2214         mReadWriteLock.writeLock().lock();
2215         try {
2216             GetOptimizeInfoResultProto optimizeInfo = getOptimizeInfoResultLocked();
2217             checkSuccess(optimizeInfo.getStatus());
2218             mOptimizeIntervalCountLocked = 0;
2219             if (mOptimizeStrategy.shouldOptimize(optimizeInfo)) {
2220                 optimize(builder);
2221             }
2222         } finally {
2223             mReadWriteLock.writeLock().unlock();
2224         }
2225         // TODO(b/147699081): Return OptimizeResultProto & log lost data detail once we add
2226         //  a field to indicate lost_schema and lost_documents in OptimizeResultProto.
2227         //  go/icing-library-apis.
2228     }
2229 
2230     /** Triggers {@link IcingSearchEngine#optimize()} directly. */
optimize(@ullable OptimizeStats.Builder builder)2231     public void optimize(@Nullable OptimizeStats.Builder builder) throws AppSearchException {
2232         mReadWriteLock.writeLock().lock();
2233         try {
2234             mLogUtil.piiTrace("optimize, request");
2235             OptimizeResultProto optimizeResultProto = mIcingSearchEngineLocked.optimize();
2236             mLogUtil.piiTrace(
2237                     "optimize, response", optimizeResultProto.getStatus(), optimizeResultProto);
2238             if (builder != null) {
2239                 builder.setStatusCode(statusProtoToResultCode(optimizeResultProto.getStatus()));
2240                 AppSearchLoggerHelper.copyNativeStats(
2241                         optimizeResultProto.getOptimizeStats(), builder);
2242             }
2243             checkSuccess(optimizeResultProto.getStatus());
2244         } finally {
2245             mReadWriteLock.writeLock().unlock();
2246         }
2247     }
2248 
2249     /** Remove the rewritten schema types from any result documents. */
2250     @NonNull
2251     @VisibleForTesting
rewriteSearchResultProto( @onNull SearchResultProto searchResultProto, @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap)2252     static SearchResultPage rewriteSearchResultProto(
2253             @NonNull SearchResultProto searchResultProto,
2254             @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap)
2255             throws AppSearchException {
2256         // Parallel array of package names for each document search result.
2257         List<String> packageNames = new ArrayList<>(searchResultProto.getResultsCount());
2258 
2259         // Parallel array of database names for each document search result.
2260         List<String> databaseNames = new ArrayList<>(searchResultProto.getResultsCount());
2261 
2262         SearchResultProto.Builder resultsBuilder = searchResultProto.toBuilder();
2263         for (int i = 0; i < searchResultProto.getResultsCount(); i++) {
2264             SearchResultProto.ResultProto.Builder resultBuilder =
2265                     searchResultProto.getResults(i).toBuilder();
2266             DocumentProto.Builder documentBuilder = resultBuilder.getDocument().toBuilder();
2267             String prefix = removePrefixesFromDocument(documentBuilder);
2268             packageNames.add(getPackageName(prefix));
2269             databaseNames.add(getDatabaseName(prefix));
2270             resultBuilder.setDocument(documentBuilder);
2271             resultsBuilder.setResults(i, resultBuilder);
2272         }
2273         return SearchResultToProtoConverter.toSearchResultPage(
2274                 resultsBuilder, packageNames, databaseNames, schemaMap);
2275     }
2276 
2277     @GuardedBy("mReadWriteLock")
2278     @VisibleForTesting
getOptimizeInfoResultLocked()2279     GetOptimizeInfoResultProto getOptimizeInfoResultLocked() {
2280         mLogUtil.piiTrace("getOptimizeInfo, request");
2281         GetOptimizeInfoResultProto result = mIcingSearchEngineLocked.getOptimizeInfo();
2282         mLogUtil.piiTrace("getOptimizeInfo, response", result.getStatus(), result);
2283         return result;
2284     }
2285 
2286     /**
2287      * Converts an erroneous status code from the Icing status enums to the AppSearchResult enums.
2288      *
2289      * <p>Callers should ensure that the status code is not OK or WARNING_DATA_LOSS.
2290      *
2291      * @param statusProto StatusProto with error code to translate into an {@link AppSearchResult}
2292      *     code.
2293      * @return {@link AppSearchResult} error code
2294      */
statusProtoToResultCode( @onNull StatusProto statusProto)2295     private static @AppSearchResult.ResultCode int statusProtoToResultCode(
2296             @NonNull StatusProto statusProto) {
2297         return ResultCodeToProtoConverter.toResultCode(statusProto.getCode());
2298     }
2299 }
2300