Merge Jetpack AppSearchSchema work from last two quarters.

Bug: 162450968
Test: AppSearchManagerTest, AppSearchSchemaTest
Change-Id: Iec02a202e01db5d92fe04b5c4abca3aff87e7000
This commit is contained in:
Alexander Dorokhine
2020-10-06 01:39:36 -07:00
parent e3c1e22e32
commit 6cfcc57428
11 changed files with 482 additions and 285 deletions

View File

@@ -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<Void> setSchema(
@NonNull List<AppSearchSchema> 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<Bundle> schemaBundles = new ArrayList<>(schemas.size());
for (AppSearchSchema schema : schemas) {
schemaBundles.add(schema.getBundle());
}
AndroidFuture<AppSearchResult> future = new AndroidFuture<>();
try {
mService.setSchema(schemaBytes, forceOverride, future);
mService.setSchema(schemaBundles, forceOverride, future);
} catch (RemoteException e) {
future.completeExceptionally(e);
}

View File

@@ -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;
* <p>For example, an e-mail message or a music recording could be a schema type.
*
* <p>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<Bundle> mProperties = new ArrayList<>();
private final Set<String> 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<String> 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.
*
* <p>Only required when {@link #setDataType(int)} is set to
* <p>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);
}
}
}

View File

@@ -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}&lt;{@link AppSearchResult}&lt;{@link Void}&gt&gt;.
* The results of the call.
*/
void setSchema(
in byte[] schemaBytes, boolean forceOverride, in AndroidFuture<AppSearchResult> callback);
in List<Bundle> schemaBundles,
boolean forceOverride,
in AndroidFuture<AppSearchResult> callback);
/**
* Inserts documents into the index.

View File

@@ -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;

View File

@@ -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}.

View File

@@ -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}.

View File

@@ -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<Bundle> schemaBundles,
boolean forceOverride,
@NonNull AndroidFuture<AppSearchResult> 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));

View File

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

View File

@@ -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");
}
}

View File

@@ -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");
}
}

View File

@@ -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);
}
}