diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchBatchResult.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchBatchResult.java new file mode 100644 index 0000000000000..773db93466251 --- /dev/null +++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchBatchResult.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 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 android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.ArrayMap; + +import java.util.Collections; +import java.util.Map; + +/** + * Provides access to multiple results from a batch operation accepting multiple inputs. + * + * @param The type of the keys for {@link #getResults} and {@link #getFailures}. + * @param The type of result objects associated with the keys. + * @hide + */ +public class AppSearchBatchResult implements Parcelable { + @NonNull private final Map mResults; + @NonNull private final Map mFailures; + + private AppSearchBatchResult( + @NonNull Map results, @NonNull Map failures) { + mResults = results; + mFailures = failures; + } + + private AppSearchBatchResult(@NonNull Parcel in) { + mResults = Collections.unmodifiableMap(in.readHashMap(/*loader=*/ null)); + mFailures = Collections.unmodifiableMap(in.readHashMap(/*loader=*/ null)); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeMap(mResults); + dest.writeMap(mFailures); + } + + /** Returns {@code true} if this {@link AppSearchBatchResult} has no failures. */ + public boolean isSuccess() { + return mFailures.isEmpty(); + } + + /** + * Returns a {@link Map} of all successful keys mapped to the results they produced. + * + *

The values of the {@link Map} may be {@code null}. + */ + @NonNull + public Map getResults() { + return mResults; + } + + /** + * Returns a {@link Map} of all failed keys mapped to a {@link Throwable} representing the cause + * of failure. + * + *

The values of the {@link Map} may be {@code null}. + */ + @NonNull + public Map getFailures() { + return mFailures; + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = + new Creator() { + @NonNull + @Override + public AppSearchBatchResult createFromParcel(@NonNull Parcel in) { + return new AppSearchBatchResult(in); + } + + @NonNull + @Override + public AppSearchBatchResult[] newArray(int size) { + return new AppSearchBatchResult[size]; + } + }; + + /** + * Creates a new {@link Builder} for this {@link AppSearchBatchResult}. + * @hide + */ + @NonNull + public static Builder newBuilder() { + return new Builder<>(); + } + + /** + * Builder for {@link AppSearchBatchResult} objects. + * + * @param The type of keys. + * @param The type of result objects associated with the keys. + * @hide + */ + public static final class Builder { + @NonNull private final Map mResults = new ArrayMap<>(); + @NonNull private final Map mFailures = new ArrayMap<>(); + + private Builder() {} + + /** + * Registers that the {@code key} was processed successfully and associates it with + * {@code value}. Any previous mapping for a key, whether success or failure, is deleted. + */ + public Builder setSuccess(@NonNull KeyType key, @Nullable ValueType value) { + mResults.put(key, value); + mFailures.remove(key); + return this; + } + + /** + * Registers that the {@code key} failed and associates it with {@code throwable}. Any + * previous mapping for a key, whether success or failure, is deleted. + */ + public Builder setFailure(@NonNull KeyType key, @Nullable Throwable throwable) { + mFailures.put(key, throwable); + mResults.remove(key); + return this; + } + + /** Builds an {@link AppSearchBatchResult} from the contents of this {@link Builder}. */ + @NonNull + public AppSearchBatchResult build() { + return new AppSearchBatchResult<>(mResults, mFailures); + } + } +} diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java index 66cba52ae03a9..e3f6b3de433a0 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java +++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java @@ -29,12 +29,12 @@ import com.google.android.icing.proto.SearchResultProto; import com.google.android.icing.proto.StatusProto; import com.google.android.icing.protobuf.InvalidProtocolBufferException; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.function.BiConsumer; -import java.util.function.Consumer; /** * This class provides access to the centralized AppSearch index maintained by the system. @@ -158,26 +158,25 @@ public class AppSearchManager { * name of a schema type previously registered via the {@link #setSchema} method. * * @param documents {@link Document Documents} that need to be indexed. - * @param executor Executor on which to invoke the callback. - * @param callback Callback to receive errors. On success, it will be called with {@code null}. - * On failure, it will be called with a {@link Throwable} describing the failure. + * @return An {@link AppSearchBatchResult} mapping the document URIs to {@link Void} if they + * were successfully indexed, or a {@link Throwable} describing the failure if they could + * not be indexed. + * @hide */ - public void putDocuments( - @NonNull List documents, - @NonNull @CallbackExecutor Executor executor, - @NonNull Consumer callback) { - AndroidFuture future = new AndroidFuture<>(); + public AppSearchBatchResult putDocuments(@NonNull List documents) { + // TODO(b/146386470): Transmit these documents as a RemoteStream instead of sending them in + // one big list. + List documentsBytes = new ArrayList<>(documents.size()); for (Document document : documents) { - // TODO(b/146386470) batching Document protos - try { - mService.putDocument(document.getProto().toByteArray(), future); - } catch (RemoteException e) { - future.completeExceptionally(e); - break; - } + documentsBytes.add(document.getProto().toByteArray()); } - // TODO(b/147614371) Fix error report for multiple documents. - future.whenCompleteAsync((noop, err) -> callback.accept(err), executor); + AndroidFuture future = new AndroidFuture<>(); + try { + mService.putDocuments(documentsBytes, future); + } catch (RemoteException e) { + future.completeExceptionally(e); + } + return getFutureOrThrow(future); } /** diff --git a/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl b/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl index c9c5d7fbfda42..20c8af985c21b 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl +++ b/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl @@ -17,6 +17,8 @@ package android.app.appsearch; import com.android.internal.infra.AndroidFuture; +parcelable AppSearchBatchResult; + /** {@hide} */ interface IAppSearchManager { /** @@ -32,14 +34,17 @@ interface IAppSearchManager { void setSchema(in byte[] schemaBytes, boolean forceOverride, in AndroidFuture callback); /** - * Inserts a document into the index. + * Inserts documents into the index. * - * @param documentBytes serialized DocumentProto - * @param callback {@link AndroidFuture}<{@link Void}>. Will be completed with - * {@code null} upon successful completion of the put call, or completed exceptionally if - * put fails. + * @param documentsBytes {@link List}<byte[]> of serialized DocumentProtos. + * @param callback + * {@link AndroidFuture}<{@link AppSearchBatchResult}<{@link String}, {@link Void}>>. + * If the call fails to start, {@code callback} will be completed exceptionally. Otherwise, + * {@code callback} will be completed with an + * {@link AppSearchBatchResult}<{@link String}, {@link Void}> + * where the keys are document URIs, and the values are {@code null}. */ - void putDocument(in byte[] documentBytes, in AndroidFuture callback); + void putDocuments(in List documentsBytes, in AndroidFuture callback); /** * Searches a document based on a given query string. diff --git a/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java b/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java index 6929202445e9e..d2d9cf9fdf17f 100644 --- a/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java +++ b/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java @@ -16,6 +16,7 @@ package com.android.server.appsearch; import android.annotation.NonNull; +import android.app.appsearch.AppSearchBatchResult; import android.app.appsearch.IAppSearchManager; import android.content.Context; import android.os.Binder; @@ -34,6 +35,8 @@ import com.google.android.icing.proto.SearchResultProto; import com.google.android.icing.proto.SearchSpecProto; import com.google.android.icing.protobuf.InvalidProtocolBufferException; +import java.util.List; + /** * TODO(b/142567528): add comments when implement this class */ @@ -72,17 +75,28 @@ public class AppSearchManagerService extends SystemService { } @Override - public void putDocument(byte[] documentBytes, AndroidFuture callback) { - Preconditions.checkNotNull(documentBytes); + public void putDocuments( + List documentsBytes, AndroidFuture callback) { + Preconditions.checkNotNull(documentsBytes); Preconditions.checkNotNull(callback); int callingUid = Binder.getCallingUidOrThrow(); int callingUserId = UserHandle.getUserId(callingUid); long callingIdentity = Binder.clearCallingIdentity(); try { - DocumentProto document = DocumentProto.parseFrom(documentBytes); AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId); - impl.putDocument(callingUid, document); - callback.complete(null); + AppSearchBatchResult.Builder resultBuilder = + AppSearchBatchResult.newBuilder(); + for (int i = 0; i < documentsBytes.size(); i++) { + byte[] documentBytes = (byte[]) documentsBytes.get(i); + DocumentProto document = DocumentProto.parseFrom(documentBytes); + try { + impl.putDocument(callingUid, document); + resultBuilder.setSuccess(document.getUri(), /*value=*/ null); + } catch (Throwable t) { + resultBuilder.setFailure(document.getUri(), t); + } + } + callback.complete(resultBuilder.build()); } catch (Throwable t) { callback.completeExceptionally(t); } finally {