diff --git a/apex/appsearch/framework/java/android/app/appsearch/SearchResults.java b/apex/appsearch/framework/java/android/app/appsearch/SearchResults.java index 05735cc59d931..82c2d26214dca 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/SearchResults.java +++ b/apex/appsearch/framework/java/android/app/appsearch/SearchResults.java @@ -45,33 +45,35 @@ public class SearchResults implements Closeable { private final IAppSearchManager mService; + // The database name to search over. If null, this will search over all database names. @Nullable private final String mDatabaseName; - @UserIdInt - private final int mUserId; - private final String mQueryExpression; private final SearchSpec mSearchSpec; + @UserIdInt + private final int mUserId; + private final Executor mExecutor; private long mNextPageToken; private boolean mIsFirstLoad = true; - SearchResults(@NonNull IAppSearchManager service, + SearchResults( + @NonNull IAppSearchManager service, @Nullable String databaseName, @NonNull String queryExpression, @NonNull SearchSpec searchSpec, @UserIdInt int userId, @NonNull @CallbackExecutor Executor executor) { mService = Objects.requireNonNull(service); - mUserId = userId; - mDatabaseName = Objects.requireNonNull(databaseName); + mDatabaseName = databaseName; mQueryExpression = Objects.requireNonNull(queryExpression); mSearchSpec = Objects.requireNonNull(searchSpec); + mUserId = userId; mExecutor = Objects.requireNonNull(executor); } @@ -90,11 +92,14 @@ public class SearchResults implements Closeable { if (mIsFirstLoad) { mIsFirstLoad = false; if (mDatabaseName == null) { + // Global query, there's no one package-database combination to check. mService.globalQuery(mQueryExpression, mSearchSpec.getBundle(), mUserId, wrapCallback(callback)); } else { - mService.query(mDatabaseName, mQueryExpression, mSearchSpec.getBundle(), - mUserId, wrapCallback(callback)); + // Normal local query, pass in specified database. + mService.query( + mDatabaseName, mQueryExpression, mSearchSpec.getBundle(), mUserId, + wrapCallback(callback)); } } else { mService.getNextPage(mNextPageToken, mUserId, wrapCallback(callback)); @@ -104,6 +109,24 @@ public class SearchResults implements Closeable { } } + @Override + public void close() { + try { + mService.invalidateNextPageToken(mNextPageToken, mUserId); + } catch (RemoteException e) { + Log.d(TAG, "Unable to close the SearchResults", e); + } + } + + private IAppSearchResultCallback wrapCallback( + @NonNull Consumer>> callback) { + return new IAppSearchResultCallback.Stub() { + public void onResult(AppSearchResult result) { + mExecutor.execute(() -> invokeCallback(result, callback)); + } + }; + } + private void invokeCallback(AppSearchResult result, @NonNull Consumer>> callback) { if (result.isSuccess()) { @@ -120,23 +143,4 @@ public class SearchResults implements Closeable { callback.accept(result); } } - @Override - public void close() { - mExecutor.execute(() -> { - try { - mService.invalidateNextPageToken(mNextPageToken, mUserId); - } catch (RemoteException e) { - Log.d(TAG, "Unable to close the SearchResults", e); - } - }); - } - - private IAppSearchResultCallback wrapCallback( - @NonNull Consumer>> callback) { - return new IAppSearchResultCallback.Stub() { - public void onResult(AppSearchResult result) { - mExecutor.execute(() -> invokeCallback(result, callback)); - } - }; - } } diff --git a/apex/appsearch/framework/java/external/android/app/appsearch/SearchSpec.java b/apex/appsearch/framework/java/external/android/app/appsearch/SearchSpec.java index 68e31f03fb4de..400b6303d12ee 100644 --- a/apex/appsearch/framework/java/external/android/app/appsearch/SearchSpec.java +++ b/apex/appsearch/framework/java/external/android/app/appsearch/SearchSpec.java @@ -23,6 +23,7 @@ import android.annotation.SuppressLint; import android.app.appsearch.exceptions.AppSearchException; import android.app.appsearch.exceptions.IllegalSearchSpecException; import android.os.Bundle; +import android.util.ArrayMap; import com.android.internal.util.Preconditions; @@ -33,6 +34,8 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.Set; /** * This class represents the specification logic for AppSearch. It can be used to set the type of @@ -40,6 +43,15 @@ import java.util.List; */ // TODO(sidchhabra) : AddResultSpec fields for Snippets etc. public final class SearchSpec { + /** + * Schema type to be used in {@link SearchSpec.Builder#addProjectionTypePropertyPath} to apply + * property paths to all results, excepting any types that have had their own, specific property + * paths set. + * + * @hide + */ + public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*"; + static final String TERM_MATCH_TYPE_FIELD = "termMatchType"; static final String SCHEMA_TYPE_FIELD = "schemaType"; static final String NAMESPACE_FIELD = "namespace"; @@ -49,6 +61,7 @@ public final class SearchSpec { static final String SNIPPET_COUNT_FIELD = "snippetCount"; static final String SNIPPET_COUNT_PER_PROPERTY_FIELD = "snippetCountPerProperty"; static final String MAX_SNIPPET_FIELD = "maxSnippet"; + static final String PROJECTION_TYPE_PROPERTY_PATHS_FIELD = "projectionTypeFieldMasks"; /** @hide */ public static final int DEFAULT_NUM_PER_PAGE = 10; @@ -206,12 +219,35 @@ public final class SearchSpec { return mBundle.getInt(MAX_SNIPPET_FIELD); } + /** + * Returns a map from schema type to property paths to be used for projection. + * + *

If the map is empty, then all properties will be retrieved for all results. + * + *

Calling this function repeatedly is inefficient. Prefer to retain the Map returned by this + * function, rather than calling it multiple times. + * + * @hide + */ + @NonNull + public Map> getProjectionTypePropertyPaths() { + Bundle typePropertyPathsBundle = mBundle.getBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD); + Set schemaTypes = typePropertyPathsBundle.keySet(); + Map> typePropertyPathsMap = new ArrayMap<>(schemaTypes.size()); + for (String schemaType : schemaTypes) { + typePropertyPathsMap.put( + schemaType, typePropertyPathsBundle.getStringArrayList(schemaType)); + } + return typePropertyPathsMap; + } + /** Builder for {@link SearchSpec objects}. */ public static final class Builder { private final Bundle mBundle; private final ArrayList mSchemaTypes = new ArrayList<>(); private final ArrayList mNamespaces = new ArrayList<>(); + private final Bundle mProjectionTypePropertyMasks = new Bundle(); private boolean mBuilt = false; /** Creates a new {@link SearchSpec.Builder}. */ @@ -385,6 +421,109 @@ public final class SearchSpec { return this; } + /** + * Adds property paths for the specified type to be used for projection. If property paths + * are added for a type, then only the properties referred to will be retrieved for results + * of that type. If a property path that is specified isn't present in a result, it will be + * ignored for that result. Property paths cannot be null. + * + *

If no property paths are added for a particular type, then all properties of results + * of that type will be retrieved. + * + *

If property path is added for the {@link SearchSpec#PROJECTION_SCHEMA_TYPE_WILDCARD}, + * then those property paths will apply to all results, excepting any types that have their + * own, specific property paths set. + * + *

Suppose the following document is in the index. + * + *

{@code
+         * Email: Document {
+         *   sender: Document {
+         *     name: "Mr. Person"
+         *     email: "mrperson123@google.com"
+         *   }
+         *   recipients: [
+         *     Document {
+         *       name: "John Doe"
+         *       email: "johndoe123@google.com"
+         *     }
+         *     Document {
+         *       name: "Jane Doe"
+         *       email: "janedoe123@google.com"
+         *     }
+         *   ]
+         *   subject: "IMPORTANT"
+         *   body: "Limited time offer!"
+         * }
+         * }
+ * + *

Then, suppose that a query for "important" is issued with the following projection + * type property paths: + * + *

{@code
+         * {schemaType: "Email", ["subject", "sender.name", "recipients.name"]}
+         * }
+ * + *

The above document will be returned as: + * + *

{@code
+         * Email: Document {
+         *   sender: Document {
+         *     name: "Mr. Body"
+         *   }
+         *   recipients: [
+         *     Document {
+         *       name: "John Doe"
+         *     }
+         *     Document {
+         *       name: "Jane Doe"
+         *     }
+         *   ]
+         *   subject: "IMPORTANT"
+         * }
+         * }
+ * + * @hide + */ + @NonNull + public SearchSpec.Builder addProjectionTypePropertyPaths( + @NonNull String schemaType, @NonNull String... propertyPaths) { + Preconditions.checkNotNull(propertyPaths); + return addProjectionTypePropertyPaths(schemaType, Arrays.asList(propertyPaths)); + } + + /** + * Adds property paths for the specified type to be used for projection. If property paths + * are added for a type, then only the properties referred to will be retrieved for results + * of that type. If a property path that is specified isn't present in a result, it will be + * ignored for that result. Property paths cannot be null. + * + *

If no property paths are added for a particular type, then all properties of results + * of that type will be retrieved. + * + *

If property path is added for the {@link SearchSpec#PROJECTION_SCHEMA_TYPE_WILDCARD}, + * then those property paths will apply to all results, excepting any types that have their + * own, specific property paths set. + * + *

{@see SearchSpec.Builder#addProjectionTypePropertyPath(String, String...)} + * + * @hide + */ + @NonNull + public SearchSpec.Builder addProjectionTypePropertyPaths( + @NonNull String schemaType, @NonNull Collection propertyPaths) { + Preconditions.checkState(!mBuilt, "Builder has already been used"); + Preconditions.checkNotNull(schemaType); + Preconditions.checkNotNull(propertyPaths); + ArrayList propertyPathsArrayList = new ArrayList<>(propertyPaths.size()); + for (String propertyPath : propertyPaths) { + Preconditions.checkNotNull(propertyPath); + propertyPathsArrayList.add(propertyPath); + } + mProjectionTypePropertyMasks.putStringArrayList(schemaType, propertyPathsArrayList); + return this; + } + /** * Constructs a new {@link SearchSpec} from the contents of this builder. * @@ -398,6 +537,7 @@ public final class SearchSpec { } mBundle.putStringArrayList(NAMESPACE_FIELD, mNamespaces); mBundle.putStringArrayList(SCHEMA_TYPE_FIELD, mSchemaTypes); + mBundle.putBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD, mProjectionTypePropertyMasks); mBuilt = true; return new SearchSpec(mBundle); } diff --git a/apex/appsearch/synced_jetpack_changeid.txt b/apex/appsearch/synced_jetpack_changeid.txt index f9a0bedf61839..5b818c761b406 100644 --- a/apex/appsearch/synced_jetpack_changeid.txt +++ b/apex/appsearch/synced_jetpack_changeid.txt @@ -1 +1 @@ -I0577839bfddf95a555399df441d317b00c7c7c48 +Idd770a064edfeb6dc648571fc6706c087b8e605a diff --git a/apex/appsearch/testing/java/com/android/server/appsearch/testing/GlobalSearchSessionShimImpl.java b/apex/appsearch/testing/java/com/android/server/appsearch/testing/GlobalSearchSessionShimImpl.java index 67af6b11a656f..9e22bf6ddbbfb 100644 --- a/apex/appsearch/testing/java/com/android/server/appsearch/testing/GlobalSearchSessionShimImpl.java +++ b/apex/appsearch/testing/java/com/android/server/appsearch/testing/GlobalSearchSessionShimImpl.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.app.appsearch.AppSearchManager; import android.app.appsearch.AppSearchResult; import android.app.appsearch.GlobalSearchSession; +import android.app.appsearch.GlobalSearchSessionShim; import android.app.appsearch.SearchResults; import android.app.appsearch.SearchResultsShim; import android.app.appsearch.SearchSpec; @@ -40,7 +41,7 @@ import java.util.concurrent.Executors; * a consistent interface. * @hide */ -public class GlobalSearchSessionShimImpl { +public class GlobalSearchSessionShimImpl implements GlobalSearchSessionShim { private final GlobalSearchSession mGlobalSearchSession; private final ExecutorService mExecutor; @@ -64,6 +65,7 @@ public class GlobalSearchSessionShimImpl { } @NonNull + @Override public SearchResultsShim query( @NonNull String queryExpression, @NonNull SearchSpec searchSpec) { SearchResults searchResults = diff --git a/apex/appsearch/testing/java/com/android/server/appsearch/testing/external/AppSearchTestUtils.java b/apex/appsearch/testing/java/com/android/server/appsearch/testing/external/AppSearchTestUtils.java index 9653def204be4..459fd151ede40 100644 --- a/apex/appsearch/testing/java/com/android/server/appsearch/testing/external/AppSearchTestUtils.java +++ b/apex/appsearch/testing/java/com/android/server/appsearch/testing/external/AppSearchTestUtils.java @@ -17,6 +17,7 @@ package com.android.server.appsearch.testing; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; import android.app.appsearch.AppSearchBatchResult; import android.app.appsearch.AppSearchSessionShim; @@ -25,8 +26,6 @@ import android.app.appsearch.GetByUriRequest; import android.app.appsearch.SearchResult; import android.app.appsearch.SearchResultsShim; -import junit.framework.AssertionFailedError; - import java.util.ArrayList; import java.util.List; import java.util.concurrent.Future; @@ -36,9 +35,9 @@ public class AppSearchTestUtils { public static AppSearchBatchResult checkIsBatchResultSuccess( Future> future) throws Exception { AppSearchBatchResult result = future.get(); - if (!result.isSuccess()) { - throw new AssertionFailedError("AppSearchBatchResult not successful: " + result); - } + assertWithMessage("AppSearchBatchResult not successful: " + result) + .that(result.isSuccess()) + .isTrue(); return result; } diff --git a/core/tests/coretests/src/android/app/appsearch/external/app/SearchSpecTest.java b/core/tests/coretests/src/android/app/appsearch/external/app/SearchSpecTest.java index 9fd480d9e22ae..b3caecc88a9aa 100644 --- a/core/tests/coretests/src/android/app/appsearch/external/app/SearchSpecTest.java +++ b/core/tests/coretests/src/android/app/appsearch/external/app/SearchSpecTest.java @@ -22,6 +22,9 @@ import android.os.Bundle; import org.junit.Test; +import java.util.List; +import java.util.Map; + public class SearchSpecTest { @Test public void testGetBundle() { @@ -53,4 +56,21 @@ public class SearchSpecTest { assertThat(bundle.getInt(SearchSpec.RANKING_STRATEGY_FIELD)) .isEqualTo(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE); } + + @Test + public void testGetProjectionTypePropertyMasks() { + SearchSpec searchSpec = + new SearchSpec.Builder() + .setTermMatch(SearchSpec.TERM_MATCH_PREFIX) + .addProjectionTypePropertyPaths("TypeA", "field1", "field2.subfield2") + .addProjectionTypePropertyPaths("TypeB", "field7") + .addProjectionTypePropertyPaths("TypeC") + .build(); + + Map> typePropertyPathMap = searchSpec.getProjectionTypePropertyPaths(); + assertThat(typePropertyPathMap.keySet()).containsExactly("TypeA", "TypeB", "TypeC"); + assertThat(typePropertyPathMap.get("TypeA")).containsExactly("field1", "field2.subfield2"); + assertThat(typePropertyPathMap.get("TypeB")).containsExactly("field7"); + assertThat(typePropertyPathMap.get("TypeC")).isEmpty(); + } }