Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6cfcc574 authored by Alexander Dorokhine's avatar Alexander Dorokhine
Browse files

Merge Jetpack AppSearchSchema work from last two quarters.

Bug: 162450968
Test: AppSearchManagerTest, AppSearchSchemaTest
Change-Id: Iec02a202e01db5d92fe04b5c4abca3aff87e7000
parent e3c1e22e
Loading
Loading
Loading
Loading
+6 −10
Original line number Diff line number Diff line
@@ -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);
        }
+127 −93
Original line number Diff line number Diff line
/*
 * 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.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 android.app.appsearch.exceptions.IllegalSchemaException;
import android.util.ArraySet;
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);
            }
        }
    }
+6 −2
Original line number Diff line number Diff line
@@ -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.
+1 −0
Original line number Diff line number Diff line
@@ -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;
+4 −2
Original line number Diff line number Diff line
/*
 * 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}.
Loading