diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java index 18bc59b91aa0c..f2c9942edbb32 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java +++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java @@ -267,14 +267,14 @@ public class AppSearchManager { searchResultsFuture.completeExceptionally(e); } - // Translate the list of Bundle into a list of SearchResult - AppSearchResult searchResultsResult = getFutureOrThrow(searchResultsFuture); - if (!searchResultsResult.isSuccess()) { - return AppSearchResult.newFailedResult( - searchResultsResult.getResultCode(), searchResultsResult.getErrorMessage()); + // Translate the Bundle into a searchResultPage. + AppSearchResult 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 uris = new ArrayList<>(request.getUris()); AndroidFuture 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. - * - *

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 deleteByTypes(@NonNull List schemaTypes) { - AndroidFuture 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 deleteAll() { - AndroidFuture future = new AndroidFuture<>(); - try { - mService.deleteAll(DEFAULT_DATABASE, future); + mService.removeByUri(DEFAULT_DATABASE, request.getNamespace(), uris, future); } catch (RemoteException e) { future.completeExceptionally(e); } diff --git a/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl b/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl index 352a980eef273..01260ea193f6a 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl +++ b/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl @@ -92,7 +92,7 @@ interface IAppSearchManager { in AndroidFuture 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 uris, in AndroidFuture 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}<{@link AppSearchBatchResult}<{@link String}, {@link Void}>>. - * If the call fails to start, {@code callback} will be completed exceptionally. Otherwise, - * {@code callback} will be completed with an - * {@link AppSearchBatchResult}<{@link String}, {@link Void}> - * 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}<{@link AppSearchResult}<{@link SearchResults}>> */ - void deleteByTypes( + void removeByQuery( in String databaseName, - in List schemaTypes, - in AndroidFuture callback); - - /** - * Deletes all documents belonging to the calling app. - * - * @param databaseName The databaseName to remove all documents from. - * @param callback {@link AndroidFuture}<{@link AppSearchResult}<{@link Void}>>. - * Will be completed with the result of the call. - */ - void deleteAll(in String databaseName, in AndroidFuture callback); + in String queryExpression, + in Bundle searchSpecBundle, + in AndroidFuture callback); } diff --git a/apex/appsearch/framework/java/android/app/appsearch/SearchResultPage.java b/apex/appsearch/framework/java/android/app/appsearch/SearchResultPage.java new file mode 100644 index 0000000000000..756d1b5d673fd --- /dev/null +++ b/apex/appsearch/framework/java/android/app/appsearch/SearchResultPage.java @@ -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 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 getResults() { + if (mResults == null) { + ArrayList 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; + } +} diff --git a/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java b/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java index f2830e5b8e6d3..1dfde528e69b9 100644 --- a/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java +++ b/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java @@ -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 schemaBundles, boolean forceOverride, @NonNull AndroidFuture 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 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 documentBundles, @NonNull AndroidFuture 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 uris, @NonNull AndroidFuture 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 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 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 uris, AndroidFuture 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 schemaTypes, AndroidFuture 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 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 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 { diff --git a/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/AppSearchImpl.java b/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/AppSearchImpl.java index b1a79f84714dc..e021544976b51 100644 --- a/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/AppSearchImpl.java +++ b/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/AppSearchImpl.java @@ -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; * * *

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 { *

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 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 newTypeNames = rewriteSchema(databaseName, existingSchemaBuilder, origSchema); + Set 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 { * *

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. + * + *

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 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. + *

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. + * + *

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. * *

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 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. - * - *

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 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. - * - *

The schemas will remain. To clear everything including schemas, please call - * {@link #setSchema} with an empty schema and {@code forceOverride} set to true. - * - *

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 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}. * - *

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. + *

If the searchSpec has empty filter lists, all existing databases from + * {@code databaseNames} will be added. *

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 existingSchemaTypes = mSchemaMap.get(databaseName); - Set existingNamespaces = mNamespaceMap.get(databaseName); - if (existingSchemaTypes == null || existingSchemaTypes.isEmpty() - || existingNamespaces == null || existingNamespaces.isEmpty()) { + boolean rewriteSearchSpecForDatabases( + @NonNull SearchSpecProto.Builder searchSpecBuilder, + @NonNull Set databaseNames) { + // Create a copy since retainAll() modifies the original set. + Set 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 schemaTypeFilters = searchSpecBuilder.getSchemaTypeFiltersList(); + searchSpecBuilder.clearSchemaTypeFilters(); + + List namespaceFilters = searchSpecBuilder.getNamespaceFiltersList(); + searchSpecBuilder.clearNamespaceFilters(); + + // Rewrite filters to include a database prefix. + for (String databaseName : existingDatabases) { + Set 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 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> map, String databaseName, String prefixedValue) { Set 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 diff --git a/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/converter/SearchResultToProtoConverter.java b/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/converter/SearchResultToProtoConverter.java index 9f7c6968e993a..4310b42162668 100644 --- a/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/converter/SearchResultToProtoConverter.java +++ b/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/converter/SearchResultToProtoConverter.java @@ -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 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( diff --git a/services/tests/servicestests/src/com/android/server/appsearch/external/localstorage/AppSearchImplTest.java b/services/tests/servicestests/src/com/android/server/appsearch/external/localstorage/AppSearchImplTest.java index 0d0ac6d2794d2..726e48a001d75 100644 --- a/services/tests/servicestests/src/com/android/server/appsearch/external/localstorage/AppSearchImplTest.java +++ b/services/tests/servicestests/src/com/android/server/appsearch/external/localstorage/AppSearchImplTest.java @@ -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 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 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 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 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 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 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 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. diff --git a/services/tests/servicestests/src/com/android/server/appsearch/external/localstorage/converter/SnippetTest.java b/services/tests/servicestests/src/com/android/server/appsearch/external/localstorage/converter/SnippetTest.java index d5762a1317b50..2e9286c6b2ad2 100644 --- a/services/tests/servicestests/src/com/android/server/appsearch/external/localstorage/converter/SnippetTest.java +++ b/services/tests/servicestests/src/com/android/server/appsearch/external/localstorage/converter/SnippetTest.java @@ -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");