GetSchemaResponse.java

/*
 * Copyright 2021 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 androidx.appsearch.app;

import android.annotation.SuppressLint;
import android.os.Bundle;

import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresFeature;
import androidx.annotation.RestrictTo;
import androidx.collection.ArrayMap;
import androidx.collection.ArraySet;
import androidx.core.util.Preconditions;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

/** The response class of {@link AppSearchSession#getSchemaAsync} */
public final class GetSchemaResponse {
    private static final String VERSION_FIELD = "version";
    private static final String SCHEMAS_FIELD = "schemas";
    private static final String SCHEMAS_NOT_DISPLAYED_BY_SYSTEM_FIELD =
            "schemasNotDisplayedBySystem";
    private static final String SCHEMAS_VISIBLE_TO_PACKAGES_FIELD = "schemasVisibleToPackages";
    private static final String SCHEMAS_VISIBLE_TO_PERMISSION_FIELD =
            "schemasVisibleToPermissions";
    private static final String ALL_REQUIRED_PERMISSION_FIELD =
            "allRequiredPermission";
    /**
     * This Set contains all schemas that are not displayed by the system. All values in the set are
     * prefixed with the package-database prefix. We do lazy fetch, the object will be created
     * when the user first time fetch it.
     */
    @Nullable
    private Set<String> mSchemasNotDisplayedBySystem;
    /**
     * This map contains all schemas and {@link PackageIdentifier} that has access to the schema.
     * All keys in the map are prefixed with the package-database prefix. We do lazy fetch, the
     * object will be created when the user first time fetch it.
     */
    @Nullable
    private Map<String, Set<PackageIdentifier>> mSchemasVisibleToPackages;

    /**
     * This map contains all schemas and Android Permissions combinations that are required to
     * access the schema. All keys in the map are prefixed with the package-database prefix. We
     * do lazy fetch, the object will be created when the user first time fetch it.
     * The Map is constructed in ANY-ALL cases. The querier could read the {@link GenericDocument}
     * objects under the {@code schemaType} if they holds ALL required permissions of ANY
     * combinations.
     * The value set represents
     * {@link androidx.appsearch.app.SetSchemaRequest.AppSearchSupportedPermission}.
     */
    @Nullable
    private Map<String, Set<Set<Integer>>> mSchemasVisibleToPermissions;

    private final Bundle mBundle;

    GetSchemaResponse(@NonNull Bundle bundle) {
        mBundle = Preconditions.checkNotNull(bundle);
    }

    /**
     * Returns the {@link Bundle} populated by this builder.
     * @hide
     */
    @NonNull
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public Bundle getBundle() {
        return mBundle;
    }

    /**
     * Returns the overall database schema version.
     *
     * <p>If the database is empty, 0 will be returned.
     */
    @IntRange(from = 0)
    public int getVersion() {
        return mBundle.getInt(VERSION_FIELD);
    }

    /**
     * Return the schemas most recently successfully provided to
     * {@link AppSearchSession#setSchemaAsync}.
     *
     * <p>It is inefficient to call this method repeatedly.
     */
    @NonNull
    @SuppressWarnings("deprecation")
    public Set<AppSearchSchema> getSchemas() {
        ArrayList<Bundle> schemaBundles = Preconditions.checkNotNull(
                    mBundle.getParcelableArrayList(SCHEMAS_FIELD));
        Set<AppSearchSchema> schemas = new ArraySet<>(schemaBundles.size());
        for (int i = 0; i < schemaBundles.size(); i++) {
            schemas.add(new AppSearchSchema(schemaBundles.get(i)));
        }
        return schemas;
    }

    /**
     * Returns all the schema types that are opted out of being displayed and visible on any
     * system UI surface.
     */
    // @exportToFramework:startStrip()
    @RequiresFeature(
            enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
            name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
    // @exportToFramework:endStrip()
    @NonNull
    public Set<String> getSchemaTypesNotDisplayedBySystem() {
        checkGetVisibilitySettingSupported();
        if (mSchemasNotDisplayedBySystem == null) {
            List<String> schemasNotDisplayedBySystemList =
                    mBundle.getStringArrayList(SCHEMAS_NOT_DISPLAYED_BY_SYSTEM_FIELD);
            mSchemasNotDisplayedBySystem =
                    Collections.unmodifiableSet(new ArraySet<>(schemasNotDisplayedBySystemList));
        }
        return mSchemasNotDisplayedBySystem;
    }

    /**
     * Returns a mapping of schema types to the set of packages that have access
     * to that schema type.
     */
    // @exportToFramework:startStrip()
    @RequiresFeature(
            enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
            name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
    // @exportToFramework:endStrip()
    @NonNull
    @SuppressWarnings("deprecation")
    public Map<String, Set<PackageIdentifier>> getSchemaTypesVisibleToPackages() {
        checkGetVisibilitySettingSupported();
        if (mSchemasVisibleToPackages == null) {
            Bundle schemaVisibleToPackagesBundle = Preconditions.checkNotNull(
                        mBundle.getBundle(SCHEMAS_VISIBLE_TO_PACKAGES_FIELD));
            Map<String, Set<PackageIdentifier>> copy = new ArrayMap<>();
            for (String key : schemaVisibleToPackagesBundle.keySet()) {
                List<Bundle> PackageIdentifierBundles = Preconditions.checkNotNull(
                            schemaVisibleToPackagesBundle.getParcelableArrayList(key));
                Set<PackageIdentifier> packageIdentifiers =
                        new ArraySet<>(PackageIdentifierBundles.size());
                for (int i = 0; i < PackageIdentifierBundles.size(); i++) {
                    packageIdentifiers.add(new PackageIdentifier(PackageIdentifierBundles.get(i)));
                }
                copy.put(key, packageIdentifiers);
            }
            mSchemasVisibleToPackages = Collections.unmodifiableMap(copy);
        }
        return mSchemasVisibleToPackages;
    }

    /**
     * Returns a mapping of schema types to the Map of {@link android.Manifest.permission}
     * combinations that querier must hold to access that schema type.
     *
     * <p> The querier could read the {@link GenericDocument} objects under the {@code schemaType}
     * if they holds ALL required permissions of ANY of the individual value sets.
     *
     * <p>For example, if the Map contains {@code {% verbatim %}{{permissionA, PermissionB},
     * { PermissionC, PermissionD}, {PermissionE}}{% endverbatim %}}.
     * <ul>
     *     <li>A querier holds both PermissionA and PermissionB has access.</li>
     *     <li>A querier holds both PermissionC and PermissionD has access.</li>
     *     <li>A querier holds only PermissionE has access.</li>
     *     <li>A querier holds both PermissionA and PermissionE has access.</li>
     *     <li>A querier holds only PermissionA doesn't have access.</li>
     *     <li>A querier holds both PermissionA and PermissionC doesn't have access.</li>
     * </ul>
     *
     * @return The map contains schema type and all combinations of required permission for querier
     *         to access it. The supported Permission are {@link SetSchemaRequest#READ_SMS},
     *         {@link SetSchemaRequest#READ_CALENDAR}, {@link SetSchemaRequest#READ_CONTACTS},
     *         {@link SetSchemaRequest#READ_EXTERNAL_STORAGE},
     *         {@link SetSchemaRequest#READ_HOME_APP_SEARCH_DATA} and
     *         {@link SetSchemaRequest#READ_ASSISTANT_APP_SEARCH_DATA}.
     */
    // @exportToFramework:startStrip()
    @RequiresFeature(
            enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
            name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
    // @exportToFramework:endStrip()
    @NonNull
    @SuppressWarnings("deprecation")
    public Map<String, Set<Set<Integer>>> getRequiredPermissionsForSchemaTypeVisibility() {
        checkGetVisibilitySettingSupported();
        if (mSchemasVisibleToPermissions == null) {
            Map<String, Set<Set<Integer>>> copy = new ArrayMap<>();
            Bundle schemaVisibleToPermissionBundle = Preconditions.checkNotNull(
                        mBundle.getBundle(SCHEMAS_VISIBLE_TO_PERMISSION_FIELD));
            for (String key : schemaVisibleToPermissionBundle.keySet()) {
                ArrayList<Bundle> allRequiredPermissionsBundle =
                        schemaVisibleToPermissionBundle.getParcelableArrayList(key);
                Set<Set<Integer>> visibleToPermissions = new ArraySet<>();
                if (allRequiredPermissionsBundle != null) {
                    // This should never be null
                    for (int i = 0; i < allRequiredPermissionsBundle.size(); i++) {
                        visibleToPermissions.add(new ArraySet<>(allRequiredPermissionsBundle.get(i)
                                .getIntegerArrayList(ALL_REQUIRED_PERMISSION_FIELD)));
                    }
                }
                copy.put(key, visibleToPermissions);
            }
            mSchemasVisibleToPermissions = Collections.unmodifiableMap(copy);
        }
        return mSchemasVisibleToPermissions;
    }

    private void checkGetVisibilitySettingSupported() {
        if (!mBundle.containsKey(SCHEMAS_VISIBLE_TO_PACKAGES_FIELD)) {
            throw new UnsupportedOperationException("Get visibility setting is not supported with"
                    + " this backend/Android API level combination.");
        }
    }

    /** Builder for {@link GetSchemaResponse} objects. */
    public static final class Builder {
        private int mVersion = 0;
        private ArrayList<Bundle> mSchemaBundles = new ArrayList<>();
        /**
         * Creates the object when we actually set them. If we never set visibility settings, we
         * should throw {@link UnsupportedOperationException} in the visibility getters.
         */
        @Nullable
        private ArrayList<String> mSchemasNotDisplayedBySystem;
        private Bundle mSchemasVisibleToPackages;
        private Bundle mSchemasVisibleToPermissions;
        private boolean mBuilt = false;

        /** Create a {@link Builder} object} */
        public Builder() {
            this(/*getVisibilitySettingSupported=*/true);
        }

        /**
         * Create a {@link Builder} object}.
         *
         * <p>This constructor should only be used in Android API below than T.
         *
         * @param getVisibilitySettingSupported whether supported
         * {@link Features#ADD_PERMISSIONS_AND_GET_VISIBILITY} by this
         *                                      backend/Android API level.
         * @hide
         */
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
        public Builder(boolean getVisibilitySettingSupported) {
            if (getVisibilitySettingSupported) {
                mSchemasNotDisplayedBySystem = new ArrayList<>();
                mSchemasVisibleToPackages = new Bundle();
                mSchemasVisibleToPermissions = new Bundle();
            }
        }

        /**
         * Sets the database overall schema version.
         *
         * <p>Default version is 0
         */
        @NonNull
        public Builder setVersion(@IntRange(from = 0) int version) {
            resetIfBuilt();
            mVersion = version;
            return this;
        }

        /**  Adds one {@link AppSearchSchema} to the schema list.  */
        @NonNull
        public Builder addSchema(@NonNull AppSearchSchema schema) {
            Preconditions.checkNotNull(schema);
            resetIfBuilt();
            mSchemaBundles.add(schema.getBundle());
            return this;
        }

        /**
         * Sets whether or not documents from the provided {@code schemaType} will be displayed
         * and visible on any system UI surface.
         *
         * @param schemaType The name of an {@link AppSearchSchema} within the same
         *                   {@link GetSchemaResponse}, which won't be displayed by system.
         */
        // Getter getSchemaTypesNotDisplayedBySystem returns plural objects.
        @SuppressLint("MissingGetterMatchingBuilder")
        @NonNull
        public Builder addSchemaTypeNotDisplayedBySystem(@NonNull String schemaType) {
            Preconditions.checkNotNull(schemaType);
            resetIfBuilt();
            if (mSchemasNotDisplayedBySystem == null) {
                mSchemasNotDisplayedBySystem = new ArrayList<>();
            }
            mSchemasNotDisplayedBySystem.add(schemaType);
            return this;
        }

        /**
         * Sets whether or not documents from the provided {@code schemaType} can be read by the
         * specified package.
         *
         * <p>Each package is represented by a {@link PackageIdentifier}, containing a package name
         * and a byte array of type {@link android.content.pm.PackageManager#CERT_INPUT_SHA256}.
         *
         * <p>To opt into one-way data sharing with another application, the developer will need to
         * explicitly grant the other application’s package name and certificate Read access to its
         * data.
         *
         * <p>For two-way data sharing, both applications need to explicitly grant Read access to
         * one another.
         *
         * @param schemaType               The schema type to set visibility on.
         * @param packageIdentifiers       Represents the package that has access to the given
         *                                 schema type.
         */
        // Getter getSchemaTypesVisibleToPackages returns a map contains all schema types.
        @SuppressLint("MissingGetterMatchingBuilder")
        @NonNull
        public Builder setSchemaTypeVisibleToPackages(
                @NonNull String schemaType,
                @NonNull Set<PackageIdentifier> packageIdentifiers) {
            Preconditions.checkNotNull(schemaType);
            Preconditions.checkNotNull(packageIdentifiers);
            resetIfBuilt();
            ArrayList<Bundle> bundles = new ArrayList<>(packageIdentifiers.size());
            for (PackageIdentifier packageIdentifier : packageIdentifiers) {
                bundles.add(packageIdentifier.getBundle());
            }
            mSchemasVisibleToPackages.putParcelableArrayList(schemaType, bundles);
            return this;
        }

        /**
         * Sets a set of required {@link android.Manifest.permission} combinations to the given
         * schema type.
         *
         * <p> The querier could read the {@link GenericDocument} objects under the
         * {@code schemaType} if they holds ALL required permissions of ANY of the individual value
         * sets.
         *
         * <p>For example, if the Map contains {@code {% verbatim %}{{permissionA, PermissionB},
         * {PermissionC, PermissionD}, {PermissionE}}{% endverbatim %}}.
         * <ul>
         *     <li>A querier holds both PermissionA and PermissionB has access.</li>
         *     <li>A querier holds both PermissionC and PermissionD has access.</li>
         *     <li>A querier holds only PermissionE has access.</li>
         *     <li>A querier holds both PermissionA and PermissionE has access.</li>
         *     <li>A querier holds only PermissionA doesn't have access.</li>
         *     <li>A querier holds both PermissionA and PermissionC doesn't have access.</li>
         * </ul>
         *
         * @see android.Manifest.permission#READ_SMS
         * @see android.Manifest.permission#READ_CALENDAR
         * @see android.Manifest.permission#READ_CONTACTS
         * @see android.Manifest.permission#READ_EXTERNAL_STORAGE
         * @see android.Manifest.permission#READ_HOME_APP_SEARCH_DATA
         * @see android.Manifest.permission#READ_ASSISTANT_APP_SEARCH_DATA
         *
         * @param schemaType             The schema type to set visibility on.
         * @param visibleToPermissions   The Android permissions that will be required to access
         *                               the given schema.
         */
        // Getter getRequiredPermissionsForSchemaTypeVisibility returns a map for all schemaTypes.
        @SuppressLint("MissingGetterMatchingBuilder")
        @NonNull
        public Builder setRequiredPermissionsForSchemaTypeVisibility(
                @NonNull String schemaType,
                @SetSchemaRequest.AppSearchSupportedPermission @NonNull
                        Set<Set<Integer>> visibleToPermissions) {
            Preconditions.checkNotNull(schemaType);
            Preconditions.checkNotNull(visibleToPermissions);
            resetIfBuilt();
            ArrayList<Bundle> visibleToPermissionsBundle = new ArrayList<>();
            for (Set<Integer> allRequiredPermissions : visibleToPermissions) {
                for (int permission : allRequiredPermissions) {
                    Preconditions.checkArgumentInRange(permission, SetSchemaRequest.READ_SMS,
                            SetSchemaRequest.READ_ASSISTANT_APP_SEARCH_DATA, "permission");
                }
                Bundle allRequiredPermissionsBundle = new Bundle();
                allRequiredPermissionsBundle.putIntegerArrayList(
                        ALL_REQUIRED_PERMISSION_FIELD, new ArrayList<>(allRequiredPermissions));
                visibleToPermissionsBundle.add(allRequiredPermissionsBundle);
            }
            mSchemasVisibleToPermissions.putParcelableArrayList(schemaType,
                    visibleToPermissionsBundle);
            return this;
        }

        /** Builds a {@link GetSchemaResponse} object. */
        @NonNull
        public GetSchemaResponse build() {
            Bundle bundle = new Bundle();
            bundle.putInt(VERSION_FIELD, mVersion);
            bundle.putParcelableArrayList(SCHEMAS_FIELD, mSchemaBundles);
            if (mSchemasNotDisplayedBySystem != null) {
                // Only save the visibility fields if it was actually set.
                bundle.putStringArrayList(SCHEMAS_NOT_DISPLAYED_BY_SYSTEM_FIELD,
                        mSchemasNotDisplayedBySystem);
                bundle.putBundle(SCHEMAS_VISIBLE_TO_PACKAGES_FIELD, mSchemasVisibleToPackages);
                bundle.putBundle(SCHEMAS_VISIBLE_TO_PERMISSION_FIELD, mSchemasVisibleToPermissions);
            }
            mBuilt = true;
            return new GetSchemaResponse(bundle);
        }

        private void resetIfBuilt() {
            if (mBuilt) {
                mSchemaBundles = new ArrayList<>(mSchemaBundles);
                if (mSchemasNotDisplayedBySystem != null) {
                    // Only reset the visibility fields if it was actually set.
                    mSchemasNotDisplayedBySystem = new ArrayList<>(mSchemasNotDisplayedBySystem);
                    Bundle copyVisibleToPackages = new Bundle();
                    copyVisibleToPackages.putAll(mSchemasVisibleToPackages);
                    mSchemasVisibleToPackages = copyVisibleToPackages;
                    Bundle copyVisibleToPermissions = new Bundle();
                    copyVisibleToPermissions.putAll(mSchemasVisibleToPermissions);
                    mSchemasVisibleToPermissions = copyVisibleToPermissions;
                }
                mBuilt = false;
            }
        }
    }
}