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

Commit 0dd233f3 authored by Alexander Dorokhine's avatar Alexander Dorokhine
Browse files

Merge over AppSearchResult, AppSearchBatchResult and setSchema error improvements.

Bug: 162450968
Test: AppSearchSchemaTest#testRemoveSchema
Change-Id: I724dc01e24acc45f10b416eb4b6728733a4b2b1b
parent 2c9639a3
Loading
Loading
Loading
Loading
+40 −11
Original line number Diff line number Diff line
/*
 * 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.
@@ -22,6 +22,8 @@ import android.os.Parcel;
import android.os.Parcelable;
import android.util.ArrayMap;

import com.android.internal.util.Preconditions;

import java.util.Collections;
import java.util.Map;

@@ -33,11 +35,11 @@ import java.util.Map;
 * @param <ValueType> The type of result objects associated with the keys.
 * @hide
 */
public class AppSearchBatchResult<KeyType, ValueType> implements Parcelable {
public final class AppSearchBatchResult<KeyType, ValueType> implements Parcelable {
    @NonNull private final Map<KeyType, ValueType> mSuccesses;
    @NonNull private final Map<KeyType, AppSearchResult<ValueType>> mFailures;

    private AppSearchBatchResult(
    AppSearchBatchResult(
            @NonNull Map<KeyType, ValueType> successes,
            @NonNull Map<KeyType, AppSearchResult<ValueType>> failures) {
        mSuccesses = successes;
@@ -61,8 +63,8 @@ public class AppSearchBatchResult<KeyType, ValueType> implements Parcelable {
    }

    /**
     * Returns a {@link Map} of all successful keys mapped to the successful {@link ValueType}
     * values they produced.
     * Returns a {@link Map} of all successful keys mapped to the successful
     * {@link AppSearchResult}s they produced.
     *
     * <p>The values of the {@link Map} will not be {@code null}.
     */
@@ -82,6 +84,22 @@ public class AppSearchBatchResult<KeyType, ValueType> implements Parcelable {
        return mFailures;
    }

    /**
     * Asserts that this {@link AppSearchBatchResult} has no failures.
     * @hide
     */
    public void checkSuccess() {
        if (!isSuccess()) {
            throw new IllegalStateException("AppSearchBatchResult has failures: " + this);
        }
    }

    @Override
    @NonNull
    public String toString() {
        return "{\n  successes: " + mSuccesses + "\n  failures: " + mFailures + "\n}";
    }

    @Override
    public int describeContents() {
        return 0;
@@ -112,16 +130,18 @@ public class AppSearchBatchResult<KeyType, ValueType> implements Parcelable {
    public static final class Builder<KeyType, ValueType> {
        private final Map<KeyType, ValueType> mSuccesses = new ArrayMap<>();
        private final Map<KeyType, AppSearchResult<ValueType>> mFailures = new ArrayMap<>();

        /** Creates a new {@link Builder} for this {@link AppSearchBatchResult}. */
        public Builder() {}
        private boolean mBuilt = false;

        /**
         * Associates the {@code key} with the given successful return value.
         *
         * <p>Any previous mapping for a key, whether success or failure, is deleted.
         */
        public Builder setSuccess(@NonNull KeyType key, @Nullable ValueType result) {
        @NonNull
        public Builder<KeyType, ValueType> setSuccess(
                @NonNull KeyType key, @Nullable ValueType result) {
            Preconditions.checkState(!mBuilt, "Builder has already been used");
            Preconditions.checkNotNull(key);
            return setResult(key, AppSearchResult.newSuccessfulResult(result));
        }

@@ -130,10 +150,13 @@ public class AppSearchBatchResult<KeyType, ValueType> implements Parcelable {
         *
         * <p>Any previous mapping for a key, whether success or failure, is deleted.
         */
        public Builder setFailure(
        @NonNull
        public Builder<KeyType, ValueType> setFailure(
                @NonNull KeyType key,
                @AppSearchResult.ResultCode int resultCode,
                @Nullable String errorMessage) {
            Preconditions.checkState(!mBuilt, "Builder has already been used");
            Preconditions.checkNotNull(key);
            return setResult(key, AppSearchResult.newFailedResult(resultCode, errorMessage));
        }

@@ -143,7 +166,11 @@ public class AppSearchBatchResult<KeyType, ValueType> implements Parcelable {
         * <p>Any previous mapping for a key, whether success or failure, is deleted.
         */
        @NonNull
        public Builder setResult(@NonNull KeyType key, @NonNull AppSearchResult<ValueType> result) {
        public Builder<KeyType, ValueType> setResult(
                @NonNull KeyType key, @NonNull AppSearchResult<ValueType> result) {
            Preconditions.checkState(!mBuilt, "Builder has already been used");
            Preconditions.checkNotNull(key);
            Preconditions.checkNotNull(result);
            if (result.isSuccess()) {
                mSuccesses.put(key, result.getResultValue());
                mFailures.remove(key);
@@ -157,6 +184,8 @@ public class AppSearchBatchResult<KeyType, ValueType> implements Parcelable {
        /** Builds an {@link AppSearchBatchResult} from the contents of this {@link Builder}. */
        @NonNull
        public AppSearchBatchResult<KeyType, ValueType> build() {
            Preconditions.checkState(!mBuilt, "Builder has already been used");
            mBuilt = true;
            return new AppSearchBatchResult<>(mSuccesses, mFailures);
        }
    }
+19 −13
Original line number Diff line number Diff line
/*
 * 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.
@@ -32,9 +32,12 @@ import java.util.Objects;
 * @param <ValueType> The type of result object for successful calls.
 * @hide
 */
public class AppSearchResult<ValueType> implements Parcelable {
    /** Result codes from {@link AppSearchManager} methods. */
    @IntDef(prefix = {"RESULT_"}, value = {
public final class AppSearchResult<ValueType> implements Parcelable {
    /**
     * Result codes from {@link AppSearchManager} methods.
     * @hide
     */
    @IntDef(value = {
            RESULT_OK,
            RESULT_UNKNOWN_ERROR,
            RESULT_INTERNAL_ERROR,
@@ -120,15 +123,18 @@ public class AppSearchResult<ValueType> implements Parcelable {
    }

    /**
     * Returns the returned value associated with this result.
     * Returns the result value associated with this result, if it was successful.
     *
     * <p>If {@link #isSuccess} is {@code false}, the result value is always {@code null}. The value
     * may be {@code null} even if {@link #isSuccess} is {@code true}. See the documentation of the
     * particular {@link AppSearchManager} call producing this {@link AppSearchResult} for what is
     * returned by {@link #getResultValue}.
     * <p>See the documentation of the particular {@link AppSearchManager} call producing this
     * {@link AppSearchResult} for what is placed in the result value by that call.
     *
     * @throws IllegalStateException if this {@link AppSearchResult} is not successful.
     */
    @Nullable
    public ValueType getResultValue() {
        if (!isSuccess()) {
            throw new IllegalStateException("AppSearchResult is a failure: " + this);
        }
        return mResultValue;
    }

@@ -146,14 +152,14 @@ public class AppSearchResult<ValueType> implements Parcelable {
    }

    @Override
    public boolean equals(Object other) {
    public boolean equals(@Nullable Object other) {
        if (this == other) {
            return true;
        }
        if (!(other instanceof AppSearchResult)) {
            return false;
        }
        AppSearchResult<?> otherResult = (AppSearchResult) other;
        AppSearchResult<?> otherResult = (AppSearchResult<?>) other;
        return mResultCode == otherResult.mResultCode
                && Objects.equals(mResultValue, otherResult.mResultValue)
                && Objects.equals(mErrorMessage, otherResult.mErrorMessage);
@@ -168,9 +174,9 @@ public class AppSearchResult<ValueType> implements Parcelable {
    @NonNull
    public String toString() {
        if (isSuccess()) {
            return "AppSearchResult [SUCCESS]: " + mResultValue;
            return "[SUCCESS]: " + mResultValue;
        }
        return "AppSearchResult [FAILURE(" + mResultCode + ")]: " + mErrorMessage;
        return "[FAILURE(" + mResultCode + ")]: " + mErrorMessage;
    }

    @Override
+4 −0
Original line number Diff line number Diff line
@@ -54,6 +54,10 @@ public class AppSearchException extends Exception {
        mResultCode = resultCode;
    }

    public @AppSearchResult.ResultCode int getResultCode() {
        return mResultCode;
    }

    /**
     * Converts this {@link java.lang.Exception} into a failed {@link AppSearchResult}
     */
+1 −3
Original line number Diff line number Diff line
@@ -69,9 +69,7 @@ public final class ImplInstanceManager {
    private static AppSearchImpl createImpl(@NonNull Context context, @UserIdInt int userId)
            throws AppSearchException {
        File appSearchDir = getAppSearchDir(context, userId);
        AppSearchImpl appSearchImpl = new AppSearchImpl(appSearchDir);
        appSearchImpl.initialize();
        return appSearchImpl;
        return AppSearchImpl.create(appSearchDir);
    }

    private static File getAppSearchDir(@NonNull Context context, @UserIdInt int userId) {
+52 −83
Original line number Diff line number Diff line
@@ -18,7 +18,6 @@ package com.android.server.appsearch.external.localbackend;

import android.util.Log;

import android.annotation.AnyThread;
import com.android.internal.annotations.GuardedBy;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -27,6 +26,7 @@ import com.android.internal.annotations.VisibleForTesting;
import android.annotation.WorkerThread;
import android.app.appsearch.AppSearchResult;
import android.app.appsearch.exceptions.AppSearchException;
import com.android.internal.util.Preconditions;

import com.google.android.icing.IcingSearchEngine;
import com.google.android.icing.proto.DeleteByNamespaceResultProto;
@@ -58,7 +58,6 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

@@ -66,8 +65,7 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
 * Manages interaction with the native IcingSearchEngine and other components to implement AppSearch
 * functionality.
 *
 * <p>Callers should call {@link #initialize} before using the AppSearchImpl instance. Never create
 * two instances using the same folder.
 * <p>Never create two instances using the same folder.
 *
 * <p>A single instance of {@link AppSearchImpl} can support all databases. Schemas and documents
 * are physically saved together in {@link IcingSearchEngine}, but logically isolated:
@@ -106,9 +104,7 @@ public final class AppSearchImpl {
    static final int CHECK_OPTIMIZE_INTERVAL = 100;

    private final ReadWriteLock mReadWriteLock = new ReentrantReadWriteLock();
    private final CountDownLatch mInitCompleteLatch = new CountDownLatch(1);
    private final File mIcingDir;
    private IcingSearchEngine mIcingSearchEngine;
    private final IcingSearchEngine mIcingSearchEngine;

    // The map contains schemaTypes and namespaces for all database. All values in the map have
    // been already added database name prefix.
@@ -121,33 +117,24 @@ public final class AppSearchImpl {
     */
    private int mOptimizeIntervalCount = 0;

    /** Creates an instance of {@link AppSearchImpl} which writes data to the given folder. */
    @AnyThread
    public AppSearchImpl(@NonNull File icingDir) {
        mIcingDir = icingDir;
    }

    /**
     * Initializes the underlying IcingSearchEngine.
     *
     * <p>This method belongs to mutate group.
     *
     * @throws AppSearchException on IcingSearchEngine error.
     * Creates and initializes an instance of {@link AppSearchImpl} which writes data to the given
     * folder.
     */
    public void initialize() throws AppSearchException {
        if (isInitialized()) {
            return;
    @NonNull
    public static AppSearchImpl create(@NonNull File icingDir) throws AppSearchException {
        Preconditions.checkNotNull(icingDir);
        return new AppSearchImpl(icingDir);
    }

    private AppSearchImpl(@NonNull File icingDir) throws AppSearchException {
        boolean isReset = false;
        mReadWriteLock.writeLock().lock();
        try {
            // We synchronize here because we don't want to call IcingSearchEngine.initialize() more
            // than once. It's unnecessary and can be a costly operation.
            if (isInitialized()) {
                return;
            }
            IcingSearchEngineOptions options = IcingSearchEngineOptions.newBuilder()
                    .setBaseDir(mIcingDir.getAbsolutePath()).build();
                    .setBaseDir(icingDir.getAbsolutePath()).build();
            mIcingSearchEngine = new IcingSearchEngine(options);

            InitializeResultProto initializeResultProto = mIcingSearchEngine.initialize();
@@ -170,7 +157,8 @@ public final class AppSearchImpl {
            for (String qualifiedNamespace : getAllNamespacesResultProto.getNamespacesList()) {
                addToMap(mNamespaceMap, getDatabaseName(qualifiedNamespace), qualifiedNamespace);
            }
            mInitCompleteLatch.countDown();
            // TODO(b/155939114): It's possible to optimize after init, which would reduce the time
            //   to when we're able to serve queries. Consider moving this optimize call out.
            if (!isReset) {
                checkForOptimize(/* force= */ true);
            }
@@ -179,12 +167,6 @@ public final class AppSearchImpl {
        }
    }

    /** Checks if the internal state of {@link AppSearchImpl} has been initialized. */
    @AnyThread
    public boolean isInitialized() {
        return mInitCompleteLatch.getCount() == 0;
    }

    /**
     * Updates the AppSearch schema for this app.
     *
@@ -195,12 +177,9 @@ public final class AppSearchImpl {
     * @param forceOverride Whether to force-apply the schema even if it is incompatible. Documents
     *                      which do not comply with the new schema will be deleted.
     * @throws AppSearchException on IcingSearchEngine error.
     * @throws InterruptedException if the current thread was interrupted during execution.
     */
    public void setSchema(@NonNull String databaseName, @NonNull SchemaProto origSchema,
            boolean forceOverride) throws AppSearchException, InterruptedException {
        awaitInitialized();

            boolean forceOverride) throws AppSearchException {
        SchemaProto schemaProto = getSchemaProto();

        SchemaProto.Builder existingSchemaBuilder = schemaProto.toBuilder();
@@ -212,10 +191,32 @@ public final class AppSearchImpl {
        SetSchemaResultProto setSchemaResultProto;
        mReadWriteLock.writeLock().lock();
        try {
            setSchemaResultProto = mIcingSearchEngine.setSchema(existingSchemaBuilder.build(),
                    forceOverride);
            // Apply schema
            setSchemaResultProto =
                    mIcingSearchEngine.setSchema(existingSchemaBuilder.build(), forceOverride);

            // Determine whether it succeeded.
            try {
                checkSuccess(setSchemaResultProto.getStatus());
            } catch (AppSearchException e) {
                // Improve the error message by merging in information about incompatible types.
                if (setSchemaResultProto.getDeletedSchemaTypesCount() > 0
                        || setSchemaResultProto.getIncompatibleSchemaTypesCount() > 0) {
                    String newMessage = e.getMessage()
                            + "\n  Deleted types: "
                            + setSchemaResultProto.getDeletedSchemaTypesList()
                            + "\n  Incompatible types: "
                            + setSchemaResultProto.getIncompatibleSchemaTypesList();
                    throw new AppSearchException(e.getResultCode(), newMessage, e.getCause());
                } else {
                    throw e;
                }
            }

            // Update derived data structures.
            mSchemaMap.put(databaseName, newTypeNames);

            // Determine whether to schedule an immediate optimize.
            if (setSchemaResultProto.getDeletedSchemaTypesCount() > 0
                    || (setSchemaResultProto.getIncompatibleSchemaTypesCount() > 0
                    && forceOverride)) {
@@ -237,12 +238,9 @@ public final class AppSearchImpl {
     * @param databaseName The databaseName this document resides in.
     * @param document     The document to index.
     * @throws AppSearchException on IcingSearchEngine error.
     * @throws InterruptedException if the current thread was interrupted during execution.
     */
    public void putDocument(@NonNull String databaseName, @NonNull DocumentProto document)
            throws AppSearchException, InterruptedException {
        awaitInitialized();

            throws AppSearchException {
        DocumentProto.Builder documentBuilder = document.toBuilder();
        rewriteDocumentTypes(getDatabasePrefix(databaseName), documentBuilder, /*add=*/ true);

@@ -270,12 +268,10 @@ public final class AppSearchImpl {
     * @param uri          The URI of the document to get.
     * @return The Document contents, or {@code null} if no such URI exists in the system.
     * @throws AppSearchException on IcingSearchEngine error.
     * @throws InterruptedException if the current thread was interrupted during execution.
     */
    @Nullable
    public DocumentProto getDocument(@NonNull String databaseName, @NonNull String namespace,
            @NonNull String uri) throws AppSearchException, InterruptedException {
        awaitInitialized();
            @NonNull String uri) throws AppSearchException {
        GetResultProto getResultProto;
        mReadWriteLock.readLock().lock();
        try {
@@ -303,16 +299,13 @@ public final class AppSearchImpl {
     * @return The results of performing this search  The proto might have no {@code results} if no
     * documents matched the query.
     * @throws AppSearchException on IcingSearchEngine error.
     * @throws InterruptedException if the current thread was interrupted during execution.
     */
    @NonNull
    public SearchResultProto query(
            @NonNull String databaseName,
            @NonNull SearchSpecProto searchSpec,
            @NonNull ResultSpecProto resultSpec,
            @NonNull ScoringSpecProto scoringSpec) throws AppSearchException, InterruptedException {
        awaitInitialized();

            @NonNull ScoringSpecProto scoringSpec) throws AppSearchException {
        SearchSpecProto.Builder searchSpecBuilder = searchSpec.toBuilder();
        SearchResultProto searchResultProto;
        mReadWriteLock.readLock().lock();
@@ -347,13 +340,10 @@ public final class AppSearchImpl {
     * @param nextPageToken The token of pre-loaded results of previously executed query.
     * @return The next page of results of previously executed query.
     * @throws AppSearchException on IcingSearchEngine error.
     * @throws InterruptedException if the current thread was interrupted during execution.
     */
    @NonNull
    public SearchResultProto getNextPage(@NonNull String databaseName, long nextPageToken)
            throws AppSearchException, InterruptedException {
        awaitInitialized();

            throws AppSearchException {
        SearchResultProto searchResultProto = mIcingSearchEngine.getNextPage(nextPageToken);
        checkSuccess(searchResultProto.getStatus());
        if (searchResultProto.getResultsCount() == 0) {
@@ -367,8 +357,7 @@ public final class AppSearchImpl {
     * @param nextPageToken The token of pre-loaded results of previously executed query to be
     *                      Invalidated.
     */
    public void invalidateNextPageToken(long nextPageToken) throws InterruptedException {
        awaitInitialized();
    public void invalidateNextPageToken(long nextPageToken) {
        mIcingSearchEngine.invalidateNextPageToken(nextPageToken);
    }

@@ -381,12 +370,9 @@ public final class AppSearchImpl {
     * @param namespace    Namespace of the document to remove.
     * @param uri          URI of the document to remove.
     * @throws AppSearchException on IcingSearchEngine error.
     * @throws InterruptedException if the current thread was interrupted during execution.
     */
    public void remove(@NonNull String databaseName, @NonNull String namespace,
            @NonNull String uri) throws AppSearchException, InterruptedException {
        awaitInitialized();

            @NonNull String uri) throws AppSearchException {
        String qualifiedNamespace = getDatabasePrefix(databaseName) + namespace;
        DeleteResultProto deleteResultProto;
        mReadWriteLock.writeLock().lock();
@@ -407,12 +393,9 @@ public final class AppSearchImpl {
     * @param databaseName The databaseName that contains documents of schemaType.
     * @param schemaType   The schemaType of documents to remove.
     * @throws AppSearchException on IcingSearchEngine error.
     * @throws InterruptedException if the current thread was interrupted during execution.
     */
    public void removeByType(@NonNull String databaseName, @NonNull String schemaType)
            throws AppSearchException, InterruptedException {
        awaitInitialized();

            throws AppSearchException {
        String qualifiedType = getDatabasePrefix(databaseName) + schemaType;
        DeleteBySchemaTypeResultProto deleteBySchemaTypeResultProto;
        mReadWriteLock.writeLock().lock();
@@ -437,12 +420,9 @@ public final class AppSearchImpl {
     * @param databaseName The databaseName that contains documents of namespace.
     * @param namespace    The namespace of documents to remove.
     * @throws AppSearchException on IcingSearchEngine error.
     * @throws InterruptedException if the current thread was interrupted during execution.
     */
    public void removeByNamespace(@NonNull String databaseName, @NonNull String namespace)
            throws AppSearchException, InterruptedException {
        awaitInitialized();

            throws AppSearchException {
        String qualifiedNamespace = getDatabasePrefix(databaseName) + namespace;
        DeleteByNamespaceResultProto deleteByNamespaceResultProto;
        mReadWriteLock.writeLock().lock();
@@ -469,11 +449,9 @@ public final class AppSearchImpl {
     *
     * @param databaseName The databaseName to remove all documents from.
     * @throws AppSearchException on IcingSearchEngine error.
     * @throws InterruptedException if the current thread was interrupted during execution.
     */
    public void removeAll(@NonNull String databaseName)
            throws AppSearchException, InterruptedException {
        awaitInitialized();
            throws AppSearchException {
        mReadWriteLock.writeLock().lock();
        try {
            Set<String> existingNamespaces = mNamespaceMap.get(databaseName);
@@ -732,15 +710,6 @@ public final class AppSearchImpl {
        values.add(prefixedValue);
    }

    /**
     * Waits for the instance to become initialized.
     *
     * @throws InterruptedException if the current thread was interrupted during waiting.
     */
    private void awaitInitialized() throws InterruptedException {
        mInitCompleteLatch.await();
    }

    /**
     * Checks the given status code and throws an {@link AppSearchException} if code is an error.
     *
Loading