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 android.app.appsearch.cts.app;
18 
19 import static android.app.appsearch.testutil.AppSearchTestUtils.calculateDigest;
20 import static android.app.appsearch.testutil.AppSearchTestUtils.checkIsBatchResultSuccess;
21 import static android.app.appsearch.testutil.AppSearchTestUtils.convertSearchResultsToDocuments;
22 import static android.app.appsearch.testutil.AppSearchTestUtils.generateRandomBytes;
23 import static android.app.appsearch.testutil.AppSearchTestUtils.retrieveAllSearchResults;
24 
25 import static com.google.common.truth.Truth.assertThat;
26 
27 import static org.junit.Assert.assertFalse;
28 import static org.junit.Assert.assertThrows;
29 import static org.junit.Assert.assertTrue;
30 import static org.junit.Assume.assumeFalse;
31 import static org.junit.Assume.assumeTrue;
32 
33 import android.annotation.NonNull;
34 import android.app.appsearch.AppSearchBatchResult;
35 import android.app.appsearch.AppSearchBlobHandle;
36 import android.app.appsearch.AppSearchResult;
37 import android.app.appsearch.AppSearchSchema;
38 import android.app.appsearch.AppSearchSchema.PropertyConfig;
39 import android.app.appsearch.AppSearchSessionShim;
40 import android.app.appsearch.Features;
41 import android.app.appsearch.GenericDocument;
42 import android.app.appsearch.GetByDocumentIdRequest;
43 import android.app.appsearch.GetSchemaResponse;
44 import android.app.appsearch.GlobalSearchSessionShim;
45 import android.app.appsearch.Migrator;
46 import android.app.appsearch.OpenBlobForReadResponse;
47 import android.app.appsearch.OpenBlobForWriteResponse;
48 import android.app.appsearch.PutDocumentsRequest;
49 import android.app.appsearch.RemoveByDocumentIdRequest;
50 import android.app.appsearch.ReportSystemUsageRequest;
51 import android.app.appsearch.SearchResult;
52 import android.app.appsearch.SearchResultsShim;
53 import android.app.appsearch.SearchSpec;
54 import android.app.appsearch.SetSchemaRequest;
55 import android.app.appsearch.exceptions.AppSearchException;
56 import android.app.appsearch.observer.DocumentChangeInfo;
57 import android.app.appsearch.observer.ObserverSpec;
58 import android.app.appsearch.observer.SchemaChangeInfo;
59 import android.app.appsearch.testutil.AppSearchEmail;
60 import android.app.appsearch.testutil.AppSearchTestUtils;
61 import android.app.appsearch.testutil.TestObserverCallback;
62 import android.content.Context;
63 import android.os.ParcelFileDescriptor;
64 import android.platform.test.annotations.RequiresFlagsEnabled;
65 
66 import androidx.test.core.app.ApplicationProvider;
67 
68 import com.android.appsearch.flags.Flags;
69 
70 import com.google.common.collect.ImmutableList;
71 import com.google.common.collect.ImmutableMap;
72 import com.google.common.collect.ImmutableSet;
73 import com.google.common.util.concurrent.ListenableFuture;
74 
75 import org.junit.After;
76 import org.junit.Before;
77 import org.junit.Rule;
78 import org.junit.Test;
79 import org.junit.rules.RuleChain;
80 
81 import java.io.InputStream;
82 import java.io.OutputStream;
83 import java.util.ArrayList;
84 import java.util.Collections;
85 import java.util.List;
86 import java.util.concurrent.ExecutionException;
87 import java.util.concurrent.Executor;
88 import java.util.concurrent.Executors;
89 
90 public abstract class GlobalSearchSessionCtsTestBase {
91     static final String DB_NAME_1 = "";
92     static final String DB_NAME_2 = "testDb2";
93 
94     private static final Executor EXECUTOR = Executors.newCachedThreadPool();
95     private final Context mContext = ApplicationProvider.getApplicationContext();
96 
97     protected AppSearchSessionShim mDb1;
98     protected AppSearchSessionShim mDb2;
99 
100     protected GlobalSearchSessionShim mGlobalSearchSession;
101 
102     @Rule public final RuleChain mRuleChain = AppSearchTestUtils.createCommonTestRules();
103 
createSearchSessionAsync( @onNull String dbName)104     protected abstract ListenableFuture<AppSearchSessionShim> createSearchSessionAsync(
105             @NonNull String dbName) throws Exception;
106 
createGlobalSearchSessionAsync()107     protected abstract ListenableFuture<GlobalSearchSessionShim> createGlobalSearchSessionAsync()
108             throws Exception;
109 
110     @Before
setUp()111     public void setUp() throws Exception {
112         mDb1 = createSearchSessionAsync(DB_NAME_1).get();
113         mDb2 = createSearchSessionAsync(DB_NAME_2).get();
114         // Cleanup whatever documents may still exist in these databases. This is needed in
115         // addition to tearDown in case a test exited without completing properly.
116         cleanup();
117 
118         mGlobalSearchSession = createGlobalSearchSessionAsync().get();
119     }
120 
121     @After
tearDown()122     public void tearDown() throws Exception {
123         // Cleanup whatever documents may still exist in these databases.
124         cleanup();
125     }
126 
cleanup()127     private void cleanup() throws Exception {
128         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
129         mDb2.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
130     }
131 
snapshotResults(String queryExpression, SearchSpec spec)132     private List<GenericDocument> snapshotResults(String queryExpression, SearchSpec spec)
133             throws Exception {
134         SearchResultsShim searchResults = mGlobalSearchSession.search(queryExpression, spec);
135         return convertSearchResultsToDocuments(searchResults);
136     }
137 
138     /**
139      * Asserts that the union of {@code addedDocuments} and {@code beforeDocuments} is exactly
140      * equivalent to {@code afterDocuments}. Order doesn't matter.
141      *
142      * @param beforeDocuments Documents that existed first.
143      * @param afterDocuments The total collection of documents that should exist now.
144      * @param addedDocuments The collection of documents that were expected to be added.
145      */
assertAddedBetweenSnapshots( List<? extends GenericDocument> beforeDocuments, List<? extends GenericDocument> afterDocuments, List<? extends GenericDocument> addedDocuments)146     private void assertAddedBetweenSnapshots(
147             List<? extends GenericDocument> beforeDocuments,
148             List<? extends GenericDocument> afterDocuments,
149             List<? extends GenericDocument> addedDocuments) {
150         List<GenericDocument> expectedDocuments = new ArrayList<>(beforeDocuments);
151         expectedDocuments.addAll(addedDocuments);
152         assertThat(afterDocuments).containsExactlyElementsIn(expectedDocuments);
153     }
154 
155     @Test
testGlobalGetById()156     public void testGlobalGetById() throws Exception {
157         assumeTrue(
158                 mGlobalSearchSession
159                         .getFeatures()
160                         .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_GET_BY_ID));
161         SearchSpec exactSearchSpec =
162                 new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY).build();
163 
164         // Schema registration
165         mDb1.setSchemaAsync(
166                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
167                 .get();
168 
169         AppSearchBatchResult<String, GenericDocument> nonExistent =
170                 mGlobalSearchSession
171                         .getByDocumentIdAsync(
172                                 mContext.getPackageName(),
173                                 DB_NAME_1,
174                                 new GetByDocumentIdRequest.Builder("namespace")
175                                         .addIds("id1")
176                                         .build())
177                         .get();
178 
179         assertThat(nonExistent.isSuccess()).isFalse();
180         assertThat(nonExistent.getSuccesses()).isEmpty();
181         assertThat(nonExistent.getFailures()).containsKey("id1");
182         assertThat(nonExistent.getFailures().get("id1").getResultCode())
183                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
184 
185         // Index a document
186         AppSearchEmail inEmail =
187                 new AppSearchEmail.Builder("namespace", "id1")
188                         .setFrom("[email protected]")
189                         .setTo("[email protected]", "[email protected]")
190                         .setSubject("testPut example")
191                         .setBody("This is the body of the testPut email")
192                         .build();
193         checkIsBatchResultSuccess(
194                 mDb1.putAsync(
195                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
196 
197         // Query for the document
198         AppSearchBatchResult<String, GenericDocument> afterPutDocuments =
199                 mGlobalSearchSession
200                         .getByDocumentIdAsync(
201                                 mContext.getPackageName(),
202                                 DB_NAME_1,
203                                 new GetByDocumentIdRequest.Builder("namespace")
204                                         .addIds("id1")
205                                         .build())
206                         .get();
207         assertThat(afterPutDocuments.getSuccesses()).containsExactly("id1", inEmail);
208     }
209 
210     @Test
testGlobalGetById_nonExistentPackage()211     public void testGlobalGetById_nonExistentPackage() throws Exception {
212         assumeTrue(
213                 mGlobalSearchSession
214                         .getFeatures()
215                         .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_GET_BY_ID));
216         AppSearchBatchResult<String, GenericDocument> fakePackage =
217                 mGlobalSearchSession
218                         .getByDocumentIdAsync(
219                                 "fake",
220                                 DB_NAME_1,
221                                 new GetByDocumentIdRequest.Builder("namespace")
222                                         .addIds("id1")
223                                         .build())
224                         .get();
225         assertThat(fakePackage.getFailures()).hasSize(1);
226         assertThat(fakePackage.getFailures().get("id1").getResultCode())
227                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
228     }
229 
230     @Test
testGlobalQuery_oneInstance()231     public void testGlobalQuery_oneInstance() throws Exception {
232         // Snapshot what documents may already exist on the device.
233         SearchSpec exactSearchSpec =
234                 new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY).build();
235         List<GenericDocument> beforeBodyDocuments = snapshotResults("body", exactSearchSpec);
236         List<GenericDocument> beforeBodyEmailDocuments =
237                 snapshotResults("body email", exactSearchSpec);
238 
239         // Schema registration
240         mDb1.setSchemaAsync(
241                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
242                 .get();
243 
244         // Index a document
245         AppSearchEmail inEmail =
246                 new AppSearchEmail.Builder("namespace", "id1")
247                         .setFrom("[email protected]")
248                         .setTo("[email protected]", "[email protected]")
249                         .setSubject("testPut example")
250                         .setBody("This is the body of the testPut email")
251                         .build();
252         checkIsBatchResultSuccess(
253                 mDb1.putAsync(
254                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
255 
256         // Query for the document
257         List<GenericDocument> afterBodyDocuments = snapshotResults("body", exactSearchSpec);
258         assertAddedBetweenSnapshots(
259                 beforeBodyDocuments, afterBodyDocuments, Collections.singletonList(inEmail));
260 
261         // Multi-term query
262         List<GenericDocument> afterBodyEmailDocuments =
263                 snapshotResults("body email", exactSearchSpec);
264         assertAddedBetweenSnapshots(
265                 beforeBodyEmailDocuments,
266                 afterBodyEmailDocuments,
267                 Collections.singletonList(inEmail));
268     }
269 
270     @Test
testGlobalQuery_twoInstances()271     public void testGlobalQuery_twoInstances() throws Exception {
272         // Snapshot what documents may already exist on the device.
273         SearchSpec exactSearchSpec =
274                 new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY).build();
275         List<GenericDocument> beforeBodyDocuments = snapshotResults("body", exactSearchSpec);
276 
277         // Schema registration
278         mDb1.setSchemaAsync(
279                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
280                 .get();
281         mDb2.setSchemaAsync(
282                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
283                 .get();
284 
285         // Index a document to instance 1.
286         AppSearchEmail inEmail1 =
287                 new AppSearchEmail.Builder("namespace", "id1")
288                         .setFrom("[email protected]")
289                         .setTo("[email protected]", "[email protected]")
290                         .setSubject("testPut example")
291                         .setBody("This is the body of the testPut email")
292                         .build();
293         checkIsBatchResultSuccess(
294                 mDb1.putAsync(
295                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
296 
297         // Index a document to instance 2.
298         AppSearchEmail inEmail2 =
299                 new AppSearchEmail.Builder("namespace", "id2")
300                         .setFrom("[email protected]")
301                         .setTo("[email protected]", "[email protected]")
302                         .setSubject("testPut example")
303                         .setBody("This is the body of the testPut email")
304                         .build();
305         checkIsBatchResultSuccess(
306                 mDb2.putAsync(
307                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
308 
309         // Query across all instances
310         List<GenericDocument> afterBodyDocuments = snapshotResults("body", exactSearchSpec);
311         assertAddedBetweenSnapshots(
312                 beforeBodyDocuments, afterBodyDocuments, ImmutableList.of(inEmail1, inEmail2));
313     }
314 
315     @Test
testGlobalQuery_getNextPage()316     public void testGlobalQuery_getNextPage() throws Exception {
317         // Snapshot what documents may already exist on the device.
318         SearchSpec exactSearchSpec =
319                 new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY).build();
320         List<GenericDocument> beforeBodyDocuments = snapshotResults("body", exactSearchSpec);
321 
322         // Schema registration
323         mDb1.setSchemaAsync(
324                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
325                 .get();
326         List<AppSearchEmail> emailList = new ArrayList<>();
327         PutDocumentsRequest.Builder putDocumentsRequestBuilder = new PutDocumentsRequest.Builder();
328 
329         // Index 31 documents
330         for (int i = 0; i < 31; i++) {
331             AppSearchEmail inEmail =
332                     new AppSearchEmail.Builder("namespace", "id" + i)
333                             .setFrom("[email protected]")
334                             .setTo("[email protected]", "[email protected]")
335                             .setSubject("testPut example")
336                             .setBody("This is the body of the testPut email")
337                             .build();
338             emailList.add(inEmail);
339             putDocumentsRequestBuilder.addGenericDocuments(inEmail);
340         }
341         checkIsBatchResultSuccess(mDb1.putAsync(putDocumentsRequestBuilder.build()));
342 
343         // Set number of results per page is 7.
344         int pageSize = 7;
345         SearchResultsShim searchResults =
346                 mGlobalSearchSession.search(
347                         "body",
348                         new SearchSpec.Builder()
349                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
350                                 .setResultCountPerPage(pageSize)
351                                 .build());
352         List<GenericDocument> documents = new ArrayList<>();
353 
354         int pageNumber = 0;
355         List<SearchResult> results;
356 
357         // keep loading next page until it's empty.
358         do {
359             results = searchResults.getNextPageAsync().get();
360             ++pageNumber;
361             for (SearchResult result : results) {
362                 documents.add(result.getGenericDocument());
363             }
364         } while (results.size() > 0);
365 
366         // check all document presents
367         assertAddedBetweenSnapshots(beforeBodyDocuments, documents, emailList);
368 
369         int totalDocuments = beforeBodyDocuments.size() + documents.size();
370 
371         // +1 for final empty page
372         int expectedPages = (int) Math.ceil(totalDocuments * 1.0 / pageSize) + 1;
373         assertThat(pageNumber).isEqualTo(expectedPages);
374     }
375 
376     @Test
testGlobalQuery_acrossTypes()377     public void testGlobalQuery_acrossTypes() throws Exception {
378         // Snapshot what documents may already exist on the device.
379         SearchSpec exactSearchSpec =
380                 new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY).build();
381         List<GenericDocument> beforeBodyDocuments = snapshotResults("body", exactSearchSpec);
382 
383         SearchSpec exactEmailSearchSpec =
384                 new SearchSpec.Builder()
385                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
386                         .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
387                         .build();
388         List<GenericDocument> beforeBodyEmailDocuments =
389                 snapshotResults("body", exactEmailSearchSpec);
390 
391         // Schema registration
392         AppSearchSchema genericSchema =
393                 new AppSearchSchema.Builder("Generic")
394                         .addProperty(
395                                 new AppSearchSchema.StringPropertyConfig.Builder("foo")
396                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
397                                         .setTokenizerType(
398                                                 AppSearchSchema.StringPropertyConfig
399                                                         .TOKENIZER_TYPE_PLAIN)
400                                         .setIndexingType(
401                                                 AppSearchSchema.StringPropertyConfig
402                                                         .INDEXING_TYPE_PREFIXES)
403                                         .build())
404                         .build();
405 
406         // db1 has both "Generic" and "builtin:Email"
407         mDb1.setSchemaAsync(
408                         new SetSchemaRequest.Builder()
409                                 .addSchemas(genericSchema)
410                                 .addSchemas(AppSearchEmail.SCHEMA)
411                                 .build())
412                 .get();
413 
414         // db2 only has "builtin:Email"
415         mDb2.setSchemaAsync(
416                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
417                 .get();
418 
419         // Index a generic document into db1
420         GenericDocument genericDocument =
421                 new GenericDocument.Builder<>("namespace", "id2", "Generic")
422                         .setPropertyString("foo", "body")
423                         .build();
424         checkIsBatchResultSuccess(
425                 mDb1.putAsync(
426                         new PutDocumentsRequest.Builder()
427                                 .addGenericDocuments(genericDocument)
428                                 .build()));
429 
430         AppSearchEmail email =
431                 new AppSearchEmail.Builder("namespace", "id1")
432                         .setFrom("[email protected]")
433                         .setTo("[email protected]", "[email protected]")
434                         .setSubject("testPut example")
435                         .setBody("This is the body of the testPut email")
436                         .build();
437 
438         // Put the email in both databases
439         checkIsBatchResultSuccess(
440                 (mDb1.putAsync(
441                         new PutDocumentsRequest.Builder().addGenericDocuments(email).build())));
442         checkIsBatchResultSuccess(
443                 mDb2.putAsync(
444                         new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
445 
446         // Query for all documents across types
447         List<GenericDocument> afterBodyDocuments = snapshotResults("body", exactSearchSpec);
448         assertAddedBetweenSnapshots(
449                 beforeBodyDocuments,
450                 afterBodyDocuments,
451                 ImmutableList.of(genericDocument, email, email));
452 
453         // Query only for email documents
454         List<GenericDocument> afterBodyEmailDocuments =
455                 snapshotResults("body", exactEmailSearchSpec);
456         assertAddedBetweenSnapshots(
457                 beforeBodyEmailDocuments, afterBodyEmailDocuments, ImmutableList.of(email, email));
458     }
459 
460     @Test
testGlobalQuery_namespaceFilter()461     public void testGlobalQuery_namespaceFilter() throws Exception {
462         // Snapshot what documents may already exist on the device.
463         SearchSpec exactSearchSpec =
464                 new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY).build();
465         List<GenericDocument> beforeBodyDocuments = snapshotResults("body", exactSearchSpec);
466 
467         SearchSpec exactNamespace1SearchSpec =
468                 new SearchSpec.Builder()
469                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
470                         .addFilterNamespaces("namespace1")
471                         .build();
472         List<GenericDocument> beforeBodyNamespace1Documents =
473                 snapshotResults("body", exactNamespace1SearchSpec);
474 
475         // Schema registration
476         mDb1.setSchemaAsync(
477                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
478                 .get();
479         mDb2.setSchemaAsync(
480                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
481                 .get();
482 
483         // Index two documents
484         AppSearchEmail document1 =
485                 new AppSearchEmail.Builder("namespace1", "id1")
486                         .setFrom("[email protected]")
487                         .setTo("[email protected]", "[email protected]")
488                         .setSubject("testPut example")
489                         .setBody("This is the body of the testPut email")
490                         .build();
491         checkIsBatchResultSuccess(
492                 mDb1.putAsync(
493                         new PutDocumentsRequest.Builder().addGenericDocuments(document1).build()));
494 
495         AppSearchEmail document2 =
496                 new AppSearchEmail.Builder("namespace2", "id1")
497                         .setFrom("[email protected]")
498                         .setTo("[email protected]", "[email protected]")
499                         .setSubject("testPut example")
500                         .setBody("This is the body of the testPut email")
501                         .build();
502         checkIsBatchResultSuccess(
503                 mDb2.putAsync(
504                         new PutDocumentsRequest.Builder().addGenericDocuments(document2).build()));
505 
506         // Query for all namespaces
507         List<GenericDocument> afterBodyDocuments = snapshotResults("body", exactSearchSpec);
508         assertAddedBetweenSnapshots(
509                 beforeBodyDocuments, afterBodyDocuments, ImmutableList.of(document1, document2));
510 
511         // Query only for "namespace1"
512         List<GenericDocument> afterBodyNamespace1Documents =
513                 snapshotResults("body", exactNamespace1SearchSpec);
514         assertAddedBetweenSnapshots(
515                 beforeBodyNamespace1Documents,
516                 afterBodyNamespace1Documents,
517                 ImmutableList.of(document1));
518     }
519 
520     @Test
testGlobalQuery_packageFilter()521     public void testGlobalQuery_packageFilter() throws Exception {
522         // Snapshot what documents may already exist on the device.
523         SearchSpec otherPackageSearchSpec =
524                 new SearchSpec.Builder()
525                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
526                         .addFilterPackageNames("some.other.package")
527                         .build();
528         List<GenericDocument> beforeOtherPackageDocuments =
529                 snapshotResults("body", otherPackageSearchSpec);
530 
531         SearchSpec testPackageSearchSpec =
532                 new SearchSpec.Builder()
533                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
534                         .addFilterPackageNames(mContext.getPackageName())
535                         .build();
536         List<GenericDocument> beforeTestPackageDocuments =
537                 snapshotResults("body", testPackageSearchSpec);
538 
539         // Schema registration
540         mDb1.setSchemaAsync(
541                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
542                 .get();
543         mDb2.setSchemaAsync(
544                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
545                 .get();
546 
547         // Index two documents
548         AppSearchEmail document1 =
549                 new AppSearchEmail.Builder("namespace1", "id1")
550                         .setFrom("[email protected]")
551                         .setTo("[email protected]", "[email protected]")
552                         .setSubject("testPut example")
553                         .setBody("This is the body of the testPut email")
554                         .build();
555         checkIsBatchResultSuccess(
556                 mDb1.putAsync(
557                         new PutDocumentsRequest.Builder().addGenericDocuments(document1).build()));
558 
559         AppSearchEmail document2 =
560                 new AppSearchEmail.Builder("namespace2", "id1")
561                         .setFrom("[email protected]")
562                         .setTo("[email protected]", "[email protected]")
563                         .setSubject("testPut example")
564                         .setBody("This is the body of the testPut email")
565                         .build();
566         checkIsBatchResultSuccess(
567                 mDb2.putAsync(
568                         new PutDocumentsRequest.Builder().addGenericDocuments(document2).build()));
569 
570         // Query in some other package
571         List<GenericDocument> afterOtherPackageDocuments =
572                 snapshotResults("body", otherPackageSearchSpec);
573         assertAddedBetweenSnapshots(
574                 beforeOtherPackageDocuments, afterOtherPackageDocuments, Collections.emptyList());
575 
576         // Query within our package
577         List<GenericDocument> afterTestPackageDocuments =
578                 snapshotResults("body", testPackageSearchSpec);
579         assertAddedBetweenSnapshots(
580                 beforeTestPackageDocuments,
581                 afterTestPackageDocuments,
582                 ImmutableList.of(document1, document2));
583     }
584 
585     // TODO(b/175039682) Add test cases for wildcard projection once go/oag/1534646 is submitted.
586     @Test
testGlobalQuery_projectionTwoInstances()587     public void testGlobalQuery_projectionTwoInstances() throws Exception {
588         // Schema registration
589         mDb1.setSchemaAsync(
590                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
591                 .get();
592         mDb2.setSchemaAsync(
593                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
594                 .get();
595 
596         // Index one document in each database.
597         AppSearchEmail email1 =
598                 new AppSearchEmail.Builder("namespace", "id1")
599                         .setCreationTimestampMillis(1000)
600                         .setFrom("[email protected]")
601                         .setTo("[email protected]", "[email protected]")
602                         .setSubject("testPut example")
603                         .setBody("This is the body of the testPut email")
604                         .build();
605         checkIsBatchResultSuccess(
606                 mDb1.putAsync(
607                         new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
608 
609         AppSearchEmail email2 =
610                 new AppSearchEmail.Builder("namespace", "id2")
611                         .setCreationTimestampMillis(1000)
612                         .setFrom("[email protected]")
613                         .setTo("[email protected]", "[email protected]")
614                         .setSubject("testPut example")
615                         .setBody("This is the body of the testPut email")
616                         .build();
617         checkIsBatchResultSuccess(
618                 mDb2.putAsync(
619                         new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
620 
621         // Query with type property paths {"Email", ["subject", "to"]}
622         List<GenericDocument> documents =
623                 snapshotResults(
624                         "body",
625                         new SearchSpec.Builder()
626                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
627                                 .addProjection(
628                                         AppSearchEmail.SCHEMA_TYPE,
629                                         ImmutableList.of("subject", "to"))
630                                 .build());
631 
632         // The two email documents should have been returned with only the "subject" and "to"
633         // properties.
634         AppSearchEmail expected1 =
635                 new AppSearchEmail.Builder("namespace", "id2")
636                         .setCreationTimestampMillis(1000)
637                         .setTo("[email protected]", "[email protected]")
638                         .setSubject("testPut example")
639                         .build();
640         AppSearchEmail expected2 =
641                 new AppSearchEmail.Builder("namespace", "id1")
642                         .setCreationTimestampMillis(1000)
643                         .setTo("[email protected]", "[email protected]")
644                         .setSubject("testPut example")
645                         .build();
646         assertThat(documents).containsExactly(expected1, expected2);
647     }
648 
649     @Test
testGlobalQuery_projectionEmptyTwoInstances()650     public void testGlobalQuery_projectionEmptyTwoInstances() throws Exception {
651         // Schema registration
652         mDb1.setSchemaAsync(
653                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
654                 .get();
655         mDb2.setSchemaAsync(
656                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
657                 .get();
658 
659         // Index one document in each database.
660         AppSearchEmail email1 =
661                 new AppSearchEmail.Builder("namespace", "id1")
662                         .setCreationTimestampMillis(1000)
663                         .setFrom("[email protected]")
664                         .setTo("[email protected]", "[email protected]")
665                         .setSubject("testPut example")
666                         .setBody("This is the body of the testPut email")
667                         .build();
668         checkIsBatchResultSuccess(
669                 mDb1.putAsync(
670                         new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
671 
672         AppSearchEmail email2 =
673                 new AppSearchEmail.Builder("namespace", "id2")
674                         .setCreationTimestampMillis(1000)
675                         .setFrom("[email protected]")
676                         .setTo("[email protected]", "[email protected]")
677                         .setSubject("testPut example")
678                         .setBody("This is the body of the testPut email")
679                         .build();
680         checkIsBatchResultSuccess(
681                 mDb2.putAsync(
682                         new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
683 
684         // Query with type property paths {"Email", []}
685         List<GenericDocument> documents =
686                 snapshotResults(
687                         "body",
688                         new SearchSpec.Builder()
689                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
690                                 .addProjection(AppSearchEmail.SCHEMA_TYPE, Collections.emptyList())
691                                 .build());
692 
693         // The two email documents should have been returned without any properties.
694         AppSearchEmail expected1 =
695                 new AppSearchEmail.Builder("namespace", "id2")
696                         .setCreationTimestampMillis(1000)
697                         .build();
698         AppSearchEmail expected2 =
699                 new AppSearchEmail.Builder("namespace", "id1")
700                         .setCreationTimestampMillis(1000)
701                         .build();
702         assertThat(documents).containsExactly(expected1, expected2);
703     }
704 
705     @Test
testGlobalQuery_projectionNonExistentTypeTwoInstances()706     public void testGlobalQuery_projectionNonExistentTypeTwoInstances() throws Exception {
707         // Schema registration
708         mDb1.setSchemaAsync(
709                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
710                 .get();
711         mDb2.setSchemaAsync(
712                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
713                 .get();
714 
715         // Index one document in each database.
716         AppSearchEmail email1 =
717                 new AppSearchEmail.Builder("namespace", "id1")
718                         .setCreationTimestampMillis(1000)
719                         .setFrom("[email protected]")
720                         .setTo("[email protected]", "[email protected]")
721                         .setSubject("testPut example")
722                         .setBody("This is the body of the testPut email")
723                         .build();
724         checkIsBatchResultSuccess(
725                 mDb1.putAsync(
726                         new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
727 
728         AppSearchEmail email2 =
729                 new AppSearchEmail.Builder("namespace", "id2")
730                         .setCreationTimestampMillis(1000)
731                         .setFrom("[email protected]")
732                         .setTo("[email protected]", "[email protected]")
733                         .setSubject("testPut example")
734                         .setBody("This is the body of the testPut email")
735                         .build();
736         checkIsBatchResultSuccess(
737                 mDb2.putAsync(
738                         new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
739 
740         // Query with type property paths {"NonExistentType", []}, {"Email", ["subject", "to"]}
741         List<GenericDocument> documents =
742                 snapshotResults(
743                         "body",
744                         new SearchSpec.Builder()
745                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
746                                 .addProjection("NonExistentType", Collections.emptyList())
747                                 .addProjection(
748                                         AppSearchEmail.SCHEMA_TYPE,
749                                         ImmutableList.of("subject", "to"))
750                                 .build());
751 
752         // The two email documents should have been returned with only the "subject" and "to"
753         // properties.
754         AppSearchEmail expected1 =
755                 new AppSearchEmail.Builder("namespace", "id2")
756                         .setCreationTimestampMillis(1000)
757                         .setTo("[email protected]", "[email protected]")
758                         .setSubject("testPut example")
759                         .build();
760         AppSearchEmail expected2 =
761                 new AppSearchEmail.Builder("namespace", "id1")
762                         .setCreationTimestampMillis(1000)
763                         .setTo("[email protected]", "[email protected]")
764                         .setSubject("testPut example")
765                         .build();
766         assertThat(documents).containsExactly(expected1, expected2);
767     }
768 
769     @Test
770     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_DOCUMENT_IDS)
testGlobalQuery_documentIdFilter()771     public void testGlobalQuery_documentIdFilter() throws Exception {
772         assumeTrue(
773                 mDb1.getFeatures()
774                         .isFeatureSupported(Features.SEARCH_SPEC_ADD_FILTER_DOCUMENT_IDS));
775 
776         // Schema registration
777         mDb1.setSchemaAsync(
778                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
779                 .get();
780         mDb2.setSchemaAsync(
781                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
782                 .get();
783 
784         // Index 3 documents to db1.
785         AppSearchEmail email1_db1 =
786                 new AppSearchEmail.Builder("namespace", "id1")
787                         .setFrom("[email protected]")
788                         .setTo("[email protected]", "[email protected]")
789                         .setSubject("testPut example")
790                         .setBody("I am from database 1")
791                         .build();
792         AppSearchEmail email2_db1 =
793                 new AppSearchEmail.Builder("namespace", "id2")
794                         .setFrom("[email protected]")
795                         .setTo("[email protected]", "[email protected]")
796                         .setSubject("testPut example")
797                         .setBody("I am from database 1")
798                         .build();
799         AppSearchEmail email3_db1 =
800                 new AppSearchEmail.Builder("namespace", "id3")
801                         .setFrom("[email protected]")
802                         .setTo("[email protected]", "[email protected]")
803                         .setSubject("testPut example")
804                         .setBody("I am from database 1")
805                         .build();
806         checkIsBatchResultSuccess(
807                 mDb1.putAsync(
808                         new PutDocumentsRequest.Builder()
809                                 .addGenericDocuments(email1_db1, email2_db1, email3_db1)
810                                 .build()));
811 
812         // Index the similar 3 documents with the same ids but with different body values to db2.
813         AppSearchEmail email1_db2 =
814                 new AppSearchEmail.Builder("namespace", "id1")
815                         .setFrom("[email protected]")
816                         .setTo("[email protected]", "[email protected]")
817                         .setSubject("testPut example")
818                         .setBody("I am from database 2")
819                         .build();
820         AppSearchEmail email2_db2 =
821                 new AppSearchEmail.Builder("namespace", "id2")
822                         .setFrom("[email protected]")
823                         .setTo("[email protected]", "[email protected]")
824                         .setSubject("testPut example")
825                         .setBody("I am from database 2")
826                         .build();
827         AppSearchEmail email3_db2 =
828                 new AppSearchEmail.Builder("namespace", "id3")
829                         .setFrom("[email protected]")
830                         .setTo("[email protected]", "[email protected]")
831                         .setSubject("testPut example")
832                         .setBody("I am from database 2")
833                         .build();
834         checkIsBatchResultSuccess(
835                 mDb2.putAsync(
836                         new PutDocumentsRequest.Builder()
837                                 .addGenericDocuments(email1_db2, email2_db2, email3_db2)
838                                 .build()));
839 
840         // Query for "id1", which should return the documents with "id1" from both of the databases.
841         List<GenericDocument> documents =
842                 snapshotResults(
843                         "example",
844                         new SearchSpec.Builder()
845                                 .addFilterDocumentIds(ImmutableSet.of("id1"))
846                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
847                                 .build());
848         assertThat(documents).containsExactly(email1_db1, email1_db2);
849     }
850 
851     @Test
testQuery_ResultGroupingLimits()852     public void testQuery_ResultGroupingLimits() throws Exception {
853         // Schema registration
854         mDb1.setSchemaAsync(
855                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
856                 .get();
857         mDb2.setSchemaAsync(
858                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
859                 .get();
860 
861         // Index one document in 'namespace1' and one document in 'namespace2' into db1.
862         AppSearchEmail inEmail1 =
863                 new AppSearchEmail.Builder("namespace1", "id1")
864                         .setFrom("[email protected]")
865                         .setTo("[email protected]", "[email protected]")
866                         .setSubject("testPut example")
867                         .setBody("This is the body of the testPut email")
868                         .build();
869         checkIsBatchResultSuccess(
870                 mDb1.putAsync(
871                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
872         AppSearchEmail inEmail2 =
873                 new AppSearchEmail.Builder("namespace2", "id2")
874                         .setFrom("[email protected]")
875                         .setTo("[email protected]", "[email protected]")
876                         .setSubject("testPut example")
877                         .setBody("This is the body of the testPut email")
878                         .build();
879         checkIsBatchResultSuccess(
880                 mDb1.putAsync(
881                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
882 
883         // Index one document in 'namespace1' and one document in 'namespace2' into db2.
884         AppSearchEmail inEmail3 =
885                 new AppSearchEmail.Builder("namespace1", "id3")
886                         .setFrom("[email protected]")
887                         .setTo("[email protected]", "[email protected]")
888                         .setSubject("testPut example")
889                         .setBody("This is the body of the testPut email")
890                         .build();
891         checkIsBatchResultSuccess(
892                 mDb2.putAsync(
893                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));
894         AppSearchEmail inEmail4 =
895                 new AppSearchEmail.Builder("namespace2", "id4")
896                         .setFrom("[email protected]")
897                         .setTo("[email protected]", "[email protected]")
898                         .setSubject("testPut example")
899                         .setBody("This is the body of the testPut email")
900                         .build();
901         checkIsBatchResultSuccess(
902                 mDb2.putAsync(
903                         new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));
904 
905         // Query with per package result grouping. Only the last document 'email4' should be
906         // returned.
907         List<GenericDocument> documents =
908                 snapshotResults(
909                         "body",
910                         new SearchSpec.Builder()
911                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
912                                 .setResultGrouping(
913                                         SearchSpec.GROUPING_TYPE_PER_PACKAGE, /* limit= */ 1)
914                                 .build());
915         assertThat(documents).containsExactly(inEmail4);
916 
917         // Query with per namespace result grouping. Only the last document in each namespace should
918         // be returned ('email4' and 'email3').
919         documents =
920                 snapshotResults(
921                         "body",
922                         new SearchSpec.Builder()
923                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
924                                 .setResultGrouping(
925                                         SearchSpec.GROUPING_TYPE_PER_NAMESPACE, /* limit= */ 1)
926                                 .build());
927         assertThat(documents).containsExactly(inEmail4, inEmail3);
928 
929         // Query with per package and per namespace result grouping. Only the last document in each
930         // namespace should be returned ('email4' and 'email3').
931         documents =
932                 snapshotResults(
933                         "body",
934                         new SearchSpec.Builder()
935                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
936                                 .setResultGrouping(
937                                         SearchSpec.GROUPING_TYPE_PER_NAMESPACE
938                                                 | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
939                                         /* limit= */ 1)
940                                 .build());
941         assertThat(documents).containsExactly(inEmail4, inEmail3);
942     }
943 
944     @Test
testReportSystemUsage_ForbiddenFromNonSystem()945     public void testReportSystemUsage_ForbiddenFromNonSystem() throws Exception {
946         // Index a document
947         mDb1.setSchemaAsync(
948                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
949                 .get();
950         AppSearchEmail email1 =
951                 new AppSearchEmail.Builder("namespace", "id1")
952                         .setCreationTimestampMillis(1000)
953                         .setFrom("[email protected]")
954                         .setTo("[email protected]", "[email protected]")
955                         .setSubject("testPut example")
956                         .setBody("This is the body of the testPut email")
957                         .build();
958         checkIsBatchResultSuccess(
959                 mDb1.putAsync(
960                         new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
961 
962         // Query
963         List<SearchResult> page;
964         try (SearchResultsShim results =
965                 mGlobalSearchSession.search(
966                         "",
967                         new SearchSpec.Builder()
968                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
969                                 .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
970                                 .build())) {
971             page = results.getNextPageAsync().get();
972         }
973         assertThat(page).isNotEmpty();
974         SearchResult firstResult = page.get(0);
975 
976         ExecutionException exception =
977                 assertThrows(
978                         ExecutionException.class,
979                         () ->
980                                 mGlobalSearchSession
981                                         .reportSystemUsageAsync(
982                                                 new ReportSystemUsageRequest.Builder(
983                                                                 firstResult.getPackageName(),
984                                                                 firstResult.getDatabaseName(),
985                                                                 firstResult
986                                                                         .getGenericDocument()
987                                                                         .getNamespace(),
988                                                                 firstResult
989                                                                         .getGenericDocument()
990                                                                         .getId())
991                                                         .build())
992                                         .get());
993         assertThat(exception).hasCauseThat().isInstanceOf(AppSearchException.class);
994         AppSearchException ase = (AppSearchException) exception.getCause();
995         assertThat(ase.getResultCode()).isEqualTo(AppSearchResult.RESULT_SECURITY_ERROR);
996         assertThat(ase)
997                 .hasMessageThat()
998                 .contains(
999                         mContext.getPackageName() + " does not have access to report system usage");
1000     }
1001 
1002     @Test
testAddObserver_notSupported()1003     public void testAddObserver_notSupported() {
1004         assumeFalse(
1005                 mGlobalSearchSession
1006                         .getFeatures()
1007                         .isFeatureSupported(
1008                                 Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
1009         assertThrows(
1010                 UnsupportedOperationException.class,
1011                 () ->
1012                         mGlobalSearchSession.registerObserverCallback(
1013                                 mContext.getPackageName(),
1014                                 new ObserverSpec.Builder().build(),
1015                                 EXECUTOR,
1016                                 new TestObserverCallback()));
1017         assertThrows(
1018                 UnsupportedOperationException.class,
1019                 () ->
1020                         mGlobalSearchSession.unregisterObserverCallback(
1021                                 mContext.getPackageName(), new TestObserverCallback()));
1022     }
1023 
1024     @Test
testAddObserver()1025     public void testAddObserver() throws Exception {
1026         assumeTrue(
1027                 mGlobalSearchSession
1028                         .getFeatures()
1029                         .isFeatureSupported(
1030                                 Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
1031 
1032         TestObserverCallback observer = new TestObserverCallback();
1033 
1034         // Register observer. Note: the type does NOT exist yet!
1035         mGlobalSearchSession.registerObserverCallback(
1036                 mContext.getPackageName(),
1037                 new ObserverSpec.Builder().addFilterSchemas("TestAddObserver-Type").build(),
1038                 EXECUTOR,
1039                 observer);
1040         try {
1041             // Index a document
1042             mDb1.setSchemaAsync(
1043                             new SetSchemaRequest.Builder()
1044                                     .addSchemas(
1045                                             new AppSearchSchema.Builder("TestAddObserver-Type")
1046                                                     .build())
1047                                     .build())
1048                     .get();
1049             GenericDocument document =
1050                     new GenericDocument.Builder<GenericDocument.Builder<?>>(
1051                                     "namespace", "testAddObserver-id1", "TestAddObserver-Type")
1052                             .build();
1053             checkIsBatchResultSuccess(
1054                     mDb1.putAsync(
1055                             new PutDocumentsRequest.Builder()
1056                                     .addGenericDocuments(document)
1057                                     .build()));
1058 
1059             // Make sure the notification was received.
1060             observer.waitForNotificationCount(2);
1061             assertThat(observer.getSchemaChanges())
1062                     .containsExactly(
1063                             new SchemaChangeInfo(
1064                                     mContext.getPackageName(),
1065                                     DB_NAME_1,
1066                                     /* changedSchemaNames= */ ImmutableSet.of(
1067                                             "TestAddObserver-Type")));
1068             assertThat(observer.getDocumentChanges())
1069                     .containsExactly(
1070                             new DocumentChangeInfo(
1071                                     mContext.getPackageName(),
1072                                     DB_NAME_1,
1073                                     "namespace",
1074                                     "TestAddObserver-Type",
1075                                     /* changedDocumentIds= */ ImmutableSet.of(
1076                                             "testAddObserver-id1")));
1077         } finally {
1078             // Clean the observer
1079             mGlobalSearchSession.unregisterObserverCallback(mContext.getPackageName(), observer);
1080         }
1081     }
1082 
1083     @Test
testRegisterObserver_MultiType()1084     public void testRegisterObserver_MultiType() throws Exception {
1085         assumeTrue(
1086                 mGlobalSearchSession
1087                         .getFeatures()
1088                         .isFeatureSupported(
1089                                 Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
1090 
1091         TestObserverCallback unfilteredObserver = new TestObserverCallback();
1092         TestObserverCallback emailObserver = new TestObserverCallback();
1093 
1094         // Set up the email type in both databases, and the gift type in db1
1095         AppSearchSchema giftSchema =
1096                 new AppSearchSchema.Builder("Gift")
1097                         .addProperty(
1098                                 new AppSearchSchema.DoublePropertyConfig.Builder("price").build())
1099                         .build();
1100         mDb1.setSchemaAsync(
1101                         new SetSchemaRequest.Builder()
1102                                 .addSchemas(AppSearchEmail.SCHEMA, giftSchema)
1103                                 .build())
1104                 .get();
1105         mDb2.setSchemaAsync(
1106                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
1107                 .get();
1108 
1109         // Register two observers. One has no filters, the other filters on email.
1110         mGlobalSearchSession.registerObserverCallback(
1111                 mContext.getPackageName(),
1112                 new ObserverSpec.Builder().build(),
1113                 EXECUTOR,
1114                 unfilteredObserver);
1115         mGlobalSearchSession.registerObserverCallback(
1116                 mContext.getPackageName(),
1117                 new ObserverSpec.Builder().addFilterSchemas(AppSearchEmail.SCHEMA_TYPE).build(),
1118                 EXECUTOR,
1119                 emailObserver);
1120         try {
1121             // Make sure everything is empty
1122             assertThat(unfilteredObserver.getSchemaChanges()).isEmpty();
1123             assertThat(unfilteredObserver.getDocumentChanges()).isEmpty();
1124             assertThat(emailObserver.getSchemaChanges()).isEmpty();
1125             assertThat(emailObserver.getDocumentChanges()).isEmpty();
1126 
1127             // Index some documents
1128             AppSearchEmail email1 = new AppSearchEmail.Builder("namespace", "id1").build();
1129             GenericDocument gift1 =
1130                     new GenericDocument.Builder<GenericDocument.Builder<?>>(
1131                                     "namespace2", "id2", "Gift")
1132                             .build();
1133 
1134             checkIsBatchResultSuccess(
1135                     mDb1.putAsync(
1136                             new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
1137             checkIsBatchResultSuccess(
1138                     mDb1.putAsync(
1139                             new PutDocumentsRequest.Builder()
1140                                     .addGenericDocuments(email1, gift1)
1141                                     .build()));
1142             checkIsBatchResultSuccess(
1143                     mDb2.putAsync(
1144                             new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
1145             checkIsBatchResultSuccess(
1146                     mDb1.putAsync(
1147                             new PutDocumentsRequest.Builder().addGenericDocuments(gift1).build()));
1148 
1149             // Make sure the notification was received.
1150             unfilteredObserver.waitForNotificationCount(5);
1151             emailObserver.waitForNotificationCount(3);
1152 
1153             assertThat(unfilteredObserver.getSchemaChanges()).isEmpty();
1154             assertThat(unfilteredObserver.getDocumentChanges())
1155                     .containsExactly(
1156                             new DocumentChangeInfo(
1157                                     mContext.getPackageName(),
1158                                     DB_NAME_1,
1159                                     "namespace",
1160                                     AppSearchEmail.SCHEMA_TYPE,
1161                                     /* changedDocumentIds= */ ImmutableSet.of("id1")),
1162                             new DocumentChangeInfo(
1163                                     mContext.getPackageName(),
1164                                     DB_NAME_1,
1165                                     "namespace",
1166                                     AppSearchEmail.SCHEMA_TYPE,
1167                                     /* changedDocumentIds= */ ImmutableSet.of("id1")),
1168                             new DocumentChangeInfo(
1169                                     mContext.getPackageName(),
1170                                     DB_NAME_1,
1171                                     "namespace2",
1172                                     "Gift",
1173                                     /* changedDocumentIds= */ ImmutableSet.of("id2")),
1174                             new DocumentChangeInfo(
1175                                     mContext.getPackageName(),
1176                                     DB_NAME_2,
1177                                     "namespace",
1178                                     AppSearchEmail.SCHEMA_TYPE,
1179                                     /* changedDocumentIds= */ ImmutableSet.of("id1")),
1180                             new DocumentChangeInfo(
1181                                     mContext.getPackageName(),
1182                                     DB_NAME_1,
1183                                     "namespace2",
1184                                     "Gift",
1185                                     /* changedDocumentIds= */ ImmutableSet.of("id2")));
1186 
1187             // Check the filtered observer
1188             assertThat(emailObserver.getSchemaChanges()).isEmpty();
1189             assertThat(emailObserver.getDocumentChanges())
1190                     .containsExactly(
1191                             new DocumentChangeInfo(
1192                                     mContext.getPackageName(),
1193                                     DB_NAME_1,
1194                                     "namespace",
1195                                     AppSearchEmail.SCHEMA_TYPE,
1196                                     /* changedDocumentIds= */ ImmutableSet.of("id1")),
1197                             new DocumentChangeInfo(
1198                                     mContext.getPackageName(),
1199                                     DB_NAME_1,
1200                                     "namespace",
1201                                     AppSearchEmail.SCHEMA_TYPE,
1202                                     /* changedDocumentIds= */ ImmutableSet.of("id1")),
1203                             new DocumentChangeInfo(
1204                                     mContext.getPackageName(),
1205                                     DB_NAME_2,
1206                                     "namespace",
1207                                     AppSearchEmail.SCHEMA_TYPE,
1208                                     /* changedDocumentIds= */ ImmutableSet.of("id1")));
1209         } finally {
1210             // Clean the observer
1211             mGlobalSearchSession.unregisterObserverCallback(
1212                     mContext.getPackageName(), emailObserver);
1213             mGlobalSearchSession.unregisterObserverCallback(
1214                     mContext.getPackageName(), unfilteredObserver);
1215         }
1216     }
1217 
1218     @Test
testRegisterObserver_removeById()1219     public void testRegisterObserver_removeById() throws Exception {
1220         assumeTrue(
1221                 mGlobalSearchSession
1222                         .getFeatures()
1223                         .isFeatureSupported(
1224                                 Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
1225 
1226         TestObserverCallback unfilteredObserver = new TestObserverCallback();
1227         TestObserverCallback emailObserver = new TestObserverCallback();
1228 
1229         // Set up the email and gift types in both databases
1230         AppSearchSchema giftSchema =
1231                 new AppSearchSchema.Builder("Gift")
1232                         .addProperty(
1233                                 new AppSearchSchema.DoublePropertyConfig.Builder("price").build())
1234                         .build();
1235         mDb1.setSchemaAsync(
1236                         new SetSchemaRequest.Builder()
1237                                 .addSchemas(AppSearchEmail.SCHEMA, giftSchema)
1238                                 .build())
1239                 .get();
1240         mDb2.setSchemaAsync(
1241                         new SetSchemaRequest.Builder()
1242                                 .addSchemas(AppSearchEmail.SCHEMA, giftSchema)
1243                                 .build())
1244                 .get();
1245 
1246         // Register two observers. One, registered later, has no filters. The other, registered
1247         // now, filters on email.
1248         mGlobalSearchSession.registerObserverCallback(
1249                 mContext.getPackageName(),
1250                 new ObserverSpec.Builder().addFilterSchemas(AppSearchEmail.SCHEMA_TYPE).build(),
1251                 EXECUTOR,
1252                 emailObserver);
1253         try {
1254             // Make sure everything is empty
1255             assertThat(unfilteredObserver.getSchemaChanges()).isEmpty();
1256             assertThat(unfilteredObserver.getDocumentChanges()).isEmpty();
1257             assertThat(emailObserver.getSchemaChanges()).isEmpty();
1258             assertThat(emailObserver.getDocumentChanges()).isEmpty();
1259 
1260             // Index some documents
1261             AppSearchEmail email1 = new AppSearchEmail.Builder("namespace", "id1").build();
1262             GenericDocument gift1 =
1263                     new GenericDocument.Builder<GenericDocument.Builder<?>>(
1264                                     "namespace2", "id2", "Gift")
1265                             .build();
1266 
1267             checkIsBatchResultSuccess(
1268                     mDb1.putAsync(
1269                             new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
1270             checkIsBatchResultSuccess(
1271                     mDb1.putAsync(
1272                             new PutDocumentsRequest.Builder()
1273                                     .addGenericDocuments(email1, gift1)
1274                                     .build()));
1275             checkIsBatchResultSuccess(
1276                     mDb2.putAsync(
1277                             new PutDocumentsRequest.Builder()
1278                                     .addGenericDocuments(email1, gift1)
1279                                     .build()));
1280             checkIsBatchResultSuccess(
1281                     mDb1.putAsync(
1282                             new PutDocumentsRequest.Builder().addGenericDocuments(gift1).build()));
1283 
1284             // Register the second observer
1285             mGlobalSearchSession.registerObserverCallback(
1286                     mContext.getPackageName(),
1287                     new ObserverSpec.Builder().build(),
1288                     EXECUTOR,
1289                     unfilteredObserver);
1290 
1291             // Remove some of the documents.
1292             checkIsBatchResultSuccess(
1293                     mDb1.removeAsync(
1294                             new RemoveByDocumentIdRequest.Builder("namespace")
1295                                     .addIds("id1")
1296                                     .build()));
1297             checkIsBatchResultSuccess(
1298                     mDb2.removeAsync(
1299                             new RemoveByDocumentIdRequest.Builder("namespace2")
1300                                     .addIds("id2")
1301                                     .build()));
1302 
1303             // Make sure the notification was received. emailObserver should have seen:
1304             //   +db1:email, +db1:email, +db2:email, -db1:email.
1305             // unfilteredObserver (registered later) should have seen:
1306             //   -db1:email, -db2:gift
1307             emailObserver.waitForNotificationCount(4);
1308             unfilteredObserver.waitForNotificationCount(2);
1309 
1310             assertThat(emailObserver.getSchemaChanges()).isEmpty();
1311             assertThat(emailObserver.getDocumentChanges())
1312                     .containsExactly(
1313                             new DocumentChangeInfo(
1314                                     mContext.getPackageName(),
1315                                     DB_NAME_1,
1316                                     "namespace",
1317                                     AppSearchEmail.SCHEMA_TYPE,
1318                                     /* changedDocumentIds= */ ImmutableSet.of("id1")),
1319                             new DocumentChangeInfo(
1320                                     mContext.getPackageName(),
1321                                     DB_NAME_1,
1322                                     "namespace",
1323                                     AppSearchEmail.SCHEMA_TYPE,
1324                                     /* changedDocumentIds= */ ImmutableSet.of("id1")),
1325                             new DocumentChangeInfo(
1326                                     mContext.getPackageName(),
1327                                     DB_NAME_2,
1328                                     "namespace",
1329                                     AppSearchEmail.SCHEMA_TYPE,
1330                                     /* changedDocumentIds= */ ImmutableSet.of("id1")),
1331                             new DocumentChangeInfo(
1332                                     mContext.getPackageName(),
1333                                     DB_NAME_1,
1334                                     "namespace",
1335                                     AppSearchEmail.SCHEMA_TYPE,
1336                                     /* changedDocumentIds= */ ImmutableSet.of("id1")));
1337 
1338             // Check unfilteredObserver
1339             assertThat(unfilteredObserver.getSchemaChanges()).isEmpty();
1340             assertThat(unfilteredObserver.getDocumentChanges())
1341                     .containsExactly(
1342                             new DocumentChangeInfo(
1343                                     mContext.getPackageName(),
1344                                     DB_NAME_1,
1345                                     "namespace",
1346                                     AppSearchEmail.SCHEMA_TYPE,
1347                                     /* changedDocumentIds= */ ImmutableSet.of("id1")),
1348                             new DocumentChangeInfo(
1349                                     mContext.getPackageName(),
1350                                     DB_NAME_2,
1351                                     "namespace2",
1352                                     "Gift",
1353                                     /* changedDocumentIds= */ ImmutableSet.of("id2")));
1354         } finally {
1355             // Clean the observer
1356             mGlobalSearchSession.unregisterObserverCallback(
1357                     mContext.getPackageName(), emailObserver);
1358             mGlobalSearchSession.unregisterObserverCallback(
1359                     mContext.getPackageName(), unfilteredObserver);
1360         }
1361     }
1362 
1363     @Test
testRegisterObserver_removeByQuery()1364     public void testRegisterObserver_removeByQuery() throws Exception {
1365         assumeTrue(
1366                 mGlobalSearchSession
1367                         .getFeatures()
1368                         .isFeatureSupported(
1369                                 Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
1370 
1371         TestObserverCallback unfilteredObserver = new TestObserverCallback();
1372         TestObserverCallback emailObserver = new TestObserverCallback();
1373 
1374         // Set up the email and gift types in both databases
1375         AppSearchSchema giftSchema =
1376                 new AppSearchSchema.Builder("Gift")
1377                         .addProperty(
1378                                 new AppSearchSchema.DoublePropertyConfig.Builder("price").build())
1379                         .build();
1380         mDb1.setSchemaAsync(
1381                         new SetSchemaRequest.Builder()
1382                                 .addSchemas(AppSearchEmail.SCHEMA, giftSchema)
1383                                 .build())
1384                 .get();
1385         mDb2.setSchemaAsync(
1386                         new SetSchemaRequest.Builder()
1387                                 .addSchemas(AppSearchEmail.SCHEMA, giftSchema)
1388                                 .build())
1389                 .get();
1390 
1391         // Index some documents
1392         AppSearchEmail email1 = new AppSearchEmail.Builder("namespace", "id1").build();
1393         AppSearchEmail email2 =
1394                 new AppSearchEmail.Builder("namespace", "id2").setBody("caterpillar").build();
1395         GenericDocument gift1 =
1396                 new GenericDocument.Builder<GenericDocument.Builder<?>>("namespace2", "id3", "Gift")
1397                         .build();
1398 
1399         checkIsBatchResultSuccess(
1400                 mDb1.putAsync(
1401                         new PutDocumentsRequest.Builder()
1402                                 .addGenericDocuments(email1, email2, gift1)
1403                                 .build()));
1404         checkIsBatchResultSuccess(
1405                 mDb2.putAsync(
1406                         new PutDocumentsRequest.Builder()
1407                                 .addGenericDocuments(email1, email2, gift1)
1408                                 .build()));
1409 
1410         // Register observers
1411         mGlobalSearchSession.registerObserverCallback(
1412                 mContext.getPackageName(),
1413                 new ObserverSpec.Builder().build(),
1414                 EXECUTOR,
1415                 unfilteredObserver);
1416         mGlobalSearchSession.registerObserverCallback(
1417                 mContext.getPackageName(),
1418                 new ObserverSpec.Builder().addFilterSchemas(AppSearchEmail.SCHEMA_TYPE).build(),
1419                 EXECUTOR,
1420                 emailObserver);
1421         try {
1422             // Make sure everything is empty
1423             assertThat(unfilteredObserver.getSchemaChanges()).isEmpty();
1424             assertThat(unfilteredObserver.getDocumentChanges()).isEmpty();
1425             assertThat(emailObserver.getSchemaChanges()).isEmpty();
1426             assertThat(emailObserver.getDocumentChanges()).isEmpty();
1427 
1428             // Remove "cat" emails in db1 and all types in db2
1429             mDb1.removeAsync(
1430                             "cat",
1431                             new SearchSpec.Builder()
1432                                     .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
1433                                     .build())
1434                     .get();
1435             mDb2.removeAsync("", new SearchSpec.Builder().build()).get();
1436 
1437             // Make sure the notification was received. UnfilteredObserver should have seen:
1438             //   -db1:id2, -db2:id1, -db2:id2, -db2:id3
1439             // emailObserver should have seen:
1440             //   -db1:id2, -db2:id1, -db2:id2
1441             unfilteredObserver.waitForNotificationCount(3);
1442             emailObserver.waitForNotificationCount(2);
1443 
1444             assertThat(unfilteredObserver.getSchemaChanges()).isEmpty();
1445             assertThat(unfilteredObserver.getDocumentChanges())
1446                     .containsExactly(
1447                             new DocumentChangeInfo(
1448                                     mContext.getPackageName(),
1449                                     DB_NAME_1,
1450                                     "namespace",
1451                                     AppSearchEmail.SCHEMA_TYPE,
1452                                     /* changedDocumentIds= */ ImmutableSet.of("id2")),
1453                             new DocumentChangeInfo(
1454                                     mContext.getPackageName(),
1455                                     DB_NAME_2,
1456                                     "namespace",
1457                                     AppSearchEmail.SCHEMA_TYPE,
1458                                     /* changedDocumentIds= */ ImmutableSet.of("id1", "id2")),
1459                             new DocumentChangeInfo(
1460                                     mContext.getPackageName(),
1461                                     DB_NAME_2,
1462                                     "namespace2",
1463                                     "Gift",
1464                                     /* changedDocumentIds= */ ImmutableSet.of("id3")));
1465 
1466             // Check emailObserver
1467             assertThat(emailObserver.getSchemaChanges()).isEmpty();
1468             assertThat(emailObserver.getDocumentChanges())
1469                     .containsExactly(
1470                             new DocumentChangeInfo(
1471                                     mContext.getPackageName(),
1472                                     DB_NAME_1,
1473                                     "namespace",
1474                                     AppSearchEmail.SCHEMA_TYPE,
1475                                     /* changedDocumentIds= */ ImmutableSet.of("id2")),
1476                             new DocumentChangeInfo(
1477                                     mContext.getPackageName(),
1478                                     DB_NAME_2,
1479                                     "namespace",
1480                                     AppSearchEmail.SCHEMA_TYPE,
1481                                     /* changedDocumentIds= */ ImmutableSet.of("id1", "id2")));
1482         } finally {
1483             // Clean the observer
1484             mGlobalSearchSession.unregisterObserverCallback(
1485                     mContext.getPackageName(), emailObserver);
1486             mGlobalSearchSession.unregisterObserverCallback(
1487                     mContext.getPackageName(), unfilteredObserver);
1488         }
1489     }
1490 
1491     @Test
testRegisterObserver_sameCallback_differentSpecs()1492     public void testRegisterObserver_sameCallback_differentSpecs() throws Exception {
1493         assumeTrue(
1494                 mGlobalSearchSession
1495                         .getFeatures()
1496                         .isFeatureSupported(
1497                                 Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
1498 
1499         TestObserverCallback observer = new TestObserverCallback();
1500 
1501         // Set up the email and gift types
1502         AppSearchSchema giftSchema =
1503                 new AppSearchSchema.Builder("Gift")
1504                         .addProperty(
1505                                 new AppSearchSchema.DoublePropertyConfig.Builder("price").build())
1506                         .build();
1507         mDb1.setSchemaAsync(
1508                         new SetSchemaRequest.Builder()
1509                                 .addSchemas(AppSearchEmail.SCHEMA, giftSchema)
1510                                 .build())
1511                 .get();
1512 
1513         // Register the same observer twice: once for gift, once for email
1514         mGlobalSearchSession.registerObserverCallback(
1515                 mContext.getPackageName(),
1516                 new ObserverSpec.Builder().addFilterSchemas("Gift").build(),
1517                 EXECUTOR,
1518                 observer);
1519         mGlobalSearchSession.registerObserverCallback(
1520                 mContext.getPackageName(),
1521                 new ObserverSpec.Builder().addFilterSchemas(AppSearchEmail.SCHEMA_TYPE).build(),
1522                 EXECUTOR,
1523                 observer);
1524         try {
1525             // Index one email and one gift
1526             AppSearchEmail email1 = new AppSearchEmail.Builder("namespace", "id1").build();
1527             GenericDocument gift1 =
1528                     new GenericDocument.Builder<GenericDocument.Builder<?>>(
1529                                     "namespace2", "id3", "Gift")
1530                             .build();
1531 
1532             checkIsBatchResultSuccess(
1533                     mDb1.putAsync(
1534                             new PutDocumentsRequest.Builder()
1535                                     .addGenericDocuments(email1, gift1)
1536                                     .build()));
1537 
1538             // Make sure the same observer received both values
1539             observer.waitForNotificationCount(2);
1540             assertThat(observer.getSchemaChanges()).isEmpty();
1541             assertThat(observer.getDocumentChanges())
1542                     .containsExactly(
1543                             new DocumentChangeInfo(
1544                                     mContext.getPackageName(),
1545                                     DB_NAME_1,
1546                                     "namespace",
1547                                     AppSearchEmail.SCHEMA_TYPE,
1548                                     /* changedDocumentIds= */ ImmutableSet.of("id1")),
1549                             new DocumentChangeInfo(
1550                                     mContext.getPackageName(),
1551                                     DB_NAME_1,
1552                                     "namespace2",
1553                                     "Gift",
1554                                     /* changedDocumentIds= */ ImmutableSet.of("id3")));
1555         } finally {
1556             // Clean the observer
1557             mGlobalSearchSession.unregisterObserverCallback(mContext.getPackageName(), observer);
1558         }
1559     }
1560 
1561     @Test
testRemoveObserver()1562     public void testRemoveObserver() throws Exception {
1563         assumeTrue(
1564                 mGlobalSearchSession
1565                         .getFeatures()
1566                         .isFeatureSupported(
1567                                 Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
1568 
1569         TestObserverCallback temporaryObserver = new TestObserverCallback();
1570         TestObserverCallback permanentObserver = new TestObserverCallback();
1571 
1572         // Set up the email and gift types
1573         AppSearchSchema giftSchema =
1574                 new AppSearchSchema.Builder("Gift")
1575                         .addProperty(
1576                                 new AppSearchSchema.DoublePropertyConfig.Builder("price").build())
1577                         .build();
1578         mDb1.setSchemaAsync(
1579                         new SetSchemaRequest.Builder()
1580                                 .addSchemas(AppSearchEmail.SCHEMA, giftSchema)
1581                                 .build())
1582                 .get();
1583         mDb2.setSchemaAsync(
1584                         new SetSchemaRequest.Builder()
1585                                 .addSchemas(AppSearchEmail.SCHEMA, giftSchema)
1586                                 .build())
1587                 .get();
1588 
1589         // Register both observers. temporaryObserver is registered twice to ensure both instances
1590         // get removed.
1591         mGlobalSearchSession.registerObserverCallback(
1592                 mContext.getPackageName(),
1593                 new ObserverSpec.Builder().addFilterSchemas(AppSearchEmail.SCHEMA_TYPE).build(),
1594                 EXECUTOR,
1595                 temporaryObserver);
1596         mGlobalSearchSession.registerObserverCallback(
1597                 mContext.getPackageName(),
1598                 new ObserverSpec.Builder().addFilterSchemas("Gift").build(),
1599                 EXECUTOR,
1600                 temporaryObserver);
1601         mGlobalSearchSession.registerObserverCallback(
1602                 mContext.getPackageName(),
1603                 new ObserverSpec.Builder().build(),
1604                 EXECUTOR,
1605                 permanentObserver);
1606         try {
1607             // Make sure everything is empty
1608             assertThat(temporaryObserver.getSchemaChanges()).isEmpty();
1609             assertThat(temporaryObserver.getDocumentChanges()).isEmpty();
1610             assertThat(permanentObserver.getSchemaChanges()).isEmpty();
1611             assertThat(permanentObserver.getDocumentChanges()).isEmpty();
1612 
1613             // Index some documents
1614             AppSearchEmail email1 = new AppSearchEmail.Builder("namespace", "id1").build();
1615             AppSearchEmail email2 =
1616                     new AppSearchEmail.Builder("namespace", "id2").setBody("caterpillar").build();
1617             GenericDocument gift1 =
1618                     new GenericDocument.Builder<GenericDocument.Builder<?>>(
1619                                     "namespace2", "id3", "Gift")
1620                             .build();
1621             GenericDocument gift2 =
1622                     new GenericDocument.Builder<GenericDocument.Builder<?>>(
1623                                     "namespace3", "id4", "Gift")
1624                             .build();
1625 
1626             checkIsBatchResultSuccess(
1627                     mDb1.putAsync(
1628                             new PutDocumentsRequest.Builder()
1629                                     .addGenericDocuments(email1, gift1)
1630                                     .build()));
1631 
1632             // Make sure the notifications were received.
1633             temporaryObserver.waitForNotificationCount(2);
1634             permanentObserver.waitForNotificationCount(2);
1635 
1636             List<DocumentChangeInfo> expectedChangesOrig =
1637                     ImmutableList.of(
1638                             new DocumentChangeInfo(
1639                                     mContext.getPackageName(),
1640                                     DB_NAME_1,
1641                                     "namespace",
1642                                     AppSearchEmail.SCHEMA_TYPE,
1643                                     /* changedDocumentIds= */ ImmutableSet.of("id1")),
1644                             new DocumentChangeInfo(
1645                                     mContext.getPackageName(),
1646                                     DB_NAME_1,
1647                                     "namespace2",
1648                                     "Gift",
1649                                     /* changedDocumentIds= */ ImmutableSet.of("id3")));
1650             assertThat(temporaryObserver.getSchemaChanges()).isEmpty();
1651             assertThat(temporaryObserver.getDocumentChanges())
1652                     .containsExactlyElementsIn(expectedChangesOrig);
1653             assertThat(permanentObserver.getSchemaChanges()).isEmpty();
1654             assertThat(permanentObserver.getDocumentChanges())
1655                     .containsExactlyElementsIn(expectedChangesOrig);
1656 
1657             // Unregister temporaryObserver
1658             mGlobalSearchSession.unregisterObserverCallback(
1659                     mContext.getPackageName(), temporaryObserver);
1660 
1661             // Index some more documents
1662             checkIsBatchResultSuccess(
1663                     mDb1.putAsync(
1664                             new PutDocumentsRequest.Builder()
1665                                     .addGenericDocuments(email2, gift2)
1666                                     .build()));
1667 
1668             // Only the permanent observer should have received this
1669             permanentObserver.waitForNotificationCount(4);
1670             temporaryObserver.waitForNotificationCount(2);
1671 
1672             assertThat(permanentObserver.getSchemaChanges()).isEmpty();
1673             assertThat(permanentObserver.getDocumentChanges())
1674                     .containsExactly(
1675                             new DocumentChangeInfo(
1676                                     mContext.getPackageName(),
1677                                     DB_NAME_1,
1678                                     "namespace",
1679                                     AppSearchEmail.SCHEMA_TYPE,
1680                                     /* changedDocumentIds= */ ImmutableSet.of("id1")),
1681                             new DocumentChangeInfo(
1682                                     mContext.getPackageName(),
1683                                     DB_NAME_1,
1684                                     "namespace2",
1685                                     "Gift",
1686                                     /* changedDocumentIds= */ ImmutableSet.of("id3")),
1687                             new DocumentChangeInfo(
1688                                     mContext.getPackageName(),
1689                                     DB_NAME_1,
1690                                     "namespace",
1691                                     AppSearchEmail.SCHEMA_TYPE,
1692                                     /* changedDocumentIds= */ ImmutableSet.of("id2")),
1693                             new DocumentChangeInfo(
1694                                     mContext.getPackageName(),
1695                                     DB_NAME_1,
1696                                     "namespace3",
1697                                     "Gift",
1698                                     /* changedDocumentIds= */ ImmutableSet.of("id4")));
1699             assertThat(temporaryObserver.getSchemaChanges()).isEmpty();
1700             assertThat(temporaryObserver.getDocumentChanges())
1701                     .containsExactlyElementsIn(expectedChangesOrig);
1702         } finally {
1703             // Clean the observer
1704             mGlobalSearchSession.unregisterObserverCallback(
1705                     mContext.getPackageName(), temporaryObserver);
1706             mGlobalSearchSession.unregisterObserverCallback(
1707                     mContext.getPackageName(), permanentObserver);
1708         }
1709     }
1710 
1711     @Test
testGlobalGetSchema()1712     public void testGlobalGetSchema() throws Exception {
1713         assumeTrue(
1714                 mGlobalSearchSession
1715                         .getFeatures()
1716                         .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA));
1717 
1718         // One schema should be set with global access and the other should be set with local
1719         // access.
1720         mDb1.setSchemaAsync(
1721                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
1722                 .get();
1723         mDb2.setSchemaAsync(
1724                         new SetSchemaRequest.Builder()
1725                                 .addSchemas(AppSearchEmail.SCHEMA)
1726                                 .setSchemaTypeDisplayedBySystem(
1727                                         AppSearchEmail.SCHEMA_TYPE, /* displayed= */ false)
1728                                 .build())
1729                 .get();
1730 
1731         GetSchemaResponse response =
1732                 mGlobalSearchSession.getSchemaAsync(mContext.getPackageName(), DB_NAME_1).get();
1733         assertThat(response.getSchemas()).containsExactly(AppSearchEmail.SCHEMA);
1734 
1735         response = mGlobalSearchSession.getSchemaAsync(mContext.getPackageName(), DB_NAME_2).get();
1736         assertThat(response.getSchemas()).containsExactly(AppSearchEmail.SCHEMA);
1737 
1738         // A request for a db that doesn't exist should return a response with no schemas.
1739         response =
1740                 mGlobalSearchSession
1741                         .getSchemaAsync(mContext.getPackageName(), "NonexistentDb")
1742                         .get();
1743         assertThat(response.getSchemas()).isEmpty();
1744     }
1745 
1746     @Test
testGlobalGetSchema_notSupported()1747     public void testGlobalGetSchema_notSupported() throws Exception {
1748         assumeFalse(
1749                 mGlobalSearchSession
1750                         .getFeatures()
1751                         .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA));
1752 
1753         // One schema should be set with global access and the other should be set with local
1754         // access.
1755         mDb1.setSchemaAsync(
1756                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
1757                 .get();
1758 
1759         UnsupportedOperationException e =
1760                 assertThrows(
1761                         UnsupportedOperationException.class,
1762                         () ->
1763                                 mGlobalSearchSession.getSchemaAsync(
1764                                         mContext.getPackageName(), DB_NAME_1));
1765         assertThat(e)
1766                 .hasMessageThat()
1767                 .isEqualTo(
1768                         Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA
1769                                 + " is not supported on this AppSearch implementation.");
1770     }
1771 
1772     @Test
testGlobalGetByDocumentId_notSupported()1773     public void testGlobalGetByDocumentId_notSupported() throws Exception {
1774         assumeFalse(
1775                 mGlobalSearchSession
1776                         .getFeatures()
1777                         .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_GET_BY_ID));
1778 
1779         Context context = ApplicationProvider.getApplicationContext();
1780 
1781         UnsupportedOperationException e =
1782                 assertThrows(
1783                         UnsupportedOperationException.class,
1784                         () ->
1785                                 mGlobalSearchSession.getByDocumentIdAsync(
1786                                         context.getPackageName(),
1787                                         DB_NAME_1,
1788                                         new GetByDocumentIdRequest.Builder("namespace")
1789                                                 .addIds("id")
1790                                                 .build()));
1791 
1792         assertThat(e)
1793                 .hasMessageThat()
1794                 .isEqualTo(
1795                         Features.GLOBAL_SEARCH_SESSION_GET_BY_ID
1796                                 + " is not supported on this AppSearch implementation.");
1797     }
1798 
1799     @Test
testAddObserver_schemaChange_added()1800     public void testAddObserver_schemaChange_added() throws Exception {
1801         assumeTrue(
1802                 mDb1.getFeatures()
1803                         .isFeatureSupported(
1804                                 Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
1805 
1806         // Register an observer
1807         TestObserverCallback observer = new TestObserverCallback();
1808         mGlobalSearchSession.registerObserverCallback(
1809                 /* targetPackageName= */ mContext.getPackageName(),
1810                 new ObserverSpec.Builder().build(),
1811                 EXECUTOR,
1812                 observer);
1813         try {
1814             // Add a schema type
1815             assertThat(observer.getSchemaChanges()).isEmpty();
1816             assertThat(observer.getDocumentChanges()).isEmpty();
1817             mDb1.setSchemaAsync(
1818                             new SetSchemaRequest.Builder()
1819                                     .addSchemas(new AppSearchSchema.Builder("Type1").build())
1820                                     .build())
1821                     .get();
1822 
1823             observer.waitForNotificationCount(1);
1824             assertThat(observer.getSchemaChanges())
1825                     .containsExactly(
1826                             new SchemaChangeInfo(
1827                                     mContext.getPackageName(),
1828                                     DB_NAME_1,
1829                                     ImmutableSet.of("Type1")));
1830             assertThat(observer.getDocumentChanges()).isEmpty();
1831 
1832             // Add two more schema types without touching the existing one
1833             observer.clear();
1834             mDb1.setSchemaAsync(
1835                             new SetSchemaRequest.Builder()
1836                                     .addSchemas(
1837                                             new AppSearchSchema.Builder("Type1").build(),
1838                                             new AppSearchSchema.Builder("Type2").build(),
1839                                             new AppSearchSchema.Builder("Type3").build())
1840                                     .build())
1841                     .get();
1842 
1843             observer.waitForNotificationCount(1);
1844             assertThat(observer.getSchemaChanges())
1845                     .containsExactly(
1846                             new SchemaChangeInfo(
1847                                     mContext.getPackageName(),
1848                                     DB_NAME_1,
1849                                     ImmutableSet.of("Type2", "Type3")));
1850             assertThat(observer.getDocumentChanges()).isEmpty();
1851         } finally {
1852             // Clean the observer
1853             mGlobalSearchSession.unregisterObserverCallback(mContext.getPackageName(), observer);
1854         }
1855     }
1856 
1857     @Test
testAddObserver_schemaChange_removed()1858     public void testAddObserver_schemaChange_removed() throws Exception {
1859         assumeTrue(
1860                 mDb1.getFeatures()
1861                         .isFeatureSupported(
1862                                 Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
1863 
1864         // Add a schema type
1865         mDb1.setSchemaAsync(
1866                         new SetSchemaRequest.Builder()
1867                                 .addSchemas(
1868                                         new AppSearchSchema.Builder("Type1").build(),
1869                                         new AppSearchSchema.Builder("Type2").build())
1870                                 .build())
1871                 .get();
1872 
1873         // Register an observer
1874         TestObserverCallback observer = new TestObserverCallback();
1875         mGlobalSearchSession.registerObserverCallback(
1876                 /* targetPackageName= */ mContext.getPackageName(),
1877                 new ObserverSpec.Builder().build(),
1878                 EXECUTOR,
1879                 observer);
1880 
1881         try {
1882             // Remove Type2
1883             mDb1.setSchemaAsync(
1884                             new SetSchemaRequest.Builder()
1885                                     .addSchemas(new AppSearchSchema.Builder("Type1").build())
1886                                     .setForceOverride(true)
1887                                     .build())
1888                     .get();
1889 
1890             observer.waitForNotificationCount(1);
1891             assertThat(observer.getSchemaChanges())
1892                     .containsExactly(
1893                             new SchemaChangeInfo(
1894                                     mContext.getPackageName(),
1895                                     DB_NAME_1,
1896                                     ImmutableSet.of("Type2")));
1897             assertThat(observer.getDocumentChanges()).isEmpty();
1898         } finally {
1899             // Clean the observer
1900             mGlobalSearchSession.unregisterObserverCallback(mContext.getPackageName(), observer);
1901         }
1902     }
1903 
1904     @Test
testAddObserver_schemaChange_contents()1905     public void testAddObserver_schemaChange_contents() throws Exception {
1906         assumeTrue(
1907                 mDb1.getFeatures()
1908                         .isFeatureSupported(
1909                                 Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
1910 
1911         AppSearchSchema type1 = new AppSearchSchema.Builder("Type1").build();
1912         AppSearchSchema type2 =
1913                 new AppSearchSchema.Builder("Type2")
1914                         .addProperty(
1915                                 new AppSearchSchema.BooleanPropertyConfig.Builder("booleanProp")
1916                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
1917                                         .build())
1918                         .build();
1919         // Add a schema
1920         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(type1, type2).build()).get();
1921 
1922         // Register an observer
1923         TestObserverCallback observer = new TestObserverCallback();
1924         mGlobalSearchSession.registerObserverCallback(
1925                 /* targetPackageName= */ mContext.getPackageName(),
1926                 new ObserverSpec.Builder().build(),
1927                 EXECUTOR,
1928                 observer);
1929 
1930         try {
1931             // Update the schema, but don't make any actual changes
1932             mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(type1, type2).build())
1933                     .get();
1934 
1935             // Now update the schema again, but this time actually make a change (cardinality of the
1936             // property)
1937 
1938             AppSearchSchema type2Optional =
1939                     new AppSearchSchema.Builder("Type2")
1940                             .addProperty(
1941                                     new AppSearchSchema.BooleanPropertyConfig.Builder("booleanProp")
1942                                             .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1943                                             .build())
1944                             .build();
1945             mDb1.setSchemaAsync(
1946                             new SetSchemaRequest.Builder().addSchemas(type1, type2Optional).build())
1947                     .get();
1948 
1949             // Dispatch notifications
1950             observer.waitForNotificationCount(1);
1951             assertThat(observer.getSchemaChanges())
1952                     .containsExactly(
1953                             new SchemaChangeInfo(
1954                                     mContext.getPackageName(),
1955                                     DB_NAME_1,
1956                                     ImmutableSet.of("Type2")));
1957             assertThat(observer.getDocumentChanges()).isEmpty();
1958         } finally {
1959             // Clean the observer
1960             mGlobalSearchSession.unregisterObserverCallback(mContext.getPackageName(), observer);
1961         }
1962     }
1963 
1964     @Test
testAddObserver_schemaChange_contents_skipBySpec()1965     public void testAddObserver_schemaChange_contents_skipBySpec() throws Exception {
1966         assumeTrue(
1967                 mDb1.getFeatures()
1968                         .isFeatureSupported(
1969                                 Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
1970 
1971         // Add a schema
1972 
1973         AppSearchSchema type1 =
1974                 new AppSearchSchema.Builder("Type1")
1975                         .addProperty(
1976                                 new AppSearchSchema.BooleanPropertyConfig.Builder("booleanProp")
1977                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
1978                                         .build())
1979                         .build();
1980         AppSearchSchema type2 =
1981                 new AppSearchSchema.Builder("Type2")
1982                         .addProperty(
1983                                 new AppSearchSchema.BooleanPropertyConfig.Builder("booleanProp")
1984                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
1985                                         .build())
1986                         .build();
1987         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(type1, type2).build()).get();
1988 
1989         // Register an observer that only listens for Type2
1990         TestObserverCallback observer = new TestObserverCallback();
1991         mGlobalSearchSession.registerObserverCallback(
1992                 /* targetPackageName= */ mContext.getPackageName(),
1993                 new ObserverSpec.Builder().addFilterSchemas("Type2").build(),
1994                 EXECUTOR,
1995                 observer);
1996         try {
1997             // Update both types of the schema (changed cardinalities)
1998             AppSearchSchema type1Optional =
1999                     new AppSearchSchema.Builder("Type1")
2000                             .addProperty(
2001                                     new AppSearchSchema.BooleanPropertyConfig.Builder("booleanProp")
2002                                             .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
2003                                             .build())
2004                             .build();
2005             AppSearchSchema type2Optional =
2006                     new AppSearchSchema.Builder("Type2")
2007                             .addProperty(
2008                                     new AppSearchSchema.BooleanPropertyConfig.Builder("booleanProp")
2009                                             .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
2010                                             .build())
2011                             .build();
2012             mDb1.setSchemaAsync(
2013                             new SetSchemaRequest.Builder()
2014                                     .addSchemas(type1Optional, type2Optional)
2015                                     .build())
2016                     .get();
2017 
2018             observer.waitForNotificationCount(1);
2019             assertThat(observer.getSchemaChanges())
2020                     .containsExactly(
2021                             new SchemaChangeInfo(
2022                                     mContext.getPackageName(),
2023                                     DB_NAME_1,
2024                                     ImmutableSet.of("Type2")));
2025             assertThat(observer.getDocumentChanges()).isEmpty();
2026         } finally {
2027             // Clean the observer
2028             mGlobalSearchSession.unregisterObserverCallback(mContext.getPackageName(), observer);
2029         }
2030     }
2031 
2032     @Test
testRegisterObserver_schemaMigration()2033     public void testRegisterObserver_schemaMigration() throws Exception {
2034         assumeTrue(
2035                 mDb1.getFeatures()
2036                         .isFeatureSupported(
2037                                 Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
2038         // Add a schema with two types
2039         mDb1.setSchemaAsync(
2040                         new SetSchemaRequest.Builder()
2041                                 .setVersion(1)
2042                                 .addSchemas(
2043                                         new AppSearchSchema.Builder("Type1")
2044                                                 .addProperty(
2045                                                         new AppSearchSchema.StringPropertyConfig
2046                                                                         .Builder("strProp1")
2047                                                                 .build())
2048                                                 .build(),
2049                                         new AppSearchSchema.Builder("Type2")
2050                                                 .addProperty(
2051                                                         new AppSearchSchema.LongPropertyConfig
2052                                                                         .Builder("longProp1")
2053                                                                 .build())
2054                                                 .build())
2055                                 .build())
2056                 .get();
2057 
2058         // Index some documents
2059         GenericDocument type1doc1 =
2060                 new GenericDocument.Builder<GenericDocument.Builder<?>>(
2061                                 "namespace", "t1id1", "Type1")
2062                         .setPropertyString("strProp1", "t1id1 prop value")
2063                         .build();
2064         GenericDocument type1doc2 =
2065                 new GenericDocument.Builder<GenericDocument.Builder<?>>(
2066                                 "namespace", "t1id2", "Type1")
2067                         .setPropertyString("strProp1", "t1id2 prop value")
2068                         .build();
2069         GenericDocument type2doc1 =
2070                 new GenericDocument.Builder<GenericDocument.Builder<?>>(
2071                                 "namespace", "t2id1", "Type2")
2072                         .setPropertyLong("longProp1", 41)
2073                         .build();
2074         GenericDocument type2doc2 =
2075                 new GenericDocument.Builder<GenericDocument.Builder<?>>(
2076                                 "namespace", "t2id2", "Type2")
2077                         .setPropertyLong("longProp1", 42)
2078                         .build();
2079         mDb1.putAsync(
2080                         new PutDocumentsRequest.Builder()
2081                                 .addGenericDocuments(type1doc1, type1doc2, type2doc1, type2doc2)
2082                                 .build())
2083                 .get();
2084 
2085         // Register an observer that only listens for Type1
2086         TestObserverCallback observer = new TestObserverCallback();
2087         mGlobalSearchSession.registerObserverCallback(
2088                 /* targetPackageName= */ mContext.getPackageName(),
2089                 new ObserverSpec.Builder().addFilterSchemas("Type1").build(),
2090                 EXECUTOR,
2091                 observer);
2092 
2093         try {
2094             // Update both types of the schema with migration to a new property name
2095             mDb1.setSchemaAsync(
2096                             new SetSchemaRequest.Builder()
2097                                     .setVersion(2)
2098                                     .addSchemas(
2099                                             new AppSearchSchema.Builder("Type1")
2100                                                     .addProperty(
2101                                                             new AppSearchSchema.StringPropertyConfig
2102                                                                             .Builder("strProp2")
2103                                                                     .build())
2104                                                     .build(),
2105                                             new AppSearchSchema.Builder("Type2")
2106                                                     .addProperty(
2107                                                             new AppSearchSchema.LongPropertyConfig
2108                                                                             .Builder("longProp2")
2109                                                                     .build())
2110                                                     .build())
2111                                     .setMigrator(
2112                                             "Type1",
2113                                             new Migrator() {
2114                                                 @Override
2115                                                 public boolean shouldMigrate(
2116                                                         int currentVersion, int finalVersion) {
2117                                                     assertThat(currentVersion).isEqualTo(1);
2118                                                     assertThat(finalVersion).isEqualTo(2);
2119                                                     return true;
2120                                                 }
2121 
2122                                                 @NonNull
2123                                                 @Override
2124                                                 public GenericDocument onUpgrade(
2125                                                         int currentVersion,
2126                                                         int finalVersion,
2127                                                         @NonNull GenericDocument document) {
2128                                                     assertThat(currentVersion).isEqualTo(1);
2129                                                     assertThat(finalVersion).isEqualTo(2);
2130                                                     assertThat(document.getSchemaType())
2131                                                             .isEqualTo("Type1");
2132                                                     String[] prop =
2133                                                             document.getPropertyStringArray(
2134                                                                     "strProp1");
2135                                                     assertThat(prop).isNotNull();
2136                                                     return new GenericDocument.Builder<
2137                                                                     GenericDocument.Builder<?>>(
2138                                                                     document.getNamespace(),
2139                                                                     document.getId(),
2140                                                                     document.getSchemaType())
2141                                                             .setPropertyString("strProp2", prop)
2142                                                             .build();
2143                                                 }
2144 
2145                                                 @NonNull
2146                                                 @Override
2147                                                 public GenericDocument onDowngrade(
2148                                                         int currentVersion,
2149                                                         int finalVersion,
2150                                                         @NonNull GenericDocument document) {
2151                                                     // Doesn't happen in this test
2152                                                     throw new UnsupportedOperationException();
2153                                                 }
2154                                             })
2155                                     .setMigrator(
2156                                             "Type2",
2157                                             new Migrator() {
2158                                                 @Override
2159                                                 public boolean shouldMigrate(
2160                                                         int currentVersion, int finalVersion) {
2161                                                     assertThat(currentVersion).isEqualTo(1);
2162                                                     assertThat(finalVersion).isEqualTo(2);
2163                                                     return true;
2164                                                 }
2165 
2166                                                 @NonNull
2167                                                 @Override
2168                                                 public GenericDocument onUpgrade(
2169                                                         int currentVersion,
2170                                                         int finalVersion,
2171                                                         @NonNull GenericDocument document) {
2172                                                     assertThat(currentVersion).isEqualTo(1);
2173                                                     assertThat(finalVersion).isEqualTo(2);
2174                                                     assertThat(document.getSchemaType())
2175                                                             .isEqualTo("Type2");
2176                                                     long[] prop =
2177                                                             document.getPropertyLongArray(
2178                                                                     "longProp1");
2179                                                     assertThat(prop).isNotNull();
2180                                                     return new GenericDocument.Builder<
2181                                                                     GenericDocument.Builder<?>>(
2182                                                                     document.getNamespace(),
2183                                                                     document.getId(),
2184                                                                     document.getSchemaType())
2185                                                             .setPropertyLong(
2186                                                                     "longProp2", prop[0] + 1000)
2187                                                             .build();
2188                                                 }
2189 
2190                                                 @NonNull
2191                                                 @Override
2192                                                 public GenericDocument onDowngrade(
2193                                                         int currentVersion,
2194                                                         int finalVersion,
2195                                                         @NonNull GenericDocument document) {
2196                                                     // Doesn't happen in this test
2197                                                     throw new UnsupportedOperationException();
2198                                                 }
2199                                             })
2200                                     .build())
2201                     .get();
2202 
2203             // Make sure the test is valid by checking that migration actually occurred
2204             AppSearchBatchResult<String, GenericDocument> getResponse =
2205                     mDb1.getByDocumentIdAsync(
2206                                     new GetByDocumentIdRequest.Builder("namespace")
2207                                             .addIds("t1id1", "t1id2", "t2id1", "t2id2")
2208                                             .build())
2209                             .get();
2210             assertThat(getResponse.isSuccess()).isTrue();
2211             assertThat(getResponse.getSuccesses().get("t1id1").getPropertyString("strProp2"))
2212                     .isEqualTo("t1id1 prop value");
2213             assertThat(getResponse.getSuccesses().get("t1id2").getPropertyString("strProp2"))
2214                     .isEqualTo("t1id2 prop value");
2215             assertThat(getResponse.getSuccesses().get("t2id1").getPropertyLong("longProp2"))
2216                     .isEqualTo(1041);
2217             assertThat(getResponse.getSuccesses().get("t2id2").getPropertyLong("longProp2"))
2218                     .isEqualTo(1042);
2219 
2220             // Per the observer documentation, for schema migrations, individual document changes
2221             // are not dispatched. Only SchemaChangeInfo is dispatched.
2222             observer.waitForNotificationCount(1);
2223             assertThat(observer.getSchemaChanges())
2224                     .containsExactly(
2225                             new SchemaChangeInfo(
2226                                     mContext.getPackageName(),
2227                                     DB_NAME_1,
2228                                     ImmutableSet.of("Type1")));
2229             assertThat(observer.getDocumentChanges()).isEmpty();
2230         } finally {
2231             // Clean the observer
2232             mGlobalSearchSession.unregisterObserverCallback(mContext.getPackageName(), observer);
2233         }
2234     }
2235 
2236     @Test
testGlobalQuery_propertyWeights()2237     public void testGlobalQuery_propertyWeights() throws Exception {
2238         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_PROPERTY_WEIGHTS));
2239 
2240         // RELEVANCE scoring depends on stats for the namespace+type of the scored document, namely
2241         // the average document length. This average document length calculation is only updated
2242         // when documents are added and when compaction runs. This means that old deleted
2243         // documents of the same namespace and type combination *can* affect RELEVANCE scores
2244         // through this channel.
2245         // To avoid this, we use a unique namespace that will not be shared by any other test
2246         // case or any other run of this test.
2247         mDb1.setSchemaAsync(
2248                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
2249                 .get();
2250         mDb2.setSchemaAsync(
2251                         new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
2252                 .get();
2253 
2254         String namespace = "propertyWeightsNamespace" + System.currentTimeMillis();
2255         // Put two documents in separate databases.
2256         AppSearchEmail emailDb1 =
2257                 new AppSearchEmail.Builder(namespace, "id1")
2258                         .setCreationTimestampMillis(1000)
2259                         .setSubject("foo")
2260                         .build();
2261         checkIsBatchResultSuccess(
2262                 mDb1.putAsync(
2263                         new PutDocumentsRequest.Builder().addGenericDocuments(emailDb1).build()));
2264         AppSearchEmail emailDb2 =
2265                 new AppSearchEmail.Builder(namespace, "id2")
2266                         .setCreationTimestampMillis(1000)
2267                         .setBody("foo")
2268                         .build();
2269         checkIsBatchResultSuccess(
2270                 mDb2.putAsync(
2271                         new PutDocumentsRequest.Builder().addGenericDocuments(emailDb2).build()));
2272 
2273         // Issue global query for "foo".
2274         SearchResultsShim searchResults =
2275                 mGlobalSearchSession.search(
2276                         "foo",
2277                         new SearchSpec.Builder()
2278                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
2279                                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
2280                                 .setOrder(SearchSpec.ORDER_DESCENDING)
2281                                 .setPropertyWeights(
2282                                         AppSearchEmail.SCHEMA_TYPE,
2283                                         ImmutableMap.of("subject", 2.0, "body", 0.5))
2284                                 .addFilterNamespaces(namespace)
2285                                 .build());
2286         List<SearchResult> globalResults = retrieveAllSearchResults(searchResults);
2287 
2288         // We expect to two emails, one from each of the databases.
2289         assertThat(globalResults).hasSize(2);
2290         assertThat(globalResults.get(0).getGenericDocument()).isEqualTo(emailDb1);
2291         assertThat(globalResults.get(1).getGenericDocument()).isEqualTo(emailDb2);
2292 
2293         // We expect that the email added to db1 will have a higher score than the email added to
2294         // db2 as the query term "foo" is contained in the "subject" property which has a higher
2295         // weight than the "body" property.
2296         assertThat(globalResults.get(0).getRankingSignal()).isGreaterThan(0);
2297         assertThat(globalResults.get(0).getRankingSignal())
2298                 .isGreaterThan(globalResults.get(1).getRankingSignal());
2299 
2300         // Query for "foo" without property weights.
2301         SearchResultsShim searchResultsWithoutWeights =
2302                 mGlobalSearchSession.search(
2303                         "foo",
2304                         new SearchSpec.Builder()
2305                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
2306                                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
2307                                 .setOrder(SearchSpec.ORDER_DESCENDING)
2308                                 .addFilterNamespaces(namespace)
2309                                 .build());
2310         List<SearchResult> resultsWithoutWeights =
2311                 retrieveAllSearchResults(searchResultsWithoutWeights);
2312 
2313         // email1 should have the same ranking signal as email2 as each contains the term "foo"
2314         // once.
2315         assertThat(resultsWithoutWeights).hasSize(2);
2316         assertThat(resultsWithoutWeights.get(0).getRankingSignal()).isGreaterThan(0);
2317         assertThat(resultsWithoutWeights.get(0).getRankingSignal())
2318                 .isEqualTo(resultsWithoutWeights.get(1).getRankingSignal());
2319     }
2320 
2321     @Test
2322     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCORABLE_PROPERTY)
testRankWithScorableProperty_searchFromMultipleDbs()2323     public void testRankWithScorableProperty_searchFromMultipleDbs() throws Exception {
2324         assumeTrue(
2325                 mGlobalSearchSession
2326                         .getFeatures()
2327                         .isFeatureSupported(Features.SCHEMA_SCORABLE_PROPERTY_CONFIG));
2328 
2329         AppSearchSchema schema =
2330                 new AppSearchSchema.Builder("Gmail")
2331                         .addProperty(
2332                                 new AppSearchSchema.BooleanPropertyConfig.Builder("important")
2333                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
2334                                         .setScoringEnabled(true)
2335                                         .build())
2336                         .build();
2337         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
2338         mDb2.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
2339 
2340         GenericDocument docInDb1 =
2341                 new GenericDocument.Builder<>("namespace", "id1", "Gmail")
2342                         .setPropertyBoolean("important", true)
2343                         .setScore(1)
2344                         .build();
2345         GenericDocument docInDb2 =
2346                 new GenericDocument.Builder<>("namespace", "id1", "Gmail")
2347                         .setPropertyBoolean("important", true)
2348                         .setScore(3)
2349                         .build();
2350         double docInDb1Score = 2;
2351         double docInDb2Score = 4;
2352         checkIsBatchResultSuccess(
2353                 mDb1.putAsync(
2354                         new PutDocumentsRequest.Builder().addGenericDocuments(docInDb1).build()));
2355         checkIsBatchResultSuccess(
2356                 mDb2.putAsync(
2357                         new PutDocumentsRequest.Builder().addGenericDocuments(docInDb2).build()));
2358 
2359         SearchSpec searchSpec =
2360                 new SearchSpec.Builder()
2361                         .setScorablePropertyRankingEnabled(true)
2362                         .setRankingStrategy(
2363                                 "this.documentScore() + sum(getScorableProperty(\"Gmail\","
2364                                     + " \"important\"))")
2365                         .addFilterPackageNames(mContext.getPackageName())
2366                         .build();
2367         SearchResultsShim searchResults = mGlobalSearchSession.search("", searchSpec);
2368         List<SearchResult> results = retrieveAllSearchResults(searchResults);
2369         assertThat(results).hasSize(2);
2370         assertThat(results.get(0).getGenericDocument()).isEqualTo(docInDb2);
2371         assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(docInDb2Score);
2372         assertThat(results.get(1).getGenericDocument()).isEqualTo(docInDb1);
2373         assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(docInDb1Score);
2374     }
2375 
2376     @Test
2377     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
testWriteAndReadBlob()2378     public void testWriteAndReadBlob() throws Exception {
2379         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.BLOB_STORAGE));
2380         byte[] data1 = generateRandomBytes(10); // 10 Bytes
2381         byte[] data2 = generateRandomBytes(20); // 20 Bytes
2382         byte[] digest1 = calculateDigest(data1);
2383         byte[] digest2 = calculateDigest(data2);
2384         AppSearchBlobHandle handle1 =
2385                 AppSearchBlobHandle.createWithSha256(
2386                         digest1, mContext.getPackageName(), DB_NAME_1, "ns");
2387         AppSearchBlobHandle handle2 =
2388                 AppSearchBlobHandle.createWithSha256(
2389                         digest2, mContext.getPackageName(), DB_NAME_1, "ns");
2390 
2391         try {
2392             try (OpenBlobForWriteResponse writeResponse =
2393                     mDb1.openBlobForWriteAsync(ImmutableSet.of(handle1, handle2)).get()) {
2394                 AppSearchBatchResult<AppSearchBlobHandle, ParcelFileDescriptor> writeResult =
2395                         writeResponse.getResult();
2396                 assertTrue(writeResult.isSuccess());
2397 
2398                 ParcelFileDescriptor writePfd1 = writeResult.getSuccesses().get(handle1);
2399                 try (OutputStream outputStream =
2400                         new ParcelFileDescriptor.AutoCloseOutputStream(writePfd1)) {
2401                     outputStream.write(data1);
2402                     outputStream.flush();
2403                 }
2404 
2405                 ParcelFileDescriptor writePfd2 = writeResult.getSuccesses().get(handle2);
2406                 try (OutputStream outputStream =
2407                         new ParcelFileDescriptor.AutoCloseOutputStream(writePfd2)) {
2408                     outputStream.write(data2);
2409                     outputStream.flush();
2410                 }
2411             }
2412 
2413             assertTrue(
2414                     mDb1.commitBlobAsync(ImmutableSet.of(handle1, handle2))
2415                             .get()
2416                             .getResult()
2417                             .isSuccess());
2418 
2419             byte[] readBytes1 = new byte[10]; // 10 Bytes
2420             byte[] readBytes2 = new byte[20]; // 20 Bytes
2421 
2422             try (OpenBlobForReadResponse readResponse =
2423                     mGlobalSearchSession
2424                             .openBlobForReadAsync(ImmutableSet.of(handle1, handle2))
2425                             .get()) {
2426                 AppSearchBatchResult<AppSearchBlobHandle, ParcelFileDescriptor> readResult =
2427                         readResponse.getResult();
2428                 assertTrue(readResult.isSuccess());
2429 
2430                 ParcelFileDescriptor readPfd1 = readResult.getSuccesses().get(handle1);
2431                 try (InputStream inputStream =
2432                         new ParcelFileDescriptor.AutoCloseInputStream(readPfd1)) {
2433                     inputStream.read(readBytes1);
2434                 }
2435                 assertThat(readBytes1).isEqualTo(data1);
2436 
2437                 ParcelFileDescriptor readPfd2 = readResult.getSuccesses().get(handle2);
2438                 try (InputStream inputStream =
2439                         new ParcelFileDescriptor.AutoCloseInputStream(readPfd2)) {
2440                     inputStream.read(readBytes2);
2441                 }
2442                 assertThat(readBytes2).isEqualTo(data2);
2443             }
2444         } finally {
2445             mDb1.removeBlobAsync(ImmutableSet.of(handle1, handle2)).get();
2446         }
2447     }
2448 
2449     @Test
2450     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
testWriteAndReadBlob_withoutCommit()2451     public void testWriteAndReadBlob_withoutCommit() throws Exception {
2452         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.BLOB_STORAGE));
2453         byte[] data = generateRandomBytes(10); // 10 Bytes
2454         byte[] digest = calculateDigest(data);
2455         AppSearchBlobHandle handle =
2456                 AppSearchBlobHandle.createWithSha256(
2457                         digest, mContext.getPackageName(), DB_NAME_1, "ns");
2458 
2459         try {
2460             try (OpenBlobForWriteResponse writeResponse =
2461                     mDb1.openBlobForWriteAsync(ImmutableSet.of(handle)).get()) {
2462                 AppSearchBatchResult<AppSearchBlobHandle, ParcelFileDescriptor> writeResult =
2463                         writeResponse.getResult();
2464                 assertTrue(writeResult.isSuccess());
2465 
2466                 ParcelFileDescriptor writePfd = writeResult.getSuccesses().get(handle);
2467                 try (OutputStream outputStream =
2468                         new ParcelFileDescriptor.AutoCloseOutputStream(writePfd)) {
2469                     outputStream.write(data);
2470                     outputStream.flush();
2471                 }
2472             }
2473 
2474             // Read blob without commit the blob first.
2475             try (OpenBlobForReadResponse readResponse =
2476                     mGlobalSearchSession.openBlobForReadAsync(ImmutableSet.of(handle)).get()) {
2477                 AppSearchBatchResult<AppSearchBlobHandle, ParcelFileDescriptor> readResult =
2478                         readResponse.getResult();
2479                 assertFalse(readResult.isSuccess());
2480 
2481                 assertThat(readResult.getFailures().keySet()).containsExactly(handle);
2482                 assertThat(readResult.getFailures().get(handle).getResultCode())
2483                         .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
2484                 assertThat(readResult.getFailures().get(handle).getErrorMessage())
2485                         .contains("Cannot find the blob for handle");
2486             }
2487         } finally {
2488             mDb1.removeBlobAsync(ImmutableSet.of(handle)).get();
2489         }
2490     }
2491 
2492     @Test
2493     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
testReadBlob_notSupported()2494     public void testReadBlob_notSupported() throws Exception {
2495         assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.BLOB_STORAGE));
2496         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
2497         byte[] data = generateRandomBytes(10); // 10 Bytes
2498         byte[] digest = calculateDigest(data);
2499         AppSearchBlobHandle handle =
2500                 AppSearchBlobHandle.createWithSha256(
2501                         digest, mContext.getPackageName(), DB_NAME_1, "ns");
2502 
2503         UnsupportedOperationException exception =
2504                 assertThrows(
2505                         UnsupportedOperationException.class,
2506                         () -> mGlobalSearchSession.openBlobForReadAsync(ImmutableSet.of(handle)));
2507         assertThat(exception)
2508                 .hasMessageThat()
2509                 .contains(
2510                         Features.BLOB_STORAGE
2511                                 + " is not available on this AppSearch implementation.");
2512     }
2513 }
2514