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());
+    }
+}