Merge "Added query and SearchResults."
diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java
index 83195dc..0aa685d 100644
--- a/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java
+++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 The Android Open Source Project
+ * 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.
@@ -25,9 +25,12 @@
import com.android.internal.infra.AndroidFuture;
import com.google.android.icing.proto.SchemaProto;
+import com.google.android.icing.proto.SearchResultProto;
+import com.google.android.icing.protobuf.InvalidProtocolBufferException;
import java.util.List;
import java.util.concurrent.Executor;
+import java.util.function.BiConsumer;
import java.util.function.Consumer;
/**
@@ -127,4 +130,94 @@
// TODO(b/147614371) Fix error report for multiple documents.
future.whenCompleteAsync((noop, err) -> callback.accept(err), executor);
}
+
+ /**
+ * This method searches for documents based on a given query string. It also accepts
+ * specifications regarding how to search and format the results.
+ *
+ *<p>Currently we support following features in the raw query format:
+ * <ul>
+ * <li>AND
+ * 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
+ * OR joins (e.g. “match documents that have either the term ‘dog’ or
+ * ‘cat’”).
+ * Example: dog OR puppy
+ * <li>Exclusion
+ * Exclude a term (e.g. “match documents that do
+ * not have the term ‘dog’”).
+ * Example: -dog excludes the term ‘dog’
+ * <li>Grouping terms
+ * 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
+ * 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
+ * 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>
+ *
+ * <p> It is strongly recommended to use Jetpack APIs.
+ *
+ * @param queryExpression Query String to search.
+ * @param searchSpec Spec for setting filters, raw query etc.
+ * @param executor Executor on which to invoke the callback.
+ * @param callback Callback to receive errors resulting from the query operation. If the
+ * operation succeeds, the callback will be invoked with {@code null}.
+ * @hide
+ */
+ @NonNull
+ public void query(
+ @NonNull String queryExpression,
+ @NonNull SearchSpec searchSpec,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull BiConsumer<? super SearchResults, ? super Throwable> callback) {
+ AndroidFuture<byte[]> future = new AndroidFuture<>();
+ future.whenCompleteAsync((searchResultBytes, err) -> {
+ if (err != null) {
+ callback.accept(null, err);
+ return;
+ }
+
+ if (searchResultBytes != null) {
+ SearchResultProto searchResultProto;
+ try {
+ searchResultProto = SearchResultProto.parseFrom(searchResultBytes);
+ } catch (InvalidProtocolBufferException e) {
+ callback.accept(null, e);
+ return;
+ }
+ if (searchResultProto.hasError()) {
+ // TODO(sidchhabra): Add better exception handling.
+ callback.accept(
+ null,
+ new RuntimeException(searchResultProto.getError().getErrorMessage()));
+ return;
+ }
+ SearchResults searchResults = new SearchResults(searchResultProto);
+ callback.accept(searchResults, null);
+ return;
+ }
+
+ // Nothing was supplied in the future at all
+ callback.accept(
+ null, new IllegalStateException("Unknown failure occurred while querying"));
+ }, executor);
+
+ try {
+ mService.query(queryExpression, searchSpec.getProto().toByteArray(), 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 fc83d8c..22250f4 100644
--- a/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl
+++ b/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl
@@ -1,5 +1,5 @@
/**
- * Copyright 2019, The Android Open Source Project
+ * 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.
@@ -29,4 +29,13 @@
*/
void setSchema(in byte[] schemaProto, in AndroidFuture callback);
void put(in byte[] documentBytes, in AndroidFuture callback);
+ /**
+ * Searches a document based on a given query string.
+ *
+ * @param queryExpression Query String to search.
+ * @param searchSpec Serialized SearchSpecProto.
+ * @param callback {@link AndroidFuture}. Will be completed with a serialized
+ * {@link SearchResultsProto}, or completed exceptionally if query fails.
+ */
+ void query(in String queryExpression, in byte[] searchSpecBytes, in AndroidFuture callback);
}
diff --git a/apex/appsearch/framework/java/android/app/appsearch/IllegalSearchSpecException.java b/apex/appsearch/framework/java/android/app/appsearch/IllegalSearchSpecException.java
new file mode 100644
index 0000000..0d029f0
--- /dev/null
+++ b/apex/appsearch/framework/java/android/app/appsearch/IllegalSearchSpecException.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+/**
+ * Indicates that a {@link android.app.appsearch.SearchResults} has logical inconsistencies such
+ * as unpopulated mandatory fields or illegal combinations of parameters.
+ *
+ * @hide
+ */
+public class IllegalSearchSpecException extends IllegalArgumentException {
+ /**
+ * Constructs a new {@link IllegalSearchSpecException}.
+ *
+ * @param message A developer-readable description of the issue with the bundle.
+ */
+ public IllegalSearchSpecException(@NonNull String message) {
+ super(message);
+ }
+}
diff --git a/apex/appsearch/framework/java/android/app/appsearch/SearchResults.java b/apex/appsearch/framework/java/android/app/appsearch/SearchResults.java
new file mode 100644
index 0000000..ec4258d
--- /dev/null
+++ b/apex/appsearch/framework/java/android/app/appsearch/SearchResults.java
@@ -0,0 +1,89 @@
+/*
+ * 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 com.google.android.icing.proto.SearchResultProto;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * SearchResults are a list of results that are returned from a query. Each result from this
+ * list contains a document and may contain other fields like snippets based on request.
+ * @hide
+ */
+public final class SearchResults {
+
+ private final SearchResultProto mSearchResultProto;
+
+ /** @hide */
+ public SearchResults(SearchResultProto searchResultProto) {
+ mSearchResultProto = searchResultProto;
+ }
+
+ /**
+ * This class represents the result obtained from the query. It will contain the document which
+ * which matched the specified query string and specifications.
+ * @hide
+ */
+ public static final class Result {
+ private final SearchResultProto.ResultProto mResultProto;
+
+ private Result(SearchResultProto.ResultProto resultProto) {
+ mResultProto = resultProto;
+ }
+
+ /**
+ * Contains the matching {@link AppSearch.Document}.
+ * @return Document object which matched the query.
+ * @hide
+ */
+ // TODO(sidchhabra): Switch to Document constructor that takes proto.
+ @NonNull
+ public AppSearch.Document getDocument() {
+ return AppSearch.Document.newBuilder(mResultProto.getDocument().getUri(),
+ mResultProto.getDocument().getSchema())
+ .setCreationTimestampSecs(mResultProto.getDocument().getCreationTimestampSecs())
+ .setScore(mResultProto.getDocument().getScore())
+ .build();
+ }
+
+ // TODO(sidchhabra): Add Getter for ResultReader for Snippet.
+ }
+
+ @Override
+ public String toString() {
+ return mSearchResultProto.toString();
+ }
+
+ /**
+ * Returns a {@link Result} iterator. Returns Empty Iterator if there are no matching results.
+ * @hide
+ */
+ @NonNull
+ public Iterator<Result> getResults() {
+ List<Result> results = new ArrayList<>();
+ // TODO(sidchhabra): Pass results using a RemoteStream.
+ for (SearchResultProto.ResultProto resultProto : mSearchResultProto.getResultsList()) {
+ results.add(new Result(resultProto));
+ }
+ return results.iterator();
+ }
+}
diff --git a/apex/appsearch/framework/java/android/app/appsearch/SearchSpec.java b/apex/appsearch/framework/java/android/app/appsearch/SearchSpec.java
new file mode 100644
index 0000000..5df7108
--- /dev/null
+++ b/apex/appsearch/framework/java/android/app/appsearch/SearchSpec.java
@@ -0,0 +1,127 @@
+/*
+ * 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.IntDef;
+import android.annotation.NonNull;
+
+import com.google.android.icing.proto.SearchSpecProto;
+import com.google.android.icing.proto.TermMatchType;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This class represents the specification logic for AppSearch. It can be used to set the type of
+ * search, like prefix or exact only or apply filters to search for a specific schema type only etc.
+ * @hide
+ *
+ */
+// TODO(sidchhabra) : AddResultSpec fields for Snippets etc.
+public final class SearchSpec {
+
+ private final SearchSpecProto mSearchSpecProto;
+
+ private SearchSpec(SearchSpecProto searchSpecProto) {
+ mSearchSpecProto = searchSpecProto;
+ }
+
+ /** Creates a new {@link SearchSpec.Builder}. */
+ @NonNull
+ public static SearchSpec.Builder newBuilder() {
+ return new SearchSpec.Builder();
+ }
+
+ /** @hide */
+ @NonNull
+ SearchSpecProto getProto() {
+ return mSearchSpecProto;
+ }
+
+ /** Term Match Type for the query. */
+ // NOTE: The integer values of these constants must match the proto enum constants in
+ // {@link com.google.android.icing.proto.SearchSpecProto.termMatchType}
+ @IntDef(prefix = {"TERM_MATCH_TYPE_"}, value = {
+ TERM_MATCH_TYPE_EXACT_ONLY,
+ TERM_MATCH_TYPE_PREFIX
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface TermMatchTypeCode {}
+
+ public static final int TERM_MATCH_TYPE_EXACT_ONLY = 1;
+ public static final int TERM_MATCH_TYPE_PREFIX = 2;
+
+ /** Builder for {@link SearchSpec objects}. */
+ public static final class Builder {
+
+ private final SearchSpecProto.Builder mBuilder = SearchSpecProto.newBuilder();
+
+ private Builder(){}
+
+ /**
+ * Indicates how the query terms should match {@link TermMatchTypeCode} in the index.
+ *
+ * TermMatchType.Code=EXACT_ONLY
+ * Query terms will only match exact tokens in the index.
+ * Ex. A query term "foo" will only match indexed token "foo", and not "foot"
+ * or "football"
+ *
+ * TermMatchType.Code=PREFIX
+ * Query terms will match indexed tokens when the query term is a prefix of
+ * the token.
+ * Ex. A query term "foo" will match indexed tokens like "foo", "foot", and
+ * "football".
+ */
+ @NonNull
+ public Builder setTermMatchType(@TermMatchTypeCode int termMatchTypeCode) {
+ TermMatchType.Code termMatchTypeCodeProto =
+ TermMatchType.Code.forNumber(termMatchTypeCode);
+ if (termMatchTypeCodeProto == null) {
+ throw new IllegalArgumentException("Invalid term match type: " + termMatchTypeCode);
+ }
+ mBuilder.setTermMatchType(termMatchTypeCodeProto);
+ return this;
+ }
+
+ /**
+ * Adds a Schema type filter to {@link SearchSpec} Entry.
+ * Only search for documents that have the specified schema types.
+ * If unset, the query will search over all schema types.
+ */
+ @NonNull
+ public Builder setSchemaTypes(@NonNull String... schemaTypes) {
+ for (String schemaType : schemaTypes) {
+ mBuilder.addSchemaTypeFilters(schemaType);
+ }
+ return this;
+ }
+
+ /**
+ * Constructs a new {@link SearchSpec} from the contents of this builder.
+ *
+ * <p>After calling this method, the builder must no longer be used.
+ */
+ @NonNull
+ public SearchSpec build() {
+ if (mBuilder.getTermMatchType() == TermMatchType.Code.UNKNOWN) {
+ throw new IllegalSearchSpecException("Missing termMatchType field.");
+ }
+ return new SearchSpec(mBuilder.build());
+ }
+ }
+
+}
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 ce7e04c..f63abd9 100644
--- a/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java
+++ b/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 The Android Open Source Project
+ * 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.
@@ -15,6 +15,7 @@
*/
package com.android.server.appsearch;
+import android.annotation.NonNull;
import android.app.appsearch.IAppSearchManager;
import android.content.Context;
import android.os.Binder;
@@ -24,9 +25,13 @@
import com.android.internal.util.Preconditions;
import com.android.server.SystemService;
import com.android.server.appsearch.impl.AppSearchImpl;
+import com.android.server.appsearch.impl.FakeIcing;
import com.android.server.appsearch.impl.ImplInstanceManager;
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.protobuf.InvalidProtocolBufferException;
/**
* TODO(b/142567528): add comments when implement this class
@@ -35,8 +40,11 @@
public AppSearchManagerService(Context context) {
super(context);
+ mFakeIcing = new FakeIcing();
}
+ private final FakeIcing mFakeIcing;
+
@Override
public void onStart() {
publishBinderService(Context.APP_SEARCH_SERVICE, new Stub());
@@ -70,5 +78,22 @@
callback.completeExceptionally(t);
}
}
+ // TODO(sidchhabra):Init FakeIcing properly.
+ // TODO(sidchhabra): Do this in a threadpool.
+ @Override
+ public void query(@NonNull String queryExpression, @NonNull byte[] searchSpec,
+ AndroidFuture callback) {
+ Preconditions.checkNotNull(queryExpression);
+ Preconditions.checkNotNull(searchSpec);
+ SearchSpecProto searchSpecProto = null;
+ try {
+ searchSpecProto = SearchSpecProto.parseFrom(searchSpec);
+ } catch (InvalidProtocolBufferException e) {
+ throw new RuntimeException(e);
+ }
+ SearchResultProto searchResults =
+ mFakeIcing.query(queryExpression);
+ callback.complete(searchResults.toByteArray());
+ }
}
}
diff --git a/core/tests/coretests/src/android/app/appsearch/SearchResultsTest.java b/core/tests/coretests/src/android/app/appsearch/SearchResultsTest.java
new file mode 100644
index 0000000..21259cc
--- /dev/null
+++ b/core/tests/coretests/src/android/app/appsearch/SearchResultsTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import androidx.test.filters.SmallTest;
+
+import com.google.android.icing.proto.DocumentProto;
+import com.google.android.icing.proto.SearchResultProto;
+
+import org.junit.Test;
+
+@SmallTest
+public class SearchResultsTest {
+
+ @Test
+ public void testSearchResultsEqual() {
+ final String uri = "testUri";
+ final String schemaType = "testSchema";
+ SearchResultProto.ResultProto result1 = SearchResultProto.ResultProto.newBuilder()
+ .setDocument(DocumentProto.newBuilder()
+ .setUri(uri)
+ .setSchema(schemaType)
+ .build())
+ .build();
+ SearchResultProto searchResults1 = SearchResultProto.newBuilder()
+ .addResults(result1)
+ .build();
+ SearchResults res1 = new SearchResults(searchResults1);
+ SearchResultProto.ResultProto result2 = SearchResultProto.ResultProto.newBuilder()
+ .setDocument(DocumentProto.newBuilder()
+ .setUri(uri)
+ .setSchema(schemaType)
+ .build())
+ .build();
+ SearchResultProto searchResults2 = SearchResultProto.newBuilder()
+ .addResults(result2)
+ .build();
+ SearchResults res2 = new SearchResults(searchResults2);
+ assertThat(res1.toString()).isEqualTo(res2.toString());
+ }
+
+ @Test
+ public void buildSearchSpecWithoutTermMatchType() {
+ assertThrows(RuntimeException.class, () -> SearchSpec.newBuilder()
+ .setSchemaTypes("testSchemaType")
+ .build());
+ }
+}