1 /*
2  * Copyright 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.appsearch.external.localstorage;
18 
19 import static android.app.appsearch.AppSearchResult.RESULT_INVALID_ARGUMENT;
20 import static android.app.appsearch.AppSearchResult.RESULT_NOT_FOUND;
21 import static android.app.appsearch.AppSearchResult.RESULT_OUT_OF_SPACE;
22 import static android.app.appsearch.testutil.AppSearchTestUtils.calculateDigest;
23 import static android.app.appsearch.testutil.AppSearchTestUtils.createMockVisibilityChecker;
24 import static android.app.appsearch.testutil.AppSearchTestUtils.generateRandomBytes;
25 
26 import static com.android.server.appsearch.external.localstorage.util.PrefixUtil.addPrefixToDocument;
27 import static com.android.server.appsearch.external.localstorage.util.PrefixUtil.createPrefix;
28 import static com.android.server.appsearch.external.localstorage.util.PrefixUtil.removePrefixesFromDocument;
29 import static com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityStore.BLOB_ANDROID_V_OVERLAY_DATABASE_NAME;
30 import static com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityStore.BLOB_VISIBILITY_DATABASE_NAME;
31 import static com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityStore.DOCUMENT_ANDROID_V_OVERLAY_DATABASE_NAME;
32 import static com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityStore.DOCUMENT_VISIBILITY_DATABASE_NAME;
33 import static com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityStore.VISIBILITY_PACKAGE_NAME;
34 
35 import static com.google.common.truth.Truth.assertThat;
36 
37 import static org.junit.Assert.assertThrows;
38 
39 import android.annotation.NonNull;
40 import android.app.appsearch.AppSearchBlobHandle;
41 import android.app.appsearch.AppSearchResult;
42 import android.app.appsearch.AppSearchSchema;
43 import android.app.appsearch.Features;
44 import android.app.appsearch.GenericDocument;
45 import android.app.appsearch.GetSchemaResponse;
46 import android.app.appsearch.InternalSetSchemaResponse;
47 import android.app.appsearch.InternalVisibilityConfig;
48 import android.app.appsearch.JoinSpec;
49 import android.app.appsearch.PackageIdentifier;
50 import android.app.appsearch.SchemaVisibilityConfig;
51 import android.app.appsearch.SearchResult;
52 import android.app.appsearch.SearchResultPage;
53 import android.app.appsearch.SearchSpec;
54 import android.app.appsearch.SearchSuggestionResult;
55 import android.app.appsearch.SearchSuggestionSpec;
56 import android.app.appsearch.SetSchemaResponse;
57 import android.app.appsearch.StorageInfo;
58 import android.app.appsearch.exceptions.AppSearchException;
59 import android.app.appsearch.observer.DocumentChangeInfo;
60 import android.app.appsearch.observer.ObserverSpec;
61 import android.app.appsearch.observer.SchemaChangeInfo;
62 import android.app.appsearch.testutil.AppSearchTestUtils;
63 import android.app.appsearch.testutil.TestObserverCallback;
64 import android.content.Context;
65 import android.os.ParcelFileDescriptor;
66 import android.platform.test.annotations.RequiresFlagsDisabled;
67 import android.platform.test.annotations.RequiresFlagsEnabled;
68 import android.util.ArrayMap;
69 import android.util.ArraySet;
70 
71 import androidx.test.core.app.ApplicationProvider;
72 import androidx.test.filters.FlakyTest;
73 
74 import com.android.appsearch.flags.Flags;
75 import com.android.server.appsearch.appsearch.proto.AndroidVOverlayProto;
76 import com.android.server.appsearch.appsearch.proto.PackageIdentifierProto;
77 import com.android.server.appsearch.appsearch.proto.VisibilityConfigProto;
78 import com.android.server.appsearch.external.localstorage.stats.InitializeStats;
79 import com.android.server.appsearch.external.localstorage.stats.OptimizeStats;
80 import com.android.server.appsearch.external.localstorage.util.PrefixUtil;
81 import com.android.server.appsearch.external.localstorage.visibilitystore.CallerAccess;
82 import com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityChecker;
83 import com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityStore;
84 import com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityToDocumentConverter;
85 import com.android.server.appsearch.icing.proto.DebugInfoProto;
86 import com.android.server.appsearch.icing.proto.DebugInfoVerbosity;
87 import com.android.server.appsearch.icing.proto.DocumentProto;
88 import com.android.server.appsearch.icing.proto.GetOptimizeInfoResultProto;
89 import com.android.server.appsearch.icing.proto.PersistType;
90 import com.android.server.appsearch.icing.proto.PropertyConfigProto;
91 import com.android.server.appsearch.icing.proto.PropertyProto;
92 import com.android.server.appsearch.icing.proto.PutResultProto;
93 import com.android.server.appsearch.icing.proto.SchemaProto;
94 import com.android.server.appsearch.icing.proto.SchemaTypeConfigProto;
95 import com.android.server.appsearch.icing.proto.StatusProto;
96 import com.android.server.appsearch.icing.proto.StorageInfoProto;
97 import com.android.server.appsearch.icing.proto.StringIndexingConfig;
98 import com.android.server.appsearch.icing.proto.TermMatchType;
99 import com.android.server.appsearch.protobuf.ByteString;
100 
101 import com.google.common.collect.ImmutableList;
102 import com.google.common.collect.ImmutableMap;
103 import com.google.common.collect.ImmutableSet;
104 import com.google.common.util.concurrent.MoreExecutors;
105 
106 import org.junit.After;
107 import org.junit.Before;
108 import org.junit.Ignore;
109 import org.junit.Rule;
110 import org.junit.Test;
111 import org.junit.rules.RuleChain;
112 import org.junit.rules.TemporaryFolder;
113 
114 import java.io.File;
115 import java.io.IOException;
116 import java.io.InputStream;
117 import java.io.OutputStream;
118 import java.util.ArrayList;
119 import java.util.Collections;
120 import java.util.List;
121 import java.util.Map;
122 import java.util.Set;
123 
124 @SuppressWarnings("GuardedBy")
125 public class AppSearchImplTest {
126     /**
127      * Always trigger optimize in this class. OptimizeStrategy will be tested in its own test class.
128      */
129     private static final OptimizeStrategy ALWAYS_OPTIMIZE = optimizeInfo -> true;
130 
131     @Rule public final RuleChain mRuleChain = AppSearchTestUtils.createCommonTestRules();
132 
133     @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
134     private File mAppSearchDir;
135 
136     private final Context mContext = ApplicationProvider.getApplicationContext();
137 
138     // The caller access for this package
139     private final CallerAccess mSelfCallerAccess = new CallerAccess(mContext.getPackageName());
140 
141     private AppSearchImpl mAppSearchImpl;
142     private AppSearchConfig mUnlimitedConfig =
143             new AppSearchConfigImpl(
144                     new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig());
145 
146     @Before
setUp()147     public void setUp() throws Exception {
148         mAppSearchDir = mTemporaryFolder.newFolder();
149         mAppSearchImpl =
150                 AppSearchImpl.create(
151                         mAppSearchDir,
152                         mUnlimitedConfig,
153                         /* initStatsBuilder= */ null,
154                         /* visibilityChecker= */ null,
155                         /* revocableFileDescriptorStore= */ null,
156                         ALWAYS_OPTIMIZE);
157     }
158 
159     @After
tearDown()160     public void tearDown() {
161         mAppSearchImpl.close();
162     }
163 
164     /**
165      * Ensure that we can rewrite an incoming schema type by adding the database as a prefix. While
166      * also keeping any other existing schema types that may already be part of Icing's persisted
167      * schema.
168      */
169     @Test
testRewriteSchema_addType()170     public void testRewriteSchema_addType() throws Exception {
171         SchemaProto.Builder existingSchemaBuilder =
172                 SchemaProto.newBuilder()
173                         .addTypes(
174                                 SchemaTypeConfigProto.newBuilder()
175                                         .setSchemaType("package$existingDatabase/Foo")
176                                         .build());
177 
178         // Create a copy so we can modify it.
179         List<SchemaTypeConfigProto> existingTypes =
180                 new ArrayList<>(existingSchemaBuilder.getTypesList());
181         SchemaTypeConfigProto schemaTypeConfigProto1 =
182                 SchemaTypeConfigProto.newBuilder().setSchemaType("Foo").build();
183         SchemaTypeConfigProto schemaTypeConfigProto2 =
184                 SchemaTypeConfigProto.newBuilder()
185                         .setSchemaType("TestType")
186                         .addProperties(
187                                 PropertyConfigProto.newBuilder()
188                                         .setPropertyName("subject")
189                                         .setDataType(PropertyConfigProto.DataType.Code.STRING)
190                                         .setCardinality(
191                                                 PropertyConfigProto.Cardinality.Code.OPTIONAL)
192                                         .setStringIndexingConfig(
193                                                 StringIndexingConfig.newBuilder()
194                                                         .setTokenizerType(
195                                                                 StringIndexingConfig.TokenizerType
196                                                                         .Code.PLAIN)
197                                                         .setTermMatchType(TermMatchType.Code.PREFIX)
198                                                         .build())
199                                         .build())
200                         .addProperties(
201                                 PropertyConfigProto.newBuilder()
202                                         .setPropertyName("link")
203                                         .setDataType(PropertyConfigProto.DataType.Code.DOCUMENT)
204                                         .setCardinality(
205                                                 PropertyConfigProto.Cardinality.Code.OPTIONAL)
206                                         .setSchemaType("RefType")
207                                         .build())
208                         .build();
209         SchemaTypeConfigProto schemaTypeConfigProto3 =
210                 SchemaTypeConfigProto.newBuilder()
211                         .setSchemaType("RefType")
212                         .addParentTypes("Foo")
213                         .build();
214         SchemaProto newSchema =
215                 SchemaProto.newBuilder()
216                         .addTypes(schemaTypeConfigProto1)
217                         .addTypes(schemaTypeConfigProto2)
218                         .addTypes(schemaTypeConfigProto3)
219                         .build();
220 
221         AppSearchImpl.RewrittenSchemaResults rewrittenSchemaResults =
222                 AppSearchImpl.rewriteSchema(
223                         createPrefix("package", "newDatabase"), existingSchemaBuilder, newSchema);
224 
225         // We rewrote all the new types that were added. And nothing was removed.
226         assertThat(rewrittenSchemaResults.mRewrittenPrefixedTypes.keySet())
227                 .containsExactly(
228                         "package$newDatabase/Foo",
229                         "package$newDatabase/TestType",
230                         "package$newDatabase/RefType");
231         assertThat(
232                         rewrittenSchemaResults
233                                 .mRewrittenPrefixedTypes
234                                 .get("package$newDatabase/Foo")
235                                 .getSchemaType())
236                 .isEqualTo("package$newDatabase/Foo");
237         assertThat(
238                         rewrittenSchemaResults
239                                 .mRewrittenPrefixedTypes
240                                 .get("package$newDatabase/TestType")
241                                 .getSchemaType())
242                 .isEqualTo("package$newDatabase/TestType");
243         assertThat(
244                         rewrittenSchemaResults
245                                 .mRewrittenPrefixedTypes
246                                 .get("package$newDatabase/RefType")
247                                 .getSchemaType())
248                 .isEqualTo("package$newDatabase/RefType");
249         assertThat(rewrittenSchemaResults.mDeletedPrefixedTypes).isEmpty();
250 
251         SchemaProto expectedSchema =
252                 SchemaProto.newBuilder()
253                         .addTypes(
254                                 SchemaTypeConfigProto.newBuilder()
255                                         .setSchemaType("package$newDatabase/Foo")
256                                         .build())
257                         .addTypes(
258                                 SchemaTypeConfigProto.newBuilder()
259                                         .setSchemaType("package$newDatabase/TestType")
260                                         .addProperties(
261                                                 PropertyConfigProto.newBuilder()
262                                                         .setPropertyName("subject")
263                                                         .setDataType(
264                                                                 PropertyConfigProto.DataType.Code
265                                                                         .STRING)
266                                                         .setCardinality(
267                                                                 PropertyConfigProto.Cardinality.Code
268                                                                         .OPTIONAL)
269                                                         .setStringIndexingConfig(
270                                                                 StringIndexingConfig.newBuilder()
271                                                                         .setTokenizerType(
272                                                                                 StringIndexingConfig
273                                                                                         .TokenizerType
274                                                                                         .Code.PLAIN)
275                                                                         .setTermMatchType(
276                                                                                 TermMatchType.Code
277                                                                                         .PREFIX)
278                                                                         .build())
279                                                         .build())
280                                         .addProperties(
281                                                 PropertyConfigProto.newBuilder()
282                                                         .setPropertyName("link")
283                                                         .setDataType(
284                                                                 PropertyConfigProto.DataType.Code
285                                                                         .DOCUMENT)
286                                                         .setCardinality(
287                                                                 PropertyConfigProto.Cardinality.Code
288                                                                         .OPTIONAL)
289                                                         .setSchemaType(
290                                                                 "package$newDatabase/RefType")
291                                                         .build())
292                                         .build())
293                         .addTypes(
294                                 SchemaTypeConfigProto.newBuilder()
295                                         .setSchemaType("package$newDatabase/RefType")
296                                         .addParentTypes("package$newDatabase/Foo")
297                                         .build())
298                         .build();
299 
300         existingTypes.addAll(expectedSchema.getTypesList());
301         assertThat(existingSchemaBuilder.getTypesList()).containsExactlyElementsIn(existingTypes);
302     }
303 
304     /**
305      * Ensure that we track all types that were rewritten in the input schema. Even if they were not
306      * technically "added" to the existing schema.
307      */
308     @Test
testRewriteSchema_rewriteType()309     public void testRewriteSchema_rewriteType() throws Exception {
310         SchemaProto.Builder existingSchemaBuilder =
311                 SchemaProto.newBuilder()
312                         .addTypes(
313                                 SchemaTypeConfigProto.newBuilder()
314                                         .setSchemaType("package$existingDatabase/Foo")
315                                         .build());
316 
317         SchemaProto newSchema =
318                 SchemaProto.newBuilder()
319                         .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("Foo").build())
320                         .build();
321 
322         AppSearchImpl.RewrittenSchemaResults rewrittenSchemaResults =
323                 AppSearchImpl.rewriteSchema(
324                         createPrefix("package", "existingDatabase"),
325                         existingSchemaBuilder,
326                         newSchema);
327 
328         // Nothing was removed, but the method did rewrite the type name.
329         assertThat(rewrittenSchemaResults.mRewrittenPrefixedTypes.keySet())
330                 .containsExactly("package$existingDatabase/Foo");
331         assertThat(rewrittenSchemaResults.mDeletedPrefixedTypes).isEmpty();
332 
333         // Same schema since nothing was added.
334         SchemaProto expectedSchema = existingSchemaBuilder.build();
335         assertThat(existingSchemaBuilder.getTypesList())
336                 .containsExactlyElementsIn(expectedSchema.getTypesList());
337     }
338 
339     /**
340      * Ensure that we track which types from the existing schema are deleted when a new schema is
341      * set.
342      */
343     @Test
testRewriteSchema_deleteType()344     public void testRewriteSchema_deleteType() throws Exception {
345         SchemaProto.Builder existingSchemaBuilder =
346                 SchemaProto.newBuilder()
347                         .addTypes(
348                                 SchemaTypeConfigProto.newBuilder()
349                                         .setSchemaType("package$existingDatabase/Foo")
350                                         .build());
351 
352         SchemaProto newSchema =
353                 SchemaProto.newBuilder()
354                         .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("Bar").build())
355                         .build();
356 
357         AppSearchImpl.RewrittenSchemaResults rewrittenSchemaResults =
358                 AppSearchImpl.rewriteSchema(
359                         createPrefix("package", "existingDatabase"),
360                         existingSchemaBuilder,
361                         newSchema);
362 
363         // Bar type was rewritten, but Foo ended up being deleted since it wasn't included in the
364         // new schema.
365         assertThat(rewrittenSchemaResults.mRewrittenPrefixedTypes)
366                 .containsKey("package$existingDatabase/Bar");
367         assertThat(rewrittenSchemaResults.mRewrittenPrefixedTypes.keySet().size()).isEqualTo(1);
368         assertThat(rewrittenSchemaResults.mDeletedPrefixedTypes)
369                 .containsExactly("package$existingDatabase/Foo");
370 
371         // Same schema since nothing was added.
372         SchemaProto expectedSchema =
373                 SchemaProto.newBuilder()
374                         .addTypes(
375                                 SchemaTypeConfigProto.newBuilder()
376                                         .setSchemaType("package$existingDatabase/Bar")
377                                         .build())
378                         .build();
379 
380         assertThat(existingSchemaBuilder.getTypesList())
381                 .containsExactlyElementsIn(expectedSchema.getTypesList());
382     }
383 
384     @Test
testAddDocumentTypePrefix()385     public void testAddDocumentTypePrefix() {
386         DocumentProto insideDocument =
387                 DocumentProto.newBuilder()
388                         .setUri("inside-id")
389                         .setSchema("type")
390                         .setNamespace("namespace")
391                         .build();
392         DocumentProto documentProto =
393                 DocumentProto.newBuilder()
394                         .setUri("id")
395                         .setSchema("type")
396                         .setNamespace("namespace")
397                         .addProperties(PropertyProto.newBuilder().addDocumentValues(insideDocument))
398                         .build();
399 
400         DocumentProto expectedInsideDocument =
401                 DocumentProto.newBuilder()
402                         .setUri("inside-id")
403                         .setSchema("package$databaseName/type")
404                         .setNamespace("package$databaseName/namespace")
405                         .build();
406         DocumentProto expectedDocumentProto =
407                 DocumentProto.newBuilder()
408                         .setUri("id")
409                         .setSchema("package$databaseName/type")
410                         .setNamespace("package$databaseName/namespace")
411                         .addProperties(
412                                 PropertyProto.newBuilder()
413                                         .addDocumentValues(expectedInsideDocument))
414                         .build();
415 
416         DocumentProto.Builder actualDocument = documentProto.toBuilder();
417         addPrefixToDocument(actualDocument, createPrefix("package", "databaseName"));
418         assertThat(actualDocument.build()).isEqualTo(expectedDocumentProto);
419     }
420 
421     @Test
testRemoveDocumentTypePrefixes()422     public void testRemoveDocumentTypePrefixes() throws Exception {
423         DocumentProto insideDocument =
424                 DocumentProto.newBuilder()
425                         .setUri("inside-id")
426                         .setSchema("package$databaseName/type")
427                         .setNamespace("package$databaseName/namespace")
428                         .build();
429         DocumentProto documentProto =
430                 DocumentProto.newBuilder()
431                         .setUri("id")
432                         .setSchema("package$databaseName/type")
433                         .setNamespace("package$databaseName/namespace")
434                         .addProperties(PropertyProto.newBuilder().addDocumentValues(insideDocument))
435                         .build();
436 
437         DocumentProto expectedInsideDocument =
438                 DocumentProto.newBuilder()
439                         .setUri("inside-id")
440                         .setSchema("type")
441                         .setNamespace("namespace")
442                         .build();
443 
444         DocumentProto expectedDocumentProto =
445                 DocumentProto.newBuilder()
446                         .setUri("id")
447                         .setSchema("type")
448                         .setNamespace("namespace")
449                         .addProperties(
450                                 PropertyProto.newBuilder()
451                                         .addDocumentValues(expectedInsideDocument))
452                         .build();
453 
454         DocumentProto.Builder actualDocument = documentProto.toBuilder();
455         assertThat(removePrefixesFromDocument(actualDocument)).isEqualTo("package$databaseName/");
456         assertThat(actualDocument.build()).isEqualTo(expectedDocumentProto);
457     }
458 
459     @Test
testRemoveDatabasesFromDocumentThrowsException()460     public void testRemoveDatabasesFromDocumentThrowsException() {
461         // Set two different database names in the document, which should never happen
462         DocumentProto documentProto =
463                 DocumentProto.newBuilder()
464                         .setUri("id")
465                         .setSchema("prefix1/type")
466                         .setNamespace("prefix2/namespace")
467                         .build();
468 
469         DocumentProto.Builder actualDocument = documentProto.toBuilder();
470         AppSearchException e =
471                 assertThrows(
472                         AppSearchException.class, () -> removePrefixesFromDocument(actualDocument));
473         assertThat(e).hasMessageThat().contains("Found unexpected multiple prefix names");
474     }
475 
476     @Test
testNestedRemoveDatabasesFromDocumentThrowsException()477     public void testNestedRemoveDatabasesFromDocumentThrowsException() {
478         // Set two different database names in the outer and inner document, which should never
479         // happen.
480         DocumentProto insideDocument =
481                 DocumentProto.newBuilder()
482                         .setUri("inside-id")
483                         .setSchema("prefix1/type")
484                         .setNamespace("prefix1/namespace")
485                         .build();
486         DocumentProto documentProto =
487                 DocumentProto.newBuilder()
488                         .setUri("id")
489                         .setSchema("prefix2/type")
490                         .setNamespace("prefix2/namespace")
491                         .addProperties(PropertyProto.newBuilder().addDocumentValues(insideDocument))
492                         .build();
493 
494         DocumentProto.Builder actualDocument = documentProto.toBuilder();
495         AppSearchException e =
496                 assertThrows(
497                         AppSearchException.class, () -> removePrefixesFromDocument(actualDocument));
498         assertThat(e).hasMessageThat().contains("Found unexpected multiple prefix names");
499     }
500 
501     @Test
testTriggerCheckOptimizeByMutationSize()502     public void testTriggerCheckOptimizeByMutationSize() throws Exception {
503         // Insert schema
504         List<AppSearchSchema> schemas =
505                 Collections.singletonList(new AppSearchSchema.Builder("type").build());
506         InternalSetSchemaResponse internalSetSchemaResponse =
507                 mAppSearchImpl.setSchema(
508                         "package",
509                         "database",
510                         schemas,
511                         /* visibilityConfigs= */ Collections.emptyList(),
512                         /* forceOverride= */ false,
513                         /* version= */ 0,
514                         /* setSchemaStatsBuilder= */ null);
515         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
516 
517         // Insert a document and then remove it to generate garbage.
518         GenericDocument document = new GenericDocument.Builder<>("namespace", "id", "type").build();
519         mAppSearchImpl.putDocument(
520                 "package",
521                 "database",
522                 document,
523                 /* sendChangeNotifications= */ false,
524                 /* logger= */ null);
525         mAppSearchImpl.remove(
526                 "package", "database", "namespace", "id", /* removeStatsBuilder= */ null);
527 
528         // Verify there is garbage documents.
529         GetOptimizeInfoResultProto optimizeInfo = mAppSearchImpl.getOptimizeInfoResultLocked();
530         assertThat(optimizeInfo.getOptimizableDocs()).isEqualTo(1);
531 
532         // Increase mutation counter and stop before reach the threshold
533         mAppSearchImpl.checkForOptimize(
534                 AppSearchImpl.CHECK_OPTIMIZE_INTERVAL - 1, /* builder= */ null);
535 
536         // Verify the optimize() isn't triggered.
537         optimizeInfo = mAppSearchImpl.getOptimizeInfoResultLocked();
538         assertThat(optimizeInfo.getOptimizableDocs()).isEqualTo(1);
539 
540         // Increase the counter and reach the threshold, optimize() should be triggered.
541         OptimizeStats.Builder builder = new OptimizeStats.Builder();
542         mAppSearchImpl.checkForOptimize(/* mutateBatchSize= */ 1, builder);
543 
544         // Verify optimize() is triggered.
545         optimizeInfo = mAppSearchImpl.getOptimizeInfoResultLocked();
546         assertThat(optimizeInfo.getOptimizableDocs()).isEqualTo(0);
547         assertThat(optimizeInfo.getEstimatedOptimizableBytes()).isEqualTo(0);
548 
549         // Verify the stats have been set.
550         OptimizeStats oStats = builder.build();
551         assertThat(oStats.getOriginalDocumentCount()).isEqualTo(1);
552         assertThat(oStats.getDeletedDocumentCount()).isEqualTo(1);
553     }
554 
555     @Test
testReset()556     public void testReset() throws Exception {
557         // Insert schema
558         List<AppSearchSchema> schemas =
559                 ImmutableList.of(
560                         new AppSearchSchema.Builder("Type1").build(),
561                         new AppSearchSchema.Builder("Type2").build());
562         InternalSetSchemaResponse internalSetSchemaResponse =
563                 mAppSearchImpl.setSchema(
564                         mContext.getPackageName(),
565                         "database1",
566                         schemas,
567                         /* visibilityConfigs= */ Collections.emptyList(),
568                         /* forceOverride= */ false,
569                         /* version= */ 0,
570                         /* setSchemaStatsBuilder= */ null);
571         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
572 
573         // Insert a valid doc
574         GenericDocument validDoc =
575                 new GenericDocument.Builder<>("namespace1", "id1", "Type1").build();
576         mAppSearchImpl.putDocument(
577                 mContext.getPackageName(),
578                 "database1",
579                 validDoc,
580                 /* sendChangeNotifications= */ false,
581                 /* logger= */ null);
582 
583         // Query it via global query. We use the same code again later so this is to make sure we
584         // have our global query configured right.
585         SearchResultPage results =
586                 mAppSearchImpl.globalQuery(
587                         /* queryExpression= */ "",
588                         new SearchSpec.Builder().addFilterSchemas("Type1").build(),
589                         mSelfCallerAccess,
590                         /* logger= */ null);
591         assertThat(results.getResults()).hasSize(1);
592         assertThat(results.getResults().get(0).getGenericDocument()).isEqualTo(validDoc);
593 
594         // Create a doc with a malformed namespace
595         DocumentProto invalidDoc =
596                 DocumentProto.newBuilder()
597                         .setNamespace("invalidNamespace")
598                         .setUri("id2")
599                         .setSchema(mContext.getPackageName() + "$database1/Type1")
600                         .build();
601         AppSearchException e =
602                 assertThrows(
603                         AppSearchException.class,
604                         () -> PrefixUtil.getPrefix(invalidDoc.getNamespace()));
605         assertThat(e)
606                 .hasMessageThat()
607                 .isEqualTo(
608                         "The prefixed value \"invalidNamespace\" doesn't contain a valid database"
609                             + " name");
610 
611         // Insert the invalid doc with an invalid namespace right into icing
612         PutResultProto putResultProto = mAppSearchImpl.mIcingSearchEngineLocked.put(invalidDoc);
613         assertThat(putResultProto.getStatus().getCode()).isEqualTo(StatusProto.Code.OK);
614 
615         // Initialize AppSearchImpl. This should cause a reset.
616         InitializeStats.Builder initStatsBuilder = new InitializeStats.Builder();
617         mAppSearchImpl.close();
618         mAppSearchImpl =
619                 AppSearchImpl.create(
620                         mAppSearchDir,
621                         new AppSearchConfigImpl(
622                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
623                         initStatsBuilder,
624                         /* visibilityChecker= */ null,
625                         /* revocableFileDescriptorStore= */ null,
626                         ALWAYS_OPTIMIZE);
627 
628         // Check recovery state
629         InitializeStats initStats = initStatsBuilder.build();
630         assertThat(initStats).isNotNull();
631         assertThat(initStats.getStatusCode()).isEqualTo(AppSearchResult.RESULT_INTERNAL_ERROR);
632         assertThat(initStats.hasDeSync()).isFalse();
633         assertThat(initStats.getDocumentStoreRecoveryCause())
634                 .isEqualTo(InitializeStats.RECOVERY_CAUSE_NONE);
635         assertThat(initStats.getIndexRestorationCause())
636                 .isEqualTo(InitializeStats.RECOVERY_CAUSE_NONE);
637         assertThat(initStats.getSchemaStoreRecoveryCause())
638                 .isEqualTo(InitializeStats.RECOVERY_CAUSE_NONE);
639         assertThat(initStats.getDocumentStoreDataStatus())
640                 .isEqualTo(InitializeStats.DOCUMENT_STORE_DATA_STATUS_NO_DATA_LOSS);
641         assertThat(initStats.hasReset()).isTrue();
642         assertThat(initStats.getResetStatusCode()).isEqualTo(AppSearchResult.RESULT_OK);
643 
644         // Make sure all our data is gone
645         assertThat(
646                         mAppSearchImpl
647                                 .getSchema(
648                                         /* packageName= */ mContext.getPackageName(),
649                                         /* databaseName= */ "database1",
650                                         /* callerAccess= */ mSelfCallerAccess)
651                                 .getSchemas())
652                 .isEmpty();
653         results =
654                 mAppSearchImpl.globalQuery(
655                         /* queryExpression= */ "",
656                         new SearchSpec.Builder().addFilterSchemas("Type1").build(),
657                         mSelfCallerAccess,
658                         /* logger= */ null);
659         assertThat(results.getResults()).isEmpty();
660 
661         // Make sure the index can now be used successfully
662         internalSetSchemaResponse =
663                 mAppSearchImpl.setSchema(
664                         mContext.getPackageName(),
665                         "database1",
666                         Collections.singletonList(new AppSearchSchema.Builder("Type1").build()),
667                         /* visibilityConfigs= */ Collections.emptyList(),
668                         /* forceOverride= */ false,
669                         /* version= */ 0,
670                         /* setSchemaStatsBuilder= */ null);
671         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
672 
673         // Insert a valid doc
674         mAppSearchImpl.putDocument(
675                 mContext.getPackageName(),
676                 "database1",
677                 validDoc,
678                 /* sendChangeNotifications= */ false,
679                 /* logger= */ null);
680 
681         // Query it via global query.
682         results =
683                 mAppSearchImpl.globalQuery(
684                         /* queryExpression= */ "",
685                         new SearchSpec.Builder().addFilterSchemas("Type1").build(),
686                         mSelfCallerAccess,
687                         /* logger= */ null);
688         assertThat(results.getResults()).hasSize(1);
689         assertThat(results.getResults().get(0).getGenericDocument()).isEqualTo(validDoc);
690     }
691 
692     @Test
testQueryEmptyDatabase()693     public void testQueryEmptyDatabase() throws Exception {
694         SearchSpec searchSpec =
695                 new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE).build();
696         SearchResultPage searchResultPage =
697                 mAppSearchImpl.query(
698                         "package", "EmptyDatabase", "", searchSpec, /* logger= */ null);
699         assertThat(searchResultPage.getResults()).isEmpty();
700     }
701 
702     /**
703      * TODO(b/169883602): This should be an integration test at the cts-level. This is a short-term
704      * test until we have official support for multiple-apps indexing at once.
705      */
706     @Test
testQueryWithMultiplePackages_noPackageFilters()707     public void testQueryWithMultiplePackages_noPackageFilters() throws Exception {
708         // Insert package1 schema
709         List<AppSearchSchema> schema1 =
710                 ImmutableList.of(new AppSearchSchema.Builder("schema1").build());
711         InternalSetSchemaResponse internalSetSchemaResponse =
712                 mAppSearchImpl.setSchema(
713                         "package1",
714                         "database1",
715                         schema1,
716                         /* visibilityConfigs= */ Collections.emptyList(),
717                         /* forceOverride= */ false,
718                         /* version= */ 0,
719                         /* setSchemaStatsBuilder= */ null);
720         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
721 
722         // Insert package2 schema
723         List<AppSearchSchema> schema2 =
724                 ImmutableList.of(new AppSearchSchema.Builder("schema2").build());
725         internalSetSchemaResponse =
726                 mAppSearchImpl.setSchema(
727                         "package2",
728                         "database2",
729                         schema2,
730                         /* visibilityConfigs= */ Collections.emptyList(),
731                         /* forceOverride= */ false,
732                         /* version= */ 0,
733                         /* setSchemaStatsBuilder= */ null);
734         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
735 
736         // Insert package1 document
737         GenericDocument document =
738                 new GenericDocument.Builder<>("namespace", "id", "schema1").build();
739         mAppSearchImpl.putDocument(
740                 "package1",
741                 "database1",
742                 document,
743                 /* sendChangeNotifications= */ false,
744                 /* logger= */ null);
745 
746         // No query filters specified, package2 shouldn't be able to query for package1's documents.
747         SearchSpec searchSpec =
748                 new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE).build();
749         SearchResultPage searchResultPage =
750                 mAppSearchImpl.query("package2", "database2", "", searchSpec, /* logger= */ null);
751         assertThat(searchResultPage.getResults()).isEmpty();
752 
753         // Insert package2 document
754         document = new GenericDocument.Builder<>("namespace", "id", "schema2").build();
755         mAppSearchImpl.putDocument(
756                 "package2",
757                 "database2",
758                 document,
759                 /* sendChangeNotifications= */ false,
760                 /* logger= */ null);
761 
762         // No query filters specified. package2 should only get its own documents back.
763         searchResultPage =
764                 mAppSearchImpl.query("package2", "database2", "", searchSpec, /* logger= */ null);
765         assertThat(searchResultPage.getResults()).hasSize(1);
766         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document);
767     }
768 
769     /**
770      * TODO(b/169883602): This should be an integration test at the cts-level. This is a short-term
771      * test until we have official support for multiple-apps indexing at once.
772      */
773     @Test
testQueryWithMultiplePackages_withPackageFilters()774     public void testQueryWithMultiplePackages_withPackageFilters() throws Exception {
775         // Insert package1 schema
776         List<AppSearchSchema> schema1 =
777                 ImmutableList.of(new AppSearchSchema.Builder("schema1").build());
778         InternalSetSchemaResponse internalSetSchemaResponse =
779                 mAppSearchImpl.setSchema(
780                         "package1",
781                         "database1",
782                         schema1,
783                         /* visibilityConfigs= */ Collections.emptyList(),
784                         /* forceOverride= */ false,
785                         /* version= */ 0,
786                         /* setSchemaStatsBuilder= */ null);
787         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
788 
789         // Insert package2 schema
790         List<AppSearchSchema> schema2 =
791                 ImmutableList.of(new AppSearchSchema.Builder("schema2").build());
792         internalSetSchemaResponse =
793                 mAppSearchImpl.setSchema(
794                         "package2",
795                         "database2",
796                         schema2,
797                         /* visibilityConfigs= */ Collections.emptyList(),
798                         /* forceOverride= */ false,
799                         /* version= */ 0,
800                         /* setSchemaStatsBuilder= */ null);
801         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
802 
803         // Insert package1 document
804         GenericDocument document =
805                 new GenericDocument.Builder<>("namespace", "id", "schema1").build();
806         mAppSearchImpl.putDocument(
807                 "package1",
808                 "database1",
809                 document,
810                 /* sendChangeNotifications= */ false,
811                 /* logger= */ null);
812 
813         // "package1" filter specified, but package2 shouldn't be able to query for package1's
814         // documents.
815         SearchSpec searchSpec =
816                 new SearchSpec.Builder()
817                         .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
818                         .addFilterPackageNames("package1")
819                         .build();
820         SearchResultPage searchResultPage =
821                 mAppSearchImpl.query("package2", "database2", "", searchSpec, /* logger= */ null);
822         assertThat(searchResultPage.getResults()).isEmpty();
823 
824         // Insert package2 document
825         document = new GenericDocument.Builder<>("namespace", "id", "schema2").build();
826         mAppSearchImpl.putDocument(
827                 "package2",
828                 "database2",
829                 document,
830                 /* sendChangeNotifications= */ false,
831                 /* logger= */ null);
832 
833         // "package2" filter specified, package2 should only get its own documents back.
834         searchSpec =
835                 new SearchSpec.Builder()
836                         .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
837                         .addFilterPackageNames("package2")
838                         .build();
839         searchResultPage =
840                 mAppSearchImpl.query("package2", "database2", "", searchSpec, /* logger= */ null);
841         assertThat(searchResultPage.getResults()).hasSize(1);
842         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document);
843     }
844 
845     @Test
testGlobalQuery_emptyPackage()846     public void testGlobalQuery_emptyPackage() throws Exception {
847         SearchSpec searchSpec =
848                 new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE).build();
849         SearchResultPage searchResultPage =
850                 mAppSearchImpl.globalQuery(
851                         /* queryExpression= */ "",
852                         searchSpec,
853                         new CallerAccess(/* callingPackageName= */ ""),
854                         /* logger= */ null);
855         assertThat(searchResultPage.getResults()).isEmpty();
856     }
857 
858     @Test
testGlobalQuery_withJoin_packageFilter()859     public void testGlobalQuery_withJoin_packageFilter() throws Exception {
860         // Create a new mAppSearchImpl with a mock Visibility Checker
861         mAppSearchImpl.close();
862         File tempFolder = mTemporaryFolder.newFolder();
863         // We need to share across packages
864         VisibilityChecker mockVisibilityChecker = createMockVisibilityChecker(true);
865         mAppSearchImpl =
866                 AppSearchImpl.create(
867                         tempFolder,
868                         new AppSearchConfigImpl(
869                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
870                         /* initStatsBuilder= */ null,
871                         mockVisibilityChecker,
872                         /* revocableFileDescriptorStore= */ null,
873                         ALWAYS_OPTIMIZE);
874 
875         // Insert package1 schema
876         List<AppSearchSchema> personSchema =
877                 ImmutableList.of(new AppSearchSchema.Builder("personSchema").build());
878         InternalSetSchemaResponse internalSetSchemaResponse =
879                 mAppSearchImpl.setSchema(
880                         "package1",
881                         "database1",
882                         personSchema,
883                         /* visibilityConfigs= */ Collections.emptyList(),
884                         /* forceOverride= */ false,
885                         /* version= */ 0,
886                         /* setSchemaStatsBuilder= */ null);
887         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
888 
889         AppSearchSchema.StringPropertyConfig personField =
890                 new AppSearchSchema.StringPropertyConfig.Builder("personId")
891                         .setJoinableValueType(
892                                 AppSearchSchema.StringPropertyConfig
893                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
894                         .build();
895         // Insert package2 schema
896         List<AppSearchSchema> callSchema =
897                 ImmutableList.of(
898                         new AppSearchSchema.Builder("callSchema").addProperty(personField).build());
899         internalSetSchemaResponse =
900                 mAppSearchImpl.setSchema(
901                         "package2",
902                         "database2",
903                         callSchema,
904                         /* visibilityConfigs= */ Collections.emptyList(),
905                         /* forceOverride= */ true,
906                         /* version= */ 0,
907                         /* setSchemaStatsBuilder= */ null);
908         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
909 
910         List<AppSearchSchema> textSchema =
911                 ImmutableList.of(
912                         new AppSearchSchema.Builder("textSchema").addProperty(personField).build());
913         internalSetSchemaResponse =
914                 mAppSearchImpl.setSchema(
915                         "package3",
916                         "database3",
917                         textSchema,
918                         /* visibilityConfigs= */ Collections.emptyList(),
919                         /* forceOverride= */ true,
920                         /* version= */ 0,
921                         /* setSchemaStatsBuilder= */ null);
922         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
923 
924         // Insert package1 document
925         GenericDocument person =
926                 new GenericDocument.Builder<>("namespace", "id", "personSchema").build();
927         mAppSearchImpl.putDocument(
928                 "package1",
929                 "database1",
930                 person,
931                 /* sendChangeNotifications= */ false,
932                 /* logger= */ null);
933 
934         // Insert package2 document
935         GenericDocument call =
936                 new GenericDocument.Builder<>("namespace", "id", "callSchema")
937                         .setPropertyString("personId", "package1$database1/namespace#id")
938                         .build();
939         mAppSearchImpl.putDocument(
940                 "package2",
941                 "database2",
942                 call,
943                 /* sendChangeNotifications= */ false,
944                 /* logger= */ null);
945 
946         // Insert package3 document
947         GenericDocument text =
948                 new GenericDocument.Builder<>("namespace", "id", "textSchema")
949                         .setPropertyString("personId", "package1$database1/namespace#id")
950                         .build();
951         mAppSearchImpl.putDocument(
952                 "package3",
953                 "database3",
954                 text,
955                 /* sendChangeNotifications= */ false,
956                 /* logger= */ null);
957 
958         // Filter on parent spec only
959         SearchSpec nested =
960                 new SearchSpec.Builder()
961                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_CREATION_TIMESTAMP)
962                         .setOrder(SearchSpec.ORDER_ASCENDING)
963                         .build();
964         JoinSpec join = new JoinSpec.Builder("personId").setNestedSearch("", nested).build();
965 
966         SearchSpec searchSpec =
967                 new SearchSpec.Builder()
968                         .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
969                         .addFilterPackageNames("package1")
970                         .setJoinSpec(join)
971                         .build();
972         SearchResultPage searchResultPage =
973                 mAppSearchImpl.globalQuery(
974                         "", searchSpec, new CallerAccess("package1"), /* logger= */ null);
975         assertThat(searchResultPage.getResults()).hasSize(1);
976         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(person);
977         SearchResult result = searchResultPage.getResults().get(0);
978         assertThat(result.getJoinedResults()).hasSize(2);
979 
980         // Filter on neither
981         searchSpec =
982                 new SearchSpec.Builder()
983                         .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
984                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_CREATION_TIMESTAMP)
985                         .setOrder(SearchSpec.ORDER_ASCENDING)
986                         .setJoinSpec(join)
987                         .build();
988         searchResultPage =
989                 mAppSearchImpl.globalQuery(
990                         "", searchSpec, new CallerAccess("package1"), /* logger= */ null);
991         assertThat(searchResultPage.getResults()).hasSize(3);
992         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(person);
993         assertThat(searchResultPage.getResults().get(1).getGenericDocument()).isEqualTo(call);
994         assertThat(searchResultPage.getResults().get(2).getGenericDocument()).isEqualTo(text);
995         result = searchResultPage.getResults().get(0);
996         assertThat(result.getJoinedResults()).hasSize(2);
997 
998         // Filter on child spec only
999         nested = new SearchSpec.Builder().addFilterPackageNames("package2").build();
1000         join = new JoinSpec.Builder("personId").setNestedSearch("", nested).build();
1001 
1002         searchSpec =
1003                 new SearchSpec.Builder()
1004                         .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
1005                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_CREATION_TIMESTAMP)
1006                         .setOrder(SearchSpec.ORDER_ASCENDING)
1007                         .setJoinSpec(join)
1008                         .build();
1009         searchResultPage =
1010                 mAppSearchImpl.globalQuery(
1011                         "", searchSpec, new CallerAccess("package1"), /* logger= */ null);
1012         assertThat(searchResultPage.getResults()).hasSize(3);
1013         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(person);
1014         assertThat(searchResultPage.getResults().get(1).getGenericDocument()).isEqualTo(call);
1015         assertThat(searchResultPage.getResults().get(2).getGenericDocument()).isEqualTo(text);
1016         result = searchResultPage.getResults().get(0);
1017         assertThat(result.getJoinedResults()).hasSize(1);
1018         assertThat(result.getJoinedResults().get(0).getGenericDocument()).isEqualTo(call);
1019 
1020         // Filter on both
1021         searchSpec =
1022                 new SearchSpec.Builder()
1023                         .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
1024                         .addFilterPackageNames("package1")
1025                         .setJoinSpec(join)
1026                         .build();
1027         searchResultPage =
1028                 mAppSearchImpl.globalQuery(
1029                         "", searchSpec, new CallerAccess("package1"), /* logger= */ null);
1030         assertThat(searchResultPage.getResults()).hasSize(1);
1031         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(person);
1032         result = searchResultPage.getResults().get(0);
1033         assertThat(result.getJoinedResults()).hasSize(1);
1034         assertThat(result.getJoinedResults().get(0).getGenericDocument()).isEqualTo(call);
1035     }
1036 
1037     @Test
testQueryInvalidPackages_withJoin()1038     public void testQueryInvalidPackages_withJoin() throws Exception {
1039         // Make sure that local queries with joinspecs including package filters don't access
1040         // other packages.
1041 
1042         // Create a new mAppSearchImpl with a mock Visibility Checker
1043         mAppSearchImpl.close();
1044         File tempFolder = mTemporaryFolder.newFolder();
1045         // We need to share across packages
1046         VisibilityChecker mockVisibilityChecker = createMockVisibilityChecker(true);
1047         mAppSearchImpl =
1048                 AppSearchImpl.create(
1049                         tempFolder,
1050                         new AppSearchConfigImpl(
1051                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
1052                         /* initStatsBuilder= */ null,
1053                         mockVisibilityChecker,
1054                         /* revocableFileDescriptorStore= */ null,
1055                         ALWAYS_OPTIMIZE);
1056 
1057         AppSearchSchema.StringPropertyConfig personField =
1058                 new AppSearchSchema.StringPropertyConfig.Builder("personId")
1059                         .setJoinableValueType(
1060                                 AppSearchSchema.StringPropertyConfig
1061                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
1062                         .build();
1063 
1064         // Insert package1 schema
1065         List<AppSearchSchema> personAndCallSchema =
1066                 ImmutableList.of(
1067                         new AppSearchSchema.Builder("personSchema").build(),
1068                         new AppSearchSchema.Builder("callSchema").addProperty(personField).build());
1069         InternalSetSchemaResponse internalSetSchemaResponse =
1070                 mAppSearchImpl.setSchema(
1071                         "package1",
1072                         "database1",
1073                         personAndCallSchema,
1074                         /* visibilityConfigs= */ Collections.emptyList(),
1075                         /* forceOverride= */ false,
1076                         /* version= */ 0,
1077                         /* setSchemaStatsBuilder= */ null);
1078         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
1079 
1080         // Insert package2 schema
1081         List<AppSearchSchema> callSchema =
1082                 ImmutableList.of(
1083                         new AppSearchSchema.Builder("callSchema").addProperty(personField).build());
1084         internalSetSchemaResponse =
1085                 mAppSearchImpl.setSchema(
1086                         "package2",
1087                         "database2",
1088                         callSchema,
1089                         /* visibilityConfigs= */ Collections.emptyList(),
1090                         /* forceOverride= */ true,
1091                         /* version= */ 0,
1092                         /* setSchemaStatsBuilder= */ null);
1093         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
1094 
1095         // Insert package1 document
1096         GenericDocument person =
1097                 new GenericDocument.Builder<>("namespace", "person", "personSchema").build();
1098         mAppSearchImpl.putDocument(
1099                 "package1",
1100                 "database1",
1101                 person,
1102                 /* sendChangeNotifications= */ false,
1103                 /* logger= */ null);
1104 
1105         GenericDocument call1 =
1106                 new GenericDocument.Builder<>("namespace", "id1", "callSchema")
1107                         .setPropertyString("personId", "package1$database1/namespace#person")
1108                         .build();
1109         GenericDocument call2 =
1110                 new GenericDocument.Builder<>("namespace", "id2", "callSchema")
1111                         .setPropertyString("personId", "package1$database1/namespace#person")
1112                         .build();
1113 
1114         // Insert package1 action document
1115         mAppSearchImpl.putDocument(
1116                 "package1",
1117                 "database1",
1118                 call1,
1119                 /* sendChangeNotifications= */ false,
1120                 /* logger= */ null);
1121 
1122         // Insert package2 action document
1123         mAppSearchImpl.putDocument(
1124                 "package2",
1125                 "database2",
1126                 call2,
1127                 /* sendChangeNotifications= */ false,
1128                 /* logger= */ null);
1129 
1130         // Invalid parent spec filter
1131         SearchSpec searchSpec =
1132                 new SearchSpec.Builder()
1133                         .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
1134                         .addFilterPackageNames("package1", "package2")
1135                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_CREATION_TIMESTAMP)
1136                         .setOrder(SearchSpec.ORDER_ASCENDING)
1137                         .build();
1138         SearchResultPage searchResultPage =
1139                 mAppSearchImpl.query("package1", "database1", "", searchSpec, /* logger= */ null);
1140         // Only package1 documents should be returned
1141         assertThat(searchResultPage.getResults()).hasSize(2);
1142         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(person);
1143         assertThat(searchResultPage.getResults().get(1).getGenericDocument()).isEqualTo(call1);
1144 
1145         // Valid parent spec filter with invalid child spec filter
1146         SearchSpec nested =
1147                 new SearchSpec.Builder()
1148                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_CREATION_TIMESTAMP)
1149                         .addFilterPackageNames("package1", "package2")
1150                         .setOrder(SearchSpec.ORDER_ASCENDING)
1151                         .build();
1152         JoinSpec join = new JoinSpec.Builder("personId").setNestedSearch("", nested).build();
1153         searchSpec =
1154                 new SearchSpec.Builder()
1155                         .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
1156                         .addFilterPackageNames("package1")
1157                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_CREATION_TIMESTAMP)
1158                         .setOrder(SearchSpec.ORDER_ASCENDING)
1159                         .setJoinSpec(join)
1160                         .build();
1161         searchResultPage =
1162                 mAppSearchImpl.query("package1", "database1", "", searchSpec, /* logger= */ null);
1163 
1164         // Only package1 documents should be returned, for both the outer and nested searches
1165         assertThat(searchResultPage.getResults()).hasSize(2);
1166         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(person);
1167         assertThat(searchResultPage.getResults().get(1).getGenericDocument()).isEqualTo(call1);
1168         SearchResult result = searchResultPage.getResults().get(0);
1169         assertThat(result.getJoinedResults()).hasSize(1);
1170         assertThat(result.getJoinedResults().get(0).getGenericDocument()).isEqualTo(call1);
1171 
1172         // Valid parent spec, but child spec package filters only contain other packages
1173         nested =
1174                 new SearchSpec.Builder()
1175                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_CREATION_TIMESTAMP)
1176                         .addFilterPackageNames("package2", "package3")
1177                         .setOrder(SearchSpec.ORDER_ASCENDING)
1178                         .build();
1179         join = new JoinSpec.Builder("personId").setNestedSearch("", nested).build();
1180         searchSpec =
1181                 new SearchSpec.Builder()
1182                         .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
1183                         .addFilterPackageNames("package1")
1184                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_CREATION_TIMESTAMP)
1185                         .setOrder(SearchSpec.ORDER_ASCENDING)
1186                         .setJoinSpec(join)
1187                         .build();
1188         searchResultPage =
1189                 mAppSearchImpl.query("package1", "database1", "", searchSpec, /* logger= */ null);
1190 
1191         // Package1 documents should be returned, but no packages should be joined
1192         assertThat(searchResultPage.getResults()).hasSize(2);
1193         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(person);
1194         assertThat(searchResultPage.getResults().get(1).getGenericDocument()).isEqualTo(call1);
1195         result = searchResultPage.getResults().get(0);
1196         assertThat(result.getJoinedResults()).isEmpty();
1197 
1198         // Valid parent spec, empty child spec package filters
1199         nested =
1200                 new SearchSpec.Builder()
1201                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_CREATION_TIMESTAMP)
1202                         .setOrder(SearchSpec.ORDER_ASCENDING)
1203                         .build();
1204         join = new JoinSpec.Builder("personId").setNestedSearch("", nested).build();
1205         searchSpec =
1206                 new SearchSpec.Builder()
1207                         .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
1208                         .addFilterPackageNames("package1")
1209                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_CREATION_TIMESTAMP)
1210                         .setOrder(SearchSpec.ORDER_ASCENDING)
1211                         .setJoinSpec(join)
1212                         .build();
1213         searchResultPage =
1214                 mAppSearchImpl.query("package1", "database1", "", searchSpec, /* logger= */ null);
1215 
1216         // Only package1 documents should be returned, for both the outer and nested searches
1217         assertThat(searchResultPage.getResults()).hasSize(2);
1218         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(person);
1219         assertThat(searchResultPage.getResults().get(1).getGenericDocument()).isEqualTo(call1);
1220         result = searchResultPage.getResults().get(0);
1221         assertThat(result.getJoinedResults()).hasSize(1);
1222         assertThat(result.getJoinedResults().get(0).getGenericDocument()).isEqualTo(call1);
1223 
1224         // Valid parent spec filter with valid child spec filter
1225         nested = new SearchSpec.Builder().build();
1226         join = new JoinSpec.Builder("personId").setNestedSearch("", nested).build();
1227         searchSpec =
1228                 new SearchSpec.Builder()
1229                         .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
1230                         .addFilterPackageNames("package1")
1231                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_CREATION_TIMESTAMP)
1232                         .setOrder(SearchSpec.ORDER_ASCENDING)
1233                         .setJoinSpec(join)
1234                         .build();
1235         searchResultPage =
1236                 mAppSearchImpl.query("package1", "database1", "", searchSpec, /* logger= */ null);
1237         // Should work as expected
1238         assertThat(searchResultPage.getResults()).hasSize(2);
1239         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(person);
1240         assertThat(searchResultPage.getResults().get(1).getGenericDocument()).isEqualTo(call1);
1241         result = searchResultPage.getResults().get(0);
1242         assertThat(result.getJoinedResults()).hasSize(1);
1243         assertThat(result.getJoinedResults().get(0).getGenericDocument()).isEqualTo(call1);
1244     }
1245 
1246     @Test
testSearchSuggestion()1247     public void testSearchSuggestion() throws Exception {
1248         // Insert schema
1249         List<AppSearchSchema> schemas =
1250                 Collections.singletonList(
1251                         new AppSearchSchema.Builder("type")
1252                                 .addProperty(
1253                                         new AppSearchSchema.StringPropertyConfig.Builder("body")
1254                                                 .setIndexingType(
1255                                                         AppSearchSchema.StringPropertyConfig
1256                                                                 .INDEXING_TYPE_PREFIXES)
1257                                                 .setTokenizerType(
1258                                                         AppSearchSchema.StringPropertyConfig
1259                                                                 .TOKENIZER_TYPE_PLAIN)
1260                                                 .build())
1261                                 .build());
1262         mAppSearchImpl.setSchema(
1263                 "package",
1264                 "database",
1265                 schemas,
1266                 /* visibilityConfigs= */ Collections.emptyList(),
1267                 /* forceOverride= */ false,
1268                 /* version= */ 0,
1269                 /* setSchemaStatsBuilder= */ null);
1270 
1271         // Insert three documents.
1272         GenericDocument doc1 =
1273                 new GenericDocument.Builder<>("namespace", "id1", "type")
1274                         .setPropertyString("body", "termOne")
1275                         .build();
1276         GenericDocument doc2 =
1277                 new GenericDocument.Builder<>("namespace", "id2", "type")
1278                         .setPropertyString("body", "termOne termTwo")
1279                         .build();
1280         GenericDocument doc3 =
1281                 new GenericDocument.Builder<>("namespace", "id3", "type")
1282                         .setPropertyString("body", "termOne termTwo termThree")
1283                         .build();
1284         mAppSearchImpl.putDocument(
1285                 "package",
1286                 "database",
1287                 doc1,
1288                 /* sendChangeNotifications= */ false,
1289                 /* logger= */ null);
1290         mAppSearchImpl.putDocument(
1291                 "package",
1292                 "database",
1293                 doc2,
1294                 /* sendChangeNotifications= */ false,
1295                 /* logger= */ null);
1296         mAppSearchImpl.putDocument(
1297                 "package",
1298                 "database",
1299                 doc3,
1300                 /* sendChangeNotifications= */ false,
1301                 /* logger= */ null);
1302 
1303         List<SearchSuggestionResult> suggestions =
1304                 mAppSearchImpl.searchSuggestion(
1305                         "package",
1306                         "database",
1307                         /* suggestionQueryExpression= */ "t",
1308                         new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10).build());
1309         assertThat(suggestions).hasSize(3);
1310         assertThat(suggestions.get(0).getSuggestedResult()).isEqualTo("termone");
1311         assertThat(suggestions.get(1).getSuggestedResult()).isEqualTo("termtwo");
1312         assertThat(suggestions.get(2).getSuggestedResult()).isEqualTo("termthree");
1313 
1314         // Set total result count to be 2.
1315         suggestions =
1316                 mAppSearchImpl.searchSuggestion(
1317                         "package",
1318                         "database",
1319                         /* suggestionQueryExpression= */ "t",
1320                         new SearchSuggestionSpec.Builder(/* totalResultCount= */ 2).build());
1321         assertThat(suggestions).hasSize(2);
1322         assertThat(suggestions.get(0).getSuggestedResult()).isEqualTo("termone");
1323         assertThat(suggestions.get(1).getSuggestedResult()).isEqualTo("termtwo");
1324     }
1325 
1326     @Test
testSearchSuggestion_removeDocument()1327     public void testSearchSuggestion_removeDocument() throws Exception {
1328         // Insert schema
1329         List<AppSearchSchema> schemas =
1330                 Collections.singletonList(
1331                         new AppSearchSchema.Builder("type")
1332                                 .addProperty(
1333                                         new AppSearchSchema.StringPropertyConfig.Builder("body")
1334                                                 .setIndexingType(
1335                                                         AppSearchSchema.StringPropertyConfig
1336                                                                 .INDEXING_TYPE_PREFIXES)
1337                                                 .setTokenizerType(
1338                                                         AppSearchSchema.StringPropertyConfig
1339                                                                 .TOKENIZER_TYPE_PLAIN)
1340                                                 .build())
1341                                 .build());
1342         mAppSearchImpl.setSchema(
1343                 "package",
1344                 "database",
1345                 schemas,
1346                 /* visibilityConfigs= */ Collections.emptyList(),
1347                 /* forceOverride= */ false,
1348                 /* version= */ 0,
1349                 /* setSchemaStatsBuilder= */ null);
1350 
1351         // Insert a document.
1352         GenericDocument doc1 =
1353                 new GenericDocument.Builder<>("namespace", "id1", "type")
1354                         .setPropertyString("body", "termOne")
1355                         .build();
1356         mAppSearchImpl.putDocument(
1357                 "package",
1358                 "database",
1359                 doc1,
1360                 /* sendChangeNotifications= */ false,
1361                 /* logger= */ null);
1362 
1363         List<SearchSuggestionResult> suggestions =
1364                 mAppSearchImpl.searchSuggestion(
1365                         "package",
1366                         "database",
1367                         /* suggestionQueryExpression= */ "t",
1368                         new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10).build());
1369         assertThat(suggestions).hasSize(1);
1370         assertThat(suggestions.get(0).getSuggestedResult()).isEqualTo("termone");
1371 
1372         // Remove the document.
1373         mAppSearchImpl.remove(
1374                 "package", "database", "namespace", "id1", /* removeStatsBuilder= */ null);
1375 
1376         // Now we cannot find any suggestion
1377         suggestions =
1378                 mAppSearchImpl.searchSuggestion(
1379                         "package",
1380                         "database",
1381                         /* suggestionQueryExpression= */ "t",
1382                         new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10).build());
1383         assertThat(suggestions).isEmpty();
1384     }
1385 
1386     @Test
testSearchSuggestion_replaceDocument()1387     public void testSearchSuggestion_replaceDocument() throws Exception {
1388         // Insert schema
1389         List<AppSearchSchema> schemas =
1390                 Collections.singletonList(
1391                         new AppSearchSchema.Builder("type")
1392                                 .addProperty(
1393                                         new AppSearchSchema.StringPropertyConfig.Builder("body")
1394                                                 .setIndexingType(
1395                                                         AppSearchSchema.StringPropertyConfig
1396                                                                 .INDEXING_TYPE_PREFIXES)
1397                                                 .setTokenizerType(
1398                                                         AppSearchSchema.StringPropertyConfig
1399                                                                 .TOKENIZER_TYPE_PLAIN)
1400                                                 .build())
1401                                 .build());
1402         mAppSearchImpl.setSchema(
1403                 "package",
1404                 "database",
1405                 schemas,
1406                 /* visibilityConfigs= */ Collections.emptyList(),
1407                 /* forceOverride= */ false,
1408                 /* version= */ 0,
1409                 /* setSchemaStatsBuilder= */ null);
1410 
1411         // Insert a document.
1412         GenericDocument doc1 =
1413                 new GenericDocument.Builder<>("namespace", "id1", "type")
1414                         .setPropertyString("body", "tart two three")
1415                         .build();
1416         mAppSearchImpl.putDocument(
1417                 "package",
1418                 "database",
1419                 doc1,
1420                 /* sendChangeNotifications= */ false,
1421                 /* logger= */ null);
1422         SearchSuggestionResult tartResult =
1423                 new SearchSuggestionResult.Builder().setSuggestedResult("tart").build();
1424         SearchSuggestionResult twoResult =
1425                 new SearchSuggestionResult.Builder().setSuggestedResult("two").build();
1426         SearchSuggestionResult threeResult =
1427                 new SearchSuggestionResult.Builder().setSuggestedResult("three").build();
1428         SearchSuggestionResult twistResult =
1429                 new SearchSuggestionResult.Builder().setSuggestedResult("twist").build();
1430         List<SearchSuggestionResult> suggestions =
1431                 mAppSearchImpl.searchSuggestion(
1432                         "package",
1433                         "database",
1434                         /* suggestionQueryExpression= */ "t",
1435                         new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10).build());
1436         assertThat(suggestions).containsExactly(tartResult, twoResult, threeResult);
1437 
1438         // replace the document with two terms.
1439         GenericDocument replaceDocument =
1440                 new GenericDocument.Builder<>("namespace", "id1", "type")
1441                         .setPropertyString("body", "twist three")
1442                         .build();
1443         mAppSearchImpl.putDocument(
1444                 "package",
1445                 "database",
1446                 replaceDocument,
1447                 /* sendChangeNotifications= */ false,
1448                 /* logger= */ null);
1449 
1450         // Now we cannot find any suggestion
1451         suggestions =
1452                 mAppSearchImpl.searchSuggestion(
1453                         "package",
1454                         "database",
1455                         /* suggestionQueryExpression= */ "t",
1456                         new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10).build());
1457         assertThat(suggestions).containsExactly(twistResult, threeResult);
1458     }
1459 
1460     @Test
testSearchSuggestion_namespaceFilter()1461     public void testSearchSuggestion_namespaceFilter() throws Exception {
1462         // Insert schema
1463         List<AppSearchSchema> schemas =
1464                 Collections.singletonList(
1465                         new AppSearchSchema.Builder("type")
1466                                 .addProperty(
1467                                         new AppSearchSchema.StringPropertyConfig.Builder("body")
1468                                                 .setIndexingType(
1469                                                         AppSearchSchema.StringPropertyConfig
1470                                                                 .INDEXING_TYPE_PREFIXES)
1471                                                 .setTokenizerType(
1472                                                         AppSearchSchema.StringPropertyConfig
1473                                                                 .TOKENIZER_TYPE_PLAIN)
1474                                                 .build())
1475                                 .build());
1476         mAppSearchImpl.setSchema(
1477                 "package",
1478                 "database",
1479                 schemas,
1480                 /* visibilityConfigs= */ Collections.emptyList(),
1481                 /* forceOverride= */ false,
1482                 /* version= */ 0,
1483                 /* setSchemaStatsBuilder= */ null);
1484 
1485         // Insert three documents.
1486         GenericDocument doc1 =
1487                 new GenericDocument.Builder<>("namespace1", "id1", "type")
1488                         .setPropertyString("body", "term1")
1489                         .build();
1490         GenericDocument doc2 =
1491                 new GenericDocument.Builder<>("namespace2", "id2", "type")
1492                         .setPropertyString("body", "term1 term2")
1493                         .build();
1494         GenericDocument doc3 =
1495                 new GenericDocument.Builder<>("namespace3", "id3", "type")
1496                         .setPropertyString("body", "term1 term2 term3")
1497                         .build();
1498 
1499         mAppSearchImpl.putDocument(
1500                 "package",
1501                 "database",
1502                 doc1,
1503                 /* sendChangeNotifications= */ false,
1504                 /* logger= */ null);
1505         mAppSearchImpl.putDocument(
1506                 "package",
1507                 "database",
1508                 doc2,
1509                 /* sendChangeNotifications= */ false,
1510                 /* logger= */ null);
1511         mAppSearchImpl.putDocument(
1512                 "package",
1513                 "database",
1514                 doc3,
1515                 /* sendChangeNotifications= */ false,
1516                 /* logger= */ null);
1517 
1518         List<SearchSuggestionResult> suggestions =
1519                 mAppSearchImpl.searchSuggestion(
1520                         "package",
1521                         "database",
1522                         /* suggestionQueryExpression= */ "t",
1523                         new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10)
1524                                 .addFilterNamespaces("namespace1")
1525                                 .build());
1526         assertThat(suggestions).hasSize(1);
1527         assertThat(suggestions.get(0).getSuggestedResult()).isEqualTo("term1");
1528 
1529         suggestions =
1530                 mAppSearchImpl.searchSuggestion(
1531                         "package",
1532                         "database",
1533                         /* suggestionQueryExpression= */ "t",
1534                         new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10)
1535                                 .addFilterNamespaces("namespace1", "namespace2")
1536                                 .build());
1537         assertThat(suggestions).hasSize(2);
1538         assertThat(suggestions.get(0).getSuggestedResult()).isEqualTo("term1");
1539         assertThat(suggestions.get(1).getSuggestedResult()).isEqualTo("term2");
1540     }
1541 
1542     @Ignore("b/273733335")
1543     @Test
testSearchSuggestion_invalidPrefix()1544     public void testSearchSuggestion_invalidPrefix() throws Exception {
1545         // Insert schema just put something in the AppSearch to make it searchable.
1546         List<AppSearchSchema> schemas =
1547                 Collections.singletonList(
1548                         new AppSearchSchema.Builder("type")
1549                                 .addProperty(
1550                                         new AppSearchSchema.StringPropertyConfig.Builder("body")
1551                                                 .setIndexingType(
1552                                                         AppSearchSchema.StringPropertyConfig
1553                                                                 .INDEXING_TYPE_PREFIXES)
1554                                                 .setTokenizerType(
1555                                                         AppSearchSchema.StringPropertyConfig
1556                                                                 .TOKENIZER_TYPE_PLAIN)
1557                                                 .build())
1558                                 .build());
1559         mAppSearchImpl.setSchema(
1560                 "package",
1561                 "database",
1562                 schemas,
1563                 /* visibilityConfigs= */ Collections.emptyList(),
1564                 /* forceOverride= */ false,
1565                 /* version= */ 0,
1566                 /* setSchemaStatsBuilder= */ null);
1567         GenericDocument doc =
1568                 new GenericDocument.Builder<>("namespace1", "id1", "type")
1569                         .setPropertyString("body", "term1")
1570                         .build();
1571         mAppSearchImpl.putDocument(
1572                 "package",
1573                 "database",
1574                 doc,
1575                 /* sendChangeNotifications= */ false,
1576                 /* logger= */ null);
1577 
1578         List<SearchSuggestionResult> suggestions =
1579                 mAppSearchImpl.searchSuggestion(
1580                         "package",
1581                         "database",
1582                         /* suggestionQueryExpression= */ "t:",
1583                         new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10).build());
1584         assertThat(suggestions).isEmpty();
1585         suggestions =
1586                 mAppSearchImpl.searchSuggestion(
1587                         "package",
1588                         "database",
1589                         /* suggestionQueryExpression= */ "t-",
1590                         new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10).build());
1591         assertThat(suggestions).isEmpty();
1592         suggestions =
1593                 mAppSearchImpl.searchSuggestion(
1594                         "package",
1595                         "database",
1596                         /* suggestionQueryExpression= */ "t  ",
1597                         new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10).build());
1598         assertThat(suggestions).isEmpty();
1599         suggestions =
1600                 mAppSearchImpl.searchSuggestion(
1601                         "package",
1602                         "database",
1603                         /* suggestionQueryExpression= */ "{t}",
1604                         new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10).build());
1605         assertThat(suggestions).isEmpty();
1606         suggestions =
1607                 mAppSearchImpl.searchSuggestion(
1608                         "package",
1609                         "database",
1610                         /* suggestionQueryExpression= */ "(t)",
1611                         new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10).build());
1612         assertThat(suggestions).isEmpty();
1613     }
1614 
1615     @Test
testSearchSuggestion_emptyPrefix()1616     public void testSearchSuggestion_emptyPrefix() throws Exception {
1617         // Insert schema just put something in the AppSearch to make it searchable.
1618         List<AppSearchSchema> schemas =
1619                 Collections.singletonList(
1620                         new AppSearchSchema.Builder("type")
1621                                 .addProperty(
1622                                         new AppSearchSchema.StringPropertyConfig.Builder("body")
1623                                                 .setIndexingType(
1624                                                         AppSearchSchema.StringPropertyConfig
1625                                                                 .INDEXING_TYPE_PREFIXES)
1626                                                 .setTokenizerType(
1627                                                         AppSearchSchema.StringPropertyConfig
1628                                                                 .TOKENIZER_TYPE_PLAIN)
1629                                                 .build())
1630                                 .build());
1631         mAppSearchImpl.setSchema(
1632                 "package",
1633                 "database",
1634                 schemas,
1635                 /* visibilityConfigs= */ Collections.emptyList(),
1636                 /* forceOverride= */ false,
1637                 /* version= */ 0,
1638                 /* setSchemaStatsBuilder= */ null);
1639         GenericDocument doc =
1640                 new GenericDocument.Builder<>("namespace1", "id1", "type")
1641                         .setPropertyString("body", "term1")
1642                         .build();
1643         mAppSearchImpl.putDocument(
1644                 "package",
1645                 "database",
1646                 doc,
1647                 /* sendChangeNotifications= */ false,
1648                 /* logger= */ null);
1649 
1650         AppSearchException e =
1651                 assertThrows(
1652                         AppSearchException.class,
1653                         () ->
1654                                 mAppSearchImpl.searchSuggestion(
1655                                         "package",
1656                                         "database",
1657                                         /* suggestionQueryExpression= */ "",
1658                                         new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10)
1659                                                 .addFilterNamespaces("namespace1")
1660                                                 .build()));
1661         assertThat(e.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
1662         assertThat(e).hasMessageThat().contains("suggestionQueryExpression cannot be empty.");
1663     }
1664 
1665     @Test
testGetNextPageToken_query()1666     public void testGetNextPageToken_query() throws Exception {
1667         // Insert package1 schema
1668         List<AppSearchSchema> schema1 =
1669                 ImmutableList.of(new AppSearchSchema.Builder("schema1").build());
1670         InternalSetSchemaResponse internalSetSchemaResponse =
1671                 mAppSearchImpl.setSchema(
1672                         "package1",
1673                         "database1",
1674                         schema1,
1675                         /* visibilityConfigs= */ Collections.emptyList(),
1676                         /* forceOverride= */ false,
1677                         /* version= */ 0,
1678                         /* setSchemaStatsBuilder= */ null);
1679         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
1680 
1681         // Insert two package1 documents
1682         GenericDocument document1 =
1683                 new GenericDocument.Builder<>("namespace", "id1", "schema1").build();
1684         GenericDocument document2 =
1685                 new GenericDocument.Builder<>("namespace", "id2", "schema1").build();
1686         mAppSearchImpl.putDocument(
1687                 "package1",
1688                 "database1",
1689                 document1,
1690                 /* sendChangeNotifications= */ false,
1691                 /* logger= */ null);
1692         mAppSearchImpl.putDocument(
1693                 "package1",
1694                 "database1",
1695                 document2,
1696                 /* sendChangeNotifications= */ false,
1697                 /* logger= */ null);
1698 
1699         // Query for only 1 result per page
1700         SearchSpec searchSpec =
1701                 new SearchSpec.Builder()
1702                         .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
1703                         .setResultCountPerPage(1)
1704                         .build();
1705         SearchResultPage searchResultPage =
1706                 mAppSearchImpl.query("package1", "database1", "", searchSpec, /* logger= */ null);
1707 
1708         // Document2 will come first because it was inserted last and default return order is
1709         // most recent.
1710         assertThat(searchResultPage.getResults()).hasSize(1);
1711         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document2);
1712 
1713         long nextPageToken = searchResultPage.getNextPageToken();
1714         searchResultPage =
1715                 mAppSearchImpl.getNextPage("package1", nextPageToken, /* statsBuilder= */ null);
1716         assertThat(searchResultPage.getResults()).hasSize(1);
1717         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document1);
1718     }
1719 
1720     @Test
testGetNextPageWithDifferentPackage_query()1721     public void testGetNextPageWithDifferentPackage_query() throws Exception {
1722         // Insert package1 schema
1723         List<AppSearchSchema> schema1 =
1724                 ImmutableList.of(new AppSearchSchema.Builder("schema1").build());
1725         InternalSetSchemaResponse internalSetSchemaResponse =
1726                 mAppSearchImpl.setSchema(
1727                         "package1",
1728                         "database1",
1729                         schema1,
1730                         /* visibilityConfigs= */ Collections.emptyList(),
1731                         /* forceOverride= */ false,
1732                         /* version= */ 0,
1733                         /* setSchemaStatsBuilder= */ null);
1734         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
1735 
1736         // Insert two package1 documents
1737         GenericDocument document1 =
1738                 new GenericDocument.Builder<>("namespace", "id1", "schema1").build();
1739         GenericDocument document2 =
1740                 new GenericDocument.Builder<>("namespace", "id2", "schema1").build();
1741         mAppSearchImpl.putDocument(
1742                 "package1",
1743                 "database1",
1744                 document1,
1745                 /* sendChangeNotifications= */ false,
1746                 /* logger= */ null);
1747         mAppSearchImpl.putDocument(
1748                 "package1",
1749                 "database1",
1750                 document2,
1751                 /* sendChangeNotifications= */ false,
1752                 /* logger= */ null);
1753 
1754         // Query for only 1 result per page
1755         SearchSpec searchSpec =
1756                 new SearchSpec.Builder()
1757                         .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
1758                         .setResultCountPerPage(1)
1759                         .build();
1760         SearchResultPage searchResultPage =
1761                 mAppSearchImpl.query("package1", "database1", "", searchSpec, /* logger= */ null);
1762 
1763         // Document2 will come first because it was inserted last and default return order is
1764         // most recent.
1765         assertThat(searchResultPage.getResults()).hasSize(1);
1766         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document2);
1767 
1768         long nextPageToken = searchResultPage.getNextPageToken();
1769 
1770         // Try getting next page with the wrong package, package2
1771         AppSearchException e =
1772                 assertThrows(
1773                         AppSearchException.class,
1774                         () ->
1775                                 mAppSearchImpl.getNextPage(
1776                                         "package2", nextPageToken, /* statsBuilder= */ null));
1777         assertThat(e)
1778                 .hasMessageThat()
1779                 .contains("Package \"package2\" cannot use nextPageToken: " + nextPageToken);
1780         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_SECURITY_ERROR);
1781 
1782         // Can continue getting next page for package1
1783         searchResultPage =
1784                 mAppSearchImpl.getNextPage("package1", nextPageToken, /* statsBuilder= */ null);
1785         assertThat(searchResultPage.getResults()).hasSize(1);
1786         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document1);
1787     }
1788 
1789     @Test
testGetNextPageToken_globalQuery()1790     public void testGetNextPageToken_globalQuery() throws Exception {
1791         // Insert package1 schema
1792         List<AppSearchSchema> schema1 =
1793                 ImmutableList.of(new AppSearchSchema.Builder("schema1").build());
1794         InternalSetSchemaResponse internalSetSchemaResponse =
1795                 mAppSearchImpl.setSchema(
1796                         "package1",
1797                         "database1",
1798                         schema1,
1799                         /* visibilityConfigs= */ Collections.emptyList(),
1800                         /* forceOverride= */ false,
1801                         /* version= */ 0,
1802                         /* setSchemaStatsBuilder= */ null);
1803         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
1804 
1805         // Insert two package1 documents
1806         GenericDocument document1 =
1807                 new GenericDocument.Builder<>("namespace", "id1", "schema1").build();
1808         GenericDocument document2 =
1809                 new GenericDocument.Builder<>("namespace", "id2", "schema1").build();
1810         mAppSearchImpl.putDocument(
1811                 "package1",
1812                 "database1",
1813                 document1,
1814                 /* sendChangeNotifications= */ false,
1815                 /* logger= */ null);
1816         mAppSearchImpl.putDocument(
1817                 "package1",
1818                 "database1",
1819                 document2,
1820                 /* sendChangeNotifications= */ false,
1821                 /* logger= */ null);
1822 
1823         // Query for only 1 result per page
1824         SearchSpec searchSpec =
1825                 new SearchSpec.Builder()
1826                         .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
1827                         .setResultCountPerPage(1)
1828                         .build();
1829         SearchResultPage searchResultPage =
1830                 mAppSearchImpl.globalQuery(
1831                         /* queryExpression= */ "",
1832                         searchSpec,
1833                         new CallerAccess(/* callingPackageName= */ "package1"),
1834                         /* logger= */ null);
1835 
1836         // Document2 will come first because it was inserted last and default return order is
1837         // most recent.
1838         assertThat(searchResultPage.getResults()).hasSize(1);
1839         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document2);
1840 
1841         long nextPageToken = searchResultPage.getNextPageToken();
1842         searchResultPage =
1843                 mAppSearchImpl.getNextPage("package1", nextPageToken, /* statsBuilder= */ null);
1844         assertThat(searchResultPage.getResults()).hasSize(1);
1845         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document1);
1846     }
1847 
1848     @Test
testGetNextPageWithDifferentPackage_globalQuery()1849     public void testGetNextPageWithDifferentPackage_globalQuery() throws Exception {
1850         // Insert package1 schema
1851         List<AppSearchSchema> schema1 =
1852                 ImmutableList.of(new AppSearchSchema.Builder("schema1").build());
1853         InternalSetSchemaResponse internalSetSchemaResponse =
1854                 mAppSearchImpl.setSchema(
1855                         "package1",
1856                         "database1",
1857                         schema1,
1858                         /* visibilityConfigs= */ Collections.emptyList(),
1859                         /* forceOverride= */ false,
1860                         /* version= */ 0,
1861                         /* setSchemaStatsBuilder= */ null);
1862         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
1863 
1864         // Insert two package1 documents
1865         GenericDocument document1 =
1866                 new GenericDocument.Builder<>("namespace", "id1", "schema1").build();
1867         GenericDocument document2 =
1868                 new GenericDocument.Builder<>("namespace", "id2", "schema1").build();
1869         mAppSearchImpl.putDocument(
1870                 "package1",
1871                 "database1",
1872                 document1,
1873                 /* sendChangeNotifications= */ false,
1874                 /* logger= */ null);
1875         mAppSearchImpl.putDocument(
1876                 "package1",
1877                 "database1",
1878                 document2,
1879                 /* sendChangeNotifications= */ false,
1880                 /* logger= */ null);
1881 
1882         // Query for only 1 result per page
1883         SearchSpec searchSpec =
1884                 new SearchSpec.Builder()
1885                         .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
1886                         .setResultCountPerPage(1)
1887                         .build();
1888         SearchResultPage searchResultPage =
1889                 mAppSearchImpl.globalQuery(
1890                         /* queryExpression= */ "",
1891                         searchSpec,
1892                         new CallerAccess(/* callingPackageName= */ "package1"),
1893                         /* logger= */ null);
1894 
1895         // Document2 will come first because it was inserted last and default return order is
1896         // most recent.
1897         assertThat(searchResultPage.getResults()).hasSize(1);
1898         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document2);
1899 
1900         long nextPageToken = searchResultPage.getNextPageToken();
1901 
1902         // Try getting next page with the wrong package, package2
1903         AppSearchException e =
1904                 assertThrows(
1905                         AppSearchException.class,
1906                         () ->
1907                                 mAppSearchImpl.getNextPage(
1908                                         "package2", nextPageToken, /* statsBuilder= */ null));
1909         assertThat(e)
1910                 .hasMessageThat()
1911                 .contains("Package \"package2\" cannot use nextPageToken: " + nextPageToken);
1912         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_SECURITY_ERROR);
1913 
1914         // Can continue getting next page for package1
1915         searchResultPage =
1916                 mAppSearchImpl.getNextPage("package1", nextPageToken, /* statsBuilder= */ null);
1917         assertThat(searchResultPage.getResults()).hasSize(1);
1918         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document1);
1919     }
1920 
1921     @Test
testInvalidateNextPageToken_query()1922     public void testInvalidateNextPageToken_query() throws Exception {
1923         // Insert package1 schema
1924         List<AppSearchSchema> schema1 =
1925                 ImmutableList.of(new AppSearchSchema.Builder("schema1").build());
1926         InternalSetSchemaResponse internalSetSchemaResponse =
1927                 mAppSearchImpl.setSchema(
1928                         "package1",
1929                         "database1",
1930                         schema1,
1931                         /* visibilityConfigs= */ Collections.emptyList(),
1932                         /* forceOverride= */ false,
1933                         /* version= */ 0,
1934                         /* setSchemaStatsBuilder= */ null);
1935         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
1936 
1937         // Insert two package1 documents
1938         GenericDocument document1 =
1939                 new GenericDocument.Builder<>("namespace", "id1", "schema1").build();
1940         GenericDocument document2 =
1941                 new GenericDocument.Builder<>("namespace", "id2", "schema1").build();
1942         mAppSearchImpl.putDocument(
1943                 "package1",
1944                 "database1",
1945                 document1,
1946                 /* sendChangeNotifications= */ false,
1947                 /* logger= */ null);
1948         mAppSearchImpl.putDocument(
1949                 "package1",
1950                 "database1",
1951                 document2,
1952                 /* sendChangeNotifications= */ false,
1953                 /* logger= */ null);
1954 
1955         // Query for only 1 result per page
1956         SearchSpec searchSpec =
1957                 new SearchSpec.Builder()
1958                         .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
1959                         .setResultCountPerPage(1)
1960                         .build();
1961         SearchResultPage searchResultPage =
1962                 mAppSearchImpl.query("package1", "database1", "", searchSpec, /* logger= */ null);
1963 
1964         // Document2 will come first because it was inserted last and default return order is
1965         // most recent.
1966         assertThat(searchResultPage.getResults()).hasSize(1);
1967         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document2);
1968 
1969         long nextPageToken = searchResultPage.getNextPageToken();
1970 
1971         // Invalidate the token
1972         mAppSearchImpl.invalidateNextPageToken("package1", nextPageToken);
1973 
1974         // Can't get next page because we invalidated the token.
1975         AppSearchException e =
1976                 assertThrows(
1977                         AppSearchException.class,
1978                         () ->
1979                                 mAppSearchImpl.getNextPage(
1980                                         "package1", nextPageToken, /* statsBuilder= */ null));
1981         assertThat(e)
1982                 .hasMessageThat()
1983                 .contains("Package \"package1\" cannot use nextPageToken: " + nextPageToken);
1984         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_SECURITY_ERROR);
1985     }
1986 
1987     @Test
testInvalidateNextPageToken_zeroNextPageToken()1988     public void testInvalidateNextPageToken_zeroNextPageToken() throws Exception {
1989         // Insert package1 schema
1990         List<AppSearchSchema> schema1 =
1991                 ImmutableList.of(new AppSearchSchema.Builder("schema1").build());
1992         InternalSetSchemaResponse internalSetSchemaResponse =
1993                 mAppSearchImpl.setSchema(
1994                         "package1",
1995                         "database1",
1996                         schema1,
1997                         /* visibilityConfigs= */ Collections.emptyList(),
1998                         /* forceOverride= */ false,
1999                         /* version= */ 0,
2000                         /* setSchemaStatsBuilder= */ null);
2001         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
2002 
2003         // Insert one package1 documents
2004         GenericDocument document1 =
2005                 new GenericDocument.Builder<>("namespace", "id1", "schema1").build();
2006         mAppSearchImpl.putDocument(
2007                 "package1",
2008                 "database1",
2009                 document1,
2010                 /* sendChangeNotifications= */ false,
2011                 /* logger= */ null);
2012 
2013         // Query for 2 results per page, so all the results can fit in one page.
2014         SearchSpec searchSpec =
2015                 new SearchSpec.Builder()
2016                         .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
2017                         .setResultCountPerPage(
2018                                 2) // make sure all the results can be returned in one page.
2019                         .build();
2020         SearchResultPage searchResultPage =
2021                 mAppSearchImpl.query("package1", "database1", "", searchSpec, /* logger= */ null);
2022 
2023         // We only have one document indexed
2024         assertThat(searchResultPage.getResults()).hasSize(1);
2025 
2026         // nextPageToken should be 0 since there is no more results
2027         long nextPageToken = searchResultPage.getNextPageToken();
2028         assertThat(nextPageToken).isEqualTo(0);
2029 
2030         // Invalidate the token, no exception should be thrown
2031         mAppSearchImpl.invalidateNextPageToken("package1", nextPageToken);
2032     }
2033 
2034     @Test
testInvalidateNextPageTokenWithDifferentPackage_query()2035     public void testInvalidateNextPageTokenWithDifferentPackage_query() throws Exception {
2036         // Insert package1 schema
2037         List<AppSearchSchema> schema1 =
2038                 ImmutableList.of(new AppSearchSchema.Builder("schema1").build());
2039         InternalSetSchemaResponse internalSetSchemaResponse =
2040                 mAppSearchImpl.setSchema(
2041                         "package1",
2042                         "database1",
2043                         schema1,
2044                         /* visibilityConfigs= */ Collections.emptyList(),
2045                         /* forceOverride= */ false,
2046                         /* version= */ 0,
2047                         /* setSchemaStatsBuilder= */ null);
2048         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
2049 
2050         // Insert two package1 documents
2051         GenericDocument document1 =
2052                 new GenericDocument.Builder<>("namespace", "id1", "schema1").build();
2053         GenericDocument document2 =
2054                 new GenericDocument.Builder<>("namespace", "id2", "schema1").build();
2055         mAppSearchImpl.putDocument(
2056                 "package1",
2057                 "database1",
2058                 document1,
2059                 /* sendChangeNotifications= */ false,
2060                 /* logger= */ null);
2061         mAppSearchImpl.putDocument(
2062                 "package1",
2063                 "database1",
2064                 document2,
2065                 /* sendChangeNotifications= */ false,
2066                 /* logger= */ null);
2067 
2068         // Query for only 1 result per page
2069         SearchSpec searchSpec =
2070                 new SearchSpec.Builder()
2071                         .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
2072                         .setResultCountPerPage(1)
2073                         .build();
2074         SearchResultPage searchResultPage =
2075                 mAppSearchImpl.query("package1", "database1", "", searchSpec, /* logger= */ null);
2076 
2077         // Document2 will come first because it was inserted last and default return order is
2078         // most recent.
2079         assertThat(searchResultPage.getResults()).hasSize(1);
2080         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document2);
2081 
2082         long nextPageToken = searchResultPage.getNextPageToken();
2083 
2084         // Try getting next page with the wrong package, package2
2085         AppSearchException e =
2086                 assertThrows(
2087                         AppSearchException.class,
2088                         () -> mAppSearchImpl.invalidateNextPageToken("package2", nextPageToken));
2089         assertThat(e)
2090                 .hasMessageThat()
2091                 .contains("Package \"package2\" cannot use nextPageToken: " + nextPageToken);
2092         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_SECURITY_ERROR);
2093 
2094         // Can continue getting next page for package1
2095         searchResultPage =
2096                 mAppSearchImpl.getNextPage("package1", nextPageToken, /* statsBuilder= */ null);
2097         assertThat(searchResultPage.getResults()).hasSize(1);
2098         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document1);
2099     }
2100 
2101     @Test
testInvalidateNextPageToken_globalQuery()2102     public void testInvalidateNextPageToken_globalQuery() throws Exception {
2103         // Insert package1 schema
2104         List<AppSearchSchema> schema1 =
2105                 ImmutableList.of(new AppSearchSchema.Builder("schema1").build());
2106         InternalSetSchemaResponse internalSetSchemaResponse =
2107                 mAppSearchImpl.setSchema(
2108                         "package1",
2109                         "database1",
2110                         schema1,
2111                         /* visibilityConfigs= */ Collections.emptyList(),
2112                         /* forceOverride= */ false,
2113                         /* version= */ 0,
2114                         /* setSchemaStatsBuilder= */ null);
2115         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
2116 
2117         // Insert two package1 documents
2118         GenericDocument document1 =
2119                 new GenericDocument.Builder<>("namespace", "id1", "schema1").build();
2120         GenericDocument document2 =
2121                 new GenericDocument.Builder<>("namespace", "id2", "schema1").build();
2122         mAppSearchImpl.putDocument(
2123                 "package1",
2124                 "database1",
2125                 document1,
2126                 /* sendChangeNotifications= */ false,
2127                 /* logger= */ null);
2128         mAppSearchImpl.putDocument(
2129                 "package1",
2130                 "database1",
2131                 document2,
2132                 /* sendChangeNotifications= */ false,
2133                 /* logger= */ null);
2134 
2135         // Query for only 1 result per page
2136         SearchSpec searchSpec =
2137                 new SearchSpec.Builder()
2138                         .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
2139                         .setResultCountPerPage(1)
2140                         .build();
2141         SearchResultPage searchResultPage =
2142                 mAppSearchImpl.globalQuery(
2143                         /* queryExpression= */ "",
2144                         searchSpec,
2145                         new CallerAccess(/* callingPackageName= */ "package1"),
2146                         /* logger= */ null);
2147 
2148         // Document2 will come first because it was inserted last and default return order is
2149         // most recent.
2150         assertThat(searchResultPage.getResults()).hasSize(1);
2151         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document2);
2152 
2153         long nextPageToken = searchResultPage.getNextPageToken();
2154 
2155         // Invalidate the token
2156         mAppSearchImpl.invalidateNextPageToken("package1", nextPageToken);
2157 
2158         // Can't get next page because we invalidated the token.
2159         AppSearchException e =
2160                 assertThrows(
2161                         AppSearchException.class,
2162                         () ->
2163                                 mAppSearchImpl.getNextPage(
2164                                         "package1", nextPageToken, /* statsBuilder= */ null));
2165         assertThat(e)
2166                 .hasMessageThat()
2167                 .contains("Package \"package1\" cannot use nextPageToken: " + nextPageToken);
2168         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_SECURITY_ERROR);
2169     }
2170 
2171     @Test
testInvalidateNextPageTokenWithDifferentPackage_globalQuery()2172     public void testInvalidateNextPageTokenWithDifferentPackage_globalQuery() throws Exception {
2173         // Insert package1 schema
2174         List<AppSearchSchema> schema1 =
2175                 ImmutableList.of(new AppSearchSchema.Builder("schema1").build());
2176         InternalSetSchemaResponse internalSetSchemaResponse =
2177                 mAppSearchImpl.setSchema(
2178                         "package1",
2179                         "database1",
2180                         schema1,
2181                         /* visibilityConfigs= */ Collections.emptyList(),
2182                         /* forceOverride= */ false,
2183                         /* version= */ 0,
2184                         /* setSchemaStatsBuilder= */ null);
2185         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
2186 
2187         // Insert two package1 documents
2188         GenericDocument document1 =
2189                 new GenericDocument.Builder<>("namespace", "id1", "schema1").build();
2190         GenericDocument document2 =
2191                 new GenericDocument.Builder<>("namespace", "id2", "schema1").build();
2192         mAppSearchImpl.putDocument(
2193                 "package1",
2194                 "database1",
2195                 document1,
2196                 /* sendChangeNotifications= */ false,
2197                 /* logger= */ null);
2198         mAppSearchImpl.putDocument(
2199                 "package1",
2200                 "database1",
2201                 document2,
2202                 /* sendChangeNotifications= */ false,
2203                 /* logger= */ null);
2204 
2205         // Query for only 1 result per page
2206         SearchSpec searchSpec =
2207                 new SearchSpec.Builder()
2208                         .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
2209                         .setResultCountPerPage(1)
2210                         .build();
2211         SearchResultPage searchResultPage =
2212                 mAppSearchImpl.globalQuery(
2213                         /* queryExpression= */ "",
2214                         searchSpec,
2215                         new CallerAccess(/* callingPackageName= */ "package1"),
2216                         /* logger= */ null);
2217 
2218         // Document2 will come first because it was inserted last and default return order is
2219         // most recent.
2220         assertThat(searchResultPage.getResults()).hasSize(1);
2221         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document2);
2222 
2223         long nextPageToken = searchResultPage.getNextPageToken();
2224 
2225         // Try getting next page with the wrong package, package2
2226         AppSearchException e =
2227                 assertThrows(
2228                         AppSearchException.class,
2229                         () -> mAppSearchImpl.invalidateNextPageToken("package2", nextPageToken));
2230         assertThat(e)
2231                 .hasMessageThat()
2232                 .contains("Package \"package2\" cannot use nextPageToken: " + nextPageToken);
2233         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_SECURITY_ERROR);
2234 
2235         // Can continue getting next page for package1
2236         searchResultPage =
2237                 mAppSearchImpl.getNextPage("package1", nextPageToken, /* statsBuilder= */ null);
2238         assertThat(searchResultPage.getResults()).hasSize(1);
2239         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document1);
2240     }
2241 
2242     @Test
testRemoveEmptyDatabase_noExceptionThrown()2243     public void testRemoveEmptyDatabase_noExceptionThrown() throws Exception {
2244         SearchSpec searchSpec =
2245                 new SearchSpec.Builder()
2246                         .addFilterSchemas("FakeType")
2247                         .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
2248                         .build();
2249         mAppSearchImpl.removeByQuery(
2250                 "package", "EmptyDatabase", "", searchSpec, /* statsBuilder= */ null);
2251 
2252         searchSpec =
2253                 new SearchSpec.Builder()
2254                         .addFilterNamespaces("FakeNamespace")
2255                         .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
2256                         .build();
2257         mAppSearchImpl.removeByQuery(
2258                 "package", "EmptyDatabase", "", searchSpec, /* statsBuilder= */ null);
2259 
2260         searchSpec = new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE).build();
2261         mAppSearchImpl.removeByQuery(
2262                 "package", "EmptyDatabase", "", searchSpec, /* statsBuilder= */ null);
2263     }
2264 
2265     @Test
testSetSchema()2266     public void testSetSchema() throws Exception {
2267         List<SchemaTypeConfigProto> existingSchemas =
2268                 mAppSearchImpl.getSchemaProtoLocked().getTypesList();
2269 
2270         List<AppSearchSchema> schemas =
2271                 Collections.singletonList(new AppSearchSchema.Builder("Email").build());
2272         // Set schema Email to AppSearch database1
2273         InternalSetSchemaResponse internalSetSchemaResponse =
2274                 mAppSearchImpl.setSchema(
2275                         "package",
2276                         "database1",
2277                         schemas,
2278                         /* visibilityConfigs= */ Collections.emptyList(),
2279                         /* forceOverride= */ false,
2280                         /* version= */ 0,
2281                         /* setSchemaStatsBuilder= */ null);
2282         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
2283 
2284         // Create expected schemaType proto.
2285         SchemaProto expectedProto =
2286                 SchemaProto.newBuilder()
2287                         .addTypes(
2288                                 SchemaTypeConfigProto.newBuilder()
2289                                         .setSchemaType("package$database1/Email")
2290                                         .setDescription("")
2291                                         .setVersion(0))
2292                         .build();
2293 
2294         List<SchemaTypeConfigProto> expectedTypes = new ArrayList<>();
2295         expectedTypes.addAll(existingSchemas);
2296         expectedTypes.addAll(expectedProto.getTypesList());
2297         assertThat(mAppSearchImpl.getSchemaProtoLocked().getTypesList())
2298                 .containsExactlyElementsIn(expectedTypes);
2299     }
2300 
2301     @Test
testSetSchema_incompatible()2302     public void testSetSchema_incompatible() throws Exception {
2303         List<AppSearchSchema> oldSchemas = new ArrayList<>();
2304         oldSchemas.add(
2305                 new AppSearchSchema.Builder("Email")
2306                         .addProperty(
2307                                 new AppSearchSchema.StringPropertyConfig.Builder("foo")
2308                                         .setCardinality(
2309                                                 AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
2310                                         .setTokenizerType(
2311                                                 AppSearchSchema.StringPropertyConfig
2312                                                         .TOKENIZER_TYPE_PLAIN)
2313                                         .setIndexingType(
2314                                                 AppSearchSchema.StringPropertyConfig
2315                                                         .INDEXING_TYPE_PREFIXES)
2316                                         .build())
2317                         .build());
2318         oldSchemas.add(new AppSearchSchema.Builder("Text").build());
2319         // Set schema Email to AppSearch database1
2320         InternalSetSchemaResponse internalSetSchemaResponse =
2321                 mAppSearchImpl.setSchema(
2322                         "package",
2323                         "database1",
2324                         oldSchemas,
2325                         /* visibilityConfigs= */ Collections.emptyList(),
2326                         /* forceOverride= */ false,
2327                         /* version= */ 0,
2328                         /* setSchemaStatsBuilder= */ null);
2329         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
2330 
2331         // Create incompatible schema
2332         List<AppSearchSchema> newSchemas =
2333                 Collections.singletonList(new AppSearchSchema.Builder("Email").build());
2334 
2335         // set email incompatible and delete text
2336         internalSetSchemaResponse =
2337                 mAppSearchImpl.setSchema(
2338                         "package",
2339                         "database1",
2340                         newSchemas,
2341                         /* visibilityConfigs= */ Collections.emptyList(),
2342                         /* forceOverride= */ true,
2343                         /* version= */ 0,
2344                         /* setSchemaStatsBuilder= */ null);
2345         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
2346         SetSchemaResponse setSchemaResponse = internalSetSchemaResponse.getSetSchemaResponse();
2347 
2348         assertThat(setSchemaResponse.getDeletedTypes()).containsExactly("Text");
2349         assertThat(setSchemaResponse.getIncompatibleTypes()).containsExactly("Email");
2350     }
2351 
2352     @Test
testRemoveSchema()2353     public void testRemoveSchema() throws Exception {
2354         List<SchemaTypeConfigProto> existingSchemas =
2355                 mAppSearchImpl.getSchemaProtoLocked().getTypesList();
2356 
2357         List<AppSearchSchema> schemas =
2358                 ImmutableList.of(
2359                         new AppSearchSchema.Builder("Email").build(),
2360                         new AppSearchSchema.Builder("Document").build());
2361         // Set schema Email and Document to AppSearch database1
2362         InternalSetSchemaResponse internalSetSchemaResponse =
2363                 mAppSearchImpl.setSchema(
2364                         "package",
2365                         "database1",
2366                         schemas,
2367                         /* visibilityConfigs= */ Collections.emptyList(),
2368                         /* forceOverride= */ false,
2369                         /* version= */ 0,
2370                         /* setSchemaStatsBuilder= */ null);
2371         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
2372 
2373         // Create expected schemaType proto.
2374         SchemaProto expectedProto =
2375                 SchemaProto.newBuilder()
2376                         .addTypes(
2377                                 SchemaTypeConfigProto.newBuilder()
2378                                         .setSchemaType("package$database1/Email")
2379                                         .setDescription("")
2380                                         .setVersion(0))
2381                         .addTypes(
2382                                 SchemaTypeConfigProto.newBuilder()
2383                                         .setSchemaType("package$database1/Document")
2384                                         .setDescription("")
2385                                         .setVersion(0))
2386                         .build();
2387 
2388         // Check both schema Email and Document saved correctly.
2389         List<SchemaTypeConfigProto> expectedTypes = new ArrayList<>();
2390         expectedTypes.addAll(existingSchemas);
2391         expectedTypes.addAll(expectedProto.getTypesList());
2392         assertThat(mAppSearchImpl.getSchemaProtoLocked().getTypesList())
2393                 .containsExactlyElementsIn(expectedTypes);
2394 
2395         final List<AppSearchSchema> finalSchemas =
2396                 Collections.singletonList(new AppSearchSchema.Builder("Email").build());
2397         internalSetSchemaResponse =
2398                 mAppSearchImpl.setSchema(
2399                         "package",
2400                         "database1",
2401                         finalSchemas,
2402                         /* visibilityConfigs= */ Collections.emptyList(),
2403                         /* forceOverride= */ false,
2404                         /* version= */ 0,
2405                         /* setSchemaStatsBuilder= */ null);
2406         // We are fail to set this call since forceOverride is false.
2407         assertThat(internalSetSchemaResponse.isSuccess()).isFalse();
2408         SetSchemaResponse setSchemaResponse = internalSetSchemaResponse.getSetSchemaResponse();
2409         // Check the incompatible reason is we are trying to delete Document type.
2410         assertThat(setSchemaResponse.getDeletedTypes()).containsExactly("Document");
2411 
2412         // ForceOverride to delete.
2413         internalSetSchemaResponse =
2414                 mAppSearchImpl.setSchema(
2415                         "package",
2416                         "database1",
2417                         finalSchemas,
2418                         /* visibilityConfigs= */ Collections.emptyList(),
2419                         /* forceOverride= */ true,
2420                         /* version= */ 0,
2421                         /* setSchemaStatsBuilder= */ null);
2422         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
2423 
2424         // Check Document schema is removed.
2425         expectedProto =
2426                 SchemaProto.newBuilder()
2427                         .addTypes(
2428                                 SchemaTypeConfigProto.newBuilder()
2429                                         .setSchemaType("package$database1/Email")
2430                                         .setDescription("")
2431                                         .setVersion(0))
2432                         .build();
2433 
2434         expectedTypes = new ArrayList<>();
2435         expectedTypes.addAll(existingSchemas);
2436         expectedTypes.addAll(expectedProto.getTypesList());
2437         assertThat(mAppSearchImpl.getSchemaProtoLocked().getTypesList())
2438                 .containsExactlyElementsIn(expectedTypes);
2439     }
2440 
2441     @Test
testRemoveSchema_differentDataBase()2442     public void testRemoveSchema_differentDataBase() throws Exception {
2443         List<SchemaTypeConfigProto> existingSchemas =
2444                 mAppSearchImpl.getSchemaProtoLocked().getTypesList();
2445 
2446         // Create schemas
2447         List<AppSearchSchema> schemas =
2448                 ImmutableList.of(
2449                         new AppSearchSchema.Builder("Email").build(),
2450                         new AppSearchSchema.Builder("Document").build());
2451 
2452         // Set schema Email and Document to AppSearch database1 and 2
2453         InternalSetSchemaResponse internalSetSchemaResponse =
2454                 mAppSearchImpl.setSchema(
2455                         "package",
2456                         "database1",
2457                         schemas,
2458                         /* visibilityConfigs= */ Collections.emptyList(),
2459                         /* forceOverride= */ false,
2460                         /* version= */ 0,
2461                         /* setSchemaStatsBuilder= */ null);
2462         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
2463         internalSetSchemaResponse =
2464                 mAppSearchImpl.setSchema(
2465                         "package",
2466                         "database2",
2467                         schemas,
2468                         /* visibilityConfigs= */ Collections.emptyList(),
2469                         /* forceOverride= */ false,
2470                         /* version= */ 0,
2471                         /* setSchemaStatsBuilder= */ null);
2472         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
2473 
2474         // Create expected schemaType proto.
2475         SchemaProto expectedProto =
2476                 SchemaProto.newBuilder()
2477                         .addTypes(
2478                                 SchemaTypeConfigProto.newBuilder()
2479                                         .setSchemaType("package$database1/Email")
2480                                         .setDescription("")
2481                                         .setVersion(0))
2482                         .addTypes(
2483                                 SchemaTypeConfigProto.newBuilder()
2484                                         .setSchemaType("package$database1/Document")
2485                                         .setDescription("")
2486                                         .setVersion(0))
2487                         .addTypes(
2488                                 SchemaTypeConfigProto.newBuilder()
2489                                         .setSchemaType("package$database2/Email")
2490                                         .setDescription("")
2491                                         .setVersion(0))
2492                         .addTypes(
2493                                 SchemaTypeConfigProto.newBuilder()
2494                                         .setSchemaType("package$database2/Document")
2495                                         .setDescription("")
2496                                         .setVersion(0))
2497                         .build();
2498 
2499         // Check Email and Document is saved in database 1 and 2 correctly.
2500         List<SchemaTypeConfigProto> expectedTypes = new ArrayList<>();
2501         expectedTypes.addAll(existingSchemas);
2502         expectedTypes.addAll(expectedProto.getTypesList());
2503         assertThat(mAppSearchImpl.getSchemaProtoLocked().getTypesList())
2504                 .containsExactlyElementsIn(expectedTypes);
2505 
2506         // Save only Email to database1 this time.
2507         schemas = Collections.singletonList(new AppSearchSchema.Builder("Email").build());
2508         internalSetSchemaResponse =
2509                 mAppSearchImpl.setSchema(
2510                         "package",
2511                         "database1",
2512                         schemas,
2513                         /* visibilityConfigs= */ Collections.emptyList(),
2514                         /* forceOverride= */ true,
2515                         /* version= */ 0,
2516                         /* setSchemaStatsBuilder= */ null);
2517         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
2518 
2519         // Create expected schemaType list, database 1 should only contain Email but database 2
2520         // remains in same.
2521         expectedProto =
2522                 SchemaProto.newBuilder()
2523                         .addTypes(
2524                                 SchemaTypeConfigProto.newBuilder()
2525                                         .setSchemaType("package$database1/Email")
2526                                         .setDescription("")
2527                                         .setVersion(0))
2528                         .addTypes(
2529                                 SchemaTypeConfigProto.newBuilder()
2530                                         .setSchemaType("package$database2/Email")
2531                                         .setDescription("")
2532                                         .setVersion(0))
2533                         .addTypes(
2534                                 SchemaTypeConfigProto.newBuilder()
2535                                         .setSchemaType("package$database2/Document")
2536                                         .setDescription("")
2537                                         .setVersion(0))
2538                         .build();
2539 
2540         // Check nothing changed in database2.
2541         expectedTypes = new ArrayList<>();
2542         expectedTypes.addAll(existingSchemas);
2543         expectedTypes.addAll(expectedProto.getTypesList());
2544         assertThat(mAppSearchImpl.getSchemaProtoLocked().getTypesList())
2545                 .containsExactlyElementsIn(expectedTypes);
2546     }
2547 
2548     @Test
2549     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
testWriteAndReadBlob()2550     public void testWriteAndReadBlob() throws Exception {
2551         mAppSearchImpl =
2552                 AppSearchImpl.create(
2553                         mAppSearchDir,
2554                         new AppSearchConfigImpl(
2555                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
2556                         /* initStatsBuilder= */ null,
2557                         /* visibilityChecker= */ null,
2558                         new JetpackRevocableFileDescriptorStore(mUnlimitedConfig),
2559                         ALWAYS_OPTIMIZE);
2560         byte[] data = generateRandomBytes(20 * 1024); // 20 KiB
2561         byte[] digest = calculateDigest(data);
2562         AppSearchBlobHandle handle =
2563                 AppSearchBlobHandle.createWithSha256(digest, "package", "db1", "ns");
2564         try (ParcelFileDescriptor writePfd =
2565                         mAppSearchImpl.openWriteBlob("package", "db1", handle);
2566                 OutputStream outputStream =
2567                         new ParcelFileDescriptor.AutoCloseOutputStream(writePfd)) {
2568             outputStream.write(data);
2569             outputStream.flush();
2570         }
2571 
2572         // commit the change and read the blob.
2573         mAppSearchImpl.commitBlob("package", "db1", handle);
2574         byte[] readBytes = new byte[20 * 1024];
2575         try (ParcelFileDescriptor readPfd = mAppSearchImpl.openReadBlob("package", "db1", handle);
2576                 InputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(readPfd)) {
2577             inputStream.read(readBytes);
2578         }
2579         assertThat(readBytes).isEqualTo(data);
2580     }
2581 
2582     @Test
2583     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
testRemovePendingBlob()2584     public void testRemovePendingBlob() throws Exception {
2585         mAppSearchImpl =
2586                 AppSearchImpl.create(
2587                         mAppSearchDir,
2588                         new AppSearchConfigImpl(
2589                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
2590                         /* initStatsBuilder= */ null,
2591                         /* visibilityChecker= */ null,
2592                         new JetpackRevocableFileDescriptorStore(mUnlimitedConfig),
2593                         ALWAYS_OPTIMIZE);
2594         byte[] data = generateRandomBytes(20 * 1024); // 20 KiB
2595         byte[] digest = calculateDigest(data);
2596         AppSearchBlobHandle handle =
2597                 AppSearchBlobHandle.createWithSha256(digest, "package", "db1", "ns");
2598         try (ParcelFileDescriptor writePfd =
2599                         mAppSearchImpl.openWriteBlob("package", "db1", handle);
2600                 OutputStream outputStream =
2601                         new ParcelFileDescriptor.AutoCloseOutputStream(writePfd)) {
2602             outputStream.write(data);
2603             outputStream.flush();
2604         }
2605 
2606         mAppSearchImpl.commitBlob("package", "db1", handle);
2607 
2608         // Remove the committed blob
2609         mAppSearchImpl.removeBlob("package", "db1", handle);
2610 
2611         // Read will get NOT_FOUND
2612         AppSearchException e =
2613                 assertThrows(
2614                         AppSearchException.class,
2615                         () -> mAppSearchImpl.openReadBlob("package", "db1", handle));
2616         assertThat(e.getResultCode()).isEqualTo(RESULT_NOT_FOUND);
2617         assertThat(e.getMessage()).contains("Cannot find the blob for handle");
2618     }
2619 
2620     @Test
2621     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
testRemoveCommittedBlob()2622     public void testRemoveCommittedBlob() throws Exception {
2623         mAppSearchImpl =
2624                 AppSearchImpl.create(
2625                         mAppSearchDir,
2626                         new AppSearchConfigImpl(
2627                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
2628                         /* initStatsBuilder= */ null,
2629                         /* visibilityChecker= */ null,
2630                         new JetpackRevocableFileDescriptorStore(mUnlimitedConfig),
2631                         ALWAYS_OPTIMIZE);
2632         byte[] data = generateRandomBytes(20 * 1024); // 20 KiB
2633         byte[] digest = calculateDigest(data);
2634         AppSearchBlobHandle handle =
2635                 AppSearchBlobHandle.createWithSha256(digest, "package", "db1", "ns");
2636         try (ParcelFileDescriptor writePfd =
2637                         mAppSearchImpl.openWriteBlob("package", "db1", handle);
2638                 OutputStream outputStream =
2639                         new ParcelFileDescriptor.AutoCloseOutputStream(writePfd)) {
2640             outputStream.write(data);
2641             outputStream.flush();
2642         }
2643 
2644         // Remove the blob
2645         mAppSearchImpl.removeBlob("package", "db1", handle);
2646 
2647         // Commit will get NOT_FOUND
2648         AppSearchException e =
2649                 assertThrows(
2650                         AppSearchException.class,
2651                         () -> mAppSearchImpl.commitBlob("package", "db1", handle));
2652         assertThat(e.getResultCode()).isEqualTo(RESULT_NOT_FOUND);
2653         assertThat(e.getMessage()).contains("Cannot find the blob for handle");
2654     }
2655 
2656     @Test
2657     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
testRemoveAndReWriteBlob()2658     public void testRemoveAndReWriteBlob() throws Exception {
2659         mAppSearchImpl =
2660                 AppSearchImpl.create(
2661                         mAppSearchDir,
2662                         new AppSearchConfigImpl(
2663                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
2664                         /* initStatsBuilder= */ null,
2665                         /* visibilityChecker= */ null,
2666                         new JetpackRevocableFileDescriptorStore(mUnlimitedConfig),
2667                         ALWAYS_OPTIMIZE);
2668         byte[] data = generateRandomBytes(20 * 1024); // 20 KiB
2669         byte[] wrongData = generateRandomBytes(10 * 1024); // 10 KiB
2670         byte[] digest = calculateDigest(data);
2671         AppSearchBlobHandle handle =
2672                 AppSearchBlobHandle.createWithSha256(digest, "package", "db1", "ns");
2673         try (ParcelFileDescriptor writePfd =
2674                         mAppSearchImpl.openWriteBlob("package", "db1", handle);
2675                 OutputStream outputStream =
2676                         new ParcelFileDescriptor.AutoCloseOutputStream(writePfd)) {
2677             // write wrong data
2678             outputStream.write(wrongData);
2679             outputStream.flush();
2680         }
2681 
2682         // Remove the blob
2683         mAppSearchImpl.removeBlob("package", "db1", handle);
2684 
2685         // reopen and rewrite
2686         try (ParcelFileDescriptor writePfd =
2687                         mAppSearchImpl.openWriteBlob("package", "db1", handle);
2688                 OutputStream outputStream =
2689                         new ParcelFileDescriptor.AutoCloseOutputStream(writePfd)) {
2690             outputStream.write(data);
2691             outputStream.flush();
2692         }
2693 
2694         // commit the change and read the blob.
2695         mAppSearchImpl.commitBlob("package", "db1", handle);
2696         byte[] readBytes = new byte[20 * 1024];
2697         try (ParcelFileDescriptor readPfd = mAppSearchImpl.openReadBlob("package", "db1", handle);
2698                 InputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(readPfd)) {
2699             inputStream.read(readBytes);
2700         }
2701         assertThat(readBytes).isEqualTo(data);
2702     }
2703 
2704     @Test
2705     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
testOpenReadForWrite_notAllowed()2706     public void testOpenReadForWrite_notAllowed() throws Exception {
2707         mAppSearchImpl =
2708                 AppSearchImpl.create(
2709                         mAppSearchDir,
2710                         new AppSearchConfigImpl(
2711                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
2712                         /* initStatsBuilder= */ null,
2713                         /* visibilityChecker= */ null,
2714                         new JetpackRevocableFileDescriptorStore(mUnlimitedConfig),
2715                         ALWAYS_OPTIMIZE);
2716         byte[] data = generateRandomBytes(20); // 20 Bytes
2717         byte[] digest = calculateDigest(data);
2718         AppSearchBlobHandle handle =
2719                 AppSearchBlobHandle.createWithSha256(digest, "package", "db1", "ns");
2720         try (ParcelFileDescriptor writePfd =
2721                         mAppSearchImpl.openWriteBlob("package", "db1", handle);
2722                 OutputStream outputStream =
2723                         new ParcelFileDescriptor.AutoCloseOutputStream(writePfd)) {
2724             outputStream.write(data);
2725             outputStream.flush();
2726         }
2727 
2728         // commit the change and read the blob.
2729         mAppSearchImpl.commitBlob("package", "db1", handle);
2730 
2731         // Open output stream on read-only pfd.
2732         assertThrows(
2733                 IOException.class,
2734                 () -> {
2735                     try (ParcelFileDescriptor readPfd =
2736                                     mAppSearchImpl.openReadBlob("package", "db1", handle);
2737                             OutputStream outputStream =
2738                                     new ParcelFileDescriptor.AutoCloseOutputStream(readPfd)) {
2739                         outputStream.write(data);
2740                     }
2741                 });
2742     }
2743 
2744     @Test
2745     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
testOpenWriteForRead_allowed()2746     public void testOpenWriteForRead_allowed() throws Exception {
2747         mAppSearchImpl =
2748                 AppSearchImpl.create(
2749                         mAppSearchDir,
2750                         new AppSearchConfigImpl(
2751                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
2752                         /* initStatsBuilder= */ null,
2753                         /* visibilityChecker= */ null,
2754                         new JetpackRevocableFileDescriptorStore(mUnlimitedConfig),
2755                         ALWAYS_OPTIMIZE);
2756         byte[] data = generateRandomBytes(20); // 20 Bytes
2757         byte[] digest = calculateDigest(data);
2758         AppSearchBlobHandle handle =
2759                 AppSearchBlobHandle.createWithSha256(digest, "package", "db1", "ns");
2760         // openWriteBlob returns read and write fd.
2761         try (ParcelFileDescriptor writePfd =
2762                         mAppSearchImpl.openWriteBlob("package", "db1", handle);
2763                 InputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(writePfd)) {
2764             inputStream.read(new byte[10]);
2765         }
2766     }
2767 
2768     @Test
2769     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
testOptimizeBlob()2770     public void testOptimizeBlob() throws Exception {
2771         // Create a new AppSearchImpl with lower orphan blob time to live.
2772         mAppSearchImpl.close();
2773         File tempFolder = mTemporaryFolder.newFolder();
2774         mAppSearchImpl =
2775                 AppSearchImpl.create(
2776                         tempFolder,
2777                         new AppSearchConfigImpl(
2778                                 new UnlimitedLimitConfig(),
2779                                 new LocalStorageIcingOptionsConfig() {
2780                                     @Override
2781                                     public long getOrphanBlobTimeToLiveMs() {
2782                                         // 0 will make it non-expire
2783                                         return 1L;
2784                                     }
2785                                 }),
2786                         /* initStatsBuilder= */ null,
2787                         /* visibilityChecker= */ null,
2788                         new JetpackRevocableFileDescriptorStore(mUnlimitedConfig),
2789                         ALWAYS_OPTIMIZE);
2790 
2791         // Write the blob and commit it.
2792         byte[] data = generateRandomBytes(20); // 20 Bytes
2793         byte[] digest = calculateDigest(data);
2794         AppSearchBlobHandle handle =
2795                 AppSearchBlobHandle.createWithSha256(digest, "package", "db1", "namespace");
2796         ParcelFileDescriptor writePfd = mAppSearchImpl.openWriteBlob("package", "db1", handle);
2797         try (OutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream(writePfd)) {
2798             outputStream.write(data);
2799             outputStream.flush();
2800         }
2801         writePfd.close();
2802         mAppSearchImpl.commitBlob("package", "db1", handle);
2803 
2804         mAppSearchImpl.persistToDisk(PersistType.Code.FULL);
2805 
2806         // Optimize remove the expired orphan blob.
2807         mAppSearchImpl.optimize(/* builder= */ null);
2808         AppSearchException e =
2809                 assertThrows(
2810                         AppSearchException.class,
2811                         () -> {
2812                             mAppSearchImpl.openReadBlob("package", "db1", handle);
2813                         });
2814         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
2815         assertThat(e.getMessage()).contains("Cannot find the blob for handle");
2816     }
2817 
2818     @Test
2819     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
testOptimizeBlobWithDocument()2820     public void testOptimizeBlobWithDocument() throws Exception {
2821         // Create a new AppSearchImpl with lower orphan blob time to live.
2822         mAppSearchImpl.close();
2823         File tempFolder = mTemporaryFolder.newFolder();
2824         mAppSearchImpl =
2825                 AppSearchImpl.create(
2826                         tempFolder,
2827                         new AppSearchConfigImpl(
2828                                 new UnlimitedLimitConfig(),
2829                                 new LocalStorageIcingOptionsConfig() {
2830                                     @Override
2831                                     public long getOrphanBlobTimeToLiveMs() {
2832                                         // 0 will make it non-expire
2833                                         return 1L;
2834                                     }
2835                                 }),
2836                         /* initStatsBuilder= */ null,
2837                         /* visibilityChecker= */ null,
2838                         new JetpackRevocableFileDescriptorStore(mUnlimitedConfig),
2839                         ALWAYS_OPTIMIZE);
2840 
2841         // Write the blob and commit it.
2842         byte[] data = generateRandomBytes(20); // 20 Bytes
2843         byte[] digest = calculateDigest(data);
2844         AppSearchBlobHandle handle =
2845                 AppSearchBlobHandle.createWithSha256(digest, "package", "db1", "namespace");
2846         ParcelFileDescriptor writePfd = mAppSearchImpl.openWriteBlob("package", "db1", handle);
2847         try (OutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream(writePfd)) {
2848             outputStream.write(data);
2849             outputStream.flush();
2850         }
2851         writePfd.close();
2852         mAppSearchImpl.commitBlob("package", "db1", handle);
2853 
2854         // Put a document link that blob handle.
2855         AppSearchSchema schema =
2856                 new AppSearchSchema.Builder("Type")
2857                         .addProperty(
2858                                 new AppSearchSchema.BlobHandlePropertyConfig.Builder("blob")
2859                                         .setCardinality(
2860                                                 AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
2861                                         .build())
2862                         .build();
2863         InternalSetSchemaResponse internalSetSchemaResponse =
2864                 mAppSearchImpl.setSchema(
2865                         "package",
2866                         "db1",
2867                         ImmutableList.of(schema),
2868                         /* visibilityConfigs= */ Collections.emptyList(),
2869                         /* forceOverride= */ true,
2870                         /* version= */ 0,
2871                         /* setSchemaStatsBuilder= */ null);
2872         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
2873         GenericDocument document =
2874                 new GenericDocument.Builder<>("namespace", "id", "Type")
2875                         .setPropertyBlobHandle("blob", handle)
2876                         .build();
2877         mAppSearchImpl.putDocument(
2878                 "package",
2879                 "db1",
2880                 document,
2881                 /* sendChangeNotifications= */ false,
2882                 /* logger= */ null);
2883 
2884         mAppSearchImpl.persistToDisk(PersistType.Code.FULL);
2885 
2886         // Optimize won't remove the blob since it has reference document.
2887         mAppSearchImpl.optimize(/* builder= */ null);
2888         byte[] readBytes = new byte[20];
2889         try (ParcelFileDescriptor readPfd = mAppSearchImpl.openReadBlob("package", "db1", handle);
2890                 InputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(readPfd)) {
2891             inputStream.read(readBytes);
2892         }
2893         assertThat(readBytes).isEqualTo(data);
2894 
2895         mAppSearchImpl.remove("package", "db1", "namespace", "id", /* statsBuilder= */ null);
2896 
2897         // The blob is orphan now and optimize will remove it.
2898         mAppSearchImpl.optimize(/* builder= */ null);
2899         AppSearchException e =
2900                 assertThrows(
2901                         AppSearchException.class,
2902                         () -> {
2903                             mAppSearchImpl.openReadBlob("package", "db1", handle);
2904                         });
2905         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
2906         assertThat(e.getMessage()).contains("Cannot find the blob for handle");
2907     }
2908 
2909     @Test
2910     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
testRevokeFileDescriptor()2911     public void testRevokeFileDescriptor() throws Exception {
2912         mAppSearchImpl =
2913                 AppSearchImpl.create(
2914                         mAppSearchDir,
2915                         new AppSearchConfigImpl(
2916                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
2917                         /* initStatsBuilder= */ null,
2918                         /* visibilityChecker= */ null,
2919                         new JetpackRevocableFileDescriptorStore(mUnlimitedConfig),
2920                         ALWAYS_OPTIMIZE);
2921         byte[] data = generateRandomBytes(20 * 1024); // 20 KiB
2922         byte[] digest = calculateDigest(data);
2923         AppSearchBlobHandle handle =
2924                 AppSearchBlobHandle.createWithSha256(digest, "package", "db1", "ns");
2925         try (ParcelFileDescriptor writePfd =
2926                 mAppSearchImpl.openWriteBlob("package", "db1", handle)) {
2927             // Clear package data and all file descriptor to that package will be revoked.
2928             mAppSearchImpl.clearPackageData("package");
2929 
2930             assertThrows(
2931                     IOException.class,
2932                     () -> {
2933                         try (OutputStream outputStream =
2934                                 new ParcelFileDescriptor.AutoCloseOutputStream(writePfd)) {
2935                             outputStream.write(data);
2936                         }
2937                     });
2938         }
2939 
2940         // reopen file descriptor could work.
2941         try (ParcelFileDescriptor writePfd2 =
2942                 mAppSearchImpl.openWriteBlob("package", "db1", handle)) {
2943             try (OutputStream outputStream =
2944                     new ParcelFileDescriptor.AutoCloseOutputStream(writePfd2)) {
2945                 outputStream.write(data);
2946             }
2947             // close the AppSearchImpl will revoke all sent fds.
2948             mAppSearchImpl.close();
2949             assertThrows(
2950                     IOException.class,
2951                     () -> {
2952                         try (OutputStream outputStream =
2953                                 new ParcelFileDescriptor.AutoCloseOutputStream(writePfd2)) {
2954                             outputStream.write(data);
2955                         }
2956                     });
2957         }
2958     }
2959 
2960     // Verify the blob handle won't sent request to Icing. So no need to enable
2961     // FLAG_ENABLE_BLOB_STORE.
2962     @Test
testInvalidBlobHandle()2963     public void testInvalidBlobHandle() throws Exception {
2964         mAppSearchImpl =
2965                 AppSearchImpl.create(
2966                         mAppSearchDir,
2967                         new AppSearchConfigImpl(
2968                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
2969                         /* initStatsBuilder= */ null,
2970                         /* visibilityChecker= */ null,
2971                         new JetpackRevocableFileDescriptorStore(mUnlimitedConfig),
2972                         ALWAYS_OPTIMIZE);
2973         byte[] data = generateRandomBytes(20 * 1024); // 20 KiB
2974         byte[] digest = calculateDigest(data);
2975         AppSearchBlobHandle handle =
2976                 AppSearchBlobHandle.createWithSha256(digest, "package", "db1", "ns");
2977 
2978         AppSearchException e =
2979                 assertThrows(
2980                         AppSearchException.class,
2981                         () -> mAppSearchImpl.openWriteBlob("wrongPackageName", "db1", handle));
2982         assertThat(e.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
2983         assertThat(e.getMessage())
2984                 .contains(
2985                         "Blob package doesn't match calling package, "
2986                                 + "calling package: wrongPackageName, blob package: package");
2987 
2988         e =
2989                 assertThrows(
2990                         AppSearchException.class,
2991                         () -> mAppSearchImpl.openWriteBlob("package", "wrongDb", handle));
2992         assertThat(e.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
2993         assertThat(e.getMessage())
2994                 .contains(
2995                         "Blob database doesn't match calling database, "
2996                                 + "calling database: wrongDb, blob database: db1");
2997     }
2998 
2999     @Test
3000     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
testSetBlobVisibility()3001     public void testSetBlobVisibility() throws Exception {
3002         mAppSearchImpl =
3003                 AppSearchImpl.create(
3004                         mAppSearchDir,
3005                         new AppSearchConfigImpl(
3006                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
3007                         /* initStatsBuilder= */ null,
3008                         /* visibilityChecker= */ null,
3009                         new JetpackRevocableFileDescriptorStore(mUnlimitedConfig),
3010                         ALWAYS_OPTIMIZE);
3011 
3012         SchemaVisibilityConfig visibleToConfig =
3013                 new SchemaVisibilityConfig.Builder()
3014                         .addAllowedPackage(new PackageIdentifier("pkgBar", new byte[32]))
3015                         .addRequiredPermissions(ImmutableSet.of(1, 2))
3016                         .setPubliclyVisibleTargetPackage(
3017                                 new PackageIdentifier("pkgFoo", new byte[32]))
3018                         .build();
3019         InternalVisibilityConfig config =
3020                 new InternalVisibilityConfig.Builder("namespace")
3021                         .setNotDisplayedBySystem(false)
3022                         .addVisibleToConfig(visibleToConfig)
3023                         .build();
3024 
3025         String prefix = PrefixUtil.createPrefix("package", "db1");
3026         mAppSearchImpl.setBlobNamespaceVisibility("package", "db1", ImmutableList.of(config));
3027 
3028         // Expect the config will be added prefix.
3029         InternalVisibilityConfig expectedConfig =
3030                 new InternalVisibilityConfig.Builder(prefix + "namespace")
3031                         .setNotDisplayedBySystem(false)
3032                         .addVisibleToConfig(visibleToConfig)
3033                         .build();
3034         assertThat(mAppSearchImpl.mBlobVisibilityStoreLocked.getVisibility(prefix + "namespace"))
3035                 .isEqualTo(expectedConfig);
3036 
3037         // Verify the InternalVisibilityConfig is saved to AppSearchImpl.
3038         GenericDocument visibilityDocument =
3039                 mAppSearchImpl.getDocument(
3040                         VISIBILITY_PACKAGE_NAME,
3041                         BLOB_VISIBILITY_DATABASE_NAME,
3042                         VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
3043                         /* id= */ prefix + "namespace",
3044                         /* typePropertyPaths= */ Collections.emptyMap());
3045         GenericDocument overLayVisibilityDocument =
3046                 mAppSearchImpl.getDocument(
3047                         VISIBILITY_PACKAGE_NAME,
3048                         BLOB_ANDROID_V_OVERLAY_DATABASE_NAME,
3049                         VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
3050                         /* id= */ prefix + "namespace",
3051                         /* typePropertyPaths= */ Collections.emptyMap());
3052 
3053         InternalVisibilityConfig outputConfig =
3054                 VisibilityToDocumentConverter.createInternalVisibilityConfig(
3055                         visibilityDocument, overLayVisibilityDocument);
3056 
3057         assertThat(outputConfig).isEqualTo(expectedConfig);
3058     }
3059 
3060     @Test
3061     @RequiresFlagsDisabled(Flags.FLAG_ENABLE_BLOB_STORE)
testSetBlobVisibility_notSupported()3062     public void testSetBlobVisibility_notSupported() throws Exception {
3063         mAppSearchImpl =
3064                 AppSearchImpl.create(
3065                         mAppSearchDir,
3066                         new AppSearchConfigImpl(
3067                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
3068                         /* initStatsBuilder= */ null,
3069                         /* visibilityChecker= */ null,
3070                         /* revocableFileDescriptorStore= */ null,
3071                         ALWAYS_OPTIMIZE);
3072 
3073         SchemaVisibilityConfig visibleToConfig =
3074                 new SchemaVisibilityConfig.Builder()
3075                         .addAllowedPackage(new PackageIdentifier("pkgBar", new byte[32]))
3076                         .addRequiredPermissions(ImmutableSet.of(1, 2))
3077                         .setPubliclyVisibleTargetPackage(
3078                                 new PackageIdentifier("pkgFoo", new byte[32]))
3079                         .build();
3080         InternalVisibilityConfig config =
3081                 new InternalVisibilityConfig.Builder("namespace")
3082                         .setNotDisplayedBySystem(false)
3083                         .addVisibleToConfig(visibleToConfig)
3084                         .build();
3085 
3086         UnsupportedOperationException exception =
3087                 assertThrows(
3088                         UnsupportedOperationException.class,
3089                         () ->
3090                                 mAppSearchImpl.setBlobNamespaceVisibility(
3091                                         "package", "db1", ImmutableList.of(config)));
3092         assertThat(exception)
3093                 .hasMessageThat()
3094                 .contains(
3095                         Features.BLOB_STORAGE
3096                                 + " is not available on this AppSearch implementation.");
3097     }
3098 
3099     @Test
3100     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
testRemoveBlobVisibility()3101     public void testRemoveBlobVisibility() throws Exception {
3102         mAppSearchImpl =
3103                 AppSearchImpl.create(
3104                         mAppSearchDir,
3105                         new AppSearchConfigImpl(
3106                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
3107                         /* initStatsBuilder= */ null,
3108                         /* visibilityChecker= */ null,
3109                         new JetpackRevocableFileDescriptorStore(mUnlimitedConfig),
3110                         ALWAYS_OPTIMIZE);
3111 
3112         SchemaVisibilityConfig visibleToConfig1 =
3113                 new SchemaVisibilityConfig.Builder()
3114                         .addAllowedPackage(new PackageIdentifier("pkgBar1", new byte[32]))
3115                         .addRequiredPermissions(ImmutableSet.of(1, 2))
3116                         .setPubliclyVisibleTargetPackage(
3117                                 new PackageIdentifier("pkgFoo1", new byte[32]))
3118                         .build();
3119         InternalVisibilityConfig config1 =
3120                 new InternalVisibilityConfig.Builder("namespace1")
3121                         .setNotDisplayedBySystem(false)
3122                         .addVisibleToConfig(visibleToConfig1)
3123                         .build();
3124         SchemaVisibilityConfig visibleToConfig2 =
3125                 new SchemaVisibilityConfig.Builder()
3126                         .addAllowedPackage(new PackageIdentifier("pkgBar2", new byte[32]))
3127                         .addRequiredPermissions(ImmutableSet.of(3, 4))
3128                         .setPubliclyVisibleTargetPackage(
3129                                 new PackageIdentifier("pkgFoo2", new byte[32]))
3130                         .build();
3131         InternalVisibilityConfig config2 =
3132                 new InternalVisibilityConfig.Builder("namespace2")
3133                         .setNotDisplayedBySystem(false)
3134                         .addVisibleToConfig(visibleToConfig2)
3135                         .build();
3136 
3137         String prefix = PrefixUtil.createPrefix("package", "db1");
3138         mAppSearchImpl.setBlobNamespaceVisibility(
3139                 "package", "db1", ImmutableList.of(config1, config2));
3140 
3141         // Expect the config will be added prefix.
3142         InternalVisibilityConfig expectedConfig1 =
3143                 new InternalVisibilityConfig.Builder(prefix + "namespace1")
3144                         .setNotDisplayedBySystem(false)
3145                         .addVisibleToConfig(visibleToConfig1)
3146                         .build();
3147         assertThat(mAppSearchImpl.mBlobVisibilityStoreLocked.getVisibility(prefix + "namespace1"))
3148                 .isEqualTo(expectedConfig1);
3149 
3150         InternalVisibilityConfig expectedConfig2 =
3151                 new InternalVisibilityConfig.Builder(prefix + "namespace2")
3152                         .setNotDisplayedBySystem(false)
3153                         .addVisibleToConfig(visibleToConfig2)
3154                         .build();
3155         assertThat(mAppSearchImpl.mBlobVisibilityStoreLocked.getVisibility(prefix + "namespace2"))
3156                 .isEqualTo(expectedConfig2);
3157 
3158         // Verify the InternalVisibilityConfig is saved to AppSearchImpl.
3159         GenericDocument visibilityDocument1 =
3160                 mAppSearchImpl.getDocument(
3161                         VISIBILITY_PACKAGE_NAME,
3162                         BLOB_VISIBILITY_DATABASE_NAME,
3163                         VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
3164                         /* id= */ prefix + "namespace1",
3165                         /* typePropertyPaths= */ Collections.emptyMap());
3166         GenericDocument overLayVisibilityDocument1 =
3167                 mAppSearchImpl.getDocument(
3168                         VISIBILITY_PACKAGE_NAME,
3169                         BLOB_ANDROID_V_OVERLAY_DATABASE_NAME,
3170                         VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
3171                         /* id= */ prefix + "namespace1",
3172                         /* typePropertyPaths= */ Collections.emptyMap());
3173         InternalVisibilityConfig outputConfig1 =
3174                 VisibilityToDocumentConverter.createInternalVisibilityConfig(
3175                         visibilityDocument1, overLayVisibilityDocument1);
3176         assertThat(outputConfig1).isEqualTo(expectedConfig1);
3177 
3178         GenericDocument visibilityDocument2 =
3179                 mAppSearchImpl.getDocument(
3180                         VISIBILITY_PACKAGE_NAME,
3181                         BLOB_VISIBILITY_DATABASE_NAME,
3182                         VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
3183                         /* id= */ prefix + "namespace2",
3184                         /* typePropertyPaths= */ Collections.emptyMap());
3185         GenericDocument overLayVisibilityDocument2 =
3186                 mAppSearchImpl.getDocument(
3187                         VISIBILITY_PACKAGE_NAME,
3188                         BLOB_ANDROID_V_OVERLAY_DATABASE_NAME,
3189                         VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
3190                         /* id= */ prefix + "namespace2",
3191                         /* typePropertyPaths= */ Collections.emptyMap());
3192         InternalVisibilityConfig outputConfig2 =
3193                 VisibilityToDocumentConverter.createInternalVisibilityConfig(
3194                         visibilityDocument2, overLayVisibilityDocument2);
3195         assertThat(outputConfig2).isEqualTo(expectedConfig2);
3196 
3197         // remove config1 by only set config2 to db
3198         mAppSearchImpl.setBlobNamespaceVisibility(
3199                 "package", "db1", /* visibilityConfigs= */ ImmutableList.of(config2));
3200 
3201         // Check config 1 is removed from VisibilityStore
3202         assertThat(mAppSearchImpl.mBlobVisibilityStoreLocked.getVisibility(prefix + "namespace1"))
3203                 .isNull();
3204         AppSearchException e =
3205                 assertThrows(
3206                         AppSearchException.class,
3207                         () ->
3208                                 mAppSearchImpl.getDocument(
3209                                         VISIBILITY_PACKAGE_NAME,
3210                                         BLOB_VISIBILITY_DATABASE_NAME,
3211                                         VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
3212                                         /* id= */ prefix + "namespace1",
3213                                         /* typePropertyPaths= */ Collections.emptyMap()));
3214         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
3215         assertThat(e.getMessage())
3216                 .isEqualTo("Document (VS#Pkg$VSBlob#Db/, package$db1/namespace1) not found.");
3217         e =
3218                 assertThrows(
3219                         AppSearchException.class,
3220                         () ->
3221                                 mAppSearchImpl.getDocument(
3222                                         VISIBILITY_PACKAGE_NAME,
3223                                         BLOB_ANDROID_V_OVERLAY_DATABASE_NAME,
3224                                         VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
3225                                         /* id= */ prefix + "namespace1",
3226                                         /* typePropertyPaths= */ Collections.emptyMap()));
3227         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
3228         assertThat(e.getMessage())
3229                 .isEqualTo(
3230                         "Document (VS#Pkg$VSBlob#AndroidVDb/androidVOverlay, "
3231                                 + "package$db1/namespace1) not found.");
3232 
3233         // Config2 remains.
3234         assertThat(mAppSearchImpl.mBlobVisibilityStoreLocked.getVisibility(prefix + "namespace2"))
3235                 .isEqualTo(expectedConfig2);
3236         visibilityDocument2 =
3237                 mAppSearchImpl.getDocument(
3238                         VISIBILITY_PACKAGE_NAME,
3239                         BLOB_VISIBILITY_DATABASE_NAME,
3240                         VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
3241                         /* id= */ prefix + "namespace2",
3242                         /* typePropertyPaths= */ Collections.emptyMap());
3243         overLayVisibilityDocument2 =
3244                 mAppSearchImpl.getDocument(
3245                         VISIBILITY_PACKAGE_NAME,
3246                         BLOB_ANDROID_V_OVERLAY_DATABASE_NAME,
3247                         VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
3248                         /* id= */ prefix + "namespace2",
3249                         /* typePropertyPaths= */ Collections.emptyMap());
3250         outputConfig2 =
3251                 VisibilityToDocumentConverter.createInternalVisibilityConfig(
3252                         visibilityDocument2, overLayVisibilityDocument2);
3253 
3254         assertThat(outputConfig2).isEqualTo(expectedConfig2);
3255     }
3256 
3257     @Test
3258     @RequiresFlagsDisabled(Flags.FLAG_ENABLE_BLOB_STORE)
testGlobalReadBlob_notSupported()3259     public void testGlobalReadBlob_notSupported() throws Exception {
3260         String visiblePrefix = PrefixUtil.createPrefix("package", "db1");
3261         VisibilityChecker mockVisibilityChecker =
3262                 createMockVisibilityChecker(ImmutableSet.of(visiblePrefix + "visibleNamespace"));
3263         mAppSearchImpl =
3264                 AppSearchImpl.create(
3265                         mAppSearchDir,
3266                         new AppSearchConfigImpl(
3267                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
3268                         /* initStatsBuilder= */ null,
3269                         mockVisibilityChecker,
3270                         /* revocableFileDescriptorStore= */ null,
3271                         ALWAYS_OPTIMIZE);
3272 
3273         byte[] data = generateRandomBytes(20 * 1024); // 20 KiB
3274         byte[] digest = calculateDigest(data);
3275         AppSearchBlobHandle handle =
3276                 AppSearchBlobHandle.createWithSha256(digest, "package", "db1", "ns");
3277         // nonVisibleHandle is not visible to the caller.
3278         UnsupportedOperationException exception =
3279                 assertThrows(
3280                         UnsupportedOperationException.class,
3281                         () -> mAppSearchImpl.globalOpenReadBlob(handle, mSelfCallerAccess));
3282         assertThat(exception)
3283                 .hasMessageThat()
3284                 .contains(
3285                         Features.BLOB_STORAGE
3286                                 + " is not available on this AppSearch implementation.");
3287     }
3288 
3289     @Test
3290     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
testGlobalReadBlob()3291     public void testGlobalReadBlob() throws Exception {
3292         String visiblePrefix = PrefixUtil.createPrefix("package", "db1");
3293         VisibilityChecker mockVisibilityChecker =
3294                 createMockVisibilityChecker(ImmutableSet.of(visiblePrefix + "visibleNamespace"));
3295         mAppSearchImpl =
3296                 AppSearchImpl.create(
3297                         mAppSearchDir,
3298                         new AppSearchConfigImpl(
3299                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
3300                         /* initStatsBuilder= */ null,
3301                         mockVisibilityChecker,
3302                         new JetpackRevocableFileDescriptorStore(mUnlimitedConfig),
3303                         ALWAYS_OPTIMIZE);
3304 
3305         // Set mock visibility setting.
3306         InternalVisibilityConfig config =
3307                 new InternalVisibilityConfig.Builder("visibleNamespace").build();
3308         mAppSearchImpl.setBlobNamespaceVisibility("package", "db1", ImmutableList.of(config));
3309 
3310         byte[] data = generateRandomBytes(20 * 1024); // 20 KiB
3311         byte[] digest = calculateDigest(data);
3312         AppSearchBlobHandle visibleHandle =
3313                 AppSearchBlobHandle.createWithSha256(digest, "package", "db1", "visibleNamespace");
3314         try (ParcelFileDescriptor writePfd =
3315                         mAppSearchImpl.openWriteBlob("package", "db1", visibleHandle);
3316                 OutputStream outputStream =
3317                         new ParcelFileDescriptor.AutoCloseOutputStream(writePfd)) {
3318             outputStream.write(data);
3319             outputStream.flush();
3320         }
3321         mAppSearchImpl.commitBlob("package", "db1", visibleHandle);
3322 
3323         AppSearchBlobHandle nonVisibleHandle =
3324                 AppSearchBlobHandle.createWithSha256(
3325                         digest, "package", "db1", "nonVisibleNamespace");
3326         try (ParcelFileDescriptor writePfd =
3327                         mAppSearchImpl.openWriteBlob("package", "db1", nonVisibleHandle);
3328                 OutputStream outputStream =
3329                         new ParcelFileDescriptor.AutoCloseOutputStream(writePfd)) {
3330             outputStream.write(data);
3331             outputStream.flush();
3332         }
3333         mAppSearchImpl.commitBlob("package", "db1", nonVisibleHandle);
3334 
3335         // visibleHandle is visible to the caller.
3336         byte[] readBytes = new byte[20 * 1024];
3337         try (ParcelFileDescriptor readPfd =
3338                         mAppSearchImpl.globalOpenReadBlob(visibleHandle, mSelfCallerAccess);
3339                 InputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(readPfd)) {
3340             inputStream.read(readBytes);
3341         }
3342         assertThat(readBytes).isEqualTo(data);
3343 
3344         // nonVisibleHandle is not visible to the caller.
3345         AppSearchException e =
3346                 assertThrows(
3347                         AppSearchException.class,
3348                         () ->
3349                                 mAppSearchImpl.globalOpenReadBlob(
3350                                         nonVisibleHandle, mSelfCallerAccess));
3351         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
3352         assertThat(e.getMessage()).contains("Cannot find the blob for handle");
3353     }
3354 
3355     @Test
3356     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
testGlobalReadBlob_sameErrorMessage()3357     public void testGlobalReadBlob_sameErrorMessage() throws Exception {
3358         String visiblePrefix = PrefixUtil.createPrefix("package", "db1");
3359         VisibilityChecker mockVisibilityChecker =
3360                 createMockVisibilityChecker(ImmutableSet.of(visiblePrefix + "visibleNamespace"));
3361         mAppSearchImpl =
3362                 AppSearchImpl.create(
3363                         mAppSearchDir,
3364                         new AppSearchConfigImpl(
3365                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
3366                         /* initStatsBuilder= */ null,
3367                         mockVisibilityChecker,
3368                         new JetpackRevocableFileDescriptorStore(mUnlimitedConfig),
3369                         ALWAYS_OPTIMIZE);
3370 
3371         // Set mock visibility setting.
3372         InternalVisibilityConfig config =
3373                 new InternalVisibilityConfig.Builder("visibleNamespace").build();
3374         mAppSearchImpl.setBlobNamespaceVisibility("package", "db1", ImmutableList.of(config));
3375 
3376         byte[] data = generateRandomBytes(20 * 1024); // 20 KiB
3377         byte[] digest = calculateDigest(data);
3378         AppSearchBlobHandle visibleHandle =
3379                 AppSearchBlobHandle.createWithSha256(digest, "package", "db1", "visibleNamespace");
3380         try (ParcelFileDescriptor writePfd =
3381                         mAppSearchImpl.openWriteBlob("package", "db1", visibleHandle);
3382                 OutputStream outputStream =
3383                         new ParcelFileDescriptor.AutoCloseOutputStream(writePfd)) {
3384             outputStream.write(data);
3385             outputStream.flush();
3386         }
3387         mAppSearchImpl.commitBlob("package", "db1", visibleHandle);
3388 
3389         AppSearchBlobHandle nonVisibleHandle =
3390                 AppSearchBlobHandle.createWithSha256(
3391                         digest, "package", "db1", "nonVisibleNamespace");
3392         try (ParcelFileDescriptor writePfd =
3393                         mAppSearchImpl.openWriteBlob("package", "db1", nonVisibleHandle);
3394                 OutputStream outputStream =
3395                         new ParcelFileDescriptor.AutoCloseOutputStream(writePfd)) {
3396             outputStream.write(data);
3397             outputStream.flush();
3398         }
3399         mAppSearchImpl.commitBlob("package", "db1", nonVisibleHandle);
3400 
3401         // visibleHandle is visible to the caller.
3402         byte[] readBytes = new byte[20 * 1024];
3403         try (ParcelFileDescriptor readPfd =
3404                         mAppSearchImpl.globalOpenReadBlob(visibleHandle, mSelfCallerAccess);
3405                 InputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(readPfd)) {
3406             inputStream.read(readBytes);
3407         }
3408         assertThat(readBytes).isEqualTo(data);
3409 
3410         // nonVisibleHandle is not visible to the caller.
3411         AppSearchException exception1 =
3412                 assertThrows(
3413                         AppSearchException.class,
3414                         () ->
3415                                 mAppSearchImpl.globalOpenReadBlob(
3416                                         nonVisibleHandle, mSelfCallerAccess));
3417         assertThat(exception1.getResultCode()).isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
3418         assertThat(exception1.getMessage()).contains("Cannot find the blob for handle:");
3419         assertThat(exception1.getCause()).isNull();
3420 
3421         // Remove visibleHandle and verify the error code and message should be same between not
3422         // found and inaccessible.
3423         mAppSearchImpl.removeBlob("package", "db1", visibleHandle);
3424         AppSearchException exception2 =
3425                 assertThrows(
3426                         AppSearchException.class,
3427                         () -> mAppSearchImpl.globalOpenReadBlob(visibleHandle, mSelfCallerAccess));
3428         assertThat(exception2.getCause()).isNull();
3429         assertThat(exception2.getResultCode()).isEqualTo(exception1.getResultCode());
3430         assertThat(exception2.getMessage()).isEqualTo(exception1.getMessage());
3431     }
3432 
3433     @Test
testClearPackageData()3434     public void testClearPackageData() throws Exception {
3435         List<SchemaTypeConfigProto> existingSchemas =
3436                 mAppSearchImpl.getSchemaProtoLocked().getTypesList();
3437         Map<String, Set<String>> existingDatabases = mAppSearchImpl.getPackageToDatabases();
3438 
3439         // Insert package schema
3440         List<AppSearchSchema> schema =
3441                 ImmutableList.of(new AppSearchSchema.Builder("schema").build());
3442         InternalSetSchemaResponse internalSetSchemaResponse =
3443                 mAppSearchImpl.setSchema(
3444                         "package",
3445                         "database",
3446                         schema,
3447                         /* visibilityConfigs= */ Collections.emptyList(),
3448                         /* forceOverride= */ false,
3449                         /* version= */ 0,
3450                         /* setSchemaStatsBuilder= */ null);
3451         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
3452 
3453         // Insert package document
3454         GenericDocument document =
3455                 new GenericDocument.Builder<>("namespace", "id", "schema").build();
3456         mAppSearchImpl.putDocument(
3457                 "package",
3458                 "database",
3459                 document,
3460                 /* sendChangeNotifications= */ false,
3461                 /* logger= */ null);
3462 
3463         // Verify the document is indexed.
3464         SearchSpec searchSpec =
3465                 new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE).build();
3466         SearchResultPage searchResultPage =
3467                 mAppSearchImpl.query(
3468                         "package",
3469                         "database",
3470                         /* queryExpression= */ "",
3471                         searchSpec,
3472                         /* logger= */ null);
3473         assertThat(searchResultPage.getResults()).hasSize(1);
3474         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document);
3475 
3476         // Remove the package
3477         mAppSearchImpl.clearPackageData("package");
3478 
3479         // Verify the document is cleared.
3480         searchResultPage =
3481                 mAppSearchImpl.query(
3482                         "package2",
3483                         "database2",
3484                         /* queryExpression= */ "",
3485                         searchSpec,
3486                         /* logger= */ null);
3487         assertThat(searchResultPage.getResults()).isEmpty();
3488 
3489         // Verify the schema is cleared.
3490         assertThat(mAppSearchImpl.getSchemaProtoLocked().getTypesList())
3491                 .containsExactlyElementsIn(existingSchemas);
3492         assertThat(mAppSearchImpl.getPackageToDatabases())
3493                 .containsExactlyEntriesIn(existingDatabases);
3494     }
3495 
3496     @Test
testPrunePackageData()3497     public void testPrunePackageData() throws AppSearchException {
3498         List<SchemaTypeConfigProto> existingSchemas =
3499                 mAppSearchImpl.getSchemaProtoLocked().getTypesList();
3500         Map<String, Set<String>> existingDatabases = mAppSearchImpl.getPackageToDatabases();
3501 
3502         Set<String> existingPackages = new ArraySet<>(existingSchemas.size());
3503         for (int i = 0; i < existingSchemas.size(); i++) {
3504             existingPackages.add(PrefixUtil.getPackageName(existingSchemas.get(i).getSchemaType()));
3505         }
3506 
3507         // Create VisibilityConfig
3508         InternalVisibilityConfig visibilityConfig =
3509                 new InternalVisibilityConfig.Builder("schema")
3510                         .setNotDisplayedBySystem(true)
3511                         .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
3512                         .build();
3513 
3514         // Insert schema for package A and B.
3515         List<AppSearchSchema> schema =
3516                 ImmutableList.of(new AppSearchSchema.Builder("schema").build());
3517         InternalSetSchemaResponse internalSetSchemaResponse =
3518                 mAppSearchImpl.setSchema(
3519                         "packageA",
3520                         "database",
3521                         schema,
3522                         /* visibilityConfigs= */ ImmutableList.of(visibilityConfig),
3523                         /* forceOverride= */ false,
3524                         /* version= */ 0,
3525                         /* setSchemaStatsBuilder= */ null);
3526         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
3527         internalSetSchemaResponse =
3528                 mAppSearchImpl.setSchema(
3529                         "packageB",
3530                         "database",
3531                         schema,
3532                         /* visibilityConfigs= */ ImmutableList.of(visibilityConfig),
3533                         /* forceOverride= */ false,
3534                         /* version= */ 0,
3535                         /* setSchemaStatsBuilder= */ null);
3536         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
3537 
3538         // Verify these two packages are stored in AppSearch.
3539         SchemaProto expectedProto =
3540                 SchemaProto.newBuilder()
3541                         .addTypes(
3542                                 SchemaTypeConfigProto.newBuilder()
3543                                         .setSchemaType("packageA$database/schema")
3544                                         .setDescription("")
3545                                         .setVersion(0))
3546                         .addTypes(
3547                                 SchemaTypeConfigProto.newBuilder()
3548                                         .setSchemaType("packageB$database/schema")
3549                                         .setDescription("")
3550                                         .setVersion(0))
3551                         .build();
3552         List<SchemaTypeConfigProto> expectedTypes = new ArrayList<>();
3553         expectedTypes.addAll(existingSchemas);
3554         expectedTypes.addAll(expectedProto.getTypesList());
3555         assertThat(mAppSearchImpl.getSchemaProtoLocked().getTypesList())
3556                 .containsExactlyElementsIn(expectedTypes);
3557 
3558         // Verify these two visibility documents are stored in AppSearch.
3559         InternalVisibilityConfig expectedVisibilityConfigA =
3560                 new InternalVisibilityConfig.Builder("packageA$database/schema")
3561                         .setNotDisplayedBySystem(true)
3562                         .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
3563                         .build();
3564         InternalVisibilityConfig expectedVisibilityConfigB =
3565                 new InternalVisibilityConfig.Builder("packageB$database/schema")
3566                         .setNotDisplayedBySystem(true)
3567                         .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
3568                         .build();
3569         assertThat(
3570                         mAppSearchImpl.mDocumentVisibilityStoreLocked.getVisibility(
3571                                 "packageA$database/schema"))
3572                 .isEqualTo(expectedVisibilityConfigA);
3573         assertThat(
3574                         mAppSearchImpl.mDocumentVisibilityStoreLocked.getVisibility(
3575                                 "packageB$database/schema"))
3576                 .isEqualTo(expectedVisibilityConfigB);
3577 
3578         // Prune packages
3579         mAppSearchImpl.prunePackageData(existingPackages);
3580 
3581         // Verify the schema is same as beginning.
3582         assertThat(mAppSearchImpl.getSchemaProtoLocked().getTypesList())
3583                 .containsExactlyElementsIn(existingSchemas);
3584         assertThat(mAppSearchImpl.getPackageToDatabases())
3585                 .containsExactlyEntriesIn(existingDatabases);
3586 
3587         // Verify the VisibilitySetting is removed.
3588         assertThat(
3589                         mAppSearchImpl.mDocumentVisibilityStoreLocked.getVisibility(
3590                                 "packageA$database/schema"))
3591                 .isNull();
3592         assertThat(
3593                         mAppSearchImpl.mDocumentVisibilityStoreLocked.getVisibility(
3594                                 "packageB$database/schema"))
3595                 .isNull();
3596     }
3597 
3598     @Test
testGetPackageToDatabases()3599     public void testGetPackageToDatabases() throws Exception {
3600         Map<String, Set<String>> existingMapping = mAppSearchImpl.getPackageToDatabases();
3601         Map<String, Set<String>> expectedMapping = new ArrayMap<>();
3602         expectedMapping.putAll(existingMapping);
3603 
3604         // Has database1
3605         expectedMapping.put("package1", ImmutableSet.of("database1"));
3606         InternalSetSchemaResponse internalSetSchemaResponse =
3607                 mAppSearchImpl.setSchema(
3608                         "package1",
3609                         "database1",
3610                         Collections.singletonList(new AppSearchSchema.Builder("schema").build()),
3611                         /* visibilityConfigs= */ Collections.emptyList(),
3612                         /* forceOverride= */ false,
3613                         /* version= */ 0,
3614                         /* setSchemaStatsBuilder= */ null);
3615         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
3616         assertThat(mAppSearchImpl.getPackageToDatabases())
3617                 .containsExactlyEntriesIn(expectedMapping);
3618 
3619         // Has both databases
3620         expectedMapping.put("package1", ImmutableSet.of("database1", "database2"));
3621         internalSetSchemaResponse =
3622                 mAppSearchImpl.setSchema(
3623                         "package1",
3624                         "database2",
3625                         Collections.singletonList(new AppSearchSchema.Builder("schema").build()),
3626                         /* visibilityConfigs= */ Collections.emptyList(),
3627                         /* forceOverride= */ false,
3628                         /* version= */ 0,
3629                         /* setSchemaStatsBuilder= */ null);
3630         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
3631         assertThat(mAppSearchImpl.getPackageToDatabases())
3632                 .containsExactlyEntriesIn(expectedMapping);
3633 
3634         // Has both packages
3635         expectedMapping.put("package2", ImmutableSet.of("database1"));
3636         internalSetSchemaResponse =
3637                 mAppSearchImpl.setSchema(
3638                         "package2",
3639                         "database1",
3640                         Collections.singletonList(new AppSearchSchema.Builder("schema").build()),
3641                         /* visibilityConfigs= */ Collections.emptyList(),
3642                         /* forceOverride= */ false,
3643                         /* version= */ 0,
3644                         /* setSchemaStatsBuilder= */ null);
3645         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
3646         assertThat(mAppSearchImpl.getPackageToDatabases())
3647                 .containsExactlyEntriesIn(expectedMapping);
3648     }
3649 
3650     @Test
3651     @RequiresFlagsDisabled(Flags.FLAG_ENABLE_BLOB_STORE)
testGetAllPrefixedSchemaTypes()3652     public void testGetAllPrefixedSchemaTypes() throws Exception {
3653         // Insert schema
3654         List<AppSearchSchema> schemas1 =
3655                 Collections.singletonList(new AppSearchSchema.Builder("type1").build());
3656         List<AppSearchSchema> schemas2 =
3657                 Collections.singletonList(new AppSearchSchema.Builder("type2").build());
3658         List<AppSearchSchema> schemas3 =
3659                 Collections.singletonList(new AppSearchSchema.Builder("type3").build());
3660         InternalSetSchemaResponse internalSetSchemaResponse =
3661                 mAppSearchImpl.setSchema(
3662                         "package1",
3663                         "database1",
3664                         schemas1,
3665                         /* visibilityConfigs= */ Collections.emptyList(),
3666                         /* forceOverride= */ false,
3667                         /* version= */ 0,
3668                         /* setSchemaStatsBuilder= */ null);
3669         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
3670         internalSetSchemaResponse =
3671                 mAppSearchImpl.setSchema(
3672                         "package1",
3673                         "database2",
3674                         schemas2,
3675                         /* visibilityConfigs= */ Collections.emptyList(),
3676                         /* forceOverride= */ false,
3677                         /* version= */ 0,
3678                         /* setSchemaStatsBuilder= */ null);
3679         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
3680         internalSetSchemaResponse =
3681                 mAppSearchImpl.setSchema(
3682                         "package2",
3683                         "database1",
3684                         schemas3,
3685                         /* visibilityConfigs= */ Collections.emptyList(),
3686                         /* forceOverride= */ false,
3687                         /* version= */ 0,
3688                         /* setSchemaStatsBuilder= */ null);
3689         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
3690         assertThat(mAppSearchImpl.getAllPrefixedSchemaTypes())
3691                 .containsExactly(
3692                         "package1$database1/type1",
3693                         "package1$database2/type2",
3694                         "package2$database1/type3",
3695                         "VS#Pkg$VS#Db/VisibilityType", // plus the stored Visibility schema
3696                         "VS#Pkg$VS#Db/VisibilityPermissionType",
3697                         "VS#Pkg$VS#AndroidVDb/AndroidVOverlayType");
3698     }
3699 
3700     @Test
3701     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
testGetAllPrefixedSchemaTypes_enableBlobStore()3702     public void testGetAllPrefixedSchemaTypes_enableBlobStore() throws Exception {
3703         mAppSearchImpl =
3704                 AppSearchImpl.create(
3705                         mAppSearchDir,
3706                         new AppSearchConfigImpl(
3707                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
3708                         /* initStatsBuilder= */ null,
3709                         /* visibilityChecker= */ null,
3710                         new JetpackRevocableFileDescriptorStore(mUnlimitedConfig),
3711                         ALWAYS_OPTIMIZE);
3712         // Insert schema
3713         List<AppSearchSchema> schemas1 =
3714                 Collections.singletonList(new AppSearchSchema.Builder("type1").build());
3715         List<AppSearchSchema> schemas2 =
3716                 Collections.singletonList(new AppSearchSchema.Builder("type2").build());
3717         List<AppSearchSchema> schemas3 =
3718                 Collections.singletonList(new AppSearchSchema.Builder("type3").build());
3719         InternalSetSchemaResponse internalSetSchemaResponse =
3720                 mAppSearchImpl.setSchema(
3721                         "package1",
3722                         "database1",
3723                         schemas1,
3724                         /* visibilityConfigs= */ Collections.emptyList(),
3725                         /* forceOverride= */ false,
3726                         /* version= */ 0,
3727                         /* setSchemaStatsBuilder= */ null);
3728         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
3729         internalSetSchemaResponse =
3730                 mAppSearchImpl.setSchema(
3731                         "package1",
3732                         "database2",
3733                         schemas2,
3734                         /* visibilityConfigs= */ Collections.emptyList(),
3735                         /* forceOverride= */ false,
3736                         /* version= */ 0,
3737                         /* setSchemaStatsBuilder= */ null);
3738         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
3739         internalSetSchemaResponse =
3740                 mAppSearchImpl.setSchema(
3741                         "package2",
3742                         "database1",
3743                         schemas3,
3744                         /* visibilityConfigs= */ Collections.emptyList(),
3745                         /* forceOverride= */ false,
3746                         /* version= */ 0,
3747                         /* setSchemaStatsBuilder= */ null);
3748         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
3749         assertThat(mAppSearchImpl.getAllPrefixedSchemaTypes())
3750                 .containsExactly(
3751                         "package1$database1/type1",
3752                         "package1$database2/type2",
3753                         "package2$database1/type3",
3754                         "VS#Pkg$VS#Db/VisibilityType", // plus the stored Visibility schema
3755                         "VS#Pkg$VS#Db/VisibilityPermissionType",
3756                         "VS#Pkg$VS#AndroidVDb/AndroidVOverlayType",
3757                         "VS#Pkg$VSBlob#Db/VisibilityType",
3758                         "VS#Pkg$VSBlob#Db/VisibilityPermissionType",
3759                         "VS#Pkg$VSBlob#AndroidVDb/AndroidVOverlayType");
3760     }
3761 
3762     @FlakyTest(bugId = 204186664)
3763     @Test
testReportUsage()3764     public void testReportUsage() throws Exception {
3765         // Insert schema
3766         List<AppSearchSchema> schemas =
3767                 Collections.singletonList(new AppSearchSchema.Builder("type").build());
3768         InternalSetSchemaResponse internalSetSchemaResponse =
3769                 mAppSearchImpl.setSchema(
3770                         "package",
3771                         "database",
3772                         schemas,
3773                         /* visibilityConfigs= */ Collections.emptyList(),
3774                         /* forceOverride= */ false,
3775                         /* version= */ 0,
3776                         /* setSchemaStatsBuilder= */ null);
3777         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
3778 
3779         // Insert two docs
3780         GenericDocument document1 =
3781                 new GenericDocument.Builder<>("namespace", "id1", "type").build();
3782         GenericDocument document2 =
3783                 new GenericDocument.Builder<>("namespace", "id2", "type").build();
3784         mAppSearchImpl.putDocument(
3785                 "package",
3786                 "database",
3787                 document1,
3788                 /* sendChangeNotifications= */ false,
3789                 /* logger= */ null);
3790         mAppSearchImpl.putDocument(
3791                 "package",
3792                 "database",
3793                 document2,
3794                 /* sendChangeNotifications= */ false,
3795                 /* logger= */ null);
3796 
3797         // Report some usages. id1 has 2 app and 1 system usage, id2 has 1 app and 2 system usage.
3798         mAppSearchImpl.reportUsage(
3799                 "package",
3800                 "database",
3801                 "namespace",
3802                 "id1",
3803                 /* usageTimestampMillis= */ 10,
3804                 /* systemUsage= */ false);
3805         mAppSearchImpl.reportUsage(
3806                 "package",
3807                 "database",
3808                 "namespace",
3809                 "id1",
3810                 /* usageTimestampMillis= */ 20,
3811                 /* systemUsage= */ false);
3812         mAppSearchImpl.reportUsage(
3813                 "package",
3814                 "database",
3815                 "namespace",
3816                 "id1",
3817                 /* usageTimestampMillis= */ 1000,
3818                 /* systemUsage= */ true);
3819 
3820         mAppSearchImpl.reportUsage(
3821                 "package",
3822                 "database",
3823                 "namespace",
3824                 "id2",
3825                 /* usageTimestampMillis= */ 100,
3826                 /* systemUsage= */ false);
3827         mAppSearchImpl.reportUsage(
3828                 "package",
3829                 "database",
3830                 "namespace",
3831                 "id2",
3832                 /* usageTimestampMillis= */ 200,
3833                 /* systemUsage= */ true);
3834         mAppSearchImpl.reportUsage(
3835                 "package",
3836                 "database",
3837                 "namespace",
3838                 "id2",
3839                 /* usageTimestampMillis= */ 150,
3840                 /* systemUsage= */ true);
3841 
3842         // Sort by app usage count: id1 should win
3843         List<SearchResult> page =
3844                 mAppSearchImpl
3845                         .query(
3846                                 "package",
3847                                 "database",
3848                                 "",
3849                                 new SearchSpec.Builder()
3850                                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
3851                                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_USAGE_COUNT)
3852                                         .build(),
3853                                 /* logger= */ null)
3854                         .getResults();
3855         assertThat(page).hasSize(2);
3856         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id1");
3857         assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id2");
3858 
3859         // Sort by app usage timestamp: id2 should win
3860         page =
3861                 mAppSearchImpl
3862                         .query(
3863                                 "package",
3864                                 "database",
3865                                 "",
3866                                 new SearchSpec.Builder()
3867                                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
3868                                         .setRankingStrategy(
3869                                                 SearchSpec
3870                                                         .RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP)
3871                                         .build(),
3872                                 /* logger= */ null)
3873                         .getResults();
3874         assertThat(page).hasSize(2);
3875         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id2");
3876         assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id1");
3877 
3878         // Sort by system usage count: id2 should win
3879         page =
3880                 mAppSearchImpl
3881                         .query(
3882                                 "package",
3883                                 "database",
3884                                 "",
3885                                 new SearchSpec.Builder()
3886                                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
3887                                         .setRankingStrategy(
3888                                                 SearchSpec.RANKING_STRATEGY_SYSTEM_USAGE_COUNT)
3889                                         .build(),
3890                                 /* logger= */ null)
3891                         .getResults();
3892         assertThat(page).hasSize(2);
3893         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id2");
3894         assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id1");
3895 
3896         // Sort by system usage timestamp: id1 should win
3897         page =
3898                 mAppSearchImpl
3899                         .query(
3900                                 "package",
3901                                 "database",
3902                                 "",
3903                                 new SearchSpec.Builder()
3904                                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
3905                                         .setRankingStrategy(
3906                                                 SearchSpec
3907                                                         .RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP)
3908                                         .build(),
3909                                 /* logger= */ null)
3910                         .getResults();
3911         assertThat(page).hasSize(2);
3912         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id1");
3913         assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id2");
3914     }
3915 
3916     @Test
testGetStorageInfoForPackage_nonexistentPackage()3917     public void testGetStorageInfoForPackage_nonexistentPackage() throws Exception {
3918         // "package2" doesn't exist yet, so it shouldn't have any storage size
3919         StorageInfo storageInfo = mAppSearchImpl.getStorageInfoForPackage("nonexistent.package");
3920         assertThat(storageInfo.getSizeBytes()).isEqualTo(0);
3921         assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(0);
3922         assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(0);
3923     }
3924 
3925     @Test
testGetStorageInfoForPackage_withoutDocument()3926     public void testGetStorageInfoForPackage_withoutDocument() throws Exception {
3927         // Insert schema for "package1"
3928         List<AppSearchSchema> schemas =
3929                 Collections.singletonList(new AppSearchSchema.Builder("type").build());
3930         InternalSetSchemaResponse internalSetSchemaResponse =
3931                 mAppSearchImpl.setSchema(
3932                         "package1",
3933                         "database",
3934                         schemas,
3935                         /* visibilityConfigs= */ Collections.emptyList(),
3936                         /* forceOverride= */ false,
3937                         /* version= */ 0,
3938                         /* setSchemaStatsBuilder= */ null);
3939         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
3940 
3941         // Since "package1" doesn't have a document, it get any space attributed to it.
3942         StorageInfo storageInfo = mAppSearchImpl.getStorageInfoForPackage("package1");
3943         assertThat(storageInfo.getSizeBytes()).isEqualTo(0);
3944         assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(0);
3945         assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(0);
3946     }
3947 
3948     @Test
testGetStorageInfoForPackage_proportionalToDocuments()3949     public void testGetStorageInfoForPackage_proportionalToDocuments() throws Exception {
3950         List<AppSearchSchema> schemas =
3951                 Collections.singletonList(new AppSearchSchema.Builder("type").build());
3952 
3953         // Insert schema for "package1"
3954         InternalSetSchemaResponse internalSetSchemaResponse =
3955                 mAppSearchImpl.setSchema(
3956                         "package1",
3957                         "database",
3958                         schemas,
3959                         /* visibilityConfigs= */ Collections.emptyList(),
3960                         /* forceOverride= */ false,
3961                         /* version= */ 0,
3962                         /* setSchemaStatsBuilder= */ null);
3963         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
3964 
3965         // Insert document for "package1"
3966         GenericDocument document =
3967                 new GenericDocument.Builder<>("namespace", "id1", "type").build();
3968         mAppSearchImpl.putDocument(
3969                 "package1",
3970                 "database",
3971                 document,
3972                 /* sendChangeNotifications= */ false,
3973                 /* logger= */ null);
3974 
3975         // Insert schema for "package2"
3976         internalSetSchemaResponse =
3977                 mAppSearchImpl.setSchema(
3978                         "package2",
3979                         "database",
3980                         schemas,
3981                         /* visibilityConfigs= */ Collections.emptyList(),
3982                         /* forceOverride= */ false,
3983                         /* version= */ 0,
3984                         /* setSchemaStatsBuilder= */ null);
3985         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
3986 
3987         // Insert two documents for "package2"
3988         document = new GenericDocument.Builder<>("namespace", "id1", "type").build();
3989         mAppSearchImpl.putDocument(
3990                 "package2",
3991                 "database",
3992                 document,
3993                 /* sendChangeNotifications= */ false,
3994                 /* logger= */ null);
3995         document = new GenericDocument.Builder<>("namespace", "id2", "type").build();
3996         mAppSearchImpl.putDocument(
3997                 "package2",
3998                 "database",
3999                 document,
4000                 /* sendChangeNotifications= */ false,
4001                 /* logger= */ null);
4002 
4003         StorageInfo storageInfo = mAppSearchImpl.getStorageInfoForPackage("package1");
4004         long size1 = storageInfo.getSizeBytes();
4005         assertThat(size1).isGreaterThan(0);
4006         assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(1);
4007         assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(1);
4008 
4009         storageInfo = mAppSearchImpl.getStorageInfoForPackage("package2");
4010         long size2 = storageInfo.getSizeBytes();
4011         assertThat(size2).isGreaterThan(0);
4012         assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(2);
4013         assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(1);
4014 
4015         // Size is proportional to number of documents. Since "package2" has twice as many
4016         // documents as "package1", its size is twice as much too.
4017         assertThat(size2).isAtLeast(2 * size1);
4018     }
4019 
4020     @Test
testGetStorageInfoForDatabase_nonexistentPackage()4021     public void testGetStorageInfoForDatabase_nonexistentPackage() throws Exception {
4022         // "package2" doesn't exist yet, so it shouldn't have any storage size
4023         StorageInfo storageInfo =
4024                 mAppSearchImpl.getStorageInfoForDatabase(
4025                         "nonexistent.package", "nonexistentDatabase");
4026         assertThat(storageInfo.getSizeBytes()).isEqualTo(0);
4027         assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(0);
4028         assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(0);
4029     }
4030 
4031     @Test
testGetStorageInfoForDatabase_nonexistentDatabase()4032     public void testGetStorageInfoForDatabase_nonexistentDatabase() throws Exception {
4033         // Insert schema for "package1"
4034         List<AppSearchSchema> schemas =
4035                 Collections.singletonList(new AppSearchSchema.Builder("type").build());
4036         InternalSetSchemaResponse internalSetSchemaResponse =
4037                 mAppSearchImpl.setSchema(
4038                         "package1",
4039                         "database",
4040                         schemas,
4041                         /* visibilityConfigs= */ Collections.emptyList(),
4042                         /* forceOverride= */ false,
4043                         /* version= */ 0,
4044                         /* setSchemaStatsBuilder= */ null);
4045         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
4046 
4047         // "package2" doesn't exist yet, so it shouldn't have any storage size
4048         StorageInfo storageInfo =
4049                 mAppSearchImpl.getStorageInfoForDatabase("package1", "nonexistentDatabase");
4050         assertThat(storageInfo.getSizeBytes()).isEqualTo(0);
4051         assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(0);
4052         assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(0);
4053     }
4054 
4055     @Test
testGetStorageInfoForDatabase_withoutDocument()4056     public void testGetStorageInfoForDatabase_withoutDocument() throws Exception {
4057         // Insert schema for "package1"
4058         List<AppSearchSchema> schemas =
4059                 Collections.singletonList(new AppSearchSchema.Builder("type").build());
4060         InternalSetSchemaResponse internalSetSchemaResponse =
4061                 mAppSearchImpl.setSchema(
4062                         "package1",
4063                         "database1",
4064                         schemas,
4065                         /* visibilityConfigs= */ Collections.emptyList(),
4066                         /* forceOverride= */ false,
4067                         /* version= */ 0,
4068                         /* setSchemaStatsBuilder= */ null);
4069         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
4070 
4071         // Since "package1", "database1" doesn't have a document, it get any space attributed to it.
4072         StorageInfo storageInfo = mAppSearchImpl.getStorageInfoForDatabase("package1", "database1");
4073         assertThat(storageInfo.getSizeBytes()).isEqualTo(0);
4074         assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(0);
4075         assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(0);
4076     }
4077 
4078     @Test
testGetStorageInfoForDatabase_proportionalToDocuments()4079     public void testGetStorageInfoForDatabase_proportionalToDocuments() throws Exception {
4080         // Insert schema for "package1", "database1" and "database2"
4081         List<AppSearchSchema> schemas =
4082                 Collections.singletonList(new AppSearchSchema.Builder("type").build());
4083         InternalSetSchemaResponse internalSetSchemaResponse =
4084                 mAppSearchImpl.setSchema(
4085                         "package1",
4086                         "database1",
4087                         schemas,
4088                         /* visibilityConfigs= */ Collections.emptyList(),
4089                         /* forceOverride= */ false,
4090                         /* version= */ 0,
4091                         /* setSchemaStatsBuilder= */ null);
4092         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
4093         internalSetSchemaResponse =
4094                 mAppSearchImpl.setSchema(
4095                         "package1",
4096                         "database2",
4097                         schemas,
4098                         /* visibilityConfigs= */ Collections.emptyList(),
4099                         /* forceOverride= */ false,
4100                         /* version= */ 0,
4101                         /* setSchemaStatsBuilder= */ null);
4102         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
4103 
4104         // Add a document for "package1", "database1"
4105         GenericDocument document =
4106                 new GenericDocument.Builder<>("namespace1", "id1", "type").build();
4107         mAppSearchImpl.putDocument(
4108                 "package1",
4109                 "database1",
4110                 document,
4111                 /* sendChangeNotifications= */ false,
4112                 /* logger= */ null);
4113 
4114         // Add two documents for "package1", "database2"
4115         document = new GenericDocument.Builder<>("namespace1", "id1", "type").build();
4116         mAppSearchImpl.putDocument(
4117                 "package1",
4118                 "database2",
4119                 document,
4120                 /* sendChangeNotifications= */ false,
4121                 /* logger= */ null);
4122         document = new GenericDocument.Builder<>("namespace1", "id2", "type").build();
4123         mAppSearchImpl.putDocument(
4124                 "package1",
4125                 "database2",
4126                 document,
4127                 /* sendChangeNotifications= */ false,
4128                 /* logger= */ null);
4129 
4130         StorageInfo storageInfo = mAppSearchImpl.getStorageInfoForDatabase("package1", "database1");
4131         long size1 = storageInfo.getSizeBytes();
4132         assertThat(size1).isGreaterThan(0);
4133         assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(1);
4134         assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(1);
4135 
4136         storageInfo = mAppSearchImpl.getStorageInfoForDatabase("package1", "database2");
4137         long size2 = storageInfo.getSizeBytes();
4138         assertThat(size2).isGreaterThan(0);
4139         assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(2);
4140         assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(1);
4141 
4142         // Size is proportional to number of documents. Since "database2" has twice as many
4143         // documents as "database1", its size is twice as much too.
4144         assertThat(size2).isAtLeast(2 * size1);
4145     }
4146 
4147     @Test
4148     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
testGetStorageInfoForPackage_withBlob()4149     public void testGetStorageInfoForPackage_withBlob() throws Exception {
4150         mAppSearchImpl =
4151                 AppSearchImpl.create(
4152                         mAppSearchDir,
4153                         new AppSearchConfigImpl(
4154                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
4155                         /* initStatsBuilder= */ null,
4156                         /* visibilityChecker= */ null,
4157                         new JetpackRevocableFileDescriptorStore(mUnlimitedConfig),
4158                         ALWAYS_OPTIMIZE);
4159 
4160         byte[] data1 = generateRandomBytes(5 * 1024); // 5 KiB
4161         byte[] digest1 = calculateDigest(data1);
4162         AppSearchBlobHandle handle1 =
4163                 AppSearchBlobHandle.createWithSha256(digest1, "package1", "db1", "ns");
4164         ParcelFileDescriptor writePfd1 = mAppSearchImpl.openWriteBlob("package1", "db1", handle1);
4165         try (OutputStream outputStream =
4166                 new ParcelFileDescriptor.AutoCloseOutputStream(writePfd1)) {
4167             outputStream.write(data1);
4168             outputStream.flush();
4169         }
4170 
4171         byte[] data2 = generateRandomBytes(10 * 1024); // 10 KiB
4172         byte[] digest2 = calculateDigest(data2);
4173         AppSearchBlobHandle handle2 =
4174                 AppSearchBlobHandle.createWithSha256(digest2, "package1", "db1", "ns");
4175         ParcelFileDescriptor writePfd2 = mAppSearchImpl.openWriteBlob("package1", "db1", handle2);
4176         try (OutputStream outputStream =
4177                 new ParcelFileDescriptor.AutoCloseOutputStream(writePfd2)) {
4178             outputStream.write(data2);
4179             outputStream.flush();
4180         }
4181 
4182         byte[] data3 = generateRandomBytes(20 * 1024); // 20 KiB
4183         byte[] digest3 = calculateDigest(data3);
4184         AppSearchBlobHandle handle3 =
4185                 AppSearchBlobHandle.createWithSha256(digest3, "package2", "db1", "ns");
4186         ParcelFileDescriptor writePfd3 = mAppSearchImpl.openWriteBlob("package2", "db1", handle3);
4187         try (OutputStream outputStream =
4188                 new ParcelFileDescriptor.AutoCloseOutputStream(writePfd3)) {
4189             outputStream.write(data3);
4190             outputStream.flush();
4191         }
4192 
4193         StorageInfo storageInfo1 = mAppSearchImpl.getStorageInfoForPackage("package1");
4194         assertThat(storageInfo1.getBlobSizeBytes()).isEqualTo(15 * 1024);
4195         assertThat(storageInfo1.getBlobCount()).isEqualTo(2);
4196         StorageInfo storageInfo2 = mAppSearchImpl.getStorageInfoForPackage("package2");
4197         assertThat(storageInfo2.getBlobSizeBytes()).isEqualTo(20 * 1024);
4198         assertThat(storageInfo2.getBlobCount()).isEqualTo(1);
4199     }
4200 
4201     @Test
4202     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
testGetStorageInfoForDatabase_withBlob()4203     public void testGetStorageInfoForDatabase_withBlob() throws Exception {
4204         mAppSearchImpl =
4205                 AppSearchImpl.create(
4206                         mAppSearchDir,
4207                         new AppSearchConfigImpl(
4208                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
4209                         /* initStatsBuilder= */ null,
4210                         /* visibilityChecker= */ null,
4211                         new JetpackRevocableFileDescriptorStore(mUnlimitedConfig),
4212                         ALWAYS_OPTIMIZE);
4213         byte[] data1 = generateRandomBytes(5 * 1024); // 5 KiB
4214         byte[] digest1 = calculateDigest(data1);
4215         AppSearchBlobHandle handle1 =
4216                 AppSearchBlobHandle.createWithSha256(digest1, "package", "db1", "ns");
4217         ParcelFileDescriptor writePfd1 = mAppSearchImpl.openWriteBlob("package", "db1", handle1);
4218         try (OutputStream outputStream =
4219                 new ParcelFileDescriptor.AutoCloseOutputStream(writePfd1)) {
4220             outputStream.write(data1);
4221             outputStream.flush();
4222         }
4223 
4224         byte[] data2 = generateRandomBytes(10 * 1024); // 10 KiB
4225         byte[] digest2 = calculateDigest(data2);
4226         AppSearchBlobHandle handle2 =
4227                 AppSearchBlobHandle.createWithSha256(digest2, "package", "db1", "ns");
4228         ParcelFileDescriptor writePfd2 = mAppSearchImpl.openWriteBlob("package", "db1", handle2);
4229         try (OutputStream outputStream =
4230                 new ParcelFileDescriptor.AutoCloseOutputStream(writePfd2)) {
4231             outputStream.write(data2);
4232             outputStream.flush();
4233         }
4234 
4235         byte[] data3 = generateRandomBytes(20 * 1024); // 20 KiB
4236         byte[] digest3 = calculateDigest(data3);
4237         AppSearchBlobHandle handle3 =
4238                 AppSearchBlobHandle.createWithSha256(digest3, "package", "db2", "ns");
4239         ParcelFileDescriptor writePfd3 = mAppSearchImpl.openWriteBlob("package", "db2", handle3);
4240         try (OutputStream outputStream =
4241                 new ParcelFileDescriptor.AutoCloseOutputStream(writePfd3)) {
4242             outputStream.write(data3);
4243             outputStream.flush();
4244         }
4245 
4246         StorageInfo storageInfo1 = mAppSearchImpl.getStorageInfoForDatabase("package", "db1");
4247         assertThat(storageInfo1.getBlobSizeBytes()).isEqualTo(15 * 1024);
4248         assertThat(storageInfo1.getBlobCount()).isEqualTo(2);
4249         StorageInfo storageInfo2 = mAppSearchImpl.getStorageInfoForDatabase("package", "db2");
4250         assertThat(storageInfo2.getBlobSizeBytes()).isEqualTo(20 * 1024);
4251         assertThat(storageInfo2.getBlobCount()).isEqualTo(1);
4252     }
4253 
4254     @Test
testThrowsExceptionIfClosed()4255     public void testThrowsExceptionIfClosed() throws Exception {
4256         // Initial check that we could do something at first.
4257         List<AppSearchSchema> schemas =
4258                 Collections.singletonList(new AppSearchSchema.Builder("type").build());
4259         InternalSetSchemaResponse internalSetSchemaResponse =
4260                 mAppSearchImpl.setSchema(
4261                         "package",
4262                         "database",
4263                         schemas,
4264                         /* visibilityConfigs= */ Collections.emptyList(),
4265                         /* forceOverride= */ false,
4266                         /* version= */ 0,
4267                         /* setSchemaStatsBuilder= */ null);
4268         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
4269 
4270         mAppSearchImpl.close();
4271 
4272         // Check all our public APIs
4273         assertThrows(
4274                 IllegalStateException.class,
4275                 () ->
4276                         mAppSearchImpl.setSchema(
4277                                 "package",
4278                                 "database",
4279                                 schemas,
4280                                 /* visibilityConfigs= */ Collections.emptyList(),
4281                                 /* forceOverride= */ false,
4282                                 /* version= */ 0,
4283                                 /* setSchemaStatsBuilder= */ null));
4284 
4285         assertThrows(
4286                 IllegalStateException.class,
4287                 () ->
4288                         mAppSearchImpl.getSchema(
4289                                 /* packageName= */ "package",
4290                                 /* databaseName= */ "database",
4291                                 /* callerAccess= */ mSelfCallerAccess));
4292 
4293         assertThrows(
4294                 IllegalStateException.class,
4295                 () ->
4296                         mAppSearchImpl.putDocument(
4297                                 "package",
4298                                 "database",
4299                                 new GenericDocument.Builder<>("namespace", "id", "type").build(),
4300                                 /* sendChangeNotifications= */ false,
4301                                 /* logger= */ null));
4302 
4303         assertThrows(
4304                 IllegalStateException.class,
4305                 () ->
4306                         mAppSearchImpl.getDocument(
4307                                 "package", "database", "namespace", "id", Collections.emptyMap()));
4308 
4309         assertThrows(
4310                 IllegalStateException.class,
4311                 () ->
4312                         mAppSearchImpl.query(
4313                                 "package",
4314                                 "database",
4315                                 "query",
4316                                 new SearchSpec.Builder().build(),
4317                                 /* logger= */ null));
4318 
4319         assertThrows(
4320                 IllegalStateException.class,
4321                 () ->
4322                         mAppSearchImpl.globalQuery(
4323                                 "query",
4324                                 new SearchSpec.Builder().build(),
4325                                 mSelfCallerAccess,
4326                                 /* logger= */ null));
4327 
4328         assertThrows(
4329                 IllegalStateException.class,
4330                 () ->
4331                         mAppSearchImpl.getNextPage(
4332                                 "package", /* nextPageToken= */ 1L, /* statsBuilder= */ null));
4333 
4334         assertThrows(
4335                 IllegalStateException.class,
4336                 () -> mAppSearchImpl.invalidateNextPageToken("package", /* nextPageToken= */ 1L));
4337 
4338         assertThrows(
4339                 IllegalStateException.class,
4340                 () ->
4341                         mAppSearchImpl.reportUsage(
4342                                 "package",
4343                                 "database",
4344                                 "namespace",
4345                                 "id",
4346                                 /* usageTimestampMillis= */ 1000L,
4347                                 /* systemUsage= */ false));
4348 
4349         assertThrows(
4350                 IllegalStateException.class,
4351                 () ->
4352                         mAppSearchImpl.remove(
4353                                 "package",
4354                                 "database",
4355                                 "namespace",
4356                                 "id",
4357                                 /* removeStatsBuilder= */ null));
4358 
4359         assertThrows(
4360                 IllegalStateException.class,
4361                 () ->
4362                         mAppSearchImpl.removeByQuery(
4363                                 "package",
4364                                 "database",
4365                                 "query",
4366                                 new SearchSpec.Builder().build(),
4367                                 /* removeStatsBuilder= */ null));
4368 
4369         assertThrows(
4370                 IllegalStateException.class,
4371                 () -> mAppSearchImpl.getStorageInfoForPackage("package"));
4372 
4373         assertThrows(
4374                 IllegalStateException.class,
4375                 () -> mAppSearchImpl.getStorageInfoForDatabase("package", "database"));
4376 
4377         assertThrows(
4378                 IllegalStateException.class,
4379                 () -> mAppSearchImpl.persistToDisk(PersistType.Code.FULL));
4380     }
4381 
4382     @Test
testPutPersistsWithLiteFlush()4383     public void testPutPersistsWithLiteFlush() throws Exception {
4384         List<AppSearchSchema> schemas =
4385                 Collections.singletonList(new AppSearchSchema.Builder("type").build());
4386         InternalSetSchemaResponse internalSetSchemaResponse =
4387                 mAppSearchImpl.setSchema(
4388                         "package",
4389                         "database",
4390                         schemas,
4391                         /* visibilityConfigs= */ Collections.emptyList(),
4392                         /* forceOverride= */ false,
4393                         /* version= */ 0,
4394                         /* setSchemaStatsBuilder= */ null);
4395         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
4396 
4397         // Add a document and persist it.
4398         GenericDocument document =
4399                 new GenericDocument.Builder<>("namespace1", "id1", "type").build();
4400         mAppSearchImpl.putDocument(
4401                 "package",
4402                 "database",
4403                 document,
4404                 /* sendChangeNotifications= */ false,
4405                 /* logger= */ null);
4406         mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
4407 
4408         GenericDocument getResult =
4409                 mAppSearchImpl.getDocument(
4410                         "package", "database", "namespace1", "id1", Collections.emptyMap());
4411         assertThat(getResult).isEqualTo(document);
4412 
4413         // That document should be visible even from another instance.
4414         AppSearchImpl appSearchImpl2 =
4415                 AppSearchImpl.create(
4416                         mAppSearchDir,
4417                         new AppSearchConfigImpl(
4418                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
4419                         /* initStatsBuilder= */ null,
4420                         /* visibilityChecker= */ null,
4421                         /* revocableFileDescriptorStore= */ null,
4422                         ALWAYS_OPTIMIZE);
4423         getResult =
4424                 appSearchImpl2.getDocument(
4425                         "package", "database", "namespace1", "id1", Collections.emptyMap());
4426         assertThat(getResult).isEqualTo(document);
4427         appSearchImpl2.close();
4428     }
4429 
4430     @Test
testDeletePersistsWithLiteFlush()4431     public void testDeletePersistsWithLiteFlush() throws Exception {
4432         List<AppSearchSchema> schemas =
4433                 Collections.singletonList(new AppSearchSchema.Builder("type").build());
4434         InternalSetSchemaResponse internalSetSchemaResponse =
4435                 mAppSearchImpl.setSchema(
4436                         "package",
4437                         "database",
4438                         schemas,
4439                         /* visibilityConfigs= */ Collections.emptyList(),
4440                         /* forceOverride= */ false,
4441                         /* version= */ 0,
4442                         /* setSchemaStatsBuilder= */ null);
4443         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
4444 
4445         // Add two documents and persist them.
4446         GenericDocument document1 =
4447                 new GenericDocument.Builder<>("namespace1", "id1", "type").build();
4448         mAppSearchImpl.putDocument(
4449                 "package",
4450                 "database",
4451                 document1,
4452                 /* sendChangeNotifications= */ false,
4453                 /* logger= */ null);
4454         GenericDocument document2 =
4455                 new GenericDocument.Builder<>("namespace1", "id2", "type").build();
4456         mAppSearchImpl.putDocument(
4457                 "package",
4458                 "database",
4459                 document2,
4460                 /* sendChangeNotifications= */ false,
4461                 /* logger= */ null);
4462         mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
4463 
4464         GenericDocument getResult =
4465                 mAppSearchImpl.getDocument(
4466                         "package", "database", "namespace1", "id1", Collections.emptyMap());
4467         assertThat(getResult).isEqualTo(document1);
4468         getResult =
4469                 mAppSearchImpl.getDocument(
4470                         "package", "database", "namespace1", "id2", Collections.emptyMap());
4471         assertThat(getResult).isEqualTo(document2);
4472 
4473         // Delete the first document
4474         mAppSearchImpl.remove("package", "database", "namespace1", "id1", /* statsBuilder= */ null);
4475         mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
4476         assertThrows(
4477                 AppSearchException.class,
4478                 () ->
4479                         mAppSearchImpl.getDocument(
4480                                 "package",
4481                                 "database",
4482                                 "namespace1",
4483                                 "id1",
4484                                 Collections.emptyMap()));
4485         getResult =
4486                 mAppSearchImpl.getDocument(
4487                         "package", "database", "namespace1", "id2", Collections.emptyMap());
4488         assertThat(getResult).isEqualTo(document2);
4489 
4490         // Only the second document should be retrievable from another instance.
4491         AppSearchImpl appSearchImpl2 =
4492                 AppSearchImpl.create(
4493                         mAppSearchDir,
4494                         new AppSearchConfigImpl(
4495                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
4496                         /* initStatsBuilder= */ null,
4497                         /* visibilityChecker= */ null,
4498                         /* revocableFileDescriptorStore= */ null,
4499                         ALWAYS_OPTIMIZE);
4500         assertThrows(
4501                 AppSearchException.class,
4502                 () ->
4503                         appSearchImpl2.getDocument(
4504                                 "package",
4505                                 "database",
4506                                 "namespace1",
4507                                 "id1",
4508                                 Collections.emptyMap()));
4509         getResult =
4510                 appSearchImpl2.getDocument(
4511                         "package", "database", "namespace1", "id2", Collections.emptyMap());
4512         assertThat(getResult).isEqualTo(document2);
4513         appSearchImpl2.close();
4514     }
4515 
4516     @Test
testDeleteByQueryPersistsWithLiteFlush()4517     public void testDeleteByQueryPersistsWithLiteFlush() throws Exception {
4518         List<AppSearchSchema> schemas =
4519                 Collections.singletonList(new AppSearchSchema.Builder("type").build());
4520         InternalSetSchemaResponse internalSetSchemaResponse =
4521                 mAppSearchImpl.setSchema(
4522                         "package",
4523                         "database",
4524                         schemas,
4525                         /* visibilityConfigs= */ Collections.emptyList(),
4526                         /* forceOverride= */ false,
4527                         /* version= */ 0,
4528                         /* setSchemaStatsBuilder= */ null);
4529         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
4530 
4531         // Add two documents and persist them.
4532         GenericDocument document1 =
4533                 new GenericDocument.Builder<>("namespace1", "id1", "type").build();
4534         mAppSearchImpl.putDocument(
4535                 "package",
4536                 "database",
4537                 document1,
4538                 /* sendChangeNotifications= */ false,
4539                 /* logger= */ null);
4540         GenericDocument document2 =
4541                 new GenericDocument.Builder<>("namespace2", "id2", "type").build();
4542         mAppSearchImpl.putDocument(
4543                 "package",
4544                 "database",
4545                 document2,
4546                 /* sendChangeNotifications= */ false,
4547                 /* logger= */ null);
4548         mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
4549 
4550         GenericDocument getResult =
4551                 mAppSearchImpl.getDocument(
4552                         "package", "database", "namespace1", "id1", Collections.emptyMap());
4553         assertThat(getResult).isEqualTo(document1);
4554         getResult =
4555                 mAppSearchImpl.getDocument(
4556                         "package", "database", "namespace2", "id2", Collections.emptyMap());
4557         assertThat(getResult).isEqualTo(document2);
4558 
4559         // Delete the first document
4560         mAppSearchImpl.removeByQuery(
4561                 "package",
4562                 "database",
4563                 "",
4564                 new SearchSpec.Builder()
4565                         .addFilterNamespaces("namespace1")
4566                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
4567                         .build(),
4568                 /* statsBuilder= */ null);
4569         mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
4570         assertThrows(
4571                 AppSearchException.class,
4572                 () ->
4573                         mAppSearchImpl.getDocument(
4574                                 "package",
4575                                 "database",
4576                                 "namespace1",
4577                                 "id1",
4578                                 Collections.emptyMap()));
4579         getResult =
4580                 mAppSearchImpl.getDocument(
4581                         "package", "database", "namespace2", "id2", Collections.emptyMap());
4582         assertThat(getResult).isEqualTo(document2);
4583 
4584         // Only the second document should be retrievable from another instance.
4585         AppSearchImpl appSearchImpl2 =
4586                 AppSearchImpl.create(
4587                         mAppSearchDir,
4588                         new AppSearchConfigImpl(
4589                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
4590                         /* initStatsBuilder= */ null,
4591                         /* visibilityChecker= */ null,
4592                         /* revocableFileDescriptorStore= */ null,
4593                         ALWAYS_OPTIMIZE);
4594         assertThrows(
4595                 AppSearchException.class,
4596                 () ->
4597                         appSearchImpl2.getDocument(
4598                                 "package",
4599                                 "database",
4600                                 "namespace1",
4601                                 "id1",
4602                                 Collections.emptyMap()));
4603         getResult =
4604                 appSearchImpl2.getDocument(
4605                         "package", "database", "namespace2", "id2", Collections.emptyMap());
4606         assertThat(getResult).isEqualTo(document2);
4607         appSearchImpl2.close();
4608     }
4609 
4610     @Test
4611     @RequiresFlagsDisabled(Flags.FLAG_ENABLE_BLOB_STORE)
testGetIcingSearchEngineStorageInfo()4612     public void testGetIcingSearchEngineStorageInfo() throws Exception {
4613         List<AppSearchSchema> schemas =
4614                 Collections.singletonList(new AppSearchSchema.Builder("type").build());
4615         InternalSetSchemaResponse internalSetSchemaResponse =
4616                 mAppSearchImpl.setSchema(
4617                         "package",
4618                         "database",
4619                         schemas,
4620                         /* visibilityConfigs= */ Collections.emptyList(),
4621                         /* forceOverride= */ false,
4622                         /* version= */ 0,
4623                         /* setSchemaStatsBuilder= */ null);
4624         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
4625 
4626         // Add two documents
4627         GenericDocument document1 =
4628                 new GenericDocument.Builder<>("namespace1", "id1", "type").build();
4629         mAppSearchImpl.putDocument(
4630                 "package",
4631                 "database",
4632                 document1,
4633                 /* sendChangeNotifications= */ false,
4634                 /* logger= */ null);
4635         GenericDocument document2 =
4636                 new GenericDocument.Builder<>("namespace1", "id2", "type").build();
4637         mAppSearchImpl.putDocument(
4638                 "package",
4639                 "database",
4640                 document2,
4641                 /* sendChangeNotifications= */ false,
4642                 /* logger= */ null);
4643 
4644         StorageInfoProto storageInfo = mAppSearchImpl.getRawStorageInfoProto();
4645 
4646         // Simple checks to verify if we can get correct StorageInfoProto from IcingSearchEngine
4647         // No need to cover all the fields
4648         assertThat(storageInfo.getTotalStorageSize()).isGreaterThan(0);
4649         assertThat(storageInfo.getDocumentStorageInfo().getNumAliveDocuments()).isEqualTo(2);
4650         assertThat(storageInfo.getSchemaStoreStorageInfo().getNumSchemaTypes())
4651                 .isEqualTo(4); // +2 for VisibilitySchema, +1 for VisibilityOverlay
4652     }
4653 
4654     @Test
4655     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
testGetIcingSearchEngineStorageInfo_enableBlobStore()4656     public void testGetIcingSearchEngineStorageInfo_enableBlobStore() throws Exception {
4657         mAppSearchImpl =
4658                 AppSearchImpl.create(
4659                         mAppSearchDir,
4660                         new AppSearchConfigImpl(
4661                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
4662                         /* initStatsBuilder= */ null,
4663                         /* visibilityChecker= */ null,
4664                         new JetpackRevocableFileDescriptorStore(mUnlimitedConfig),
4665                         ALWAYS_OPTIMIZE);
4666         List<AppSearchSchema> schemas =
4667                 Collections.singletonList(new AppSearchSchema.Builder("type").build());
4668         InternalSetSchemaResponse internalSetSchemaResponse =
4669                 mAppSearchImpl.setSchema(
4670                         "package",
4671                         "database",
4672                         schemas,
4673                         /* visibilityConfigs= */ Collections.emptyList(),
4674                         /* forceOverride= */ false,
4675                         /* version= */ 0,
4676                         /* setSchemaStatsBuilder= */ null);
4677         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
4678 
4679         // Add two documents
4680         GenericDocument document1 =
4681                 new GenericDocument.Builder<>("namespace1", "id1", "type").build();
4682         mAppSearchImpl.putDocument(
4683                 "package",
4684                 "database",
4685                 document1,
4686                 /* sendChangeNotifications= */ false,
4687                 /* logger= */ null);
4688         GenericDocument document2 =
4689                 new GenericDocument.Builder<>("namespace1", "id2", "type").build();
4690         mAppSearchImpl.putDocument(
4691                 "package",
4692                 "database",
4693                 document2,
4694                 /* sendChangeNotifications= */ false,
4695                 /* logger= */ null);
4696 
4697         StorageInfoProto storageInfo = mAppSearchImpl.getRawStorageInfoProto();
4698 
4699         // Simple checks to verify if we can get correct StorageInfoProto from IcingSearchEngine
4700         // No need to cover all the fields
4701         assertThat(storageInfo.getTotalStorageSize()).isGreaterThan(0);
4702         assertThat(storageInfo.getDocumentStorageInfo().getNumAliveDocuments()).isEqualTo(2);
4703         // +2 (document and blob db) * (2 for VisibilitySchema +1 for VisibilityOverlay)
4704         assertThat(storageInfo.getSchemaStoreStorageInfo().getNumSchemaTypes()).isEqualTo(7);
4705     }
4706 
4707     @Test
4708     @RequiresFlagsDisabled(Flags.FLAG_ENABLE_BLOB_STORE)
testGetIcingSearchEngineDebugInfo()4709     public void testGetIcingSearchEngineDebugInfo() throws Exception {
4710         List<AppSearchSchema> schemas =
4711                 Collections.singletonList(new AppSearchSchema.Builder("type").build());
4712         InternalSetSchemaResponse internalSetSchemaResponse =
4713                 mAppSearchImpl.setSchema(
4714                         "package",
4715                         "database",
4716                         schemas,
4717                         /* visibilityConfigs= */ Collections.emptyList(),
4718                         /* forceOverride= */ false,
4719                         /* version= */ 0,
4720                         /* setSchemaStatsBuilder= */ null);
4721         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
4722 
4723         // Add two documents
4724         GenericDocument document1 =
4725                 new GenericDocument.Builder<>("namespace1", "id1", "type").build();
4726         mAppSearchImpl.putDocument(
4727                 "package",
4728                 "database",
4729                 document1,
4730                 /* sendChangeNotifications= */ false,
4731                 /* logger= */ null);
4732         GenericDocument document2 =
4733                 new GenericDocument.Builder<>("namespace1", "id2", "type").build();
4734         mAppSearchImpl.putDocument(
4735                 "package",
4736                 "database",
4737                 document2,
4738                 /* sendChangeNotifications= */ false,
4739                 /* logger= */ null);
4740 
4741         DebugInfoProto debugInfo =
4742                 mAppSearchImpl.getRawDebugInfoProto(DebugInfoVerbosity.Code.DETAILED);
4743 
4744         // Simple checks to verify if we can get correct DebugInfoProto from IcingSearchEngine
4745         // No need to cover all the fields
4746         assertThat(debugInfo.getDocumentInfo().getCorpusInfoList()).hasSize(1);
4747         assertThat(debugInfo.getDocumentInfo().getDocumentStorageInfo().getNumAliveDocuments())
4748                 .isEqualTo(2);
4749         assertThat(debugInfo.getSchemaInfo().getSchema().getTypesList())
4750                 .hasSize(4); // +2 for VisibilitySchema, +1 for VisibilityOverlay
4751     }
4752 
4753     @Test
4754     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
testGetIcingSearchEngineDebugInfo_enableBlobStore()4755     public void testGetIcingSearchEngineDebugInfo_enableBlobStore() throws Exception {
4756         mAppSearchImpl =
4757                 AppSearchImpl.create(
4758                         mAppSearchDir,
4759                         new AppSearchConfigImpl(
4760                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
4761                         /* initStatsBuilder= */ null,
4762                         /* visibilityChecker= */ null,
4763                         new JetpackRevocableFileDescriptorStore(mUnlimitedConfig),
4764                         ALWAYS_OPTIMIZE);
4765         List<AppSearchSchema> schemas =
4766                 Collections.singletonList(new AppSearchSchema.Builder("type").build());
4767         InternalSetSchemaResponse internalSetSchemaResponse =
4768                 mAppSearchImpl.setSchema(
4769                         "package",
4770                         "database",
4771                         schemas,
4772                         /* visibilityConfigs= */ Collections.emptyList(),
4773                         /* forceOverride= */ false,
4774                         /* version= */ 0,
4775                         /* setSchemaStatsBuilder= */ null);
4776         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
4777 
4778         // Add two documents
4779         GenericDocument document1 =
4780                 new GenericDocument.Builder<>("namespace1", "id1", "type").build();
4781         mAppSearchImpl.putDocument(
4782                 "package",
4783                 "database",
4784                 document1,
4785                 /* sendChangeNotifications= */ false,
4786                 /* logger= */ null);
4787         GenericDocument document2 =
4788                 new GenericDocument.Builder<>("namespace1", "id2", "type").build();
4789         mAppSearchImpl.putDocument(
4790                 "package",
4791                 "database",
4792                 document2,
4793                 /* sendChangeNotifications= */ false,
4794                 /* logger= */ null);
4795 
4796         DebugInfoProto debugInfo =
4797                 mAppSearchImpl.getRawDebugInfoProto(DebugInfoVerbosity.Code.DETAILED);
4798 
4799         // Simple checks to verify if we can get correct DebugInfoProto from IcingSearchEngine
4800         // No need to cover all the fields
4801         assertThat(debugInfo.getDocumentInfo().getCorpusInfoList()).hasSize(1);
4802         assertThat(debugInfo.getDocumentInfo().getDocumentStorageInfo().getNumAliveDocuments())
4803                 .isEqualTo(2);
4804         // +2 (document and blob db) * (2 for VisibilitySchema +1 for VisibilityOverlay)
4805         assertThat(debugInfo.getSchemaInfo().getSchema().getTypesList()).hasSize(7);
4806     }
4807 
4808     @Test
testLimitConfig_DocumentSize()4809     public void testLimitConfig_DocumentSize() throws Exception {
4810         // Create a new mAppSearchImpl with a lower limit
4811         mAppSearchImpl.close();
4812         mAppSearchImpl =
4813                 AppSearchImpl.create(
4814                         mTemporaryFolder.newFolder(),
4815                         new AppSearchConfigImpl(
4816                                 new LimitConfig() {
4817                                     @Override
4818                                     public int getMaxDocumentSizeBytes() {
4819                                         return 80;
4820                                     }
4821 
4822                                     @Override
4823                                     public int getPerPackageDocumentCountLimit() {
4824                                         return 1;
4825                                     }
4826 
4827                                     @Override
4828                                     public int getDocumentCountLimitStartThreshold() {
4829                                         return 0;
4830                                     }
4831 
4832                                     @Override
4833                                     public int getMaxSuggestionCount() {
4834                                         return Integer.MAX_VALUE;
4835                                     }
4836 
4837                                     @Override
4838                                     public int getMaxOpenBlobCount() {
4839                                         return Integer.MAX_VALUE;
4840                                     }
4841                                 },
4842                                 new LocalStorageIcingOptionsConfig()),
4843                         /* initStatsBuilder= */ null,
4844                         /* visibilityChecker= */ null,
4845                         /* revocableFileDescriptorStore= */ null,
4846                         ALWAYS_OPTIMIZE);
4847 
4848         // Insert schema
4849         List<AppSearchSchema> schemas =
4850                 Collections.singletonList(new AppSearchSchema.Builder("type").build());
4851         InternalSetSchemaResponse internalSetSchemaResponse =
4852                 mAppSearchImpl.setSchema(
4853                         "package",
4854                         "database",
4855                         schemas,
4856                         /* visibilityConfigs= */ Collections.emptyList(),
4857                         /* forceOverride= */ false,
4858                         /* version= */ 0,
4859                         /* setSchemaStatsBuilder= */ null);
4860         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
4861 
4862         // Insert a document which is too large
4863         GenericDocument document =
4864                 new GenericDocument.Builder<>(
4865                                 "this_namespace_is_long_to_make_the_doc_big", "id", "type")
4866                         .build();
4867         AppSearchException e =
4868                 assertThrows(
4869                         AppSearchException.class,
4870                         () ->
4871                                 mAppSearchImpl.putDocument(
4872                                         "package",
4873                                         "database",
4874                                         document,
4875                                         /* sendChangeNotifications= */ false,
4876                                         /* logger= */ null));
4877         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
4878         assertThat(e)
4879                 .hasMessageThat()
4880                 .contains(
4881                         "Document \"id\" for package \"package\" serialized to 99 bytes, which"
4882                             + " exceeds limit of 80 bytes");
4883 
4884         // Make sure this failure didn't increase our document count. We should still be able to
4885         // index 1 document.
4886         GenericDocument document2 =
4887                 new GenericDocument.Builder<>("namespace", "id2", "type").build();
4888         mAppSearchImpl.putDocument(
4889                 "package",
4890                 "database",
4891                 document2,
4892                 /* sendChangeNotifications= */ false,
4893                 /* logger= */ null);
4894 
4895         // Now we should get a failure
4896         GenericDocument document3 =
4897                 new GenericDocument.Builder<>("namespace", "id3", "type").build();
4898         e =
4899                 assertThrows(
4900                         AppSearchException.class,
4901                         () ->
4902                                 mAppSearchImpl.putDocument(
4903                                         "package",
4904                                         "database",
4905                                         document3,
4906                                         /* sendChangeNotifications= */ false,
4907                                         /* logger= */ null));
4908         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
4909         assertThat(e)
4910                 .hasMessageThat()
4911                 .contains("Package \"package\" exceeded limit of 1 documents");
4912     }
4913 
4914     @Test
testLimitConfig_Init()4915     public void testLimitConfig_Init() throws Exception {
4916         // Create a new mAppSearchImpl with a lower limit
4917         mAppSearchImpl.close();
4918         File tempFolder = mTemporaryFolder.newFolder();
4919         mAppSearchImpl =
4920                 AppSearchImpl.create(
4921                         tempFolder,
4922                         new AppSearchConfigImpl(
4923                                 new LimitConfig() {
4924                                     @Override
4925                                     public int getMaxDocumentSizeBytes() {
4926                                         return 80;
4927                                     }
4928 
4929                                     @Override
4930                                     public int getPerPackageDocumentCountLimit() {
4931                                         return 1;
4932                                     }
4933 
4934                                     @Override
4935                                     public int getDocumentCountLimitStartThreshold() {
4936                                         return 0;
4937                                     }
4938 
4939                                     @Override
4940                                     public int getMaxSuggestionCount() {
4941                                         return Integer.MAX_VALUE;
4942                                     }
4943 
4944                                     @Override
4945                                     public int getMaxOpenBlobCount() {
4946                                         return Integer.MAX_VALUE;
4947                                     }
4948                                 },
4949                                 new LocalStorageIcingOptionsConfig()),
4950                         /* initStatsBuilder= */ null,
4951                         /* visibilityChecker= */ null,
4952                         /* revocableFileDescriptorStore= */ null,
4953                         ALWAYS_OPTIMIZE);
4954 
4955         // Insert schema
4956         List<AppSearchSchema> schemas =
4957                 Collections.singletonList(new AppSearchSchema.Builder("type").build());
4958         InternalSetSchemaResponse internalSetSchemaResponse =
4959                 mAppSearchImpl.setSchema(
4960                         "package",
4961                         "database",
4962                         schemas,
4963                         /* visibilityConfigs= */ Collections.emptyList(),
4964                         /* forceOverride= */ false,
4965                         /* version= */ 0,
4966                         /* setSchemaStatsBuilder= */ null);
4967         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
4968 
4969         // Index a document
4970         mAppSearchImpl.putDocument(
4971                 "package",
4972                 "database",
4973                 new GenericDocument.Builder<>("namespace", "id1", "type").build(),
4974                 /* sendChangeNotifications= */ false,
4975                 /* logger= */ null);
4976 
4977         // Now we should get a failure
4978         GenericDocument document2 =
4979                 new GenericDocument.Builder<>("namespace", "id2", "type").build();
4980         AppSearchException e =
4981                 assertThrows(
4982                         AppSearchException.class,
4983                         () ->
4984                                 mAppSearchImpl.putDocument(
4985                                         "package",
4986                                         "database",
4987                                         document2,
4988                                         /* sendChangeNotifications= */ false,
4989                                         /* logger= */ null));
4990         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
4991         assertThat(e)
4992                 .hasMessageThat()
4993                 .contains("Package \"package\" exceeded limit of 1 documents");
4994 
4995         // Close and reinitialize AppSearchImpl
4996         mAppSearchImpl.close();
4997         mAppSearchImpl =
4998                 AppSearchImpl.create(
4999                         tempFolder,
5000                         new AppSearchConfigImpl(
5001                                 new LimitConfig() {
5002                                     @Override
5003                                     public int getMaxDocumentSizeBytes() {
5004                                         return 80;
5005                                     }
5006 
5007                                     @Override
5008                                     public int getPerPackageDocumentCountLimit() {
5009                                         return 1;
5010                                     }
5011 
5012                                     @Override
5013                                     public int getDocumentCountLimitStartThreshold() {
5014                                         return 0;
5015                                     }
5016 
5017                                     @Override
5018                                     public int getMaxSuggestionCount() {
5019                                         return Integer.MAX_VALUE;
5020                                     }
5021 
5022                                     @Override
5023                                     public int getMaxOpenBlobCount() {
5024                                         return Integer.MAX_VALUE;
5025                                     }
5026                                 },
5027                                 new LocalStorageIcingOptionsConfig()),
5028                         /* initStatsBuilder= */ null,
5029                         /* visibilityChecker= */ null,
5030                         /* revocableFileDescriptorStore= */ null,
5031                         ALWAYS_OPTIMIZE);
5032 
5033         // Make sure the limit is maintained
5034         e =
5035                 assertThrows(
5036                         AppSearchException.class,
5037                         () ->
5038                                 mAppSearchImpl.putDocument(
5039                                         "package",
5040                                         "database",
5041                                         document2,
5042                                         /* sendChangeNotifications= */ false,
5043                                         /* logger= */ null));
5044         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
5045         assertThat(e)
5046                 .hasMessageThat()
5047                 .contains("Package \"package\" exceeded limit of 1 documents");
5048     }
5049 
5050     @Test
testLimitConfig_Remove()5051     public void testLimitConfig_Remove() throws Exception {
5052         // Create a new mAppSearchImpl with a lower limit
5053         mAppSearchImpl.close();
5054         mAppSearchImpl =
5055                 AppSearchImpl.create(
5056                         mTemporaryFolder.newFolder(),
5057                         new AppSearchConfigImpl(
5058                                 new LimitConfig() {
5059                                     @Override
5060                                     public int getMaxDocumentSizeBytes() {
5061                                         return Integer.MAX_VALUE;
5062                                     }
5063 
5064                                     @Override
5065                                     public int getPerPackageDocumentCountLimit() {
5066                                         return 3;
5067                                     }
5068 
5069                                     @Override
5070                                     public int getDocumentCountLimitStartThreshold() {
5071                                         return 0;
5072                                     }
5073 
5074                                     @Override
5075                                     public int getMaxSuggestionCount() {
5076                                         return Integer.MAX_VALUE;
5077                                     }
5078 
5079                                     @Override
5080                                     public int getMaxOpenBlobCount() {
5081                                         return Integer.MAX_VALUE;
5082                                     }
5083                                 },
5084                                 new LocalStorageIcingOptionsConfig()),
5085                         /* initStatsBuilder= */ null,
5086                         /* visibilityChecker= */ null,
5087                         /* revocableFileDescriptorStore= */ null,
5088                         ALWAYS_OPTIMIZE);
5089 
5090         // Insert schema
5091         List<AppSearchSchema> schemas =
5092                 Collections.singletonList(new AppSearchSchema.Builder("type").build());
5093         InternalSetSchemaResponse internalSetSchemaResponse =
5094                 mAppSearchImpl.setSchema(
5095                         "package",
5096                         "database",
5097                         schemas,
5098                         /* visibilityConfigs= */ Collections.emptyList(),
5099                         /* forceOverride= */ false,
5100                         /* version= */ 0,
5101                         /* setSchemaStatsBuilder= */ null);
5102         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
5103 
5104         // Index 3 documents
5105         mAppSearchImpl.putDocument(
5106                 "package",
5107                 "database",
5108                 new GenericDocument.Builder<>("namespace", "id1", "type").build(),
5109                 /* sendChangeNotifications= */ false,
5110                 /* logger= */ null);
5111         mAppSearchImpl.putDocument(
5112                 "package",
5113                 "database",
5114                 new GenericDocument.Builder<>("namespace", "id2", "type").build(),
5115                 /* sendChangeNotifications= */ false,
5116                 /* logger= */ null);
5117         mAppSearchImpl.putDocument(
5118                 "package",
5119                 "database",
5120                 new GenericDocument.Builder<>("namespace", "id3", "type").build(),
5121                 /* sendChangeNotifications= */ false,
5122                 /* logger= */ null);
5123 
5124         // Now we should get a failure
5125         GenericDocument document4 =
5126                 new GenericDocument.Builder<>("namespace", "id4", "type").build();
5127         AppSearchException e =
5128                 assertThrows(
5129                         AppSearchException.class,
5130                         () ->
5131                                 mAppSearchImpl.putDocument(
5132                                         "package",
5133                                         "database",
5134                                         document4,
5135                                         /* sendChangeNotifications= */ false,
5136                                         /* logger= */ null));
5137         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
5138         assertThat(e)
5139                 .hasMessageThat()
5140                 .contains("Package \"package\" exceeded limit of 3 documents");
5141 
5142         // Remove a document that doesn't exist
5143         assertThrows(
5144                 AppSearchException.class,
5145                 () ->
5146                         mAppSearchImpl.remove(
5147                                 "package",
5148                                 "database",
5149                                 "namespace",
5150                                 "id4",
5151                                 /* removeStatsBuilder= */ null));
5152 
5153         // Should still fail
5154         e =
5155                 assertThrows(
5156                         AppSearchException.class,
5157                         () ->
5158                                 mAppSearchImpl.putDocument(
5159                                         "package",
5160                                         "database",
5161                                         document4,
5162                                         /* sendChangeNotifications= */ false,
5163                                         /* logger= */ null));
5164         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
5165         assertThat(e)
5166                 .hasMessageThat()
5167                 .contains("Package \"package\" exceeded limit of 3 documents");
5168 
5169         // Remove a document that does exist
5170         mAppSearchImpl.remove(
5171                 "package", "database", "namespace", "id2", /* removeStatsBuilder= */ null);
5172 
5173         // Now doc4 should work
5174         mAppSearchImpl.putDocument(
5175                 "package",
5176                 "database",
5177                 document4,
5178                 /* sendChangeNotifications= */ false,
5179                 /* logger= */ null);
5180 
5181         // The next one should fail again
5182         e =
5183                 assertThrows(
5184                         AppSearchException.class,
5185                         () ->
5186                                 mAppSearchImpl.putDocument(
5187                                         "package",
5188                                         "database",
5189                                         new GenericDocument.Builder<>("namespace", "id5", "type")
5190                                                 .build(),
5191                                         /* sendChangeNotifications= */ false,
5192                                         /* logger= */ null));
5193         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
5194         assertThat(e)
5195                 .hasMessageThat()
5196                 .contains("Package \"package\" exceeded limit of 3 documents");
5197     }
5198 
5199     @Test
testLimitConfig_DifferentPackages()5200     public void testLimitConfig_DifferentPackages() throws Exception {
5201         // Create a new mAppSearchImpl with a lower limit
5202         mAppSearchImpl.close();
5203         File tempFolder = mTemporaryFolder.newFolder();
5204         mAppSearchImpl =
5205                 AppSearchImpl.create(
5206                         tempFolder,
5207                         new AppSearchConfigImpl(
5208                                 new LimitConfig() {
5209                                     @Override
5210                                     public int getMaxDocumentSizeBytes() {
5211                                         return Integer.MAX_VALUE;
5212                                     }
5213 
5214                                     @Override
5215                                     public int getPerPackageDocumentCountLimit() {
5216                                         return 2;
5217                                     }
5218 
5219                                     @Override
5220                                     public int getDocumentCountLimitStartThreshold() {
5221                                         return 0;
5222                                     }
5223 
5224                                     @Override
5225                                     public int getMaxSuggestionCount() {
5226                                         return Integer.MAX_VALUE;
5227                                     }
5228 
5229                                     @Override
5230                                     public int getMaxOpenBlobCount() {
5231                                         return Integer.MAX_VALUE;
5232                                     }
5233                                 },
5234                                 new LocalStorageIcingOptionsConfig()),
5235                         /* initStatsBuilder= */ null,
5236                         /* visibilityChecker= */ null,
5237                         /* revocableFileDescriptorStore= */ null,
5238                         ALWAYS_OPTIMIZE);
5239 
5240         // Insert schema
5241         List<AppSearchSchema> schemas =
5242                 Collections.singletonList(new AppSearchSchema.Builder("type").build());
5243         InternalSetSchemaResponse internalSetSchemaResponse =
5244                 mAppSearchImpl.setSchema(
5245                         "package1",
5246                         "database1",
5247                         schemas,
5248                         /* visibilityConfigs= */ Collections.emptyList(),
5249                         /* forceOverride= */ false,
5250                         /* version= */ 0,
5251                         /* setSchemaStatsBuilder= */ null);
5252         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
5253         internalSetSchemaResponse =
5254                 mAppSearchImpl.setSchema(
5255                         "package1",
5256                         "database2",
5257                         schemas,
5258                         /* visibilityConfigs= */ Collections.emptyList(),
5259                         /* forceOverride= */ false,
5260                         /* version= */ 0,
5261                         /* setSchemaStatsBuilder= */ null);
5262         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
5263         internalSetSchemaResponse =
5264                 mAppSearchImpl.setSchema(
5265                         "package2",
5266                         "database1",
5267                         schemas,
5268                         /* visibilityConfigs= */ Collections.emptyList(),
5269                         /* forceOverride= */ false,
5270                         /* version= */ 0,
5271                         /* setSchemaStatsBuilder= */ null);
5272         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
5273         internalSetSchemaResponse =
5274                 mAppSearchImpl.setSchema(
5275                         "package2",
5276                         "database2",
5277                         schemas,
5278                         /* visibilityConfigs= */ Collections.emptyList(),
5279                         /* forceOverride= */ false,
5280                         /* version= */ 0,
5281                         /* setSchemaStatsBuilder= */ null);
5282         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
5283 
5284         // Index documents in package1/database1
5285         mAppSearchImpl.putDocument(
5286                 "package1",
5287                 "database1",
5288                 new GenericDocument.Builder<>("namespace", "id1", "type").build(),
5289                 /* sendChangeNotifications= */ false,
5290                 /* logger= */ null);
5291         mAppSearchImpl.putDocument(
5292                 "package1",
5293                 "database2",
5294                 new GenericDocument.Builder<>("namespace", "id2", "type").build(),
5295                 /* sendChangeNotifications= */ false,
5296                 /* logger= */ null);
5297 
5298         // Indexing a third doc into package1 should fail (here we use database3)
5299         AppSearchException e =
5300                 assertThrows(
5301                         AppSearchException.class,
5302                         () ->
5303                                 mAppSearchImpl.putDocument(
5304                                         "package1",
5305                                         "database3",
5306                                         new GenericDocument.Builder<>("namespace", "id3", "type")
5307                                                 .build(),
5308                                         /* sendChangeNotifications= */ false,
5309                                         /* logger= */ null));
5310         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
5311         assertThat(e)
5312                 .hasMessageThat()
5313                 .contains("Package \"package1\" exceeded limit of 2 documents");
5314 
5315         // Indexing a doc into package2 should succeed
5316         mAppSearchImpl.putDocument(
5317                 "package2",
5318                 "database1",
5319                 new GenericDocument.Builder<>("namespace", "id1", "type").build(),
5320                 /* sendChangeNotifications= */ false,
5321                 /* logger= */ null);
5322 
5323         // Reinitialize to make sure packages are parsed correctly on init
5324         mAppSearchImpl.close();
5325         mAppSearchImpl =
5326                 AppSearchImpl.create(
5327                         tempFolder,
5328                         new AppSearchConfigImpl(
5329                                 new LimitConfig() {
5330                                     @Override
5331                                     public int getMaxDocumentSizeBytes() {
5332                                         return Integer.MAX_VALUE;
5333                                     }
5334 
5335                                     @Override
5336                                     public int getPerPackageDocumentCountLimit() {
5337                                         return 2;
5338                                     }
5339 
5340                                     @Override
5341                                     public int getDocumentCountLimitStartThreshold() {
5342                                         return 0;
5343                                     }
5344 
5345                                     @Override
5346                                     public int getMaxSuggestionCount() {
5347                                         return Integer.MAX_VALUE;
5348                                     }
5349 
5350                                     @Override
5351                                     public int getMaxOpenBlobCount() {
5352                                         return Integer.MAX_VALUE;
5353                                     }
5354                                 },
5355                                 new LocalStorageIcingOptionsConfig()),
5356                         /* initStatsBuilder= */ null,
5357                         /* visibilityChecker= */ null,
5358                         /* revocableFileDescriptorStore= */ null,
5359                         ALWAYS_OPTIMIZE);
5360 
5361         // package1 should still be out of space
5362         e =
5363                 assertThrows(
5364                         AppSearchException.class,
5365                         () ->
5366                                 mAppSearchImpl.putDocument(
5367                                         "package1",
5368                                         "database4",
5369                                         new GenericDocument.Builder<>("namespace", "id4", "type")
5370                                                 .build(),
5371                                         /* sendChangeNotifications= */ false,
5372                                         /* logger= */ null));
5373         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
5374         assertThat(e)
5375                 .hasMessageThat()
5376                 .contains("Package \"package1\" exceeded limit of 2 documents");
5377 
5378         // package2 has room for one more
5379         mAppSearchImpl.putDocument(
5380                 "package2",
5381                 "database2",
5382                 new GenericDocument.Builder<>("namespace", "id2", "type").build(),
5383                 /* sendChangeNotifications= */ false,
5384                 /* logger= */ null);
5385 
5386         // now package2 really is out of space
5387         e =
5388                 assertThrows(
5389                         AppSearchException.class,
5390                         () ->
5391                                 mAppSearchImpl.putDocument(
5392                                         "package2",
5393                                         "database3",
5394                                         new GenericDocument.Builder<>("namespace", "id3", "type")
5395                                                 .build(),
5396                                         /* sendChangeNotifications= */ false,
5397                                         /* logger= */ null));
5398         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
5399         assertThat(e)
5400                 .hasMessageThat()
5401                 .contains("Package \"package2\" exceeded limit of 2 documents");
5402     }
5403 
5404     @Test
testLimitConfig_RemoveByQuery()5405     public void testLimitConfig_RemoveByQuery() throws Exception {
5406         // Create a new mAppSearchImpl with a lower limit
5407         mAppSearchImpl.close();
5408         mAppSearchImpl =
5409                 AppSearchImpl.create(
5410                         mTemporaryFolder.newFolder(),
5411                         new AppSearchConfigImpl(
5412                                 new LimitConfig() {
5413                                     @Override
5414                                     public int getMaxDocumentSizeBytes() {
5415                                         return Integer.MAX_VALUE;
5416                                     }
5417 
5418                                     @Override
5419                                     public int getPerPackageDocumentCountLimit() {
5420                                         return 3;
5421                                     }
5422 
5423                                     @Override
5424                                     public int getDocumentCountLimitStartThreshold() {
5425                                         return 0;
5426                                     }
5427 
5428                                     @Override
5429                                     public int getMaxSuggestionCount() {
5430                                         return Integer.MAX_VALUE;
5431                                     }
5432 
5433                                     @Override
5434                                     public int getMaxOpenBlobCount() {
5435                                         return Integer.MAX_VALUE;
5436                                     }
5437                                 },
5438                                 new LocalStorageIcingOptionsConfig()),
5439                         /* initStatsBuilder= */ null,
5440                         /* visibilityChecker= */ null,
5441                         /* revocableFileDescriptorStore= */ null,
5442                         ALWAYS_OPTIMIZE);
5443 
5444         // Insert schema
5445         List<AppSearchSchema> schemas =
5446                 Collections.singletonList(
5447                         new AppSearchSchema.Builder("type")
5448                                 .addProperty(
5449                                         new AppSearchSchema.StringPropertyConfig.Builder("body")
5450                                                 .setIndexingType(
5451                                                         AppSearchSchema.StringPropertyConfig
5452                                                                 .INDEXING_TYPE_PREFIXES)
5453                                                 .setTokenizerType(
5454                                                         AppSearchSchema.StringPropertyConfig
5455                                                                 .TOKENIZER_TYPE_PLAIN)
5456                                                 .build())
5457                                 .build());
5458         InternalSetSchemaResponse internalSetSchemaResponse =
5459                 mAppSearchImpl.setSchema(
5460                         "package",
5461                         "database",
5462                         schemas,
5463                         /* visibilityConfigs= */ Collections.emptyList(),
5464                         /* forceOverride= */ false,
5465                         /* version= */ 0,
5466                         /* setSchemaStatsBuilder= */ null);
5467         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
5468 
5469         // Index 3 documents
5470         mAppSearchImpl.putDocument(
5471                 "package",
5472                 "database",
5473                 new GenericDocument.Builder<>("namespace", "id1", "type")
5474                         .setPropertyString("body", "tablet")
5475                         .build(),
5476                 /* sendChangeNotifications= */ false,
5477                 /* logger= */ null);
5478         mAppSearchImpl.putDocument(
5479                 "package",
5480                 "database",
5481                 new GenericDocument.Builder<>("namespace", "id2", "type")
5482                         .setPropertyString("body", "tabby")
5483                         .build(),
5484                 /* sendChangeNotifications= */ false,
5485                 /* logger= */ null);
5486         mAppSearchImpl.putDocument(
5487                 "package",
5488                 "database",
5489                 new GenericDocument.Builder<>("namespace", "id3", "type")
5490                         .setPropertyString("body", "grabby")
5491                         .build(),
5492                 /* sendChangeNotifications= */ false,
5493                 /* logger= */ null);
5494 
5495         // Now we should get a failure
5496         GenericDocument document4 =
5497                 new GenericDocument.Builder<>("namespace", "id4", "type").build();
5498         AppSearchException e =
5499                 assertThrows(
5500                         AppSearchException.class,
5501                         () ->
5502                                 mAppSearchImpl.putDocument(
5503                                         "package",
5504                                         "database",
5505                                         document4,
5506                                         /* sendChangeNotifications= */ false,
5507                                         /* logger= */ null));
5508         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
5509         assertThat(e)
5510                 .hasMessageThat()
5511                 .contains("Package \"package\" exceeded limit of 3 documents");
5512 
5513         // Run removebyquery, deleting nothing
5514         mAppSearchImpl.removeByQuery(
5515                 "package",
5516                 "database",
5517                 "nothing",
5518                 new SearchSpec.Builder().build(),
5519                 /* removeStatsBuilder= */ null);
5520 
5521         // Should still fail
5522         e =
5523                 assertThrows(
5524                         AppSearchException.class,
5525                         () ->
5526                                 mAppSearchImpl.putDocument(
5527                                         "package",
5528                                         "database",
5529                                         document4,
5530                                         /* sendChangeNotifications= */ false,
5531                                         /* logger= */ null));
5532         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
5533         assertThat(e)
5534                 .hasMessageThat()
5535                 .contains("Package \"package\" exceeded limit of 3 documents");
5536 
5537         // Remove "tab*"
5538         mAppSearchImpl.removeByQuery(
5539                 "package",
5540                 "database",
5541                 "tab",
5542                 new SearchSpec.Builder().build(),
5543                 /* removeStatsBuilder= */ null);
5544 
5545         // Now doc4 and doc5 should work
5546         mAppSearchImpl.putDocument(
5547                 "package",
5548                 "database",
5549                 document4,
5550                 /* sendChangeNotifications= */ false,
5551                 /* logger= */ null);
5552         mAppSearchImpl.putDocument(
5553                 "package",
5554                 "database",
5555                 new GenericDocument.Builder<>("namespace", "id5", "type").build(),
5556                 /* sendChangeNotifications= */ false,
5557                 /* logger= */ null);
5558 
5559         // We only deleted 2 docs so the next one should fail again
5560         e =
5561                 assertThrows(
5562                         AppSearchException.class,
5563                         () ->
5564                                 mAppSearchImpl.putDocument(
5565                                         "package",
5566                                         "database",
5567                                         new GenericDocument.Builder<>("namespace", "id6", "type")
5568                                                 .build(),
5569                                         /* sendChangeNotifications= */ false,
5570                                         /* logger= */ null));
5571         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
5572         assertThat(e)
5573                 .hasMessageThat()
5574                 .contains("Package \"package\" exceeded limit of 3 documents");
5575     }
5576 
5577     @Test
testRemoveByQuery_withJoinSpec_throwsException()5578     public void testRemoveByQuery_withJoinSpec_throwsException() {
5579         Exception e =
5580                 assertThrows(
5581                         IllegalArgumentException.class,
5582                         () ->
5583                                 mAppSearchImpl.removeByQuery(
5584                                         /* packageName= */ "",
5585                                         /* databaseName= */ "",
5586                                         /* queryExpression= */ "",
5587                                         new SearchSpec.Builder()
5588                                                 .setJoinSpec(
5589                                                         new JoinSpec.Builder("childProp").build())
5590                                                 .build(),
5591                                         null));
5592         assertThat(e.getMessage())
5593                 .isEqualTo("JoinSpec not allowed in removeByQuery, but JoinSpec was provided");
5594     }
5595 
5596     @Test
testLimitConfig_Replace()5597     public void testLimitConfig_Replace() throws Exception {
5598         // Create a new mAppSearchImpl with a lower limit
5599         mAppSearchImpl.close();
5600         mAppSearchImpl =
5601                 AppSearchImpl.create(
5602                         mTemporaryFolder.newFolder(),
5603                         new AppSearchConfigImpl(
5604                                 new LimitConfig() {
5605                                     @Override
5606                                     public int getMaxDocumentSizeBytes() {
5607                                         return Integer.MAX_VALUE;
5608                                     }
5609 
5610                                     @Override
5611                                     public int getPerPackageDocumentCountLimit() {
5612                                         return 2;
5613                                     }
5614 
5615                                     @Override
5616                                     public int getDocumentCountLimitStartThreshold() {
5617                                         return 0;
5618                                     }
5619 
5620                                     @Override
5621                                     public int getMaxSuggestionCount() {
5622                                         return Integer.MAX_VALUE;
5623                                     }
5624 
5625                                     @Override
5626                                     public int getMaxOpenBlobCount() {
5627                                         return Integer.MAX_VALUE;
5628                                     }
5629                                 },
5630                                 new LocalStorageIcingOptionsConfig()),
5631                         /* initStatsBuilder= */ null,
5632                         /* visibilityChecker= */ null,
5633                         /* revocableFileDescriptorStore= */ null,
5634                         ALWAYS_OPTIMIZE);
5635 
5636         // Insert schema
5637         List<AppSearchSchema> schemas =
5638                 Collections.singletonList(
5639                         new AppSearchSchema.Builder("type")
5640                                 .addProperty(
5641                                         new AppSearchSchema.StringPropertyConfig.Builder("body")
5642                                                 .build())
5643                                 .build());
5644         InternalSetSchemaResponse internalSetSchemaResponse =
5645                 mAppSearchImpl.setSchema(
5646                         "package",
5647                         "database",
5648                         schemas,
5649                         /* visibilityConfigs= */ Collections.emptyList(),
5650                         /* forceOverride= */ false,
5651                         /* version= */ 0,
5652                         /* setSchemaStatsBuilder= */ null);
5653         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
5654 
5655         // Index a document
5656         mAppSearchImpl.putDocument(
5657                 "package",
5658                 "database",
5659                 new GenericDocument.Builder<>("namespace", "id1", "type")
5660                         .setPropertyString("body", "id1.orig")
5661                         .build(),
5662                 /* sendChangeNotifications= */ false,
5663                 /* logger= */ null);
5664         // Replace it with another doc
5665         mAppSearchImpl.putDocument(
5666                 "package",
5667                 "database",
5668                 new GenericDocument.Builder<>("namespace", "id1", "type")
5669                         .setPropertyString("body", "id1.new")
5670                         .build(),
5671                 /* sendChangeNotifications= */ false,
5672                 /* logger= */ null);
5673 
5674         // Index id2. This should pass but only because we check for replacements.
5675         mAppSearchImpl.putDocument(
5676                 "package",
5677                 "database",
5678                 new GenericDocument.Builder<>("namespace", "id2", "type").build(),
5679                 /* sendChangeNotifications= */ false,
5680                 /* logger= */ null);
5681 
5682         // Now we should get a failure on id3
5683         GenericDocument document3 =
5684                 new GenericDocument.Builder<>("namespace", "id3", "type").build();
5685         AppSearchException e =
5686                 assertThrows(
5687                         AppSearchException.class,
5688                         () ->
5689                                 mAppSearchImpl.putDocument(
5690                                         "package",
5691                                         "database",
5692                                         document3,
5693                                         /* sendChangeNotifications= */ false,
5694                                         /* logger= */ null));
5695         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
5696         assertThat(e)
5697                 .hasMessageThat()
5698                 .contains("Package \"package\" exceeded limit of 2 documents");
5699     }
5700 
5701     @Test
testLimitConfig_ReplaceReinit()5702     public void testLimitConfig_ReplaceReinit() throws Exception {
5703         // Create a new mAppSearchImpl with a lower limit
5704         mAppSearchImpl.close();
5705         File tempFolder = mTemporaryFolder.newFolder();
5706         mAppSearchImpl =
5707                 AppSearchImpl.create(
5708                         tempFolder,
5709                         new AppSearchConfigImpl(
5710                                 new LimitConfig() {
5711                                     @Override
5712                                     public int getMaxDocumentSizeBytes() {
5713                                         return Integer.MAX_VALUE;
5714                                     }
5715 
5716                                     @Override
5717                                     public int getPerPackageDocumentCountLimit() {
5718                                         return 2;
5719                                     }
5720 
5721                                     @Override
5722                                     public int getDocumentCountLimitStartThreshold() {
5723                                         return 0;
5724                                     }
5725 
5726                                     @Override
5727                                     public int getMaxSuggestionCount() {
5728                                         return Integer.MAX_VALUE;
5729                                     }
5730 
5731                                     @Override
5732                                     public int getMaxOpenBlobCount() {
5733                                         return Integer.MAX_VALUE;
5734                                     }
5735                                 },
5736                                 new LocalStorageIcingOptionsConfig()),
5737                         /* initStatsBuilder= */ null,
5738                         /* visibilityChecker= */ null,
5739                         /* revocableFileDescriptorStore= */ null,
5740                         ALWAYS_OPTIMIZE);
5741 
5742         // Insert schema
5743         List<AppSearchSchema> schemas =
5744                 Collections.singletonList(
5745                         new AppSearchSchema.Builder("type")
5746                                 .addProperty(
5747                                         new AppSearchSchema.StringPropertyConfig.Builder("body")
5748                                                 .build())
5749                                 .build());
5750         InternalSetSchemaResponse internalSetSchemaResponse =
5751                 mAppSearchImpl.setSchema(
5752                         "package",
5753                         "database",
5754                         schemas,
5755                         /* visibilityConfigs= */ Collections.emptyList(),
5756                         /* forceOverride= */ false,
5757                         /* version= */ 0,
5758                         /* setSchemaStatsBuilder= */ null);
5759         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
5760 
5761         // Index a document
5762         mAppSearchImpl.putDocument(
5763                 "package",
5764                 "database",
5765                 new GenericDocument.Builder<>("namespace", "id1", "type")
5766                         .setPropertyString("body", "id1.orig")
5767                         .build(),
5768                 /* sendChangeNotifications= */ false,
5769                 /* logger= */ null);
5770         // Replace it with another doc
5771         mAppSearchImpl.putDocument(
5772                 "package",
5773                 "database",
5774                 new GenericDocument.Builder<>("namespace", "id1", "type")
5775                         .setPropertyString("body", "id1.new")
5776                         .build(),
5777                 /* sendChangeNotifications= */ false,
5778                 /* logger= */ null);
5779 
5780         // Reinitialize to make sure replacements are correctly accounted for by init
5781         mAppSearchImpl.close();
5782         mAppSearchImpl =
5783                 AppSearchImpl.create(
5784                         tempFolder,
5785                         new AppSearchConfigImpl(
5786                                 new LimitConfig() {
5787                                     @Override
5788                                     public int getMaxDocumentSizeBytes() {
5789                                         return Integer.MAX_VALUE;
5790                                     }
5791 
5792                                     @Override
5793                                     public int getPerPackageDocumentCountLimit() {
5794                                         return 2;
5795                                     }
5796 
5797                                     @Override
5798                                     public int getDocumentCountLimitStartThreshold() {
5799                                         return 0;
5800                                     }
5801 
5802                                     @Override
5803                                     public int getMaxSuggestionCount() {
5804                                         return Integer.MAX_VALUE;
5805                                     }
5806 
5807                                     @Override
5808                                     public int getMaxOpenBlobCount() {
5809                                         return Integer.MAX_VALUE;
5810                                     }
5811                                 },
5812                                 new LocalStorageIcingOptionsConfig()),
5813                         /* initStatsBuilder= */ null,
5814                         /* visibilityChecker= */ null,
5815                         /* revocableFileDescriptorStore= */ null,
5816                         ALWAYS_OPTIMIZE);
5817 
5818         // Index id2. This should pass but only because we check for replacements.
5819         mAppSearchImpl.putDocument(
5820                 "package",
5821                 "database",
5822                 new GenericDocument.Builder<>("namespace", "id2", "type").build(),
5823                 /* sendChangeNotifications= */ false,
5824                 /* logger= */ null);
5825 
5826         // Now we should get a failure on id3
5827         GenericDocument document3 =
5828                 new GenericDocument.Builder<>("namespace", "id3", "type").build();
5829         AppSearchException e =
5830                 assertThrows(
5831                         AppSearchException.class,
5832                         () ->
5833                                 mAppSearchImpl.putDocument(
5834                                         "package",
5835                                         "database",
5836                                         document3,
5837                                         /* sendChangeNotifications= */ false,
5838                                         /* logger= */ null));
5839         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
5840         assertThat(e)
5841                 .hasMessageThat()
5842                 .contains("Package \"package\" exceeded limit of 2 documents");
5843     }
5844 
5845     @Test
testLimitConfig_suggestion()5846     public void testLimitConfig_suggestion() throws Exception {
5847         mAppSearchImpl.close();
5848         File tempFolder = mTemporaryFolder.newFolder();
5849         mAppSearchImpl =
5850                 AppSearchImpl.create(
5851                         tempFolder,
5852                         new AppSearchConfigImpl(
5853                                 new LimitConfig() {
5854                                     @Override
5855                                     public int getMaxDocumentSizeBytes() {
5856                                         return Integer.MAX_VALUE;
5857                                     }
5858 
5859                                     @Override
5860                                     public int getPerPackageDocumentCountLimit() {
5861                                         return Integer.MAX_VALUE;
5862                                     }
5863 
5864                                     @Override
5865                                     public int getDocumentCountLimitStartThreshold() {
5866                                         return 0;
5867                                     }
5868 
5869                                     @Override
5870                                     public int getMaxSuggestionCount() {
5871                                         return 2;
5872                                     }
5873 
5874                                     @Override
5875                                     public int getMaxOpenBlobCount() {
5876                                         return Integer.MAX_VALUE;
5877                                     }
5878                                 },
5879                                 new LocalStorageIcingOptionsConfig()),
5880                         /* initStatsBuilder= */ null,
5881                         /* visibilityChecker= */ null,
5882                         /* revocableFileDescriptorStore= */ null,
5883                         ALWAYS_OPTIMIZE);
5884 
5885         AppSearchException e =
5886                 assertThrows(
5887                         AppSearchException.class,
5888                         () ->
5889                                 mAppSearchImpl.searchSuggestion(
5890                                         "package",
5891                                         "database",
5892                                         /* suggestionQueryExpression= */ "t",
5893                                         new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10)
5894                                                 .build()));
5895         assertThat(e.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
5896         assertThat(e)
5897                 .hasMessageThat()
5898                 .contains("Trying to get 10 suggestion results, which exceeds limit of 2");
5899     }
5900 
5901     @Test
testLimitConfig_belowLimitStartThreshold_limitHasNoEffect()5902     public void testLimitConfig_belowLimitStartThreshold_limitHasNoEffect() throws Exception {
5903         // Create a new mAppSearchImpl with a low limit, but a higher limit start threshold.
5904         mAppSearchImpl.close();
5905         File tempFolder = mTemporaryFolder.newFolder();
5906         mAppSearchImpl =
5907                 AppSearchImpl.create(
5908                         tempFolder,
5909                         new AppSearchConfigImpl(
5910                                 new LimitConfig() {
5911                                     @Override
5912                                     public int getMaxDocumentSizeBytes() {
5913                                         return Integer.MAX_VALUE;
5914                                     }
5915 
5916                                     @Override
5917                                     public int getPerPackageDocumentCountLimit() {
5918                                         return 1;
5919                                     }
5920 
5921                                     @Override
5922                                     public int getDocumentCountLimitStartThreshold() {
5923                                         return 3;
5924                                     }
5925 
5926                                     @Override
5927                                     public int getMaxSuggestionCount() {
5928                                         return Integer.MAX_VALUE;
5929                                     }
5930 
5931                                     @Override
5932                                     public int getMaxOpenBlobCount() {
5933                                         return Integer.MAX_VALUE;
5934                                     }
5935                                 },
5936                                 new LocalStorageIcingOptionsConfig()),
5937                         /* initStatsBuilder= */ null,
5938                         /* visibilityChecker= */ null,
5939                         /* revocableFileDescriptorStore= */ null,
5940                         ALWAYS_OPTIMIZE);
5941 
5942         // Insert schema
5943         List<AppSearchSchema> schemas =
5944                 Collections.singletonList(new AppSearchSchema.Builder("type").build());
5945         InternalSetSchemaResponse internalSetSchemaResponse =
5946                 mAppSearchImpl.setSchema(
5947                         "package",
5948                         "database",
5949                         schemas,
5950                         /* visibilityConfigs= */ Collections.emptyList(),
5951                         /* forceOverride= */ false,
5952                         /* version= */ 0,
5953                         /* setSchemaStatsBuilder= */ null);
5954         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
5955 
5956         // Index a document
5957         mAppSearchImpl.putDocument(
5958                 "package",
5959                 "database",
5960                 new GenericDocument.Builder<>("namespace", "id1", "type").build(),
5961                 /* sendChangeNotifications= */ false,
5962                 /* logger= */ null);
5963 
5964         // We should still be able to index another document even though we are over the
5965         // getPerPackageDocumentCountLimit threshold.
5966         GenericDocument document2 =
5967                 new GenericDocument.Builder<>("namespace", "id2", "type").build();
5968         mAppSearchImpl.putDocument(
5969                 "package",
5970                 "database",
5971                 document2,
5972                 /* sendChangeNotifications= */ false,
5973                 /* logger= */ null);
5974     }
5975 
5976     @Test
testLimitConfig_aboveLimitStartThreshold_limitTakesEffect()5977     public void testLimitConfig_aboveLimitStartThreshold_limitTakesEffect() throws Exception {
5978         // Create a new mAppSearchImpl with a low limit, but a higher limit start threshold.
5979         mAppSearchImpl.close();
5980         File tempFolder = mTemporaryFolder.newFolder();
5981         mAppSearchImpl =
5982                 AppSearchImpl.create(
5983                         tempFolder,
5984                         new AppSearchConfigImpl(
5985                                 new LimitConfig() {
5986                                     @Override
5987                                     public int getMaxDocumentSizeBytes() {
5988                                         return Integer.MAX_VALUE;
5989                                     }
5990 
5991                                     @Override
5992                                     public int getPerPackageDocumentCountLimit() {
5993                                         return 1;
5994                                     }
5995 
5996                                     @Override
5997                                     public int getDocumentCountLimitStartThreshold() {
5998                                         return 3;
5999                                     }
6000 
6001                                     @Override
6002                                     public int getMaxSuggestionCount() {
6003                                         return Integer.MAX_VALUE;
6004                                     }
6005 
6006                                     @Override
6007                                     public int getMaxOpenBlobCount() {
6008                                         return Integer.MAX_VALUE;
6009                                     }
6010                                 },
6011                                 new LocalStorageIcingOptionsConfig()),
6012                         /* initStatsBuilder= */ null,
6013                         /* visibilityChecker= */ null,
6014                         /* revocableFileDescriptorStore= */ null,
6015                         ALWAYS_OPTIMIZE);
6016 
6017         // Insert schemas for thress packages
6018         List<AppSearchSchema> schemas =
6019                 Collections.singletonList(new AppSearchSchema.Builder("type").build());
6020         InternalSetSchemaResponse internalSetSchemaResponse =
6021                 mAppSearchImpl.setSchema(
6022                         "package",
6023                         "database",
6024                         schemas,
6025                         /* visibilityConfigs= */ Collections.emptyList(),
6026                         /* forceOverride= */ false,
6027                         /* version= */ 0,
6028                         /* setSchemaStatsBuilder= */ null);
6029         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
6030         internalSetSchemaResponse =
6031                 mAppSearchImpl.setSchema(
6032                         "package2",
6033                         "database",
6034                         schemas,
6035                         /* visibilityConfigs= */ Collections.emptyList(),
6036                         /* forceOverride= */ false,
6037                         /* version= */ 0,
6038                         /* setSchemaStatsBuilder= */ null);
6039         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
6040         internalSetSchemaResponse =
6041                 mAppSearchImpl.setSchema(
6042                         "package3",
6043                         "database",
6044                         schemas,
6045                         /* visibilityConfigs= */ Collections.emptyList(),
6046                         /* forceOverride= */ false,
6047                         /* version= */ 0,
6048                         /* setSchemaStatsBuilder= */ null);
6049         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
6050 
6051         // Index a document
6052         mAppSearchImpl.putDocument(
6053                 "package",
6054                 "database",
6055                 new GenericDocument.Builder<>("namespace", "id1", "type").build(),
6056                 /* sendChangeNotifications= */ false,
6057                 /* logger= */ null);
6058 
6059         // We should still be able to index another document even though we are over the
6060         // getPerPackageDocumentCountLimit threshold.
6061         GenericDocument document2 =
6062                 new GenericDocument.Builder<>("namespace", "id2", "type").build();
6063         mAppSearchImpl.putDocument(
6064                 "package",
6065                 "database",
6066                 document2,
6067                 /* sendChangeNotifications= */ false,
6068                 /* logger= */ null);
6069 
6070         // Index a document in another package. We will now be at the limit start threshold.
6071         GenericDocument document3 =
6072                 new GenericDocument.Builder<>("namespace", "id3", "type").build();
6073         mAppSearchImpl.putDocument(
6074                 "package2",
6075                 "database",
6076                 document3,
6077                 /* sendChangeNotifications= */ false,
6078                 /* logger= */ null);
6079 
6080         // Both packages are at the maxPerPackageDocumentLimitCount and the limit is in force.
6081         // Neither should be able to add another document.
6082         GenericDocument document4 =
6083                 new GenericDocument.Builder<>("namespace", "id4", "type").build();
6084         AppSearchException e =
6085                 assertThrows(
6086                         AppSearchException.class,
6087                         () ->
6088                                 mAppSearchImpl.putDocument(
6089                                         "package",
6090                                         "database",
6091                                         document4,
6092                                         /* sendChangeNotifications= */ false,
6093                                         /* logger= */ null));
6094         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
6095         assertThat(e)
6096                 .hasMessageThat()
6097                 .contains("Package \"package\" exceeded limit of 1 documents");
6098 
6099         e =
6100                 assertThrows(
6101                         AppSearchException.class,
6102                         () ->
6103                                 mAppSearchImpl.putDocument(
6104                                         "package2",
6105                                         "database",
6106                                         document4,
6107                                         /* sendChangeNotifications= */ false,
6108                                         /* logger= */ null));
6109         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
6110         assertThat(e)
6111                 .hasMessageThat()
6112                 .contains("Package \"package2\" exceeded limit of 1 documents");
6113 
6114         // A new package should still be able to add a document however.
6115         mAppSearchImpl.putDocument(
6116                 "package3",
6117                 "database",
6118                 document4,
6119                 /* sendChangeNotifications= */ false,
6120                 /* logger= */ null);
6121     }
6122 
6123     @Test
testLimitConfig_replacement_doesntTriggerLimitStartThreshold()6124     public void testLimitConfig_replacement_doesntTriggerLimitStartThreshold() throws Exception {
6125         // Create a new mAppSearchImpl with a low limit, but a higher limit start threshold.
6126         mAppSearchImpl.close();
6127         File tempFolder = mTemporaryFolder.newFolder();
6128         mAppSearchImpl =
6129                 AppSearchImpl.create(
6130                         tempFolder,
6131                         new AppSearchConfigImpl(
6132                                 new LimitConfig() {
6133                                     @Override
6134                                     public int getMaxDocumentSizeBytes() {
6135                                         return Integer.MAX_VALUE;
6136                                     }
6137 
6138                                     @Override
6139                                     public int getPerPackageDocumentCountLimit() {
6140                                         return 1;
6141                                     }
6142 
6143                                     @Override
6144                                     public int getDocumentCountLimitStartThreshold() {
6145                                         return 3;
6146                                     }
6147 
6148                                     @Override
6149                                     public int getMaxSuggestionCount() {
6150                                         return Integer.MAX_VALUE;
6151                                     }
6152 
6153                                     @Override
6154                                     public int getMaxOpenBlobCount() {
6155                                         return Integer.MAX_VALUE;
6156                                     }
6157                                 },
6158                                 new LocalStorageIcingOptionsConfig()),
6159                         /* initStatsBuilder= */ null,
6160                         /* visibilityChecker= */ null,
6161                         /* revocableFileDescriptorStore= */ null,
6162                         ALWAYS_OPTIMIZE);
6163 
6164         // Insert schema
6165         List<AppSearchSchema> schemas =
6166                 Collections.singletonList(new AppSearchSchema.Builder("type").build());
6167         InternalSetSchemaResponse internalSetSchemaResponse =
6168                 mAppSearchImpl.setSchema(
6169                         "package",
6170                         "database",
6171                         schemas,
6172                         /* visibilityConfigs= */ Collections.emptyList(),
6173                         /* forceOverride= */ false,
6174                         /* version= */ 0,
6175                         /* setSchemaStatsBuilder= */ null);
6176         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
6177 
6178         // Index two documents
6179         mAppSearchImpl.putDocument(
6180                 "package",
6181                 "database",
6182                 new GenericDocument.Builder<>("namespace", "id1", "type").build(),
6183                 /* sendChangeNotifications= */ false,
6184                 /* logger= */ null);
6185 
6186         GenericDocument document2 =
6187                 new GenericDocument.Builder<>("namespace", "id2", "type").build();
6188         mAppSearchImpl.putDocument(
6189                 "package",
6190                 "database",
6191                 document2,
6192                 /* sendChangeNotifications= */ false,
6193                 /* logger= */ null);
6194 
6195         // Now Index a replacement. This should not trigger the DocumentCountLimitStartThreshold
6196         // because the total number of living documents should still be two.
6197         mAppSearchImpl.putDocument(
6198                 "package",
6199                 "database",
6200                 document2,
6201                 /* sendChangeNotifications= */ false,
6202                 /* logger= */ null);
6203 
6204         // We should be able to index one more document before triggering the limit.
6205         GenericDocument document3 =
6206                 new GenericDocument.Builder<>("namespace", "id3", "type").build();
6207         mAppSearchImpl.putDocument(
6208                 "package",
6209                 "database",
6210                 document3,
6211                 /* sendChangeNotifications= */ false,
6212                 /* logger= */ null);
6213     }
6214 
6215     @Test
testLimitConfig_remove_deactivatesDocumentCountLimit()6216     public void testLimitConfig_remove_deactivatesDocumentCountLimit() throws Exception {
6217         // Create a new mAppSearchImpl with a low limit, but a higher limit start threshold.
6218         mAppSearchImpl.close();
6219         File tempFolder = mTemporaryFolder.newFolder();
6220         mAppSearchImpl =
6221                 AppSearchImpl.create(
6222                         tempFolder,
6223                         new AppSearchConfigImpl(
6224                                 new LimitConfig() {
6225                                     @Override
6226                                     public int getMaxDocumentSizeBytes() {
6227                                         return Integer.MAX_VALUE;
6228                                     }
6229 
6230                                     @Override
6231                                     public int getPerPackageDocumentCountLimit() {
6232                                         return 1;
6233                                     }
6234 
6235                                     @Override
6236                                     public int getDocumentCountLimitStartThreshold() {
6237                                         return 3;
6238                                     }
6239 
6240                                     @Override
6241                                     public int getMaxSuggestionCount() {
6242                                         return Integer.MAX_VALUE;
6243                                     }
6244 
6245                                     @Override
6246                                     public int getMaxOpenBlobCount() {
6247                                         return Integer.MAX_VALUE;
6248                                     }
6249                                 },
6250                                 new LocalStorageIcingOptionsConfig()),
6251                         /* initStatsBuilder= */ null,
6252                         /* visibilityChecker= */ null,
6253                         /* revocableFileDescriptorStore= */ null,
6254                         ALWAYS_OPTIMIZE);
6255 
6256         // Insert schema
6257         List<AppSearchSchema> schemas =
6258                 Collections.singletonList(new AppSearchSchema.Builder("type").build());
6259         InternalSetSchemaResponse internalSetSchemaResponse =
6260                 mAppSearchImpl.setSchema(
6261                         "package",
6262                         "database",
6263                         schemas,
6264                         /* visibilityConfigs= */ Collections.emptyList(),
6265                         /* forceOverride= */ false,
6266                         /* version= */ 0,
6267                         /* setSchemaStatsBuilder= */ null);
6268         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
6269         internalSetSchemaResponse =
6270                 mAppSearchImpl.setSchema(
6271                         "package2",
6272                         "database",
6273                         schemas,
6274                         /* visibilityConfigs= */ Collections.emptyList(),
6275                         /* forceOverride= */ false,
6276                         /* version= */ 0,
6277                         /* setSchemaStatsBuilder= */ null);
6278         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
6279 
6280         // Index three documents in "package" and one in "package2". This will mean four total
6281         // documents in the system which will exceed the limit start threshold of three. The limit
6282         // will be in force and neither package will be able to documents.
6283         mAppSearchImpl.putDocument(
6284                 "package",
6285                 "database",
6286                 new GenericDocument.Builder<>("namespace", "id1", "type").build(),
6287                 /* sendChangeNotifications= */ false,
6288                 /* logger= */ null);
6289 
6290         GenericDocument document2 =
6291                 new GenericDocument.Builder<>("namespace", "id2", "type").build();
6292         mAppSearchImpl.putDocument(
6293                 "package",
6294                 "database",
6295                 document2,
6296                 /* sendChangeNotifications= */ false,
6297                 /* logger= */ null);
6298 
6299         GenericDocument document3 =
6300                 new GenericDocument.Builder<>("namespace", "id3", "type").build();
6301         mAppSearchImpl.putDocument(
6302                 "package",
6303                 "database",
6304                 document3,
6305                 /* sendChangeNotifications= */ false,
6306                 /* logger= */ null);
6307 
6308         GenericDocument document4 =
6309                 new GenericDocument.Builder<>("namespace", "id4", "type").build();
6310         mAppSearchImpl.putDocument(
6311                 "package2",
6312                 "database",
6313                 document4,
6314                 /* sendChangeNotifications= */ false,
6315                 /* logger= */ null);
6316 
6317         // The limit is in force. We should be unable to index another document. Even after we
6318         // delete one document, the system is still over the limit start threshold.
6319         GenericDocument document5 =
6320                 new GenericDocument.Builder<>("namespace", "id5", "type").build();
6321         AppSearchException e =
6322                 assertThrows(
6323                         AppSearchException.class,
6324                         () ->
6325                                 mAppSearchImpl.putDocument(
6326                                         "package",
6327                                         "database",
6328                                         document5,
6329                                         /* sendChangeNotifications= */ false,
6330                                         /* logger= */ null));
6331         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
6332         assertThat(e)
6333                 .hasMessageThat()
6334                 .contains("Package \"package\" exceeded limit of 1 documents");
6335 
6336         mAppSearchImpl.remove(
6337                 "package", "database", "namespace", "id2", /* removeStatsBuilder= */ null);
6338         e =
6339                 assertThrows(
6340                         AppSearchException.class,
6341                         () ->
6342                                 mAppSearchImpl.putDocument(
6343                                         "package",
6344                                         "database",
6345                                         document5,
6346                                         /* sendChangeNotifications= */ false,
6347                                         /* logger= */ null));
6348         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
6349         assertThat(e)
6350                 .hasMessageThat()
6351                 .contains("Package \"package\" exceeded limit of 1 documents");
6352 
6353         // Removing another document will bring the system below the limit start threshold. Now,
6354         // adding another document can succeed.
6355         mAppSearchImpl.remove(
6356                 "package", "database", "namespace", "id3", /* removeStatsBuilder= */ null);
6357         mAppSearchImpl.putDocument(
6358                 "package",
6359                 "database",
6360                 document5,
6361                 /* sendChangeNotifications= */ false,
6362                 /* logger= */ null);
6363     }
6364 
6365     @Test
testLimitConfig_removeByQuery_deactivatesDocumentCountLimit()6366     public void testLimitConfig_removeByQuery_deactivatesDocumentCountLimit() throws Exception {
6367         // Create a new mAppSearchImpl with a low limit, but a higher limit start threshold.
6368         mAppSearchImpl.close();
6369         File tempFolder = mTemporaryFolder.newFolder();
6370         mAppSearchImpl =
6371                 AppSearchImpl.create(
6372                         tempFolder,
6373                         new AppSearchConfigImpl(
6374                                 new LimitConfig() {
6375                                     @Override
6376                                     public int getMaxDocumentSizeBytes() {
6377                                         return Integer.MAX_VALUE;
6378                                     }
6379 
6380                                     @Override
6381                                     public int getPerPackageDocumentCountLimit() {
6382                                         return 1;
6383                                     }
6384 
6385                                     @Override
6386                                     public int getDocumentCountLimitStartThreshold() {
6387                                         return 3;
6388                                     }
6389 
6390                                     @Override
6391                                     public int getMaxSuggestionCount() {
6392                                         return Integer.MAX_VALUE;
6393                                     }
6394 
6395                                     @Override
6396                                     public int getMaxOpenBlobCount() {
6397                                         return Integer.MAX_VALUE;
6398                                     }
6399                                 },
6400                                 new LocalStorageIcingOptionsConfig()),
6401                         /* initStatsBuilder= */ null,
6402                         /* visibilityChecker= */ null,
6403                         /* revocableFileDescriptorStore= */ null,
6404                         ALWAYS_OPTIMIZE);
6405 
6406         // Insert schema
6407         AppSearchSchema schema =
6408                 new AppSearchSchema.Builder("type")
6409                         .addProperty(
6410                                 new AppSearchSchema.StringPropertyConfig.Builder("number")
6411                                         .setIndexingType(
6412                                                 AppSearchSchema.StringPropertyConfig
6413                                                         .INDEXING_TYPE_PREFIXES)
6414                                         .setTokenizerType(
6415                                                 AppSearchSchema.StringPropertyConfig
6416                                                         .TOKENIZER_TYPE_PLAIN)
6417                                         .build())
6418                         .addProperty(
6419                                 new AppSearchSchema.StringPropertyConfig.Builder("evenOdd")
6420                                         .setIndexingType(
6421                                                 AppSearchSchema.StringPropertyConfig
6422                                                         .INDEXING_TYPE_PREFIXES)
6423                                         .setTokenizerType(
6424                                                 AppSearchSchema.StringPropertyConfig
6425                                                         .TOKENIZER_TYPE_PLAIN)
6426                                         .build())
6427                         .build();
6428         List<AppSearchSchema> schemas = Collections.singletonList(schema);
6429 
6430         InternalSetSchemaResponse internalSetSchemaResponse =
6431                 mAppSearchImpl.setSchema(
6432                         "package",
6433                         "database",
6434                         schemas,
6435                         /* visibilityConfigs= */ Collections.emptyList(),
6436                         /* forceOverride= */ false,
6437                         /* version= */ 0,
6438                         /* setSchemaStatsBuilder= */ null);
6439         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
6440         internalSetSchemaResponse =
6441                 mAppSearchImpl.setSchema(
6442                         "package2",
6443                         "database",
6444                         schemas,
6445                         /* visibilityConfigs= */ Collections.emptyList(),
6446                         /* forceOverride= */ false,
6447                         /* version= */ 0,
6448                         /* setSchemaStatsBuilder= */ null);
6449         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
6450 
6451         // Index three documents in "package" and one in "package2". This will mean four total
6452         // documents in the system which will exceed the limit start threshold of three. The limit
6453         // will be in force and neither package will be able to documents.
6454         GenericDocument document1 =
6455                 new GenericDocument.Builder<>("namespace", "id1", "type")
6456                         .setPropertyString("number", "first")
6457                         .setPropertyString("evenOdd", "odd")
6458                         .build();
6459         mAppSearchImpl.putDocument(
6460                 "package",
6461                 "database",
6462                 document1,
6463                 /* sendChangeNotifications= */ false,
6464                 /* logger= */ null);
6465 
6466         GenericDocument document2 =
6467                 new GenericDocument.Builder<>("namespace", "id2", "type")
6468                         .setPropertyString("number", "second")
6469                         .setPropertyString("evenOdd", "even")
6470                         .build();
6471         mAppSearchImpl.putDocument(
6472                 "package",
6473                 "database",
6474                 document2,
6475                 /* sendChangeNotifications= */ false,
6476                 /* logger= */ null);
6477 
6478         GenericDocument document3 =
6479                 new GenericDocument.Builder<>("namespace", "id3", "type")
6480                         .setPropertyString("number", "third")
6481                         .setPropertyString("evenOdd", "odd")
6482                         .build();
6483         mAppSearchImpl.putDocument(
6484                 "package",
6485                 "database",
6486                 document3,
6487                 /* sendChangeNotifications= */ false,
6488                 /* logger= */ null);
6489 
6490         GenericDocument document4 =
6491                 new GenericDocument.Builder<>("namespace", "id4", "type")
6492                         .setPropertyString("number", "fourth")
6493                         .setPropertyString("evenOdd", "even")
6494                         .build();
6495         mAppSearchImpl.putDocument(
6496                 "package2",
6497                 "database",
6498                 document4,
6499                 /* sendChangeNotifications= */ false,
6500                 /* logger= */ null);
6501 
6502         // The limit is in force. We should be unable to index another document.
6503         GenericDocument document5 =
6504                 new GenericDocument.Builder<>("namespace", "id5", "type")
6505                         .setPropertyString("number", "five")
6506                         .setPropertyString("evenOdd", "odd")
6507                         .build();
6508         AppSearchException e =
6509                 assertThrows(
6510                         AppSearchException.class,
6511                         () ->
6512                                 mAppSearchImpl.putDocument(
6513                                         "package",
6514                                         "database",
6515                                         document5,
6516                                         /* sendChangeNotifications= */ false,
6517                                         /* logger= */ null));
6518         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
6519         assertThat(e)
6520                 .hasMessageThat()
6521                 .contains("Package \"package\" exceeded limit of 1 documents");
6522 
6523         // Remove two documents by query. Now we should be under the limit and be able to add
6524         // another document.
6525         mAppSearchImpl.removeByQuery(
6526                 "package",
6527                 "database",
6528                 "evenOdd:odd",
6529                 new SearchSpec.Builder().build(),
6530                 /* removeStatsBuilder= */ null);
6531         mAppSearchImpl.putDocument(
6532                 "package",
6533                 "database",
6534                 document5,
6535                 /* sendChangeNotifications= */ false,
6536                 /* logger= */ null);
6537     }
6538 
6539     @Test
6540     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
testLimitConfig_activeWriteFds()6541     public void testLimitConfig_activeWriteFds() throws Exception {
6542         mAppSearchImpl.close();
6543         File tempFolder = mTemporaryFolder.newFolder();
6544         AppSearchConfig config =
6545                 new AppSearchConfigImpl(
6546                         new LimitConfig() {
6547                             @Override
6548                             public int getMaxDocumentSizeBytes() {
6549                                 return Integer.MAX_VALUE;
6550                             }
6551 
6552                             @Override
6553                             public int getPerPackageDocumentCountLimit() {
6554                                 return Integer.MAX_VALUE;
6555                             }
6556 
6557                             @Override
6558                             public int getDocumentCountLimitStartThreshold() {
6559                                 return Integer.MAX_VALUE;
6560                             }
6561 
6562                             @Override
6563                             public int getMaxSuggestionCount() {
6564                                 return Integer.MAX_VALUE;
6565                             }
6566 
6567                             @Override
6568                             public int getMaxOpenBlobCount() {
6569                                 return 2;
6570                             }
6571                         },
6572                         new LocalStorageIcingOptionsConfig());
6573         mAppSearchImpl =
6574                 AppSearchImpl.create(
6575                         tempFolder,
6576                         config,
6577                         /* initStatsBuilder= */ null,
6578                         /* visibilityChecker= */ null,
6579                         new JetpackRevocableFileDescriptorStore(config),
6580                         ALWAYS_OPTIMIZE);
6581         // We could open only 2 fds per package.
6582         byte[] data1 = generateRandomBytes(20 * 1024); // 20 KiB
6583         byte[] digest1 = calculateDigest(data1);
6584         AppSearchBlobHandle handle1 =
6585                 AppSearchBlobHandle.createWithSha256(digest1, "package", "db1", "ns");
6586         ParcelFileDescriptor writer1 = mAppSearchImpl.openWriteBlob("package", "db1", handle1);
6587 
6588         byte[] data2 = generateRandomBytes(20 * 1024); // 20 KiB
6589         byte[] digest2 = calculateDigest(data2);
6590         AppSearchBlobHandle handle2 =
6591                 AppSearchBlobHandle.createWithSha256(digest2, "package", "db1", "ns");
6592         ParcelFileDescriptor writer2 = mAppSearchImpl.openWriteBlob("package", "db1", handle2);
6593 
6594         // Open 3rd fd will fail.
6595         byte[] data3 = generateRandomBytes(20 * 1024); // 20 KiB
6596         byte[] digest3 = calculateDigest(data3);
6597         AppSearchBlobHandle handle3 =
6598                 AppSearchBlobHandle.createWithSha256(digest3, "package", "db1", "ns");
6599         AppSearchException e =
6600                 assertThrows(
6601                         AppSearchException.class,
6602                         () -> mAppSearchImpl.openWriteBlob("package", "db1", handle3));
6603         assertThat(e.getResultCode()).isEqualTo(RESULT_OUT_OF_SPACE);
6604         assertThat(e)
6605                 .hasMessageThat()
6606                 .contains(
6607                         "Package \"package\" exceeded limit of 2 opened file descriptors. "
6608                                 + "Some file descriptors must be closed to open additional ones.");
6609     }
6610 
6611     @Test
6612     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
testLimitConfig_activeReadFds()6613     public void testLimitConfig_activeReadFds() throws Exception {
6614         mAppSearchImpl.close();
6615         File tempFolder = mTemporaryFolder.newFolder();
6616         AppSearchConfig config =
6617                 new AppSearchConfigImpl(
6618                         new LimitConfig() {
6619                             @Override
6620                             public int getMaxDocumentSizeBytes() {
6621                                 return Integer.MAX_VALUE;
6622                             }
6623 
6624                             @Override
6625                             public int getPerPackageDocumentCountLimit() {
6626                                 return Integer.MAX_VALUE;
6627                             }
6628 
6629                             @Override
6630                             public int getDocumentCountLimitStartThreshold() {
6631                                 return Integer.MAX_VALUE;
6632                             }
6633 
6634                             @Override
6635                             public int getMaxSuggestionCount() {
6636                                 return Integer.MAX_VALUE;
6637                             }
6638 
6639                             @Override
6640                             public int getMaxOpenBlobCount() {
6641                                 return 2;
6642                             }
6643                         },
6644                         new LocalStorageIcingOptionsConfig());
6645         mAppSearchImpl =
6646                 AppSearchImpl.create(
6647                         tempFolder,
6648                         config,
6649                         /* initStatsBuilder= */ null,
6650                         /* visibilityChecker= */ null,
6651                         new JetpackRevocableFileDescriptorStore(config),
6652                         ALWAYS_OPTIMIZE);
6653 
6654         // Write and commit one blob
6655         byte[] data = generateRandomBytes(20 * 1024); // 20 KiB
6656         byte[] digest = calculateDigest(data);
6657         AppSearchBlobHandle handle =
6658                 AppSearchBlobHandle.createWithSha256(
6659                         digest, mContext.getPackageName(), "db1", "ns");
6660         try (ParcelFileDescriptor writePfd =
6661                         mAppSearchImpl.openWriteBlob(mContext.getPackageName(), "db1", handle);
6662                 OutputStream outputStream =
6663                         new ParcelFileDescriptor.AutoCloseOutputStream(writePfd)) {
6664             outputStream.write(data);
6665             outputStream.flush();
6666         }
6667         mAppSearchImpl.commitBlob(mContext.getPackageName(), "db1", handle);
6668 
6669         ParcelFileDescriptor reader1 =
6670                 mAppSearchImpl.openReadBlob(mContext.getPackageName(), "db1", handle);
6671         ParcelFileDescriptor reader2 =
6672                 mAppSearchImpl.openReadBlob(mContext.getPackageName(), "db1", handle);
6673         // Open 3rd fd will fail.
6674         AppSearchException e =
6675                 assertThrows(
6676                         AppSearchException.class,
6677                         () ->
6678                                 mAppSearchImpl.openReadBlob(
6679                                         mContext.getPackageName(), "db1", handle));
6680         assertThat(e.getResultCode()).isEqualTo(RESULT_OUT_OF_SPACE);
6681         assertThat(e)
6682                 .hasMessageThat()
6683                 .contains(
6684                         "Package \""
6685                                 + mContext.getPackageName()
6686                                 + "\" exceeded limit of 2 opened file descriptors. Some file"
6687                                 + " descriptors must be closed to open additional ones.");
6688 
6689         // Close 1st fd and open 3rd fd will success
6690         reader1.close();
6691         ParcelFileDescriptor reader3 =
6692                 mAppSearchImpl.openReadBlob(mContext.getPackageName(), "db1", handle);
6693 
6694         // GlobalOpenRead will share same limit.
6695         e =
6696                 assertThrows(
6697                         AppSearchException.class,
6698                         () -> mAppSearchImpl.globalOpenReadBlob(handle, mSelfCallerAccess));
6699         assertThat(e.getResultCode()).isEqualTo(RESULT_OUT_OF_SPACE);
6700         assertThat(e)
6701                 .hasMessageThat()
6702                 .contains(
6703                         "Package \""
6704                                 + mContext.getPackageName()
6705                                 + "\" exceeded limit of 2 opened file descriptors. Some file"
6706                                 + " descriptors must be closed to open additional ones.");
6707         // Close 2st fd and global open fd will success
6708         reader2.close();
6709         ParcelFileDescriptor reader4 = mAppSearchImpl.globalOpenReadBlob(handle, mSelfCallerAccess);
6710 
6711         // Keep opening will fail
6712         e =
6713                 assertThrows(
6714                         AppSearchException.class,
6715                         () ->
6716                                 mAppSearchImpl.openReadBlob(
6717                                         mContext.getPackageName(), "db1", handle));
6718         assertThat(e.getResultCode()).isEqualTo(RESULT_OUT_OF_SPACE);
6719         assertThat(e)
6720                 .hasMessageThat()
6721                 .contains(
6722                         "Package \""
6723                                 + mContext.getPackageName()
6724                                 + "\" exceeded limit of 2 opened file descriptors. Some file"
6725                                 + " descriptors must be closed to open additional ones.");
6726         e =
6727                 assertThrows(
6728                         AppSearchException.class,
6729                         () -> mAppSearchImpl.globalOpenReadBlob(handle, mSelfCallerAccess));
6730         assertThat(e.getResultCode()).isEqualTo(RESULT_OUT_OF_SPACE);
6731         assertThat(e)
6732                 .hasMessageThat()
6733                 .contains(
6734                         "Package \""
6735                                 + mContext.getPackageName()
6736                                 + "\" exceeded limit of 2 opened file descriptors. Some file"
6737                                 + " descriptors must be closed to open additional ones.");
6738 
6739         reader3.close();
6740         reader4.close();
6741     }
6742 
6743     /**
6744      * Ensure that it is okay to register the same observer for multiple packages and that removing
6745      * the observer for one package doesn't remove it for the other.
6746      */
6747     @Test
testRemoveObserver_onlyAffectsOnePackage()6748     public void testRemoveObserver_onlyAffectsOnePackage() throws Exception {
6749         final String fakePackage = "com.android.appsearch.fake.package";
6750 
6751         InternalSetSchemaResponse internalSetSchemaResponse =
6752                 mAppSearchImpl.setSchema(
6753                         mContext.getPackageName(),
6754                         "database1",
6755                         /* schemas= */ ImmutableList.of(
6756                                 new AppSearchSchema.Builder("Type1").build()),
6757                         /* visibilityConfigs= */ Collections.emptyList(),
6758                         /* forceOverride= */ false,
6759                         /* version= */ 0,
6760                         /* setSchemaStatsBuilder= */ null);
6761         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
6762 
6763         // Register an observer twice, on different packages.
6764         TestObserverCallback observer = new TestObserverCallback();
6765         mAppSearchImpl.registerObserverCallback(
6766                 /* listeningPackageAccess= */ mSelfCallerAccess,
6767                 /* targetPackageName= */ mContext.getPackageName(),
6768                 new ObserverSpec.Builder().build(),
6769                 MoreExecutors.directExecutor(),
6770                 observer);
6771         mAppSearchImpl.registerObserverCallback(
6772                 /* listeningPackageAccess= */ mSelfCallerAccess,
6773                 /* targetPackageName= */ fakePackage,
6774                 new ObserverSpec.Builder().build(),
6775                 MoreExecutors.directExecutor(),
6776                 observer);
6777 
6778         // Insert a valid doc
6779         GenericDocument validDoc =
6780                 new GenericDocument.Builder<>("namespace1", "id1", "Type1").build();
6781         assertThat(observer.getSchemaChanges()).isEmpty();
6782         assertThat(observer.getDocumentChanges()).isEmpty();
6783         mAppSearchImpl.putDocument(
6784                 mContext.getPackageName(),
6785                 "database1",
6786                 validDoc,
6787                 /* sendChangeNotifications= */ true,
6788                 /* logger= */ null);
6789 
6790         // Dispatch notifications and empty the observers
6791         mAppSearchImpl.dispatchAndClearChangeNotifications();
6792         observer.clear();
6793 
6794         // Remove the observer from the fake package
6795         mAppSearchImpl.unregisterObserverCallback(fakePackage, observer);
6796 
6797         // Index a second document
6798         GenericDocument doc2 = new GenericDocument.Builder<>("namespace1", "id2", "Type1").build();
6799         mAppSearchImpl.putDocument(
6800                 mContext.getPackageName(),
6801                 "database1",
6802                 doc2,
6803                 /* sendChangeNotifications= */ true,
6804                 /* logger= */ null);
6805 
6806         // Observer should still have received this data from its registration on
6807         // context.getPackageName(), as we only removed the copy from fakePackage.
6808         mAppSearchImpl.dispatchAndClearChangeNotifications();
6809         assertThat(observer.getSchemaChanges()).isEmpty();
6810         assertThat(observer.getDocumentChanges())
6811                 .containsExactly(
6812                         new DocumentChangeInfo(
6813                                 mContext.getPackageName(),
6814                                 "database1",
6815                                 "namespace1",
6816                                 "Type1",
6817                                 /* changedDocumentIds= */ ImmutableSet.of("id2")));
6818     }
6819 
6820     @Test
testGetGlobalDocumentThrowsExceptionWhenNotVisible()6821     public void testGetGlobalDocumentThrowsExceptionWhenNotVisible() throws Exception {
6822         List<AppSearchSchema> schemas =
6823                 Collections.singletonList(new AppSearchSchema.Builder("type").build());
6824 
6825         // Create a new mAppSearchImpl with a mock Visibility Checker
6826         mAppSearchImpl.close();
6827         File tempFolder = mTemporaryFolder.newFolder();
6828         VisibilityChecker mockVisibilityChecker = createMockVisibilityChecker(false);
6829         mAppSearchImpl =
6830                 AppSearchImpl.create(
6831                         tempFolder,
6832                         new AppSearchConfigImpl(
6833                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
6834                         /* initStatsBuilder= */ null,
6835                         mockVisibilityChecker,
6836                         /* revocableFileDescriptorStore= */ null,
6837                         ALWAYS_OPTIMIZE);
6838 
6839         InternalSetSchemaResponse internalSetSchemaResponse =
6840                 mAppSearchImpl.setSchema(
6841                         "package",
6842                         "database",
6843                         schemas,
6844                         /* visibilityConfigs= */ Collections.emptyList(),
6845                         /* forceOverride= */ false,
6846                         /* version= */ 0,
6847                         /* setSchemaStatsBuilder= */ null);
6848         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
6849 
6850         // Add a document and persist it.
6851         GenericDocument document =
6852                 new GenericDocument.Builder<>("namespace1", "id1", "type").build();
6853         mAppSearchImpl.putDocument(
6854                 "package",
6855                 "database",
6856                 document,
6857                 /* sendChangeNotifications= */ false,
6858                 /* logger= */ null);
6859         mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
6860 
6861         AppSearchException e =
6862                 assertThrows(
6863                         AppSearchException.class,
6864                         () ->
6865                                 mAppSearchImpl.globalGetDocument(
6866                                         "package",
6867                                         "database",
6868                                         "namespace1",
6869                                         "id1",
6870                                         /* typePropertyPaths= */ Collections.emptyMap(),
6871                                         /* callerAccess= */ mSelfCallerAccess));
6872         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6873         assertThat(e.getMessage()).isEqualTo("Document (namespace1, id1) not found.");
6874     }
6875 
6876     @Test
testGetGlobalDocument()6877     public void testGetGlobalDocument() throws Exception {
6878         List<AppSearchSchema> schemas =
6879                 Collections.singletonList(new AppSearchSchema.Builder("type").build());
6880 
6881         // Create a new mAppSearchImpl with a mock Visibility Checker
6882         mAppSearchImpl.close();
6883         File tempFolder = mTemporaryFolder.newFolder();
6884         VisibilityChecker mockVisibilityChecker = createMockVisibilityChecker(true);
6885         mAppSearchImpl =
6886                 AppSearchImpl.create(
6887                         tempFolder,
6888                         new AppSearchConfigImpl(
6889                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
6890                         /* initStatsBuilder= */ null,
6891                         mockVisibilityChecker,
6892                         /* revocableFileDescriptorStore= */ null,
6893                         ALWAYS_OPTIMIZE);
6894 
6895         InternalSetSchemaResponse internalSetSchemaResponse =
6896                 mAppSearchImpl.setSchema(
6897                         "package",
6898                         "database",
6899                         schemas,
6900                         /* visibilityConfigs= */ Collections.emptyList(),
6901                         /* forceOverride= */ false,
6902                         /* version= */ 0,
6903                         /* setSchemaStatsBuilder= */ null);
6904         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
6905 
6906         // Add a document and persist it.
6907         GenericDocument document =
6908                 new GenericDocument.Builder<>("namespace1", "id1", "type").build();
6909         mAppSearchImpl.putDocument(
6910                 "package",
6911                 "database",
6912                 document,
6913                 /* sendChangeNotifications= */ false,
6914                 /* logger= */ null);
6915         mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
6916 
6917         GenericDocument getResult =
6918                 mAppSearchImpl.globalGetDocument(
6919                         "package",
6920                         "database",
6921                         "namespace1",
6922                         "id1",
6923                         /* typePropertyPaths= */ Collections.emptyMap(),
6924                         /* callerAccess= */ mSelfCallerAccess);
6925         assertThat(getResult).isEqualTo(document);
6926     }
6927 
6928     @Test
getGlobalDocumentTest_notFound()6929     public void getGlobalDocumentTest_notFound() throws Exception {
6930         List<AppSearchSchema> schemas =
6931                 Collections.singletonList(new AppSearchSchema.Builder("type").build());
6932 
6933         // Create a new mAppSearchImpl with a mock Visibility Checker
6934         mAppSearchImpl.close();
6935         File tempFolder = mTemporaryFolder.newFolder();
6936         VisibilityChecker mockVisibilityChecker = createMockVisibilityChecker(true);
6937         mAppSearchImpl =
6938                 AppSearchImpl.create(
6939                         tempFolder,
6940                         new AppSearchConfigImpl(
6941                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
6942                         /* initStatsBuilder= */ null,
6943                         mockVisibilityChecker,
6944                         /* revocableFileDescriptorStore= */ null,
6945                         ALWAYS_OPTIMIZE);
6946 
6947         InternalSetSchemaResponse internalSetSchemaResponse =
6948                 mAppSearchImpl.setSchema(
6949                         "package",
6950                         "database",
6951                         schemas,
6952                         /* visibilityConfigs= */ Collections.emptyList(),
6953                         /* forceOverride= */ false,
6954                         /* version= */ 0,
6955                         /* setSchemaStatsBuilder= */ null);
6956         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
6957 
6958         // Add a document and persist it.
6959         GenericDocument document =
6960                 new GenericDocument.Builder<>("namespace1", "id1", "type").build();
6961         mAppSearchImpl.putDocument(
6962                 "package",
6963                 "database",
6964                 document,
6965                 /* sendChangeNotifications= */ false,
6966                 /* logger= */ null);
6967         mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
6968 
6969         AppSearchException e =
6970                 assertThrows(
6971                         AppSearchException.class,
6972                         () ->
6973                                 mAppSearchImpl.globalGetDocument(
6974                                         "package",
6975                                         "database",
6976                                         "namespace1",
6977                                         "id2",
6978                                         /* typePropertyPaths= */ Collections.emptyMap(),
6979                                         /* callerAccess= */ mSelfCallerAccess));
6980         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
6981         assertThat(e.getMessage()).isEqualTo("Document (namespace1, id2) not found.");
6982     }
6983 
6984     @Test
getGlobalDocumentNoAccessNoFileHasSameException()6985     public void getGlobalDocumentNoAccessNoFileHasSameException() throws Exception {
6986         List<AppSearchSchema> schemas =
6987                 Collections.singletonList(new AppSearchSchema.Builder("type").build());
6988         // Create a new mAppSearchImpl with a mock Visibility Checker
6989         mAppSearchImpl.close();
6990         File tempFolder = mTemporaryFolder.newFolder();
6991         VisibilityChecker mockVisibilityChecker =
6992                 new VisibilityChecker() {
6993                     @Override
6994                     public boolean isSchemaSearchableByCaller(
6995                             @NonNull CallerAccess callerAccess,
6996                             @NonNull String packageName,
6997                             @NonNull String prefixedSchema,
6998                             @NonNull VisibilityStore visibilityStore) {
6999                         return callerAccess.getCallingPackageName().equals("visiblePackage");
7000                     }
7001 
7002                     @Override
7003                     public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
7004                         return false;
7005                     }
7006                 };
7007 
7008         mAppSearchImpl =
7009                 AppSearchImpl.create(
7010                         tempFolder,
7011                         new AppSearchConfigImpl(
7012                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
7013                         /* initStatsBuilder= */ null,
7014                         mockVisibilityChecker,
7015                         /* revocableFileDescriptorStore= */ null,
7016                         ALWAYS_OPTIMIZE);
7017 
7018         InternalSetSchemaResponse internalSetSchemaResponse =
7019                 mAppSearchImpl.setSchema(
7020                         "package",
7021                         "database",
7022                         schemas,
7023                         /* visibilityConfigs= */ Collections.emptyList(),
7024                         /* forceOverride= */ false,
7025                         /* version= */ 0,
7026                         /* setSchemaStatsBuilder= */ null);
7027         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
7028 
7029         // Add a document and persist it.
7030         GenericDocument document =
7031                 new GenericDocument.Builder<>("namespace1", "id1", "type").build();
7032         mAppSearchImpl.putDocument(
7033                 "package",
7034                 "database",
7035                 document,
7036                 /* sendChangeNotifications= */ false,
7037                 /* logger= */ null);
7038         mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
7039 
7040         AppSearchException unauthorizedException =
7041                 assertThrows(
7042                         AppSearchException.class,
7043                         () ->
7044                                 mAppSearchImpl.globalGetDocument(
7045                                         "package",
7046                                         "database",
7047                                         "namespace1",
7048                                         "id1",
7049                                         /* typePropertyPaths= */ Collections.emptyMap(),
7050                                         new CallerAccess(
7051                                                 /* callingPackageName= */ "invisiblePackage")));
7052 
7053         mAppSearchImpl.remove(
7054                 "package", "database", "namespace1", "id1", /* removeStatsBuilder= */ null);
7055 
7056         AppSearchException noDocException =
7057                 assertThrows(
7058                         AppSearchException.class,
7059                         () ->
7060                                 mAppSearchImpl.globalGetDocument(
7061                                         "package",
7062                                         "database",
7063                                         "namespace1",
7064                                         "id1",
7065                                         /* typePropertyPaths= */ Collections.emptyMap(),
7066                                         new CallerAccess(
7067                                                 /* callingPackageName= */ "visiblePackage")));
7068 
7069         assertThat(noDocException.getResultCode()).isEqualTo(unauthorizedException.getResultCode());
7070         assertThat(noDocException.getMessage()).isEqualTo(unauthorizedException.getMessage());
7071     }
7072 
7073     @Test
testSetVisibility()7074     public void testSetVisibility() throws Exception {
7075         InternalVisibilityConfig visibilityConfig =
7076                 new InternalVisibilityConfig.Builder("Email")
7077                         .setNotDisplayedBySystem(true)
7078                         .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
7079                         .build();
7080         List<AppSearchSchema> schemas =
7081                 Collections.singletonList(new AppSearchSchema.Builder("Email").build());
7082 
7083         // Set schema Email to AppSearch database1 with a visibility document
7084         InternalSetSchemaResponse internalSetSchemaResponse =
7085                 mAppSearchImpl.setSchema(
7086                         "package",
7087                         "database1",
7088                         schemas,
7089                         /* visibilityConfigs= */ ImmutableList.of(visibilityConfig),
7090                         /* forceOverride= */ false,
7091                         /* version= */ 0,
7092                         /* setSchemaStatsBuilder= */ null);
7093         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
7094         String prefix = PrefixUtil.createPrefix("package", "database1");
7095 
7096         // assert the visibility document is saved.
7097         InternalVisibilityConfig expectedDocument =
7098                 new InternalVisibilityConfig.Builder(prefix + "Email")
7099                         .setNotDisplayedBySystem(true)
7100                         .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
7101                         .build();
7102         assertThat(mAppSearchImpl.mDocumentVisibilityStoreLocked.getVisibility(prefix + "Email"))
7103                 .isEqualTo(expectedDocument);
7104         // Verify the InternalVisibilityConfig is saved to AppSearchImpl.
7105         InternalVisibilityConfig actualDocument =
7106                 VisibilityToDocumentConverter.createInternalVisibilityConfig(
7107                         mAppSearchImpl.getDocument(
7108                                 VISIBILITY_PACKAGE_NAME,
7109                                 DOCUMENT_VISIBILITY_DATABASE_NAME,
7110                                 VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
7111                                 /* id= */ prefix + "Email",
7112                                 /* typePropertyPaths= */ Collections.emptyMap()),
7113                         /* androidVOverlayDocument= */ null);
7114         assertThat(actualDocument).isEqualTo(expectedDocument);
7115     }
7116 
7117     @Test
testSetVisibility_existingVisibilitySettingRetains()7118     public void testSetVisibility_existingVisibilitySettingRetains() throws Exception {
7119         // Create Visibility Document for Email1
7120         InternalVisibilityConfig visibilityConfig1 =
7121                 new InternalVisibilityConfig.Builder("Email1")
7122                         .setNotDisplayedBySystem(true)
7123                         .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
7124                         .build();
7125         List<AppSearchSchema> schemas1 =
7126                 Collections.singletonList(new AppSearchSchema.Builder("Email1").build());
7127 
7128         // Set schema Email1 to package1 with a visibility document
7129         InternalSetSchemaResponse internalSetSchemaResponse =
7130                 mAppSearchImpl.setSchema(
7131                         "package1",
7132                         "database",
7133                         schemas1,
7134                         /* visibilityConfigs= */ ImmutableList.of(visibilityConfig1),
7135                         /* forceOverride= */ false,
7136                         /* version= */ 0,
7137                         /* setSchemaStatsBuilder= */ null);
7138         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
7139         String prefix1 = PrefixUtil.createPrefix("package1", "database");
7140 
7141         // assert the visibility document is saved.
7142         InternalVisibilityConfig expectedDocument1 =
7143                 new InternalVisibilityConfig.Builder(prefix1 + "Email1")
7144                         .setNotDisplayedBySystem(true)
7145                         .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
7146                         .build();
7147         assertThat(mAppSearchImpl.mDocumentVisibilityStoreLocked.getVisibility(prefix1 + "Email1"))
7148                 .isEqualTo(expectedDocument1);
7149         // Verify the InternalVisibilityConfig is saved to AppSearchImpl.
7150         InternalVisibilityConfig actualDocument1 =
7151                 VisibilityToDocumentConverter.createInternalVisibilityConfig(
7152                         mAppSearchImpl.getDocument(
7153                                 VISIBILITY_PACKAGE_NAME,
7154                                 DOCUMENT_VISIBILITY_DATABASE_NAME,
7155                                 VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
7156                                 /* id= */ prefix1 + "Email1",
7157                                 /* typePropertyPaths= */ Collections.emptyMap()),
7158                         /* androidVOverlayDocument= */ null);
7159 
7160         assertThat(actualDocument1).isEqualTo(expectedDocument1);
7161 
7162         // Create Visibility Document for Email2
7163         InternalVisibilityConfig visibilityConfig2 =
7164                 new InternalVisibilityConfig.Builder("Email2")
7165                         .setNotDisplayedBySystem(false)
7166                         .addVisibleToPackage(new PackageIdentifier("pkgFoo", new byte[32]))
7167                         .build();
7168         List<AppSearchSchema> schemas2 =
7169                 Collections.singletonList(new AppSearchSchema.Builder("Email2").build());
7170 
7171         // Set schema Email2 to package1 with a visibility document
7172         internalSetSchemaResponse =
7173                 mAppSearchImpl.setSchema(
7174                         "package2",
7175                         "database",
7176                         schemas2,
7177                         /* visibilityConfigs= */ ImmutableList.of(visibilityConfig2),
7178                         /* forceOverride= */ false,
7179                         /* version= */ 0,
7180                         /* setSchemaStatsBuilder= */ null);
7181         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
7182         String prefix2 = PrefixUtil.createPrefix("package2", "database");
7183 
7184         // assert the visibility document is saved.
7185         InternalVisibilityConfig expectedDocument2 =
7186                 new InternalVisibilityConfig.Builder(prefix2 + "Email2")
7187                         .setNotDisplayedBySystem(false)
7188                         .addVisibleToPackage(new PackageIdentifier("pkgFoo", new byte[32]))
7189                         .build();
7190         assertThat(mAppSearchImpl.mDocumentVisibilityStoreLocked.getVisibility(prefix2 + "Email2"))
7191                 .isEqualTo(expectedDocument2);
7192         // Verify the InternalVisibilityConfig is saved to AppSearchImpl.
7193         InternalVisibilityConfig actualDocument2 =
7194                 VisibilityToDocumentConverter.createInternalVisibilityConfig(
7195                         mAppSearchImpl.getDocument(
7196                                 VISIBILITY_PACKAGE_NAME,
7197                                 DOCUMENT_VISIBILITY_DATABASE_NAME,
7198                                 VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
7199                                 /* id= */ prefix2 + "Email2",
7200                                 /* typePropertyPaths= */ Collections.emptyMap()),
7201                         /* androidVOverlayDocument= */ null);
7202         assertThat(actualDocument2).isEqualTo(expectedDocument2);
7203 
7204         // Check the existing visibility document retains.
7205         assertThat(mAppSearchImpl.mDocumentVisibilityStoreLocked.getVisibility(prefix1 + "Email1"))
7206                 .isEqualTo(expectedDocument1);
7207         // Verify the VisibilityDocument is saved to AppSearchImpl.
7208         actualDocument1 =
7209                 VisibilityToDocumentConverter.createInternalVisibilityConfig(
7210                         mAppSearchImpl.getDocument(
7211                                 VISIBILITY_PACKAGE_NAME,
7212                                 DOCUMENT_VISIBILITY_DATABASE_NAME,
7213                                 VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
7214                                 /* id= */ prefix1 + "Email1",
7215                                 /* typePropertyPaths= */ Collections.emptyMap()),
7216                         /* androidVOverlayDocument= */ null);
7217         assertThat(actualDocument1).isEqualTo(expectedDocument1);
7218     }
7219 
7220     @Test
testSetVisibility_removeVisibilitySettings()7221     public void testSetVisibility_removeVisibilitySettings() throws Exception {
7222         // Create a non-all-default visibility document
7223         InternalVisibilityConfig visibilityConfig =
7224                 new InternalVisibilityConfig.Builder("Email")
7225                         .setNotDisplayedBySystem(true)
7226                         .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
7227                         .build();
7228 
7229         List<AppSearchSchema> schemas =
7230                 Collections.singletonList(new AppSearchSchema.Builder("Email").build());
7231 
7232         // Set schema Email and its visibility document to AppSearch database1
7233         InternalSetSchemaResponse internalSetSchemaResponse =
7234                 mAppSearchImpl.setSchema(
7235                         "package",
7236                         "database1",
7237                         schemas,
7238                         /* visibilityConfigs= */ ImmutableList.of(visibilityConfig),
7239                         /* forceOverride= */ false,
7240                         /* version= */ 0,
7241                         /* setSchemaStatsBuilder= */ null);
7242         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
7243         String prefix = PrefixUtil.createPrefix("package", "database1");
7244         InternalVisibilityConfig expectedDocument =
7245                 new InternalVisibilityConfig.Builder(prefix + "Email")
7246                         .setNotDisplayedBySystem(true)
7247                         .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
7248                         .build();
7249         assertThat(mAppSearchImpl.mDocumentVisibilityStoreLocked.getVisibility(prefix + "Email"))
7250                 .isEqualTo(expectedDocument);
7251         InternalVisibilityConfig actualDocument =
7252                 VisibilityToDocumentConverter.createInternalVisibilityConfig(
7253                         mAppSearchImpl.getDocument(
7254                                 VISIBILITY_PACKAGE_NAME,
7255                                 DOCUMENT_VISIBILITY_DATABASE_NAME,
7256                                 VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
7257                                 /* id= */ prefix + "Email",
7258                                 /* typePropertyPaths= */ Collections.emptyMap()),
7259                         /* androidVOverlayDocument= */ null);
7260         assertThat(actualDocument).isEqualTo(expectedDocument);
7261 
7262         // Set schema Email and its all-default visibility document to AppSearch database1
7263         internalSetSchemaResponse =
7264                 mAppSearchImpl.setSchema(
7265                         "package",
7266                         "database1",
7267                         schemas,
7268                         /* visibilityConfigs= */ ImmutableList.of(),
7269                         /* forceOverride= */ false,
7270                         /* version= */ 0,
7271                         /* setSchemaStatsBuilder= */ null);
7272         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
7273         // All-default visibility document won't be saved in AppSearch.
7274         assertThat(mAppSearchImpl.mDocumentVisibilityStoreLocked.getVisibility(prefix + "Email"))
7275                 .isNull();
7276         // Verify the InternalVisibilityConfig is removed from AppSearchImpl.
7277         AppSearchException e =
7278                 assertThrows(
7279                         AppSearchException.class,
7280                         () ->
7281                                 mAppSearchImpl.getDocument(
7282                                         VISIBILITY_PACKAGE_NAME,
7283                                         DOCUMENT_VISIBILITY_DATABASE_NAME,
7284                                         VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
7285                                         /* id= */ prefix + "Email",
7286                                         /* typePropertyPaths= */ Collections.emptyMap()));
7287         assertThat(e)
7288                 .hasMessageThat()
7289                 .contains("Document (VS#Pkg$VS#Db/, package$database1/Email) not found.");
7290     }
7291 
7292     @Test
testRemoveVisibility_noRemainingSettings()7293     public void testRemoveVisibility_noRemainingSettings() throws Exception {
7294         // Create a non-all-default visibility document
7295         InternalVisibilityConfig visibilityConfig =
7296                 new InternalVisibilityConfig.Builder("Email")
7297                         .setNotDisplayedBySystem(true)
7298                         .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
7299                         .build();
7300 
7301         List<AppSearchSchema> schemas =
7302                 Collections.singletonList(new AppSearchSchema.Builder("Email").build());
7303 
7304         // Set schema Email and its visibility document to AppSearch database1
7305         mAppSearchImpl.setSchema(
7306                 "package",
7307                 "database1",
7308                 schemas,
7309                 /* visibilityConfigs= */ ImmutableList.of(visibilityConfig),
7310                 /* forceOverride= */ false,
7311                 /* version= */ 0,
7312                 /* setSchemaStatsBuilder= */ null);
7313         String prefix = PrefixUtil.createPrefix("package", "database1");
7314         InternalVisibilityConfig expectedDocument =
7315                 new InternalVisibilityConfig.Builder(prefix + "Email")
7316                         .setNotDisplayedBySystem(true)
7317                         .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
7318                         .build();
7319         assertThat(mAppSearchImpl.mDocumentVisibilityStoreLocked.getVisibility(prefix + "Email"))
7320                 .isEqualTo(expectedDocument);
7321         // Verify the InternalVisibilityConfig is saved to AppSearchImpl.
7322         InternalVisibilityConfig actualDocument =
7323                 VisibilityToDocumentConverter.createInternalVisibilityConfig(
7324                         mAppSearchImpl.getDocument(
7325                                 VISIBILITY_PACKAGE_NAME,
7326                                 DOCUMENT_VISIBILITY_DATABASE_NAME,
7327                                 VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
7328                                 /* id= */ prefix + "Email",
7329                                 /* typePropertyPaths= */ Collections.emptyMap()),
7330                         /* androidVOverlayDocument= */ null);
7331         assertThat(actualDocument).isEqualTo(expectedDocument);
7332 
7333         // remove the schema and visibility setting from AppSearch
7334         mAppSearchImpl.setSchema(
7335                 "package",
7336                 "database1",
7337                 /* schemas= */ new ArrayList<>(),
7338                 /* visibilityConfigs= */ ImmutableList.of(),
7339                 /* forceOverride= */ false,
7340                 /* version= */ 0,
7341                 /* setSchemaStatsBuilder= */ null);
7342 
7343         // add the schema back with an all default visibility setting.
7344         mAppSearchImpl.setSchema(
7345                 "package",
7346                 "database1",
7347                 schemas,
7348                 /* visibilityConfigs= */ ImmutableList.of(),
7349                 /* forceOverride= */ false,
7350                 /* version= */ 0,
7351                 /* setSchemaStatsBuilder= */ null);
7352         // All-default visibility document won't be saved in AppSearch.
7353         assertThat(mAppSearchImpl.mDocumentVisibilityStoreLocked.getVisibility(prefix + "Email"))
7354                 .isNull();
7355         // Verify there is no visibility setting for the schema.
7356         AppSearchException e =
7357                 assertThrows(
7358                         AppSearchException.class,
7359                         () ->
7360                                 mAppSearchImpl.getDocument(
7361                                         VISIBILITY_PACKAGE_NAME,
7362                                         DOCUMENT_VISIBILITY_DATABASE_NAME,
7363                                         VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
7364                                         /* id= */ prefix + "Email",
7365                                         /* typePropertyPaths= */ Collections.emptyMap()));
7366         assertThat(e)
7367                 .hasMessageThat()
7368                 .contains("Document (VS#Pkg$VS#Db/, package$database1/Email) not found.");
7369     }
7370 
7371     @Test
testCloseAndReopen_visibilityInfoRetains()7372     public void testCloseAndReopen_visibilityInfoRetains() throws Exception {
7373         // set Schema and visibility to AppSearch
7374         InternalVisibilityConfig visibilityConfig =
7375                 new InternalVisibilityConfig.Builder("Email")
7376                         .setNotDisplayedBySystem(true)
7377                         .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
7378                         .build();
7379         List<AppSearchSchema> schemas =
7380                 Collections.singletonList(new AppSearchSchema.Builder("Email").build());
7381         InternalSetSchemaResponse internalSetSchemaResponse =
7382                 mAppSearchImpl.setSchema(
7383                         "packageName",
7384                         "databaseName",
7385                         schemas,
7386                         ImmutableList.of(visibilityConfig),
7387                         /* forceOverride= */ true,
7388                         /* version= */ 0,
7389                         /* setSchemaStatsBuilder= */ null);
7390         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
7391 
7392         // close and re-open AppSearchImpl, the visibility document retains
7393         mAppSearchImpl.close();
7394         mAppSearchImpl =
7395                 AppSearchImpl.create(
7396                         mAppSearchDir,
7397                         new AppSearchConfigImpl(
7398                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
7399                         /* initStatsBuilder= */ null,
7400                         /* visibilityChecker= */ null,
7401                         /* revocableFileDescriptorStore= */ null,
7402                         ALWAYS_OPTIMIZE);
7403 
7404         String prefix = PrefixUtil.createPrefix("packageName", "databaseName");
7405         InternalVisibilityConfig expectedDocument =
7406                 new InternalVisibilityConfig.Builder(prefix + "Email")
7407                         .setNotDisplayedBySystem(true)
7408                         .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
7409                         .build();
7410 
7411         assertThat(mAppSearchImpl.mDocumentVisibilityStoreLocked.getVisibility(prefix + "Email"))
7412                 .isEqualTo(expectedDocument);
7413         // Verify the InternalVisibilityConfig is saved to AppSearchImpl.
7414         InternalVisibilityConfig actualDocument =
7415                 VisibilityToDocumentConverter.createInternalVisibilityConfig(
7416                         mAppSearchImpl.getDocument(
7417                                 VISIBILITY_PACKAGE_NAME,
7418                                 DOCUMENT_VISIBILITY_DATABASE_NAME,
7419                                 VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
7420                                 /* id= */ prefix + "Email",
7421                                 /* typePropertyPaths= */ Collections.emptyMap()),
7422                         /* androidVOverlayDocument= */ null);
7423         assertThat(actualDocument).isEqualTo(expectedDocument);
7424 
7425         // remove schema and visibility document
7426         internalSetSchemaResponse =
7427                 mAppSearchImpl.setSchema(
7428                         "packageName",
7429                         "databaseName",
7430                         ImmutableList.of(),
7431                         ImmutableList.of(),
7432                         /* forceOverride= */ true,
7433                         /* version= */ 0,
7434                         /* setSchemaStatsBuilder= */ null);
7435         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
7436 
7437         // close and re-open AppSearchImpl, the visibility document removed
7438         mAppSearchImpl.close();
7439         mAppSearchImpl =
7440                 AppSearchImpl.create(
7441                         mAppSearchDir,
7442                         new AppSearchConfigImpl(
7443                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
7444                         /* initStatsBuilder= */ null,
7445                         /* visibilityChecker= */ null,
7446                         /* revocableFileDescriptorStore= */ null,
7447                         ALWAYS_OPTIMIZE);
7448 
7449         assertThat(mAppSearchImpl.mDocumentVisibilityStoreLocked.getVisibility(prefix + "Email"))
7450                 .isNull();
7451         // Verify the InternalVisibilityConfig is removed from AppSearchImpl.
7452         AppSearchException e =
7453                 assertThrows(
7454                         AppSearchException.class,
7455                         () ->
7456                                 mAppSearchImpl.getDocument(
7457                                         VISIBILITY_PACKAGE_NAME,
7458                                         DOCUMENT_VISIBILITY_DATABASE_NAME,
7459                                         VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
7460                                         /* id= */ prefix + "Email",
7461                                         /* typePropertyPaths= */ Collections.emptyMap()));
7462         assertThat(e)
7463                 .hasMessageThat()
7464                 .contains("Document (VS#Pkg$VS#Db/, packageName$databaseName/Email) not found.");
7465     }
7466 
7467     @Test
testGetSchema_global()7468     public void testGetSchema_global() throws Exception {
7469         List<AppSearchSchema> schemas =
7470                 Collections.singletonList(new AppSearchSchema.Builder("Type").build());
7471 
7472         // Create a new mAppSearchImpl with a mock Visibility Checker
7473         mAppSearchImpl.close();
7474         File tempFolder = mTemporaryFolder.newFolder();
7475         VisibilityChecker mockVisibilityChecker = createMockVisibilityChecker(true);
7476         mAppSearchImpl =
7477                 AppSearchImpl.create(
7478                         tempFolder,
7479                         new AppSearchConfigImpl(
7480                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
7481                         /* initStatsBuilder= */ null,
7482                         mockVisibilityChecker,
7483                         /* revocableFileDescriptorStore= */ null,
7484                         ALWAYS_OPTIMIZE);
7485 
7486         // Add a schema type that is not displayed by the system
7487         InternalSetSchemaResponse internalSetSchemaResponse =
7488                 mAppSearchImpl.setSchema(
7489                         "package",
7490                         "database",
7491                         schemas,
7492                         /* visibilityConfigs= */ ImmutableList.of(
7493                                 new InternalVisibilityConfig.Builder("Type")
7494                                         .setNotDisplayedBySystem(true)
7495                                         .build()),
7496                         /* forceOverride= */ false,
7497                         /* version= */ 0,
7498                         /* setSchemaStatsBuilder= */ null);
7499         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
7500 
7501         // Get this schema as another package
7502         GetSchemaResponse getResponse =
7503                 mAppSearchImpl.getSchema(
7504                         "package",
7505                         "database",
7506                         new CallerAccess(
7507                                 /* callingPackageName= */ "com.android.appsearch.fake.package"));
7508         assertThat(getResponse.getSchemas()).containsExactlyElementsIn(schemas);
7509         assertThat(getResponse.getSchemaTypesNotDisplayedBySystem()).containsExactly("Type");
7510     }
7511 
7512     @Test
testGetSchema_nonExistentApp()7513     public void testGetSchema_nonExistentApp() throws Exception {
7514         // Add a schema. The test loses meaning if the schema is completely empty.
7515         InternalSetSchemaResponse internalSetSchemaResponse =
7516                 mAppSearchImpl.setSchema(
7517                         "package",
7518                         "database",
7519                         Collections.singletonList(new AppSearchSchema.Builder("Type").build()),
7520                         /* visibilityConfigs= */ ImmutableList.of(),
7521                         /* forceOverride= */ false,
7522                         /* version= */ 0,
7523                         /* setSchemaStatsBuilder= */ null);
7524         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
7525 
7526         // Try to get the schema of a nonexistent package.
7527         GetSchemaResponse getResponse =
7528                 mAppSearchImpl.getSchema(
7529                         "com.android.appsearch.fake.package",
7530                         "database",
7531                         new CallerAccess(/* callingPackageName= */ "package"));
7532         assertThat(getResponse.getSchemas()).isEmpty();
7533         assertThat(getResponse.getSchemaTypesNotDisplayedBySystem()).isEmpty();
7534     }
7535 
7536     @Test
testGetSchema_noAccess()7537     public void testGetSchema_noAccess() throws Exception {
7538         List<AppSearchSchema> schemas =
7539                 Collections.singletonList(new AppSearchSchema.Builder("Type").build());
7540         // Add a schema type
7541         InternalSetSchemaResponse internalSetSchemaResponse =
7542                 mAppSearchImpl.setSchema(
7543                         "package",
7544                         "database",
7545                         schemas,
7546                         /* visibilityConfigs= */ ImmutableList.of(),
7547                         /* forceOverride= */ false,
7548                         /* version= */ 1,
7549                         /* setSchemaStatsBuilder= */ null);
7550         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
7551         GetSchemaResponse getResponse =
7552                 mAppSearchImpl.getSchema(
7553                         "package",
7554                         "database",
7555                         new CallerAccess(
7556                                 /* callingPackageName= */ "com.android.appsearch.fake.package"));
7557         assertThat(getResponse.getSchemas()).isEmpty();
7558         assertThat(getResponse.getSchemaTypesNotDisplayedBySystem()).isEmpty();
7559         assertThat(getResponse.getVersion()).isEqualTo(0);
7560 
7561         // Make sure the test is hooked up right by calling getSchema with the same parameters but
7562         // from the same package
7563         getResponse =
7564                 mAppSearchImpl.getSchema(
7565                         "package",
7566                         "database",
7567                         new CallerAccess(/* callingPackageName= */ "package"));
7568         assertThat(getResponse.getSchemas()).containsExactlyElementsIn(schemas);
7569     }
7570 
7571     @Test
testGetSchema_global_partialAccess()7572     public void testGetSchema_global_partialAccess() throws Exception {
7573         List<AppSearchSchema> schemas =
7574                 ImmutableList.of(
7575                         new AppSearchSchema.Builder("VisibleType").build(),
7576                         new AppSearchSchema.Builder("PrivateType").build());
7577 
7578         // Create a new mAppSearchImpl with a mock Visibility Checker
7579         mAppSearchImpl.close();
7580         File tempFolder = mTemporaryFolder.newFolder();
7581         VisibilityChecker mockVisibilityChecker =
7582                 new VisibilityChecker() {
7583                     @Override
7584                     public boolean isSchemaSearchableByCaller(
7585                             @NonNull CallerAccess callerAccess,
7586                             @NonNull String packageName,
7587                             @NonNull String prefixedSchema,
7588                             @NonNull VisibilityStore visibilityStore) {
7589                         return prefixedSchema.endsWith("VisibleType");
7590                     }
7591 
7592                     @Override
7593                     public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
7594                         return false;
7595                     }
7596                 };
7597 
7598         mAppSearchImpl =
7599                 AppSearchImpl.create(
7600                         tempFolder,
7601                         new AppSearchConfigImpl(
7602                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
7603                         /* initStatsBuilder= */ null,
7604                         mockVisibilityChecker,
7605                         /* revocableFileDescriptorStore= */ null,
7606                         ALWAYS_OPTIMIZE);
7607 
7608         // Add two schema types that are not displayed by the system.
7609         InternalSetSchemaResponse internalSetSchemaResponse =
7610                 mAppSearchImpl.setSchema(
7611                         "package",
7612                         "database",
7613                         schemas,
7614                         /* visibilityConfigs= */ ImmutableList.of(
7615                                 new InternalVisibilityConfig.Builder("VisibleType")
7616                                         .setNotDisplayedBySystem(true)
7617                                         .build(),
7618                                 new InternalVisibilityConfig.Builder("PrivateType")
7619                                         .setNotDisplayedBySystem(true)
7620                                         .build()),
7621                         /* forceOverride= */ false,
7622                         /* version= */ 1,
7623                         /* setSchemaStatsBuilder= */ null);
7624         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
7625 
7626         GetSchemaResponse getResponse =
7627                 mAppSearchImpl.getSchema(
7628                         "package",
7629                         "database",
7630                         new CallerAccess(
7631                                 /* callingPackageName= */ "com.android.appsearch.fake.package"));
7632         assertThat(getResponse.getSchemas()).containsExactly(schemas.get(0));
7633         assertThat(getResponse.getSchemaTypesNotDisplayedBySystem()).containsExactly("VisibleType");
7634         assertThat(getResponse.getVersion()).isEqualTo(1);
7635     }
7636 
7637     @Test
testGetSchema_global_publicAcl()7638     public void testGetSchema_global_publicAcl() throws Exception {
7639         List<AppSearchSchema> schemas =
7640                 ImmutableList.of(
7641                         new AppSearchSchema.Builder("PublicTypeA").build(),
7642                         new AppSearchSchema.Builder("PublicTypeB").build(),
7643                         new AppSearchSchema.Builder("PublicTypeC").build());
7644 
7645         PackageIdentifier pkgA = new PackageIdentifier("A", new byte[32]);
7646         PackageIdentifier pkgB = new PackageIdentifier("B", new byte[32]);
7647         PackageIdentifier pkgC = new PackageIdentifier("C", new byte[32]);
7648 
7649         // Create a new mAppSearchImpl with a mock Visibility Checker
7650         mAppSearchImpl.close();
7651         File tempFolder = mTemporaryFolder.newFolder();
7652 
7653         // Package A is visible to package B & C, package B is visible to package C (based on
7654         // canPackageQuery, which we are mocking).
7655         Map<String, Set<String>> packageCanSee =
7656                 ImmutableMap.of(
7657                         "A", ImmutableSet.of("A"),
7658                         "B", ImmutableSet.of("A", "B"),
7659                         "C", ImmutableSet.of("A", "B", "C"));
7660         final VisibilityChecker publicAclMockChecker =
7661                 new VisibilityChecker() {
7662                     @Override
7663                     public boolean isSchemaSearchableByCaller(
7664                             @NonNull CallerAccess callerAccess,
7665                             @NonNull String packageName,
7666                             @NonNull String prefixedSchema,
7667                             @NonNull VisibilityStore visibilityStore) {
7668                         InternalVisibilityConfig param =
7669                                 visibilityStore.getVisibility(prefixedSchema);
7670                         return packageCanSee
7671                                 .get(callerAccess.getCallingPackageName())
7672                                 .contains(
7673                                         param.getVisibilityConfig()
7674                                                 .getPubliclyVisibleTargetPackage()
7675                                                 .getPackageName());
7676                     }
7677 
7678                     @Override
7679                     public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
7680                         return false;
7681                     }
7682                 };
7683 
7684         mAppSearchImpl =
7685                 AppSearchImpl.create(
7686                         tempFolder,
7687                         new AppSearchConfigImpl(
7688                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
7689                         /* initStatsBuilder= */ null,
7690                         publicAclMockChecker,
7691                         /* revocableFileDescriptorStore= */ null,
7692                         ALWAYS_OPTIMIZE);
7693 
7694         List<InternalVisibilityConfig> visibilityConfigs =
7695                 ImmutableList.of(
7696                         new InternalVisibilityConfig.Builder("PublicTypeA")
7697                                 .setPubliclyVisibleTargetPackage(pkgA)
7698                                 .build(),
7699                         new InternalVisibilityConfig.Builder("PublicTypeB")
7700                                 .setPubliclyVisibleTargetPackage(pkgB)
7701                                 .build(),
7702                         new InternalVisibilityConfig.Builder("PublicTypeC")
7703                                 .setPubliclyVisibleTargetPackage(pkgC)
7704                                 .build());
7705 
7706         // Add the three schema types, each with their own publicly visible target package.
7707         InternalSetSchemaResponse internalSetSchemaResponse =
7708                 mAppSearchImpl.setSchema(
7709                         "package",
7710                         "database",
7711                         schemas,
7712                         visibilityConfigs,
7713                         /* forceOverride= */ true,
7714                         /* version= */ 1,
7715                         /* setSchemaStatsBuilder= */ null);
7716         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
7717 
7718         // Verify access to schemas based on calling package
7719         GetSchemaResponse getResponse =
7720                 mAppSearchImpl.getSchema(
7721                         "package", "database", new CallerAccess(pkgA.getPackageName()));
7722         assertThat(getResponse.getSchemas()).containsExactly(schemas.get(0));
7723         assertThat(getResponse.getPubliclyVisibleSchemas()).containsKey("PublicTypeA");
7724 
7725         getResponse =
7726                 mAppSearchImpl.getSchema(
7727                         "package", "database", new CallerAccess(pkgB.getPackageName()));
7728         assertThat(getResponse.getSchemas()).containsExactly(schemas.get(0), schemas.get(1));
7729         assertThat(getResponse.getPubliclyVisibleSchemas()).containsKey("PublicTypeA");
7730         assertThat(getResponse.getPubliclyVisibleSchemas()).containsKey("PublicTypeB");
7731 
7732         getResponse =
7733                 mAppSearchImpl.getSchema(
7734                         "package", "database", new CallerAccess(pkgC.getPackageName()));
7735         assertThat(getResponse.getSchemas()).containsExactlyElementsIn(schemas);
7736         assertThat(getResponse.getPubliclyVisibleSchemas()).containsKey("PublicTypeA");
7737         assertThat(getResponse.getPubliclyVisibleSchemas()).containsKey("PublicTypeB");
7738         assertThat(getResponse.getPubliclyVisibleSchemas()).containsKey("PublicTypeC");
7739     }
7740 
7741     @Test
testGetSchema_global_publicAcl_removal()7742     public void testGetSchema_global_publicAcl_removal() throws Exception {
7743         // This test to ensure the proper documents are created through setSchema, then removed
7744         // when setSchema is called again
7745         List<AppSearchSchema> schemas =
7746                 ImmutableList.of(
7747                         new AppSearchSchema.Builder("PublicTypeA").build(),
7748                         new AppSearchSchema.Builder("PublicTypeB").build(),
7749                         new AppSearchSchema.Builder("PublicTypeC").build());
7750 
7751         PackageIdentifier pkgA = new PackageIdentifier("A", new byte[32]);
7752         PackageIdentifier pkgB = new PackageIdentifier("B", new byte[32]);
7753         PackageIdentifier pkgC = new PackageIdentifier("C", new byte[32]);
7754 
7755         // Create a new mAppSearchImpl with a mock Visibility Checker
7756         mAppSearchImpl.close();
7757         File tempFolder = mTemporaryFolder.newFolder();
7758 
7759         // Package A is visible to package B & C, package B is visible to package C (based on
7760         // canPackageQuery, which we are mocking).
7761         Map<String, Set<String>> packageCanSee =
7762                 ImmutableMap.of(
7763                         "A", ImmutableSet.of("A"),
7764                         "B", ImmutableSet.of("A", "B"),
7765                         "C", ImmutableSet.of("A", "B", "C"));
7766         final VisibilityChecker publicAclMockChecker =
7767                 new VisibilityChecker() {
7768                     @Override
7769                     public boolean isSchemaSearchableByCaller(
7770                             @NonNull CallerAccess callerAccess,
7771                             @NonNull String packageName,
7772                             @NonNull String prefixedSchema,
7773                             @NonNull VisibilityStore visibilityStore) {
7774                         InternalVisibilityConfig param =
7775                                 visibilityStore.getVisibility(prefixedSchema);
7776                         return packageCanSee
7777                                 .get(callerAccess.getCallingPackageName())
7778                                 .contains(
7779                                         param.getVisibilityConfig()
7780                                                 .getPubliclyVisibleTargetPackage()
7781                                                 .getPackageName());
7782                     }
7783 
7784                     @Override
7785                     public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
7786                         return false;
7787                     }
7788                 };
7789 
7790         mAppSearchImpl =
7791                 AppSearchImpl.create(
7792                         tempFolder,
7793                         new AppSearchConfigImpl(
7794                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
7795                         /* initStatsBuilder= */ null,
7796                         publicAclMockChecker,
7797                         /* revocableFileDescriptorStore= */ null,
7798                         ALWAYS_OPTIMIZE);
7799 
7800         List<InternalVisibilityConfig> visibilityConfigs =
7801                 ImmutableList.of(
7802                         new InternalVisibilityConfig.Builder("PublicTypeA")
7803                                 .setPubliclyVisibleTargetPackage(pkgA)
7804                                 .build(),
7805                         new InternalVisibilityConfig.Builder("PublicTypeB")
7806                                 .setPubliclyVisibleTargetPackage(pkgB)
7807                                 .build(),
7808                         new InternalVisibilityConfig.Builder("PublicTypeC")
7809                                 .setPubliclyVisibleTargetPackage(pkgC)
7810                                 .build());
7811 
7812         // Add two schema types that are not displayed by the system.
7813         InternalSetSchemaResponse internalSetSchemaResponse =
7814                 mAppSearchImpl.setSchema(
7815                         "package",
7816                         "database",
7817                         schemas,
7818                         visibilityConfigs,
7819                         /* forceOverride= */ true,
7820                         /* version= */ 1,
7821                         /* setSchemaStatsBuilder= */ null);
7822         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
7823 
7824         // Now check for documents
7825         GenericDocument visibilityOverlayA =
7826                 mAppSearchImpl.getDocument(
7827                         VISIBILITY_PACKAGE_NAME,
7828                         DOCUMENT_ANDROID_V_OVERLAY_DATABASE_NAME,
7829                         VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
7830                         "package$database/PublicTypeA",
7831                         Collections.emptyMap());
7832         GenericDocument visibilityOverlayB =
7833                 mAppSearchImpl.getDocument(
7834                         VISIBILITY_PACKAGE_NAME,
7835                         DOCUMENT_ANDROID_V_OVERLAY_DATABASE_NAME,
7836                         VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
7837                         "package$database/PublicTypeB",
7838                         Collections.emptyMap());
7839         GenericDocument visibilityOverlayC =
7840                 mAppSearchImpl.getDocument(
7841                         VISIBILITY_PACKAGE_NAME,
7842                         DOCUMENT_ANDROID_V_OVERLAY_DATABASE_NAME,
7843                         VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
7844                         "package$database/PublicTypeC",
7845                         Collections.emptyMap());
7846 
7847         AndroidVOverlayProto overlayProtoA =
7848                 AndroidVOverlayProto.newBuilder()
7849                         .setVisibilityConfig(
7850                                 VisibilityConfigProto.newBuilder()
7851                                         .setPubliclyVisibleTargetPackage(
7852                                                 PackageIdentifierProto.newBuilder()
7853                                                         .setPackageName("A")
7854                                                         .setPackageSha256Cert(
7855                                                                 ByteString.copyFrom(new byte[32]))
7856                                                         .build())
7857                                         .build())
7858                         .build();
7859         AndroidVOverlayProto overlayProtoB =
7860                 AndroidVOverlayProto.newBuilder()
7861                         .setVisibilityConfig(
7862                                 VisibilityConfigProto.newBuilder()
7863                                         .setPubliclyVisibleTargetPackage(
7864                                                 PackageIdentifierProto.newBuilder()
7865                                                         .setPackageName("B")
7866                                                         .setPackageSha256Cert(
7867                                                                 ByteString.copyFrom(new byte[32]))
7868                                                         .build())
7869                                         .build())
7870                         .build();
7871         AndroidVOverlayProto overlayProtoC =
7872                 AndroidVOverlayProto.newBuilder()
7873                         .setVisibilityConfig(
7874                                 VisibilityConfigProto.newBuilder()
7875                                         .setPubliclyVisibleTargetPackage(
7876                                                 PackageIdentifierProto.newBuilder()
7877                                                         .setPackageName("C")
7878                                                         .setPackageSha256Cert(
7879                                                                 ByteString.copyFrom(new byte[32]))
7880                                                         .build())
7881                                         .build())
7882                         .build();
7883 
7884         assertThat(visibilityOverlayA.getPropertyBytes("visibilityProtoSerializeProperty"))
7885                 .isEqualTo(overlayProtoA.toByteArray());
7886         assertThat(visibilityOverlayB.getPropertyBytes("visibilityProtoSerializeProperty"))
7887                 .isEqualTo(overlayProtoB.toByteArray());
7888         assertThat(visibilityOverlayC.getPropertyBytes("visibilityProtoSerializeProperty"))
7889                 .isEqualTo(overlayProtoC.toByteArray());
7890 
7891         // now undo the "public" setting
7892         visibilityConfigs =
7893                 ImmutableList.of(
7894                         new InternalVisibilityConfig.Builder("PublicTypeA").build(),
7895                         new InternalVisibilityConfig.Builder("PublicTypeB").build(),
7896                         new InternalVisibilityConfig.Builder("PublicTypeC").build());
7897 
7898         InternalSetSchemaResponse internalSetSchemaResponseRemoved =
7899                 mAppSearchImpl.setSchema(
7900                         "package",
7901                         "database",
7902                         schemas,
7903                         visibilityConfigs,
7904                         /* forceOverride= */ true,
7905                         /* version= */ 1,
7906                         /* setSchemaStatsBuilder= */ null);
7907         assertThat(internalSetSchemaResponseRemoved.isSuccess()).isTrue();
7908 
7909         // Now check for documents again
7910         Exception e =
7911                 assertThrows(
7912                         AppSearchException.class,
7913                         () ->
7914                                 mAppSearchImpl.getDocument(
7915                                         VISIBILITY_PACKAGE_NAME,
7916                                         DOCUMENT_VISIBILITY_DATABASE_NAME,
7917                                         VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
7918                                         "package$database/PublicTypeA",
7919                                         Collections.emptyMap()));
7920         assertThat(e.getMessage()).endsWith("not found.");
7921         e =
7922                 assertThrows(
7923                         AppSearchException.class,
7924                         () ->
7925                                 mAppSearchImpl.getDocument(
7926                                         VISIBILITY_PACKAGE_NAME,
7927                                         DOCUMENT_VISIBILITY_DATABASE_NAME,
7928                                         VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
7929                                         "package$database/PublicTypeB",
7930                                         Collections.emptyMap()));
7931         assertThat(e.getMessage()).endsWith("not found.");
7932         e =
7933                 assertThrows(
7934                         AppSearchException.class,
7935                         () ->
7936                                 mAppSearchImpl.getDocument(
7937                                         VISIBILITY_PACKAGE_NAME,
7938                                         DOCUMENT_VISIBILITY_DATABASE_NAME,
7939                                         VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
7940                                         "package$database/PublicTypeC",
7941                                         Collections.emptyMap()));
7942         assertThat(e.getMessage()).endsWith("not found.");
7943     }
7944 
7945     @Test
testDispatchObserver_samePackage_noVisStore_accept()7946     public void testDispatchObserver_samePackage_noVisStore_accept() throws Exception {
7947         // Add a schema type
7948         InternalSetSchemaResponse internalSetSchemaResponse =
7949                 mAppSearchImpl.setSchema(
7950                         mContext.getPackageName(),
7951                         "database1",
7952                         ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
7953                         /* visibilityConfigs= */ Collections.emptyList(),
7954                         /* forceOverride= */ false,
7955                         /* version= */ 0,
7956                         /* setSchemaStatsBuilder= */ null);
7957         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
7958 
7959         // Register an observer
7960         TestObserverCallback observer = new TestObserverCallback();
7961         mAppSearchImpl.registerObserverCallback(
7962                 /* listeningPackageAccess= */ mSelfCallerAccess,
7963                 /* targetPackageName= */ mContext.getPackageName(),
7964                 new ObserverSpec.Builder().build(),
7965                 MoreExecutors.directExecutor(),
7966                 observer);
7967 
7968         // Insert a valid doc
7969         assertThat(observer.getSchemaChanges()).isEmpty();
7970         assertThat(observer.getDocumentChanges()).isEmpty();
7971         mAppSearchImpl.putDocument(
7972                 mContext.getPackageName(),
7973                 "database1",
7974                 new GenericDocument.Builder<>("namespace1", "id1", "Type1").build(),
7975                 /* sendChangeNotifications= */ true,
7976                 /* logger= */ null);
7977         assertThat(observer.getSchemaChanges()).isEmpty();
7978         assertThat(observer.getDocumentChanges()).isEmpty();
7979 
7980         // Dispatch notifications
7981         mAppSearchImpl.dispatchAndClearChangeNotifications();
7982         assertThat(observer.getSchemaChanges()).isEmpty();
7983         assertThat(observer.getDocumentChanges())
7984                 .containsExactly(
7985                         new DocumentChangeInfo(
7986                                 mContext.getPackageName(),
7987                                 "database1",
7988                                 "namespace1",
7989                                 "Type1",
7990                                 ImmutableSet.of("id1")));
7991     }
7992 
7993     @Test
testDispatchObserver_samePackage_withVisStore_accept()7994     public void testDispatchObserver_samePackage_withVisStore_accept() throws Exception {
7995         // Make a visibility checker that rejects everything
7996         final VisibilityChecker rejectChecker = createMockVisibilityChecker(false);
7997         mAppSearchImpl.close();
7998         mAppSearchImpl =
7999                 AppSearchImpl.create(
8000                         mAppSearchDir,
8001                         new AppSearchConfigImpl(
8002                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
8003                         /* initStatsBuilder= */ null,
8004                         rejectChecker,
8005                         /* revocableFileDescriptorStore= */ null,
8006                         ALWAYS_OPTIMIZE);
8007 
8008         // Add a schema type
8009         InternalSetSchemaResponse internalSetSchemaResponse =
8010                 mAppSearchImpl.setSchema(
8011                         mContext.getPackageName(),
8012                         "database1",
8013                         ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
8014                         /* visibilityConfigs= */ Collections.emptyList(),
8015                         /* forceOverride= */ false,
8016                         /* version= */ 0,
8017                         /* setSchemaStatsBuilder= */ null);
8018         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
8019 
8020         // Register an observer
8021         TestObserverCallback observer = new TestObserverCallback();
8022         mAppSearchImpl.registerObserverCallback(
8023                 /* listeningPackageAccess= */ mSelfCallerAccess,
8024                 /* targetPackageName= */ mContext.getPackageName(),
8025                 new ObserverSpec.Builder().build(),
8026                 MoreExecutors.directExecutor(),
8027                 observer);
8028 
8029         // Insert a valid doc
8030         assertThat(observer.getSchemaChanges()).isEmpty();
8031         assertThat(observer.getDocumentChanges()).isEmpty();
8032         mAppSearchImpl.putDocument(
8033                 mContext.getPackageName(),
8034                 "database1",
8035                 new GenericDocument.Builder<>("namespace1", "id1", "Type1").build(),
8036                 /* sendChangeNotifications= */ true,
8037                 /* logger= */ null);
8038         assertThat(observer.getSchemaChanges()).isEmpty();
8039         assertThat(observer.getDocumentChanges()).isEmpty();
8040 
8041         // Dispatch notifications
8042         mAppSearchImpl.dispatchAndClearChangeNotifications();
8043         assertThat(observer.getSchemaChanges()).isEmpty();
8044         assertThat(observer.getDocumentChanges())
8045                 .containsExactly(
8046                         new DocumentChangeInfo(
8047                                 mContext.getPackageName(),
8048                                 "database1",
8049                                 "namespace1",
8050                                 "Type1",
8051                                 ImmutableSet.of("id1")));
8052     }
8053 
8054     @Test
testDispatchObserver_differentPackage_noVisStore_reject()8055     public void testDispatchObserver_differentPackage_noVisStore_reject() throws Exception {
8056         // Add a schema type
8057         InternalSetSchemaResponse internalSetSchemaResponse =
8058                 mAppSearchImpl.setSchema(
8059                         mContext.getPackageName(),
8060                         "database1",
8061                         ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
8062                         /* visibilityConfigs= */ Collections.emptyList(),
8063                         /* forceOverride= */ false,
8064                         /* version= */ 0,
8065                         /* setSchemaStatsBuilder= */ null);
8066         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
8067 
8068         // Register an observer from a simulated different package
8069         TestObserverCallback observer = new TestObserverCallback();
8070         mAppSearchImpl.registerObserverCallback(
8071                 new CallerAccess(/* callingPackageName= */ "com.fake.Listening.package"),
8072                 /* targetPackageName= */ mContext.getPackageName(),
8073                 new ObserverSpec.Builder().build(),
8074                 MoreExecutors.directExecutor(),
8075                 observer);
8076 
8077         // Insert a valid doc
8078         assertThat(observer.getSchemaChanges()).isEmpty();
8079         assertThat(observer.getDocumentChanges()).isEmpty();
8080         mAppSearchImpl.putDocument(
8081                 mContext.getPackageName(),
8082                 "database1",
8083                 new GenericDocument.Builder<>("namespace1", "id1", "Type1").build(),
8084                 /* sendChangeNotifications= */ true,
8085                 /* logger= */ null);
8086         assertThat(observer.getSchemaChanges()).isEmpty();
8087         assertThat(observer.getDocumentChanges()).isEmpty();
8088 
8089         // Dispatch notifications
8090         mAppSearchImpl.dispatchAndClearChangeNotifications();
8091         assertThat(observer.getSchemaChanges()).isEmpty();
8092         assertThat(observer.getDocumentChanges()).isEmpty();
8093     }
8094 
8095     @Test
testDispatchObserver_differentPackage_withVisStore_accept()8096     public void testDispatchObserver_differentPackage_withVisStore_accept() throws Exception {
8097         final String fakeListeningPackage = "com.fake.listening.package";
8098 
8099         // Make a visibility checker that allows only fakeListeningPackage.
8100         final VisibilityChecker visibilityChecker =
8101                 new VisibilityChecker() {
8102                     @Override
8103                     public boolean isSchemaSearchableByCaller(
8104                             @NonNull CallerAccess callerAccess,
8105                             @NonNull String packageName,
8106                             @NonNull String prefixedSchema,
8107                             @NonNull VisibilityStore visibilityStore) {
8108                         return callerAccess.getCallingPackageName().equals(fakeListeningPackage);
8109                     }
8110 
8111                     @Override
8112                     public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
8113                         return false;
8114                     }
8115                 };
8116         mAppSearchImpl.close();
8117         mAppSearchImpl =
8118                 AppSearchImpl.create(
8119                         mAppSearchDir,
8120                         new AppSearchConfigImpl(
8121                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
8122                         /* initStatsBuilder= */ null,
8123                         visibilityChecker,
8124                         /* revocableFileDescriptorStore= */ null,
8125                         ALWAYS_OPTIMIZE);
8126 
8127         // Add a schema type
8128         InternalSetSchemaResponse internalSetSchemaResponse =
8129                 mAppSearchImpl.setSchema(
8130                         mContext.getPackageName(),
8131                         "database1",
8132                         ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
8133                         /* visibilityConfigs= */ Collections.emptyList(),
8134                         /* forceOverride= */ false,
8135                         /* version= */ 0,
8136                         /* setSchemaStatsBuilder= */ null);
8137         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
8138 
8139         // Register an observer
8140         TestObserverCallback observer = new TestObserverCallback();
8141         mAppSearchImpl.registerObserverCallback(
8142                 new CallerAccess(/* callingPackageName= */ fakeListeningPackage),
8143                 /* targetPackageName= */ mContext.getPackageName(),
8144                 new ObserverSpec.Builder().build(),
8145                 MoreExecutors.directExecutor(),
8146                 observer);
8147 
8148         // Insert a valid doc
8149         assertThat(observer.getSchemaChanges()).isEmpty();
8150         assertThat(observer.getDocumentChanges()).isEmpty();
8151         mAppSearchImpl.putDocument(
8152                 mContext.getPackageName(),
8153                 "database1",
8154                 new GenericDocument.Builder<>("namespace1", "id1", "Type1").build(),
8155                 /* sendChangeNotifications= */ true,
8156                 /* logger= */ null);
8157         assertThat(observer.getSchemaChanges()).isEmpty();
8158         assertThat(observer.getDocumentChanges()).isEmpty();
8159 
8160         // Dispatch notifications
8161         mAppSearchImpl.dispatchAndClearChangeNotifications();
8162         assertThat(observer.getSchemaChanges()).isEmpty();
8163         assertThat(observer.getDocumentChanges())
8164                 .containsExactly(
8165                         new DocumentChangeInfo(
8166                                 mContext.getPackageName(),
8167                                 "database1",
8168                                 "namespace1",
8169                                 "Type1",
8170                                 ImmutableSet.of("id1")));
8171     }
8172 
8173     @Test
testDispatchObserver_differentPackage_withVisStore_reject()8174     public void testDispatchObserver_differentPackage_withVisStore_reject() throws Exception {
8175         final String fakeListeningPackage = "com.fake.Listening.package";
8176 
8177         // Make a visibility checker that rejects everything.
8178         final VisibilityChecker rejectChecker = createMockVisibilityChecker(false);
8179         mAppSearchImpl.close();
8180         mAppSearchImpl =
8181                 AppSearchImpl.create(
8182                         mAppSearchDir,
8183                         new AppSearchConfigImpl(
8184                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
8185                         /* initStatsBuilder= */ null,
8186                         rejectChecker,
8187                         /* revocableFileDescriptorStore= */ null,
8188                         ALWAYS_OPTIMIZE);
8189 
8190         // Add a schema type
8191         InternalSetSchemaResponse internalSetSchemaResponse =
8192                 mAppSearchImpl.setSchema(
8193                         mContext.getPackageName(),
8194                         "database1",
8195                         ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
8196                         /* visibilityConfigs= */ Collections.emptyList(),
8197                         /* forceOverride= */ false,
8198                         /* version= */ 0,
8199                         /* setSchemaStatsBuilder= */ null);
8200         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
8201 
8202         // Register an observer
8203         TestObserverCallback observer = new TestObserverCallback();
8204         mAppSearchImpl.registerObserverCallback(
8205                 new CallerAccess(/* callingPackageName= */ fakeListeningPackage),
8206                 /* targetPackageName= */ mContext.getPackageName(),
8207                 new ObserverSpec.Builder().build(),
8208                 MoreExecutors.directExecutor(),
8209                 observer);
8210 
8211         // Insert a doc
8212         assertThat(observer.getSchemaChanges()).isEmpty();
8213         assertThat(observer.getDocumentChanges()).isEmpty();
8214         mAppSearchImpl.putDocument(
8215                 mContext.getPackageName(),
8216                 "database1",
8217                 new GenericDocument.Builder<>("namespace1", "id1", "Type1").build(),
8218                 /* sendChangeNotifications= */ true,
8219                 /* logger= */ null);
8220         assertThat(observer.getSchemaChanges()).isEmpty();
8221         assertThat(observer.getDocumentChanges()).isEmpty();
8222 
8223         // Dispatch notifications
8224         mAppSearchImpl.dispatchAndClearChangeNotifications();
8225         assertThat(observer.getSchemaChanges()).isEmpty();
8226         assertThat(observer.getDocumentChanges()).isEmpty();
8227     }
8228 
8229     @Test
testAddObserver_schemaChange_added()8230     public void testAddObserver_schemaChange_added() throws Exception {
8231         // Register an observer
8232         TestObserverCallback observer = new TestObserverCallback();
8233         mAppSearchImpl.registerObserverCallback(
8234                 /* listeningPackageAccess= */ mSelfCallerAccess,
8235                 /* targetPackageName= */ mContext.getPackageName(),
8236                 new ObserverSpec.Builder().build(),
8237                 MoreExecutors.directExecutor(),
8238                 observer);
8239 
8240         // Add a schema type
8241         assertThat(observer.getSchemaChanges()).isEmpty();
8242         assertThat(observer.getDocumentChanges()).isEmpty();
8243         InternalSetSchemaResponse internalSetSchemaResponse =
8244                 mAppSearchImpl.setSchema(
8245                         mContext.getPackageName(),
8246                         "database1",
8247                         ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
8248                         /* visibilityConfigs= */ Collections.emptyList(),
8249                         /* forceOverride= */ false,
8250                         /* version= */ 0,
8251                         /* setSchemaStatsBuilder= */ null);
8252         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
8253         assertThat(observer.getSchemaChanges()).isEmpty();
8254         assertThat(observer.getDocumentChanges()).isEmpty();
8255 
8256         // Dispatch notifications
8257         mAppSearchImpl.dispatchAndClearChangeNotifications();
8258         assertThat(observer.getSchemaChanges())
8259                 .containsExactly(
8260                         new SchemaChangeInfo(
8261                                 mContext.getPackageName(), "database1", ImmutableSet.of("Type1")));
8262         assertThat(observer.getDocumentChanges()).isEmpty();
8263 
8264         // Add two more schema types without touching the existing one
8265         observer.clear();
8266         internalSetSchemaResponse =
8267                 mAppSearchImpl.setSchema(
8268                         mContext.getPackageName(),
8269                         "database1",
8270                         ImmutableList.of(
8271                                 new AppSearchSchema.Builder("Type1").build(),
8272                                 new AppSearchSchema.Builder("Type2").build(),
8273                                 new AppSearchSchema.Builder("Type3").build()),
8274                         /* visibilityConfigs= */ Collections.emptyList(),
8275                         /* forceOverride= */ false,
8276                         /* version= */ 0,
8277                         /* setSchemaStatsBuilder= */ null);
8278         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
8279         assertThat(observer.getSchemaChanges()).isEmpty();
8280         assertThat(observer.getDocumentChanges()).isEmpty();
8281 
8282         // Dispatch notifications
8283         mAppSearchImpl.dispatchAndClearChangeNotifications();
8284         assertThat(observer.getSchemaChanges())
8285                 .containsExactly(
8286                         new SchemaChangeInfo(
8287                                 mContext.getPackageName(),
8288                                 "database1",
8289                                 ImmutableSet.of("Type2", "Type3")));
8290         assertThat(observer.getDocumentChanges()).isEmpty();
8291     }
8292 
8293     @Test
testAddObserver_schemaChange_removed()8294     public void testAddObserver_schemaChange_removed() throws Exception {
8295         // Add a schema type
8296         InternalSetSchemaResponse internalSetSchemaResponse =
8297                 mAppSearchImpl.setSchema(
8298                         mContext.getPackageName(),
8299                         "database1",
8300                         ImmutableList.of(
8301                                 new AppSearchSchema.Builder("Type1").build(),
8302                                 new AppSearchSchema.Builder("Type2").build()),
8303                         /* visibilityConfigs= */ Collections.emptyList(),
8304                         /* forceOverride= */ false,
8305                         /* version= */ 0,
8306                         /* setSchemaStatsBuilder= */ null);
8307         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
8308 
8309         // Register an observer
8310         TestObserverCallback observer = new TestObserverCallback();
8311         mAppSearchImpl.registerObserverCallback(
8312                 /* listeningPackageAccess= */ mSelfCallerAccess,
8313                 /* targetPackageName= */ mContext.getPackageName(),
8314                 new ObserverSpec.Builder().build(),
8315                 MoreExecutors.directExecutor(),
8316                 observer);
8317 
8318         // Remove Type2
8319         internalSetSchemaResponse =
8320                 mAppSearchImpl.setSchema(
8321                         mContext.getPackageName(),
8322                         "database1",
8323                         ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
8324                         /* visibilityConfigs= */ Collections.emptyList(),
8325                         /* forceOverride= */ true,
8326                         /* version= */ 0,
8327                         /* setSchemaStatsBuilder= */ null);
8328         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
8329 
8330         // Dispatch notifications
8331         assertThat(observer.getSchemaChanges()).isEmpty();
8332         assertThat(observer.getDocumentChanges()).isEmpty();
8333         mAppSearchImpl.dispatchAndClearChangeNotifications();
8334         assertThat(observer.getSchemaChanges())
8335                 .containsExactly(
8336                         new SchemaChangeInfo(
8337                                 mContext.getPackageName(), "database1", ImmutableSet.of("Type2")));
8338         assertThat(observer.getDocumentChanges()).isEmpty();
8339     }
8340 
8341     @Test
testAddObserver_schemaChange_contents()8342     public void testAddObserver_schemaChange_contents() throws Exception {
8343         // Add a schema
8344         InternalSetSchemaResponse internalSetSchemaResponse =
8345                 mAppSearchImpl.setSchema(
8346                         mContext.getPackageName(),
8347                         "database1",
8348                         ImmutableList.of(
8349                                 new AppSearchSchema.Builder("Type1").build(),
8350                                 new AppSearchSchema.Builder("Type2")
8351                                         .addProperty(
8352                                                 new AppSearchSchema.BooleanPropertyConfig.Builder(
8353                                                                 "booleanProp")
8354                                                         .setCardinality(
8355                                                                 AppSearchSchema.PropertyConfig
8356                                                                         .CARDINALITY_REQUIRED)
8357                                                         .build())
8358                                         .build()),
8359                         /* visibilityConfigs= */ Collections.emptyList(),
8360                         /* forceOverride= */ false,
8361                         /* version= */ 0,
8362                         /* setSchemaStatsBuilder= */ null);
8363         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
8364 
8365         // Register an observer
8366         TestObserverCallback observer = new TestObserverCallback();
8367         mAppSearchImpl.registerObserverCallback(
8368                 /* listeningPackageAccess= */ mSelfCallerAccess,
8369                 /* targetPackageName= */ mContext.getPackageName(),
8370                 new ObserverSpec.Builder().build(),
8371                 MoreExecutors.directExecutor(),
8372                 observer);
8373 
8374         // Update the schema, but don't make any actual changes
8375         internalSetSchemaResponse =
8376                 mAppSearchImpl.setSchema(
8377                         mContext.getPackageName(),
8378                         "database1",
8379                         ImmutableList.of(
8380                                 new AppSearchSchema.Builder("Type1").build(),
8381                                 new AppSearchSchema.Builder("Type2")
8382                                         .addProperty(
8383                                                 new AppSearchSchema.BooleanPropertyConfig.Builder(
8384                                                                 "booleanProp")
8385                                                         .setCardinality(
8386                                                                 AppSearchSchema.PropertyConfig
8387                                                                         .CARDINALITY_REQUIRED)
8388                                                         .build())
8389                                         .build()),
8390                         /* visibilityConfigs= */ Collections.emptyList(),
8391                         /* forceOverride= */ false,
8392                         /* version= */ 1,
8393                         /* setSchemaStatsBuilder= */ null);
8394         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
8395 
8396         // Dispatch notifications
8397         assertThat(observer.getSchemaChanges()).isEmpty();
8398         assertThat(observer.getDocumentChanges()).isEmpty();
8399         mAppSearchImpl.dispatchAndClearChangeNotifications();
8400         assertThat(observer.getSchemaChanges()).isEmpty();
8401         assertThat(observer.getDocumentChanges()).isEmpty();
8402 
8403         // Now update the schema again, but this time actually make a change (cardinality of the
8404         // property)
8405         internalSetSchemaResponse =
8406                 mAppSearchImpl.setSchema(
8407                         mContext.getPackageName(),
8408                         "database1",
8409                         ImmutableList.of(
8410                                 new AppSearchSchema.Builder("Type1").build(),
8411                                 new AppSearchSchema.Builder("Type2")
8412                                         .addProperty(
8413                                                 new AppSearchSchema.BooleanPropertyConfig.Builder(
8414                                                                 "booleanProp")
8415                                                         .setCardinality(
8416                                                                 AppSearchSchema.PropertyConfig
8417                                                                         .CARDINALITY_OPTIONAL)
8418                                                         .build())
8419                                         .build()),
8420                         /* visibilityConfigs= */ Collections.emptyList(),
8421                         /* forceOverride= */ false,
8422                         /* version= */ 2,
8423                         /* setSchemaStatsBuilder= */ null);
8424         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
8425 
8426         // Dispatch notifications
8427         assertThat(observer.getSchemaChanges()).isEmpty();
8428         assertThat(observer.getDocumentChanges()).isEmpty();
8429         mAppSearchImpl.dispatchAndClearChangeNotifications();
8430         assertThat(observer.getSchemaChanges())
8431                 .containsExactly(
8432                         new SchemaChangeInfo(
8433                                 mContext.getPackageName(), "database1", ImmutableSet.of("Type2")));
8434         assertThat(observer.getDocumentChanges()).isEmpty();
8435     }
8436 
8437     @Test
testAddObserver_schemaChange_contents_skipBySpec()8438     public void testAddObserver_schemaChange_contents_skipBySpec() throws Exception {
8439         // Add a schema
8440         InternalSetSchemaResponse internalSetSchemaResponse =
8441                 mAppSearchImpl.setSchema(
8442                         mContext.getPackageName(),
8443                         "database1",
8444                         ImmutableList.of(
8445                                 new AppSearchSchema.Builder("Type1")
8446                                         .addProperty(
8447                                                 new AppSearchSchema.BooleanPropertyConfig.Builder(
8448                                                                 "booleanProp")
8449                                                         .setCardinality(
8450                                                                 AppSearchSchema.PropertyConfig
8451                                                                         .CARDINALITY_REQUIRED)
8452                                                         .build())
8453                                         .build(),
8454                                 new AppSearchSchema.Builder("Type2")
8455                                         .addProperty(
8456                                                 new AppSearchSchema.BooleanPropertyConfig.Builder(
8457                                                                 "booleanProp")
8458                                                         .setCardinality(
8459                                                                 AppSearchSchema.PropertyConfig
8460                                                                         .CARDINALITY_REQUIRED)
8461                                                         .build())
8462                                         .build()),
8463                         /* visibilityConfigs= */ Collections.emptyList(),
8464                         /* forceOverride= */ false,
8465                         /* version= */ 0,
8466                         /* setSchemaStatsBuilder= */ null);
8467         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
8468 
8469         // Register an observer that only listens for Type2
8470         TestObserverCallback observer = new TestObserverCallback();
8471         mAppSearchImpl.registerObserverCallback(
8472                 /* listeningPackageAccess= */ mSelfCallerAccess,
8473                 /* targetPackageName= */ mContext.getPackageName(),
8474                 new ObserverSpec.Builder().addFilterSchemas("Type2").build(),
8475                 MoreExecutors.directExecutor(),
8476                 observer);
8477 
8478         // Update both types of the schema (changed cardinalities)
8479         internalSetSchemaResponse =
8480                 mAppSearchImpl.setSchema(
8481                         mContext.getPackageName(),
8482                         "database1",
8483                         ImmutableList.of(
8484                                 new AppSearchSchema.Builder("Type1")
8485                                         .addProperty(
8486                                                 new AppSearchSchema.BooleanPropertyConfig.Builder(
8487                                                                 "booleanProp")
8488                                                         .setCardinality(
8489                                                                 AppSearchSchema.PropertyConfig
8490                                                                         .CARDINALITY_OPTIONAL)
8491                                                         .build())
8492                                         .build(),
8493                                 new AppSearchSchema.Builder("Type2")
8494                                         .addProperty(
8495                                                 new AppSearchSchema.BooleanPropertyConfig.Builder(
8496                                                                 "booleanProp")
8497                                                         .setCardinality(
8498                                                                 AppSearchSchema.PropertyConfig
8499                                                                         .CARDINALITY_OPTIONAL)
8500                                                         .build())
8501                                         .build()),
8502                         /* visibilityConfigs= */ Collections.emptyList(),
8503                         /* forceOverride= */ false,
8504                         /* version= */ 0,
8505                         /* setSchemaStatsBuilder= */ null);
8506         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
8507 
8508         // Dispatch notifications
8509         assertThat(observer.getSchemaChanges()).isEmpty();
8510         assertThat(observer.getDocumentChanges()).isEmpty();
8511         mAppSearchImpl.dispatchAndClearChangeNotifications();
8512         assertThat(observer.getSchemaChanges())
8513                 .containsExactly(
8514                         new SchemaChangeInfo(
8515                                 mContext.getPackageName(), "database1", ImmutableSet.of("Type2")));
8516         assertThat(observer.getDocumentChanges()).isEmpty();
8517     }
8518 
8519     @Test
testAddObserver_schemaChange_visibilityOnly()8520     public void testAddObserver_schemaChange_visibilityOnly() throws Exception {
8521         final String fakeListeningPackage = "com.fake.listening.package";
8522 
8523         // Make a fake visibility checker that actually looks at visibility store
8524         final VisibilityChecker visibilityChecker =
8525                 new VisibilityChecker() {
8526                     @Override
8527                     public boolean isSchemaSearchableByCaller(
8528                             @NonNull CallerAccess callerAccess,
8529                             @NonNull String packageName,
8530                             @NonNull String prefixedSchema,
8531                             @NonNull VisibilityStore visibilityStore) {
8532                         if (!callerAccess.getCallingPackageName().equals(fakeListeningPackage)) {
8533                             return false;
8534                         }
8535 
8536                         for (PackageIdentifier packageIdentifier :
8537                                 visibilityStore
8538                                         .getVisibility(prefixedSchema)
8539                                         .getVisibilityConfig()
8540                                         .getAllowedPackages()) {
8541                             if (packageIdentifier.getPackageName().equals(fakeListeningPackage)) {
8542                                 return true;
8543                             }
8544                         }
8545                         return false;
8546                     }
8547 
8548                     @Override
8549                     public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
8550                         return false;
8551                     }
8552                 };
8553         mAppSearchImpl.close();
8554         mAppSearchImpl =
8555                 AppSearchImpl.create(
8556                         mAppSearchDir,
8557                         new AppSearchConfigImpl(
8558                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
8559                         /* initStatsBuilder= */ null,
8560                         visibilityChecker,
8561                         /* revocableFileDescriptorStore= */ null,
8562                         ALWAYS_OPTIMIZE);
8563 
8564         // Register an observer
8565         TestObserverCallback observer = new TestObserverCallback();
8566         mAppSearchImpl.registerObserverCallback(
8567                 new CallerAccess(/* callingPackageName= */ fakeListeningPackage),
8568                 /* targetPackageName= */ mContext.getPackageName(),
8569                 new ObserverSpec.Builder().build(),
8570                 MoreExecutors.directExecutor(),
8571                 observer);
8572 
8573         // Add a schema where both types are visible to the fake package.
8574         List<AppSearchSchema> schemas =
8575                 ImmutableList.of(
8576                         new AppSearchSchema.Builder("Type1").build(),
8577                         new AppSearchSchema.Builder("Type2").build());
8578         InternalSetSchemaResponse internalSetSchemaResponse =
8579                 mAppSearchImpl.setSchema(
8580                         mContext.getPackageName(),
8581                         "database1",
8582                         schemas,
8583                         /* visibilityConfigs= */ ImmutableList.of(
8584                                 new InternalVisibilityConfig.Builder("Type1")
8585                                         .addVisibleToPackage(
8586                                                 new PackageIdentifier(
8587                                                         fakeListeningPackage, new byte[0]))
8588                                         .build(),
8589                                 new InternalVisibilityConfig.Builder("Type2")
8590                                         .addVisibleToPackage(
8591                                                 new PackageIdentifier(
8592                                                         fakeListeningPackage, new byte[0]))
8593                                         .build()),
8594                         /* forceOverride= */ false,
8595                         /* version= */ 0,
8596                         /* setSchemaStatsBuilder= */ null);
8597         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
8598 
8599         // Notifications of addition should now be dispatched
8600         assertThat(observer.getSchemaChanges()).isEmpty();
8601         assertThat(observer.getDocumentChanges()).isEmpty();
8602         mAppSearchImpl.dispatchAndClearChangeNotifications();
8603         assertThat(observer.getSchemaChanges())
8604                 .containsExactly(
8605                         new SchemaChangeInfo(
8606                                 mContext.getPackageName(),
8607                                 "database1",
8608                                 ImmutableSet.of("Type1", "Type2")));
8609         assertThat(observer.getDocumentChanges()).isEmpty();
8610         observer.clear();
8611 
8612         // Update schema, keeping the types identical but denying visibility to type2
8613         internalSetSchemaResponse =
8614                 mAppSearchImpl.setSchema(
8615                         mContext.getPackageName(),
8616                         "database1",
8617                         schemas,
8618                         /* visibilityConfigs= */ ImmutableList.of(
8619                                 new InternalVisibilityConfig.Builder("Type1")
8620                                         .addVisibleToPackage(
8621                                                 new PackageIdentifier(
8622                                                         fakeListeningPackage, new byte[0]))
8623                                         .build(),
8624                                 new InternalVisibilityConfig.Builder("Type2").build()),
8625                         /* forceOverride= */ false,
8626                         /* version= */ 0,
8627                         /* setSchemaStatsBuilder= */ null);
8628         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
8629 
8630         // Dispatch notifications. This should look like a deletion of Type2.
8631         assertThat(observer.getSchemaChanges()).isEmpty();
8632         assertThat(observer.getDocumentChanges()).isEmpty();
8633         mAppSearchImpl.dispatchAndClearChangeNotifications();
8634         assertThat(observer.getSchemaChanges())
8635                 .containsExactly(
8636                         new SchemaChangeInfo(
8637                                 mContext.getPackageName(), "database1", ImmutableSet.of("Type2")));
8638         assertThat(observer.getDocumentChanges()).isEmpty();
8639         observer.clear();
8640 
8641         // Now update Type2 and make sure no further notification is received.
8642         internalSetSchemaResponse =
8643                 mAppSearchImpl.setSchema(
8644                         mContext.getPackageName(),
8645                         "database1",
8646                         ImmutableList.of(
8647                                 new AppSearchSchema.Builder("Type1").build(),
8648                                 new AppSearchSchema.Builder("Type2")
8649                                         .addProperty(
8650                                                 new AppSearchSchema.BooleanPropertyConfig.Builder(
8651                                                                 "booleanProp")
8652                                                         .setCardinality(
8653                                                                 AppSearchSchema.PropertyConfig
8654                                                                         .CARDINALITY_OPTIONAL)
8655                                                         .build())
8656                                         .build()),
8657                         /* visibilityConfigs= */ ImmutableList.of(
8658                                 new InternalVisibilityConfig.Builder("Type1")
8659                                         .addVisibleToPackage(
8660                                                 new PackageIdentifier(
8661                                                         fakeListeningPackage, new byte[0]))
8662                                         .build(),
8663                                 new InternalVisibilityConfig.Builder("Type2").build()),
8664                         /* forceOverride= */ false,
8665                         /* version= */ 0,
8666                         /* setSchemaStatsBuilder= */ null);
8667         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
8668 
8669         assertThat(observer.getSchemaChanges()).isEmpty();
8670         assertThat(observer.getDocumentChanges()).isEmpty();
8671         mAppSearchImpl.dispatchAndClearChangeNotifications();
8672         assertThat(observer.getSchemaChanges()).isEmpty();
8673         assertThat(observer.getDocumentChanges()).isEmpty();
8674 
8675         // Grant visibility to Type2 again and make sure it appears
8676         internalSetSchemaResponse =
8677                 mAppSearchImpl.setSchema(
8678                         mContext.getPackageName(),
8679                         "database1",
8680                         ImmutableList.of(
8681                                 new AppSearchSchema.Builder("Type1").build(),
8682                                 new AppSearchSchema.Builder("Type2")
8683                                         .addProperty(
8684                                                 new AppSearchSchema.BooleanPropertyConfig.Builder(
8685                                                                 "booleanProp")
8686                                                         .setCardinality(
8687                                                                 AppSearchSchema.PropertyConfig
8688                                                                         .CARDINALITY_OPTIONAL)
8689                                                         .build())
8690                                         .build()),
8691                         /* visibilityConfigs= */ ImmutableList.of(
8692                                 new InternalVisibilityConfig.Builder("Type1")
8693                                         .addVisibleToPackage(
8694                                                 new PackageIdentifier(
8695                                                         fakeListeningPackage, new byte[0]))
8696                                         .build(),
8697                                 new InternalVisibilityConfig.Builder("Type2")
8698                                         .addVisibleToPackage(
8699                                                 new PackageIdentifier(
8700                                                         fakeListeningPackage, new byte[0]))
8701                                         .build()),
8702                         /* forceOverride= */ false,
8703                         /* version= */ 0,
8704                         /* setSchemaStatsBuilder= */ null);
8705         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
8706 
8707         // Dispatch notifications. This should look like a creation of Type2.
8708         assertThat(observer.getSchemaChanges()).isEmpty();
8709         assertThat(observer.getDocumentChanges()).isEmpty();
8710         mAppSearchImpl.dispatchAndClearChangeNotifications();
8711         assertThat(observer.getSchemaChanges())
8712                 .containsExactly(
8713                         new SchemaChangeInfo(
8714                                 mContext.getPackageName(), "database1", ImmutableSet.of("Type2")));
8715         assertThat(observer.getDocumentChanges()).isEmpty();
8716     }
8717 
8718     @Test
testAddObserver_schemaChange_visibilityAndContents()8719     public void testAddObserver_schemaChange_visibilityAndContents() throws Exception {
8720         final String fakeListeningPackage = "com.fake.listening.package";
8721 
8722         // Make a visibility checker that allows fakeListeningPackage access only to Type2.
8723         final VisibilityChecker visibilityChecker =
8724                 new VisibilityChecker() {
8725                     @Override
8726                     public boolean isSchemaSearchableByCaller(
8727                             @NonNull CallerAccess callerAccess,
8728                             @NonNull String packageName,
8729                             @NonNull String prefixedSchema,
8730                             @NonNull VisibilityStore visibilityStore) {
8731                         return callerAccess.getCallingPackageName().equals(fakeListeningPackage)
8732                                 && prefixedSchema.endsWith("Type2");
8733                     }
8734 
8735                     @Override
8736                     public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
8737                         return false;
8738                     }
8739                 };
8740         mAppSearchImpl.close();
8741         mAppSearchImpl =
8742                 AppSearchImpl.create(
8743                         mAppSearchDir,
8744                         new AppSearchConfigImpl(
8745                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
8746                         /* initStatsBuilder= */ null,
8747                         visibilityChecker,
8748                         /* revocableFileDescriptorStore= */ null,
8749                         ALWAYS_OPTIMIZE);
8750 
8751         // Add a schema.
8752         InternalSetSchemaResponse internalSetSchemaResponse =
8753                 mAppSearchImpl.setSchema(
8754                         mContext.getPackageName(),
8755                         "database1",
8756                         ImmutableList.of(
8757                                 new AppSearchSchema.Builder("Type1")
8758                                         .addProperty(
8759                                                 new AppSearchSchema.BooleanPropertyConfig.Builder(
8760                                                                 "booleanProp")
8761                                                         .setCardinality(
8762                                                                 AppSearchSchema.PropertyConfig
8763                                                                         .CARDINALITY_REQUIRED)
8764                                                         .build())
8765                                         .build(),
8766                                 new AppSearchSchema.Builder("Type2")
8767                                         .addProperty(
8768                                                 new AppSearchSchema.BooleanPropertyConfig.Builder(
8769                                                                 "booleanProp")
8770                                                         .setCardinality(
8771                                                                 AppSearchSchema.PropertyConfig
8772                                                                         .CARDINALITY_REQUIRED)
8773                                                         .build())
8774                                         .build()),
8775                         /* visibilityConfigs= */ Collections.emptyList(),
8776                         /* forceOverride= */ false,
8777                         /* version= */ 0,
8778                         /* setSchemaStatsBuilder= */ null);
8779         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
8780 
8781         // Register an observer
8782         TestObserverCallback observer = new TestObserverCallback();
8783         mAppSearchImpl.registerObserverCallback(
8784                 new CallerAccess(/* callingPackageName= */ fakeListeningPackage),
8785                 /* targetPackageName= */ mContext.getPackageName(),
8786                 new ObserverSpec.Builder().build(),
8787                 MoreExecutors.directExecutor(),
8788                 observer);
8789 
8790         // Update both types of the schema (changed cardinalities)
8791         internalSetSchemaResponse =
8792                 mAppSearchImpl.setSchema(
8793                         mContext.getPackageName(),
8794                         "database1",
8795                         ImmutableList.of(
8796                                 new AppSearchSchema.Builder("Type1")
8797                                         .addProperty(
8798                                                 new AppSearchSchema.BooleanPropertyConfig.Builder(
8799                                                                 "booleanProp")
8800                                                         .setCardinality(
8801                                                                 AppSearchSchema.PropertyConfig
8802                                                                         .CARDINALITY_OPTIONAL)
8803                                                         .build())
8804                                         .build(),
8805                                 new AppSearchSchema.Builder("Type2")
8806                                         .addProperty(
8807                                                 new AppSearchSchema.BooleanPropertyConfig.Builder(
8808                                                                 "booleanProp")
8809                                                         .setCardinality(
8810                                                                 AppSearchSchema.PropertyConfig
8811                                                                         .CARDINALITY_OPTIONAL)
8812                                                         .build())
8813                                         .build()),
8814                         /* visibilityConfigs= */ Collections.emptyList(),
8815                         /* forceOverride= */ false,
8816                         /* version= */ 0,
8817                         /* setSchemaStatsBuilder= */ null);
8818         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
8819 
8820         // Dispatch notifications
8821         assertThat(observer.getSchemaChanges()).isEmpty();
8822         assertThat(observer.getDocumentChanges()).isEmpty();
8823         mAppSearchImpl.dispatchAndClearChangeNotifications();
8824         assertThat(observer.getSchemaChanges())
8825                 .containsExactly(
8826                         new SchemaChangeInfo(
8827                                 mContext.getPackageName(), "database1", ImmutableSet.of("Type2")));
8828         assertThat(observer.getDocumentChanges()).isEmpty();
8829     }
8830 
8831     @Test
testAddObserver_schemaChange_partialVisibility_removed()8832     public void testAddObserver_schemaChange_partialVisibility_removed() throws Exception {
8833         final String fakeListeningPackage = "com.fake.listening.package";
8834 
8835         // Make a visibility checker that allows fakeListeningPackage access only to Type2.
8836         final VisibilityChecker visibilityChecker =
8837                 new VisibilityChecker() {
8838                     @Override
8839                     public boolean isSchemaSearchableByCaller(
8840                             @NonNull CallerAccess callerAccess,
8841                             @NonNull String packageName,
8842                             @NonNull String prefixedSchema,
8843                             @NonNull VisibilityStore visibilityStore) {
8844                         return callerAccess.getCallingPackageName().equals(fakeListeningPackage)
8845                                 && prefixedSchema.endsWith("Type2");
8846                     }
8847 
8848                     @Override
8849                     public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
8850                         return false;
8851                     }
8852                 };
8853         mAppSearchImpl.close();
8854         mAppSearchImpl =
8855                 AppSearchImpl.create(
8856                         mAppSearchDir,
8857                         new AppSearchConfigImpl(
8858                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
8859                         /* initStatsBuilder= */ null,
8860                         visibilityChecker,
8861                         /* revocableFileDescriptorStore= */ null,
8862                         ALWAYS_OPTIMIZE);
8863 
8864         // Add a schema.
8865         InternalSetSchemaResponse internalSetSchemaResponse =
8866                 mAppSearchImpl.setSchema(
8867                         mContext.getPackageName(),
8868                         "database1",
8869                         ImmutableList.of(
8870                                 new AppSearchSchema.Builder("Type1").build(),
8871                                 new AppSearchSchema.Builder("Type2").build()),
8872                         /* visibilityConfigs= */ Collections.emptyList(),
8873                         /* forceOverride= */ false,
8874                         /* version= */ 0,
8875                         /* setSchemaStatsBuilder= */ null);
8876         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
8877 
8878         // Register an observer
8879         TestObserverCallback observer = new TestObserverCallback();
8880         mAppSearchImpl.registerObserverCallback(
8881                 new CallerAccess(/* callingPackageName= */ fakeListeningPackage),
8882                 /* targetPackageName= */ mContext.getPackageName(),
8883                 new ObserverSpec.Builder().build(),
8884                 MoreExecutors.directExecutor(),
8885                 observer);
8886 
8887         // Remove Type1
8888         internalSetSchemaResponse =
8889                 mAppSearchImpl.setSchema(
8890                         mContext.getPackageName(),
8891                         "database1",
8892                         ImmutableList.of(new AppSearchSchema.Builder("Type2").build()),
8893                         /* visibilityConfigs= */ Collections.emptyList(),
8894                         /* forceOverride= */ true,
8895                         /* version= */ 0,
8896                         /* setSchemaStatsBuilder= */ null);
8897         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
8898 
8899         // Dispatch notifications. Nothing should appear since Type1 is not visible to us.
8900         assertThat(observer.getSchemaChanges()).isEmpty();
8901         assertThat(observer.getDocumentChanges()).isEmpty();
8902         mAppSearchImpl.dispatchAndClearChangeNotifications();
8903         assertThat(observer.getSchemaChanges()).isEmpty();
8904         assertThat(observer.getDocumentChanges()).isEmpty();
8905 
8906         // Now remove Type2. This should cause a notification.
8907         internalSetSchemaResponse =
8908                 mAppSearchImpl.setSchema(
8909                         mContext.getPackageName(),
8910                         "database1",
8911                         ImmutableList.of(),
8912                         /* visibilityConfigs= */ Collections.emptyList(),
8913                         /* forceOverride= */ true,
8914                         /* version= */ 0,
8915                         /* setSchemaStatsBuilder= */ null);
8916         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
8917         assertThat(observer.getSchemaChanges()).isEmpty();
8918         assertThat(observer.getDocumentChanges()).isEmpty();
8919         mAppSearchImpl.dispatchAndClearChangeNotifications();
8920         assertThat(observer.getSchemaChanges())
8921                 .containsExactly(
8922                         new SchemaChangeInfo(
8923                                 mContext.getPackageName(), "database1", ImmutableSet.of("Type2")));
8924         assertThat(observer.getDocumentChanges()).isEmpty();
8925     }
8926 
8927     @Test
testAddObserver_schemaChange_multipleObservers()8928     public void testAddObserver_schemaChange_multipleObservers() throws Exception {
8929         // Create two fake packages. One can access Type1, one can access Type2, they both can
8930         // access Type3, and no one can access Type4.
8931         final String fakePackage1 = "com.fake.listening.package1";
8932 
8933         final String fakePackage2 = "com.fake.listening.package2";
8934 
8935         final VisibilityChecker visibilityChecker =
8936                 new VisibilityChecker() {
8937                     @Override
8938                     public boolean isSchemaSearchableByCaller(
8939                             @NonNull CallerAccess callerAccess,
8940                             @NonNull String packageName,
8941                             @NonNull String prefixedSchema,
8942                             @NonNull VisibilityStore visibilityStore) {
8943                         if (prefixedSchema.endsWith("Type1")) {
8944                             return callerAccess.getCallingPackageName().equals(fakePackage1);
8945                         } else if (prefixedSchema.endsWith("Type2")) {
8946                             return callerAccess.getCallingPackageName().equals(fakePackage2);
8947                         } else if (prefixedSchema.endsWith("Type3")) {
8948                             return false;
8949                         } else if (prefixedSchema.endsWith("Type4")) {
8950                             return true;
8951                         } else {
8952                             throw new IllegalArgumentException(prefixedSchema);
8953                         }
8954                     }
8955 
8956                     @Override
8957                     public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
8958                         return false;
8959                     }
8960                 };
8961         mAppSearchImpl.close();
8962         mAppSearchImpl =
8963                 AppSearchImpl.create(
8964                         mAppSearchDir,
8965                         new AppSearchConfigImpl(
8966                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
8967                         /* initStatsBuilder= */ null,
8968                         visibilityChecker,
8969                         /* revocableFileDescriptorStore= */ null,
8970                         ALWAYS_OPTIMIZE);
8971 
8972         // Add a schema.
8973         InternalSetSchemaResponse internalSetSchemaResponse =
8974                 mAppSearchImpl.setSchema(
8975                         mContext.getPackageName(),
8976                         "database1",
8977                         ImmutableList.of(
8978                                 new AppSearchSchema.Builder("Type1").build(),
8979                                 new AppSearchSchema.Builder("Type2").build(),
8980                                 new AppSearchSchema.Builder("Type3").build(),
8981                                 new AppSearchSchema.Builder("Type4").build()),
8982                         /* visibilityConfigs= */ Collections.emptyList(),
8983                         /* forceOverride= */ false,
8984                         /* version= */ 0,
8985                         /* setSchemaStatsBuilder= */ null);
8986         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
8987 
8988         // Register three observers: one in each package, and another in package1 with a filter.
8989         TestObserverCallback observerPkg1NoFilter = new TestObserverCallback();
8990         mAppSearchImpl.registerObserverCallback(
8991                 new CallerAccess(/* callingPackageName= */ fakePackage1),
8992                 /* targetPackageName= */ mContext.getPackageName(),
8993                 new ObserverSpec.Builder().build(),
8994                 MoreExecutors.directExecutor(),
8995                 observerPkg1NoFilter);
8996 
8997         TestObserverCallback observerPkg2NoFilter = new TestObserverCallback();
8998         mAppSearchImpl.registerObserverCallback(
8999                 new CallerAccess(/* callingPackageName= */ fakePackage2),
9000                 /* targetPackageName= */ mContext.getPackageName(),
9001                 new ObserverSpec.Builder().build(),
9002                 MoreExecutors.directExecutor(),
9003                 observerPkg2NoFilter);
9004 
9005         TestObserverCallback observerPkg1FilterType4 = new TestObserverCallback();
9006         mAppSearchImpl.registerObserverCallback(
9007                 new CallerAccess(/* callingPackageName= */ fakePackage1),
9008                 /* targetPackageName= */ mContext.getPackageName(),
9009                 new ObserverSpec.Builder().addFilterSchemas("Type4").build(),
9010                 MoreExecutors.directExecutor(),
9011                 observerPkg1FilterType4);
9012 
9013         // Remove everything
9014         internalSetSchemaResponse =
9015                 mAppSearchImpl.setSchema(
9016                         mContext.getPackageName(),
9017                         "database1",
9018                         ImmutableList.of(),
9019                         /* visibilityConfigs= */ Collections.emptyList(),
9020                         /* forceOverride= */ true,
9021                         /* version= */ 0,
9022                         /* setSchemaStatsBuilder= */ null);
9023         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
9024 
9025         // Dispatch notifications.
9026         mAppSearchImpl.dispatchAndClearChangeNotifications();
9027 
9028         // observerPkg1NoFilter should see Type1 and Type4 vanish.
9029         // observerPkg2NoFilter should see Type2 and Type4 vanish.
9030         // observerPkg2WithFilter should see Type4 vanish.
9031         assertThat(observerPkg1NoFilter.getSchemaChanges())
9032                 .containsExactly(
9033                         new SchemaChangeInfo(
9034                                 mContext.getPackageName(),
9035                                 "database1",
9036                                 ImmutableSet.of("Type1", "Type4")));
9037         assertThat(observerPkg1NoFilter.getDocumentChanges()).isEmpty();
9038 
9039         assertThat(observerPkg2NoFilter.getSchemaChanges())
9040                 .containsExactly(
9041                         new SchemaChangeInfo(
9042                                 mContext.getPackageName(),
9043                                 "database1",
9044                                 ImmutableSet.of("Type2", "Type4")));
9045         assertThat(observerPkg2NoFilter.getDocumentChanges()).isEmpty();
9046 
9047         assertThat(observerPkg1FilterType4.getSchemaChanges())
9048                 .containsExactly(
9049                         new SchemaChangeInfo(
9050                                 mContext.getPackageName(), "database1", ImmutableSet.of("Type4")));
9051         assertThat(observerPkg1FilterType4.getDocumentChanges()).isEmpty();
9052     }
9053 
9054     @Test
testAddObserver_schemaChange_noChangeIfIncompatible()9055     public void testAddObserver_schemaChange_noChangeIfIncompatible() throws Exception {
9056         // Add a schema with two types.
9057         InternalSetSchemaResponse internalSetSchemaResponse =
9058                 mAppSearchImpl.setSchema(
9059                         mContext.getPackageName(),
9060                         "database1",
9061                         ImmutableList.of(
9062                                 new AppSearchSchema.Builder("Type1")
9063                                         .addProperty(
9064                                                 new AppSearchSchema.StringPropertyConfig.Builder(
9065                                                                 "strProp")
9066                                                         .setCardinality(
9067                                                                 AppSearchSchema.PropertyConfig
9068                                                                         .CARDINALITY_OPTIONAL)
9069                                                         .build())
9070                                         .build(),
9071                                 new AppSearchSchema.Builder("Type2")
9072                                         .addProperty(
9073                                                 new AppSearchSchema.StringPropertyConfig.Builder(
9074                                                                 "strProp")
9075                                                         .setCardinality(
9076                                                                 AppSearchSchema.PropertyConfig
9077                                                                         .CARDINALITY_OPTIONAL)
9078                                                         .build())
9079                                         .build()),
9080                         /* visibilityConfigs= */ Collections.emptyList(),
9081                         /* forceOverride= */ false,
9082                         /* version= */ 1,
9083                         /* setSchemaStatsBuilder= */ null);
9084         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
9085 
9086         // Register an observer
9087         TestObserverCallback observer = new TestObserverCallback();
9088         mAppSearchImpl.registerObserverCallback(
9089                 new CallerAccess(/* callingPackageName= */ mContext.getPackageName()),
9090                 /* targetPackageName= */ mContext.getPackageName(),
9091                 new ObserverSpec.Builder().build(),
9092                 MoreExecutors.directExecutor(),
9093                 observer);
9094 
9095         // Update schema to try to make an incompatible change to Type1, and a compatible change to
9096         // Type2.
9097         List<AppSearchSchema> updatedSchemaTypes =
9098                 ImmutableList.of(
9099                         new AppSearchSchema.Builder("Type1")
9100                                 .addProperty(
9101                                         new AppSearchSchema.StringPropertyConfig.Builder("strProp")
9102                                                 .setCardinality(
9103                                                         AppSearchSchema.PropertyConfig
9104                                                                 .CARDINALITY_REQUIRED)
9105                                                 .build())
9106                                 .build(),
9107                         new AppSearchSchema.Builder("Type2")
9108                                 .addProperty(
9109                                         new AppSearchSchema.StringPropertyConfig.Builder("strProp")
9110                                                 .setCardinality(
9111                                                         AppSearchSchema.PropertyConfig
9112                                                                 .CARDINALITY_REPEATED)
9113                                                 .build())
9114                                 .build());
9115         internalSetSchemaResponse =
9116                 mAppSearchImpl.setSchema(
9117                         mContext.getPackageName(),
9118                         "database1",
9119                         updatedSchemaTypes,
9120                         /* visibilityConfigs= */ Collections.emptyList(),
9121                         /* forceOverride= */ false,
9122                         /* version= */ 2,
9123                         /* setSchemaStatsBuilder= */ null);
9124         assertThat(internalSetSchemaResponse.isSuccess()).isFalse();
9125         SetSchemaResponse setSchemaResponse = internalSetSchemaResponse.getSetSchemaResponse();
9126         assertThat(setSchemaResponse.getDeletedTypes()).isEmpty();
9127         assertThat(setSchemaResponse.getIncompatibleTypes()).containsExactly("Type1");
9128 
9129         // Dispatch notifications. Nothing should appear since the schema was incompatible and has
9130         // not changed.
9131         assertThat(observer.getSchemaChanges()).isEmpty();
9132         assertThat(observer.getDocumentChanges()).isEmpty();
9133         mAppSearchImpl.dispatchAndClearChangeNotifications();
9134         assertThat(observer.getSchemaChanges()).isEmpty();
9135         assertThat(observer.getDocumentChanges()).isEmpty();
9136 
9137         // Now force apply the schemas Type2. This should cause a notification.
9138         internalSetSchemaResponse =
9139                 mAppSearchImpl.setSchema(
9140                         mContext.getPackageName(),
9141                         "database1",
9142                         updatedSchemaTypes,
9143                         /* visibilityConfigs= */ Collections.emptyList(),
9144                         /* forceOverride= */ true,
9145                         /* version= */ 3,
9146                         /* setSchemaStatsBuilder= */ null);
9147         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
9148         assertThat(observer.getSchemaChanges()).isEmpty();
9149         assertThat(observer.getDocumentChanges()).isEmpty();
9150         mAppSearchImpl.dispatchAndClearChangeNotifications();
9151         assertThat(observer.getSchemaChanges())
9152                 .containsExactly(
9153                         new SchemaChangeInfo(
9154                                 mContext.getPackageName(),
9155                                 "database1",
9156                                 ImmutableSet.of("Type1", "Type2")));
9157         assertThat(observer.getDocumentChanges()).isEmpty();
9158     }
9159 }
9160