Merge "Pull upstream changes from AndroidX."

This commit is contained in:
Terry Wang
2020-11-14 06:55:53 +00:00
committed by Android (Google) Code Review
8 changed files with 519 additions and 423 deletions

View File

@@ -267,14 +267,14 @@ public class AppSearchManager {
searchResultsFuture.completeExceptionally(e);
}
// Translate the list of Bundle into a list of SearchResult
AppSearchResult<SearchResults> searchResultsResult = getFutureOrThrow(searchResultsFuture);
if (!searchResultsResult.isSuccess()) {
return AppSearchResult.newFailedResult(
searchResultsResult.getResultCode(), searchResultsResult.getErrorMessage());
// Translate the Bundle into a searchResultPage.
AppSearchResult<Bundle> bundleResult = getFutureOrThrow(searchResultsFuture);
if (!bundleResult.isSuccess()) {
return AppSearchResult.newFailedResult(bundleResult.getResultCode(),
bundleResult.getErrorMessage());
}
SearchResults searchResults = searchResultsResult.getResultValue();
return AppSearchResult.newSuccessfulResult(searchResults.mResults);
SearchResultPage searchResultPage = new SearchResultPage(bundleResult.getResultValue());
return AppSearchResult.newSuccessfulResult(searchResultPage.getResults());
}
/**
@@ -294,39 +294,7 @@ public class AppSearchManager {
List<String> uris = new ArrayList<>(request.getUris());
AndroidFuture<AppSearchBatchResult> future = new AndroidFuture<>();
try {
mService.delete(DEFAULT_DATABASE, request.getNamespace(), uris, future);
} catch (RemoteException e) {
future.completeExceptionally(e);
}
return getFutureOrThrow(future);
}
/**
* Deletes {@link android.app.appsearch.GenericDocument}s by schema type.
*
* <p>You should not call this method directly; instead, use the
* {@code AppSearch#deleteByType()} API provided by JetPack.
*
* @param schemaTypes Schema types whose documents to delete.
* @return An {@link AppSearchBatchResult} mapping each schema type to a {@code null} success if
* deletion was successful, to a {@code null} failure if the type did not exist, or to a
* {@code throwable} failure if deletion failed for another reason.
*/
public AppSearchBatchResult<String, Void> deleteByTypes(@NonNull List<String> schemaTypes) {
AndroidFuture<AppSearchBatchResult> future = new AndroidFuture<>();
try {
mService.deleteByTypes(DEFAULT_DATABASE, schemaTypes, future);
} catch (RemoteException e) {
future.completeExceptionally(e);
}
return getFutureOrThrow(future);
}
/** Deletes all documents owned by the calling app. */
public AppSearchResult<Void> deleteAll() {
AndroidFuture<AppSearchResult> future = new AndroidFuture<>();
try {
mService.deleteAll(DEFAULT_DATABASE, future);
mService.removeByUri(DEFAULT_DATABASE, request.getNamespace(), uris, future);
} catch (RemoteException e) {
future.completeExceptionally(e);
}

View File

@@ -92,7 +92,7 @@ interface IAppSearchManager {
in AndroidFuture<AppSearchResult> callback);
/**
* Deletes documents by URI.
* Removes documents by URI.
*
* @param databaseName The databaseName the document is in.
* @param namespace Namespace of the document to remove.
@@ -105,36 +105,23 @@ interface IAppSearchManager {
* where the keys are document URIs. If a document doesn't exist, it will be reported as a
* failure where the {@code throwable} is {@code null}.
*/
void delete(
void removeByUri(
in String databaseName,
in String namespace,
in List<String> uris,
in AndroidFuture<AppSearchBatchResult> callback);
/**
* Deletes documents by schema type.
* Removes documents by given query.
*
* @param databaseName The databaseName the document is in.
* @param schemaTypes The schema types of the documents to delete
* @param callback
* {@link AndroidFuture}&lt;{@link AppSearchBatchResult}&lt;{@link String}, {@link Void}&gt;&gt;.
* If the call fails to start, {@code callback} will be completed exceptionally. Otherwise,
* {@code callback} will be completed with an
* {@link AppSearchBatchResult}&lt;{@link String}, {@link Void}&gt;
* where the keys are schema types. If a schema type doesn't exist, it will be reported as a
* failure where the {@code throwable} is {@code null}.
* @param databaseName The databaseName this query for.
* @param queryExpression String to search for
* @param searchSpecBundle SearchSpec bundle
* @param callback {@link AndroidFuture}&lt;{@link AppSearchResult}&lt;{@link SearchResults}&gt;&gt;
*/
void deleteByTypes(
void removeByQuery(
in String databaseName,
in List<String> schemaTypes,
in AndroidFuture<AppSearchBatchResult> callback);
/**
* Deletes all documents belonging to the calling app.
*
* @param databaseName The databaseName to remove all documents from.
* @param callback {@link AndroidFuture}&lt;{@link AppSearchResult}&lt;{@link Void}&gt;&gt;.
* Will be completed with the result of the call.
*/
void deleteAll(in String databaseName, in AndroidFuture<AppSearchResult> callback);
in String queryExpression,
in Bundle searchSpecBundle,
in AndroidFuture<AppSearchResult> callback);
}

View File

@@ -0,0 +1,78 @@
/*
* 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;
import android.os.Bundle;
import android.annotation.NonNull;
import android.annotation.Nullable;
import com.android.internal.util.Preconditions;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* This class represents a page of {@link SearchResult}s
* @hide
*/
public class SearchResultPage {
public static final String RESULTS_FIELD = "results";
public static final String NEXT_PAGE_TOKEN_FIELD = "nextPageToken";
private final long mNextPageToken;
@Nullable
private List<SearchResult> mResults;
@NonNull
private final Bundle mBundle;
public SearchResultPage(@NonNull Bundle bundle) {
mBundle = Preconditions.checkNotNull(bundle);
mNextPageToken = mBundle.getLong(NEXT_PAGE_TOKEN_FIELD);
}
/** Returns the {@link Bundle} of this class. */
@NonNull
public Bundle getBundle() {
return mBundle;
}
/** Returns the Token to get next {@link SearchResultPage}. */
public long getNextPageToken() {
return mNextPageToken;
}
/** Returns all {@link android.app.appsearch.SearchResult}s of this page */
@NonNull
public List<SearchResult> getResults() {
if (mResults == null) {
ArrayList<Bundle> resultBundles = mBundle.getParcelableArrayList(RESULTS_FIELD);
if (resultBundles == null) {
mResults = Collections.emptyList();
} else {
mResults = new ArrayList<>(resultBundles.size());
for (int i = 0; i < resultBundles.size(); i++) {
mResults.add(new SearchResult(resultBundles.get(i)));
}
}
}
return mResults;
}
}

View File

@@ -21,33 +21,23 @@ import android.app.appsearch.AppSearchResult;
import android.app.appsearch.AppSearchSchema;
import android.app.appsearch.GenericDocument;
import android.app.appsearch.IAppSearchManager;
import android.app.appsearch.SearchResult;
import android.app.appsearch.SearchResults;
import android.app.appsearch.SearchResultPage;
import android.app.appsearch.SearchSpec;
import android.app.appsearch.exceptions.AppSearchException;
import android.content.Context;
import android.os.Binder;
import android.os.Bundle;
import android.os.UserHandle;
import android.util.ArraySet;
import com.android.internal.infra.AndroidFuture;
import com.android.internal.util.Preconditions;
import com.android.server.SystemService;
import com.android.server.appsearch.external.localstorage.AppSearchImpl;
import com.android.server.appsearch.external.localstorage.converter.GenericDocumentToProtoConverter;
import com.android.server.appsearch.external.localstorage.converter.SchemaToProtoConverter;
import com.android.server.appsearch.external.localstorage.converter.SearchResultToProtoConverter;
import com.android.server.appsearch.external.localstorage.converter.SearchSpecToProtoConverter;
import com.google.android.icing.proto.DocumentProto;
import com.google.android.icing.proto.SchemaProto;
import com.google.android.icing.proto.SchemaTypeConfigProto;
import com.google.android.icing.proto.SearchResultProto;
import com.google.android.icing.proto.SearchSpecProto;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* TODO(b/142567528): add comments when implement this class
@@ -72,21 +62,20 @@ public class AppSearchManagerService extends SystemService {
@NonNull List<Bundle> schemaBundles,
boolean forceOverride,
@NonNull AndroidFuture<AppSearchResult> callback) {
Preconditions.checkNotNull(databaseName);
Preconditions.checkNotNull(schemaBundles);
Preconditions.checkNotNull(callback);
int callingUid = Binder.getCallingUidOrThrow();
int callingUserId = UserHandle.getUserId(callingUid);
final long callingIdentity = Binder.clearCallingIdentity();
try {
SchemaProto.Builder schemaProtoBuilder = SchemaProto.newBuilder();
Set<AppSearchSchema> schemas = new ArraySet<>(schemaBundles.size());
for (int i = 0; i < schemaBundles.size(); i++) {
AppSearchSchema schema = new AppSearchSchema(schemaBundles.get(i));
SchemaTypeConfigProto schemaTypeProto = SchemaToProtoConverter.convert(schema);
schemaProtoBuilder.addTypes(schemaTypeProto);
schemas.add(new AppSearchSchema(schemaBundles.get(i)));
}
AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId);
databaseName = rewriteDatabaseNameWithUid(databaseName, callingUid);
impl.setSchema(databaseName, schemaProtoBuilder.build(), forceOverride);
impl.setSchema(databaseName, schemas, forceOverride);
callback.complete(AppSearchResult.newSuccessfulResult(/*result=*/ null));
} catch (Throwable t) {
callback.complete(throwableToFailedResult(t));
@@ -100,6 +89,7 @@ public class AppSearchManagerService extends SystemService {
@NonNull String databaseName,
@NonNull List<Bundle> documentBundles,
@NonNull AndroidFuture<AppSearchBatchResult> callback) {
Preconditions.checkNotNull(databaseName);
Preconditions.checkNotNull(documentBundles);
Preconditions.checkNotNull(callback);
int callingUid = Binder.getCallingUidOrThrow();
@@ -112,9 +102,8 @@ public class AppSearchManagerService extends SystemService {
new AppSearchBatchResult.Builder<>();
for (int i = 0; i < documentBundles.size(); i++) {
GenericDocument document = new GenericDocument(documentBundles.get(i));
DocumentProto documentProto = GenericDocumentToProtoConverter.convert(document);
try {
impl.putDocument(databaseName, documentProto);
impl.putDocument(databaseName, document);
resultBuilder.setSuccess(document.getUri(), /*result=*/ null);
} catch (Throwable t) {
resultBuilder.setResult(document.getUri(), throwableToFailedResult(t));
@@ -131,6 +120,8 @@ public class AppSearchManagerService extends SystemService {
@Override
public void getDocuments(@NonNull String databaseName, @NonNull String namespace,
@NonNull List<String> uris, @NonNull AndroidFuture<AppSearchBatchResult> callback) {
Preconditions.checkNotNull(databaseName);
Preconditions.checkNotNull(namespace);
Preconditions.checkNotNull(uris);
Preconditions.checkNotNull(callback);
int callingUid = Binder.getCallingUidOrThrow();
@@ -144,16 +135,8 @@ public class AppSearchManagerService extends SystemService {
for (int i = 0; i < uris.size(); i++) {
String uri = uris.get(i);
try {
DocumentProto documentProto = impl.getDocument(
databaseName, namespace, uri);
if (documentProto == null) {
resultBuilder.setFailure(
uri, AppSearchResult.RESULT_NOT_FOUND, /*errorMessage=*/ null);
} else {
GenericDocument genericDocument =
GenericDocumentToProtoConverter.convert(documentProto);
resultBuilder.setSuccess(uri, genericDocument.getBundle());
}
GenericDocument document = impl.getDocument(databaseName, namespace, uri);
resultBuilder.setSuccess(uri, document.getBundle());
} catch (Throwable t) {
resultBuilder.setResult(uri, throwableToFailedResult(t));
}
@@ -167,12 +150,14 @@ public class AppSearchManagerService extends SystemService {
}
// TODO(sidchhabra): Do this in a threadpool.
// TODO(b/162450968) handle pagination after getNextPage and SearchResults is ready.
@Override
public void query(
@NonNull String databaseName,
@NonNull String queryExpression,
@NonNull Bundle searchSpecBundle,
@NonNull AndroidFuture<AppSearchResult> callback) {
Preconditions.checkNotNull(databaseName);
Preconditions.checkNotNull(queryExpression);
Preconditions.checkNotNull(searchSpecBundle);
Preconditions.checkNotNull(callback);
@@ -180,29 +165,14 @@ public class AppSearchManagerService extends SystemService {
int callingUserId = UserHandle.getUserId(callingUid);
final long callingIdentity = Binder.clearCallingIdentity();
try {
SearchSpec searchSpec = new SearchSpec(searchSpecBundle);
SearchSpecProto searchSpecProto =
SearchSpecToProtoConverter.toSearchSpecProto(searchSpec);
searchSpecProto = searchSpecProto.toBuilder()
.setQuery(queryExpression).build();
AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId);
databaseName = rewriteDatabaseNameWithUid(databaseName, callingUid);
// TODO(adorokhine): handle pagination
SearchResultProto searchResultProto = impl.query(
SearchResultPage searchResultPage = impl.query(
databaseName,
searchSpecProto,
SearchSpecToProtoConverter.toResultSpecProto(searchSpec),
SearchSpecToProtoConverter.toScoringSpecProto(searchSpec));
List<SearchResult> searchResultList =
new ArrayList<>(searchResultProto.getResultsCount());
for (int i = 0; i < searchResultProto.getResultsCount(); i++) {
SearchResult result = SearchResultToProtoConverter.convertSearchResult(
searchResultProto.getResults(i));
searchResultList.add(result);
}
SearchResults searchResults =
new SearchResults(searchResultList, searchResultProto.getNextPageToken());
callback.complete(AppSearchResult.newSuccessfulResult(searchResults));
queryExpression,
new SearchSpec(searchSpecBundle));
callback.complete(
AppSearchResult.newSuccessfulResult(searchResultPage.getBundle()));
} catch (Throwable t) {
callback.complete(throwableToFailedResult(t));
} finally {
@@ -211,8 +181,10 @@ public class AppSearchManagerService extends SystemService {
}
@Override
public void delete(@NonNull String databaseName, @NonNull String namespace,
public void removeByUri(@NonNull String databaseName, @NonNull String namespace,
List<String> uris, AndroidFuture<AppSearchBatchResult> callback) {
Preconditions.checkNotNull(databaseName);
Preconditions.checkNotNull(namespace);
Preconditions.checkNotNull(uris);
Preconditions.checkNotNull(callback);
int callingUid = Binder.getCallingUidOrThrow();
@@ -241,38 +213,14 @@ public class AppSearchManagerService extends SystemService {
}
@Override
public void deleteByTypes(@NonNull String databaseName,
List<String> schemaTypes, AndroidFuture<AppSearchBatchResult> callback) {
Preconditions.checkNotNull(schemaTypes);
Preconditions.checkNotNull(callback);
int callingUid = Binder.getCallingUidOrThrow();
int callingUserId = UserHandle.getUserId(callingUid);
final long callingIdentity = Binder.clearCallingIdentity();
try {
AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId);
databaseName = rewriteDatabaseNameWithUid(databaseName, callingUid);
AppSearchBatchResult.Builder<String, Void> resultBuilder =
new AppSearchBatchResult.Builder<>();
for (int i = 0; i < schemaTypes.size(); i++) {
String schemaType = schemaTypes.get(i);
try {
impl.removeByType(databaseName, schemaType);
resultBuilder.setSuccess(schemaType, /*result=*/ null);
} catch (Throwable t) {
resultBuilder.setResult(schemaType, throwableToFailedResult(t));
}
}
callback.complete(resultBuilder.build());
} catch (Throwable t) {
callback.completeExceptionally(t);
} finally {
Binder.restoreCallingIdentity(callingIdentity);
}
}
@Override
public void deleteAll(@NonNull String databaseName,
public void removeByQuery(
@NonNull String databaseName,
@NonNull String queryExpression,
@NonNull Bundle searchSpecBundle,
@NonNull AndroidFuture<AppSearchResult> callback) {
Preconditions.checkNotNull(databaseName);
Preconditions.checkNotNull(queryExpression);
Preconditions.checkNotNull(searchSpecBundle);
Preconditions.checkNotNull(callback);
int callingUid = Binder.getCallingUidOrThrow();
int callingUserId = UserHandle.getUserId(callingUid);
@@ -280,8 +228,8 @@ public class AppSearchManagerService extends SystemService {
try {
AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId);
databaseName = rewriteDatabaseNameWithUid(databaseName, callingUid);
impl.removeAll(databaseName);
callback.complete(AppSearchResult.newSuccessfulResult(null));
impl.removeByQuery(databaseName, queryExpression, new SearchSpec(searchSpecBundle));
callback.complete(AppSearchResult.newSuccessfulResult(/*result= */null));
} catch (Throwable t) {
callback.complete(throwableToFailedResult(t));
} finally {

View File

@@ -16,21 +16,27 @@
package com.android.server.appsearch.external.localstorage;
import android.os.Bundle;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import android.annotation.NonNull;
import android.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import android.annotation.WorkerThread;
import android.app.appsearch.AppSearchResult;
import android.app.appsearch.AppSearchSchema;
import android.app.appsearch.GenericDocument;
import android.app.appsearch.SearchResultPage;
import android.app.appsearch.SearchSpec;
import android.app.appsearch.exceptions.AppSearchException;
import com.android.internal.util.Preconditions;
import com.android.server.appsearch.external.localstorage.converter.GenericDocumentToProtoConverter;
import com.android.server.appsearch.external.localstorage.converter.SchemaToProtoConverter;
import com.android.server.appsearch.external.localstorage.converter.SearchResultToProtoConverter;
import com.android.server.appsearch.external.localstorage.converter.SearchSpecToProtoConverter;
import com.google.android.icing.IcingSearchEngine;
import com.google.android.icing.proto.DeleteByNamespaceResultProto;
import com.google.android.icing.proto.DeleteBySchemaTypeResultProto;
import com.google.android.icing.proto.DeleteResultProto;
import com.google.android.icing.proto.DocumentProto;
import com.google.android.icing.proto.GetAllNamespacesResultProto;
@@ -54,8 +60,10 @@ import com.google.android.icing.proto.SetSchemaResultProto;
import com.google.android.icing.proto.StatusProto;
import java.io.File;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.ReadWriteLock;
@@ -89,12 +97,14 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
* </ul>
*
* <p>This class is thread safe.
*
* @hide
*/
@WorkerThread
public final class AppSearchImpl {
private static final String TAG = "AppSearchImpl";
private static final char DATABASE_DELIMITER = '/';
@VisibleForTesting
static final int OPTIMIZE_THRESHOLD_DOC_COUNT = 1000;
@@ -173,20 +183,27 @@ public final class AppSearchImpl {
* <p>This method belongs to mutate group.
*
* @param databaseName The name of the database where this schema lives.
* @param origSchema The schema to set for this app.
* @param schemas Schemas to set for this app.
* @param forceOverride Whether to force-apply the schema even if it is incompatible. Documents
* which do not comply with the new schema will be deleted.
* @throws AppSearchException on IcingSearchEngine error.
*/
public void setSchema(@NonNull String databaseName, @NonNull SchemaProto origSchema,
public void setSchema(@NonNull String databaseName, @NonNull Set<AppSearchSchema> schemas,
boolean forceOverride) throws AppSearchException {
SchemaProto schemaProto = getSchemaProto();
SchemaProto.Builder existingSchemaBuilder = schemaProto.toBuilder();
SchemaProto.Builder newSchemaBuilder = SchemaProto.newBuilder();
for (AppSearchSchema schema : schemas) {
SchemaTypeConfigProto schemaTypeProto = SchemaToProtoConverter.convert(schema);
newSchemaBuilder.addTypes(schemaTypeProto);
}
// Combine the existing schema (which may have types from other databases) with this
// database's new schema. Modifies the existingSchemaBuilder.
Set<String> newTypeNames = rewriteSchema(databaseName, existingSchemaBuilder, origSchema);
Set<String> newTypeNames = rewriteSchema(databaseName, existingSchemaBuilder,
newSchemaBuilder.build());
SetSchemaResultProto setSchemaResultProto;
mReadWriteLock.writeLock().lock();
@@ -220,7 +237,7 @@ public final class AppSearchImpl {
if (setSchemaResultProto.getDeletedSchemaTypesCount() > 0
|| (setSchemaResultProto.getIncompatibleSchemaTypesCount() > 0
&& forceOverride)) {
// Any existing schemas which is not in origSchema will be deleted, and all
// Any existing schemas which is not in 'schemas' will be deleted, and all
// documents of these types were also deleted. And so well if we force override
// incompatible schemas.
checkForOptimize(/* force= */true);
@@ -239,10 +256,11 @@ public final class AppSearchImpl {
* @param document The document to index.
* @throws AppSearchException on IcingSearchEngine error.
*/
public void putDocument(@NonNull String databaseName, @NonNull DocumentProto document)
public void putDocument(@NonNull String databaseName, @NonNull GenericDocument document)
throws AppSearchException {
DocumentProto.Builder documentBuilder = document.toBuilder();
rewriteDocumentTypes(getDatabasePrefix(databaseName), documentBuilder, /*add=*/ true);
DocumentProto.Builder documentBuilder = GenericDocumentToProtoConverter.convert(
document).toBuilder();
addPrefixToDocument(documentBuilder, getDatabasePrefix(databaseName));
PutResultProto putResultProto;
mReadWriteLock.writeLock().lock();
@@ -266,11 +284,11 @@ public final class AppSearchImpl {
* @param databaseName The databaseName this document resides in.
* @param namespace The namespace this document resides in.
* @param uri The URI of the document to get.
* @return The Document contents, or {@code null} if no such URI exists in the system.
* @return The Document contents
* @throws AppSearchException on IcingSearchEngine error.
*/
@Nullable
public DocumentProto getDocument(@NonNull String databaseName, @NonNull String namespace,
@NonNull
public GenericDocument getDocument(@NonNull String databaseName, @NonNull String namespace,
@NonNull String uri) throws AppSearchException {
GetResultProto getResultProto;
mReadWriteLock.readLock().lock();
@@ -283,8 +301,8 @@ public final class AppSearchImpl {
checkSuccess(getResultProto.getStatus());
DocumentProto.Builder documentBuilder = getResultProto.getDocument().toBuilder();
rewriteDocumentTypes(getDatabasePrefix(databaseName), documentBuilder, /*add=*/ false);
return documentBuilder.build();
removeDatabasesFromDocument(documentBuilder);
return GenericDocumentToProtoConverter.convert(documentBuilder.build());
}
/**
@@ -292,33 +310,60 @@ public final class AppSearchImpl {
*
* <p>This method belongs to query group.
*
* @param databaseName The databaseName this query for.
* @param searchSpec Defines what and how to search
* @param resultSpec Defines what results to show
* @param scoringSpec Defines how to order results
* @return The results of performing this search The proto might have no {@code results} if no
* documents matched the query.
* @param databaseName The databaseName this query for.
* @param queryExpression Query String to search.
* @param searchSpec Spec for setting filters, raw query etc.
* @return The results of performing this search. It may contain an empty list of results if
* no documents matched the query.
* @throws AppSearchException on IcingSearchEngine error.
*/
@NonNull
public SearchResultProto query(
public SearchResultPage query(
@NonNull String databaseName,
@NonNull SearchSpecProto searchSpec,
@NonNull ResultSpecProto resultSpec,
@NonNull ScoringSpecProto scoringSpec) throws AppSearchException {
SearchSpecProto.Builder searchSpecBuilder = searchSpec.toBuilder();
@NonNull String queryExpression,
@NonNull SearchSpec searchSpec) throws AppSearchException {
return doQuery(Collections.singleton(databaseName), queryExpression, searchSpec);
}
/**
* Executes a global query, i.e. over all permitted databases, against the AppSearch index and
* returns results.
*
* <p>This method belongs to query group.
*
* @param queryExpression Query String to search.
* @param searchSpec Spec for setting filters, raw query etc.
* @return The results of performing this search. It may contain an empty list of results if
* no documents matched the query.
* @throws AppSearchException on IcingSearchEngine error.
*/
@NonNull
public SearchResultPage globalQuery(
@NonNull String queryExpression,
@NonNull SearchSpec searchSpec) throws AppSearchException {
return doQuery(mNamespaceMap.keySet(), queryExpression, searchSpec);
}
private SearchResultPage doQuery(
@NonNull Set<String> databases, @NonNull String queryExpression,
@NonNull SearchSpec searchSpec)
throws AppSearchException {
SearchSpecProto searchSpecProto =
SearchSpecToProtoConverter.toSearchSpecProto(searchSpec);
SearchSpecProto.Builder searchSpecBuilder = searchSpecProto.toBuilder()
.setQuery(queryExpression);
ResultSpecProto resultSpec = SearchSpecToProtoConverter.toResultSpecProto(searchSpec);
ScoringSpecProto scoringSpec = SearchSpecToProtoConverter.toScoringSpecProto(searchSpec);
SearchResultProto searchResultProto;
mReadWriteLock.readLock().lock();
try {
// Only rewrite SearchSpec for non empty database.
// rewriteSearchSpecForNonEmptyDatabase will return false for empty database, we
// should just return an empty SearchResult and skip sending request to Icing.
if (!rewriteSearchSpecForNonEmptyDatabase(databaseName, searchSpecBuilder)) {
return SearchResultProto.newBuilder()
.setStatus(StatusProto.newBuilder()
.setCode(StatusProto.Code.OK)
.build())
.build();
// rewriteSearchSpecForDatabases will return false if none of the databases have
// documents, so we can return an empty SearchResult and skip sending request to Icing.
// We use the mNamespaceMap.keySet here because it's the smaller set of valid databases
// that could exist.
if (!rewriteSearchSpecForDatabases(searchSpecBuilder, databases)) {
return new SearchResultPage(Bundle.EMPTY);
}
searchResultProto = mIcingSearchEngine.search(
searchSpecBuilder.build(), scoringSpec, resultSpec);
@@ -326,34 +371,32 @@ public final class AppSearchImpl {
mReadWriteLock.readLock().unlock();
}
checkSuccess(searchResultProto.getStatus());
if (searchResultProto.getResultsCount() == 0) {
return searchResultProto;
}
return rewriteSearchResultProto(databaseName, searchResultProto);
return rewriteSearchResultProto(searchResultProto);
}
/**
* Fetches the next page of results of a previously executed query. Results can be empty if
* next-page token is invalid or all pages have been returned.
*
* @param databaseName The databaseName of the previously executed query.
* <p>This method belongs to query group.
*
* @param nextPageToken The token of pre-loaded results of previously executed query.
* @return The next page of results of previously executed query.
* @throws AppSearchException on IcingSearchEngine error.
*/
@NonNull
public SearchResultProto getNextPage(@NonNull String databaseName, long nextPageToken)
public SearchResultPage getNextPage(long nextPageToken)
throws AppSearchException {
SearchResultProto searchResultProto = mIcingSearchEngine.getNextPage(nextPageToken);
checkSuccess(searchResultProto.getStatus());
if (searchResultProto.getResultsCount() == 0) {
return searchResultProto;
}
return rewriteSearchResultProto(databaseName, searchResultProto);
return rewriteSearchResultProto(searchResultProto);
}
/**
* Invalidates the next-page token so that no more results of the related query can be returned.
*
* <p>This method belongs to query group.
*
* @param nextPageToken The token of pre-loaded results of previously executed query to be
* Invalidated.
*/
@@ -386,93 +429,43 @@ public final class AppSearchImpl {
}
/**
* Removes all documents having the given {@code schemaType} in given database.
* Removes documents by given query.
*
* <p>This method belongs to mutate group.
*
* @param databaseName The databaseName that contains documents of schemaType.
* @param schemaType The schemaType of documents to remove.
* @param databaseName The databaseName the document is in.
* @param queryExpression Query String to search.
* @param searchSpec Defines what and how to remove
* @throws AppSearchException on IcingSearchEngine error.
*/
public void removeByType(@NonNull String databaseName, @NonNull String schemaType)
public void removeByQuery(@NonNull String databaseName, @NonNull String queryExpression,
@NonNull SearchSpec searchSpec)
throws AppSearchException {
String qualifiedType = getDatabasePrefix(databaseName) + schemaType;
DeleteBySchemaTypeResultProto deleteBySchemaTypeResultProto;
mReadWriteLock.writeLock().lock();
try {
Set<String> existingSchemaTypes = mSchemaMap.get(databaseName);
if (existingSchemaTypes == null || !existingSchemaTypes.contains(qualifiedType)) {
return;
}
deleteBySchemaTypeResultProto = mIcingSearchEngine.deleteBySchemaType(qualifiedType);
checkForOptimize(/* force= */true);
} finally {
mReadWriteLock.writeLock().unlock();
}
checkSuccess(deleteBySchemaTypeResultProto.getStatus());
}
/**
* Removes all documents having the given {@code namespace} in given database.
*
* <p>This method belongs to mutate group.
*
* @param databaseName The databaseName that contains documents of namespace.
* @param namespace The namespace of documents to remove.
* @throws AppSearchException on IcingSearchEngine error.
*/
public void removeByNamespace(@NonNull String databaseName, @NonNull String namespace)
throws AppSearchException {
String qualifiedNamespace = getDatabasePrefix(databaseName) + namespace;
DeleteByNamespaceResultProto deleteByNamespaceResultProto;
SearchSpecProto searchSpecProto =
SearchSpecToProtoConverter.toSearchSpecProto(searchSpec);
SearchSpecProto.Builder searchSpecBuilder = searchSpecProto.toBuilder()
.setQuery(queryExpression);
DeleteResultProto deleteResultProto;
mReadWriteLock.writeLock().lock();
try {
Set<String> existingNamespaces = mNamespaceMap.get(databaseName);
if (existingNamespaces == null || !existingNamespaces.contains(qualifiedNamespace)) {
// Only rewrite SearchSpec for non empty database.
// rewriteSearchSpecForNonEmptyDatabase will return false for empty database, we
// should skip sending request to Icing and return in here.
if (!rewriteSearchSpecForDatabases(searchSpecBuilder,
Collections.singleton(databaseName))) {
return;
}
deleteByNamespaceResultProto = mIcingSearchEngine.deleteByNamespace(qualifiedNamespace);
checkForOptimize(/* force= */true);
} finally {
mReadWriteLock.writeLock().unlock();
}
checkSuccess(deleteByNamespaceResultProto.getStatus());
}
/**
* Clears the given database by removing all documents and types.
*
* <p>The schemas will remain. To clear everything including schemas, please call
* {@link #setSchema} with an empty schema and {@code forceOverride} set to true.
*
* <p>This method belongs to mutate group.
*
* @param databaseName The databaseName to remove all documents from.
* @throws AppSearchException on IcingSearchEngine error.
*/
public void removeAll(@NonNull String databaseName)
throws AppSearchException {
mReadWriteLock.writeLock().lock();
try {
Set<String> existingNamespaces = mNamespaceMap.get(databaseName);
if (existingNamespaces == null) {
return;
}
for (String namespace : existingNamespaces) {
DeleteByNamespaceResultProto deleteByNamespaceResultProto =
mIcingSearchEngine.deleteByNamespace(namespace);
// There's no way for AppSearch to know that all documents in a particular
// namespace have been deleted, but if you try to delete an empty namespace, Icing
// returns NOT_FOUND. Just ignore that code.
checkCodeOneOf(
deleteByNamespaceResultProto.getStatus(),
StatusProto.Code.OK, StatusProto.Code.NOT_FOUND);
}
mNamespaceMap.remove(databaseName);
deleteResultProto = mIcingSearchEngine.deleteByQuery(
searchSpecBuilder.build());
checkForOptimize(/* force= */true);
} finally {
mReadWriteLock.writeLock().unlock();
}
// It seems that the caller wants to get success if the data matching the query is not in
// the DB because it was not there or was successfully deleted.
checkCodeOneOf(deleteResultProto.getStatus(),
StatusProto.Code.OK, StatusProto.Code.NOT_FOUND);
}
/**
@@ -565,36 +558,63 @@ public final class AppSearchImpl {
}
/**
* Rewrites all types and namespaces mentioned anywhere in {@code documentBuilder} to prepend
* or remove {@code prefix}.
* Prepends {@code prefix} to all types and namespaces mentioned anywhere in
* {@code documentBuilder}.
*
* @param prefix The prefix to add or remove
* @param documentBuilder The document to mutate
* @param add Whether to add prefix to the types and namespaces. If {@code false},
* prefix will be removed.
* @throws IllegalStateException If {@code add=false} and the document has a type or namespace
* that doesn't start with {@code prefix}.
* @param prefix The prefix to add
*/
@VisibleForTesting
void rewriteDocumentTypes(
@NonNull String prefix,
void addPrefixToDocument(
@NonNull DocumentProto.Builder documentBuilder,
boolean add) {
@NonNull String prefix) {
// Rewrite the type name to include/remove the prefix.
String newSchema;
if (add) {
newSchema = prefix + documentBuilder.getSchema();
} else {
newSchema = removePrefix(prefix, "schemaType", documentBuilder.getSchema());
}
String newSchema = prefix + documentBuilder.getSchema();
documentBuilder.setSchema(newSchema);
// Rewrite the namespace to include/remove the prefix.
if (add) {
documentBuilder.setNamespace(prefix + documentBuilder.getNamespace());
} else {
documentBuilder.setNamespace(
removePrefix(prefix, "namespace", documentBuilder.getNamespace()));
documentBuilder.setNamespace(prefix + documentBuilder.getNamespace());
// Recurse into derived documents
for (int propertyIdx = 0;
propertyIdx < documentBuilder.getPropertiesCount();
propertyIdx++) {
int documentCount = documentBuilder.getProperties(propertyIdx).getDocumentValuesCount();
if (documentCount > 0) {
PropertyProto.Builder propertyBuilder =
documentBuilder.getProperties(propertyIdx).toBuilder();
for (int documentIdx = 0; documentIdx < documentCount; documentIdx++) {
DocumentProto.Builder derivedDocumentBuilder =
propertyBuilder.getDocumentValues(documentIdx).toBuilder();
addPrefixToDocument(derivedDocumentBuilder, prefix);
propertyBuilder.setDocumentValues(documentIdx, derivedDocumentBuilder);
}
documentBuilder.setProperties(propertyIdx, propertyBuilder);
}
}
}
/**
* Removes any database names from types and namespaces mentioned anywhere in
* {@code documentBuilder}.
*
* @param documentBuilder The document to mutate
*/
@VisibleForTesting
void removeDatabasesFromDocument(@NonNull DocumentProto.Builder documentBuilder) {
int delimiterIndex;
if ((delimiterIndex = documentBuilder.getSchema().indexOf(DATABASE_DELIMITER)) != -1) {
// Rewrite the type name to remove the prefix.
// Add 1 to include the char size of the DATABASE_DELIMITER
String newSchema = documentBuilder.getSchema().substring(delimiterIndex + 1);
documentBuilder.setSchema(newSchema);
}
if ((delimiterIndex = documentBuilder.getNamespace().indexOf(DATABASE_DELIMITER)) != -1) {
// Rewrite the namespace to remove the prefix.
// Add 1 to include the char size of the DATABASE_DELIMITER
String newNamespace = documentBuilder.getNamespace().substring(delimiterIndex + 1);
documentBuilder.setNamespace(newNamespace);
}
// Recurse into derived documents
@@ -608,7 +628,7 @@ public final class AppSearchImpl {
for (int documentIdx = 0; documentIdx < documentCount; documentIdx++) {
DocumentProto.Builder derivedDocumentBuilder =
propertyBuilder.getDocumentValues(documentIdx).toBuilder();
rewriteDocumentTypes(prefix, derivedDocumentBuilder, add);
removeDatabasesFromDocument(derivedDocumentBuilder);
propertyBuilder.setDocumentValues(documentIdx, derivedDocumentBuilder);
}
documentBuilder.setProperties(propertyIdx, propertyBuilder);
@@ -617,51 +637,68 @@ public final class AppSearchImpl {
}
/**
* Rewrites searchSpec by adding schemaTypeFilter and namespacesFilter
* Rewrites the schemaTypeFilters and namespacesFilters that exist in {@code databaseNames}.
*
* <p>If user input empty filter lists, will look up {@link #mSchemaMap} and
* {@link #mNamespaceMap} and put all values belong to current database to narrow down Icing
* search area.
* <p>If the searchSpec has empty filter lists, all existing databases from
* {@code databaseNames} will be added.
* <p>This method should be only called in query methods and get the READ lock to keep thread
* safety.
* @return false if the current database is brand new and contains nothing. We should just
* return an empty query result to user.
*
* @return false if none of the requested databases exist.
*/
@VisibleForTesting
@GuardedBy("mReadWriteLock")
boolean rewriteSearchSpecForNonEmptyDatabase(@NonNull String databaseName,
@NonNull SearchSpecProto.Builder searchSpecBuilder) {
Set<String> existingSchemaTypes = mSchemaMap.get(databaseName);
Set<String> existingNamespaces = mNamespaceMap.get(databaseName);
if (existingSchemaTypes == null || existingSchemaTypes.isEmpty()
|| existingNamespaces == null || existingNamespaces.isEmpty()) {
boolean rewriteSearchSpecForDatabases(
@NonNull SearchSpecProto.Builder searchSpecBuilder,
@NonNull Set<String> databaseNames) {
// Create a copy since retainAll() modifies the original set.
Set<String> existingDatabases = new HashSet<>(mNamespaceMap.keySet());
existingDatabases.retainAll(databaseNames);
if (existingDatabases.isEmpty()) {
// None of the databases exist, empty query.
return false;
}
// Rewrite any existing schema types specified in the searchSpec, or add schema types to
// limit the search to this database instance.
if (searchSpecBuilder.getSchemaTypeFiltersCount() > 0) {
for (int i = 0; i < searchSpecBuilder.getSchemaTypeFiltersCount(); i++) {
String qualifiedType = getDatabasePrefix(databaseName)
+ searchSpecBuilder.getSchemaTypeFilters(i);
if (existingSchemaTypes.contains(qualifiedType)) {
searchSpecBuilder.setSchemaTypeFilters(i, qualifiedType);
// Cache the schema type filters and namespaces before clearing everything.
List<String> schemaTypeFilters = searchSpecBuilder.getSchemaTypeFiltersList();
searchSpecBuilder.clearSchemaTypeFilters();
List<String> namespaceFilters = searchSpecBuilder.getNamespaceFiltersList();
searchSpecBuilder.clearNamespaceFilters();
// Rewrite filters to include a database prefix.
for (String databaseName : existingDatabases) {
Set<String> existingSchemaTypes = mSchemaMap.get(databaseName);
if (schemaTypeFilters.isEmpty()) {
// Include all schema types
searchSpecBuilder.addAllSchemaTypeFilters(existingSchemaTypes);
} else {
// Qualify the given schema types
for (String schemaType : schemaTypeFilters) {
String qualifiedType = getDatabasePrefix(databaseName) + schemaType;
if (existingSchemaTypes.contains(qualifiedType)) {
searchSpecBuilder.addSchemaTypeFilters(qualifiedType);
}
}
}
Set<String> existingNamespaces = mNamespaceMap.get(databaseName);
if (namespaceFilters.isEmpty()) {
// Include all namespaces
searchSpecBuilder.addAllNamespaceFilters(existingNamespaces);
} else {
// Qualify the given namespaces.
for (String namespace : namespaceFilters) {
String qualifiedNamespace = getDatabasePrefix(databaseName) + namespace;
if (existingNamespaces.contains(qualifiedNamespace)) {
searchSpecBuilder.addNamespaceFilters(qualifiedNamespace);
}
}
}
} else {
searchSpecBuilder.addAllSchemaTypeFilters(existingSchemaTypes);
}
// Rewrite any existing namespaces specified in the searchSpec, or add namespaces to
// limit the search to this database instance.
if (searchSpecBuilder.getNamespaceFiltersCount() > 0) {
for (int i = 0; i < searchSpecBuilder.getNamespaceFiltersCount(); i++) {
String qualifiedNamespace = getDatabasePrefix(databaseName)
+ searchSpecBuilder.getNamespaceFilters(i);
searchSpecBuilder.setNamespaceFilters(i, qualifiedNamespace);
}
} else {
searchSpecBuilder.addAllNamespaceFilters(existingNamespaces);
}
return true;
}
@@ -676,12 +713,13 @@ public final class AppSearchImpl {
@NonNull
private String getDatabasePrefix(@NonNull String databaseName) {
return databaseName + "/";
// TODO(b/170370381): Reconsider the way we separate database names for security reasons.
return databaseName + DATABASE_DELIMITER;
}
@NonNull
private String getDatabaseName(@NonNull String prefixedValue) throws AppSearchException {
int delimiterIndex = prefixedValue.indexOf('/');
int delimiterIndex = prefixedValue.indexOf(DATABASE_DELIMITER);
if (delimiterIndex == -1) {
throw new AppSearchException(AppSearchResult.RESULT_UNKNOWN_ERROR,
"The databaseName prefixed value doesn't contains a valid database name.");
@@ -689,17 +727,6 @@ public final class AppSearchImpl {
return prefixedValue.substring(0, delimiterIndex);
}
@NonNull
private static String removePrefix(@NonNull String prefix, @NonNull String inputType,
@NonNull String input) {
if (!input.startsWith(prefix)) {
throw new IllegalStateException(
"Unexpected " + inputType + " \"" + input
+ "\" does not start with \"" + prefix + "\"");
}
return input.substring(prefix.length());
}
@GuardedBy("mReadWriteLock")
private void addToMap(Map<String, Set<String>> map, String databaseName, String prefixedValue) {
Set<String> values = map.get(databaseName);
@@ -733,7 +760,7 @@ public final class AppSearchImpl {
}
if (statusProto.getCode() == StatusProto.Code.WARNING_DATA_LOSS) {
// TODO: May want to propagate WARNING_DATA_LOSS up to AppSearchManager so they can
// TODO: May want to propagate WARNING_DATA_LOSS up to AppSearchSession so they can
// choose to log the error or potentially pass it on to clients.
Log.w(TAG, "Encountered WARNING_DATA_LOSS: " + statusProto.getMessage());
return;
@@ -776,22 +803,21 @@ public final class AppSearchImpl {
}
}
/** Remove the rewritten schema types from any result documents.*/
private SearchResultProto rewriteSearchResultProto(@NonNull String databaseName,
/** Remove the rewritten schema types from any result documents. */
private SearchResultPage rewriteSearchResultProto(
@NonNull SearchResultProto searchResultProto) {
SearchResultProto.Builder searchResultsBuilder = searchResultProto.toBuilder();
for (int i = 0; i < searchResultsBuilder.getResultsCount(); i++) {
SearchResultProto.Builder resultsBuilder = searchResultProto.toBuilder();
for (int i = 0; i < searchResultProto.getResultsCount(); i++) {
if (searchResultProto.getResults(i).hasDocument()) {
SearchResultProto.ResultProto.Builder resultBuilder =
searchResultsBuilder.getResults(i).toBuilder();
searchResultProto.getResults(i).toBuilder();
DocumentProto.Builder documentBuilder = resultBuilder.getDocument().toBuilder();
rewriteDocumentTypes(
getDatabasePrefix(databaseName), documentBuilder, /*add=*/false);
removeDatabasesFromDocument(documentBuilder);
resultBuilder.setDocument(documentBuilder);
searchResultsBuilder.setResults(i, resultBuilder);
resultsBuilder.setResults(i, resultBuilder);
}
}
return searchResultsBuilder.build();
return SearchResultToProtoConverter.convertToSearchResultPage(resultsBuilder);
}
@VisibleForTesting

View File

@@ -22,8 +22,10 @@ import android.annotation.NonNull;
import android.app.appsearch.GenericDocument;
import android.app.appsearch.SearchResult;
import android.app.appsearch.SearchResultPage;
import com.google.android.icing.proto.SearchResultProto;
import com.google.android.icing.proto.SearchResultProtoOrBuilder;
import com.google.android.icing.proto.SnippetMatchProto;
import com.google.android.icing.proto.SnippetProto;
@@ -38,9 +40,24 @@ import java.util.ArrayList;
public class SearchResultToProtoConverter {
private SearchResultToProtoConverter() {}
/** Translate a {@link SearchResultProto} into {@link SearchResultPage}. */
@NonNull
public static SearchResultPage convertToSearchResultPage(
@NonNull SearchResultProtoOrBuilder proto) {
Bundle bundle = new Bundle();
bundle.putLong(SearchResultPage.NEXT_PAGE_TOKEN_FIELD, proto.getNextPageToken());
ArrayList<Bundle> resultBundles = new ArrayList<>(proto.getResultsCount());
for (int i = 0; i < proto.getResultsCount(); i++) {
resultBundles.add(convertToSearchResultBundle(proto.getResults(i)));
}
bundle.putParcelableArrayList(SearchResultPage.RESULTS_FIELD, resultBundles);
return new SearchResultPage(bundle);
}
/** Translate a {@link SearchResultProto.ResultProto} into {@link SearchResult}. */
@NonNull
public static SearchResult convertSearchResult(
private static Bundle convertToSearchResultBundle(
@NonNull SearchResultProto.ResultProtoOrBuilder proto) {
Bundle bundle = new Bundle();
GenericDocument document = GenericDocumentToProtoConverter.convert(proto.getDocument());
@@ -59,7 +76,7 @@ public class SearchResultToProtoConverter {
}
bundle.putParcelableArrayList(SearchResult.MATCHES_FIELD, matchList);
return new SearchResult(bundle);
return bundle;
}
private static Bundle convertToMatchInfoBundle(

View File

@@ -20,6 +20,10 @@ import static com.google.common.truth.Truth.assertThat;
import static org.testng.Assert.expectThrows;
import android.app.appsearch.AppSearchSchema;
import android.app.appsearch.GenericDocument;
import android.app.appsearch.SearchResultPage;
import android.app.appsearch.SearchSpec;
import android.app.appsearch.exceptions.AppSearchException;
import com.android.server.appsearch.proto.DocumentProto;
@@ -27,20 +31,19 @@ import com.android.server.appsearch.proto.GetOptimizeInfoResultProto;
import com.android.server.appsearch.proto.IndexingConfig;
import com.android.server.appsearch.proto.PropertyConfigProto;
import com.android.server.appsearch.proto.PropertyProto;
import com.android.server.appsearch.proto.ResultSpecProto;
import com.android.server.appsearch.proto.SchemaProto;
import com.android.server.appsearch.proto.SchemaTypeConfigProto;
import com.android.server.appsearch.proto.ScoringSpecProto;
import com.android.server.appsearch.proto.SearchResultProto;
import com.android.server.appsearch.proto.SearchSpecProto;
import com.android.server.appsearch.proto.StatusProto;
import com.android.server.appsearch.proto.TermMatchType;
import com.google.common.collect.ImmutableSet;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public class AppSearchImplTest {
@@ -93,7 +96,7 @@ public class AppSearchImplTest {
SchemaProto expectedSchema = SchemaProto.newBuilder()
.addTypes(SchemaTypeConfigProto.newBuilder()
.setSchemaType("databaseName/Foo").build())
.setSchemaType("databaseName/Foo").build())
.addTypes(SchemaTypeConfigProto.newBuilder()
.setSchemaType("databaseName/TestType")
.addProperties(PropertyConfigProto.newBuilder()
@@ -120,7 +123,7 @@ public class AppSearchImplTest {
}
@Test
public void testRewriteDocumentProto() {
public void testAddDocumentTypePrefix() {
DocumentProto insideDocument = DocumentProto.newBuilder()
.setUri("inside-uri")
.setSchema("type")
@@ -146,30 +149,56 @@ public class AppSearchImplTest {
.build();
DocumentProto.Builder actualDocument = documentProto.toBuilder();
mAppSearchImpl.rewriteDocumentTypes("databaseName/", actualDocument, /*add=*/true);
mAppSearchImpl.addPrefixToDocument(actualDocument, "databaseName/");
assertThat(actualDocument.build()).isEqualTo(expectedDocumentProto);
}
@Test
public void testRemoveDocumentTypePrefixes() {
DocumentProto insideDocument = DocumentProto.newBuilder()
.setUri("inside-uri")
.setSchema("databaseName1/type")
.setNamespace("databaseName2/namespace")
.build();
DocumentProto documentProto = DocumentProto.newBuilder()
.setUri("uri")
.setSchema("databaseName2/type")
.setNamespace("databaseName3/namespace")
.addProperties(PropertyProto.newBuilder().addDocumentValues(insideDocument))
.build();
DocumentProto expectedInsideDocument = DocumentProto.newBuilder()
.setUri("inside-uri")
.setSchema("type")
.setNamespace("namespace")
.build();
// Since we don't pass in "databaseName3/" as a prefix to remove, it stays on the Document.
DocumentProto expectedDocumentProto = DocumentProto.newBuilder()
.setUri("uri")
.setSchema("type")
.setNamespace("namespace")
.addProperties(PropertyProto.newBuilder().addDocumentValues(expectedInsideDocument))
.build();
DocumentProto.Builder actualDocument = documentProto.toBuilder();
mAppSearchImpl.removeDatabasesFromDocument(actualDocument);
assertThat(actualDocument.build()).isEqualTo(expectedDocumentProto);
mAppSearchImpl.rewriteDocumentTypes("databaseName/", actualDocument, /*add=*/false);
assertThat(actualDocument.build()).isEqualTo(documentProto);
}
@Test
public void testOptimize() throws Exception {
// Insert schema
SchemaProto schema = SchemaProto.newBuilder()
.addTypes(SchemaTypeConfigProto.newBuilder()
.setSchemaType("type").build())
.build();
mAppSearchImpl.setSchema("database", schema, /*forceOverride=*/false);
Set<AppSearchSchema> schemas =
Collections.singleton(new AppSearchSchema.Builder("type").build());
mAppSearchImpl.setSchema("database", schemas, /*forceOverride=*/false);
// Insert enough documents.
for (int i = 0; i < AppSearchImpl.OPTIMIZE_THRESHOLD_DOC_COUNT
+ AppSearchImpl.CHECK_OPTIMIZE_INTERVAL; i++) {
DocumentProto insideDocument = DocumentProto.newBuilder()
.setUri("inside-uri" + i)
.setSchema("type")
.setNamespace("namespace")
.build();
mAppSearchImpl.putDocument("database", insideDocument);
GenericDocument document =
new GenericDocument.Builder("uri" + i, "type").setNamespace(
"namespace").build();
mAppSearchImpl.putDocument("database", document);
}
// Check optimize() will release 0 docs since there is no deletion.
@@ -179,7 +208,7 @@ public class AppSearchImplTest {
// delete 999 documents , we will reach the threshold to trigger optimize() in next
// deletion.
for (int i = 0; i < AppSearchImpl.OPTIMIZE_THRESHOLD_DOC_COUNT - 1; i++) {
mAppSearchImpl.remove("database", "namespace", "inside-uri" + i);
mAppSearchImpl.remove("database", "namespace", "uri" + i);
}
// optimize() still not be triggered since we are in the interval to call getOptimizeInfo()
@@ -191,65 +220,111 @@ public class AppSearchImplTest {
for (int i = AppSearchImpl.OPTIMIZE_THRESHOLD_DOC_COUNT;
i < AppSearchImpl.OPTIMIZE_THRESHOLD_DOC_COUNT
+ AppSearchImpl.CHECK_OPTIMIZE_INTERVAL; i++) {
mAppSearchImpl.remove("database", "namespace", "inside-uri" + i);
mAppSearchImpl.remove("database", "namespace", "uri" + i);
}
// Verify optimize() is triggered
optimizeInfo = mAppSearchImpl.getOptimizeInfoResult();
assertThat(optimizeInfo.getOptimizableDocs())
.isLessThan((long) AppSearchImpl.CHECK_OPTIMIZE_INTERVAL);
.isLessThan(AppSearchImpl.CHECK_OPTIMIZE_INTERVAL);
}
@Test
public void testRewriteSearchSpec() throws Exception {
public void testRewriteSearchSpec_OneInstance() throws Exception {
SearchSpecProto.Builder searchSpecProto =
SearchSpecProto.newBuilder().setQuery("");
// Insert schema
SchemaProto schema = SchemaProto.newBuilder()
.addTypes(SchemaTypeConfigProto.newBuilder()
.setSchemaType("type").build())
.build();
mAppSearchImpl.setSchema("database", schema, /*forceOverride=*/false);
Set<AppSearchSchema> schemas =
Collections.singleton(new AppSearchSchema.Builder("type").build());
mAppSearchImpl.setSchema("database", schemas, /*forceOverride=*/false);
// Insert document
DocumentProto insideDocument = DocumentProto.newBuilder()
.setUri("inside-uri")
.setSchema("type")
.setNamespace("namespace")
.build();
mAppSearchImpl.putDocument("database", insideDocument);
GenericDocument document = new GenericDocument.Builder("uri", "type").setNamespace(
"namespace").build();
mAppSearchImpl.putDocument("database", document);
// Rewrite SearchSpec
mAppSearchImpl.rewriteSearchSpecForNonEmptyDatabase(
"database", searchSpecProto);
mAppSearchImpl.rewriteSearchSpecForDatabases(searchSpecProto, Collections.singleton(
"database"));
assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly("database/type");
assertThat(searchSpecProto.getNamespaceFiltersList()).containsExactly("database/namespace");
}
@Test
public void testRewriteSearchSpec_TwoInstances() throws Exception {
SearchSpecProto.Builder searchSpecProto =
SearchSpecProto.newBuilder().setQuery("");
// Insert schema
Set<AppSearchSchema> schemas = Set.of(
new AppSearchSchema.Builder("typeA").build(),
new AppSearchSchema.Builder("typeB").build());
mAppSearchImpl.setSchema("database1", schemas, /*forceOverride=*/false);
mAppSearchImpl.setSchema("database2", schemas, /*forceOverride=*/false);
// Insert documents
GenericDocument document1 = new GenericDocument.Builder("uri", "typeA").setNamespace(
"namespace").build();
mAppSearchImpl.putDocument("database1", document1);
GenericDocument document2 = new GenericDocument.Builder("uri", "typeB").setNamespace(
"namespace").build();
mAppSearchImpl.putDocument("database2", document2);
// Rewrite SearchSpec
mAppSearchImpl.rewriteSearchSpecForDatabases(searchSpecProto,
ImmutableSet.of("database1", "database2"));
assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly(
"database1/typeA", "database1/typeB", "database2/typeA", "database2/typeB");
assertThat(searchSpecProto.getNamespaceFiltersList()).containsExactly(
"database1/namespace", "database2/namespace");
}
@Test
public void testQueryEmptyDatabase() throws Exception {
SearchResultProto searchResultProto = mAppSearchImpl.query("EmptyDatabase",
SearchSpecProto.getDefaultInstance(),
ResultSpecProto.getDefaultInstance(), ScoringSpecProto.getDefaultInstance());
assertThat(searchResultProto.getResultsCount()).isEqualTo(0);
assertThat(searchResultProto.getStatus().getCode()).isEqualTo(StatusProto.Code.OK);
SearchSpec searchSpec =
new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE).build();
SearchResultPage searchResultPage = mAppSearchImpl.query(
"EmptyDatabase",
"", searchSpec);
assertThat(searchResultPage.getResults()).isEmpty();
}
@Test
public void testGlobalQueryEmptyDatabase() throws Exception {
SearchSpec searchSpec =
new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE).build();
SearchResultPage searchResultPage = mAppSearchImpl.query(
"EmptyDatabase",
"", searchSpec);
assertThat(searchResultPage.getResults()).isEmpty();
}
@Test
public void testRemoveEmptyDatabase_NoExceptionThrown() throws Exception {
mAppSearchImpl.removeByType("EmptyDatabase", "FakeType");
mAppSearchImpl.removeByNamespace("EmptyDatabase", "FakeNamespace");
mAppSearchImpl.removeAll("EmptyDatabase");
SearchSpec searchSpec =
new SearchSpec.Builder().addSchema("FakeType").setTermMatch(
TermMatchType.Code.PREFIX_VALUE).build();
mAppSearchImpl.removeByQuery("EmptyDatabase",
"", searchSpec);
searchSpec =
new SearchSpec.Builder().addNamespace("FakeNamespace").setTermMatch(
TermMatchType.Code.PREFIX_VALUE).build();
mAppSearchImpl.removeByQuery("EmptyDatabase",
"", searchSpec);
searchSpec = new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE).build();
mAppSearchImpl.removeByQuery("EmptyDatabase", "", searchSpec);
}
@Test
public void testSetSchema() throws Exception {
// Create schemas
SchemaProto schemaProto = SchemaProto.newBuilder()
.addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("Email")).build();
Set<AppSearchSchema> schemas =
Collections.singleton(new AppSearchSchema.Builder("Email").build());
// Set schema Email to AppSearch database1
mAppSearchImpl.setSchema("database1", schemaProto, /*forceOverride=*/false);
mAppSearchImpl.setSchema("database1", schemas, /*forceOverride=*/false);
// Create excepted schemaType proto.
SchemaProto exceptedProto = SchemaProto.newBuilder()
@@ -261,13 +336,11 @@ public class AppSearchImplTest {
@Test
public void testRemoveSchema() throws Exception {
// Create schemas
SchemaProto schemaProto = SchemaProto.newBuilder()
.addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("Email"))
.addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("Document")).build();
Set<AppSearchSchema> schemas = new HashSet<>();
schemas.add(new AppSearchSchema.Builder("Email").build());
schemas.add(new AppSearchSchema.Builder("Document").build());
// Set schema Email and Document to AppSearch database1
mAppSearchImpl.setSchema("database1", schemaProto, /*forceOverride=*/false);
mAppSearchImpl.setSchema("database1", schemas, /*forceOverride=*/false);
// Create excepted schemaType proto.
SchemaProto exceptedProto = SchemaProto.newBuilder()
@@ -279,19 +352,16 @@ public class AppSearchImplTest {
assertThat(mAppSearchImpl.getSchemaProto().getTypesList())
.containsExactlyElementsIn(exceptedProto.getTypesList());
// Save only Email this time.
schemaProto = SchemaProto.newBuilder()
.addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("Email")).build();
final Set<AppSearchSchema> finalSchemas = Collections.singleton(new AppSearchSchema.Builder(
"Email").build());
// Check the incompatible error has been thrown.
SchemaProto finalSchemaProto = schemaProto;
AppSearchException e = expectThrows(AppSearchException.class, () ->
mAppSearchImpl.setSchema("database1", finalSchemaProto, /*forceOverride=*/false));
mAppSearchImpl.setSchema("database1", finalSchemas, /*forceOverride=*/false));
assertThat(e).hasMessageThat().contains("Schema is incompatible");
assertThat(e).hasMessageThat().contains("Deleted types: [database1/Document]");
// ForceOverride to delete.
mAppSearchImpl.setSchema("database1", finalSchemaProto, /*forceOverride=*/true);
mAppSearchImpl.setSchema("database1", finalSchemas, /*forceOverride=*/true);
// Check Document schema is removed.
exceptedProto = SchemaProto.newBuilder()
@@ -304,13 +374,13 @@ public class AppSearchImplTest {
@Test
public void testRemoveSchema_differentDataBase() throws Exception {
// Create schemas
SchemaProto emailAndDocSchemaProto = SchemaProto.newBuilder()
.addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("Email"))
.addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("Document")).build();
Set<AppSearchSchema> schemas = new HashSet<>();
schemas.add(new AppSearchSchema.Builder("Email").build());
schemas.add(new AppSearchSchema.Builder("Document").build());
// Set schema Email and Document to AppSearch database1 and 2
mAppSearchImpl.setSchema("database1", emailAndDocSchemaProto, /*forceOverride=*/false);
mAppSearchImpl.setSchema("database2", emailAndDocSchemaProto, /*forceOverride=*/false);
mAppSearchImpl.setSchema("database1", schemas, /*forceOverride=*/false);
mAppSearchImpl.setSchema("database2", schemas, /*forceOverride=*/false);
// Create excepted schemaType proto.
SchemaProto exceptedProto = SchemaProto.newBuilder()
@@ -325,10 +395,8 @@ public class AppSearchImplTest {
.containsExactlyElementsIn(exceptedProto.getTypesList());
// Save only Email to database1 this time.
SchemaProto emailSchemaProto = SchemaProto.newBuilder()
.addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("Email"))
.build();
mAppSearchImpl.setSchema("database1", emailSchemaProto, /*forceOverride=*/true);
schemas = Collections.singleton(new AppSearchSchema.Builder("Email").build());
mAppSearchImpl.setSchema("database1", schemas, /*forceOverride=*/true);
// Create excepted schemaType list, database 1 should only contain Email but database 2
// remains in same.

View File

@@ -19,6 +19,7 @@ package com.android.server.appsearch.external.localstorage.converter;
import static com.google.common.truth.Truth.assertThat;
import android.app.appsearch.SearchResult;
import android.app.appsearch.SearchResultPage;
import com.android.server.appsearch.proto.DocumentProto;
import com.android.server.appsearch.proto.PropertyProto;
@@ -75,8 +76,9 @@ public class SnippetTest {
.build();
// Making ResultReader and getting Snippet values.
for (SearchResultProto.ResultProto proto : searchResultProto.getResultsList()) {
SearchResult result = SearchResultToProtoConverter.convertSearchResult(proto);
SearchResultPage searchResultPage =
SearchResultToProtoConverter.convertToSearchResultPage(searchResultProto);
for (SearchResult result : searchResultPage.getResults()) {
SearchResult.MatchInfo match = result.getMatches().get(0);
assertThat(match.getPropertyPath()).isEqualTo(propertyKeyString);
assertThat(match.getFullText()).isEqualTo(propertyValueString);
@@ -121,8 +123,9 @@ public class SnippetTest {
.addResults(resultProto)
.build();
for (SearchResultProto.ResultProto proto : searchResultProto.getResultsList()) {
SearchResult result = SearchResultToProtoConverter.convertSearchResult(proto);
SearchResultPage searchResultPage =
SearchResultToProtoConverter.convertToSearchResultPage(searchResultProto);
for (SearchResult result : searchResultPage.getResults()) {
assertThat(result.getMatches()).isEmpty();
}
}
@@ -182,8 +185,9 @@ public class SnippetTest {
.build();
// Making ResultReader and getting Snippet values.
for (SearchResultProto.ResultProto proto : searchResultProto.getResultsList()) {
SearchResult result = SearchResultToProtoConverter.convertSearchResult(proto);
SearchResultPage searchResultPage =
SearchResultToProtoConverter.convertToSearchResultPage(searchResultProto);
for (SearchResult result : searchResultPage.getResults()) {
SearchResult.MatchInfo match1 = result.getMatches().get(0);
assertThat(match1.getPropertyPath()).isEqualTo("sender.name");