blob: 75dc064c3672f58f46ec835323629cb1b82fd5b1 [file] [log] [blame]
/*
* Copyright (C) 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.annotation.NonNull;
import android.annotation.SystemService;
import android.content.Context;
import android.os.RemoteException;
import com.android.internal.infra.AndroidFuture;
import com.google.android.icing.proto.DocumentProto;
import com.google.android.icing.proto.SchemaProto;
import com.google.android.icing.proto.SearchResultProto;
import com.google.android.icing.proto.SearchSpecProto;
import com.google.android.icing.proto.StatusProto;
import com.google.android.icing.protobuf.InvalidProtocolBufferException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
/**
* This class provides access to the centralized AppSearch index maintained by the system.
*
* <p>Apps can index structured text documents with AppSearch, which can then be retrieved through
* the query API.
*
* @hide
*/
// TODO(b/148046169): This class header needs a detailed example/tutorial.
@SystemService(Context.APP_SEARCH_SERVICE)
public class AppSearchManager {
private final IAppSearchManager mService;
/** @hide */
public AppSearchManager(@NonNull IAppSearchManager service) {
mService = service;
}
/**
* Sets the schema being used by documents provided to the {@link #putDocuments} method.
*
* <p>The schema provided here is compared to the stored copy of the schema previously supplied
* to {@link #setSchema}, if any, to determine how to treat existing documents. The following
* types of schema modifications are always safe and are made without deleting any existing
* documents:
* <ul>
* <li>Addition of new types
* <li>Addition of new
* {@link android.app.appsearch.AppSearchSchema.PropertyConfig#CARDINALITY_OPTIONAL
* OPTIONAL} or
* {@link android.app.appsearch.AppSearchSchema.PropertyConfig#CARDINALITY_REPEATED
* REPEATED} properties to a type
* <li>Changing the cardinality of a data type to be less restrictive (e.g. changing an
* {@link android.app.appsearch.AppSearchSchema.PropertyConfig#CARDINALITY_OPTIONAL
* OPTIONAL} property into a
* {@link android.app.appsearch.AppSearchSchema.PropertyConfig#CARDINALITY_REPEATED
* REPEATED} property.
* </ul>
*
* <p>The following types of schema changes are not backwards-compatible:
* <ul>
* <li>Removal of an existing type
* <li>Removal of a property from a type
* <li>Changing the data type ({@code boolean}, {@code long}, etc.) of an existing property
* <li>For properties of {@code AppSearchDocument} type, changing the schema type of
* {@code AppSearchDocument}s of that property
* <li>Changing the cardinality of a data type to be more restrictive (e.g. changing an
* {@link android.app.appsearch.AppSearchSchema.PropertyConfig#CARDINALITY_OPTIONAL
* OPTIONAL} property into a
* {@link android.app.appsearch.AppSearchSchema.PropertyConfig#CARDINALITY_REQUIRED
* REQUIRED} property).
* <li>Adding a
* {@link android.app.appsearch.AppSearchSchema.PropertyConfig#CARDINALITY_REQUIRED
* REQUIRED} property.
* </ul>
* <p>Supplying a schema with such changes will result in this call returning an
* {@link AppSearchResult} with a code of {@link AppSearchResult#RESULT_INVALID_SCHEMA} and an
* error message describing the incompatibility. In this case the previously set schema will
* remain active.
*
* <p>If you need to make non-backwards-compatible changes as described above, instead use the
* {@link #setSchema(List, boolean)} method with the {@code forceOverride} parameter set to
* {@code true}.
*
* <p>It is a no-op to set the same schema as has been previously set; this is handled
* efficiently.
*
* @param schemas The schema configs for the types used by the calling app.
* @return the result of performing this operation.
*
* @hide
*/
@NonNull
public AppSearchResult<Void> setSchema(@NonNull AppSearchSchema... schemas) {
return setSchema(Arrays.asList(schemas), /*forceOverride=*/false);
}
/**
* Sets the schema being used by documents provided to the {@link #putDocuments} method.
*
* <p>This method is similar to {@link #setSchema(AppSearchSchema...)}, except for the
* {@code forceOverride} parameter. If a backwards-incompatible schema is specified but the
* {@code forceOverride} parameter is set to {@code true}, instead of returning an
* {@link AppSearchResult} with the {@link AppSearchResult#RESULT_INVALID_SCHEMA} code, all
* documents which are not compatible with the new schema will be deleted and the incompatible
* schema will be applied.
*
* @param schemas The schema configs for the types used by the calling app.
* @param forceOverride Whether to force the new schema to be applied even if there are
* incompatible changes versus the previously set schema. Documents which are incompatible
* with the new schema will be deleted.
* @return the result of performing this operation.
*
* @hide
*/
@NonNull
public AppSearchResult<Void> setSchema(
@NonNull List<AppSearchSchema> schemas, boolean forceOverride) {
// Prepare the merged schema for transmission.
SchemaProto.Builder schemaProtoBuilder = SchemaProto.newBuilder();
for (AppSearchSchema schema : schemas) {
schemaProtoBuilder.addTypes(schema.getProto());
}
// Serialize and send the schema.
// TODO: This should use com.android.internal.infra.RemoteStream or another mechanism to
// avoid binder limits.
byte[] schemaBytes = schemaProtoBuilder.build().toByteArray();
AndroidFuture<AppSearchResult> future = new AndroidFuture<>();
try {
mService.setSchema(schemaBytes, forceOverride, future);
} catch (RemoteException e) {
future.completeExceptionally(e);
}
return getFutureOrThrow(future);
}
/**
* Index {@link AppSearchDocument}s into AppSearch.
*
* <p>You should not call this method directly; instead, use the
* {@code AppSearch#putDocuments()} API provided by JetPack.
*
* <p>Each {@link AppSearchDocument}'s {@code schemaType} field must be set to the name of a
* schema type previously registered via the {@link #setSchema} method.
*
* @param documents {@link AppSearchDocument}s that need to be indexed.
* @return An {@link AppSearchBatchResult} mapping the document URIs to {@link Void} if they
* were successfully indexed, or a {@link Throwable} describing the failure if they could
* not be indexed.
* @hide
*/
public AppSearchBatchResult<String, Void> putDocuments(
@NonNull List<AppSearchDocument> documents) {
// TODO(b/146386470): Transmit these documents as a RemoteStream instead of sending them in
// one big list.
List<byte[]> documentsBytes = new ArrayList<>(documents.size());
for (AppSearchDocument document : documents) {
documentsBytes.add(document.getProto().toByteArray());
}
AndroidFuture<AppSearchBatchResult> future = new AndroidFuture<>();
try {
mService.putDocuments(documentsBytes, future);
} catch (RemoteException e) {
future.completeExceptionally(e);
}
return getFutureOrThrow(future);
}
/**
* Retrieves {@link AppSearchDocument}s by URI.
*
* <p>You should not call this method directly; instead, use the
* {@code AppSearch#getDocuments()} API provided by JetPack.
*
* @param uris URIs of the documents to look up.
* @return An {@link AppSearchBatchResult} mapping the document URIs to
* {@link AppSearchDocument} values if they were successfully retrieved, a {@code null}
* failure if they were not found, or a {@link Throwable} failure describing the problem if
* an error occurred.
*/
public AppSearchBatchResult<String, AppSearchDocument> getDocuments(
@NonNull List<String> uris) {
// TODO(b/146386470): Transmit the result documents as a RemoteStream instead of sending
// them in one big list.
AndroidFuture<AppSearchBatchResult> future = new AndroidFuture<>();
try {
mService.getDocuments(uris, future);
} catch (RemoteException e) {
future.completeExceptionally(e);
}
// Deserialize the protos into Document objects
AppSearchBatchResult<String, byte[]> protoResults = getFutureOrThrow(future);
AppSearchBatchResult.Builder<String, AppSearchDocument> documentResultBuilder =
new AppSearchBatchResult.Builder<>();
// Translate successful results
for (Map.Entry<String, byte[]> protoResult : protoResults.getSuccesses().entrySet()) {
DocumentProto documentProto;
try {
documentProto = DocumentProto.parseFrom(protoResult.getValue());
} catch (InvalidProtocolBufferException e) {
documentResultBuilder.setFailure(
protoResult.getKey(), AppSearchResult.RESULT_IO_ERROR, e.getMessage());
continue;
}
AppSearchDocument document;
try {
document = new AppSearchDocument(documentProto);
} catch (Throwable t) {
// These documents went through validation, so how could this fail? We must have
// done something wrong.
documentResultBuilder.setFailure(
protoResult.getKey(),
AppSearchResult.RESULT_INTERNAL_ERROR,
t.getMessage());
continue;
}
documentResultBuilder.setSuccess(protoResult.getKey(), document);
}
// Translate failed results
for (Map.Entry<String, AppSearchResult<byte[]>> protoResult :
protoResults.getFailures().entrySet()) {
documentResultBuilder.setFailure(
protoResult.getKey(),
protoResult.getValue().getResultCode(),
protoResult.getValue().getErrorMessage());
}
return documentResultBuilder.build();
}
/**
* Searches a document based on a given query string.
*
* <p>You should not call this method directly; instead, use the {@code AppSearch#query()} API
* provided by JetPack.
*
* <p>Currently we support following features in the raw query format:
* <ul>
* <li>AND
* <p>AND joins (e.g. “match documents that have both the terms ‘dog’ and
* ‘cat’”).
* Example: hello world matches documents that have both ‘hello’ and ‘world’
* <li>OR
* <p>OR joins (e.g. “match documents that have either the term ‘dog’ or
* ‘cat’”).
* Example: dog OR puppy
* <li>Exclusion
* <p>Exclude a term (e.g. “match documents that do
* not have the term ‘dog’”).
* Example: -dog excludes the term ‘dog’
* <li>Grouping terms
* <p>Allow for conceptual grouping of subqueries to enable hierarchical structures (e.g.
* “match documents that have either ‘dog’ or ‘puppy’, and either ‘cat’ or ‘kitten’”).
* Example: (dog puppy) (cat kitten) two one group containing two terms.
* <li>Property restricts
* <p> Specifies which properties of a document to specifically match terms in (e.g.
* “match documents where the ‘subject’ property contains ‘important’”).
* Example: subject:important matches documents with the term ‘important’ in the
* ‘subject’ property
* <li>Schema type restricts
* <p>This is similar to property restricts, but allows for restricts on top-level document
* fields, such as schema_type. Clients should be able to limit their query to documents of
* a certain schema_type (e.g. “match documents that are of the ‘Email’ schema_type”).
* Example: { schema_type_filters: “Email”, “Video”,query: “dog” } will match documents
* that contain the query term ‘dog’ and are of either the ‘Email’ schema type or the
* ‘Video’ schema type.
* </ul>
*
* @param queryExpression Query String to search.
* @param searchSpec Spec for setting filters, raw query etc.
* @hide
*/
@NonNull
public AppSearchResult<SearchResults> query(
@NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
// TODO(b/146386470): Transmit the result documents as a RemoteStream instead of sending
// them in one big list.
AndroidFuture<AppSearchResult> searchResultFuture = new AndroidFuture<>();
try {
SearchSpecProto searchSpecProto = searchSpec.getSearchSpecProto();
searchSpecProto = searchSpecProto.toBuilder().setQuery(queryExpression).build();
mService.query(
searchSpecProto.toByteArray(),
searchSpec.getResultSpecProto().toByteArray(),
searchSpec.getScoringSpecProto().toByteArray(),
searchResultFuture);
} catch (RemoteException e) {
searchResultFuture.completeExceptionally(e);
}
// Deserialize the protos into Document objects
AppSearchResult<byte[]> searchResultBytes = getFutureOrThrow(searchResultFuture);
if (!searchResultBytes.isSuccess()) {
return AppSearchResult.newFailedResult(
searchResultBytes.getResultCode(), searchResultBytes.getErrorMessage());
}
SearchResultProto searchResultProto;
try {
searchResultProto = SearchResultProto.parseFrom(searchResultBytes.getResultValue());
} catch (InvalidProtocolBufferException e) {
return AppSearchResult.newFailedResult(
AppSearchResult.RESULT_INTERNAL_ERROR, e.getMessage());
}
if (searchResultProto.getStatus().getCode() != StatusProto.Code.OK) {
// This should never happen; AppSearchManagerService should catch failed searchResults
// entries and transmit them as a failed AppSearchResult.
return AppSearchResult.newFailedResult(
AppSearchResult.RESULT_INTERNAL_ERROR,
searchResultProto.getStatus().getMessage());
}
return AppSearchResult.newSuccessfulResult(new SearchResults(searchResultProto));
}
/**
* Deletes {@link AppSearchDocument}s by URI.
*
* <p>You should not call this method directly; instead, use the {@code AppSearch#delete()} API
* provided by JetPack.
*
* @param uris URIs of the documents to delete
* @return An {@link AppSearchBatchResult} mapping each URI to a {@code null} success if
* deletion was successful, to a {@code null} failure if the document did not exist, or to a
* {@code throwable} failure if deletion failed for another reason.
*/
public AppSearchBatchResult<String, Void> delete(@NonNull List<String> uris) {
AndroidFuture<AppSearchBatchResult> future = new AndroidFuture<>();
try {
mService.delete(uris, future);
} catch (RemoteException e) {
future.completeExceptionally(e);
}
return getFutureOrThrow(future);
}
/**
* Deletes {@link android.app.appsearch.AppSearch.Document}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(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(future);
} catch (RemoteException e) {
future.completeExceptionally(e);
}
return getFutureOrThrow(future);
}
private static <T> T getFutureOrThrow(@NonNull AndroidFuture<T> future) {
try {
return future.get();
} catch (Throwable e) {
if (e instanceof ExecutionException) {
e = e.getCause();
}
if (e instanceof RuntimeException) {
throw (RuntimeException) e;
}
if (e instanceof Error) {
throw (Error) e;
}
throw new RuntimeException(e);
}
}
}