diff options
18 files changed, 973 insertions, 204 deletions
diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchSchema.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchSchema.java index 3933726d6729..e2add9247d08 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/AppSearchSchema.java +++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchSchema.java @@ -72,7 +72,7 @@ public final class AppSearchSchema { /** Returns the name of this schema type, e.g. Email. */ @NonNull - public String getSchemaTypeName() { + public String getSchemaType() { return mBundle.getString(SCHEMA_TYPE_FIELD, ""); } diff --git a/apex/appsearch/framework/java/android/app/appsearch/GenericDocument.java b/apex/appsearch/framework/java/android/app/appsearch/GenericDocument.java index 48d3ac09d997..cbbb2c62bb59 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/GenericDocument.java +++ b/apex/appsearch/framework/java/android/app/appsearch/GenericDocument.java @@ -79,9 +79,8 @@ public class GenericDocument { * The maximum number of indexed properties a document can have. * * <p>Indexed properties are properties where the - * {@link android.app.appsearch.annotation.AppSearchDocument.Property#indexingType} constant is - * anything other than {@link - * android.app.appsearch.AppSearchSchema.PropertyConfig.IndexingType#INDEXING_TYPE_NONE}. + * {@link AppSearchSchema.PropertyConfig#getIndexingType()} constant is anything other than + * {@link AppSearchSchema.PropertyConfig.IndexingType#INDEXING_TYPE_NONE}. */ public static int getMaxIndexedProperties() { return MAX_INDEXED_PROPERTIES; diff --git a/apex/appsearch/framework/java/android/app/appsearch/SearchSpec.java b/apex/appsearch/framework/java/android/app/appsearch/SearchSpec.java index 15acf103f2e6..f9f719e744a6 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/SearchSpec.java +++ b/apex/appsearch/framework/java/android/app/appsearch/SearchSpec.java @@ -154,12 +154,12 @@ public final class SearchSpec { * <p>If empty, the query will search over all schema types. */ @NonNull - public List<String> getSchemas() { - List<String> schemas = mBundle.getStringArrayList(SCHEMA_TYPE_FIELD); - if (schemas == null) { + public List<String> getSchemaTypes() { + List<String> schemaTypes = mBundle.getStringArrayList(SCHEMA_TYPE_FIELD); + if (schemaTypes == null) { return Collections.emptyList(); } - return Collections.unmodifiableList(schemas); + return Collections.unmodifiableList(schemaTypes); } /** @@ -241,10 +241,10 @@ public final class SearchSpec { * <p>If unset, the query will search over all schema types. */ @NonNull - public Builder addSchema(@NonNull String... schemaTypes) { + public Builder addSchemaType(@NonNull String... schemaTypes) { Preconditions.checkNotNull(schemaTypes); Preconditions.checkState(!mBuilt, "Builder has already been used"); - return addSchema(Arrays.asList(schemaTypes)); + return addSchemaType(Arrays.asList(schemaTypes)); } /** @@ -254,7 +254,7 @@ public final class SearchSpec { * <p>If unset, the query will search over all schema types. */ @NonNull - public Builder addSchema(@NonNull Collection<String> schemaTypes) { + public Builder addSchemaType(@NonNull Collection<String> schemaTypes) { Preconditions.checkNotNull(schemaTypes); Preconditions.checkState(!mBuilt, "Builder has already been used"); mSchemaTypes.addAll(schemaTypes); @@ -341,8 +341,7 @@ public final class SearchSpec { /** * Sets {@code snippetCountPerProperty}. Only the first {@code snippetCountPerProperty} - * snippets for a every property of {@link GenericDocument} will contain snippet - * information. + * snippets for each property of {@link GenericDocument} will contain snippet information. * * <p>If set to 0, snippeting is disabled and {@link SearchResult#getMatches} * will return {@code null} for that result. 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 e021544976b5..684bd2bd8205 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 @@ -30,11 +30,12 @@ import android.app.appsearch.GenericDocument; import android.app.appsearch.SearchResultPage; import android.app.appsearch.SearchSpec; import android.app.appsearch.exceptions.AppSearchException; -import com.android.internal.util.Preconditions; import com.android.server.appsearch.external.localstorage.converter.GenericDocumentToProtoConverter; import com.android.server.appsearch.external.localstorage.converter.SchemaToProtoConverter; import com.android.server.appsearch.external.localstorage.converter.SearchResultToProtoConverter; import com.android.server.appsearch.external.localstorage.converter.SearchSpecToProtoConverter; +import android.util.ArraySet; +import com.android.internal.util.Preconditions; import com.google.android.icing.IcingSearchEngine; import com.google.android.icing.proto.DeleteResultProto; @@ -62,7 +63,6 @@ import com.google.android.icing.proto.StatusProto; import java.io.File; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -104,7 +104,9 @@ import java.util.concurrent.locks.ReentrantReadWriteLock; @WorkerThread public final class AppSearchImpl { private static final String TAG = "AppSearchImpl"; - private static final char DATABASE_DELIMITER = '/'; + + @VisibleForTesting + static final char DATABASE_DELIMITER = '/'; @VisibleForTesting static final int OPTIMIZE_THRESHOLD_DOC_COUNT = 1000; @@ -114,17 +116,28 @@ public final class AppSearchImpl { static final int CHECK_OPTIMIZE_INTERVAL = 100; private final ReadWriteLock mReadWriteLock = new ReentrantReadWriteLock(); + + @GuardedBy("mReadWriteLock") private final IcingSearchEngine mIcingSearchEngine; + @GuardedBy("mReadWriteLock") + private final VisibilityStore mVisibilityStore; + // The map contains schemaTypes and namespaces for all database. All values in the map have - // been already added database name prefix. + // the database name prefix. + // TODO(b/172360376): Check if this can be replaced with an ArrayMap + @GuardedBy("mReadWriteLock") private final Map<String, Set<String>> mSchemaMap = new HashMap<>(); + + // TODO(b/172360376): Check if this can be replaced with an ArrayMap + @GuardedBy("mReadWriteLock") private final Map<String, Set<String>> mNamespaceMap = new HashMap<>(); /** * The counter to check when to call {@link #checkForOptimize(boolean)}. The interval is * {@link #CHECK_OPTIMIZE_INTERVAL}. */ + @GuardedBy("mReadWriteLock") private int mOptimizeIntervalCount = 0; /** @@ -134,12 +147,15 @@ public final class AppSearchImpl { @NonNull public static AppSearchImpl create(@NonNull File icingDir) throws AppSearchException { Preconditions.checkNotNull(icingDir); - return new AppSearchImpl(icingDir); + AppSearchImpl appSearchImpl = new AppSearchImpl(icingDir); + appSearchImpl.initializeVisibilityStore(); + return appSearchImpl; } private AppSearchImpl(@NonNull File icingDir) throws AppSearchException { boolean isReset = false; mReadWriteLock.writeLock().lock(); + try { // We synchronize here because we don't want to call IcingSearchEngine.initialize() more // than once. It's unnecessary and can be a costly operation. @@ -156,28 +172,46 @@ public final class AppSearchImpl { getAllNamespacesResultProto = mIcingSearchEngine.getAllNamespaces(); checkSuccess(getAllNamespacesResultProto.getStatus()); } catch (AppSearchException e) { + Log.w(TAG, "Error initializing, resetting IcingSearchEngine.", e); // Some error. Reset and see if it fixes it. reset(); isReset = true; } + + // Populate schema map for (SchemaTypeConfigProto schema : schemaProto.getTypesList()) { String qualifiedSchemaType = schema.getSchemaType(); addToMap(mSchemaMap, getDatabaseName(qualifiedSchemaType), qualifiedSchemaType); } + + // Populate namespace map for (String qualifiedNamespace : getAllNamespacesResultProto.getNamespacesList()) { - addToMap(mNamespaceMap, getDatabaseName(qualifiedNamespace), qualifiedNamespace); + addToMap(mNamespaceMap, getDatabaseName(qualifiedNamespace), + qualifiedNamespace); } + // TODO(b/155939114): It's possible to optimize after init, which would reduce the time // to when we're able to serve queries. Consider moving this optimize call out. if (!isReset) { checkForOptimize(/* force= */ true); } + + mVisibilityStore = new VisibilityStore(this); } finally { mReadWriteLock.writeLock().unlock(); } } /** + * Initialize the visibility store in AppSearchImpl. + * + * @throws AppSearchException on IcingSearchEngine error. + */ + void initializeVisibilityStore() throws AppSearchException { + mVisibilityStore.initialize(); + } + + /** * Updates the AppSearch schema for this app. * * <p>This method belongs to mutate group. @@ -190,26 +224,24 @@ public final class AppSearchImpl { */ public void setSchema(@NonNull String databaseName, @NonNull Set<AppSearchSchema> schemas, boolean forceOverride) throws AppSearchException { - SchemaProto schemaProto = getSchemaProto(); - - SchemaProto.Builder existingSchemaBuilder = schemaProto.toBuilder(); + mReadWriteLock.writeLock().lock(); + try { + SchemaProto.Builder existingSchemaBuilder = getSchemaProto().toBuilder(); - SchemaProto.Builder newSchemaBuilder = SchemaProto.newBuilder(); - for (AppSearchSchema schema : schemas) { - SchemaTypeConfigProto schemaTypeProto = SchemaToProtoConverter.convert(schema); - newSchemaBuilder.addTypes(schemaTypeProto); - } + SchemaProto.Builder newSchemaBuilder = SchemaProto.newBuilder(); + for (AppSearchSchema schema : schemas) { + SchemaTypeConfigProto schemaTypeProto = SchemaToProtoConverter.convert(schema); + newSchemaBuilder.addTypes(schemaTypeProto); + } - // Combine the existing schema (which may have types from other databases) with this - // database's new schema. Modifies the existingSchemaBuilder. - Set<String> newTypeNames = rewriteSchema(databaseName, existingSchemaBuilder, - newSchemaBuilder.build()); + // Combine the existing schema (which may have types from other databases) with this + // database's new schema. Modifies the existingSchemaBuilder. + RewrittenSchemaResults rewrittenSchemaResults = rewriteSchema(databaseName, + existingSchemaBuilder, + newSchemaBuilder.build()); - SetSchemaResultProto setSchemaResultProto; - mReadWriteLock.writeLock().lock(); - try { // Apply schema - setSchemaResultProto = + SetSchemaResultProto setSchemaResultProto = mIcingSearchEngine.setSchema(existingSchemaBuilder.build(), forceOverride); // Determine whether it succeeded. @@ -231,7 +263,9 @@ public final class AppSearchImpl { } // Update derived data structures. - mSchemaMap.put(databaseName, newTypeNames); + mSchemaMap.put(databaseName, rewrittenSchemaResults.mRewrittenQualifiedTypes); + mVisibilityStore.updateSchemas(databaseName, + rewrittenSchemaResults.mDeletedQualifiedTypes); // Determine whether to schedule an immediate optimize. if (setSchemaResultProto.getDeletedSchemaTypesCount() > 0 @@ -248,6 +282,40 @@ public final class AppSearchImpl { } /** + * Update the visibility settings for this app. + * + * <p>This method belongs to the mutate group + * + * @param databaseName The name of the database where the + * visibility settings will apply. + * @param schemasHiddenFromPlatformSurfaces Schemas that should be hidden from platform + * surfaces + * @throws AppSearchException on IcingSearchEngine error + */ + public void setVisibility(@NonNull String databaseName, + @NonNull Set<String> schemasHiddenFromPlatformSurfaces) + throws AppSearchException { + mReadWriteLock.writeLock().lock(); + try { + String databasePrefix = getDatabasePrefix(databaseName); + Set<String> qualifiedSchemasHiddenFromPlatformSurface = + new ArraySet<>(schemasHiddenFromPlatformSurfaces.size()); + for (String schema : schemasHiddenFromPlatformSurfaces) { + Set<String> existingSchemas = mSchemaMap.get(databaseName); + if (existingSchemas == null || !existingSchemas.contains(databasePrefix + schema)) { + throw new AppSearchException(AppSearchResult.RESULT_NOT_FOUND, + "Unknown schema(s): " + schemasHiddenFromPlatformSurfaces + + " provided during setVisibility."); + } + qualifiedSchemasHiddenFromPlatformSurface.add(databasePrefix + schema); + } + mVisibilityStore.setVisibility(databaseName, qualifiedSchemasHiddenFromPlatformSurface); + } finally { + mReadWriteLock.writeLock().lock(); + } + } + + /** * Adds a document to the AppSearch index. * * <p>This method belongs to mutate group. @@ -341,6 +409,9 @@ public final class AppSearchImpl { public SearchResultPage globalQuery( @NonNull String queryExpression, @NonNull SearchSpec searchSpec) throws AppSearchException { + // TODO(b/169883602): Check if the platform is querying us at a higher level. At this + // point, we should add all platform-surfaceable schemas assuming the querier has been + // verified. return doQuery(mNamespaceMap.keySet(), queryExpression, searchSpec); } @@ -358,8 +429,9 @@ public final class AppSearchImpl { SearchResultProto searchResultProto; mReadWriteLock.readLock().lock(); try { - // rewriteSearchSpecForDatabases will return false if none of the databases have - // documents, so we can return an empty SearchResult and skip sending request to Icing. + // rewriteSearchSpecForDatabases will return false if none of the databases that the + // client is trying to search on exist, so we can return an empty SearchResult and skip + // sending request to Icing. // We use the mNamespaceMap.keySet here because it's the smaller set of valid databases // that could exist. if (!rewriteSearchSpecForDatabases(searchSpecBuilder, databases)) { @@ -475,8 +547,7 @@ public final class AppSearchImpl { * * @throws AppSearchException on IcingSearchEngine error. */ - @VisibleForTesting - public void reset() throws AppSearchException { + private void reset() throws AppSearchException { ResetResultProto resetResultProto; mReadWriteLock.writeLock().lock(); try { @@ -484,12 +555,27 @@ public final class AppSearchImpl { mOptimizeIntervalCount = 0; mSchemaMap.clear(); mNamespaceMap.clear(); + + // Must be called after everything else since VisibilityStore may repopulate + // IcingSearchEngine with an initial schema. + mVisibilityStore.handleReset(); } finally { mReadWriteLock.writeLock().unlock(); } checkSuccess(resetResultProto.getStatus()); } + /** Wrapper around schema changes */ + @VisibleForTesting + static class RewrittenSchemaResults { + // Any database-qualified types that used to exist in the schema, but are deleted in the + // new one. + final Set<String> mDeletedQualifiedTypes = new ArraySet<>(); + + // Database-qualified types that were part of the new schema. + final Set<String> mRewrittenQualifiedTypes = new ArraySet<>(); + } + /** * Rewrites all types mentioned in the given {@code newSchema} to prepend {@code prefix}. * Rewritten types will be added to the {@code existingSchema}. @@ -499,10 +585,11 @@ public final class AppSearchImpl { * instances. Will be mutated to contain the properly rewritten schema * types from {@code newSchema}. * @param newSchema Schema with types to add to the {@code existingSchema}. - * @return a Set contains all remaining qualified schema type names in given database. + * @return a RewrittenSchemaResults contains all qualified schema type names in the given + * database as well as a set of schema types that were deleted from the database. */ @VisibleForTesting - Set<String> rewriteSchema(@NonNull String databaseName, + RewrittenSchemaResults rewriteSchema(@NonNull String databaseName, @NonNull SchemaProto.Builder existingSchema, @NonNull SchemaProto newSchema) throws AppSearchException { String prefix = getDatabasePrefix(databaseName); @@ -533,7 +620,9 @@ public final class AppSearchImpl { newTypesToProto.put(newSchemaType, typeConfigBuilder.build()); } - Set<String> newSchemaTypesName = newTypesToProto.keySet(); + // newTypesToProto is modified below, so we need a copy first + RewrittenSchemaResults rewrittenSchemaResults = new RewrittenSchemaResults(); + rewrittenSchemaResults.mRewrittenQualifiedTypes.addAll(newTypesToProto.keySet()); // Combine the existing schema (which may have types from other databases) with this // database's new schema. Modifies the existingSchemaBuilder. @@ -548,13 +637,14 @@ public final class AppSearchImpl { // All types existing before but not in newSchema should be removed. existingSchema.removeTypes(i); --i; + rewrittenSchemaResults.mDeletedQualifiedTypes.add(schemaType); } } // We've been removing existing types from newTypesToProto, so everything that remains is // new. existingSchema.addAllTypes(newTypesToProto.values()); - return newSchemaTypesName; + return rewrittenSchemaResults; } /** @@ -601,21 +691,11 @@ public final class AppSearchImpl { * @param documentBuilder The document to mutate */ @VisibleForTesting - void removeDatabasesFromDocument(@NonNull DocumentProto.Builder documentBuilder) { - int delimiterIndex; - if ((delimiterIndex = documentBuilder.getSchema().indexOf(DATABASE_DELIMITER)) != -1) { - // Rewrite the type name to remove the prefix. - // Add 1 to include the char size of the DATABASE_DELIMITER - String newSchema = documentBuilder.getSchema().substring(delimiterIndex + 1); - documentBuilder.setSchema(newSchema); - } - - if ((delimiterIndex = documentBuilder.getNamespace().indexOf(DATABASE_DELIMITER)) != -1) { - // Rewrite the namespace to remove the prefix. - // Add 1 to include the char size of the DATABASE_DELIMITER - String newNamespace = documentBuilder.getNamespace().substring(delimiterIndex + 1); - documentBuilder.setNamespace(newNamespace); - } + void removeDatabasesFromDocument(@NonNull DocumentProto.Builder documentBuilder) + throws AppSearchException { + // Rewrite the type name and namespace to remove the prefix. + documentBuilder.setSchema(removeDatabasePrefix(documentBuilder.getSchema())); + documentBuilder.setNamespace(removeDatabasePrefix(documentBuilder.getNamespace())); // Recurse into derived documents for (int propertyIdx = 0; @@ -652,7 +732,7 @@ public final class AppSearchImpl { @NonNull SearchSpecProto.Builder searchSpecBuilder, @NonNull Set<String> databaseNames) { // Create a copy since retainAll() modifies the original set. - Set<String> existingDatabases = new HashSet<>(mNamespaceMap.keySet()); + Set<String> existingDatabases = new ArraySet<>(mNamespaceMap.keySet()); existingDatabases.retainAll(databaseNames); if (existingDatabases.isEmpty()) { @@ -670,17 +750,17 @@ public final class AppSearchImpl { // Rewrite filters to include a database prefix. for (String databaseName : existingDatabases) { Set<String> existingSchemaTypes = mSchemaMap.get(databaseName); + String databaseNamePrefix = getDatabasePrefix(databaseName); if (schemaTypeFilters.isEmpty()) { // Include all schema types searchSpecBuilder.addAllSchemaTypeFilters(existingSchemaTypes); } else { // Qualify the given schema types - for (String schemaType : schemaTypeFilters) { - String qualifiedType = getDatabasePrefix(databaseName) + schemaType; + for (int i = 0; i < schemaTypeFilters.size(); i++) { + String qualifiedType = databaseNamePrefix + schemaTypeFilters.get(i); if (existingSchemaTypes.contains(qualifiedType)) { searchSpecBuilder.addSchemaTypeFilters(qualifiedType); } - } } @@ -690,8 +770,8 @@ public final class AppSearchImpl { searchSpecBuilder.addAllNamespaceFilters(existingNamespaces); } else { // Qualify the given namespaces. - for (String namespace : namespaceFilters) { - String qualifiedNamespace = getDatabasePrefix(databaseName) + namespace; + for (int i = 0; i < namespaceFilters.size(); i++) { + String qualifiedNamespace = databaseNamePrefix + namespaceFilters.get(i); if (existingNamespaces.contains(qualifiedNamespace)) { searchSpecBuilder.addNamespaceFilters(qualifiedNamespace); } @@ -711,13 +791,45 @@ public final class AppSearchImpl { return schemaProto.getSchema(); } + /** Returns true if {@code databaseName} has a {@code schemaType} */ + @GuardedBy("mReadWriteLock") + boolean hasSchemaType(@NonNull String databaseName, @NonNull String schemaType) { + Preconditions.checkNotNull(databaseName); + Preconditions.checkNotNull(schemaType); + + Set<String> schemaTypes = mSchemaMap.get(databaseName); + if (schemaTypes == null) { + return false; + } + + return schemaTypes.contains(getDatabasePrefix(databaseName) + schemaType); + } + + /** Returns a set of all databases AppSearchImpl knows about. */ @NonNull - private String getDatabasePrefix(@NonNull String databaseName) { + Set<String> getDatabases() { + return mSchemaMap.keySet(); + } + + @NonNull + private static String getDatabasePrefix(@NonNull String databaseName) { // TODO(b/170370381): Reconsider the way we separate database names for security reasons. return databaseName + DATABASE_DELIMITER; } @NonNull + private static String removeDatabasePrefix(@NonNull String prefixedString) + throws AppSearchException { + int delimiterIndex; + if ((delimiterIndex = prefixedString.indexOf(DATABASE_DELIMITER)) != -1) { + // Add 1 to include the char size of the DATABASE_DELIMITER + return prefixedString.substring(delimiterIndex + 1); + } + throw new AppSearchException(AppSearchResult.RESULT_UNKNOWN_ERROR, + "The prefixed value doesn't contains a valid database name."); + } + + @NonNull private String getDatabaseName(@NonNull String prefixedValue) throws AppSearchException { int delimiterIndex = prefixedValue.indexOf(DATABASE_DELIMITER); if (delimiterIndex == -1) { @@ -731,7 +843,7 @@ public final class AppSearchImpl { private void addToMap(Map<String, Set<String>> map, String databaseName, String prefixedValue) { Set<String> values = map.get(databaseName); if (values == null) { - values = new HashSet<>(); + values = new ArraySet<>(); map.put(databaseName, values); } values.add(prefixedValue); @@ -805,7 +917,7 @@ public final class AppSearchImpl { /** Remove the rewritten schema types from any result documents. */ private SearchResultPage rewriteSearchResultProto( - @NonNull SearchResultProto searchResultProto) { + @NonNull SearchResultProto searchResultProto) throws AppSearchException { SearchResultProto.Builder resultsBuilder = searchResultProto.toBuilder(); for (int i = 0; i < searchResultProto.getResultsCount(); i++) { if (searchResultProto.getResults(i).hasDocument()) { @@ -825,6 +937,11 @@ public final class AppSearchImpl { return mIcingSearchEngine.getOptimizeInfo(); } + @VisibleForTesting + VisibilityStore getVisibilityStore() { + return mVisibilityStore; + } + /** * Converts an erroneous status code to an AppSearchException. Callers should ensure that * the status code is not OK or WARNING_DATA_LOSS. 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 new file mode 100644 index 000000000000..24238c56d1e5 --- /dev/null +++ b/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/VisibilityStore.java @@ -0,0 +1,250 @@ +/* + * 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 com.android.server.appsearch.external.localstorage; + +import android.annotation.NonNull; +import com.android.internal.annotations.VisibleForTesting; +import android.app.appsearch.AppSearchResult; +import android.app.appsearch.AppSearchSchema; +import android.app.appsearch.GenericDocument; +import android.app.appsearch.exceptions.AppSearchException; +import android.util.ArrayMap; +import android.util.ArraySet; +import com.android.internal.util.Preconditions; + +import java.util.Arrays; +import java.util.Collections; +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. + * + * <p>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. + * + * <p>These visibility settings are used to ensure AppSearch queries respect the clients' + * settings on who their data is visible to. + * + * <p>This class doesn't handle any locking itself. Its callers should handle the locking at a + * higher level. + * + * <p>NOTE: This class holds an instance of AppSearchImpl and AppSearchImpl holds an instance of + * this class. Take care to not cause any circular dependencies. + */ +class VisibilityStore { + // Schema type for documents that hold AppSearch's metadata, e.g. visibility settings + @VisibleForTesting + static final String SCHEMA_TYPE = "Visibility"; + // Property that holds the list of platform-hidden schemas, as part of the visibility + // settings. + @VisibleForTesting + static final String PLATFORM_HIDDEN_PROPERTY = "platformHidden"; + // Database name to prefix all visibility schemas and documents with. Special-cased to + // minimize the chance of collision with a client-supplied database. + @VisibleForTesting + static final String DATABASE_NAME = "$$__AppSearch__Database"; + // Namespace of documents that contain visibility settings + private static final String NAMESPACE = "namespace"; + private final AppSearchImpl mAppSearchImpl; + + // The map contains schemas that are platform-hidden for each database. All schemas in the map + // have a database name prefix. + private final Map<String, Set<String>> mPlatformHiddenMap = new ArrayMap<>(); + + /** + * Creates an uninitialized VisibilityStore object. Callers must also call {@link #initialize()} + * before using the object. + * + * @param appSearchImpl AppSearchImpl instance + */ + VisibilityStore(@NonNull AppSearchImpl appSearchImpl) { + mAppSearchImpl = appSearchImpl; + } + + /** + * Initializes schemas and member variables to track visibility settings. + * + * <p>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 #updateSchemas}. 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.hasSchemaType(DATABASE_NAME, SCHEMA_TYPE)) { + // Schema type doesn't exist yet. Add it. + mAppSearchImpl.setSchema(DATABASE_NAME, + Collections.singleton(new AppSearchSchema.Builder(SCHEMA_TYPE) + .addProperty(new AppSearchSchema.PropertyConfig.Builder( + PLATFORM_HIDDEN_PROPERTY) + .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING) + .setCardinality( + AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED) + .build()) + .build()), + /*forceOverride=*/ false); + } + + // Populate visibility settings map + for (String database : mAppSearchImpl.getDatabases()) { + if (database.equals(DATABASE_NAME)) { + // Our own database. Skip + continue; + } + + try { + // Note: We use the other clients' database names as uris + GenericDocument document = mAppSearchImpl.getDocument( + DATABASE_NAME, NAMESPACE, /*uri=*/ database); + + String[] schemas = document.getPropertyStringArray(PLATFORM_HIDDEN_PROPERTY); + mPlatformHiddenMap.put(database, new ArraySet<>(Arrays.asList(schemas))); + } catch (AppSearchException e) { + if (e.getResultCode() == AppSearchResult.RESULT_NOT_FOUND) { + // TODO(b/172068212): This indicates some desync error. We were expecting a + // document, but didn't find one. Should probably reset AppSearch instead of + // ignoring it. + continue; + } + // Otherwise, this is some other error we should pass up. + throw e; + } + } + } + + /** + * Update visibility settings for the {@code databaseName}. + * + * @param schemasToRemove Database-prefixed schemas that should be removed + */ + public void updateSchemas(@NonNull String databaseName, + @NonNull Set<String> schemasToRemove) throws AppSearchException { + Preconditions.checkNotNull(databaseName); + Preconditions.checkNotNull(schemasToRemove); + + GenericDocument visibilityDocument; + try { + visibilityDocument = mAppSearchImpl.getDocument( + DATABASE_NAME, NAMESPACE, /*uri=*/ databaseName); + } catch (AppSearchException e) { + if (e.getResultCode() == AppSearchResult.RESULT_NOT_FOUND) { + // This might be the first time we're seeing visibility changes for a database. + // Create a new visibility document. + mAppSearchImpl.putDocument(DATABASE_NAME, new GenericDocument.Builder( + /*uri=*/ databaseName, SCHEMA_TYPE) + .setNamespace(NAMESPACE).build()); + + // Since we know there was nothing that existed before, we don't need to remove + // anything either. Return early. + return; + } + // Otherwise, this is some real error we should pass up. + throw e; + } + + String[] hiddenSchemas = + visibilityDocument.getPropertyStringArray(PLATFORM_HIDDEN_PROPERTY); + if (hiddenSchemas == null) { + // Nothing to remove. + return; + } + + // Create a new set so we can remove from it. + Set<String> remainingSchemas = new ArraySet<>(Arrays.asList(hiddenSchemas)); + boolean changed = remainingSchemas.removeAll(schemasToRemove); + if (!changed) { + // Nothing was actually removed. Can return early. + return; + } + + // Update our persisted document + // TODO(b/171882200): Switch to a .toBuilder API when it's available. + GenericDocument.Builder newVisibilityDocument = new GenericDocument.Builder( + /*uri=*/ databaseName, SCHEMA_TYPE) + .setNamespace(NAMESPACE); + if (!remainingSchemas.isEmpty()) { + newVisibilityDocument.setPropertyString(PLATFORM_HIDDEN_PROPERTY, + remainingSchemas.toArray(new String[0])); + } + mAppSearchImpl.putDocument(DATABASE_NAME, newVisibilityDocument.build()); + + // Update derived data structures + mPlatformHiddenMap.put(databaseName, remainingSchemas); + } + + /** + * Sets visibility settings for {@code databaseName}. Any previous visibility settings will be + * overwritten. + * + * @param databaseName Database name that owns the {@code platformHiddenSchemas}. + * @param platformHiddenSchemas Set of database-qualified schemas that should be hidden from + * the platform. + * @throws AppSearchException on AppSearchImpl error. + */ + public void setVisibility(@NonNull String databaseName, + @NonNull Set<String> platformHiddenSchemas) throws AppSearchException { + Preconditions.checkNotNull(databaseName); + Preconditions.checkNotNull(platformHiddenSchemas); + + // Persist the document + GenericDocument.Builder visibilityDocument = new GenericDocument.Builder( + /*uri=*/ databaseName, SCHEMA_TYPE) + .setNamespace(NAMESPACE); + if (!platformHiddenSchemas.isEmpty()) { + visibilityDocument.setPropertyString(PLATFORM_HIDDEN_PROPERTY, + platformHiddenSchemas.toArray(new String[0])); + } + mAppSearchImpl.putDocument(DATABASE_NAME, visibilityDocument.build()); + + // Update derived data structures. + mPlatformHiddenMap.put(databaseName, platformHiddenSchemas); + } + + /** + * Returns the set of database-qualified schemas in {@code databaseName} that are hidden from + * the platform. + * + * @param databaseName Database name to retrieve schemas for + * @return Set of database-qualified schemas that are hidden from the platform. Empty set if + * none exist. + */ + @NonNull + public Set<String> getPlatformHiddenSchemas(@NonNull String databaseName) { + Preconditions.checkNotNull(databaseName); + Set<String> platformHiddenSchemas = mPlatformHiddenMap.get(databaseName); + if (platformHiddenSchemas == null) { + return Collections.emptySet(); + } + return platformHiddenSchemas; + } + + /** + * Handles an {@link AppSearchImpl#reset()} by clearing any cached state and resetting to a + * first-initialized state. + * + * @throws AppSearchException on AppSearchImpl error. + */ + public void handleReset() throws AppSearchException { + mPlatformHiddenMap.clear(); + initialize(); + } +} diff --git a/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/converter/SchemaToProtoConverter.java b/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/converter/SchemaToProtoConverter.java index 403711f29544..1ecf2ca7554e 100644 --- a/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/converter/SchemaToProtoConverter.java +++ b/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/converter/SchemaToProtoConverter.java @@ -21,9 +21,9 @@ import android.annotation.NonNull; import android.app.appsearch.AppSearchSchema; import com.android.internal.util.Preconditions; -import com.google.android.icing.proto.IndexingConfig; import com.google.android.icing.proto.PropertyConfigProto; import com.google.android.icing.proto.SchemaTypeConfigProto; +import com.google.android.icing.proto.StringIndexingConfig; import com.google.android.icing.proto.TermMatchType; import java.util.List; @@ -44,7 +44,7 @@ public final class SchemaToProtoConverter { public static SchemaTypeConfigProto convert(@NonNull AppSearchSchema schema) { Preconditions.checkNotNull(schema); SchemaTypeConfigProto.Builder protoBuilder = - SchemaTypeConfigProto.newBuilder().setSchemaType(schema.getSchemaTypeName()); + SchemaTypeConfigProto.newBuilder().setSchemaType(schema.getSchemaType()); List<AppSearchSchema.PropertyConfig> properties = schema.getProperties(); for (int i = 0; i < properties.size(); i++) { PropertyConfigProto propertyProto = convertProperty(properties.get(i)); @@ -59,7 +59,7 @@ public final class SchemaToProtoConverter { Preconditions.checkNotNull(property); PropertyConfigProto.Builder propertyConfigProto = PropertyConfigProto.newBuilder() .setPropertyName(property.getName()); - IndexingConfig.Builder indexingConfig = IndexingConfig.newBuilder(); + StringIndexingConfig.Builder indexingConfig = StringIndexingConfig.newBuilder(); // Set dataType @AppSearchSchema.PropertyConfig.DataType int dataType = property.getDataType(); @@ -106,15 +106,15 @@ public final class SchemaToProtoConverter { // Set tokenizerType @AppSearchSchema.PropertyConfig.TokenizerType int tokenizerType = property.getTokenizerType(); - IndexingConfig.TokenizerType.Code tokenizerTypeProto = - IndexingConfig.TokenizerType.Code.forNumber(tokenizerType); + StringIndexingConfig.TokenizerType.Code tokenizerTypeProto = + StringIndexingConfig.TokenizerType.Code.forNumber(tokenizerType); if (tokenizerTypeProto == null) { throw new IllegalArgumentException("Invalid tokenizerType: " + tokenizerType); } indexingConfig.setTokenizerType(tokenizerTypeProto); // Build! - propertyConfigProto.setIndexingConfig(indexingConfig); + propertyConfigProto.setStringIndexingConfig(indexingConfig); return propertyConfigProto.build(); } } diff --git a/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/converter/SearchResultToProtoConverter.java b/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/converter/SearchResultToProtoConverter.java index 4310b4216266..5b5258db3ba8 100644 --- a/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/converter/SearchResultToProtoConverter.java +++ b/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/converter/SearchResultToProtoConverter.java @@ -40,7 +40,6 @@ import java.util.ArrayList; public class SearchResultToProtoConverter { private SearchResultToProtoConverter() {} - /** Translate a {@link SearchResultProto} into {@link SearchResultPage}. */ @NonNull public static SearchResultPage convertToSearchResultPage( 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 14822dcdc793..c1b827fe25e0 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 @@ -39,7 +39,7 @@ public final class SearchSpecToProtoConverter { public static SearchSpecProto toSearchSpecProto(@NonNull SearchSpec spec) { Preconditions.checkNotNull(spec); SearchSpecProto.Builder protoBuilder = SearchSpecProto.newBuilder() - .addAllSchemaTypeFilters(spec.getSchemas()) + .addAllSchemaTypeFilters(spec.getSchemaTypes()) .addAllNamespaceFilters(spec.getNamespaces()); @SearchSpec.TermMatch int termMatchCode = spec.getTermMatch(); diff --git a/apex/appsearch/synced_jetpack_changeid.txt b/apex/appsearch/synced_jetpack_changeid.txt new file mode 100644 index 000000000000..a8e72ddee7bf --- /dev/null +++ b/apex/appsearch/synced_jetpack_changeid.txt @@ -0,0 +1 @@ +I9ba99ecc4f9a7eb177e678d22d083750efce81b5 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 d4635fdda052..4747fe4a9bee 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 @@ -18,54 +18,17 @@ package android.app.appsearch; import static com.google.common.truth.Truth.assertThat; -import static org.testng.Assert.expectThrows; - import android.os.Bundle; import org.junit.Test; public class SearchSpecTest { @Test - public void buildSearchSpecWithoutTermMatchType() { - expectThrows(RuntimeException.class, () -> new SearchSpec.Builder() - .addSchema("testSchemaType") - .build()); - } - - @Test - public void testBuildSearchSpec() { - SearchSpec searchSpec = new SearchSpec.Builder() - .setTermMatch(SearchSpec.TERM_MATCH_PREFIX) - .addNamespace("namespace1", "namespace2") - .addSchema("schemaTypes1", "schemaTypes2") - .setSnippetCount(5) - .setSnippetCountPerProperty(10) - .setMaxSnippetSize(15) - .setNumPerPage(42) - .setOrder(SearchSpec.ORDER_ASCENDING) - .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE) - .build(); - - assertThat(searchSpec.getTermMatch()).isEqualTo(SearchSpec.TERM_MATCH_PREFIX); - assertThat(searchSpec.getNamespaces()) - .containsExactly("namespace1", "namespace2").inOrder(); - assertThat(searchSpec.getSchemas()) - .containsExactly("schemaTypes1", "schemaTypes2").inOrder(); - assertThat(searchSpec.getSnippetCount()).isEqualTo(5); - assertThat(searchSpec.getSnippetCountPerProperty()).isEqualTo(10); - assertThat(searchSpec.getMaxSnippetSize()).isEqualTo(15); - assertThat(searchSpec.getNumPerPage()).isEqualTo(42); - assertThat(searchSpec.getOrder()).isEqualTo(SearchSpec.ORDER_ASCENDING); - assertThat(searchSpec.getRankingStrategy()) - .isEqualTo(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE); - } - - @Test public void testGetBundle() { SearchSpec searchSpec = new SearchSpec.Builder() .setTermMatch(SearchSpec.TERM_MATCH_PREFIX) .addNamespace("namespace1", "namespace2") - .addSchema("schemaTypes1", "schemaTypes2") + .addSchemaType("schemaTypes1", "schemaTypes2") .setSnippetCount(5) .setSnippetCountPerProperty(10) .setMaxSnippetSize(15) diff --git a/core/tests/coretests/src/android/app/appsearch/external/app/cts/AppSearchResultCtsTest.java b/core/tests/coretests/src/android/app/appsearch/external/app/cts/AppSearchResultCtsTest.java new file mode 100644 index 000000000000..154779f7e5b6 --- /dev/null +++ b/core/tests/coretests/src/android/app/appsearch/external/app/cts/AppSearchResultCtsTest.java @@ -0,0 +1,82 @@ +/* + * 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.cts; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.appsearch.AppSearchResult; + +import org.junit.Test; + +public class AppSearchResultCtsTest { + + @Test + public void testResultEquals_identical() { + AppSearchResult<String> result1 = AppSearchResult.newSuccessfulResult("String"); + AppSearchResult<String> result2 = AppSearchResult.newSuccessfulResult("String"); + + assertThat(result1).isEqualTo(result2); + assertThat(result1.hashCode()).isEqualTo(result2.hashCode()); + + AppSearchResult<String> result3 = + AppSearchResult.newFailedResult(AppSearchResult.RESULT_INTERNAL_ERROR, + "errorMessage"); + AppSearchResult<String> result4 = + AppSearchResult.newFailedResult(AppSearchResult.RESULT_INTERNAL_ERROR, + "errorMessage"); + + assertThat(result3).isEqualTo(result4); + assertThat(result3.hashCode()).isEqualTo(result4.hashCode()); + } + + @Test + public void testResultEquals_failure() { + AppSearchResult<String> result1 = AppSearchResult.newSuccessfulResult("String"); + AppSearchResult<String> result2 = AppSearchResult.newSuccessfulResult("Wrong"); + AppSearchResult<String> resultNull = AppSearchResult.newSuccessfulResult(/*value=*/null); + + assertThat(result1).isNotEqualTo(result2); + assertThat(result1.hashCode()).isNotEqualTo(result2.hashCode()); + assertThat(result1).isNotEqualTo(resultNull); + assertThat(result1.hashCode()).isNotEqualTo(resultNull.hashCode()); + + AppSearchResult<String> result3 = + AppSearchResult.newFailedResult(AppSearchResult.RESULT_INTERNAL_ERROR, + "errorMessage"); + AppSearchResult<String> result4 = + AppSearchResult.newFailedResult(AppSearchResult.RESULT_IO_ERROR, + "errorMessage"); + + assertThat(result3).isNotEqualTo(result4); + assertThat(result3.hashCode()).isNotEqualTo(result4.hashCode()); + + + AppSearchResult<String> result5 = + AppSearchResult.newFailedResult(AppSearchResult.RESULT_INTERNAL_ERROR, + "Wrong"); + + assertThat(result3).isNotEqualTo(result5); + assertThat(result3.hashCode()).isNotEqualTo(result5.hashCode()); + + AppSearchResult<String> result6 = + AppSearchResult.newFailedResult(AppSearchResult.RESULT_INTERNAL_ERROR, + /*errorMessage=*/null); + + assertThat(result3).isNotEqualTo(result6); + assertThat(result3.hashCode()).isNotEqualTo(result6.hashCode()); + } +} diff --git a/core/tests/coretests/src/android/app/appsearch/external/app/AppSearchSchemaTest.java b/core/tests/coretests/src/android/app/appsearch/external/app/cts/AppSearchSchemaCtsTest.java index c171270e23ea..dc39f46cf01b 100644 --- a/core/tests/coretests/src/android/app/appsearch/external/app/AppSearchSchemaTest.java +++ b/core/tests/coretests/src/android/app/appsearch/external/app/cts/AppSearchSchemaCtsTest.java @@ -14,19 +14,20 @@ * limitations under the License. */ -package android.app.appsearch; +package android.app.appsearch.cts; import static com.google.common.truth.Truth.assertThat; import static org.testng.Assert.expectThrows; +import android.app.appsearch.AppSearchSchema; import android.app.appsearch.AppSearchSchema.PropertyConfig; import android.app.appsearch.exceptions.IllegalSchemaException; import org.junit.Test; -public class AppSearchSchemaTest { +public class AppSearchSchemaCtsTest { @Test public void testInvalidEnums() { PropertyConfig.Builder builder = new PropertyConfig.Builder("test"); diff --git a/core/tests/coretests/src/android/app/appsearch/external/app/GenericDocumentTest.java b/core/tests/coretests/src/android/app/appsearch/external/app/cts/GenericDocumentCtsTest.java index 9c29943dbef4..0b9c3d2b4259 100644 --- a/core/tests/coretests/src/android/app/appsearch/external/app/GenericDocumentTest.java +++ b/core/tests/coretests/src/android/app/appsearch/external/app/cts/GenericDocumentCtsTest.java @@ -14,29 +14,31 @@ * limitations under the License. */ -package android.app.appsearch; +package android.app.appsearch.cts; import static com.google.common.truth.Truth.assertThat; import static org.testng.Assert.expectThrows; +import android.app.appsearch.GenericDocument; + import org.junit.Test; -public class GenericDocumentTest { +public class GenericDocumentCtsTest { private static final byte[] sByteArray1 = new byte[]{(byte) 1, (byte) 2, (byte) 3}; private static final byte[] sByteArray2 = new byte[]{(byte) 4, (byte) 5, (byte) 6, (byte) 7}; private static final GenericDocument sDocumentProperties1 = new GenericDocument - .Builder("sDocumentProperties1", "sDocumentPropertiesSchemaType1") + .Builder<>("sDocumentProperties1", "sDocumentPropertiesSchemaType1") .setCreationTimestampMillis(12345L) .build(); private static final GenericDocument sDocumentProperties2 = new GenericDocument - .Builder("sDocumentProperties2", "sDocumentPropertiesSchemaType2") + .Builder<>("sDocumentProperties2", "sDocumentPropertiesSchemaType2") .setCreationTimestampMillis(6789L) .build(); @Test - public void testDocumentEquals_Identical() { - GenericDocument document1 = new GenericDocument.Builder("uri1", "schemaType1") + public void testDocumentEquals_identical() { + GenericDocument document1 = new GenericDocument.Builder<>("uri1", "schemaType1") .setCreationTimestampMillis(5L) .setTtlMillis(1L) .setPropertyLong("longKey1", 1L, 2L, 3L) @@ -46,7 +48,7 @@ public class GenericDocumentTest { .setPropertyBytes("byteKey1", sByteArray1, sByteArray2) .setPropertyDocument("documentKey1", sDocumentProperties1, sDocumentProperties2) .build(); - GenericDocument document2 = new GenericDocument.Builder("uri1", "schemaType1") + GenericDocument document2 = new GenericDocument.Builder<>("uri1", "schemaType1") .setCreationTimestampMillis(5L) .setTtlMillis(1L) .setPropertyLong("longKey1", 1L, 2L, 3L) @@ -61,8 +63,8 @@ public class GenericDocumentTest { } @Test - public void testDocumentEquals_DifferentOrder() { - GenericDocument document1 = new GenericDocument.Builder("uri1", "schemaType1") + public void testDocumentEquals_differentOrder() { + GenericDocument document1 = new GenericDocument.Builder<>("uri1", "schemaType1") .setCreationTimestampMillis(5L) .setPropertyLong("longKey1", 1L, 2L, 3L) .setPropertyBytes("byteKey1", sByteArray1, sByteArray2) @@ -73,7 +75,7 @@ public class GenericDocumentTest { .build(); // Create second document with same parameter but different order. - GenericDocument document2 = new GenericDocument.Builder("uri1", "schemaType1") + GenericDocument document2 = new GenericDocument.Builder<>("uri1", "schemaType1") .setCreationTimestampMillis(5L) .setPropertyBoolean("booleanKey1", true, false, true) .setPropertyDocument("documentKey1", sDocumentProperties1, sDocumentProperties2) @@ -87,14 +89,14 @@ public class GenericDocumentTest { } @Test - public void testDocumentEquals_Failure() { - GenericDocument document1 = new GenericDocument.Builder("uri1", "schemaType1") + public void testDocumentEquals_failure() { + GenericDocument document1 = new GenericDocument.Builder<>("uri1", "schemaType1") .setCreationTimestampMillis(5L) .setPropertyLong("longKey1", 1L, 2L, 3L) .build(); // Create second document with same order but different value. - GenericDocument document2 = new GenericDocument.Builder("uri1", "schemaType1") + GenericDocument document2 = new GenericDocument.Builder<>("uri1", "schemaType1") .setCreationTimestampMillis(5L) .setPropertyLong("longKey1", 1L, 2L, 4L) // Different .build(); @@ -103,14 +105,14 @@ public class GenericDocumentTest { } @Test - public void testDocumentEquals_Failure_RepeatedFieldOrder() { - GenericDocument document1 = new GenericDocument.Builder("uri1", "schemaType1") + public void testDocumentEquals_repeatedFieldOrder_failure() { + GenericDocument document1 = new GenericDocument.Builder<>("uri1", "schemaType1") .setCreationTimestampMillis(5L) .setPropertyBoolean("booleanKey1", true, false, true) .build(); // Create second document with same order but different value. - GenericDocument document2 = new GenericDocument.Builder("uri1", "schemaType1") + GenericDocument document2 = new GenericDocument.Builder<>("uri1", "schemaType1") .setCreationTimestampMillis(5L) .setPropertyBoolean("booleanKey1", true, true, false) // Different .build(); @@ -120,7 +122,7 @@ public class GenericDocumentTest { @Test public void testDocumentGetSingleValue() { - GenericDocument document = new GenericDocument.Builder("uri1", "schemaType1") + GenericDocument document = new GenericDocument.Builder<>("uri1", "schemaType1") .setCreationTimestampMillis(5L) .setScore(1) .setTtlMillis(1L) @@ -147,7 +149,7 @@ public class GenericDocumentTest { @Test public void testDocumentGetArrayValues() { - GenericDocument document = new GenericDocument.Builder("uri1", "schemaType1") + GenericDocument document = new GenericDocument.Builder<>("uri1", "schemaType1") .setCreationTimestampMillis(5L) .setPropertyLong("longKey1", 1L, 2L, 3L) .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0) @@ -173,8 +175,8 @@ public class GenericDocumentTest { } @Test - public void testDocument_ToString() throws Exception { - GenericDocument document = new GenericDocument.Builder("uri1", "schemaType1") + public void testDocument_toString() { + GenericDocument document = new GenericDocument.Builder<>("uri1", "schemaType1") .setCreationTimestampMillis(5L) .setPropertyLong("longKey1", 1L, 2L, 3L) .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0) @@ -216,8 +218,8 @@ public class GenericDocumentTest { } @Test - public void testDocumentGetValues_DifferentTypes() { - GenericDocument document = new GenericDocument.Builder("uri1", "schemaType1") + public void testDocumentGetValues_differentTypes() { + GenericDocument document = new GenericDocument.Builder<>("uri1", "schemaType1") .setScore(1) .setPropertyLong("longKey1", 1L) .setPropertyBoolean("booleanKey1", true, false, true) @@ -244,7 +246,7 @@ public class GenericDocumentTest { @Test public void testDocumentInvalid() { - GenericDocument.Builder builder = new GenericDocument.Builder("uri1", "schemaType1"); + GenericDocument.Builder<?> builder = new GenericDocument.Builder<>("uri1", "schemaType1"); expectThrows( IllegalArgumentException.class, () -> builder.setPropertyBoolean("test", new boolean[]{})); diff --git a/core/tests/coretests/src/android/app/appsearch/external/app/cts/SearchSpecCtsTest.java b/core/tests/coretests/src/android/app/appsearch/external/app/cts/SearchSpecCtsTest.java new file mode 100644 index 000000000000..c5b800ab18e7 --- /dev/null +++ b/core/tests/coretests/src/android/app/appsearch/external/app/cts/SearchSpecCtsTest.java @@ -0,0 +1,63 @@ +/* + * 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.cts; + +import static com.google.common.truth.Truth.assertThat; + +import static org.testng.Assert.expectThrows; + +import android.app.appsearch.SearchSpec; + +import org.junit.Test; + +public class SearchSpecCtsTest { + @Test + public void buildSearchSpecWithoutTermMatchType() { + RuntimeException e = expectThrows(RuntimeException.class, () -> new SearchSpec.Builder() + .addSchemaType("testSchemaType") + .build()); + assertThat(e).hasMessageThat().contains("Missing termMatchType field"); + } + + @Test + public void testBuildSearchSpec() { + SearchSpec searchSpec = new SearchSpec.Builder() + .setTermMatch(SearchSpec.TERM_MATCH_PREFIX) + .addNamespace("namespace1", "namespace2") + .addSchemaType("schemaTypes1", "schemaTypes2") + .setSnippetCount(5) + .setSnippetCountPerProperty(10) + .setMaxSnippetSize(15) + .setNumPerPage(42) + .setOrder(SearchSpec.ORDER_ASCENDING) + .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE) + .build(); + + assertThat(searchSpec.getTermMatch()).isEqualTo(SearchSpec.TERM_MATCH_PREFIX); + assertThat(searchSpec.getNamespaces()) + .containsExactly("namespace1", "namespace2").inOrder(); + assertThat(searchSpec.getSchemaTypes()) + .containsExactly("schemaTypes1", "schemaTypes2").inOrder(); + assertThat(searchSpec.getSnippetCount()).isEqualTo(5); + assertThat(searchSpec.getSnippetCountPerProperty()).isEqualTo(10); + assertThat(searchSpec.getMaxSnippetSize()).isEqualTo(15); + assertThat(searchSpec.getNumPerPage()).isEqualTo(42); + assertThat(searchSpec.getOrder()).isEqualTo(SearchSpec.ORDER_ASCENDING); + assertThat(searchSpec.getRankingStrategy()) + .isEqualTo(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE); + } +} diff --git a/core/tests/coretests/src/android/app/appsearch/external/app/customer/CustomerDocumentTest.java b/core/tests/coretests/src/android/app/appsearch/external/app/cts/customer/CustomerDocumentTest.java index d56d0c3c5c8e..ac24dbf1a47d 100644 --- a/core/tests/coretests/src/android/app/appsearch/external/app/customer/CustomerDocumentTest.java +++ b/core/tests/coretests/src/android/app/appsearch/external/app/cts/customer/CustomerDocumentTest.java @@ -14,13 +14,13 @@ * limitations under the License. */ -package android.app.appsearch.customer; +package android.app.appsearch.cts.customer; + +import static com.google.common.truth.Truth.assertThat; import android.annotation.NonNull; import android.app.appsearch.GenericDocument; -import static com.google.common.truth.Truth.assertThat; - import org.junit.Test; /** @@ -32,13 +32,13 @@ import org.junit.Test; */ public class CustomerDocumentTest { - private static byte[] sByteArray1 = new byte[]{(byte) 1, (byte) 2, (byte) 3}; - private static byte[] sByteArray2 = new byte[]{(byte) 4, (byte) 5, (byte) 6}; - private static GenericDocument sDocumentProperties1 = new GenericDocument - .Builder("sDocumentProperties1", "sDocumentPropertiesSchemaType1") + private static final byte[] BYTE_ARRAY1 = new byte[]{(byte) 1, (byte) 2, (byte) 3}; + private static final byte[] BYTE_ARRAY2 = new byte[]{(byte) 4, (byte) 5, (byte) 6}; + private static final GenericDocument DOCUMENT_PROPERTIES1 = new GenericDocument + .Builder<>("sDocumentProperties1", "sDocumentPropertiesSchemaType1") .build(); - private static GenericDocument sDocumentProperties2 = new GenericDocument - .Builder("sDocumentProperties2", "sDocumentPropertiesSchemaType2") + private static final GenericDocument DOCUMENT_PROPERTIES2 = new GenericDocument + .Builder<>("sDocumentProperties2", "sDocumentPropertiesSchemaType2") .build(); @Test @@ -50,8 +50,8 @@ public class CustomerDocumentTest { .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0) .setPropertyBoolean("booleanKey1", true, false, true) .setPropertyString("stringKey1", "test-value1", "test-value2", "test-value3") - .setPropertyBytes("byteKey1", sByteArray1, sByteArray2) - .setPropertyDocument("documentKey1", sDocumentProperties1, sDocumentProperties2) + .setPropertyBytes("byteKey1", BYTE_ARRAY1, BYTE_ARRAY2) + .setPropertyDocument("documentKey1", DOCUMENT_PROPERTIES1, DOCUMENT_PROPERTIES2) .build(); assertThat(customerDocument.getUri()).isEqualTo("uri1"); @@ -67,9 +67,9 @@ public class CustomerDocumentTest { assertThat(customerDocument.getPropertyStringArray("stringKey1")).asList() .containsExactly("test-value1", "test-value2", "test-value3"); assertThat(customerDocument.getPropertyBytesArray("byteKey1")).asList() - .containsExactly(sByteArray1, sByteArray2); + .containsExactly(BYTE_ARRAY1, BYTE_ARRAY2); assertThat(customerDocument.getPropertyDocumentArray("documentKey1")).asList() - .containsExactly(sDocumentProperties1, sDocumentProperties2); + .containsExactly(DOCUMENT_PROPERTIES1, DOCUMENT_PROPERTIES2); } /** 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 726e48a001d7..40c795b56232 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 @@ -25,15 +25,16 @@ import android.app.appsearch.GenericDocument; import android.app.appsearch.SearchResultPage; import android.app.appsearch.SearchSpec; import android.app.appsearch.exceptions.AppSearchException; +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.IndexingConfig; import com.android.server.appsearch.proto.PropertyConfigProto; import com.android.server.appsearch.proto.PropertyProto; import com.android.server.appsearch.proto.SchemaProto; import com.android.server.appsearch.proto.SchemaTypeConfigProto; import com.android.server.appsearch.proto.SearchSpecProto; +import com.android.server.appsearch.proto.StringIndexingConfig; import com.android.server.appsearch.proto.TermMatchType; import com.google.common.collect.ImmutableSet; @@ -42,18 +43,33 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; +import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Set; 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 visibilityAppSearchSchema = + new AppSearchSchema.Builder( + VisibilityStore.DATABASE_NAME + AppSearchImpl.DATABASE_DELIMITER + + VisibilityStore.SCHEMA_TYPE) + .addProperty(new AppSearchSchema.PropertyConfig.Builder( + VisibilityStore.PLATFORM_HIDDEN_PROPERTY) + .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING) + .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED) + .build()) + .build(); + mVisibilitySchemaProto = SchemaToProtoConverter.convert(visibilityAppSearchSchema); } /** @@ -62,8 +78,14 @@ public class AppSearchImplTest { * schema. */ @Test - public void testRewriteSchema() throws Exception { - SchemaProto.Builder existingSchemaBuilder = mAppSearchImpl.getSchemaProto().toBuilder(); + public void testRewriteSchema_addType() throws Exception { + SchemaProto.Builder existingSchemaBuilder = SchemaProto.newBuilder() + .addTypes(SchemaTypeConfigProto.newBuilder() + .setSchemaType("existingDatabase/Foo").build()); + + // Create a copy so we can modify it. + List<SchemaTypeConfigProto> existingTypes = + new ArrayList<>(existingSchemaBuilder.getTypesList()); SchemaProto newSchema = SchemaProto.newBuilder() .addTypes(SchemaTypeConfigProto.newBuilder() @@ -74,12 +96,11 @@ public class AppSearchImplTest { .setPropertyName("subject") .setDataType(PropertyConfigProto.DataType.Code.STRING) .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL) - .setIndexingConfig( - IndexingConfig.newBuilder() - .setTokenizerType( - IndexingConfig.TokenizerType.Code.PLAIN) - .setTermMatchType(TermMatchType.Code.PREFIX) - .build() + .setStringIndexingConfig(StringIndexingConfig.newBuilder() + .setTokenizerType( + StringIndexingConfig.TokenizerType.Code.PLAIN) + .setTermMatchType(TermMatchType.Code.PREFIX) + .build() ).build() ).addProperties(PropertyConfigProto.newBuilder() .setPropertyName("link") @@ -90,34 +111,103 @@ public class AppSearchImplTest { ).build() ).build(); - Set<String> newTypes = mAppSearchImpl.rewriteSchema("databaseName", existingSchemaBuilder, + AppSearchImpl.RewrittenSchemaResults rewrittenSchemaResults = mAppSearchImpl.rewriteSchema( + "newDatabase", existingSchemaBuilder, newSchema); - assertThat(newTypes).containsExactly("databaseName/Foo", "databaseName/TestType"); + + // We rewrote all the new types that were added. And nothing was removed. + assertThat(rewrittenSchemaResults.mRewrittenQualifiedTypes) + .containsExactly("newDatabase/Foo", "newDatabase/TestType"); + assertThat(rewrittenSchemaResults.mDeletedQualifiedTypes).isEmpty(); SchemaProto expectedSchema = SchemaProto.newBuilder() .addTypes(SchemaTypeConfigProto.newBuilder() - .setSchemaType("databaseName/Foo").build()) + .setSchemaType("newDatabase/Foo").build()) .addTypes(SchemaTypeConfigProto.newBuilder() - .setSchemaType("databaseName/TestType") + .setSchemaType("newDatabase/TestType") .addProperties(PropertyConfigProto.newBuilder() .setPropertyName("subject") .setDataType(PropertyConfigProto.DataType.Code.STRING) .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL) - .setIndexingConfig( - IndexingConfig.newBuilder() - .setTokenizerType( - IndexingConfig.TokenizerType.Code.PLAIN) - .setTermMatchType(TermMatchType.Code.PREFIX) - .build() + .setStringIndexingConfig(StringIndexingConfig.newBuilder() + .setTokenizerType( + StringIndexingConfig.TokenizerType.Code.PLAIN) + .setTermMatchType(TermMatchType.Code.PREFIX) + .build() ).build() ).addProperties(PropertyConfigProto.newBuilder() .setPropertyName("link") .setDataType(PropertyConfigProto.DataType.Code.DOCUMENT) .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL) - .setSchemaType("databaseName/RefType") + .setSchemaType("newDatabase/RefType") .build() ).build()) .build(); + + existingTypes.addAll(expectedSchema.getTypesList()); + assertThat(existingSchemaBuilder.getTypesList()).containsExactlyElementsIn(existingTypes); + } + + /** + * Ensure that we track all types that were rewritten in the input schema. Even if they were + * not technically "added" to the existing schema. + */ + @Test + public void testRewriteSchema_rewriteType() throws Exception { + SchemaProto.Builder existingSchemaBuilder = SchemaProto.newBuilder() + .addTypes(SchemaTypeConfigProto.newBuilder() + .setSchemaType("existingDatabase/Foo").build()); + + SchemaProto newSchema = SchemaProto.newBuilder() + .addTypes(SchemaTypeConfigProto.newBuilder() + .setSchemaType("Foo").build()) + .build(); + + AppSearchImpl.RewrittenSchemaResults rewrittenSchemaResults = mAppSearchImpl.rewriteSchema( + "existingDatabase", existingSchemaBuilder, newSchema); + + // Nothing was removed, but the method did rewrite the type name. + assertThat(rewrittenSchemaResults.mRewrittenQualifiedTypes) + .containsExactly("existingDatabase/Foo"); + assertThat(rewrittenSchemaResults.mDeletedQualifiedTypes).isEmpty(); + + // Same schema since nothing was added. + SchemaProto expectedSchema = existingSchemaBuilder.build(); + assertThat(existingSchemaBuilder.getTypesList()) + .containsExactlyElementsIn(expectedSchema.getTypesList()); + } + + /** + * Ensure that we track which types from the existing schema are deleted when a new schema is + * set. + */ + @Test + public void testRewriteSchema_deleteType() throws Exception { + SchemaProto.Builder existingSchemaBuilder = SchemaProto.newBuilder() + .addTypes(SchemaTypeConfigProto.newBuilder() + .setSchemaType("existingDatabase/Foo").build()); + + SchemaProto newSchema = SchemaProto.newBuilder() + .addTypes(SchemaTypeConfigProto.newBuilder() + .setSchemaType("Bar").build()) + .build(); + + AppSearchImpl.RewrittenSchemaResults rewrittenSchemaResults = mAppSearchImpl.rewriteSchema( + "existingDatabase", existingSchemaBuilder, newSchema); + + // Bar type was rewritten, but Foo ended up being deleted since it wasn't included in the + // new schema. + assertThat(rewrittenSchemaResults.mRewrittenQualifiedTypes) + .containsExactly("existingDatabase/Bar"); + assertThat(rewrittenSchemaResults.mDeletedQualifiedTypes) + .containsExactly("existingDatabase/Foo"); + + // Same schema since nothing was added. + SchemaProto expectedSchema = SchemaProto.newBuilder() + .addTypes(SchemaTypeConfigProto.newBuilder() + .setSchemaType("existingDatabase/Bar").build()) + .build(); + assertThat(existingSchemaBuilder.getTypesList()) .containsExactlyElementsIn(expectedSchema.getTypesList()); } @@ -154,7 +244,7 @@ public class AppSearchImplTest { } @Test - public void testRemoveDocumentTypePrefixes() { + public void testRemoveDocumentTypePrefixes() throws Exception { DocumentProto insideDocument = DocumentProto.newBuilder() .setUri("inside-uri") .setSchema("databaseName1/type") @@ -230,7 +320,7 @@ public class AppSearchImplTest { } @Test - public void testRewriteSearchSpec_OneInstance() throws Exception { + public void testRewriteSearchSpec_oneInstance() throws Exception { SearchSpecProto.Builder searchSpecProto = SearchSpecProto.newBuilder().setQuery(""); @@ -252,7 +342,7 @@ public class AppSearchImplTest { } @Test - public void testRewriteSearchSpec_TwoInstances() throws Exception { + public void testRewriteSearchSpec_twoInstances() throws Exception { SearchSpecProto.Builder searchSpecProto = SearchSpecProto.newBuilder().setQuery(""); @@ -302,9 +392,9 @@ public class AppSearchImplTest { } @Test - public void testRemoveEmptyDatabase_NoExceptionThrown() throws Exception { + public void testRemoveEmptyDatabase_noExceptionThrown() throws Exception { SearchSpec searchSpec = - new SearchSpec.Builder().addSchema("FakeType").setTermMatch( + new SearchSpec.Builder().addSchemaType("FakeType").setTermMatch( TermMatchType.Code.PREFIX_VALUE).build(); mAppSearchImpl.removeByQuery("EmptyDatabase", "", searchSpec); @@ -326,12 +416,37 @@ public class AppSearchImplTest { // Set schema Email to AppSearch database1 mAppSearchImpl.setSchema("database1", schemas, /*forceOverride=*/false); - // Create excepted schemaType proto. - SchemaProto exceptedProto = SchemaProto.newBuilder() + // Create expected schemaType proto. + SchemaProto expectedProto = SchemaProto.newBuilder() .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("database1/Email")) .build(); + + List<SchemaTypeConfigProto> expectedTypes = new ArrayList<>(); + expectedTypes.add(mVisibilitySchemaProto); + expectedTypes.addAll(expectedProto.getTypesList()); assertThat(mAppSearchImpl.getSchemaProto().getTypesList()) - .containsExactlyElementsIn(exceptedProto.getTypesList()); + .containsExactlyElementsIn(expectedTypes); + } + + @Test + public void testSetSchema_existingSchemaRetainsVisibilitySetting() throws Exception { + mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder( + "schema1").build()), /*forceOverride=*/false); + mAppSearchImpl.setVisibility("database", Set.of("schema1")); + + // "schema1" is platform hidden now + assertThat(mAppSearchImpl.getVisibilityStore().getPlatformHiddenSchemas( + "database")).containsExactly("database/schema1"); + + // Add a new schema, and include the already-existing "schema1" + mAppSearchImpl.setSchema("database", Set.of(new AppSearchSchema.Builder( + "schema1").build(), new AppSearchSchema.Builder( + "schema2").build()), /*forceOverride=*/false); + + // Check that "schema1" is still platform hidden, but "schema2" is the default platform + // visible. + assertThat(mAppSearchImpl.getVisibilityStore().getPlatformHiddenSchemas( + "database")).containsExactly("database/schema1"); } @Test @@ -342,15 +457,18 @@ public class AppSearchImplTest { // Set schema Email and Document to AppSearch database1 mAppSearchImpl.setSchema("database1", schemas, /*forceOverride=*/false); - // Create excepted schemaType proto. - SchemaProto exceptedProto = SchemaProto.newBuilder() + // Create expected schemaType proto. + SchemaProto expectedProto = SchemaProto.newBuilder() .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("database1/Email")) .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("database1/Document")) .build(); // Check both schema Email and Document saved correctly. + List<SchemaTypeConfigProto> expectedTypes = new ArrayList<>(); + expectedTypes.add(mVisibilitySchemaProto); + expectedTypes.addAll(expectedProto.getTypesList()); assertThat(mAppSearchImpl.getSchemaProto().getTypesList()) - .containsExactlyElementsIn(exceptedProto.getTypesList()); + .containsExactlyElementsIn(expectedTypes); final Set<AppSearchSchema> finalSchemas = Collections.singleton(new AppSearchSchema.Builder( "Email").build()); @@ -364,11 +482,15 @@ public class AppSearchImplTest { mAppSearchImpl.setSchema("database1", finalSchemas, /*forceOverride=*/true); // Check Document schema is removed. - exceptedProto = SchemaProto.newBuilder() + expectedProto = SchemaProto.newBuilder() .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("database1/Email")) .build(); + + expectedTypes = new ArrayList<>(); + expectedTypes.add(mVisibilitySchemaProto); + expectedTypes.addAll(expectedProto.getTypesList()); assertThat(mAppSearchImpl.getSchemaProto().getTypesList()) - .containsExactlyElementsIn(exceptedProto.getTypesList()); + .containsExactlyElementsIn(expectedTypes); } @Test @@ -382,8 +504,8 @@ public class AppSearchImplTest { mAppSearchImpl.setSchema("database1", schemas, /*forceOverride=*/false); mAppSearchImpl.setSchema("database2", schemas, /*forceOverride=*/false); - // Create excepted schemaType proto. - SchemaProto exceptedProto = SchemaProto.newBuilder() + // Create expected schemaType proto. + SchemaProto expectedProto = SchemaProto.newBuilder() .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("database1/Email")) .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("database1/Document")) .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("database2/Email")) @@ -391,23 +513,114 @@ public class AppSearchImplTest { .build(); // Check Email and Document is saved in database 1 and 2 correctly. + List<SchemaTypeConfigProto> expectedTypes = new ArrayList<>(); + expectedTypes.add(mVisibilitySchemaProto); + expectedTypes.addAll(expectedProto.getTypesList()); assertThat(mAppSearchImpl.getSchemaProto().getTypesList()) - .containsExactlyElementsIn(exceptedProto.getTypesList()); + .containsExactlyElementsIn(expectedTypes); // Save only Email to database1 this time. schemas = Collections.singleton(new AppSearchSchema.Builder("Email").build()); mAppSearchImpl.setSchema("database1", schemas, /*forceOverride=*/true); - // Create excepted schemaType list, database 1 should only contain Email but database 2 + // Create expected schemaType list, database 1 should only contain Email but database 2 // remains in same. - exceptedProto = SchemaProto.newBuilder() + expectedProto = SchemaProto.newBuilder() .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("database1/Email")) .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("database2/Email")) .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("database2/Document")) .build(); // Check nothing changed in database2. + expectedTypes = new ArrayList<>(); + expectedTypes.add(mVisibilitySchemaProto); + expectedTypes.addAll(expectedProto.getTypesList()); assertThat(mAppSearchImpl.getSchemaProto().getTypesList()) - .containsExactlyElementsIn(exceptedProto.getTypesList()); + .containsExactlyElementsIn(expectedTypes); + } + + + @Test + public void testRemoveSchema_removedFromVisibilityStore() throws Exception { + mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder( + "schema1").build()), /*forceOverride=*/false); + mAppSearchImpl.setVisibility("database", Set.of("schema1")); + + // "schema1" is platform hidden now + assertThat(mAppSearchImpl.getVisibilityStore().getPlatformHiddenSchemas( + "database")).containsExactly("database/schema1"); + + // Remove "schema1" by force overriding + mAppSearchImpl.setSchema("database", Collections.emptySet(), /*forceOverride=*/true); + + // Check that "schema1" is no longer considered platform hidden + assertThat( + mAppSearchImpl.getVisibilityStore().getPlatformHiddenSchemas("database")).isEmpty(); + + // Add "schema1" back, it gets default visibility settings which means it's not platform + // hidden. + mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder( + "schema1").build()), /*forceOverride=*/false); + assertThat( + mAppSearchImpl.getVisibilityStore().getPlatformHiddenSchemas("database")).isEmpty(); + } + + @Test + public void testSetVisibility_defaultPlatformVisible() throws Exception { + mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder( + "Schema").build()), /*forceOverride=*/false); + assertThat( + mAppSearchImpl.getVisibilityStore().getPlatformHiddenSchemas("database")).isEmpty(); + } + + @Test + public void testSetVisibility_platformHidden() throws Exception { + mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder( + "Schema").build()), /*forceOverride=*/false); + mAppSearchImpl.setVisibility("database", Set.of("Schema")); + assertThat(mAppSearchImpl.getVisibilityStore().getPlatformHiddenSchemas( + "database")).containsExactly("database/Schema"); + } + + @Test + public void testSetVisibility_unknownSchema() throws Exception { + mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder( + "Schema").build()), /*forceOverride=*/false); + + // We'll throw an exception if a client tries to set visibility on a schema we don't know + // about. + AppSearchException e = expectThrows(AppSearchException.class, + () -> mAppSearchImpl.setVisibility("database", Set.of("UnknownSchema"))); + assertThat(e).hasMessageThat().contains("Unknown schema(s)"); + } + + @Test + public void testHasSchemaType() throws Exception { + // Nothing exists yet + assertThat(mAppSearchImpl.hasSchemaType("database", "Schema")).isFalse(); + + mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder( + "Schema").build()), /*forceOverride=*/false); + assertThat(mAppSearchImpl.hasSchemaType("database", "Schema")).isTrue(); + + assertThat(mAppSearchImpl.hasSchemaType("database", "UnknownSchema")).isFalse(); + } + + @Test + public void testGetDatabases() throws Exception { + // No client databases exist yet, but the VisibilityStore's does + assertThat(mAppSearchImpl.getDatabases()).containsExactly(VisibilityStore.DATABASE_NAME); + + // Has database1 + mAppSearchImpl.setSchema("database1", Collections.singleton(new AppSearchSchema.Builder( + "schema").build()), /*forceOverride=*/false); + assertThat(mAppSearchImpl.getDatabases()).containsExactly( + VisibilityStore.DATABASE_NAME, "database1"); + + // Has both databases + mAppSearchImpl.setSchema("database2", Collections.singleton(new AppSearchSchema.Builder( + "schema").build()), /*forceOverride=*/false); + assertThat(mAppSearchImpl.getDatabases()).containsExactly( + VisibilityStore.DATABASE_NAME, "database1", "database2"); } } 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 new file mode 100644 index 000000000000..ddf0808f7c75 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/appsearch/external/localstorage/VisibilityStoreTest.java @@ -0,0 +1,76 @@ +/* + * 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 com.android.server.appsearch.external.localstorage; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.util.Collections; +import java.util.Set; + +public class VisibilityStoreTest { + + @Rule + public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); + private AppSearchImpl mAppSearchImpl; + private VisibilityStore mVisibilityStore; + + @Before + public void setUp() throws Exception { + mAppSearchImpl = AppSearchImpl.create(mTemporaryFolder.newFolder()); + mVisibilityStore = mAppSearchImpl.getVisibilityStore(); + } + + @Test + public void testSetVisibility() throws Exception { + mVisibilityStore.setVisibility( + "database", /*platformHiddenSchemas=*/ Set.of("schema1", "schema2")); + assertThat(mVisibilityStore.getPlatformHiddenSchemas("database")) + .containsExactly("schema1", "schema2"); + + // New .setVisibility() call completely overrides previous visibility settings. So + // "schema1" isn't preserved. + mVisibilityStore.setVisibility( + "database", /*platformHiddenSchemas=*/ Set.of("schema1", "schema3")); + assertThat(mVisibilityStore.getPlatformHiddenSchemas("database")) + .containsExactly("schema1", "schema3"); + + mVisibilityStore.setVisibility( + "database", /*platformHiddenSchemas=*/ Collections.emptySet()); + assertThat(mVisibilityStore.getPlatformHiddenSchemas("database")).isEmpty(); + } + + @Test + public void testRemoveSchemas() throws Exception { + mVisibilityStore.setVisibility( + "database", /*platformHiddenSchemas=*/ Set.of("schema1", "schema2")); + + // Removed just schema1 + mVisibilityStore.updateSchemas("database", /*schemasToRemove=*/ Set.of("schema1")); + assertThat(mVisibilityStore.getPlatformHiddenSchemas("database")) + .containsExactly("schema2"); + + // Removed everything now + mVisibilityStore.updateSchemas("database", /*schemasToRemove=*/ Set.of("schema2")); + assertThat(mVisibilityStore.getPlatformHiddenSchemas("database")).isEmpty(); + } + +} diff --git a/services/tests/servicestests/src/com/android/server/appsearch/external/localstorage/converter/SchemaToProtoConverterTest.java b/services/tests/servicestests/src/com/android/server/appsearch/external/localstorage/converter/SchemaToProtoConverterTest.java index 7336c3c36417..5ad8350b926d 100644 --- a/services/tests/servicestests/src/com/android/server/appsearch/external/localstorage/converter/SchemaToProtoConverterTest.java +++ b/services/tests/servicestests/src/com/android/server/appsearch/external/localstorage/converter/SchemaToProtoConverterTest.java @@ -20,9 +20,9 @@ import static com.google.common.truth.Truth.assertThat; import android.app.appsearch.AppSearchSchema; -import com.android.server.appsearch.proto.IndexingConfig; import com.android.server.appsearch.proto.PropertyConfigProto; import com.android.server.appsearch.proto.SchemaTypeConfigProto; +import com.android.server.appsearch.proto.StringIndexingConfig; import com.android.server.appsearch.proto.TermMatchType; import org.junit.Test; @@ -51,18 +51,20 @@ public class SchemaToProtoConverterTest { .setPropertyName("subject") .setDataType(PropertyConfigProto.DataType.Code.STRING) .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL) - .setIndexingConfig( - com.android.server.appsearch.proto.IndexingConfig.newBuilder() - .setTokenizerType(IndexingConfig.TokenizerType.Code.PLAIN) + .setStringIndexingConfig( + StringIndexingConfig.newBuilder() + .setTokenizerType( + StringIndexingConfig.TokenizerType.Code.PLAIN) .setTermMatchType(TermMatchType.Code.PREFIX) ) ).addProperties(PropertyConfigProto.newBuilder() .setPropertyName("body") .setDataType(PropertyConfigProto.DataType.Code.STRING) .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL) - .setIndexingConfig( - com.android.server.appsearch.proto.IndexingConfig.newBuilder() - .setTokenizerType(IndexingConfig.TokenizerType.Code.PLAIN) + .setStringIndexingConfig( + StringIndexingConfig.newBuilder() + .setTokenizerType( + StringIndexingConfig.TokenizerType.Code.PLAIN) .setTermMatchType(TermMatchType.Code.PREFIX) ) ).build(); @@ -93,18 +95,20 @@ public class SchemaToProtoConverterTest { .setPropertyName("artist") .setDataType(PropertyConfigProto.DataType.Code.STRING) .setCardinality(PropertyConfigProto.Cardinality.Code.REPEATED) - .setIndexingConfig( - com.android.server.appsearch.proto.IndexingConfig.newBuilder() - .setTokenizerType(IndexingConfig.TokenizerType.Code.PLAIN) + .setStringIndexingConfig( + StringIndexingConfig.newBuilder() + .setTokenizerType( + StringIndexingConfig.TokenizerType.Code.PLAIN) .setTermMatchType(TermMatchType.Code.PREFIX) ) ).addProperties(PropertyConfigProto.newBuilder() .setPropertyName("pubDate") .setDataType(PropertyConfigProto.DataType.Code.INT64) .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL) - .setIndexingConfig( - com.android.server.appsearch.proto.IndexingConfig.newBuilder() - .setTokenizerType(IndexingConfig.TokenizerType.Code.NONE) + .setStringIndexingConfig( + StringIndexingConfig.newBuilder() + .setTokenizerType( + StringIndexingConfig.TokenizerType.Code.NONE) .setTermMatchType(TermMatchType.Code.UNKNOWN) ) ).build(); |