diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java index 64264c03f79ec..8e8f21863ca73 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java +++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java @@ -18,12 +18,12 @@ package android.app.appsearch; import android.annotation.NonNull; import android.annotation.SystemService; import android.content.Context; +import android.os.Bundle; import android.os.RemoteException; import com.android.internal.infra.AndroidFuture; import com.google.android.icing.proto.DocumentProto; -import com.google.android.icing.proto.SchemaProto; import com.google.android.icing.proto.SearchResultProto; import com.google.android.icing.proto.SearchSpecProto; import com.google.android.icing.proto.StatusProto; @@ -133,19 +133,15 @@ public class AppSearchManager { @NonNull public AppSearchResult setSchema( @NonNull List schemas, boolean forceOverride) { - // Prepare the merged schema for transmission. - SchemaProto.Builder schemaProtoBuilder = SchemaProto.newBuilder(); - for (AppSearchSchema schema : schemas) { - schemaProtoBuilder.addTypes(schema.getProto()); - } - - // Serialize and send the schema. // TODO: This should use com.android.internal.infra.RemoteStream or another mechanism to // avoid binder limits. - byte[] schemaBytes = schemaProtoBuilder.build().toByteArray(); + List schemaBundles = new ArrayList<>(schemas.size()); + for (AppSearchSchema schema : schemas) { + schemaBundles.add(schema.getBundle()); + } AndroidFuture future = new AndroidFuture<>(); try { - mService.setSchema(schemaBytes, forceOverride, future); + mService.setSchema(schemaBundles, forceOverride, future); } catch (RemoteException e) { future.completeExceptionally(e); } diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchSchema.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchSchema.java index 4b0b41b1a282f..90e4df68f734f 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/AppSearchSchema.java +++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchSchema.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 The Android Open Source Project + * Copyright 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,18 +16,18 @@ package android.app.appsearch; +import android.os.Bundle; + import android.annotation.IntDef; import android.annotation.NonNull; + +import android.app.appsearch.exceptions.IllegalSchemaException; import android.util.ArraySet; - -import com.android.internal.annotations.VisibleForTesting; - -import com.google.android.icing.proto.PropertyConfigProto; -import com.google.android.icing.proto.SchemaTypeConfigProto; -import com.google.android.icing.proto.TermMatchType; +import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; import java.util.Set; /** @@ -36,45 +36,64 @@ import java.util.Set; *

For example, an e-mail message or a music recording could be a schema type. * *

The schema consists of type information, properties, and config (like tokenization type). - * * @hide */ public final class AppSearchSchema { - private final SchemaTypeConfigProto mProto; + /** @hide */ + + public static final String SCHEMA_TYPE_FIELD = "schemaType"; - private AppSearchSchema(SchemaTypeConfigProto proto) { - mProto = proto; + /** @hide */ + + public static final String PROPERTIES_FIELD = "properties"; + + private final Bundle mBundle; + + /** @hide */ + + public AppSearchSchema(@NonNull Bundle bundle) { + Preconditions.checkNotNull(bundle); + mBundle = bundle; } /** - * Returns the {@link SchemaTypeConfigProto} populated by this builder. + * Returns the {@link Bundle} populated by this builder. * @hide */ + @NonNull - @VisibleForTesting - public SchemaTypeConfigProto getProto() { - return mProto; + public Bundle getBundle() { + return mBundle; } @Override public String toString() { - return mProto.toString(); + return mBundle.toString(); } /** Builder for {@link AppSearchSchema objects}. */ public static final class Builder { - private final SchemaTypeConfigProto.Builder mProtoBuilder = - SchemaTypeConfigProto.newBuilder(); + private final String mTypeName; + private final ArrayList mProperties = new ArrayList<>(); + private final Set mPropertyNames = new ArraySet<>(); + private boolean mBuilt = false; /** Creates a new {@link AppSearchSchema.Builder}. */ public Builder(@NonNull String typeName) { - mProtoBuilder.setSchemaType(typeName); + Preconditions.checkNotNull(typeName); + mTypeName = typeName; } /** Adds a property to the given type. */ @NonNull public AppSearchSchema.Builder addProperty(@NonNull PropertyConfig propertyConfig) { - mProtoBuilder.addProperties(propertyConfig.mProto); + Preconditions.checkState(!mBuilt, "Builder has already been used"); + Preconditions.checkNotNull(propertyConfig); + if (!mPropertyNames.add(propertyConfig.mName)) { + throw new IllegalSchemaException( + "Property defined more than once: " + propertyConfig.mName); + } + mProperties.add(propertyConfig.mBundle); return this; } @@ -85,15 +104,12 @@ public final class AppSearchSchema { */ @NonNull public AppSearchSchema build() { - Set propertyNames = new ArraySet<>(); - for (PropertyConfigProto propertyConfigProto : mProtoBuilder.getPropertiesList()) { - if (!propertyNames.add(propertyConfigProto.getPropertyName())) { - throw new IllegalSchemaException( - "Property defined more than once: " - + propertyConfigProto.getPropertyName()); - } - } - return new AppSearchSchema(mProtoBuilder.build()); + Preconditions.checkState(!mBuilt, "Builder has already been used"); + Bundle bundle = new Bundle(); + bundle.putString(AppSearchSchema.SCHEMA_TYPE_FIELD, mTypeName); + bundle.putParcelableArrayList(AppSearchSchema.PROPERTIES_FIELD, mProperties); + mBuilt = true; + return new AppSearchSchema(bundle); } } @@ -104,10 +120,37 @@ public final class AppSearchSchema { * a property. */ public static final class PropertyConfig { - /** Physical data-types of the contents of the property. */ + /** @hide */ + + public static final String NAME_FIELD = "name"; + + /** @hide */ + + public static final String DATA_TYPE_FIELD = "dataType"; + + /** @hide */ + + public static final String SCHEMA_TYPE_FIELD = "schemaType"; + + /** @hide */ + + public static final String CARDINALITY_FIELD = "cardinality"; + + /** @hide */ + + public static final String INDEXING_TYPE_FIELD = "indexingType"; + + /** @hide */ + + public static final String TOKENIZER_TYPE_FIELD = "tokenizerType"; + + /** + * Physical data-types of the contents of the property. + * @hide + */ // NOTE: The integer values of these constants must match the proto enum constants in // com.google.android.icing.proto.PropertyConfigProto.DataType.Code. - @IntDef(prefix = {"DATA_TYPE_"}, value = { + @IntDef(value = { DATA_TYPE_STRING, DATA_TYPE_INT64, DATA_TYPE_DOUBLE, @@ -133,10 +176,13 @@ public final class AppSearchSchema { */ public static final int DATA_TYPE_DOCUMENT = 6; - /** The cardinality of the property (whether it is required, optional or repeated). */ + /** + * The cardinality of the property (whether it is required, optional or repeated). + * @hide + */ // NOTE: The integer values of these constants must match the proto enum constants in // com.google.android.icing.proto.PropertyConfigProto.Cardinality.Code. - @IntDef(prefix = {"CARDINALITY_"}, value = { + @IntDef(value = { CARDINALITY_REPEATED, CARDINALITY_OPTIONAL, CARDINALITY_REQUIRED, @@ -153,8 +199,11 @@ public final class AppSearchSchema { /** Exactly one value [1]. */ public static final int CARDINALITY_REQUIRED = 3; - /** Encapsulates the configurations on how AppSearch should query/index these terms. */ - @IntDef(prefix = {"INDEXING_TYPE_"}, value = { + /** + * Encapsulates the configurations on how AppSearch should query/index these terms. + * @hide + */ + @IntDef(value = { INDEXING_TYPE_NONE, INDEXING_TYPE_EXACT_TERMS, INDEXING_TYPE_PREFIXES, @@ -188,10 +237,13 @@ public final class AppSearchSchema { */ public static final int INDEXING_TYPE_PREFIXES = 2; - /** Configures how tokens should be extracted from this property. */ + /** + * Configures how tokens should be extracted from this property. + * @hide + */ // NOTE: The integer values of these constants must match the proto enum constants in // com.google.android.icing.proto.IndexingConfig.TokenizerType.Code. - @IntDef(prefix = {"TOKENIZER_TYPE_"}, value = { + @IntDef(value = { TOKENIZER_TYPE_NONE, TOKENIZER_TYPE_PLAIN, }) @@ -207,15 +259,17 @@ public final class AppSearchSchema { /** Tokenization for plain text. */ public static final int TOKENIZER_TYPE_PLAIN = 1; - private final PropertyConfigProto mProto; + final String mName; + final Bundle mBundle; - private PropertyConfig(PropertyConfigProto proto) { - mProto = proto; + PropertyConfig(@NonNull String name, @NonNull Bundle bundle) { + mName = Preconditions.checkNotNull(name); + mBundle = Preconditions.checkNotNull(bundle); } @Override public String toString() { - return mProto.toString(); + return mBundle.toString(); } /** @@ -232,15 +286,14 @@ public final class AppSearchSchema { * is also required. */ public static final class Builder { - private final PropertyConfigProto.Builder mPropertyConfigProto = - PropertyConfigProto.newBuilder(); - private final com.google.android.icing.proto.IndexingConfig.Builder - mIndexingConfigProto = - com.google.android.icing.proto.IndexingConfig.newBuilder(); + private final String mName; + private final Bundle mBundle = new Bundle(); + private boolean mBuilt = false; /** Creates a new {@link PropertyConfig.Builder}. */ public Builder(@NonNull String propertyName) { - mPropertyConfigProto.setPropertyName(propertyName); + mName = Preconditions.checkNotNull(propertyName); + mBundle.putString(NAME_FIELD, propertyName); } /** @@ -250,24 +303,24 @@ public final class AppSearchSchema { */ @NonNull public PropertyConfig.Builder setDataType(@DataType int dataType) { - PropertyConfigProto.DataType.Code dataTypeProto = - PropertyConfigProto.DataType.Code.forNumber(dataType); - if (dataTypeProto == null) { - throw new IllegalArgumentException("Invalid dataType: " + dataType); - } - mPropertyConfigProto.setDataType(dataTypeProto); + Preconditions.checkState(!mBuilt, "Builder has already been used"); + Preconditions.checkArgumentInRange( + dataType, DATA_TYPE_STRING, DATA_TYPE_DOCUMENT, "dataType"); + mBundle.putInt(DATA_TYPE_FIELD, dataType); return this; } /** * The logical schema-type of the contents of this property. * - *

Only required when {@link #setDataType(int)} is set to + *

Only required when {@link #setDataType} is set to * {@link #DATA_TYPE_DOCUMENT}. Otherwise, it is ignored. */ @NonNull public PropertyConfig.Builder setSchemaType(@NonNull String schemaType) { - mPropertyConfigProto.setSchemaType(schemaType); + Preconditions.checkState(!mBuilt, "Builder has already been used"); + Preconditions.checkNotNull(schemaType); + mBundle.putString(SCHEMA_TYPE_FIELD, schemaType); return this; } @@ -278,12 +331,10 @@ public final class AppSearchSchema { */ @NonNull public PropertyConfig.Builder setCardinality(@Cardinality int cardinality) { - PropertyConfigProto.Cardinality.Code cardinalityProto = - PropertyConfigProto.Cardinality.Code.forNumber(cardinality); - if (cardinalityProto == null) { - throw new IllegalArgumentException("Invalid cardinality: " + cardinality); - } - mPropertyConfigProto.setCardinality(cardinalityProto); + Preconditions.checkState(!mBuilt, "Builder has already been used"); + Preconditions.checkArgumentInRange( + cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality"); + mBundle.putInt(CARDINALITY_FIELD, cardinality); return this; } @@ -292,35 +343,20 @@ public final class AppSearchSchema { */ @NonNull public PropertyConfig.Builder setIndexingType(@IndexingType int indexingType) { - TermMatchType.Code termMatchTypeProto; - switch (indexingType) { - case INDEXING_TYPE_NONE: - termMatchTypeProto = TermMatchType.Code.UNKNOWN; - break; - case INDEXING_TYPE_EXACT_TERMS: - termMatchTypeProto = TermMatchType.Code.EXACT_ONLY; - break; - case INDEXING_TYPE_PREFIXES: - termMatchTypeProto = TermMatchType.Code.PREFIX; - break; - default: - throw new IllegalArgumentException("Invalid indexingType: " + indexingType); - } - mIndexingConfigProto.setTermMatchType(termMatchTypeProto); + Preconditions.checkState(!mBuilt, "Builder has already been used"); + Preconditions.checkArgumentInRange( + indexingType, INDEXING_TYPE_NONE, INDEXING_TYPE_PREFIXES, "indexingType"); + mBundle.putInt(INDEXING_TYPE_FIELD, indexingType); return this; } /** Configures how this property should be tokenized (split into words). */ @NonNull public PropertyConfig.Builder setTokenizerType(@TokenizerType int tokenizerType) { - com.google.android.icing.proto.IndexingConfig.TokenizerType.Code - tokenizerTypeProto = - com.google.android.icing.proto.IndexingConfig - .TokenizerType.Code.forNumber(tokenizerType); - if (tokenizerTypeProto == null) { - throw new IllegalArgumentException("Invalid tokenizerType: " + tokenizerType); - } - mIndexingConfigProto.setTokenizerType(tokenizerTypeProto); + Preconditions.checkState(!mBuilt, "Builder has already been used"); + Preconditions.checkArgumentInRange( + tokenizerType, TOKENIZER_TYPE_NONE, TOKENIZER_TYPE_PLAIN, "tokenizerType"); + mBundle.putInt(TOKENIZER_TYPE_FIELD, tokenizerType); return this; } @@ -334,25 +370,23 @@ public final class AppSearchSchema { */ @NonNull public PropertyConfig build() { - mPropertyConfigProto.setIndexingConfig(mIndexingConfigProto); + Preconditions.checkState(!mBuilt, "Builder has already been used"); // TODO(b/147692920): Send the schema to Icing Lib for official validation, instead // of partially reimplementing some of the validation Icing does here. - if (mPropertyConfigProto.getDataType() - == PropertyConfigProto.DataType.Code.UNKNOWN) { + if (!mBundle.containsKey(DATA_TYPE_FIELD)) { throw new IllegalSchemaException("Missing field: dataType"); } - if (mPropertyConfigProto.getSchemaType().isEmpty() - && mPropertyConfigProto.getDataType() - == PropertyConfigProto.DataType.Code.DOCUMENT) { + if (mBundle.getString(SCHEMA_TYPE_FIELD, "").isEmpty() + && mBundle.getInt(DATA_TYPE_FIELD) == DATA_TYPE_DOCUMENT) { throw new IllegalSchemaException( "Missing field: schemaType (required for configs with " + "dataType = DOCUMENT)"); } - if (mPropertyConfigProto.getCardinality() - == PropertyConfigProto.Cardinality.Code.UNKNOWN) { + if (!mBundle.containsKey(CARDINALITY_FIELD)) { throw new IllegalSchemaException("Missing field: cardinality"); } - return new PropertyConfig(mPropertyConfigProto.build()); + mBuilt = true; + return new PropertyConfig(mName, mBundle); } } } diff --git a/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl b/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl index 68de4f0684707..b95fe314120f3 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl +++ b/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl @@ -15,6 +15,8 @@ */ package android.app.appsearch; +import android.os.Bundle; + import com.android.internal.infra.AndroidFuture; parcelable AppSearchResult; @@ -25,14 +27,16 @@ interface IAppSearchManager { /** * Sets the schema. * - * @param schemaBytes Serialized SchemaProto. + * @param schemaBundles List of AppSearchSchema bundles. * @param forceOverride Whether to apply the new schema even if it is incompatible. All * incompatible documents will be deleted. * @param callback {@link AndroidFuture}<{@link AppSearchResult}<{@link Void}>>. * The results of the call. */ void setSchema( - in byte[] schemaBytes, boolean forceOverride, in AndroidFuture callback); + in List schemaBundles, + boolean forceOverride, + in AndroidFuture callback); /** * Inserts documents into the index. diff --git a/apex/appsearch/framework/java/android/app/appsearch/SearchSpec.java b/apex/appsearch/framework/java/android/app/appsearch/SearchSpec.java index 6e644ffb8b27f..970808cedd6e9 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/SearchSpec.java +++ b/apex/appsearch/framework/java/android/app/appsearch/SearchSpec.java @@ -18,6 +18,7 @@ package android.app.appsearch; import android.annotation.IntDef; import android.annotation.NonNull; +import android.app.appsearch.exceptions.IllegalSearchSpecException; import com.google.android.icing.proto.ResultSpecProto; import com.google.android.icing.proto.ScoringSpecProto; diff --git a/apex/appsearch/framework/java/android/app/appsearch/IllegalSchemaException.java b/apex/appsearch/framework/java/android/app/appsearch/exceptions/IllegalSchemaException.java similarity index 92% rename from apex/appsearch/framework/java/android/app/appsearch/IllegalSchemaException.java rename to apex/appsearch/framework/java/android/app/appsearch/exceptions/IllegalSchemaException.java index f9e528cd2951c..6dd86f5e6de17 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/IllegalSchemaException.java +++ b/apex/appsearch/framework/java/android/app/appsearch/exceptions/IllegalSchemaException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 The Android Open Source Project + * Copyright 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,16 +14,18 @@ * limitations under the License. */ -package android.app.appsearch; +package android.app.appsearch.exceptions; import android.annotation.NonNull; + /** * Indicates that a {@link android.app.appsearch.AppSearchSchema} has logical inconsistencies such * as unpopulated mandatory fields or illegal combinations of parameters. * * @hide */ + public class IllegalSchemaException extends IllegalArgumentException { /** * Constructs a new {@link IllegalSchemaException}. diff --git a/apex/appsearch/framework/java/android/app/appsearch/IllegalSearchSpecException.java b/apex/appsearch/framework/java/android/app/appsearch/exceptions/IllegalSearchSpecException.java similarity index 84% rename from apex/appsearch/framework/java/android/app/appsearch/IllegalSearchSpecException.java rename to apex/appsearch/framework/java/android/app/appsearch/exceptions/IllegalSearchSpecException.java index 0d029f029ee57..3ef887f09eabd 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/IllegalSearchSpecException.java +++ b/apex/appsearch/framework/java/android/app/appsearch/exceptions/IllegalSearchSpecException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,16 +14,18 @@ * limitations under the License. */ -package android.app.appsearch; +package android.app.appsearch.exceptions; import android.annotation.NonNull; + /** - * Indicates that a {@link android.app.appsearch.SearchResults} has logical inconsistencies such + * Indicates that a {@link android.app.appsearch.SearchResult} has logical inconsistencies such * as unpopulated mandatory fields or illegal combinations of parameters. * * @hide */ + public class IllegalSearchSpecException extends IllegalArgumentException { /** * Constructs a new {@link IllegalSearchSpecException}. 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 75fad82d3fffd..3e72ceb59e88f 100644 --- a/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java +++ b/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java @@ -19,20 +19,24 @@ import android.annotation.NonNull; import android.app.appsearch.AppSearchBatchResult; import android.app.appsearch.AppSearchDocument; import android.app.appsearch.AppSearchResult; +import android.app.appsearch.AppSearchSchema; import android.app.appsearch.IAppSearchManager; import android.app.appsearch.exceptions.AppSearchException; import android.content.Context; import android.os.Binder; +import android.os.Bundle; import android.os.UserHandle; import com.android.internal.infra.AndroidFuture; import com.android.internal.util.Preconditions; import com.android.server.SystemService; import com.android.server.appsearch.external.localbackend.AppSearchImpl; +import com.android.server.appsearch.external.localbackend.converter.SchemaToProtoConverter; import com.google.android.icing.proto.DocumentProto; import com.google.android.icing.proto.ResultSpecProto; import com.google.android.icing.proto.SchemaProto; +import com.google.android.icing.proto.SchemaTypeConfigProto; import com.google.android.icing.proto.ScoringSpecProto; import com.google.android.icing.proto.SearchResultProto; import com.google.android.icing.proto.SearchSpecProto; @@ -59,19 +63,24 @@ public class AppSearchManagerService extends SystemService { private class Stub extends IAppSearchManager.Stub { @Override public void setSchema( - @NonNull byte[] schemaBytes, + @NonNull List schemaBundles, boolean forceOverride, @NonNull AndroidFuture callback) { - Preconditions.checkNotNull(schemaBytes); + Preconditions.checkNotNull(schemaBundles); Preconditions.checkNotNull(callback); int callingUid = Binder.getCallingUidOrThrow(); int callingUserId = UserHandle.getUserId(callingUid); long callingIdentity = Binder.clearCallingIdentity(); try { - SchemaProto schema = SchemaProto.parseFrom(schemaBytes); + SchemaProto.Builder schemaProtoBuilder = SchemaProto.newBuilder(); + for (int i = 0; i < schemaBundles.size(); i++) { + AppSearchSchema schema = new AppSearchSchema(schemaBundles.get(i)); + SchemaTypeConfigProto schemaTypeProto = SchemaToProtoConverter.convert(schema); + schemaProtoBuilder.addTypes(schemaTypeProto); + } AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId); String databaseName = makeDatabaseName(callingUid); - impl.setSchema(databaseName, schema, forceOverride); + impl.setSchema(databaseName, schemaProtoBuilder.build(), forceOverride); callback.complete(AppSearchResult.newSuccessfulResult(/*value=*/ null)); } catch (Throwable t) { callback.complete(throwableToFailedResult(t)); diff --git a/apex/appsearch/service/java/com/android/server/appsearch/external/localbackend/converter/SchemaToProtoConverter.java b/apex/appsearch/service/java/com/android/server/appsearch/external/localbackend/converter/SchemaToProtoConverter.java new file mode 100644 index 0000000000000..ca0d2ee970cb0 --- /dev/null +++ b/apex/appsearch/service/java/com/android/server/appsearch/external/localbackend/converter/SchemaToProtoConverter.java @@ -0,0 +1,127 @@ +/* + * 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.localbackend.converter; + +import android.os.Bundle; + +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.TermMatchType; + +import java.util.ArrayList; + +/** + * Translates an {@link AppSearchSchema} into a {@link SchemaTypeConfigProto}. + * @hide + */ + +public final class SchemaToProtoConverter { + private SchemaToProtoConverter() {} + + /** + * Converts an {@link android.app.appsearch.AppSearchSchema} into a + * {@link SchemaTypeConfigProto}. + */ + @NonNull + public static SchemaTypeConfigProto convert(@NonNull AppSearchSchema schema) { + Preconditions.checkNotNull(schema); + Bundle bundle = schema.getBundle(); + SchemaTypeConfigProto.Builder protoBuilder = + SchemaTypeConfigProto.newBuilder() + .setSchemaType(bundle.getString(AppSearchSchema.SCHEMA_TYPE_FIELD, "")); + ArrayList properties = + bundle.getParcelableArrayList(AppSearchSchema.PROPERTIES_FIELD); + if (properties != null) { + for (int i = 0; i < properties.size(); i++) { + PropertyConfigProto propertyProto = convertProperty(properties.get(i)); + protoBuilder.addProperties(propertyProto); + } + } + return protoBuilder.build(); + } + + @NonNull + private static PropertyConfigProto convertProperty(@NonNull Bundle bundle) { + Preconditions.checkNotNull(bundle); + PropertyConfigProto.Builder propertyConfigProto = PropertyConfigProto.newBuilder() + .setPropertyName(bundle.getString(AppSearchSchema.PropertyConfig.NAME_FIELD, "")); + IndexingConfig.Builder indexingConfig = IndexingConfig.newBuilder(); + + // Set dataType + @AppSearchSchema.PropertyConfig.DataType int dataType = + bundle.getInt(AppSearchSchema.PropertyConfig.DATA_TYPE_FIELD); + PropertyConfigProto.DataType.Code dataTypeProto = + PropertyConfigProto.DataType.Code.forNumber(dataType); + if (dataTypeProto == null) { + throw new IllegalArgumentException("Invalid dataType: " + dataType); + } + propertyConfigProto.setDataType(dataTypeProto); + + // Set schemaType + propertyConfigProto.setSchemaType( + bundle.getString(AppSearchSchema.PropertyConfig.SCHEMA_TYPE_FIELD, "")); + + // Set cardinality + @AppSearchSchema.PropertyConfig.Cardinality int cardinality = + bundle.getInt(AppSearchSchema.PropertyConfig.CARDINALITY_FIELD); + PropertyConfigProto.Cardinality.Code cardinalityProto = + PropertyConfigProto.Cardinality.Code.forNumber(cardinality); + if (cardinalityProto == null) { + throw new IllegalArgumentException("Invalid cardinality: " + dataType); + } + propertyConfigProto.setCardinality(cardinalityProto); + + // Set indexingType + @AppSearchSchema.PropertyConfig.IndexingType int indexingType = + bundle.getInt(AppSearchSchema.PropertyConfig.INDEXING_TYPE_FIELD); + TermMatchType.Code termMatchTypeProto; + switch (indexingType) { + case AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE: + termMatchTypeProto = TermMatchType.Code.UNKNOWN; + break; + case AppSearchSchema.PropertyConfig.INDEXING_TYPE_EXACT_TERMS: + termMatchTypeProto = TermMatchType.Code.EXACT_ONLY; + break; + case AppSearchSchema.PropertyConfig.INDEXING_TYPE_PREFIXES: + termMatchTypeProto = TermMatchType.Code.PREFIX; + break; + default: + throw new IllegalArgumentException("Invalid indexingType: " + indexingType); + } + indexingConfig.setTermMatchType(termMatchTypeProto); + + // Set tokenizerType + @AppSearchSchema.PropertyConfig.TokenizerType int tokenizerType = + bundle.getInt(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_FIELD); + IndexingConfig.TokenizerType.Code tokenizerTypeProto = + IndexingConfig.TokenizerType.Code.forNumber(tokenizerType); + if (tokenizerTypeProto == null) { + throw new IllegalArgumentException("Invalid tokenizerType: " + tokenizerType); + } + indexingConfig.setTokenizerType(tokenizerTypeProto); + + // Build! + propertyConfigProto.setIndexingConfig(indexingConfig); + return propertyConfigProto.build(); + } +} diff --git a/core/tests/coretests/src/android/app/appsearch/AppSearchSchemaTest.java b/core/tests/coretests/src/android/app/appsearch/AppSearchSchemaTest.java deleted file mode 100644 index cdc6d250feeb0..0000000000000 --- a/core/tests/coretests/src/android/app/appsearch/AppSearchSchemaTest.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.app.appsearch; - -import static com.google.common.truth.Truth.assertThat; - -import static org.testng.Assert.assertThrows; -import static org.testng.Assert.expectThrows; - -import android.app.appsearch.AppSearchSchema.PropertyConfig; -import android.app.appsearch.proto.IndexingConfig; -import android.app.appsearch.proto.PropertyConfigProto; -import android.app.appsearch.proto.SchemaTypeConfigProto; -import android.app.appsearch.proto.TermMatchType; - -import androidx.test.filters.SmallTest; - -import org.junit.Test; - -@SmallTest -public class AppSearchSchemaTest { - @Test - public void testGetProto_Email() { - AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email") - .addProperty(new AppSearchSchema.PropertyConfig.Builder("subject") - .setDataType(PropertyConfig.DATA_TYPE_STRING) - .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) - .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES) - .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN) - .build() - ).addProperty(new AppSearchSchema.PropertyConfig.Builder("body") - .setDataType(PropertyConfig.DATA_TYPE_STRING) - .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) - .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES) - .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN) - .build() - ).build(); - - SchemaTypeConfigProto expectedEmailProto = SchemaTypeConfigProto.newBuilder() - .setSchemaType("Email") - .addProperties(PropertyConfigProto.newBuilder() - .setPropertyName("subject") - .setDataType(PropertyConfigProto.DataType.Code.STRING) - .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL) - .setIndexingConfig( - android.app.appsearch.proto.IndexingConfig.newBuilder() - .setTokenizerType(IndexingConfig.TokenizerType.Code.PLAIN) - .setTermMatchType(TermMatchType.Code.PREFIX) - ) - ).addProperties(PropertyConfigProto.newBuilder() - .setPropertyName("body") - .setDataType(PropertyConfigProto.DataType.Code.STRING) - .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL) - .setIndexingConfig( - android.app.appsearch.proto.IndexingConfig.newBuilder() - .setTokenizerType( - android.app.appsearch.proto.IndexingConfig - .TokenizerType.Code.PLAIN) - .setTermMatchType(TermMatchType.Code.PREFIX) - ) - ).build(); - - assertThat(emailSchema.getProto()).isEqualTo(expectedEmailProto); - } - - @Test - public void testGetProto_MusicRecording() { - AppSearchSchema musicRecordingSchema = new AppSearchSchema.Builder("MusicRecording") - .addProperty(new AppSearchSchema.PropertyConfig.Builder("artist") - .setDataType(PropertyConfig.DATA_TYPE_STRING) - .setCardinality(PropertyConfig.CARDINALITY_REPEATED) - .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES) - .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN) - .build() - ).addProperty(new AppSearchSchema.PropertyConfig.Builder("pubDate") - .setDataType(PropertyConfig.DATA_TYPE_INT64) - .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) - .setIndexingType(PropertyConfig.INDEXING_TYPE_NONE) - .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_NONE) - .build() - ).build(); - - SchemaTypeConfigProto expectedMusicRecordingProto = SchemaTypeConfigProto.newBuilder() - .setSchemaType("MusicRecording") - .addProperties(PropertyConfigProto.newBuilder() - .setPropertyName("artist") - .setDataType(PropertyConfigProto.DataType.Code.STRING) - .setCardinality(PropertyConfigProto.Cardinality.Code.REPEATED) - .setIndexingConfig( - android.app.appsearch.proto.IndexingConfig.newBuilder() - .setTokenizerType( - android.app.appsearch.proto.IndexingConfig - .TokenizerType.Code.PLAIN) - .setTermMatchType(TermMatchType.Code.PREFIX) - ) - ).addProperties(PropertyConfigProto.newBuilder() - .setPropertyName("pubDate") - .setDataType(PropertyConfigProto.DataType.Code.INT64) - .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL) - .setIndexingConfig( - android.app.appsearch.proto.IndexingConfig.newBuilder() - .setTokenizerType( - android.app.appsearch.proto.IndexingConfig - .TokenizerType.Code.NONE) - .setTermMatchType(TermMatchType.Code.UNKNOWN) - ) - ).build(); - - assertThat(musicRecordingSchema.getProto()).isEqualTo(expectedMusicRecordingProto); - } - - @Test - public void testInvalidEnums() { - PropertyConfig.Builder builder = new AppSearchSchema.PropertyConfig.Builder("test"); - assertThrows(IllegalArgumentException.class, () -> builder.setDataType(99)); - assertThrows(IllegalArgumentException.class, () -> builder.setCardinality(99)); - } - - @Test - public void testMissingFields() { - PropertyConfig.Builder builder = new AppSearchSchema.PropertyConfig.Builder("test"); - Exception e = expectThrows(IllegalSchemaException.class, builder::build); - assertThat(e).hasMessageThat().contains("Missing field: dataType"); - - builder.setDataType(PropertyConfig.DATA_TYPE_DOCUMENT); - e = expectThrows(IllegalSchemaException.class, builder::build); - assertThat(e).hasMessageThat().contains("Missing field: schemaType"); - - builder.setSchemaType("TestType"); - e = expectThrows(IllegalSchemaException.class, builder::build); - assertThat(e).hasMessageThat().contains("Missing field: cardinality"); - - builder.setCardinality(PropertyConfig.CARDINALITY_REPEATED); - builder.build(); - } - - @Test - public void testDuplicateProperties() { - AppSearchSchema.Builder builder = new AppSearchSchema.Builder("Email") - .addProperty(new AppSearchSchema.PropertyConfig.Builder("subject") - .setDataType(PropertyConfig.DATA_TYPE_STRING) - .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) - .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES) - .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN) - .build() - ).addProperty(new AppSearchSchema.PropertyConfig.Builder("subject") - .setDataType(PropertyConfig.DATA_TYPE_STRING) - .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) - .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES) - .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN) - .build() - ); - - Exception e = expectThrows(IllegalSchemaException.class, builder::build); - assertThat(e).hasMessageThat().contains("Property defined more than once: subject"); - } -} diff --git a/core/tests/coretests/src/android/app/appsearch/external/app/AppSearchSchemaTest.java b/core/tests/coretests/src/android/app/appsearch/external/app/AppSearchSchemaTest.java new file mode 100644 index 0000000000000..c171270e23ea0 --- /dev/null +++ b/core/tests/coretests/src/android/app/appsearch/external/app/AppSearchSchemaTest.java @@ -0,0 +1,74 @@ +/* + * 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 static org.testng.Assert.expectThrows; + +import android.app.appsearch.AppSearchSchema.PropertyConfig; +import android.app.appsearch.exceptions.IllegalSchemaException; + +import org.junit.Test; + +public class AppSearchSchemaTest { + @Test + public void testInvalidEnums() { + PropertyConfig.Builder builder = new PropertyConfig.Builder("test"); + expectThrows(IllegalArgumentException.class, () -> builder.setDataType(99)); + expectThrows(IllegalArgumentException.class, () -> builder.setCardinality(99)); + } + + @Test + public void testMissingFields() { + PropertyConfig.Builder builder = new PropertyConfig.Builder("test"); + IllegalSchemaException e = expectThrows(IllegalSchemaException.class, builder::build); + assertThat(e).hasMessageThat().contains("Missing field: dataType"); + + builder.setDataType(PropertyConfig.DATA_TYPE_DOCUMENT); + e = expectThrows(IllegalSchemaException.class, builder::build); + assertThat(e).hasMessageThat().contains("Missing field: schemaType"); + + builder.setSchemaType("TestType"); + e = expectThrows(IllegalSchemaException.class, builder::build); + assertThat(e).hasMessageThat().contains("Missing field: cardinality"); + + builder.setCardinality(PropertyConfig.CARDINALITY_REPEATED); + builder.build(); + } + + @Test + public void testDuplicateProperties() { + AppSearchSchema.Builder builder = new AppSearchSchema.Builder("Email") + .addProperty(new PropertyConfig.Builder("subject") + .setDataType(PropertyConfig.DATA_TYPE_STRING) + .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) + .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES) + .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN) + .build() + ); + IllegalSchemaException e = expectThrows(IllegalSchemaException.class, + () -> builder.addProperty(new PropertyConfig.Builder("subject") + .setDataType(PropertyConfig.DATA_TYPE_STRING) + .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) + .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES) + .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN) + .build())); + assertThat(e).hasMessageThat().contains("Property defined more than once: subject"); + } +} diff --git a/services/tests/servicestests/src/com/android/server/appsearch/external/localbackend/converter/SchemaToProtoConverterTest.java b/services/tests/servicestests/src/com/android/server/appsearch/external/localbackend/converter/SchemaToProtoConverterTest.java new file mode 100644 index 0000000000000..8b2fd1ce9feff --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/appsearch/external/localbackend/converter/SchemaToProtoConverterTest.java @@ -0,0 +1,119 @@ +/* + * 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.localbackend.converter; + +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.TermMatchType; + +import org.junit.Test; + +public class SchemaToProtoConverterTest { + @Test + public void testGetProto_Email() { + AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email") + .addProperty(new AppSearchSchema.PropertyConfig.Builder("subject") + .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING) + .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL) + .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_PREFIXES) + .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN) + .build() + ).addProperty(new AppSearchSchema.PropertyConfig.Builder("body") + .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING) + .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL) + .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_PREFIXES) + .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN) + .build() + ).build(); + + SchemaTypeConfigProto expectedEmailProto = SchemaTypeConfigProto.newBuilder() + .setSchemaType("Email") + .addProperties(PropertyConfigProto.newBuilder() + .setPropertyName("subject") + .setDataType(PropertyConfigProto.DataType.Code.STRING) + .setSchemaType("") + .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL) + .setIndexingConfig( + com.android.server.appsearch.proto.IndexingConfig.newBuilder() + .setTokenizerType(IndexingConfig.TokenizerType.Code.PLAIN) + .setTermMatchType(TermMatchType.Code.PREFIX) + ) + ).addProperties(PropertyConfigProto.newBuilder() + .setPropertyName("body") + .setDataType(PropertyConfigProto.DataType.Code.STRING) + .setSchemaType("") + .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL) + .setIndexingConfig( + com.android.server.appsearch.proto.IndexingConfig.newBuilder() + .setTokenizerType(IndexingConfig.TokenizerType.Code.PLAIN) + .setTermMatchType(TermMatchType.Code.PREFIX) + ) + ).build(); + + assertThat(SchemaToProtoConverter.convert(emailSchema)).isEqualTo(expectedEmailProto); + } + + @Test + public void testGetProto_MusicRecording() { + AppSearchSchema musicRecordingSchema = new AppSearchSchema.Builder("MusicRecording") + .addProperty(new AppSearchSchema.PropertyConfig.Builder("artist") + .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING) + .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED) + .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_PREFIXES) + .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN) + .build() + ).addProperty(new AppSearchSchema.PropertyConfig.Builder("pubDate") + .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64) + .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL) + .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE) + .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE) + .build() + ).build(); + + SchemaTypeConfigProto expectedMusicRecordingProto = SchemaTypeConfigProto.newBuilder() + .setSchemaType("MusicRecording") + .addProperties(PropertyConfigProto.newBuilder() + .setPropertyName("artist") + .setDataType(PropertyConfigProto.DataType.Code.STRING) + .setSchemaType("") + .setCardinality(PropertyConfigProto.Cardinality.Code.REPEATED) + .setIndexingConfig( + com.android.server.appsearch.proto.IndexingConfig.newBuilder() + .setTokenizerType(IndexingConfig.TokenizerType.Code.PLAIN) + .setTermMatchType(TermMatchType.Code.PREFIX) + ) + ).addProperties(PropertyConfigProto.newBuilder() + .setPropertyName("pubDate") + .setDataType(PropertyConfigProto.DataType.Code.INT64) + .setSchemaType("") + .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL) + .setIndexingConfig( + com.android.server.appsearch.proto.IndexingConfig.newBuilder() + .setTokenizerType(IndexingConfig.TokenizerType.Code.NONE) + .setTermMatchType(TermMatchType.Code.UNKNOWN) + ) + ).build(); + + assertThat(SchemaToProtoConverter.convert(musicRecordingSchema)) + .isEqualTo(expectedMusicRecordingProto); + } +}