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