diff --git a/apex/appsearch/framework/api/current.txt b/apex/appsearch/framework/api/current.txt index ae32fba443f67..9fa4b8e5a4e7c 100644 --- a/apex/appsearch/framework/api/current.txt +++ b/apex/appsearch/framework/api/current.txt @@ -162,7 +162,7 @@ package android.app.appsearch { public static final class PutDocumentsRequest.Builder { ctor public PutDocumentsRequest.Builder(); method @NonNull public android.app.appsearch.PutDocumentsRequest.Builder addGenericDocument(@NonNull android.app.appsearch.GenericDocument...); - method @NonNull public android.app.appsearch.PutDocumentsRequest.Builder addGenericDocument(@NonNull java.util.Collection); + method @NonNull public android.app.appsearch.PutDocumentsRequest.Builder addGenericDocument(@NonNull java.util.Collection); method @NonNull public android.app.appsearch.PutDocumentsRequest build(); } @@ -182,6 +182,7 @@ package android.app.appsearch { public final class SearchResult { method @NonNull public android.app.appsearch.GenericDocument getDocument(); method @NonNull public java.util.List getMatches(); + method @NonNull public String getPackageName(); } public static final class SearchResult.MatchInfo { @@ -204,9 +205,11 @@ package android.app.appsearch { } public final class SearchSpec { + method @NonNull public java.util.List getFilterPackageNames(); method public int getMaxSnippetSize(); method @NonNull public java.util.List getNamespaces(); method public int getOrder(); + method @NonNull public java.util.Map> getProjections(); method public int getRankingStrategy(); method public int getResultCountPerPage(); method @NonNull public java.util.List getSchemaTypes(); @@ -215,17 +218,23 @@ package android.app.appsearch { method public int getTermMatch(); field public static final int ORDER_ASCENDING = 1; // 0x1 field public static final int ORDER_DESCENDING = 0; // 0x0 + field public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*"; field public static final int RANKING_STRATEGY_CREATION_TIMESTAMP = 2; // 0x2 field public static final int RANKING_STRATEGY_DOCUMENT_SCORE = 1; // 0x1 field public static final int RANKING_STRATEGY_NONE = 0; // 0x0 + field public static final int RANKING_STRATEGY_RELEVANCE_SCORE = 3; // 0x3 field public static final int TERM_MATCH_EXACT_ONLY = 1; // 0x1 field public static final int TERM_MATCH_PREFIX = 2; // 0x2 } public static final class SearchSpec.Builder { ctor public SearchSpec.Builder(); + method @NonNull public android.app.appsearch.SearchSpec.Builder addFilterPackageNames(@NonNull java.lang.String...); + method @NonNull public android.app.appsearch.SearchSpec.Builder addFilterPackageNames(@NonNull java.util.Collection); method @NonNull public android.app.appsearch.SearchSpec.Builder addNamespace(@NonNull java.lang.String...); method @NonNull public android.app.appsearch.SearchSpec.Builder addNamespace(@NonNull java.util.Collection); + method @NonNull public android.app.appsearch.SearchSpec.Builder addProjection(@NonNull String, @NonNull java.lang.String...); + method @NonNull public android.app.appsearch.SearchSpec.Builder addProjection(@NonNull String, @NonNull java.util.Collection); method @NonNull public android.app.appsearch.SearchSpec.Builder addSchemaType(@NonNull java.lang.String...); method @NonNull public android.app.appsearch.SearchSpec.Builder addSchemaType(@NonNull java.util.Collection); method @NonNull public android.app.appsearch.SearchSpec build(); diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java index 685e5ffd59d23..a5b7080fcfba2 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java +++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java @@ -28,6 +28,7 @@ import com.android.internal.infra.AndroidFuture; import com.android.internal.util.Preconditions; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; @@ -234,6 +235,7 @@ public class AppSearchManager { DEFAULT_DATABASE_NAME, schemaBundles, new ArrayList<>(request.getSchemasNotVisibleToSystemUi()), + /*schemasPackageAccessible=*/ Collections.emptyMap(), request.isForceOverride(), mContext.getUserId(), new IAppSearchResultCallback.Stub() { diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchSession.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchSession.java index d628fb5142724..d4872e8151348 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/AppSearchSession.java +++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchSession.java @@ -22,6 +22,7 @@ import android.annotation.UserIdInt; import android.os.Bundle; import android.os.ParcelableException; import android.os.RemoteException; +import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; @@ -161,11 +162,22 @@ public final class AppSearchSession { for (AppSearchSchema schema : request.getSchemas()) { schemaBundles.add(schema.getBundle()); } + Map> schemasPackageAccessibleBundles = + new ArrayMap<>(request.getSchemasVisibleToPackagesInternal().size()); + for (Map.Entry> entry : + request.getSchemasVisibleToPackagesInternal().entrySet()) { + List packageIdentifierBundles = new ArrayList<>(entry.getValue().size()); + for (PackageIdentifier packageIdentifier : entry.getValue()) { + packageIdentifierBundles.add(packageIdentifier.getBundle()); + } + schemasPackageAccessibleBundles.put(entry.getKey(), packageIdentifierBundles); + } try { mService.setSchema( mDatabaseName, schemaBundles, new ArrayList<>(request.getSchemasNotVisibleToSystemUi()), + schemasPackageAccessibleBundles, request.isForceOverride(), mUserId, new IAppSearchResultCallback.Stub() { diff --git a/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl b/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl index af8b6132bb7e3..2b437774b4733 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl +++ b/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl @@ -34,6 +34,8 @@ interface IAppSearchManager { * @param schemaBundles List of {@link AppSearchSchema} bundles. * @param schemasNotPlatformSurfaceable Schema types that should not be surfaced on platform * surfaces. + * @param schemasPackageAccessibleBundles Schema types that are visible to the specified + * packages. The value List contains PackageIdentifier Bundles. * @param forceOverride Whether to apply the new schema even if it is incompatible. All * incompatible documents will be deleted. * @param userId Id of the calling user @@ -44,11 +46,11 @@ interface IAppSearchManager { in String databaseName, in List schemaBundles, in List schemasNotPlatformSurfaceable, + in Map> schemasPackageAccessibleBundles, boolean forceOverride, in int userId, in IAppSearchResultCallback callback); - /** * Retrieves the AppSearch schema for this database. * diff --git a/apex/appsearch/framework/java/external/android/app/appsearch/PackageIdentifier.java b/apex/appsearch/framework/java/external/android/app/appsearch/PackageIdentifier.java index 43be442bd4dc3..bfb9323047c84 100644 --- a/apex/appsearch/framework/java/external/android/app/appsearch/PackageIdentifier.java +++ b/apex/appsearch/framework/java/external/android/app/appsearch/PackageIdentifier.java @@ -17,16 +17,17 @@ package android.app.appsearch; import android.annotation.NonNull; +import android.app.appsearch.util.BundleUtil; +import android.os.Bundle; import com.android.internal.util.Preconditions; -import java.util.Arrays; -import java.util.Objects; - /** This class represents a uniquely identifiable package. */ public class PackageIdentifier { - private final String mPackageName; - private final byte[] mSha256Certificate; + private static final String PACKAGE_NAME_FIELD = "packageName"; + private static final String SHA256_CERTIFICATE_FIELD = "sha256Certificate"; + + private final Bundle mBundle; /** * Creates a unique identifier for a package. @@ -35,18 +36,30 @@ public class PackageIdentifier { * @param sha256Certificate SHA256 certificate digest of the package. */ public PackageIdentifier(@NonNull String packageName, @NonNull byte[] sha256Certificate) { - mPackageName = Preconditions.checkNotNull(packageName); - mSha256Certificate = Preconditions.checkNotNull(sha256Certificate); + mBundle = new Bundle(); + mBundle.putString(PACKAGE_NAME_FIELD, packageName); + mBundle.putByteArray(SHA256_CERTIFICATE_FIELD, sha256Certificate); + } + + /** @hide */ + public PackageIdentifier(@NonNull Bundle bundle) { + mBundle = Preconditions.checkNotNull(bundle); + } + + /** @hide */ + @NonNull + public Bundle getBundle() { + return mBundle; } @NonNull public String getPackageName() { - return mPackageName; + return Preconditions.checkNotNull(mBundle.getString(PACKAGE_NAME_FIELD)); } @NonNull public byte[] getSha256Certificate() { - return mSha256Certificate; + return Preconditions.checkNotNull(mBundle.getByteArray(SHA256_CERTIFICATE_FIELD)); } @Override @@ -58,12 +71,11 @@ public class PackageIdentifier { return false; } final PackageIdentifier other = (PackageIdentifier) obj; - return this.mPackageName.equals(other.mPackageName) - && Arrays.equals(this.mSha256Certificate, other.mSha256Certificate); + return BundleUtil.deepEquals(mBundle, other.mBundle); } @Override public int hashCode() { - return Objects.hash(mPackageName, Arrays.hashCode(mSha256Certificate)); + return BundleUtil.deepHashCode(mBundle); } } diff --git a/apex/appsearch/framework/java/external/android/app/appsearch/PutDocumentsRequest.java b/apex/appsearch/framework/java/external/android/app/appsearch/PutDocumentsRequest.java index b9503eed153c6..0f141d6bc6e26 100644 --- a/apex/appsearch/framework/java/external/android/app/appsearch/PutDocumentsRequest.java +++ b/apex/appsearch/framework/java/external/android/app/appsearch/PutDocumentsRequest.java @@ -30,7 +30,7 @@ import java.util.List; /** * Encapsulates a request to index a document into an {@link AppSearchSession} database. * - * @see AppSearchSession#putDocuments + *

@see AppSearchSession#putDocuments */ public final class PutDocumentsRequest { private final List mDocuments; @@ -45,12 +45,16 @@ public final class PutDocumentsRequest { return Collections.unmodifiableList(mDocuments); } - /** Builder for {@link PutDocumentsRequest} objects. */ + /** + * Builder for {@link PutDocumentsRequest} objects. + * + *

Once {@link #build} is called, the instance can no longer be used. + */ public static final class Builder { private final List mDocuments = new ArrayList<>(); private boolean mBuilt = false; - /** Adds one or more documents to the request. */ + /** Adds one or more {@link GenericDocument} objects to the request. */ @SuppressLint("MissingGetterMatchingBuilder") // Merged list available from getDocuments() @NonNull public Builder addGenericDocument(@NonNull GenericDocument... documents) { @@ -58,17 +62,18 @@ public final class PutDocumentsRequest { return addGenericDocument(Arrays.asList(documents)); } - /** Adds one or more documents to the request. */ + /** Adds a collection of {@link GenericDocument} objects to the request. */ @SuppressLint("MissingGetterMatchingBuilder") // Merged list available from getDocuments() @NonNull - public Builder addGenericDocument(@NonNull Collection documents) { + public Builder addGenericDocument( + @NonNull Collection documents) { Preconditions.checkState(!mBuilt, "Builder has already been used"); Preconditions.checkNotNull(documents); mDocuments.addAll(documents); return this; } - /** Builds a new {@link PutDocumentsRequest}. */ + /** Creates a new {@link PutDocumentsRequest} object. */ @NonNull public PutDocumentsRequest build() { Preconditions.checkState(!mBuilt, "Builder has already been used"); diff --git a/apex/appsearch/framework/java/external/android/app/appsearch/SearchResult.java b/apex/appsearch/framework/java/external/android/app/appsearch/SearchResult.java index eb0b7324a1177..4931cc0264665 100644 --- a/apex/appsearch/framework/java/external/android/app/appsearch/SearchResult.java +++ b/apex/appsearch/framework/java/external/android/app/appsearch/SearchResult.java @@ -109,10 +109,9 @@ public final class SearchResult { } /** - * Contains the package name that stored the {@link GenericDocument}. + * Contains the package name of the app that stored the {@link GenericDocument}. * * @return Package name that stored the document - * @hide */ @NonNull public String getPackageName() { 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 c3f0d8ab53e3c..b5d5f04dfae70 100644 --- a/apex/appsearch/framework/java/external/android/app/appsearch/SearchSpec.java +++ b/apex/appsearch/framework/java/external/android/app/appsearch/SearchSpec.java @@ -19,6 +19,7 @@ package android.app.appsearch; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; +import android.annotation.SuppressLint; import android.app.appsearch.exceptions.IllegalSearchSpecException; import android.os.Bundle; import android.util.ArrayMap; @@ -42,17 +43,15 @@ import java.util.Set; // 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 + * Schema type to be used in {@link SearchSpec.Builder#addProjection} to apply property paths to + * all results, excepting any types that have had their own, specific property paths set. */ 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"; + static final String PACKAGE_NAME_FIELD = "packageName"; static final String NUM_PER_PAGE_FIELD = "numPerPage"; static final String RANKING_STRATEGY_FIELD = "rankingStrategy"; static final String ORDER_FIELD = "order"; @@ -106,7 +105,8 @@ public final class SearchSpec { value = { RANKING_STRATEGY_NONE, RANKING_STRATEGY_DOCUMENT_SCORE, - RANKING_STRATEGY_CREATION_TIMESTAMP + RANKING_STRATEGY_CREATION_TIMESTAMP, + RANKING_STRATEGY_RELEVANCE_SCORE }) @Retention(RetentionPolicy.SOURCE) public @interface RankingStrategy {} @@ -117,6 +117,8 @@ public final class SearchSpec { public static final int RANKING_STRATEGY_DOCUMENT_SCORE = 1; /** Ranked by document creation timestamps. */ public static final int RANKING_STRATEGY_CREATION_TIMESTAMP = 2; + /** Ranked by document relevance score. */ + public static final int RANKING_STRATEGY_RELEVANCE_SCORE = 3; /** * Order for query result. @@ -172,7 +174,7 @@ public final class SearchSpec { } /** - * Returns the list of namespaces to search for. + * Returns the list of namespaces to search over. * *

If empty, the query will search over all namespaces. */ @@ -185,6 +187,40 @@ public final class SearchSpec { return Collections.unmodifiableList(namespaces); } + /** + * Returns the list of package name filters to search over. + * + *

If empty, the query will search over all packages that the caller has access to. If + * package names are specified which caller doesn't have access to, then those package names + * will be ignored. + */ + @NonNull + public List getFilterPackageNames() { + List packageNames = mBundle.getStringArrayList(PACKAGE_NAME_FIELD); + if (packageNames == null) { + return Collections.emptyList(); + } + return Collections.unmodifiableList(packageNames); + } + + /** + * Returns the list of package names to search over. + * + *

If unset, the query will search over all packages that the caller has access to. If + * package names are specified which caller doesn't have access to, then those package names + * will be ignored. + * + * @hide + */ + @NonNull + public List getPackageNames() { + List packageNames = mBundle.getStringArrayList(PACKAGE_NAME_FIELD); + if (packageNames == null) { + return Collections.emptyList(); + } + return Collections.unmodifiableList(packageNames); + } + /** Returns the number of results per page in the result set. */ public int getResultCountPerPage() { return mBundle.getInt(NUM_PER_PAGE_FIELD, DEFAULT_NUM_PER_PAGE); @@ -224,11 +260,9 @@ public final class SearchSpec { * *

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() { + public Map> getProjections() { Bundle typePropertyPathsBundle = mBundle.getBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD); Set schemaTypes = typePropertyPathsBundle.keySet(); Map> typePropertyPathsMap = new ArrayMap<>(schemaTypes.size()); @@ -245,6 +279,7 @@ public final class SearchSpec { private final Bundle mBundle; private final ArrayList mSchemaTypes = new ArrayList<>(); private final ArrayList mNamespaces = new ArrayList<>(); + private final ArrayList mPackageNames = new ArrayList<>(); private final Bundle mProjectionTypePropertyMasks = new Bundle(); private boolean mBuilt = false; @@ -318,6 +353,43 @@ public final class SearchSpec { return this; } + /** + * Adds a package name filter to {@link SearchSpec} Entry. Only search for documents that + * were indexed from the specified packages. + * + *

If unset, the query will search over all packages that the caller has access to. If + * package names are specified which caller doesn't have access to, then those package names + * will be ignored. + */ + // Getter is called "getFilterPackageNames" (as opposed to the suggested + // "getFilterPackageNameses") + @SuppressLint("MissingGetterMatchingBuilder") + @NonNull + public Builder addFilterPackageNames(@NonNull String... packageNames) { + Preconditions.checkNotNull(packageNames); + Preconditions.checkState(!mBuilt, "Builder has already been used"); + return addFilterPackageNames(Arrays.asList(packageNames)); + } + + /** + * Adds a package name filter to {@link SearchSpec} Entry. Only search for documents that + * were indexed from the specified packages. + * + *

If unset, the query will search over all packages that the caller has access to. If + * package names are specified which caller doesn't have access to, then those package names + * will be ignored. + */ + // Getter is called "getFilterPackageNames" (as opposed to the suggested + // "getFilterPackageNameses") + @SuppressLint("MissingGetterMatchingBuilder") + @NonNull + public Builder addFilterPackageNames(@NonNull Collection packageNames) { + Preconditions.checkNotNull(packageNames); + Preconditions.checkState(!mBuilt, "Builder has already been used"); + mPackageNames.addAll(packageNames); + return this; + } + /** * Sets the number of results per page in the returned object. * @@ -339,7 +411,7 @@ public final class SearchSpec { Preconditions.checkArgumentInRange( rankingStrategy, RANKING_STRATEGY_NONE, - RANKING_STRATEGY_CREATION_TIMESTAMP, + RANKING_STRATEGY_RELEVANCE_SCORE, "Result ranking strategy"); mBundle.putInt(RANKING_STRATEGY_FIELD, rankingStrategy); return this; @@ -480,14 +552,12 @@ public final class SearchSpec { * subject: "IMPORTANT" * } * } - * - * @hide */ @NonNull - public SearchSpec.Builder addProjectionTypePropertyPaths( + public SearchSpec.Builder addProjection( @NonNull String schemaType, @NonNull String... propertyPaths) { Preconditions.checkNotNull(propertyPaths); - return addProjectionTypePropertyPaths(schemaType, Arrays.asList(propertyPaths)); + return addProjection(schemaType, Arrays.asList(propertyPaths)); } /** @@ -503,12 +573,10 @@ public final class SearchSpec { * 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 + *

{@see SearchSpec.Builder#addProjection(String, String...)} */ @NonNull - public SearchSpec.Builder addProjectionTypePropertyPaths( + public SearchSpec.Builder addProjection( @NonNull String schemaType, @NonNull Collection propertyPaths) { Preconditions.checkState(!mBuilt, "Builder has already been used"); Preconditions.checkNotNull(schemaType); @@ -535,6 +603,7 @@ public final class SearchSpec { } mBundle.putStringArrayList(NAMESPACE_FIELD, mNamespaces); mBundle.putStringArrayList(SCHEMA_TYPE_FIELD, mSchemaTypes); + mBundle.putStringArrayList(PACKAGE_NAME_FIELD, mPackageNames); mBundle.putBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD, mProjectionTypePropertyMasks); mBuilt = true; return new SearchSpec(mBundle); 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 87c41e53dc63a..1f1e9a1a28a80 100644 --- a/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java +++ b/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java @@ -27,6 +27,7 @@ import android.app.appsearch.GenericDocument; import android.app.appsearch.IAppSearchBatchResultCallback; import android.app.appsearch.IAppSearchManager; import android.app.appsearch.IAppSearchResultCallback; +import android.app.appsearch.PackageIdentifier; import android.app.appsearch.SearchResultPage; import android.app.appsearch.SearchSpec; import android.content.Context; @@ -34,6 +35,7 @@ import android.os.Binder; import android.os.Bundle; import android.os.ParcelableException; import android.os.RemoteException; +import android.util.ArrayMap; import android.util.Log; import com.android.internal.util.Preconditions; @@ -42,6 +44,7 @@ import com.android.server.appsearch.external.localstorage.AppSearchImpl; import java.util.ArrayList; import java.util.List; +import java.util.Map; /** * TODO(b/142567528): add comments when implement this class @@ -64,6 +67,7 @@ public class AppSearchManagerService extends SystemService { @NonNull String databaseName, @NonNull List schemaBundles, @NonNull List schemasNotPlatformSurfaceable, + @NonNull Map> schemasPackageAccessibleBundles, boolean forceOverride, @UserIdInt int userId, @NonNull IAppSearchResultCallback callback) { @@ -78,9 +82,25 @@ public class AppSearchManagerService extends SystemService { for (int i = 0; i < schemaBundles.size(); i++) { schemas.add(new AppSearchSchema(schemaBundles.get(i))); } + Map> schemasPackageAccessible = + new ArrayMap<>(schemasPackageAccessibleBundles.size()); + for (Map.Entry> entry : + schemasPackageAccessibleBundles.entrySet()) { + List packageIdentifiers = + new ArrayList<>(entry.getValue().size()); + for (int i = 0; i < packageIdentifiers.size(); i++) { + packageIdentifiers.add(new PackageIdentifier(entry.getValue().get(i))); + } + schemasPackageAccessible.put(entry.getKey(), packageIdentifiers); + } AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId); String packageName = convertUidToPackageName(callingUid); - impl.setSchema(packageName, databaseName, schemas, schemasNotPlatformSurfaceable, + impl.setSchema( + packageName, + databaseName, + schemas, + schemasNotPlatformSurfaceable, + schemasPackageAccessible, forceOverride); invokeCallbackOnResult(callback, AppSearchResult.newSuccessfulResult(/*result=*/ null)); 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 a2126b19d31ed..b75492629a2a1 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 @@ -21,10 +21,12 @@ import android.annotation.WorkerThread; import android.app.appsearch.AppSearchResult; import android.app.appsearch.AppSearchSchema; import android.app.appsearch.GenericDocument; +import android.app.appsearch.PackageIdentifier; import android.app.appsearch.SearchResultPage; import android.app.appsearch.SearchSpec; import android.app.appsearch.exceptions.AppSearchException; import android.os.Bundle; +import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; @@ -230,6 +232,7 @@ public final class AppSearchImpl { * @param schemas Schemas to set for this app. * @param schemasNotPlatformSurfaceable Schema types that should not be surfaced on platform * surfaces. + * @param schemasPackageAccessible Schema types that are visible to the specified packages. * @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. @@ -239,6 +242,7 @@ public final class AppSearchImpl { @NonNull String databaseName, @NonNull List schemas, @NonNull List schemasNotPlatformSurfaceable, + @NonNull Map> schemasPackageAccessible, boolean forceOverride) throws AppSearchException { mReadWriteLock.writeLock().lock(); @@ -291,7 +295,18 @@ public final class AppSearchImpl { prefixedSchemasNotPlatformSurfaceable.add( prefix + schemasNotPlatformSurfaceable.get(i)); } - mVisibilityStoreLocked.setVisibility(prefix, prefixedSchemasNotPlatformSurfaceable); + + Map> prefixedSchemasPackageAccessible = + new ArrayMap<>(schemasNotPlatformSurfaceable.size()); + for (Map.Entry> entry : + schemasPackageAccessible.entrySet()) { + prefixedSchemasPackageAccessible.put(prefix + entry.getKey(), entry.getValue()); + } + + mVisibilityStoreLocked.setVisibility( + prefix, + prefixedSchemasNotPlatformSurfaceable, + prefixedSchemasPackageAccessible); // Determine whether to schedule an immediate optimize. if (setSchemaResultProto.getDeletedSchemaTypesCount() > 0 @@ -448,6 +463,13 @@ public final class AppSearchImpl { @NonNull String queryExpression, @NonNull SearchSpec searchSpec) throws AppSearchException { + if (!searchSpec.getPackageNames().isEmpty() + && !searchSpec.getPackageNames().contains(packageName)) { + // Client wanted to query over some packages that weren't its own. This isn't + // allowed through local query so we can return early with no results. + return new SearchResultPage(Bundle.EMPTY); + } + mReadWriteLock.readLock().lock(); try { return doQueryLocked( @@ -480,13 +502,25 @@ public final class AppSearchImpl { // verified. mReadWriteLock.readLock().lock(); try { - // We use the mNamespaceMap.keySet here because it's the smaller set of valid prefixes - // that could exist. - Set prefixes = mNamespaceMapLocked.keySet(); + Set prefixes = new ArraySet<>(); + Set packageFilters = new ArraySet<>(searchSpec.getPackageNames()); - // Filter out any VisibilityStore documents which are AppSearch-internal only. - prefixes.remove( - createPrefix(VisibilityStore.PACKAGE_NAME, VisibilityStore.DATABASE_NAME)); + for (String prefix : mNamespaceMapLocked.keySet()) { + if (prefix.equals(VisibilityStore.VISIBILITY_STORE_PREFIX)) { + // Filter out any VisibilityStore documents which are AppSearch-internal only. + continue; + } + + if (!packageFilters.isEmpty() && !packageFilters.contains(getPackageName(prefix))) { + // Client wanted to restrict search over specified packages. Since the + // specified packages don't include this prefix, don't add it to our search + // filters. + continue; + } + + // Otherwise, include this prefix in our global search. + prefixes.add(prefix); + } return doQueryLocked(prefixes, queryExpression, searchSpec); } finally { @@ -500,22 +534,30 @@ public final class AppSearchImpl { @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; - + SearchSpecToProtoConverter.toSearchSpecProto(searchSpec).toBuilder() + .setQuery(queryExpression); // rewriteSearchSpecForPrefixesLocked will return false if none of the prefixes that the // client is trying to search on exist, so we can return an empty SearchResult and skip // sending request to Icing. if (!rewriteSearchSpecForPrefixesLocked(searchSpecBuilder, prefixes)) { return new SearchResultPage(Bundle.EMPTY); } - searchResultProto = - mIcingSearchEngineLocked.search(searchSpecBuilder.build(), scoringSpec, resultSpec); + + ResultSpecProto.Builder resultSpecBuilder = + SearchSpecToProtoConverter.toResultSpecProto(searchSpec).toBuilder(); + + // rewriteResultSpecForPrefixesLocked will return false if none of the prefixes that the + // client is trying to search on exist, so we can return an empty SearchResult and skip + // sending request to Icing. + if (!rewriteResultSpecForPrefixesLocked(resultSpecBuilder, prefixes)) { + return new SearchResultPage(Bundle.EMPTY); + } + + ScoringSpecProto scoringSpec = SearchSpecToProtoConverter.toScoringSpecProto(searchSpec); + SearchResultProto searchResultProto = + mIcingSearchEngineLocked.search( + searchSpecBuilder.build(), scoringSpec, resultSpecBuilder.build()); checkSuccess(searchResultProto.getStatus()); return rewriteSearchResultProto(searchResultProto); @@ -607,6 +649,14 @@ public final class AppSearchImpl { @NonNull String queryExpression, @NonNull SearchSpec searchSpec) throws AppSearchException { + if (!searchSpec.getPackageNames().isEmpty() + && !searchSpec.getPackageNames().contains(packageName)) { + // We're only removing documents within the parameter `packageName`. If we're not + // restricting our remove-query to this package name, then there's nothing for us to + // remove. + return; + } + SearchSpecProto searchSpecProto = SearchSpecToProtoConverter.toSearchSpecProto(searchSpec); SearchSpecProto.Builder searchSpecBuilder = searchSpecProto.toBuilder().setQuery(queryExpression); @@ -915,6 +965,47 @@ public final class AppSearchImpl { return true; } + /** + * Rewrites the typePropertyMasks that exist in {@code prefixes}. + * + *

This method should be only called in query methods and get the READ lock to keep thread + * safety. + * + * @return false if none of the requested prefixes exist. + */ + @VisibleForTesting + @GuardedBy("mReadWriteLock") + boolean rewriteResultSpecForPrefixesLocked( + @NonNull ResultSpecProto.Builder resultSpecBuilder, @NonNull Set prefixes) { + // Create a copy since retainAll() modifies the original set. + Set existingPrefixes = new ArraySet<>(mNamespaceMapLocked.keySet()); + existingPrefixes.retainAll(prefixes); + + if (existingPrefixes.isEmpty()) { + // None of the prefixes exist, empty query. + return false; + } + + List prefixedTypePropertyMasks = new ArrayList<>(); + // Rewrite filters to include a database prefix. + for (String prefix : existingPrefixes) { + Set existingSchemaTypes = mSchemaMapLocked.get(prefix); + // Qualify the given schema types + for (ResultSpecProto.TypePropertyMask typePropertyMask : + resultSpecBuilder.getTypePropertyMasksList()) { + String qualifiedType = prefix + typePropertyMask.getSchemaType(); + if (existingSchemaTypes.contains(qualifiedType)) { + prefixedTypePropertyMasks.add( + typePropertyMask.toBuilder().setSchemaType(qualifiedType).build()); + } + } + } + resultSpecBuilder + .clearTypePropertyMasks() + .addAllTypePropertyMasks(prefixedTypePropertyMasks); + return true; + } + @VisibleForTesting @GuardedBy("mReadWriteLock") SchemaProto getSchemaProtoLocked() throws AppSearchException { diff --git a/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/VisibilityStore.java b/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/VisibilityStore.java index 7e4ebb51c388a..a940ec10b44d7 100644 --- a/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/VisibilityStore.java +++ b/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/VisibilityStore.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.app.appsearch.AppSearchResult; import android.app.appsearch.AppSearchSchema; import android.app.appsearch.GenericDocument; +import android.app.appsearch.PackageIdentifier; import android.app.appsearch.exceptions.AppSearchException; import android.util.ArrayMap; import android.util.ArraySet; @@ -27,19 +28,21 @@ import android.util.ArraySet; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Set; /** - * Manages any visibility settings for all the databases that AppSearchImpl knows about. Persists - * the visibility settings and reloads them on initialization. + * Manages any visibility settings for all the package's databases that AppSearchImpl knows about. + * Persists the visibility settings and reloads them on initialization. * - *

The VisibilityStore creates a document for each database. This document holds the visibility - * settings that apply to that database. The VisibilityStore also creates a schema for these - * documents and has its own database so that its data doesn't interfere with any clients' data. It - * persists the document and schema through AppSearchImpl. + *

The VisibilityStore creates a document for each package's databases. This document holds the + * visibility settings that apply to that package's database. The VisibilityStore also creates a + * schema for these documents and has its own package and database so that its data doesn't + * interfere with any clients' data. It persists the document and schema through AppSearchImpl. * *

These visibility settings are used to ensure AppSearch queries respect the clients' settings * on who their data is visible to. @@ -52,18 +55,31 @@ import java.util.Set; */ class VisibilityStore { /** Schema type for documents that hold AppSearch's metadata, e.g. visibility settings */ - @VisibleForTesting static final String SCHEMA_TYPE = "Visibility"; + @VisibleForTesting static final String VISIBILITY_TYPE = "VisibilityType"; /** * Property that holds the list of platform-hidden schemas, as part of the visibility settings. */ - @VisibleForTesting - static final String NOT_PLATFORM_SURFACEABLE_PROPERTY = "notPlatformSurfaceable"; + private static final String NOT_PLATFORM_SURFACEABLE_PROPERTY = "notPlatformSurfaceable"; - /** Schema for the VisibilityStore's docuemnts. */ - @VisibleForTesting - static final AppSearchSchema SCHEMA = - new AppSearchSchema.Builder(SCHEMA_TYPE) + /** Property that holds nested documents of package accessible schemas. */ + private static final String PACKAGE_ACCESSIBLE_PROPERTY = "packageAccessible"; + + /** Schema type for nested documents that hold package accessible information. */ + private static final String PACKAGE_ACCESSIBLE_TYPE = "PackageAccessibleType"; + + /** Property that holds the package name that can access a schema. */ + private static final String PACKAGE_NAME_PROPERTY = "packageName"; + + /** Property that holds the SHA 256 certificate of the app that can access a schema. */ + private static final String SHA_256_CERT_PROPERTY = "sha256Cert"; + + /** Property that holds the prefixed schema type that is accessible by some package. */ + private static final String ACCESSIBLE_SCHEMA_PROPERTY = "accessibleSchema"; + + /** Schema for the VisibilityStore's documents. */ + private static final AppSearchSchema VISIBILITY_SCHEMA = + new AppSearchSchema.Builder(VISIBILITY_TYPE) .addProperty( new AppSearchSchema.PropertyConfig.Builder( NOT_PLATFORM_SURFACEABLE_PROPERTY) @@ -71,6 +87,39 @@ class VisibilityStore { .setCardinality( AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED) .build()) + .addProperty( + new AppSearchSchema.PropertyConfig.Builder(PACKAGE_ACCESSIBLE_PROPERTY) + .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOCUMENT) + .setSchemaType(PACKAGE_ACCESSIBLE_TYPE) + .setCardinality( + AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED) + .build()) + .build(); + + /** + * Schema for package accessible documents, these will be nested in a top-level visibility + * document. + */ + private static final AppSearchSchema PACKAGE_ACCESSIBLE_SCHEMA = + new AppSearchSchema.Builder(PACKAGE_ACCESSIBLE_TYPE) + .addProperty( + new AppSearchSchema.PropertyConfig.Builder(PACKAGE_NAME_PROPERTY) + .setCardinality( + AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL) + .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING) + .build()) + .addProperty( + new AppSearchSchema.PropertyConfig.Builder(SHA_256_CERT_PROPERTY) + .setCardinality( + AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL) + .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES) + .build()) + .addProperty( + new AppSearchSchema.PropertyConfig.Builder(ACCESSIBLE_SCHEMA_PROPERTY) + .setCardinality( + AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL) + .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING) + .build()) .build(); /** @@ -86,7 +135,7 @@ class VisibilityStore { * database name. Tracked here to tell when we're looking at our own prefix when looking through * AppSearchImpl. */ - private static final String VISIBILITY_STORE_PREFIX = + static final String VISIBILITY_STORE_PREFIX = AppSearchImpl.createPrefix(PACKAGE_NAME, DATABASE_NAME); /** Namespace of documents that contain visibility settings */ @@ -102,9 +151,22 @@ class VisibilityStore { /** * Maps prefixes to the set of schemas that are platform-hidden within that prefix. All schemas * in the map are prefixed. + * + *

Although the prefix key isn't used for lookup, it's helpful in ensuring that all previous + * visibility settings for a prefix are completely overridden by new visibility settings. */ private final Map> mNotPlatformSurfaceableMap = new ArrayMap<>(); + /** + * Maps prefixes to a an internal map. The internal map maps prefixed schemas to the set of + * PackageIdentifiers that have access to that schema. + * + *

Although the prefix key isn't used for lookup, it's helpful in ensuring that all previous + * visibility settings for a prefix are completely overridden by new visibility settings. + */ + private final Map>> mPackageAccessibleMap = + new ArrayMap<>(); + /** * Creates an uninitialized VisibilityStore object. Callers must also call {@link #initialize()} * before using the object. @@ -120,19 +182,22 @@ class VisibilityStore { * *

This is kept separate from the constructor because this will call methods on * AppSearchImpl. Some may even then recursively call back into VisibilityStore (for example, - * {@link AppSearchImpl#setSchema} will call {@link #setVisibility(String, Set)}. We need to - * have both AppSearchImpl and VisibilityStore fully initialized for this call flow to work. + * {@link AppSearchImpl#setSchema} will call {@link #setVisibility}. We need to have both + * AppSearchImpl and VisibilityStore fully initialized for this call flow to work. * * @throws AppSearchException AppSearchException on AppSearchImpl error. */ public void initialize() throws AppSearchException { - if (!mAppSearchImpl.hasSchemaTypeLocked(PACKAGE_NAME, DATABASE_NAME, SCHEMA_TYPE)) { + if (!mAppSearchImpl.hasSchemaTypeLocked(PACKAGE_NAME, DATABASE_NAME, VISIBILITY_TYPE) + || !mAppSearchImpl.hasSchemaTypeLocked( + PACKAGE_NAME, DATABASE_NAME, PACKAGE_ACCESSIBLE_TYPE)) { // Schema type doesn't exist yet. Add it. mAppSearchImpl.setSchema( PACKAGE_NAME, DATABASE_NAME, - Collections.singletonList(SCHEMA), + Arrays.asList(VISIBILITY_SCHEMA, PACKAGE_ACCESSIBLE_SCHEMA), /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), /*forceOverride=*/ false); } @@ -153,9 +218,41 @@ class VisibilityStore { NAMESPACE, /*uri=*/ addUriPrefix(prefix)); + // Update platform visibility settings String[] schemas = document.getPropertyStringArray(NOT_PLATFORM_SURFACEABLE_PROPERTY); - mNotPlatformSurfaceableMap.put(prefix, new ArraySet<>(Arrays.asList(schemas))); + if (schemas != null) { + mNotPlatformSurfaceableMap.put(prefix, new ArraySet<>(Arrays.asList(schemas))); + } + + // Update 3p package visibility settings + Map> schemaToPackageIdentifierMap = new ArrayMap<>(); + GenericDocument[] packageAccessibleDocuments = + document.getPropertyDocumentArray(PACKAGE_ACCESSIBLE_PROPERTY); + if (packageAccessibleDocuments != null) { + for (int i = 0; i < packageAccessibleDocuments.length; i++) { + String packageName = + packageAccessibleDocuments[i].getPropertyString( + PACKAGE_NAME_PROPERTY); + byte[] sha256Cert = + packageAccessibleDocuments[i].getPropertyBytes( + SHA_256_CERT_PROPERTY); + PackageIdentifier packageIdentifier = + new PackageIdentifier(packageName, sha256Cert); + + String prefixedSchema = + packageAccessibleDocuments[i].getPropertyString( + ACCESSIBLE_SCHEMA_PROPERTY); + Set packageIdentifiers = + schemaToPackageIdentifierMap.get(prefixedSchema); + if (packageIdentifiers == null) { + packageIdentifiers = new ArraySet<>(); + } + packageIdentifiers.add(packageIdentifier); + schemaToPackageIdentifierMap.put(prefixedSchema, packageIdentifiers); + } + } + mPackageAccessibleMap.put(prefix, schemaToPackageIdentifierMap); } catch (AppSearchException e) { if (e.getResultCode() == AppSearchResult.RESULT_NOT_FOUND) { // TODO(b/172068212): This indicates some desync error. We were expecting a @@ -176,31 +273,67 @@ class VisibilityStore { * @param prefix Prefix that identifies who owns the {@code schemasNotPlatformSurfaceable}. * @param schemasNotPlatformSurfaceable Set of prefixed schemas that should be hidden from the * platform. + * @param schemasPackageAccessible Map of prefixed schemas to a list of package identifiers that + * have access to the schema. * @throws AppSearchException on AppSearchImpl error. */ public void setVisibility( - @NonNull String prefix, @NonNull Set schemasNotPlatformSurfaceable) + @NonNull String prefix, + @NonNull Set schemasNotPlatformSurfaceable, + @NonNull Map> schemasPackageAccessible) throws AppSearchException { Preconditions.checkNotNull(prefix); Preconditions.checkNotNull(schemasNotPlatformSurfaceable); + Preconditions.checkNotNull(schemasPackageAccessible); // Persist the document GenericDocument.Builder visibilityDocument = - new GenericDocument.Builder(/*uri=*/ addUriPrefix(prefix), SCHEMA_TYPE) + new GenericDocument.Builder(/*uri=*/ addUriPrefix(prefix), VISIBILITY_TYPE) .setNamespace(NAMESPACE); if (!schemasNotPlatformSurfaceable.isEmpty()) { visibilityDocument.setPropertyString( NOT_PLATFORM_SURFACEABLE_PROPERTY, schemasNotPlatformSurfaceable.toArray(new String[0])); } + + Map> schemaToPackageIdentifierMap = new ArrayMap<>(); + List packageAccessibleDocuments = new ArrayList<>(); + for (Map.Entry> entry : + schemasPackageAccessible.entrySet()) { + for (int i = 0; i < entry.getValue().size(); i++) { + // TODO(b/169883602): remove the "placeholder" uri once upstream changes to relax + // nested + // document uri rules gets synced down. + GenericDocument packageAccessibleDocument = + new GenericDocument.Builder(/*uri=*/ "placeholder", PACKAGE_ACCESSIBLE_TYPE) + .setNamespace(NAMESPACE) + .setPropertyString( + PACKAGE_NAME_PROPERTY, + entry.getValue().get(i).getPackageName()) + .setPropertyBytes( + SHA_256_CERT_PROPERTY, + entry.getValue().get(i).getSha256Certificate()) + .setPropertyString(ACCESSIBLE_SCHEMA_PROPERTY, entry.getKey()) + .build(); + packageAccessibleDocuments.add(packageAccessibleDocument); + } + schemaToPackageIdentifierMap.put(entry.getKey(), new ArraySet<>(entry.getValue())); + } + if (!packageAccessibleDocuments.isEmpty()) { + visibilityDocument.setPropertyDocument( + PACKAGE_ACCESSIBLE_PROPERTY, + packageAccessibleDocuments.toArray(new GenericDocument[0])); + } + mAppSearchImpl.putDocument(PACKAGE_NAME, DATABASE_NAME, visibilityDocument.build()); // Update derived data structures. mNotPlatformSurfaceableMap.put(prefix, schemasNotPlatformSurfaceable); + mPackageAccessibleMap.put(prefix, schemaToPackageIdentifierMap); } /** Returns if the schema is surfaceable by the platform. */ - @NonNull + // TODO(b/169883602): check permissions against the allowlisted global querier package name. public boolean isSchemaPlatformSurfaceable( @NonNull String prefix, @NonNull String prefixedSchema) { Preconditions.checkNotNull(prefix); @@ -212,6 +345,31 @@ class VisibilityStore { return !notPlatformSurfaceableSchemas.contains(prefixedSchema); } + /** Returns whether the schema is accessible by {@code accessingPackage}. */ + // TODO(b/169883602): check certificate and package against the incoming querier's uid/package. + public boolean isSchemaPackageAccessible( + @NonNull String prefix, + @NonNull String prefixedSchema, + @NonNull PackageIdentifier accessingPackage) { + Preconditions.checkNotNull(prefix); + Preconditions.checkNotNull(prefixedSchema); + Preconditions.checkNotNull(accessingPackage); + + Map> schemaToPackageIdentifierMap = + mPackageAccessibleMap.get(prefix); + if (schemaToPackageIdentifierMap == null) { + return false; + } + + Set packageIdentifiers = + schemaToPackageIdentifierMap.get(prefixedSchema); + if (packageIdentifiers == null) { + return false; + } + + return packageIdentifiers.contains(accessingPackage); + } + /** * Handles an {@code AppSearchImpl#reset()} by clearing any cached state. * @@ -219,6 +377,7 @@ class VisibilityStore { */ void handleReset() { mNotPlatformSurfaceableMap.clear(); + mPackageAccessibleMap.clear(); } /** diff --git a/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/converter/SearchSpecToProtoConverter.java b/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/converter/SearchSpecToProtoConverter.java index 814ee4f7636aa..073a7f624d896 100644 --- a/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/converter/SearchSpecToProtoConverter.java +++ b/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/converter/SearchSpecToProtoConverter.java @@ -26,6 +26,9 @@ import com.google.android.icing.proto.ScoringSpecProto; import com.google.android.icing.proto.SearchSpecProto; import com.google.android.icing.proto.TermMatchType; +import java.util.List; +import java.util.Map; + /** * Translates a {@link SearchSpec} into icing search protos. * @@ -57,14 +60,22 @@ public final class SearchSpecToProtoConverter { @NonNull public static ResultSpecProto toResultSpecProto(@NonNull SearchSpec spec) { Preconditions.checkNotNull(spec); - return ResultSpecProto.newBuilder() - .setNumPerPage(spec.getResultCountPerPage()) - .setSnippetSpec( - ResultSpecProto.SnippetSpecProto.newBuilder() - .setNumToSnippet(spec.getSnippetCount()) - .setNumMatchesPerProperty(spec.getSnippetCountPerProperty()) - .setMaxWindowBytes(spec.getMaxSnippetSize())) - .build(); + ResultSpecProto.Builder builder = + ResultSpecProto.newBuilder() + .setNumPerPage(spec.getResultCountPerPage()) + .setSnippetSpec( + ResultSpecProto.SnippetSpecProto.newBuilder() + .setNumToSnippet(spec.getSnippetCount()) + .setNumMatchesPerProperty(spec.getSnippetCountPerProperty()) + .setMaxWindowBytes(spec.getMaxSnippetSize())); + Map> projectionTypePropertyPaths = spec.getProjections(); + for (Map.Entry> e : projectionTypePropertyPaths.entrySet()) { + builder.addTypePropertyMasks( + ResultSpecProto.TypePropertyMask.newBuilder() + .setSchemaType(e.getKey()) + .addAllPaths(e.getValue())); + } + return builder.build(); } /** Extracts {@link ScoringSpecProto} information from a {@link SearchSpec}. */ @@ -79,17 +90,28 @@ public final class SearchSpecToProtoConverter { if (orderCodeProto == null) { throw new IllegalArgumentException("Invalid result ranking order: " + orderCode); } - protoBuilder.setOrderBy(orderCodeProto); - - @SearchSpec.RankingStrategy int rankingStrategyCode = spec.getRankingStrategy(); - ScoringSpecProto.RankingStrategy.Code rankingStrategyCodeProto = - ScoringSpecProto.RankingStrategy.Code.forNumber(rankingStrategyCode); - if (rankingStrategyCodeProto == null) { - throw new IllegalArgumentException( - "Invalid result ranking strategy: " + rankingStrategyCode); - } - protoBuilder.setRankBy(rankingStrategyCodeProto); + protoBuilder + .setOrderBy(orderCodeProto) + .setRankBy(toProtoRankingStrategy(spec.getRankingStrategy())); return protoBuilder.build(); } + + private static ScoringSpecProto.RankingStrategy.Code toProtoRankingStrategy( + @SearchSpec.RankingStrategy int rankingStrategyCode) { + switch (rankingStrategyCode) { + case SearchSpec.RANKING_STRATEGY_NONE: + return ScoringSpecProto.RankingStrategy.Code.NONE; + case SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE: + return ScoringSpecProto.RankingStrategy.Code.DOCUMENT_SCORE; + case SearchSpec.RANKING_STRATEGY_CREATION_TIMESTAMP: + return ScoringSpecProto.RankingStrategy.Code.CREATION_TIMESTAMP; + case SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE: + return ScoringSpecProto.RankingStrategy.Code + .RELEVANCE_SCORE_NONFUNCTIONAL_PLACEHOLDER; + default: + throw new IllegalArgumentException( + "Invalid result ranking strategy: " + rankingStrategyCode); + } + } } diff --git a/apex/appsearch/synced_jetpack_changeid.txt b/apex/appsearch/synced_jetpack_changeid.txt index 73f64dc500aeb..f8e5a8aba17be 100644 --- a/apex/appsearch/synced_jetpack_changeid.txt +++ b/apex/appsearch/synced_jetpack_changeid.txt @@ -1 +1 @@ -I8b7425b3f87153547d1c8f5b560be5a54c9be97e +I6745091e5cb97d69ce2e5f85d3d15c073e7e3ef7 diff --git a/core/tests/coretests/src/android/app/appsearch/external/app/PutDocumentsRequestTest.java b/core/tests/coretests/src/android/app/appsearch/external/app/PutDocumentsRequestTest.java new file mode 100644 index 0000000000000..986079f80da68 --- /dev/null +++ b/core/tests/coretests/src/android/app/appsearch/external/app/PutDocumentsRequestTest.java @@ -0,0 +1,42 @@ +/* + * 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 static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableSet; + +import org.junit.Test; + +import java.util.Set; + +public class PutDocumentsRequestTest { + + @Test + public void addGenericDocument_byCollection() { + Set emails = + ImmutableSet.of( + new AppSearchEmail.Builder("test1").build(), + new AppSearchEmail.Builder("test2").build()); + PutDocumentsRequest request = + new PutDocumentsRequest.Builder().addGenericDocument(emails).build(); + + assertThat(request.getDocuments().get(0).getUri()).isEqualTo("test1"); + assertThat(request.getDocuments().get(1).getUri()).isEqualTo("test2"); + } +} 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 b3caecc88a9aa..c8cee853acd40 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 @@ -16,6 +16,7 @@ package android.app.appsearch; + import static com.google.common.truth.Truth.assertThat; import android.os.Bundle; @@ -26,6 +27,7 @@ import java.util.List; import java.util.Map; public class SearchSpecTest { + @Test public void testGetBundle() { SearchSpec searchSpec = @@ -33,6 +35,7 @@ public class SearchSpecTest { .setTermMatch(SearchSpec.TERM_MATCH_PREFIX) .addNamespace("namespace1", "namespace2") .addSchemaType("schemaTypes1", "schemaTypes2") + .addFilterPackageNames("package1", "package2") .setSnippetCount(5) .setSnippetCountPerProperty(10) .setMaxSnippetSize(15) @@ -48,6 +51,8 @@ public class SearchSpecTest { .containsExactly("namespace1", "namespace2"); assertThat(bundle.getStringArrayList(SearchSpec.SCHEMA_TYPE_FIELD)) .containsExactly("schemaTypes1", "schemaTypes2"); + assertThat(bundle.getStringArrayList(SearchSpec.PACKAGE_NAME_FIELD)) + .containsExactly("package1", "package2"); assertThat(bundle.getInt(SearchSpec.SNIPPET_COUNT_FIELD)).isEqualTo(5); assertThat(bundle.getInt(SearchSpec.SNIPPET_COUNT_PER_PROPERTY_FIELD)).isEqualTo(10); assertThat(bundle.getInt(SearchSpec.MAX_SNIPPET_FIELD)).isEqualTo(15); @@ -62,15 +67,26 @@ public class SearchSpecTest { SearchSpec searchSpec = new SearchSpec.Builder() .setTermMatch(SearchSpec.TERM_MATCH_PREFIX) - .addProjectionTypePropertyPaths("TypeA", "field1", "field2.subfield2") - .addProjectionTypePropertyPaths("TypeB", "field7") - .addProjectionTypePropertyPaths("TypeC") + .addProjection("TypeA", "field1", "field2.subfield2") + .addProjection("TypeB", "field7") + .addProjection("TypeC") .build(); - Map> typePropertyPathMap = searchSpec.getProjectionTypePropertyPaths(); + Map> typePropertyPathMap = searchSpec.getProjections(); 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(); } + + @Test + public void testGetRankingStrategy() { + SearchSpec searchSpec = + new SearchSpec.Builder() + .setTermMatch(SearchSpec.TERM_MATCH_PREFIX) + .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE) + .build(); + assertThat(searchSpec.getRankingStrategy()) + .isEqualTo(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE); + } } 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 c6fde87f59535..009cb0b363e40 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 @@ -22,13 +22,13 @@ import static org.testng.Assert.expectThrows; import android.app.appsearch.AppSearchSchema; import android.app.appsearch.GenericDocument; +import android.app.appsearch.PackageIdentifier; import android.app.appsearch.SearchResult; import android.app.appsearch.SearchResultPage; import android.app.appsearch.SearchSpec; import android.app.appsearch.exceptions.AppSearchException; import com.android.server.appsearch.external.localstorage.converter.GenericDocumentToProtoConverter; -import com.android.server.appsearch.external.localstorage.converter.SchemaToProtoConverter; import com.android.server.appsearch.proto.DocumentProto; import com.android.server.appsearch.proto.GetOptimizeInfoResultProto; import com.android.server.appsearch.proto.PropertyConfigProto; @@ -41,6 +41,7 @@ import com.android.server.appsearch.proto.StringIndexingConfig; import com.android.server.appsearch.proto.TermMatchType; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import org.junit.Before; @@ -55,27 +56,10 @@ import java.util.List; public class AppSearchImplTest { @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); private AppSearchImpl mAppSearchImpl; - private SchemaTypeConfigProto mVisibilitySchemaProto; @Before public void setUp() throws Exception { mAppSearchImpl = AppSearchImpl.create(mTemporaryFolder.newFolder()); - - AppSearchSchema visibilitySchema = VisibilityStore.SCHEMA; - - // We need to rewrite the schema type to follow AppSearchImpl's prefixing scheme. - AppSearchSchema.Builder rewrittenVisibilitySchema = - new AppSearchSchema.Builder( - AppSearchImpl.createPrefix( - VisibilityStore.PACKAGE_NAME, VisibilityStore.DATABASE_NAME) - + VisibilityStore.SCHEMA_TYPE); - List visibilityProperties = - visibilitySchema.getProperties(); - for (AppSearchSchema.PropertyConfig property : visibilityProperties) { - rewrittenVisibilitySchema.addProperty(property); - } - mVisibilitySchemaProto = - SchemaToProtoConverter.toSchemaTypeConfigProto(rewrittenVisibilitySchema.build()); } // TODO(b/175430168) add test to verify reset is working properly. @@ -407,6 +391,7 @@ public class AppSearchImplTest { "database", schemas, /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), /*forceOverride=*/ false); // Insert enough documents. @@ -464,6 +449,7 @@ public class AppSearchImplTest { "database", schemas, /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), /*forceOverride=*/ false); // Insert document @@ -495,12 +481,14 @@ public class AppSearchImplTest { "database1", schemas, /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), /*forceOverride=*/ false); mAppSearchImpl.setSchema( "package", "database2", schemas, /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), /*forceOverride=*/ false); // Insert documents @@ -537,6 +525,117 @@ public class AppSearchImplTest { assertThat(searchResultPage.getResults()).isEmpty(); } + /** + * TODO(b/169883602): This should be an integration test at the cts-level. This is a short-term + * test until we have official support for multiple-apps indexing at once. + */ + @Test + public void testQueryWithMultiplePackages_noPackageFilters() throws Exception { + // Insert package1 schema + List schema1 = + ImmutableList.of(new AppSearchSchema.Builder("schema1").build()); + mAppSearchImpl.setSchema( + "package1", + "database1", + schema1, + /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), + /*forceOverride=*/ false); + + // Insert package2 schema + List schema2 = + ImmutableList.of(new AppSearchSchema.Builder("schema2").build()); + mAppSearchImpl.setSchema( + "package2", + "database2", + schema2, + /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), + /*forceOverride=*/ false); + + // Insert package1 document + GenericDocument document = + new GenericDocument.Builder<>("uri", "schema1").setNamespace("namespace").build(); + mAppSearchImpl.putDocument("package1", "database1", document); + + // No query filters specified, package2 shouldn't be able to query for package1's documents. + SearchSpec searchSpec = + new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE).build(); + SearchResultPage searchResultPage = + mAppSearchImpl.query("package2", "database2", "", searchSpec); + assertThat(searchResultPage.getResults()).isEmpty(); + + // Insert package2 document + document = + new GenericDocument.Builder<>("uri", "schema2").setNamespace("namespace").build(); + mAppSearchImpl.putDocument("package2", "database2", document); + + // No query filters specified. package2 should only get its own documents back. + searchResultPage = mAppSearchImpl.query("package2", "database2", "", searchSpec); + assertThat(searchResultPage.getResults()).hasSize(1); + assertThat(searchResultPage.getResults().get(0).getDocument()).isEqualTo(document); + } + + /** + * TODO(b/169883602): This should be an integration test at the cts-level. This is a short-term + * test until we have official support for multiple-apps indexing at once. + */ + @Test + public void testQueryWithMultiplePackages_withPackageFilters() throws Exception { + // Insert package1 schema + List schema1 = + ImmutableList.of(new AppSearchSchema.Builder("schema1").build()); + mAppSearchImpl.setSchema( + "package1", + "database1", + schema1, + /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), + /*forceOverride=*/ false); + + // Insert package2 schema + List schema2 = + ImmutableList.of(new AppSearchSchema.Builder("schema2").build()); + mAppSearchImpl.setSchema( + "package2", + "database2", + schema2, + /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), + /*forceOverride=*/ false); + + // Insert package1 document + GenericDocument document = + new GenericDocument.Builder<>("uri", "schema1").setNamespace("namespace").build(); + mAppSearchImpl.putDocument("package1", "database1", document); + + // "package1" filter specified, but package2 shouldn't be able to query for package1's + // documents. + SearchSpec searchSpec = + new SearchSpec.Builder() + .setTermMatch(TermMatchType.Code.PREFIX_VALUE) + .addFilterPackageNames("package1") + .build(); + SearchResultPage searchResultPage = + mAppSearchImpl.query("package2", "database2", "", searchSpec); + assertThat(searchResultPage.getResults()).isEmpty(); + + // Insert package2 document + document = + new GenericDocument.Builder<>("uri", "schema2").setNamespace("namespace").build(); + mAppSearchImpl.putDocument("package2", "database2", document); + + // "package2" filter specified, package2 should only get its own documents back. + searchSpec = + new SearchSpec.Builder() + .setTermMatch(TermMatchType.Code.PREFIX_VALUE) + .addFilterPackageNames("package2") + .build(); + searchResultPage = mAppSearchImpl.query("package2", "database2", "", searchSpec); + assertThat(searchResultPage.getResults()).hasSize(1); + assertThat(searchResultPage.getResults().get(0).getDocument()).isEqualTo(document); + } + @Test public void testGlobalQueryEmptyDatabase() throws Exception { SearchSpec searchSpec = @@ -545,6 +644,115 @@ public class AppSearchImplTest { assertThat(searchResultPage.getResults()).isEmpty(); } + /** + * TODO(b/169883602): This should be an integration test at the cts-level. This is a short-term + * test until we have official support for multiple-apps indexing at once. + */ + @Test + public void testGlobalQueryWithMultiplePackages_noPackageFilters() throws Exception { + // Insert package1 schema + List schema1 = + ImmutableList.of(new AppSearchSchema.Builder("schema1").build()); + mAppSearchImpl.setSchema( + "package1", + "database1", + schema1, + /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), + /*forceOverride=*/ false); + + // Insert package2 schema + List schema2 = + ImmutableList.of(new AppSearchSchema.Builder("schema2").build()); + mAppSearchImpl.setSchema( + "package2", + "database2", + schema2, + /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), + /*forceOverride=*/ false); + + // Insert package1 document + GenericDocument document1 = + new GenericDocument.Builder<>("uri", "schema1").setNamespace("namespace").build(); + mAppSearchImpl.putDocument("package1", "database1", document1); + + // Insert package2 document + GenericDocument document2 = + new GenericDocument.Builder<>("uri", "schema2").setNamespace("namespace").build(); + mAppSearchImpl.putDocument("package2", "database2", document2); + + // No query filters specified, global query can retrieve all documents. + SearchSpec searchSpec = + new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE).build(); + SearchResultPage searchResultPage = mAppSearchImpl.globalQuery("", searchSpec); + assertThat(searchResultPage.getResults()).hasSize(2); + + // Document2 will be first since it got indexed later and has a "better", aka more recent + // score. + assertThat(searchResultPage.getResults().get(0).getDocument()).isEqualTo(document2); + assertThat(searchResultPage.getResults().get(1).getDocument()).isEqualTo(document1); + } + + /** + * TODO(b/169883602): This should be an integration test at the cts-level. This is a short-term + * test until we have official support for multiple-apps indexing at once. + */ + @Test + public void testGlobalQueryWithMultiplePackages_withPackageFilters() throws Exception { + // Insert package1 schema + List schema1 = + ImmutableList.of(new AppSearchSchema.Builder("schema1").build()); + mAppSearchImpl.setSchema( + "package1", + "database1", + schema1, + /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), + /*forceOverride=*/ false); + + // Insert package2 schema + List schema2 = + ImmutableList.of(new AppSearchSchema.Builder("schema2").build()); + mAppSearchImpl.setSchema( + "package2", + "database2", + schema2, + /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), + /*forceOverride=*/ false); + + // Insert package1 document + GenericDocument document1 = + new GenericDocument.Builder<>("uri", "schema1").setNamespace("namespace").build(); + mAppSearchImpl.putDocument("package1", "database1", document1); + + // Insert package2 document + GenericDocument document2 = + new GenericDocument.Builder<>("uri", "schema2").setNamespace("namespace").build(); + mAppSearchImpl.putDocument("package2", "database2", document2); + + // "package1" filter specified + SearchSpec searchSpec = + new SearchSpec.Builder() + .setTermMatch(TermMatchType.Code.PREFIX_VALUE) + .addFilterPackageNames("package1") + .build(); + SearchResultPage searchResultPage = mAppSearchImpl.globalQuery("", searchSpec); + assertThat(searchResultPage.getResults()).hasSize(1); + assertThat(searchResultPage.getResults().get(0).getDocument()).isEqualTo(document1); + + // "package2" filter specified + searchSpec = + new SearchSpec.Builder() + .setTermMatch(TermMatchType.Code.PREFIX_VALUE) + .addFilterPackageNames("package2") + .build(); + searchResultPage = mAppSearchImpl.globalQuery("", searchSpec); + assertThat(searchResultPage.getResults()).hasSize(1); + assertThat(searchResultPage.getResults().get(0).getDocument()).isEqualTo(document2); + } + @Test public void testRemoveEmptyDatabase_noExceptionThrown() throws Exception { SearchSpec searchSpec = @@ -567,6 +775,9 @@ public class AppSearchImplTest { @Test public void testSetSchema() throws Exception { + List existingSchemas = + mAppSearchImpl.getSchemaProtoLocked().getTypesList(); + List schemas = Collections.singletonList(new AppSearchSchema.Builder("Email").build()); // Set schema Email to AppSearch database1 @@ -575,6 +786,7 @@ public class AppSearchImplTest { "database1", schemas, /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), /*forceOverride=*/ false); // Create expected schemaType proto. @@ -586,7 +798,7 @@ public class AppSearchImplTest { .build(); List expectedTypes = new ArrayList<>(); - expectedTypes.add(mVisibilitySchemaProto); + expectedTypes.addAll(existingSchemas); expectedTypes.addAll(expectedProto.getTypesList()); assertThat(mAppSearchImpl.getSchemaProtoLocked().getTypesList()) .containsExactlyElementsIn(expectedTypes); @@ -594,20 +806,30 @@ public class AppSearchImplTest { @Test public void testSetSchema_existingSchemaRetainsVisibilitySetting() throws Exception { + PackageIdentifier package1 = + new PackageIdentifier("package1", /*sha256Certificate=*/ new byte[] {100}); + String prefix = AppSearchImpl.createPrefix("package", "database"); mAppSearchImpl.setSchema( "package", "database", Collections.singletonList(new AppSearchSchema.Builder("schema1").build()), /*schemasNotPlatformSurfaceable=*/ Collections.singletonList("schema1"), + /*schemasPackageAccessible=*/ ImmutableMap.of( + "schema1", ImmutableList.of(package1)), /*forceOverride=*/ false); - // "schema1" is platform hidden now + // "schema1" is platform hidden now and package visible to package1 assertThat( mAppSearchImpl .getVisibilityStoreLocked() .isSchemaPlatformSurfaceable(prefix, prefix + "schema1")) .isFalse(); + assertThat( + mAppSearchImpl + .getVisibilityStoreLocked() + .isSchemaPackageAccessible(prefix, prefix + "schema1", package1)) + .isTrue(); // Add a new schema, and include the already-existing "schema1" mAppSearchImpl.setSchema( @@ -617,24 +839,40 @@ public class AppSearchImplTest { new AppSearchSchema.Builder("schema1").build(), new AppSearchSchema.Builder("schema2").build()), /*schemasNotPlatformSurfaceable=*/ Collections.singletonList("schema1"), + /*schemasPackageAccessible=*/ ImmutableMap.of( + "schema1", ImmutableList.of(package1)), /*forceOverride=*/ false); - // Check that "schema1" is still platform hidden, but "schema2" is the default platform - // visible. + // Check that "schema1" still has the same visibility settings assertThat( mAppSearchImpl .getVisibilityStoreLocked() .isSchemaPlatformSurfaceable(prefix, prefix + "schema1")) .isFalse(); + assertThat( + mAppSearchImpl + .getVisibilityStoreLocked() + .isSchemaPackageAccessible(prefix, prefix + "schema1", package1)) + .isTrue(); + + // "schema2" has default visibility settings assertThat( mAppSearchImpl .getVisibilityStoreLocked() .isSchemaPlatformSurfaceable(prefix, prefix + "schema2")) .isTrue(); + assertThat( + mAppSearchImpl + .getVisibilityStoreLocked() + .isSchemaPackageAccessible(prefix, prefix + "schema2", package1)) + .isFalse(); } @Test public void testRemoveSchema() throws Exception { + List existingSchemas = + mAppSearchImpl.getSchemaProtoLocked().getTypesList(); + List schemas = ImmutableList.of( new AppSearchSchema.Builder("Email").build(), @@ -645,6 +883,7 @@ public class AppSearchImplTest { "database1", schemas, /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), /*forceOverride=*/ false); // Create expected schemaType proto. @@ -660,7 +899,7 @@ public class AppSearchImplTest { // Check both schema Email and Document saved correctly. List expectedTypes = new ArrayList<>(); - expectedTypes.add(mVisibilitySchemaProto); + expectedTypes.addAll(existingSchemas); expectedTypes.addAll(expectedProto.getTypesList()); assertThat(mAppSearchImpl.getSchemaProtoLocked().getTypesList()) .containsExactlyElementsIn(expectedTypes); @@ -677,6 +916,7 @@ public class AppSearchImplTest { "database1", finalSchemas, /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), /*forceOverride=*/ false)); assertThat(e).hasMessageThat().contains("Schema is incompatible"); assertThat(e).hasMessageThat().contains("Deleted types: [package$database1/Document]"); @@ -687,6 +927,7 @@ public class AppSearchImplTest { "database1", finalSchemas, /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), /*forceOverride=*/ true); // Check Document schema is removed. @@ -698,7 +939,7 @@ public class AppSearchImplTest { .build(); expectedTypes = new ArrayList<>(); - expectedTypes.add(mVisibilitySchemaProto); + expectedTypes.addAll(existingSchemas); expectedTypes.addAll(expectedProto.getTypesList()); assertThat(mAppSearchImpl.getSchemaProtoLocked().getTypesList()) .containsExactlyElementsIn(expectedTypes); @@ -706,6 +947,9 @@ public class AppSearchImplTest { @Test public void testRemoveSchema_differentDataBase() throws Exception { + List existingSchemas = + mAppSearchImpl.getSchemaProtoLocked().getTypesList(); + // Create schemas List schemas = ImmutableList.of( @@ -718,12 +962,14 @@ public class AppSearchImplTest { "database1", schemas, /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), /*forceOverride=*/ false); mAppSearchImpl.setSchema( "package", "database2", schemas, /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), /*forceOverride=*/ false); // Create expected schemaType proto. @@ -745,7 +991,7 @@ public class AppSearchImplTest { // Check Email and Document is saved in database 1 and 2 correctly. List expectedTypes = new ArrayList<>(); - expectedTypes.add(mVisibilitySchemaProto); + expectedTypes.addAll(existingSchemas); expectedTypes.addAll(expectedProto.getTypesList()); assertThat(mAppSearchImpl.getSchemaProtoLocked().getTypesList()) .containsExactlyElementsIn(expectedTypes); @@ -757,6 +1003,7 @@ public class AppSearchImplTest { "database1", schemas, /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), /*forceOverride=*/ true); // Create expected schemaType list, database 1 should only contain Email but database 2 @@ -776,7 +1023,7 @@ public class AppSearchImplTest { // Check nothing changed in database2. expectedTypes = new ArrayList<>(); - expectedTypes.add(mVisibilitySchemaProto); + expectedTypes.addAll(existingSchemas); expectedTypes.addAll(expectedProto.getTypesList()); assertThat(mAppSearchImpl.getSchemaProtoLocked().getTypesList()) .containsExactlyElementsIn(expectedTypes); @@ -784,49 +1031,71 @@ public class AppSearchImplTest { @Test public void testRemoveSchema_removedFromVisibilityStore() throws Exception { + PackageIdentifier package1 = + new PackageIdentifier("package1", /*sha256Certificate=*/ new byte[] {100}); + String prefix = AppSearchImpl.createPrefix("package", "database"); mAppSearchImpl.setSchema( "package", "database", Collections.singletonList(new AppSearchSchema.Builder("schema1").build()), /*schemasNotPlatformSurfaceable=*/ Collections.singletonList("schema1"), + /*schemasPackageAccessible=*/ ImmutableMap.of( + "schema1", ImmutableList.of(package1)), /*forceOverride=*/ false); - // "schema1" is platform hidden now + // "schema1" is platform hidden now and package accessible assertThat( mAppSearchImpl .getVisibilityStoreLocked() .isSchemaPlatformSurfaceable(prefix, prefix + "schema1")) .isFalse(); + assertThat( + mAppSearchImpl + .getVisibilityStoreLocked() + .isSchemaPackageAccessible(prefix, prefix + "schema1", package1)) + .isTrue(); // Remove "schema1" by force overriding mAppSearchImpl.setSchema( "package", "database", - Collections.emptyList(), + /*schemas=*/ Collections.emptyList(), /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), /*forceOverride=*/ true); - // Check that "schema1" is no longer considered platform hidden + // Check that "schema1" is no longer considered platform hidden or package accessible assertThat( mAppSearchImpl .getVisibilityStoreLocked() .isSchemaPlatformSurfaceable(prefix, prefix + "schema1")) .isTrue(); + assertThat( + mAppSearchImpl + .getVisibilityStoreLocked() + .isSchemaPackageAccessible(prefix, prefix + "schema1", package1)) + .isFalse(); // Add "schema1" back, it gets default visibility settings which means it's not platform - // hidden. + // hidden and not package accessible mAppSearchImpl.setSchema( "package", "database", Collections.singletonList(new AppSearchSchema.Builder("schema1").build()), /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), /*forceOverride=*/ false); assertThat( mAppSearchImpl .getVisibilityStoreLocked() .isSchemaPlatformSurfaceable(prefix, prefix + "schema1")) .isTrue(); + assertThat( + mAppSearchImpl + .getVisibilityStoreLocked() + .isSchemaPackageAccessible(prefix, prefix + "schema1", package1)) + .isFalse(); } @Test @@ -837,6 +1106,7 @@ public class AppSearchImplTest { "database", Collections.singletonList(new AppSearchSchema.Builder("Schema").build()), /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), /*forceOverride=*/ false); assertThat( mAppSearchImpl @@ -853,6 +1123,7 @@ public class AppSearchImplTest { "database", Collections.singletonList(new AppSearchSchema.Builder("Schema").build()), /*schemasNotPlatformSurfaceable=*/ Collections.singletonList("Schema"), + /*schemasPackageAccessible=*/ Collections.emptyMap(), /*forceOverride=*/ false); assertThat( mAppSearchImpl @@ -861,6 +1132,46 @@ public class AppSearchImplTest { .isFalse(); } + @Test + public void testSetSchema_defaultNotPackageAccessible() throws Exception { + PackageIdentifier package1 = + new PackageIdentifier("package1", /*sha256Certificate=*/ new byte[] {100}); + + String prefix = AppSearchImpl.createPrefix("package", "database"); + mAppSearchImpl.setSchema( + "package", + "database", + Collections.singletonList(new AppSearchSchema.Builder("Schema").build()), + /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), + /*forceOverride=*/ false); + assertThat( + mAppSearchImpl + .getVisibilityStoreLocked() + .isSchemaPackageAccessible(prefix, prefix + "Schema", package1)) + .isFalse(); + } + + @Test + public void testSetSchema_packageAccessible() throws Exception { + PackageIdentifier package1 = + new PackageIdentifier("package1", /*sha256Certificate=*/ new byte[] {100}); + + String prefix = AppSearchImpl.createPrefix("package", "database"); + mAppSearchImpl.setSchema( + "package", + "database", + Collections.singletonList(new AppSearchSchema.Builder("Schema").build()), + /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ ImmutableMap.of("Schema", ImmutableList.of(package1)), + /*forceOverride=*/ false); + assertThat( + mAppSearchImpl + .getVisibilityStoreLocked() + .isSchemaPackageAccessible(prefix, prefix + "Schema", package1)) + .isTrue(); + } + @Test public void testHasSchemaType() throws Exception { // Nothing exists yet @@ -871,6 +1182,7 @@ public class AppSearchImplTest { "database", Collections.singletonList(new AppSearchSchema.Builder("Schema").build()), /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), /*forceOverride=*/ false); assertThat(mAppSearchImpl.hasSchemaTypeLocked("package", "database", "Schema")).isTrue(); @@ -892,6 +1204,7 @@ public class AppSearchImplTest { "database1", Collections.singletonList(new AppSearchSchema.Builder("schema").build()), /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), /*forceOverride=*/ false); assertThat(mAppSearchImpl.getPrefixesLocked()) .containsExactly( @@ -905,6 +1218,7 @@ public class AppSearchImplTest { "database2", Collections.singletonList(new AppSearchSchema.Builder("schema").build()), /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(), + /*schemasPackageAccessible=*/ Collections.emptyMap(), /*forceOverride=*/ false); assertThat(mAppSearchImpl.getPrefixesLocked()) .containsExactly( diff --git a/services/tests/servicestests/src/com/android/server/appsearch/external/localstorage/VisibilityStoreTest.java b/services/tests/servicestests/src/com/android/server/appsearch/external/localstorage/VisibilityStoreTest.java index 415c1f59e2fbb..e491ac3272804 100644 --- a/services/tests/servicestests/src/com/android/server/appsearch/external/localstorage/VisibilityStoreTest.java +++ b/services/tests/servicestests/src/com/android/server/appsearch/external/localstorage/VisibilityStoreTest.java @@ -18,6 +18,10 @@ package com.android.server.appsearch.external.localstorage; import static com.google.common.truth.Truth.assertThat; +import android.app.appsearch.PackageIdentifier; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import org.junit.Before; @@ -70,11 +74,12 @@ public class VisibilityStoreTest { } @Test - public void testSetVisibility() throws Exception { + public void testSetVisibility_platformSurfaceable() throws Exception { mVisibilityStore.setVisibility( "prefix", /*schemasNotPlatformSurfaceable=*/ ImmutableSet.of( - "prefix/schema1", "prefix/schema2")); + "prefix/schema1", "prefix/schema2"), + /*schemasPackageAccessible=*/ Collections.emptyMap()); assertThat(mVisibilityStore.isSchemaPlatformSurfaceable("prefix", "prefix/schema1")) .isFalse(); assertThat(mVisibilityStore.isSchemaPlatformSurfaceable("prefix", "prefix/schema2")) @@ -85,7 +90,8 @@ public class VisibilityStoreTest { mVisibilityStore.setVisibility( "prefix", /*schemasNotPlatformSurfaceable=*/ ImmutableSet.of( - "prefix/schema1", "prefix/schema3")); + "prefix/schema1", "prefix/schema3"), + /*schemasPackageAccessible=*/ Collections.emptyMap()); assertThat(mVisibilityStore.isSchemaPlatformSurfaceable("prefix", "prefix/schema1")) .isFalse(); assertThat(mVisibilityStore.isSchemaPlatformSurfaceable("prefix", "prefix/schema2")) @@ -94,7 +100,9 @@ public class VisibilityStoreTest { .isFalse(); mVisibilityStore.setVisibility( - "prefix", /*schemasNotPlatformSurfaceable=*/ Collections.emptySet()); + "prefix", + /*schemasNotPlatformSurfaceable=*/ Collections.emptySet(), + /*schemasPackageAccessible=*/ Collections.emptyMap()); assertThat(mVisibilityStore.isSchemaPlatformSurfaceable("prefix", "prefix/schema1")) .isTrue(); assertThat(mVisibilityStore.isSchemaPlatformSurfaceable("prefix", "prefix/schema2")) @@ -103,14 +111,73 @@ public class VisibilityStoreTest { .isTrue(); } + @Test + public void testSetVisibility_packageAccessible() throws Exception { + PackageIdentifier package1 = + new PackageIdentifier("package1", /*sha256Certificate=*/ new byte[] {100}); + PackageIdentifier package2 = + new PackageIdentifier("package2", /*sha256Certificate=*/ new byte[] {100}); + PackageIdentifier package3 = + new PackageIdentifier("package3", /*sha256Certificate=*/ new byte[] {100}); + + mVisibilityStore.setVisibility( + "prefix", + /*schemasNotPlatformSurfaceable=*/ Collections.emptySet(), + /*schemasPackageAccessible=*/ ImmutableMap.of( + "prefix/schema1", ImmutableList.of(package1), + "prefix/schema2", ImmutableList.of(package2))); + assertThat(mVisibilityStore.isSchemaPackageAccessible("prefix", "prefix/schema1", package1)) + .isTrue(); + assertThat(mVisibilityStore.isSchemaPackageAccessible("prefix", "prefix/schema2", package2)) + .isTrue(); + + // New .setVisibility() call completely overrides previous visibility settings. So + // "schema2" isn't preserved. + mVisibilityStore.setVisibility( + "prefix", + /*schemasNotPlatformSurfaceable=*/ Collections.emptySet(), + /*schemasPackageAccessible=*/ ImmutableMap.of( + "prefix/schema1", ImmutableList.of(package1), + "prefix/schema3", ImmutableList.of(package3))); + assertThat(mVisibilityStore.isSchemaPackageAccessible("prefix", "prefix/schema1", package1)) + .isTrue(); + assertThat(mVisibilityStore.isSchemaPackageAccessible("prefix", "prefix/schema2", package2)) + .isFalse(); + assertThat(mVisibilityStore.isSchemaPackageAccessible("prefix", "prefix/schema3", package3)) + .isTrue(); + + mVisibilityStore.setVisibility( + "prefix", + /*schemasNotPlatformSurfaceable=*/ Collections.emptySet(), + /*schemasPackageAccessible=*/ Collections.emptyMap()); + assertThat(mVisibilityStore.isSchemaPackageAccessible("prefix", "prefix/schema1", package1)) + .isFalse(); + assertThat(mVisibilityStore.isSchemaPackageAccessible("prefix", "prefix/schema2", package2)) + .isFalse(); + assertThat(mVisibilityStore.isSchemaPackageAccessible("prefix", "prefix/schema3", package3)) + .isFalse(); + } + @Test public void testEmptyPrefix() throws Exception { + PackageIdentifier package1 = + new PackageIdentifier("package1", /*sha256Certificate=*/ new byte[] {100}); + PackageIdentifier package2 = + new PackageIdentifier("package2", /*sha256Certificate=*/ new byte[] {100}); + mVisibilityStore.setVisibility( /*prefix=*/ "", - /*schemasNotPlatformSurfaceable=*/ ImmutableSet.of("schema1", "schema2")); + /*schemasNotPlatformSurfaceable=*/ ImmutableSet.of("schema1", "schema2"), + /*schemasPackageAccessible=*/ ImmutableMap.of( + "schema1", ImmutableList.of(package1), + "schema2", ImmutableList.of(package2))); assertThat(mVisibilityStore.isSchemaPlatformSurfaceable(/*prefix=*/ "", "schema1")) .isFalse(); assertThat(mVisibilityStore.isSchemaPlatformSurfaceable(/*prefix=*/ "", "schema2")) .isFalse(); + assertThat(mVisibilityStore.isSchemaPackageAccessible(/*prefix=*/ "", "schema1", package1)) + .isTrue(); + assertThat(mVisibilityStore.isSchemaPackageAccessible(/*prefix=*/ "", "schema2", package2)) + .isTrue(); } }