blob: 47337cac18e1651c3498189031480176f5944f46 [file] [log] [blame]
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.app.appsearch.cts.app;
import static android.app.appsearch.testutil.AppSearchTestUtils.checkIsBatchResultSuccess;
import static android.app.appsearch.testutil.AppSearchTestUtils.convertSearchResultsToDocuments;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
import android.annotation.NonNull;
import android.app.appsearch.AppSearchBatchResult;
import android.app.appsearch.AppSearchResult;
import android.app.appsearch.AppSearchSchema;
import android.app.appsearch.AppSearchSchema.PropertyConfig;
import android.app.appsearch.AppSearchSessionShim;
import android.app.appsearch.Features;
import android.app.appsearch.GenericDocument;
import android.app.appsearch.GetByDocumentIdRequest;
import android.app.appsearch.GetSchemaResponse;
import android.app.appsearch.GlobalSearchSessionShim;
import android.app.appsearch.Migrator;
import android.app.appsearch.PutDocumentsRequest;
import android.app.appsearch.RemoveByDocumentIdRequest;
import android.app.appsearch.ReportSystemUsageRequest;
import android.app.appsearch.SearchResult;
import android.app.appsearch.SearchResultsShim;
import android.app.appsearch.SearchSpec;
import android.app.appsearch.SetSchemaRequest;
import android.app.appsearch.exceptions.AppSearchException;
import android.app.appsearch.observer.DocumentChangeInfo;
import android.app.appsearch.observer.ObserverSpec;
import android.app.appsearch.observer.SchemaChangeInfo;
import android.app.appsearch.testutil.AppSearchEmail;
import android.app.appsearch.testutil.TestObserverCallback;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.ListenableFuture;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public abstract class GlobalSearchSessionCtsTestBase {
static final String DB_NAME_1 = "";
static final String DB_NAME_2 = "testDb2";
private static final Executor EXECUTOR = Executors.newCachedThreadPool();
private final Context mContext = ApplicationProvider.getApplicationContext();
protected AppSearchSessionShim mDb1;
protected AppSearchSessionShim mDb2;
protected GlobalSearchSessionShim mGlobalSearchSession;
protected abstract ListenableFuture<AppSearchSessionShim> createSearchSessionAsync(
@NonNull String dbName);
protected abstract ListenableFuture<GlobalSearchSessionShim> createGlobalSearchSessionAsync();
@Before
public void setUp() throws Exception {
mDb1 = createSearchSessionAsync(DB_NAME_1).get();
mDb2 = createSearchSessionAsync(DB_NAME_2).get();
// Cleanup whatever documents may still exist in these databases. This is needed in
// addition to tearDown in case a test exited without completing properly.
cleanup();
mGlobalSearchSession = createGlobalSearchSessionAsync().get();
}
@After
public void tearDown() throws Exception {
// Cleanup whatever documents may still exist in these databases.
cleanup();
}
private void cleanup() throws Exception {
mDb1.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
mDb2.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
}
private List<GenericDocument> snapshotResults(String queryExpression, SearchSpec spec)
throws Exception {
SearchResultsShim searchResults = mGlobalSearchSession.search(queryExpression, spec);
return convertSearchResultsToDocuments(searchResults);
}
/**
* Asserts that the union of {@code addedDocuments} and {@code beforeDocuments} is exactly
* equivalent to {@code afterDocuments}. Order doesn't matter.
*
* @param beforeDocuments Documents that existed first.
* @param afterDocuments The total collection of documents that should exist now.
* @param addedDocuments The collection of documents that were expected to be added.
*/
private void assertAddedBetweenSnapshots(
List<? extends GenericDocument> beforeDocuments,
List<? extends GenericDocument> afterDocuments,
List<? extends GenericDocument> addedDocuments) {
List<GenericDocument> expectedDocuments = new ArrayList<>(beforeDocuments);
expectedDocuments.addAll(addedDocuments);
assertThat(afterDocuments).containsExactlyElementsIn(expectedDocuments);
}
@Test
public void testGlobalGetById() throws Exception {
assumeTrue(
mGlobalSearchSession
.getFeatures()
.isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_GET_BY_ID));
SearchSpec exactSearchSpec =
new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY).build();
// Schema registration
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
.get();
AppSearchBatchResult<String, GenericDocument> nonExistent =
mGlobalSearchSession
.getByDocumentIdAsync(
mContext.getPackageName(),
DB_NAME_1,
new GetByDocumentIdRequest.Builder("namespace")
.addIds("id1")
.build())
.get();
assertThat(nonExistent.isSuccess()).isFalse();
assertThat(nonExistent.getSuccesses()).isEmpty();
assertThat(nonExistent.getFailures()).containsKey("id1");
assertThat(nonExistent.getFailures().get("id1").getResultCode())
.isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
// Index a document
AppSearchEmail inEmail =
new AppSearchEmail.Builder("namespace", "id1")
.setFrom("from@example.com")
.setTo("to1@example.com", "to2@example.com")
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
checkIsBatchResultSuccess(
mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
// Query for the document
AppSearchBatchResult<String, GenericDocument> afterPutDocuments =
mGlobalSearchSession
.getByDocumentIdAsync(
mContext.getPackageName(),
DB_NAME_1,
new GetByDocumentIdRequest.Builder("namespace")
.addIds("id1")
.build())
.get();
assertThat(afterPutDocuments.getSuccesses()).containsExactly("id1", inEmail);
}
@Test
public void testGlobalGetById_nonExistentPackage() throws Exception {
assumeTrue(
mGlobalSearchSession
.getFeatures()
.isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_GET_BY_ID));
AppSearchBatchResult<String, GenericDocument> fakePackage =
mGlobalSearchSession
.getByDocumentIdAsync(
"fake",
DB_NAME_1,
new GetByDocumentIdRequest.Builder("namespace")
.addIds("id1")
.build())
.get();
assertThat(fakePackage.getFailures()).hasSize(1);
assertThat(fakePackage.getFailures().get("id1").getResultCode())
.isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
}
@Test
public void testGlobalQuery_oneInstance() throws Exception {
// Snapshot what documents may already exist on the device.
SearchSpec exactSearchSpec =
new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY).build();
List<GenericDocument> beforeBodyDocuments = snapshotResults("body", exactSearchSpec);
List<GenericDocument> beforeBodyEmailDocuments =
snapshotResults("body email", exactSearchSpec);
// Schema registration
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
.get();
// Index a document
AppSearchEmail inEmail =
new AppSearchEmail.Builder("namespace", "id1")
.setFrom("from@example.com")
.setTo("to1@example.com", "to2@example.com")
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
checkIsBatchResultSuccess(
mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
// Query for the document
List<GenericDocument> afterBodyDocuments = snapshotResults("body", exactSearchSpec);
assertAddedBetweenSnapshots(
beforeBodyDocuments, afterBodyDocuments, Collections.singletonList(inEmail));
// Multi-term query
List<GenericDocument> afterBodyEmailDocuments =
snapshotResults("body email", exactSearchSpec);
assertAddedBetweenSnapshots(
beforeBodyEmailDocuments,
afterBodyEmailDocuments,
Collections.singletonList(inEmail));
}
@Test
public void testGlobalQuery_twoInstances() throws Exception {
// Snapshot what documents may already exist on the device.
SearchSpec exactSearchSpec =
new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY).build();
List<GenericDocument> beforeBodyDocuments = snapshotResults("body", exactSearchSpec);
// Schema registration
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
.get();
mDb2.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
.get();
// Index a document to instance 1.
AppSearchEmail inEmail1 =
new AppSearchEmail.Builder("namespace", "id1")
.setFrom("from@example.com")
.setTo("to1@example.com", "to2@example.com")
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
checkIsBatchResultSuccess(
mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
// Index a document to instance 2.
AppSearchEmail inEmail2 =
new AppSearchEmail.Builder("namespace", "id2")
.setFrom("from@example.com")
.setTo("to1@example.com", "to2@example.com")
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
checkIsBatchResultSuccess(
mDb2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
// Query across all instances
List<GenericDocument> afterBodyDocuments = snapshotResults("body", exactSearchSpec);
assertAddedBetweenSnapshots(
beforeBodyDocuments, afterBodyDocuments, ImmutableList.of(inEmail1, inEmail2));
}
@Test
public void testGlobalQuery_getNextPage() throws Exception {
// Snapshot what documents may already exist on the device.
SearchSpec exactSearchSpec =
new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY).build();
List<GenericDocument> beforeBodyDocuments = snapshotResults("body", exactSearchSpec);
// Schema registration
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
.get();
List<AppSearchEmail> emailList = new ArrayList<>();
PutDocumentsRequest.Builder putDocumentsRequestBuilder = new PutDocumentsRequest.Builder();
// Index 31 documents
for (int i = 0; i < 31; i++) {
AppSearchEmail inEmail =
new AppSearchEmail.Builder("namespace", "id" + i)
.setFrom("from@example.com")
.setTo("to1@example.com", "to2@example.com")
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
emailList.add(inEmail);
putDocumentsRequestBuilder.addGenericDocuments(inEmail);
}
checkIsBatchResultSuccess(mDb1.putAsync(putDocumentsRequestBuilder.build()));
// Set number of results per page is 7.
int pageSize = 7;
SearchResultsShim searchResults =
mGlobalSearchSession.search(
"body",
new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.setResultCountPerPage(pageSize)
.build());
List<GenericDocument> documents = new ArrayList<>();
int pageNumber = 0;
List<SearchResult> results;
// keep loading next page until it's empty.
do {
results = searchResults.getNextPageAsync().get();
++pageNumber;
for (SearchResult result : results) {
documents.add(result.getGenericDocument());
}
} while (results.size() > 0);
// check all document presents
assertAddedBetweenSnapshots(beforeBodyDocuments, documents, emailList);
int totalDocuments = beforeBodyDocuments.size() + documents.size();
// +1 for final empty page
int expectedPages = (int) Math.ceil(totalDocuments * 1.0 / pageSize) + 1;
assertThat(pageNumber).isEqualTo(expectedPages);
}
@Test
public void testGlobalQuery_acrossTypes() throws Exception {
// Snapshot what documents may already exist on the device.
SearchSpec exactSearchSpec =
new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY).build();
List<GenericDocument> beforeBodyDocuments = snapshotResults("body", exactSearchSpec);
SearchSpec exactEmailSearchSpec =
new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
.build();
List<GenericDocument> beforeBodyEmailDocuments =
snapshotResults("body", exactEmailSearchSpec);
// Schema registration
AppSearchSchema genericSchema =
new AppSearchSchema.Builder("Generic")
.addProperty(
new AppSearchSchema.StringPropertyConfig.Builder("foo")
.setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
.setTokenizerType(
AppSearchSchema.StringPropertyConfig
.TOKENIZER_TYPE_PLAIN)
.setIndexingType(
AppSearchSchema.StringPropertyConfig
.INDEXING_TYPE_PREFIXES)
.build())
.build();
// db1 has both "Generic" and "builtin:Email"
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(genericSchema)
.addSchemas(AppSearchEmail.SCHEMA)
.build())
.get();
// db2 only has "builtin:Email"
mDb2.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
.get();
// Index a generic document into db1
GenericDocument genericDocument =
new GenericDocument.Builder<>("namespace", "id2", "Generic")
.setPropertyString("foo", "body")
.build();
checkIsBatchResultSuccess(
mDb1.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(genericDocument)
.build()));
AppSearchEmail email =
new AppSearchEmail.Builder("namespace", "id1")
.setFrom("from@example.com")
.setTo("to1@example.com", "to2@example.com")
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
// Put the email in both databases
checkIsBatchResultSuccess(
(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email).build())));
checkIsBatchResultSuccess(
mDb2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
// Query for all documents across types
List<GenericDocument> afterBodyDocuments = snapshotResults("body", exactSearchSpec);
assertAddedBetweenSnapshots(
beforeBodyDocuments,
afterBodyDocuments,
ImmutableList.of(genericDocument, email, email));
// Query only for email documents
List<GenericDocument> afterBodyEmailDocuments =
snapshotResults("body", exactEmailSearchSpec);
assertAddedBetweenSnapshots(
beforeBodyEmailDocuments, afterBodyEmailDocuments, ImmutableList.of(email, email));
}
@Test
public void testGlobalQuery_namespaceFilter() throws Exception {
// Snapshot what documents may already exist on the device.
SearchSpec exactSearchSpec =
new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY).build();
List<GenericDocument> beforeBodyDocuments = snapshotResults("body", exactSearchSpec);
SearchSpec exactNamespace1SearchSpec =
new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.addFilterNamespaces("namespace1")
.build();
List<GenericDocument> beforeBodyNamespace1Documents =
snapshotResults("body", exactNamespace1SearchSpec);
// Schema registration
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
.get();
mDb2.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
.get();
// Index two documents
AppSearchEmail document1 =
new AppSearchEmail.Builder("namespace1", "id1")
.setFrom("from@example.com")
.setTo("to1@example.com", "to2@example.com")
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
checkIsBatchResultSuccess(
mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(document1).build()));
AppSearchEmail document2 =
new AppSearchEmail.Builder("namespace2", "id1")
.setFrom("from@example.com")
.setTo("to1@example.com", "to2@example.com")
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
checkIsBatchResultSuccess(
mDb2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(document2).build()));
// Query for all namespaces
List<GenericDocument> afterBodyDocuments = snapshotResults("body", exactSearchSpec);
assertAddedBetweenSnapshots(
beforeBodyDocuments, afterBodyDocuments, ImmutableList.of(document1, document2));
// Query only for "namespace1"
List<GenericDocument> afterBodyNamespace1Documents =
snapshotResults("body", exactNamespace1SearchSpec);
assertAddedBetweenSnapshots(
beforeBodyNamespace1Documents,
afterBodyNamespace1Documents,
ImmutableList.of(document1));
}
@Test
public void testGlobalQuery_packageFilter() throws Exception {
// Snapshot what documents may already exist on the device.
SearchSpec otherPackageSearchSpec =
new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.addFilterPackageNames("some.other.package")
.build();
List<GenericDocument> beforeOtherPackageDocuments =
snapshotResults("body", otherPackageSearchSpec);
SearchSpec testPackageSearchSpec =
new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.addFilterPackageNames(mContext.getPackageName())
.build();
List<GenericDocument> beforeTestPackageDocuments =
snapshotResults("body", testPackageSearchSpec);
// Schema registration
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
.get();
mDb2.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
.get();
// Index two documents
AppSearchEmail document1 =
new AppSearchEmail.Builder("namespace1", "id1")
.setFrom("from@example.com")
.setTo("to1@example.com", "to2@example.com")
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
checkIsBatchResultSuccess(
mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(document1).build()));
AppSearchEmail document2 =
new AppSearchEmail.Builder("namespace2", "id1")
.setFrom("from@example.com")
.setTo("to1@example.com", "to2@example.com")
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
checkIsBatchResultSuccess(
mDb2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(document2).build()));
// Query in some other package
List<GenericDocument> afterOtherPackageDocuments =
snapshotResults("body", otherPackageSearchSpec);
assertAddedBetweenSnapshots(
beforeOtherPackageDocuments, afterOtherPackageDocuments, Collections.emptyList());
// Query within our package
List<GenericDocument> afterTestPackageDocuments =
snapshotResults("body", testPackageSearchSpec);
assertAddedBetweenSnapshots(
beforeTestPackageDocuments,
afterTestPackageDocuments,
ImmutableList.of(document1, document2));
}
// TODO(b/175039682) Add test cases for wildcard projection once go/oag/1534646 is submitted.
@Test
public void testGlobalQuery_projectionTwoInstances() throws Exception {
// Schema registration
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
.get();
mDb2.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
.get();
// Index one document in each database.
AppSearchEmail email1 =
new AppSearchEmail.Builder("namespace", "id1")
.setCreationTimestampMillis(1000)
.setFrom("from@example.com")
.setTo("to1@example.com", "to2@example.com")
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
checkIsBatchResultSuccess(
mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
AppSearchEmail email2 =
new AppSearchEmail.Builder("namespace", "id2")
.setCreationTimestampMillis(1000)
.setFrom("from@example.com")
.setTo("to1@example.com", "to2@example.com")
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
checkIsBatchResultSuccess(
mDb2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
// Query with type property paths {"Email", ["subject", "to"]}
List<GenericDocument> documents =
snapshotResults(
"body",
new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.addProjection(
AppSearchEmail.SCHEMA_TYPE,
ImmutableList.of("subject", "to"))
.build());
// The two email documents should have been returned with only the "subject" and "to"
// properties.
AppSearchEmail expected1 =
new AppSearchEmail.Builder("namespace", "id2")
.setCreationTimestampMillis(1000)
.setTo("to1@example.com", "to2@example.com")
.setSubject("testPut example")
.build();
AppSearchEmail expected2 =
new AppSearchEmail.Builder("namespace", "id1")
.setCreationTimestampMillis(1000)
.setTo("to1@example.com", "to2@example.com")
.setSubject("testPut example")
.build();
assertThat(documents).containsExactly(expected1, expected2);
}
@Test
public void testGlobalQuery_projectionEmptyTwoInstances() throws Exception {
// Schema registration
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
.get();
mDb2.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
.get();
// Index one document in each database.
AppSearchEmail email1 =
new AppSearchEmail.Builder("namespace", "id1")
.setCreationTimestampMillis(1000)
.setFrom("from@example.com")
.setTo("to1@example.com", "to2@example.com")
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
checkIsBatchResultSuccess(
mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
AppSearchEmail email2 =
new AppSearchEmail.Builder("namespace", "id2")
.setCreationTimestampMillis(1000)
.setFrom("from@example.com")
.setTo("to1@example.com", "to2@example.com")
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
checkIsBatchResultSuccess(
mDb2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
// Query with type property paths {"Email", []}
List<GenericDocument> documents =
snapshotResults(
"body",
new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.addProjection(AppSearchEmail.SCHEMA_TYPE, Collections.emptyList())
.build());
// The two email documents should have been returned without any properties.
AppSearchEmail expected1 =
new AppSearchEmail.Builder("namespace", "id2")
.setCreationTimestampMillis(1000)
.build();
AppSearchEmail expected2 =
new AppSearchEmail.Builder("namespace", "id1")
.setCreationTimestampMillis(1000)
.build();
assertThat(documents).containsExactly(expected1, expected2);
}
@Test
public void testGlobalQuery_projectionNonExistentTypeTwoInstances() throws Exception {
// Schema registration
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
.get();
mDb2.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
.get();
// Index one document in each database.
AppSearchEmail email1 =
new AppSearchEmail.Builder("namespace", "id1")
.setCreationTimestampMillis(1000)
.setFrom("from@example.com")
.setTo("to1@example.com", "to2@example.com")
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
checkIsBatchResultSuccess(
mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
AppSearchEmail email2 =
new AppSearchEmail.Builder("namespace", "id2")
.setCreationTimestampMillis(1000)
.setFrom("from@example.com")
.setTo("to1@example.com", "to2@example.com")
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
checkIsBatchResultSuccess(
mDb2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
// Query with type property paths {"NonExistentType", []}, {"Email", ["subject", "to"]}
List<GenericDocument> documents =
snapshotResults(
"body",
new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.addProjection("NonExistentType", Collections.emptyList())
.addProjection(
AppSearchEmail.SCHEMA_TYPE,
ImmutableList.of("subject", "to"))
.build());
// The two email documents should have been returned with only the "subject" and "to"
// properties.
AppSearchEmail expected1 =
new AppSearchEmail.Builder("namespace", "id2")
.setCreationTimestampMillis(1000)
.setTo("to1@example.com", "to2@example.com")
.setSubject("testPut example")
.build();
AppSearchEmail expected2 =
new AppSearchEmail.Builder("namespace", "id1")
.setCreationTimestampMillis(1000)
.setTo("to1@example.com", "to2@example.com")
.setSubject("testPut example")
.build();
assertThat(documents).containsExactly(expected1, expected2);
}
@Test
public void testQuery_ResultGroupingLimits() throws Exception {
// Schema registration
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
.get();
mDb2.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
.get();
// Index one document in 'namespace1' and one document in 'namespace2' into db1.
AppSearchEmail inEmail1 =
new AppSearchEmail.Builder("namespace1", "id1")
.setFrom("from@example.com")
.setTo("to1@example.com", "to2@example.com")
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
checkIsBatchResultSuccess(
mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
AppSearchEmail inEmail2 =
new AppSearchEmail.Builder("namespace2", "id2")
.setFrom("from@example.com")
.setTo("to1@example.com", "to2@example.com")
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
checkIsBatchResultSuccess(
mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
// Index one document in 'namespace1' and one document in 'namespace2' into db2.
AppSearchEmail inEmail3 =
new AppSearchEmail.Builder("namespace1", "id3")
.setFrom("from@example.com")
.setTo("to1@example.com", "to2@example.com")
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
checkIsBatchResultSuccess(
mDb2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));
AppSearchEmail inEmail4 =
new AppSearchEmail.Builder("namespace2", "id4")
.setFrom("from@example.com")
.setTo("to1@example.com", "to2@example.com")
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
checkIsBatchResultSuccess(
mDb2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));
// Query with per package result grouping. Only the last document 'email4' should be
// returned.
List<GenericDocument> documents =
snapshotResults(
"body",
new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.setResultGrouping(
SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*resultLimit=*/ 1)
.build());
assertThat(documents).containsExactly(inEmail4);
// Query with per namespace result grouping. Only the last document in each namespace should
// be returned ('email4' and 'email3').
documents =
snapshotResults(
"body",
new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.setResultGrouping(
SearchSpec.GROUPING_TYPE_PER_NAMESPACE, /*resultLimit=*/ 1)
.build());
assertThat(documents).containsExactly(inEmail4, inEmail3);
// Query with per package and per namespace result grouping. Only the last document in each
// namespace should be returned ('email4' and 'email3').
documents =
snapshotResults(
"body",
new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.setResultGrouping(
SearchSpec.GROUPING_TYPE_PER_NAMESPACE
| SearchSpec.GROUPING_TYPE_PER_PACKAGE,
/*resultLimit=*/ 1)
.build());
assertThat(documents).containsExactly(inEmail4, inEmail3);
}
@Test
public void testReportSystemUsage_ForbiddenFromNonSystem() throws Exception {
// Index a document
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
.get();
AppSearchEmail email1 =
new AppSearchEmail.Builder("namespace", "id1")
.setCreationTimestampMillis(1000)
.setFrom("from@example.com")
.setTo("to1@example.com", "to2@example.com")
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
checkIsBatchResultSuccess(
mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
// Query
List<SearchResult> page;
try (SearchResultsShim results =
mGlobalSearchSession.search(
"",
new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
.build())) {
page = results.getNextPageAsync().get();
}
assertThat(page).isNotEmpty();
SearchResult firstResult = page.get(0);
ExecutionException exception =
assertThrows(
ExecutionException.class,
() ->
mGlobalSearchSession
.reportSystemUsageAsync(
new ReportSystemUsageRequest.Builder(
firstResult.getPackageName(),
firstResult.getDatabaseName(),
firstResult
.getGenericDocument()
.getNamespace(),
firstResult
.getGenericDocument()
.getId())
.build())
.get());
assertThat(exception).hasCauseThat().isInstanceOf(AppSearchException.class);
AppSearchException ase = (AppSearchException) exception.getCause();
assertThat(ase.getResultCode()).isEqualTo(AppSearchResult.RESULT_SECURITY_ERROR);
assertThat(ase)
.hasMessageThat()
.contains(
mContext.getPackageName() + " does not have access to report system usage");
}
@Test
public void testAddObserver_notSupported() {
assumeFalse(
mGlobalSearchSession
.getFeatures()
.isFeatureSupported(
Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
assertThrows(
UnsupportedOperationException.class,
() ->
mGlobalSearchSession.registerObserverCallback(
mContext.getPackageName(),
new ObserverSpec.Builder().build(),
EXECUTOR,
new TestObserverCallback()));
assertThrows(
UnsupportedOperationException.class,
() ->
mGlobalSearchSession.unregisterObserverCallback(
mContext.getPackageName(), new TestObserverCallback()));
}
@Test
public void testAddObserver() throws Exception {
assumeTrue(
mGlobalSearchSession
.getFeatures()
.isFeatureSupported(
Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
TestObserverCallback observer = new TestObserverCallback();
// Register observer. Note: the type does NOT exist yet!
mGlobalSearchSession.registerObserverCallback(
mContext.getPackageName(),
new ObserverSpec.Builder().addFilterSchemas(AppSearchEmail.SCHEMA_TYPE).build(),
EXECUTOR,
observer);
// Index a document
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
.get();
AppSearchEmail email1 = new AppSearchEmail.Builder("namespace", "id1").build();
checkIsBatchResultSuccess(
mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
// Make sure the notification was received.
observer.waitForNotificationCount(2);
assertThat(observer.getSchemaChanges())
.containsExactly(
new SchemaChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
/*changedSchemaNames=*/ ImmutableSet.of(
AppSearchEmail.SCHEMA_TYPE)));
assertThat(observer.getDocumentChanges())
.containsExactly(
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace",
AppSearchEmail.SCHEMA_TYPE,
/*changedDocumentIds=*/ ImmutableSet.of("id1")));
}
@Test
public void testRegisterObserver_MultiType() throws Exception {
assumeTrue(
mGlobalSearchSession
.getFeatures()
.isFeatureSupported(
Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
TestObserverCallback unfilteredObserver = new TestObserverCallback();
TestObserverCallback emailObserver = new TestObserverCallback();
// Set up the email type in both databases, and the gift type in db1
AppSearchSchema giftSchema =
new AppSearchSchema.Builder("Gift")
.addProperty(
new AppSearchSchema.DoublePropertyConfig.Builder("price").build())
.build();
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA, giftSchema)
.build())
.get();
mDb2.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
.get();
// Register two observers. One has no filters, the other filters on email.
mGlobalSearchSession.registerObserverCallback(
mContext.getPackageName(),
new ObserverSpec.Builder().build(),
EXECUTOR,
unfilteredObserver);
mGlobalSearchSession.registerObserverCallback(
mContext.getPackageName(),
new ObserverSpec.Builder().addFilterSchemas(AppSearchEmail.SCHEMA_TYPE).build(),
EXECUTOR,
emailObserver);
// Make sure everything is empty
assertThat(unfilteredObserver.getSchemaChanges()).isEmpty();
assertThat(unfilteredObserver.getDocumentChanges()).isEmpty();
assertThat(emailObserver.getSchemaChanges()).isEmpty();
assertThat(emailObserver.getDocumentChanges()).isEmpty();
// Index some documents
AppSearchEmail email1 = new AppSearchEmail.Builder("namespace", "id1").build();
GenericDocument gift1 =
new GenericDocument.Builder<GenericDocument.Builder<?>>("namespace2", "id2", "Gift")
.build();
checkIsBatchResultSuccess(
mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
checkIsBatchResultSuccess(
mDb1.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(email1, gift1)
.build()));
checkIsBatchResultSuccess(
mDb2.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
checkIsBatchResultSuccess(
mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(gift1).build()));
// Make sure the notification was received.
unfilteredObserver.waitForNotificationCount(5);
emailObserver.waitForNotificationCount(3);
assertThat(unfilteredObserver.getSchemaChanges()).isEmpty();
assertThat(unfilteredObserver.getDocumentChanges())
.containsExactly(
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace",
AppSearchEmail.SCHEMA_TYPE,
/*changedDocumentIds=*/ ImmutableSet.of("id1")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace",
AppSearchEmail.SCHEMA_TYPE,
/*changedDocumentIds=*/ ImmutableSet.of("id1")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace2",
"Gift",
/*changedDocumentIds=*/ ImmutableSet.of("id2")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_2,
"namespace",
AppSearchEmail.SCHEMA_TYPE,
/*changedDocumentIds=*/ ImmutableSet.of("id1")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace2",
"Gift",
/*changedDocumentIds=*/ ImmutableSet.of("id2")));
// Check the filtered observer
assertThat(emailObserver.getSchemaChanges()).isEmpty();
assertThat(emailObserver.getDocumentChanges())
.containsExactly(
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace",
AppSearchEmail.SCHEMA_TYPE,
/*changedDocumentIds=*/ ImmutableSet.of("id1")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace",
AppSearchEmail.SCHEMA_TYPE,
/*changedDocumentIds=*/ ImmutableSet.of("id1")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_2,
"namespace",
AppSearchEmail.SCHEMA_TYPE,
/*changedDocumentIds=*/ ImmutableSet.of("id1")));
}
@Test
public void testRegisterObserver_removeById() throws Exception {
assumeTrue(
mGlobalSearchSession
.getFeatures()
.isFeatureSupported(
Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
TestObserverCallback unfilteredObserver = new TestObserverCallback();
TestObserverCallback emailObserver = new TestObserverCallback();
// Set up the email and gift types in both databases
AppSearchSchema giftSchema =
new AppSearchSchema.Builder("Gift")
.addProperty(
new AppSearchSchema.DoublePropertyConfig.Builder("price").build())
.build();
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA, giftSchema)
.build())
.get();
mDb2.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA, giftSchema)
.build())
.get();
// Register two observers. One, registered later, has no filters. The other, registered
// now, filters on email.
mGlobalSearchSession.registerObserverCallback(
mContext.getPackageName(),
new ObserverSpec.Builder().addFilterSchemas(AppSearchEmail.SCHEMA_TYPE).build(),
EXECUTOR,
emailObserver);
// Make sure everything is empty
assertThat(unfilteredObserver.getSchemaChanges()).isEmpty();
assertThat(unfilteredObserver.getDocumentChanges()).isEmpty();
assertThat(emailObserver.getSchemaChanges()).isEmpty();
assertThat(emailObserver.getDocumentChanges()).isEmpty();
// Index some documents
AppSearchEmail email1 = new AppSearchEmail.Builder("namespace", "id1").build();
GenericDocument gift1 =
new GenericDocument.Builder<GenericDocument.Builder<?>>("namespace2", "id2", "Gift")
.build();
checkIsBatchResultSuccess(
mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
checkIsBatchResultSuccess(
mDb1.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(email1, gift1)
.build()));
checkIsBatchResultSuccess(
mDb2.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(email1, gift1)
.build()));
checkIsBatchResultSuccess(
mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(gift1).build()));
// Register the second observer
mGlobalSearchSession.registerObserverCallback(
mContext.getPackageName(),
new ObserverSpec.Builder().build(),
EXECUTOR,
unfilteredObserver);
// Remove some of the documents.
checkIsBatchResultSuccess(
mDb1.removeAsync(
new RemoveByDocumentIdRequest.Builder("namespace").addIds("id1").build()));
checkIsBatchResultSuccess(
mDb2.removeAsync(
new RemoveByDocumentIdRequest.Builder("namespace2").addIds("id2").build()));
// Make sure the notification was received. emailObserver should have seen:
// +db1:email, +db1:email, +db2:email, -db1:email.
// unfilteredObserver (registered later) should have seen:
// -db1:email, -db2:gift
emailObserver.waitForNotificationCount(4);
unfilteredObserver.waitForNotificationCount(2);
assertThat(emailObserver.getSchemaChanges()).isEmpty();
assertThat(emailObserver.getDocumentChanges())
.containsExactly(
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace",
AppSearchEmail.SCHEMA_TYPE,
/*changedDocumentIds=*/ ImmutableSet.of("id1")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace",
AppSearchEmail.SCHEMA_TYPE,
/*changedDocumentIds=*/ ImmutableSet.of("id1")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_2,
"namespace",
AppSearchEmail.SCHEMA_TYPE,
/*changedDocumentIds=*/ ImmutableSet.of("id1")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace",
AppSearchEmail.SCHEMA_TYPE,
/*changedDocumentIds=*/ ImmutableSet.of("id1")));
// Check unfilteredObserver
assertThat(unfilteredObserver.getSchemaChanges()).isEmpty();
assertThat(unfilteredObserver.getDocumentChanges())
.containsExactly(
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace",
AppSearchEmail.SCHEMA_TYPE,
/*changedDocumentIds=*/ ImmutableSet.of("id1")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_2,
"namespace2",
"Gift",
/*changedDocumentIds=*/ ImmutableSet.of("id2")));
}
@Test
public void testRegisterObserver_removeByQuery() throws Exception {
assumeTrue(
mGlobalSearchSession
.getFeatures()
.isFeatureSupported(
Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
TestObserverCallback unfilteredObserver = new TestObserverCallback();
TestObserverCallback emailObserver = new TestObserverCallback();
// Set up the email and gift types in both databases
AppSearchSchema giftSchema =
new AppSearchSchema.Builder("Gift")
.addProperty(
new AppSearchSchema.DoublePropertyConfig.Builder("price").build())
.build();
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA, giftSchema)
.build())
.get();
mDb2.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA, giftSchema)
.build())
.get();
// Index some documents
AppSearchEmail email1 = new AppSearchEmail.Builder("namespace", "id1").build();
AppSearchEmail email2 =
new AppSearchEmail.Builder("namespace", "id2").setBody("caterpillar").build();
GenericDocument gift1 =
new GenericDocument.Builder<GenericDocument.Builder<?>>("namespace2", "id3", "Gift")
.build();
checkIsBatchResultSuccess(
mDb1.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(email1, email2, gift1)
.build()));
checkIsBatchResultSuccess(
mDb2.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(email1, email2, gift1)
.build()));
// Register observers
mGlobalSearchSession.registerObserverCallback(
mContext.getPackageName(),
new ObserverSpec.Builder().build(),
EXECUTOR,
unfilteredObserver);
mGlobalSearchSession.registerObserverCallback(
mContext.getPackageName(),
new ObserverSpec.Builder().addFilterSchemas(AppSearchEmail.SCHEMA_TYPE).build(),
EXECUTOR,
emailObserver);
// Make sure everything is empty
assertThat(unfilteredObserver.getSchemaChanges()).isEmpty();
assertThat(unfilteredObserver.getDocumentChanges()).isEmpty();
assertThat(emailObserver.getSchemaChanges()).isEmpty();
assertThat(emailObserver.getDocumentChanges()).isEmpty();
// Remove "cat" emails in db1 and all types in db2
mDb1.removeAsync(
"cat",
new SearchSpec.Builder()
.addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
.build())
.get();
mDb2.removeAsync("", new SearchSpec.Builder().build()).get();
// Make sure the notification was received. UnfilteredObserver should have seen:
// -db1:id2, -db2:id1, -db2:id2, -db2:id3
// emailObserver should have seen:
// -db1:id2, -db2:id1, -db2:id2
unfilteredObserver.waitForNotificationCount(3);
emailObserver.waitForNotificationCount(2);
assertThat(unfilteredObserver.getSchemaChanges()).isEmpty();
assertThat(unfilteredObserver.getDocumentChanges())
.containsExactly(
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace",
AppSearchEmail.SCHEMA_TYPE,
/*changedDocumentIds=*/ ImmutableSet.of("id2")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_2,
"namespace",
AppSearchEmail.SCHEMA_TYPE,
/*changedDocumentIds=*/ ImmutableSet.of("id1", "id2")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_2,
"namespace2",
"Gift",
/*changedDocumentIds=*/ ImmutableSet.of("id3")));
// Check emailObserver
assertThat(emailObserver.getSchemaChanges()).isEmpty();
assertThat(emailObserver.getDocumentChanges())
.containsExactly(
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace",
AppSearchEmail.SCHEMA_TYPE,
/*changedDocumentIds=*/ ImmutableSet.of("id2")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_2,
"namespace",
AppSearchEmail.SCHEMA_TYPE,
/*changedDocumentIds=*/ ImmutableSet.of("id1", "id2")));
}
@Test
public void testRegisterObserver_sameCallback_differentSpecs() throws Exception {
assumeTrue(
mGlobalSearchSession
.getFeatures()
.isFeatureSupported(
Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
TestObserverCallback observer = new TestObserverCallback();
// Set up the email and gift types
AppSearchSchema giftSchema =
new AppSearchSchema.Builder("Gift")
.addProperty(
new AppSearchSchema.DoublePropertyConfig.Builder("price").build())
.build();
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA, giftSchema)
.build())
.get();
// Register the same observer twice: once for gift, once for email
mGlobalSearchSession.registerObserverCallback(
mContext.getPackageName(),
new ObserverSpec.Builder().addFilterSchemas("Gift").build(),
EXECUTOR,
observer);
mGlobalSearchSession.registerObserverCallback(
mContext.getPackageName(),
new ObserverSpec.Builder().addFilterSchemas(AppSearchEmail.SCHEMA_TYPE).build(),
EXECUTOR,
observer);
// Index one email and one gift
AppSearchEmail email1 = new AppSearchEmail.Builder("namespace", "id1").build();
GenericDocument gift1 =
new GenericDocument.Builder<GenericDocument.Builder<?>>("namespace2", "id3", "Gift")
.build();
checkIsBatchResultSuccess(
mDb1.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(email1, gift1)
.build()));
// Make sure the same observer received both values
observer.waitForNotificationCount(2);
assertThat(observer.getSchemaChanges()).isEmpty();
assertThat(observer.getDocumentChanges())
.containsExactly(
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace",
AppSearchEmail.SCHEMA_TYPE,
/*changedDocumentIds=*/ ImmutableSet.of("id1")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace2",
"Gift",
/*changedDocumentIds=*/ ImmutableSet.of("id3")));
}
@Test
public void testRemoveObserver() throws Exception {
assumeTrue(
mGlobalSearchSession
.getFeatures()
.isFeatureSupported(
Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
TestObserverCallback temporaryObserver = new TestObserverCallback();
TestObserverCallback permanentObserver = new TestObserverCallback();
// Set up the email and gift types
AppSearchSchema giftSchema =
new AppSearchSchema.Builder("Gift")
.addProperty(
new AppSearchSchema.DoublePropertyConfig.Builder("price").build())
.build();
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA, giftSchema)
.build())
.get();
mDb2.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA, giftSchema)
.build())
.get();
// Register both observers. temporaryObserver is registered twice to ensure both instances
// get removed.
mGlobalSearchSession.registerObserverCallback(
mContext.getPackageName(),
new ObserverSpec.Builder().addFilterSchemas(AppSearchEmail.SCHEMA_TYPE).build(),
EXECUTOR,
temporaryObserver);
mGlobalSearchSession.registerObserverCallback(
mContext.getPackageName(),
new ObserverSpec.Builder().addFilterSchemas("Gift").build(),
EXECUTOR,
temporaryObserver);
mGlobalSearchSession.registerObserverCallback(
mContext.getPackageName(),
new ObserverSpec.Builder().build(),
EXECUTOR,
permanentObserver);
// Make sure everything is empty
assertThat(temporaryObserver.getSchemaChanges()).isEmpty();
assertThat(temporaryObserver.getDocumentChanges()).isEmpty();
assertThat(permanentObserver.getSchemaChanges()).isEmpty();
assertThat(permanentObserver.getDocumentChanges()).isEmpty();
// Index some documents
AppSearchEmail email1 = new AppSearchEmail.Builder("namespace", "id1").build();
AppSearchEmail email2 =
new AppSearchEmail.Builder("namespace", "id2").setBody("caterpillar").build();
GenericDocument gift1 =
new GenericDocument.Builder<GenericDocument.Builder<?>>("namespace2", "id3", "Gift")
.build();
GenericDocument gift2 =
new GenericDocument.Builder<GenericDocument.Builder<?>>("namespace3", "id4", "Gift")
.build();
checkIsBatchResultSuccess(
mDb1.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(email1, gift1)
.build()));
// Make sure the notifications were received.
temporaryObserver.waitForNotificationCount(2);
permanentObserver.waitForNotificationCount(2);
List<DocumentChangeInfo> expectedChangesOrig =
ImmutableList.of(
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace",
AppSearchEmail.SCHEMA_TYPE,
/*changedDocumentIds=*/ ImmutableSet.of("id1")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace2",
"Gift",
/*changedDocumentIds=*/ ImmutableSet.of("id3")));
assertThat(temporaryObserver.getSchemaChanges()).isEmpty();
assertThat(temporaryObserver.getDocumentChanges())
.containsExactlyElementsIn(expectedChangesOrig);
assertThat(permanentObserver.getSchemaChanges()).isEmpty();
assertThat(permanentObserver.getDocumentChanges())
.containsExactlyElementsIn(expectedChangesOrig);
// Unregister temporaryObserver
mGlobalSearchSession.unregisterObserverCallback(
mContext.getPackageName(), temporaryObserver);
// Index some more documents
checkIsBatchResultSuccess(
mDb1.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(email2, gift2)
.build()));
// Only the permanent observer should have received this
permanentObserver.waitForNotificationCount(4);
temporaryObserver.waitForNotificationCount(2);
assertThat(permanentObserver.getSchemaChanges()).isEmpty();
assertThat(permanentObserver.getDocumentChanges())
.containsExactly(
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace",
AppSearchEmail.SCHEMA_TYPE,
/*changedDocumentIds=*/ ImmutableSet.of("id1")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace2",
"Gift",
/*changedDocumentIds=*/ ImmutableSet.of("id3")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace",
AppSearchEmail.SCHEMA_TYPE,
/*changedDocumentIds=*/ ImmutableSet.of("id2")),
new DocumentChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
"namespace3",
"Gift",
/*changedDocumentIds=*/ ImmutableSet.of("id4")));
assertThat(temporaryObserver.getSchemaChanges()).isEmpty();
assertThat(temporaryObserver.getDocumentChanges())
.containsExactlyElementsIn(expectedChangesOrig);
}
@Test
public void testGlobalGetSchema() throws Exception {
assumeTrue(
mGlobalSearchSession
.getFeatures()
.isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA));
// One schema should be set with global access and the other should be set with local
// access.
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
.get();
mDb2.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(AppSearchEmail.SCHEMA)
.setSchemaTypeDisplayedBySystem(
AppSearchEmail.SCHEMA_TYPE, /*displayed=*/ false)
.build())
.get();
GetSchemaResponse response =
mGlobalSearchSession.getSchemaAsync(mContext.getPackageName(), DB_NAME_1).get();
assertThat(response.getSchemas()).containsExactly(AppSearchEmail.SCHEMA);
response = mGlobalSearchSession.getSchemaAsync(mContext.getPackageName(), DB_NAME_2).get();
assertThat(response.getSchemas()).containsExactly(AppSearchEmail.SCHEMA);
// A request for a db that doesn't exist should return a response with no schemas.
response =
mGlobalSearchSession
.getSchemaAsync(mContext.getPackageName(), "NonexistentDb")
.get();
assertThat(response.getSchemas()).isEmpty();
}
@Test
public void testGlobalGetSchema_notSupported() throws Exception {
assumeFalse(
mGlobalSearchSession
.getFeatures()
.isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA));
// One schema should be set with global access and the other should be set with local
// access.
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
.get();
UnsupportedOperationException e =
assertThrows(
UnsupportedOperationException.class,
() ->
mGlobalSearchSession.getSchemaAsync(
mContext.getPackageName(), DB_NAME_1));
assertThat(e)
.hasMessageThat()
.isEqualTo(
Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA
+ " is not supported on this AppSearch implementation.");
}
@Test
public void testGlobalGetByDocumentId_notSupported() throws Exception {
assumeFalse(
mGlobalSearchSession
.getFeatures()
.isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_GET_BY_ID));
Context context = ApplicationProvider.getApplicationContext();
UnsupportedOperationException e =
assertThrows(
UnsupportedOperationException.class,
() ->
mGlobalSearchSession.getByDocumentIdAsync(
context.getPackageName(),
DB_NAME_1,
new GetByDocumentIdRequest.Builder("namespace")
.addIds("id")
.build()));
assertThat(e)
.hasMessageThat()
.isEqualTo(
Features.GLOBAL_SEARCH_SESSION_GET_BY_ID
+ " is not supported on this AppSearch implementation.");
}
@Test
public void testAddObserver_schemaChange_added() throws Exception {
assumeTrue(
mDb1.getFeatures()
.isFeatureSupported(
Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
// Register an observer
TestObserverCallback observer = new TestObserverCallback();
mGlobalSearchSession.registerObserverCallback(
/*targetPackageName=*/ mContext.getPackageName(),
new ObserverSpec.Builder().build(),
EXECUTOR,
observer);
// Add a schema type
assertThat(observer.getSchemaChanges()).isEmpty();
assertThat(observer.getDocumentChanges()).isEmpty();
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(new AppSearchSchema.Builder("Type1").build())
.build())
.get();
observer.waitForNotificationCount(1);
assertThat(observer.getSchemaChanges())
.containsExactly(
new SchemaChangeInfo(
mContext.getPackageName(), DB_NAME_1, ImmutableSet.of("Type1")));
assertThat(observer.getDocumentChanges()).isEmpty();
// Add two more schema types without touching the existing one
observer.clear();
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(
new AppSearchSchema.Builder("Type1").build(),
new AppSearchSchema.Builder("Type2").build(),
new AppSearchSchema.Builder("Type3").build())
.build())
.get();
observer.waitForNotificationCount(1);
assertThat(observer.getSchemaChanges())
.containsExactly(
new SchemaChangeInfo(
mContext.getPackageName(),
DB_NAME_1,
ImmutableSet.of("Type2", "Type3")));
assertThat(observer.getDocumentChanges()).isEmpty();
}
@Test
public void testAddObserver_schemaChange_removed() throws Exception {
assumeTrue(
mDb1.getFeatures()
.isFeatureSupported(
Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
// Add a schema type
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(
new AppSearchSchema.Builder("Type1").build(),
new AppSearchSchema.Builder("Type2").build())
.build())
.get();
// Register an observer
TestObserverCallback observer = new TestObserverCallback();
mGlobalSearchSession.registerObserverCallback(
/*targetPackageName=*/ mContext.getPackageName(),
new ObserverSpec.Builder().build(),
EXECUTOR,
observer);
// Remove Type2
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(new AppSearchSchema.Builder("Type1").build())
.setForceOverride(true)
.build())
.get();
observer.waitForNotificationCount(1);
assertThat(observer.getSchemaChanges())
.containsExactly(
new SchemaChangeInfo(
mContext.getPackageName(), DB_NAME_1, ImmutableSet.of("Type2")));
assertThat(observer.getDocumentChanges()).isEmpty();
}
@Test
public void testAddObserver_schemaChange_contents() throws Exception {
assumeTrue(
mDb1.getFeatures()
.isFeatureSupported(
Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
// Add a schema
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(
new AppSearchSchema.Builder("Type1").build(),
new AppSearchSchema.Builder("Type2")
.addProperty(
new AppSearchSchema.BooleanPropertyConfig
.Builder("booleanProp")
.setCardinality(
PropertyConfig
.CARDINALITY_REQUIRED)
.build())
.build())
.build())
.get();
// Register an observer
TestObserverCallback observer = new TestObserverCallback();
mGlobalSearchSession.registerObserverCallback(
/*targetPackageName=*/ mContext.getPackageName(),
new ObserverSpec.Builder().build(),
EXECUTOR,
observer);
// Update the schema, but don't make any actual changes
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(
new AppSearchSchema.Builder("Type1").build(),
new AppSearchSchema.Builder("Type2")
.addProperty(
new AppSearchSchema.BooleanPropertyConfig
.Builder("booleanProp")
.setCardinality(
PropertyConfig
.CARDINALITY_REQUIRED)
.build())
.build())
.build())
.get();
// Now update the schema again, but this time actually make a change (cardinality of the
// property)
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(
new AppSearchSchema.Builder("Type1").build(),
new AppSearchSchema.Builder("Type2")
.addProperty(
new AppSearchSchema.BooleanPropertyConfig
.Builder("booleanProp")
.setCardinality(
PropertyConfig
.CARDINALITY_OPTIONAL)
.build())
.build())
.build())
.get();
// Dispatch notifications
observer.waitForNotificationCount(1);
assertThat(observer.getSchemaChanges())
.containsExactly(
new SchemaChangeInfo(
mContext.getPackageName(), DB_NAME_1, ImmutableSet.of("Type2")));
assertThat(observer.getDocumentChanges()).isEmpty();
}
@Test
public void testAddObserver_schemaChange_contents_skipBySpec() throws Exception {
assumeTrue(
mDb1.getFeatures()
.isFeatureSupported(
Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
// Add a schema
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(
new AppSearchSchema.Builder("Type1")
.addProperty(
new AppSearchSchema.BooleanPropertyConfig
.Builder("booleanProp")
.setCardinality(
PropertyConfig
.CARDINALITY_REQUIRED)
.build())
.build(),
new AppSearchSchema.Builder("Type2")
.addProperty(
new AppSearchSchema.BooleanPropertyConfig
.Builder("booleanProp")
.setCardinality(
PropertyConfig
.CARDINALITY_REQUIRED)
.build())
.build())
.build())
.get();
// Register an observer that only listens for Type2
TestObserverCallback observer = new TestObserverCallback();
mGlobalSearchSession.registerObserverCallback(
/*targetPackageName=*/ mContext.getPackageName(),
new ObserverSpec.Builder().addFilterSchemas("Type2").build(),
EXECUTOR,
observer);
// Update both types of the schema (changed cardinalities)
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.addSchemas(
new AppSearchSchema.Builder("Type1")
.addProperty(
new AppSearchSchema.BooleanPropertyConfig
.Builder("booleanProp")
.setCardinality(
PropertyConfig
.CARDINALITY_OPTIONAL)
.build())
.build(),
new AppSearchSchema.Builder("Type2")
.addProperty(
new AppSearchSchema.BooleanPropertyConfig
.Builder("booleanProp")
.setCardinality(
PropertyConfig
.CARDINALITY_OPTIONAL)
.build())
.build())
.build())
.get();
observer.waitForNotificationCount(1);
assertThat(observer.getSchemaChanges())
.containsExactly(
new SchemaChangeInfo(
mContext.getPackageName(), DB_NAME_1, ImmutableSet.of("Type2")));
assertThat(observer.getDocumentChanges()).isEmpty();
}
@Test
public void testRegisterObserver_schemaMigration() throws Exception {
assumeTrue(
mDb1.getFeatures()
.isFeatureSupported(
Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
// Add a schema with two types
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.setVersion(1)
.addSchemas(
new AppSearchSchema.Builder("Type1")
.addProperty(
new AppSearchSchema.StringPropertyConfig
.Builder("strProp1")
.build())
.build(),
new AppSearchSchema.Builder("Type2")
.addProperty(
new AppSearchSchema.LongPropertyConfig
.Builder("longProp1")
.build())
.build())
.build())
.get();
// Index some documents
GenericDocument type1doc1 =
new GenericDocument.Builder<GenericDocument.Builder<?>>(
"namespace", "t1id1", "Type1")
.setPropertyString("strProp1", "t1id1 prop value")
.build();
GenericDocument type1doc2 =
new GenericDocument.Builder<GenericDocument.Builder<?>>(
"namespace", "t1id2", "Type1")
.setPropertyString("strProp1", "t1id2 prop value")
.build();
GenericDocument type2doc1 =
new GenericDocument.Builder<GenericDocument.Builder<?>>(
"namespace", "t2id1", "Type2")
.setPropertyLong("longProp1", 41)
.build();
GenericDocument type2doc2 =
new GenericDocument.Builder<GenericDocument.Builder<?>>(
"namespace", "t2id2", "Type2")
.setPropertyLong("longProp1", 42)
.build();
mDb1.putAsync(
new PutDocumentsRequest.Builder()
.addGenericDocuments(type1doc1, type1doc2, type2doc1, type2doc2)
.build())
.get();
// Register an observer that only listens for Type1
TestObserverCallback observer = new TestObserverCallback();
mGlobalSearchSession.registerObserverCallback(
/*targetPackageName=*/ mContext.getPackageName(),
new ObserverSpec.Builder().addFilterSchemas("Type1").build(),
EXECUTOR,
observer);
// Update both types of the schema with migration to a new property name
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
.setVersion(2)
.addSchemas(
new AppSearchSchema.Builder("Type1")
.addProperty(
new AppSearchSchema.StringPropertyConfig
.Builder("strProp2")
.build())
.build(),
new AppSearchSchema.Builder("Type2")
.addProperty(
new AppSearchSchema.LongPropertyConfig
.Builder("longProp2")
.build())
.build())
.setMigrator(
"Type1",
new Migrator() {
@Override
public boolean shouldMigrate(
int currentVersion, int finalVersion) {
assertThat(currentVersion).isEqualTo(1);
assertThat(finalVersion).isEqualTo(2);
return true;
}
@NonNull
@Override
public GenericDocument onUpgrade(
int currentVersion,
int finalVersion,
@NonNull GenericDocument document) {
assertThat(currentVersion).isEqualTo(1);
assertThat(finalVersion).isEqualTo(2);
assertThat(document.getSchemaType())
.isEqualTo("Type1");
String[] prop =
document.getPropertyStringArray("strProp1");
assertThat(prop).isNotNull();
return new GenericDocument.Builder<
GenericDocument.Builder<?>>(
document.getNamespace(),
document.getId(),
document.getSchemaType())
.setPropertyString("strProp2", prop)
.build();
}
@NonNull
@Override
public GenericDocument onDowngrade(
int currentVersion,
int finalVersion,
@NonNull GenericDocument document) {
// Doesn't happen in this test
throw new UnsupportedOperationException();
}
})
.setMigrator(
"Type2",
new Migrator() {
@Override
public boolean shouldMigrate(
int currentVersion, int finalVersion) {
assertThat(currentVersion).isEqualTo(1);
assertThat(finalVersion).isEqualTo(2);
return true;
}
@NonNull
@Override
public GenericDocument onUpgrade(
int currentVersion,
int finalVersion,
@NonNull GenericDocument document) {
assertThat(currentVersion).isEqualTo(1);
assertThat(finalVersion).isEqualTo(2);
assertThat(document.getSchemaType())
.isEqualTo("Type2");
long[] prop =
document.getPropertyLongArray("longProp1");
assertThat(prop).isNotNull();
return new GenericDocument.Builder<
GenericDocument.Builder<?>>(
document.getNamespace(),
document.getId(),
document.getSchemaType())
.setPropertyLong(
"longProp2", prop[0] + 1000)
.build();
}
@NonNull
@Override
public GenericDocument onDowngrade(
int currentVersion,
int finalVersion,
@NonNull GenericDocument document) {
// Doesn't happen in this test
throw new UnsupportedOperationException();
}
})
.build())
.get();
// Make sure the test is valid by checking that migration actually occurred
AppSearchBatchResult<String, GenericDocument> getResponse =
mDb1.getByDocumentIdAsync(
new GetByDocumentIdRequest.Builder("namespace")
.addIds("t1id1", "t1id2", "t2id1", "t2id2")
.build())
.get();
assertThat(getResponse.isSuccess()).isTrue();
assertThat(getResponse.getSuccesses().get("t1id1").getPropertyString("strProp2"))
.isEqualTo("t1id1 prop value");
assertThat(getResponse.getSuccesses().get("t1id2").getPropertyString("strProp2"))
.isEqualTo("t1id2 prop value");
assertThat(getResponse.getSuccesses().get("t2id1").getPropertyLong("longProp2"))
.isEqualTo(1041);
assertThat(getResponse.getSuccesses().get("t2id2").getPropertyLong("longProp2"))
.isEqualTo(1042);
// Per the observer documentation, for schema migrations, individual document changes are
// not dispatched. Only SchemaChangeInfo is dispatched.
observer.waitForNotificationCount(1);
assertThat(observer.getSchemaChanges())
.containsExactly(
new SchemaChangeInfo(
mContext.getPackageName(), DB_NAME_1, ImmutableSet.of("Type1")));
assertThat(observer.getDocumentChanges()).isEmpty();
}
}