diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchBatchResult.aidl b/apex/appsearch/framework/java/android/app/appsearch/AppSearchBatchResult.aidl new file mode 100644 index 0000000000000..4686de8df268e --- /dev/null +++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchBatchResult.aidl @@ -0,0 +1,19 @@ +/** + * 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; + +/** {@hide} */ +parcelable AppSearchBatchResult; \ No newline at end of file diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java index f2c9942edbb32..e57359fbf50ed 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java +++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java @@ -15,10 +15,12 @@ */ package android.app.appsearch; +import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.annotation.SystemService; import android.content.Context; import android.os.Bundle; +import android.os.ParcelableException; import android.os.RemoteException; import com.android.internal.infra.AndroidFuture; @@ -27,7 +29,10 @@ import com.android.internal.util.Preconditions; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.function.Consumer; /** * This class provides access to the centralized AppSearch index maintained by the system. @@ -40,12 +45,99 @@ import java.util.concurrent.ExecutionException; // TODO(b/148046169): This class header needs a detailed example/tutorial. @SystemService(Context.APP_SEARCH_SERVICE) public class AppSearchManager { - private static final String DEFAULT_DATABASE = ""; + /** + * The default empty database name. + * @hide + */ + public static final String DEFAULT_DATABASE_NAME = ""; + private final IAppSearchManager mService; /** @hide */ public AppSearchManager(@NonNull IAppSearchManager service) { - mService = service; + mService = Objects.requireNonNull(service); + } + + /** Contains information about how to create the search session. */ + public static final class SearchContext { + final String mDatabaseName; + + SearchContext(@NonNull String databaseName) { + mDatabaseName = Objects.requireNonNull(databaseName); + } + + /** + * Returns the name of the database to create or open. + * + *

Databases with different names are fully separate with distinct types, namespaces, + * and data. + */ + @NonNull + public String getDatabaseName() { + return mDatabaseName; + } + + /** Builder for {@link SearchContext} objects. */ + public static final class Builder { + private String mDatabaseName = DEFAULT_DATABASE_NAME; + private boolean mBuilt = false; + + /** + * Sets the name of the database associated with {@link AppSearchSession}. + * + *

{@link AppSearchSession} will create or open a database under the given name. + * + *

Databases with different names are fully separate with distinct types, namespaces, + * and data. + * + *

Database name cannot contain {@code '/'}. + * + *

If not specified, defaults to {@link #DEFAULT_DATABASE_NAME}. + * @param databaseName The name of the database. + * @throws IllegalArgumentException if the databaseName contains {@code '/'}. + */ + @NonNull + public Builder setDatabaseName(@NonNull String databaseName) { + Preconditions.checkState(!mBuilt, "Builder has already been used"); + Objects.requireNonNull(databaseName); + if (databaseName.contains("/")) { + throw new IllegalArgumentException("Database name cannot contain '/'"); + } + mDatabaseName = databaseName; + return this; + } + + /** Builds a {@link SearchContext} instance. */ + @NonNull + public SearchContext build() { + Preconditions.checkState(!mBuilt, "Builder has already been used"); + mBuilt = true; + return new SearchContext(mDatabaseName); + } + } + } + + /** + * Creates a new {@link AppSearchSession}. + * + *

This process requires an AppSearch native indexing file system for each user. If it's not + * created for this user, the initialization process will create one under user's directory. + * + * @param searchContext The {@link SearchContext} contains all information to create a new + * {@link AppSearchSession} + * @param executor Executor on which to invoke the callback. + * @param callback The {@link AppSearchResult}<{@link AppSearchSession}> of + * performing this operation. Or a {@link AppSearchResult} with failure + * reason code and error information. + */ + public void createSearchSession( + @NonNull SearchContext searchContext, + @NonNull @CallbackExecutor Executor executor, + @NonNull Consumer> callback) { + Objects.requireNonNull(searchContext); + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + AppSearchSession.createSearchSession(searchContext, mService, executor, callback); } /** @@ -99,7 +191,7 @@ public class AppSearchManager { * * @param request The schema update request. * @return the result of performing this operation. - * + * @deprecated use {@link AppSearchSession#setSchema} instead. * @hide */ @NonNull @@ -113,9 +205,14 @@ public class AppSearchManager { } AndroidFuture future = new AndroidFuture<>(); try { - mService.setSchema(DEFAULT_DATABASE, schemaBundles, request.isForceOverride(), future); + mService.setSchema(DEFAULT_DATABASE_NAME, schemaBundles, request.isForceOverride(), + new IAppSearchResultCallback.Stub() { + public void onResult(AppSearchResult result) { + future.complete(result); + } + }); } catch (RemoteException e) { - future.completeExceptionally(e); + throw e.rethrowFromSystemServer(); } return getFutureOrThrow(future); } @@ -134,6 +231,9 @@ public class AppSearchManager { * {@link AppSearchBatchResult} are the URIs of the input documents. The values are * {@code null} if they were successfully indexed, or a failed {@link AppSearchResult} * otherwise. + * @throws RuntimeException If an error occurred during the execution. + * + * @deprecated use {@link AppSearchSession#putDocuments} instead. * @hide */ public AppSearchBatchResult putDocuments(@NonNull PutDocumentsRequest request) { @@ -146,7 +246,16 @@ public class AppSearchManager { } AndroidFuture future = new AndroidFuture<>(); try { - mService.putDocuments(DEFAULT_DATABASE, documentBundles, future); + mService.putDocuments(DEFAULT_DATABASE_NAME, documentBundles, + new IAppSearchBatchResultCallback.Stub() { + public void onResult(AppSearchBatchResult result) { + future.complete(result); + } + + public void onSystemError(ParcelableException exception) { + future.completeExceptionally(exception); + } + }); } catch (RemoteException e) { future.completeExceptionally(e); } @@ -165,6 +274,9 @@ public class AppSearchManager { * {@link GenericDocument}s on success, or a failed {@link AppSearchResult} otherwise. * URIs that are not found will return a failed {@link AppSearchResult} with a result code * of {@link AppSearchResult#RESULT_NOT_FOUND}. + * @throws RuntimeException If an error occurred during the execution. + * + * @deprecated use {@link AppSearchSession#getByUri} instead. */ public AppSearchBatchResult getByUri( @NonNull GetByUriRequest request) { @@ -173,7 +285,16 @@ public class AppSearchManager { List uris = new ArrayList<>(request.getUris()); AndroidFuture future = new AndroidFuture<>(); try { - mService.getDocuments(DEFAULT_DATABASE, request.getNamespace(), uris, future); + mService.getDocuments(DEFAULT_DATABASE_NAME, request.getNamespace(), uris, + new IAppSearchBatchResultCallback.Stub() { + public void onResult(AppSearchBatchResult result) { + future.complete(result); + } + + public void onSystemError(ParcelableException exception) { + future.completeExceptionally(exception); + } + }); } catch (RemoteException e) { future.completeExceptionally(e); } @@ -252,6 +373,9 @@ public class AppSearchManager { * * @param queryExpression Query String to search. * @param searchSpec Spec for setting filters, raw query etc. + * @throws RuntimeException If an error occurred during the execution. + * + * @deprecated use AppSearchSession#query instead. * @hide */ @NonNull @@ -261,7 +385,7 @@ public class AppSearchManager { // them in one big list. AndroidFuture searchResultsFuture = new AndroidFuture<>(); try { - mService.query(DEFAULT_DATABASE, queryExpression, + mService.query(DEFAULT_DATABASE_NAME, queryExpression, searchSpec.getBundle(), searchResultsFuture); } catch (RemoteException e) { searchResultsFuture.completeExceptionally(e); @@ -278,7 +402,7 @@ public class AppSearchManager { } /** - * Deletes {@link GenericDocument}s by URI. + * Removes {@link GenericDocument}s by URI. * *

You should not call this method directly; instead, use the {@code AppSearch#delete()} API * provided by JetPack. @@ -289,12 +413,24 @@ public class AppSearchManager { * or a failed {@link AppSearchResult} otherwise. URIs that are not found will return a * failed {@link AppSearchResult} with a result code of * {@link AppSearchResult#RESULT_NOT_FOUND}. + * @throws RuntimeException If an error occurred during the execution. + * + * @deprecated use {@link AppSearchSession#removeByUri} instead. */ public AppSearchBatchResult removeByUri(@NonNull RemoveByUriRequest request) { List uris = new ArrayList<>(request.getUris()); AndroidFuture future = new AndroidFuture<>(); try { - mService.removeByUri(DEFAULT_DATABASE, request.getNamespace(), uris, future); + mService.removeByUri(DEFAULT_DATABASE_NAME, request.getNamespace(), uris, + new IAppSearchBatchResultCallback.Stub() { + public void onResult(AppSearchBatchResult result) { + future.complete(result); + } + + public void onSystemError(ParcelableException exception) { + future.completeExceptionally(exception); + } + }); } catch (RemoteException e) { future.completeExceptionally(e); } diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchResult.aidl b/apex/appsearch/framework/java/android/app/appsearch/AppSearchResult.aidl new file mode 100644 index 0000000000000..f0b29964b895d --- /dev/null +++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchResult.aidl @@ -0,0 +1,19 @@ +/** + * 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; + +/** {@hide} */ +parcelable AppSearchResult; \ No newline at end of file diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchResult.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchResult.java index 979eab90a980c..6e2ed70cca016 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/AppSearchResult.java +++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchResult.java @@ -19,9 +19,11 @@ package android.app.appsearch; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.appsearch.exceptions.AppSearchException; import android.os.Parcel; import android.os.Parcelable; +import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Objects; @@ -218,4 +220,25 @@ public final class AppSearchResult implements Parcelable { @ResultCode int resultCode, @Nullable String errorMessage) { return new AppSearchResult<>(resultCode, /*resultValue=*/ null, errorMessage); } + + /** @hide */ + @NonNull + public static AppSearchResult throwableToFailedResult( + @NonNull Throwable t) { + if (t instanceof AppSearchException) { + return ((AppSearchException) t).toAppSearchResult(); + } + + @AppSearchResult.ResultCode int resultCode; + if (t instanceof IllegalStateException) { + resultCode = AppSearchResult.RESULT_INTERNAL_ERROR; + } else if (t instanceof IllegalArgumentException) { + resultCode = AppSearchResult.RESULT_INVALID_ARGUMENT; + } else if (t instanceof IOException) { + resultCode = AppSearchResult.RESULT_IO_ERROR; + } else { + resultCode = AppSearchResult.RESULT_UNKNOWN_ERROR; + } + return AppSearchResult.newFailedResult(resultCode, t.toString()); + } } diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchSession.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchSession.java new file mode 100644 index 0000000000000..531436ecaf0c4 --- /dev/null +++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchSession.java @@ -0,0 +1,312 @@ +/* + * 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 android.annotation.CallbackExecutor; +import android.annotation.NonNull; +import android.app.appsearch.exceptions.AppSearchException; +import android.os.Bundle; +import android.os.ParcelableException; +import android.os.RemoteException; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +/** + * Represents a connection to an AppSearch storage system where {@link GenericDocument}s can be + * placed and queried. + * @hide + */ +public final class AppSearchSession { + private final String mDatabaseName; + private final IAppSearchManager mService; + + static void createSearchSession( + @NonNull AppSearchManager.SearchContext searchContext, + @NonNull IAppSearchManager service, + @NonNull @CallbackExecutor Executor executor, + @NonNull Consumer> callback) { + AppSearchSession searchSession = + new AppSearchSession(searchContext.mDatabaseName, service); + searchSession.initialize(executor, callback); + } + + // NOTE: No instance of this class should be created or returned except via initialize(). + // Once the callback.accept has been called here, the class is ready to use. + private void initialize( + @NonNull @CallbackExecutor Executor executor, + @NonNull Consumer> callback) { + try { + mService.initialize(new IAppSearchResultCallback.Stub() { + public void onResult(AppSearchResult result) { + executor.execute(() -> { + if (result.isSuccess()) { + callback.accept( + AppSearchResult.newSuccessfulResult(AppSearchSession.this)); + } else { + callback.accept(result); + } + }); + } + }); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + private AppSearchSession(@NonNull String databaseName, @NonNull IAppSearchManager service) { + mDatabaseName = databaseName; + mService = service; + } + + /** + * Sets the schema will be used by documents provided to the {@link #putDocuments} method. + * + *

The schema provided here is compared to the stored copy of the schema previously supplied + * to {@link #setSchema}, if any, to determine how to treat existing documents. The following + * types of schema modifications are always safe and are made without deleting any existing + * documents: + *

+ * + *

The following types of schema changes are not backwards-compatible: + *

+ *

Supplying a schema with such changes will, by default, result in this call returning an + * {@link AppSearchResult} with a code of {@link AppSearchResult#RESULT_INVALID_SCHEMA} and an + * error message describing the incompatibility. In this case the previously set schema will + * remain active. + * + *

If you need to make non-backwards-compatible changes as described above, you can set the + * {@link SetSchemaRequest.Builder#setForceOverride} method to {@code true}. In this case, + * instead of returning an {@link AppSearchResult} with the + * {@link AppSearchResult#RESULT_INVALID_SCHEMA} error code, all documents which are not + * compatible with the new schema will be deleted and the incompatible schema will be applied. + * + *

It is a no-op to set the same schema as has been previously set; this is handled + * efficiently. + * + * @param request The schema update request. + * @param executor Executor on which to invoke the callback. + * @param callback Callback to receive errors resulting from setting the schema. If the + * operation succeeds, the callback will be invoked with {@code null}. + */ + public void setSchema( + @NonNull SetSchemaRequest request, + @NonNull @CallbackExecutor Executor executor, + @NonNull Consumer> callback) { + Objects.requireNonNull(request); + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + List schemaBundles = new ArrayList<>(request.getSchemas().size()); + for (AppSearchSchema schema : request.getSchemas()) { + schemaBundles.add(schema.getBundle()); + } + try { + mService.setSchema(mDatabaseName, schemaBundles, request.isForceOverride(), + new IAppSearchResultCallback.Stub() { + public void onResult(AppSearchResult result) { + executor.execute(() -> callback.accept(result)); + } + }); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Indexes documents into AppSearch. + * + *

Each {@link GenericDocument}'s {@code schemaType} field must be set to the name of a + * schema type previously registered via the {@link #setSchema} method. + * + * @param request {@link PutDocumentsRequest} containing documents to be indexed + * @param executor Executor on which to invoke the callback. + * @param callback Callback to receive pending result of performing this operation. The keys + * of the returned {@link AppSearchBatchResult} are the URIs of the input + * documents. The values are {@code null} if they were successfully indexed, + * or a failed {@link AppSearchResult} otherwise. + * Or {@link BatchResultCallback#onSystemError} will be invoked with an + * {@link AppSearchException} if an error occurred in AppSearch initialization + * or a cause {@link Throwable} if other error occurred in AppSearch service. + */ + public void putDocuments( + @NonNull PutDocumentsRequest request, + @NonNull @CallbackExecutor Executor executor, + @NonNull BatchResultCallback callback) { + Objects.requireNonNull(request); + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + List documents = request.getDocuments(); + List documentBundles = new ArrayList<>(documents.size()); + for (int i = 0; i < documents.size(); i++) { + documentBundles.add(documents.get(i).getBundle()); + } + try { + mService.putDocuments(mDatabaseName, documentBundles, + new IAppSearchBatchResultCallback.Stub() { + public void onResult(AppSearchBatchResult result) { + executor.execute(() -> callback.onResult(result)); + } + + public void onSystemError(ParcelableException exception) { + executor.execute(() -> callback.onSystemError(exception.getCause())); + } + }); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Retrieves {@link GenericDocument}s by URI. + * + * @param request {@link GetByUriRequest} containing URIs to be retrieved. + * @param executor Executor on which to invoke the callback. + * @param callback Callback to receive the pending result of performing this operation. The keys + * of the returned {@link AppSearchBatchResult} are the input URIs. The values + * are the returned {@link GenericDocument}s on success, or a failed + * {@link AppSearchResult} otherwise. URIs that are not found will return a + * failed {@link AppSearchResult} with a result code of + * {@link AppSearchResult#RESULT_NOT_FOUND}. + * Or {@link BatchResultCallback#onSystemError} will be invoked with an + * {@link AppSearchException} if an error occurred in AppSearch initialization + * or a cause {@link Throwable} if other error occurred in AppSearch service. + */ + public void getByUri( + @NonNull GetByUriRequest request, + @NonNull @CallbackExecutor Executor executor, + @NonNull BatchResultCallback callback) { + Objects.requireNonNull(request); + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + try { + mService.getDocuments(mDatabaseName, request.getNamespace(), + new ArrayList<>(request.getUris()), + new IAppSearchBatchResultCallback.Stub() { + public void onResult(AppSearchBatchResult result) { + executor.execute(() -> { + AppSearchBatchResult.Builder + documentResultBuilder = + new AppSearchBatchResult.Builder<>(); + + // Translate successful results + for (Map.Entry bundleEntry : + (Set>) + result.getSuccesses().entrySet()) { + GenericDocument document; + try { + document = new GenericDocument(bundleEntry.getValue()); + } catch (Throwable t) { + // These documents went through validation, so how could + // this fail? We must have done something wrong. + documentResultBuilder.setFailure( + bundleEntry.getKey(), + AppSearchResult.RESULT_INTERNAL_ERROR, + t.getMessage()); + continue; + } + documentResultBuilder.setSuccess( + bundleEntry.getKey(), document); + } + + // Translate failed results + for (Map.Entry> bundleEntry : + (Set>>) + result.getFailures().entrySet()) { + documentResultBuilder.setFailure( + bundleEntry.getKey(), + bundleEntry.getValue().getResultCode(), + bundleEntry.getValue().getErrorMessage()); + } + callback.onResult(documentResultBuilder.build()); + }); + } + + public void onSystemError(ParcelableException exception) { + executor.execute(() -> callback.onSystemError(exception.getCause())); + } + }); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Removes {@link GenericDocument}s from the index by URI. + * + * @param request Request containing URIs to be removed. + * @param executor Executor on which to invoke the callback. + * @param callback Callback to receive the pending result of performing this operation. The keys + * of the returned {@link AppSearchBatchResult} are the input URIs. The values + * are {@code null} on success, or a failed {@link AppSearchResult} otherwise. + * URIs that are not found will return a failed {@link AppSearchResult} with a + * result code of {@link AppSearchResult#RESULT_NOT_FOUND}. + * Or {@link BatchResultCallback#onSystemError} will be invoked with an + * {@link AppSearchException} if an error occurred in AppSearch initialization + * or a cause {@link Throwable} if other error occurred in AppSearch service. + */ + public void removeByUri( + @NonNull RemoveByUriRequest request, + @NonNull @CallbackExecutor Executor executor, + @NonNull BatchResultCallback callback) { + Objects.requireNonNull(request); + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + try { + mService.removeByUri(mDatabaseName, request.getNamespace(), + new ArrayList<>(request.getUris()), + new IAppSearchBatchResultCallback.Stub() { + public void onResult(AppSearchBatchResult result) { + executor.execute(() -> callback.onResult(result)); + } + + public void onSystemError(ParcelableException exception) { + executor.execute(() -> callback.onSystemError(exception.getCause())); + } + }); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + // TODO(b/162450968) port query() and SearchResults.java to platform. + // TODO(b/162450968) port removeByQuery() to platform. +} diff --git a/apex/appsearch/framework/java/android/app/appsearch/BatchResultCallback.java b/apex/appsearch/framework/java/android/app/appsearch/BatchResultCallback.java new file mode 100644 index 0000000000000..1689e02c7ce43 --- /dev/null +++ b/apex/appsearch/framework/java/android/app/appsearch/BatchResultCallback.java @@ -0,0 +1,47 @@ +/* + * 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; + +/** + * The callback interface to return {@link AppSearchBatchResult}. + * + * @param The type of the keys for {@link AppSearchBatchResult#getSuccesses} and + * {@link AppSearchBatchResult#getFailures}. + * @param The type of result objects associated with the keys. + * @hide + */ +public interface BatchResultCallback { + + /** + * Called when {@link AppSearchBatchResult} results are ready. + * + * @param result The result of the executed request. + */ + void onResult(AppSearchBatchResult result); + + + /** + * Called when a system error occurred. + * + * @param throwable The cause throwable. + */ + default void onSystemError(Throwable throwable) { + if (throwable != null) { + throw new RuntimeException(throwable); + } + } +} diff --git a/apex/appsearch/framework/java/android/app/appsearch/IAppSearchBatchResultCallback.aidl b/apex/appsearch/framework/java/android/app/appsearch/IAppSearchBatchResultCallback.aidl new file mode 100644 index 0000000000000..b1bbd18b98e23 --- /dev/null +++ b/apex/appsearch/framework/java/android/app/appsearch/IAppSearchBatchResultCallback.aidl @@ -0,0 +1,25 @@ +/** + * 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 android.app.appsearch.AppSearchBatchResult; +import android.os.ParcelableException; + +/** {@hide} */ +oneway interface IAppSearchBatchResultCallback { + void onResult(in AppSearchBatchResult result); + void onSystemError(in ParcelableException exception); +} \ No newline at end of file diff --git a/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl b/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl index 01260ea193f6a..4a981022da738 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl +++ b/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl @@ -17,10 +17,12 @@ package android.app.appsearch; import android.os.Bundle; +import android.app.appsearch.AppSearchBatchResult; +import android.app.appsearch.AppSearchResult; +import android.app.appsearch.IAppSearchBatchResultCallback; +import android.app.appsearch.IAppSearchResultCallback; import com.android.internal.infra.AndroidFuture; -parcelable AppSearchResult; -parcelable AppSearchBatchResult; parcelable SearchResults; /** {@hide} */ @@ -32,14 +34,14 @@ interface IAppSearchManager { * @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}<{@link AppSearchResult}<{@link Void}>>. - * The results of the call. + * @param callback {@link IAppSearchResultCallback#onResult} will be called with an + * {@link AppSearchResult}<{@link Void}>. */ void setSchema( in String databaseName, in List schemaBundles, boolean forceOverride, - in AndroidFuture callback); + in IAppSearchResultCallback callback); /** * Inserts documents into the index. @@ -47,16 +49,16 @@ interface IAppSearchManager { * @param databaseName The name of the database where this document lives. * @param documentBundes List of GenericDocument bundles. * @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 + * If the call fails to start, {@link IAppSearchBatchResultCallback#onSystemError} + * will be called with the cause throwable. Otherwise, + * {@link IAppSearchBatchResultCallback#onResult} will be called with an * {@link AppSearchBatchResult}<{@link String}, {@link Void}> * where the keys are document URIs, and the values are {@code null}. */ void putDocuments( in String databaseName, in List documentBundles, - in AndroidFuture callback); + in IAppSearchBatchResultCallback callback); /** * Retrieves documents from the index. @@ -65,9 +67,9 @@ interface IAppSearchManager { * @param namespace The namespace this document resides in. * @param uris The URIs of the documents to retrieve * @param callback - * {@link AndroidFuture}<{@link AppSearchBatchResult}<{@link String}, {@link Bundle}>>. - * If the call fails to start, {@code callback} will be completed exceptionally. Otherwise, - * {@code callback} will be completed with an + * If the call fails to start, {@link IAppSearchBatchResultCallback#onSystemError} + * will be called with the cause throwable. Otherwise, + * {@link IAppSearchBatchResultCallback#onResult} will be called with an * {@link AppSearchBatchResult}<{@link String}, {@link Bundle}> * where the keys are document URIs, and the values are Document bundles. */ @@ -75,7 +77,7 @@ interface IAppSearchManager { in String databaseName, in String namespace, in List uris, - in AndroidFuture callback); + in IAppSearchBatchResultCallback callback); /** * Searches a document based on a given specifications. @@ -98,9 +100,9 @@ interface IAppSearchManager { * @param namespace Namespace of the document to remove. * @param uris The URIs of the documents to delete * @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 + * If the call fails to start, {@link IAppSearchBatchResultCallback#onSystemError} + * will be called with the cause throwable. Otherwise, + * {@link IAppSearchBatchResultCallback#onResult} will be called with an * {@link AppSearchBatchResult}<{@link String}, {@link Void}> * where the keys are document URIs. If a document doesn't exist, it will be reported as a * failure where the {@code throwable} is {@code null}. @@ -109,7 +111,7 @@ interface IAppSearchManager { in String databaseName, in String namespace, in List uris, - in AndroidFuture callback); + in IAppSearchBatchResultCallback callback); /** * Removes documents by given query. @@ -124,4 +126,12 @@ interface IAppSearchManager { in String queryExpression, in Bundle searchSpecBundle, in AndroidFuture callback); + + /** + * Creates and initializes AppSearchImpl for the calling app. + * + * @param callback {@link IAppSearchResultCallback#onResult} will be called with an + * {@link AppSearchResult}<{@link Void}>. + */ + void initialize(in IAppSearchResultCallback callback); } diff --git a/apex/appsearch/framework/java/android/app/appsearch/IAppSearchResultCallback.aidl b/apex/appsearch/framework/java/android/app/appsearch/IAppSearchResultCallback.aidl new file mode 100644 index 0000000000000..27729a5ad0584 --- /dev/null +++ b/apex/appsearch/framework/java/android/app/appsearch/IAppSearchResultCallback.aidl @@ -0,0 +1,24 @@ +/** + * 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 android.app.appsearch.AppSearchResult; +import android.os.ParcelableException; + +/** {@hide} */ +oneway interface IAppSearchResultCallback { + void onResult(in AppSearchResult result); +} \ No newline at end of file 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 1dfde528e69b9..8269799d5a0d6 100644 --- a/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java +++ b/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java @@ -15,27 +15,32 @@ */ package com.android.server.appsearch; +import static android.app.appsearch.AppSearchResult.throwableToFailedResult; + import android.annotation.NonNull; import android.app.appsearch.AppSearchBatchResult; import android.app.appsearch.AppSearchResult; import android.app.appsearch.AppSearchSchema; import android.app.appsearch.GenericDocument; +import android.app.appsearch.IAppSearchBatchResultCallback; import android.app.appsearch.IAppSearchManager; +import android.app.appsearch.IAppSearchResultCallback; import android.app.appsearch.SearchResultPage; import android.app.appsearch.SearchSpec; -import android.app.appsearch.exceptions.AppSearchException; import android.content.Context; import android.os.Binder; import android.os.Bundle; +import android.os.ParcelableException; +import android.os.RemoteException; import android.os.UserHandle; import android.util.ArraySet; +import android.util.Log; import com.android.internal.infra.AndroidFuture; import com.android.internal.util.Preconditions; import com.android.server.SystemService; import com.android.server.appsearch.external.localstorage.AppSearchImpl; -import java.io.IOException; import java.util.List; import java.util.Set; @@ -61,10 +66,9 @@ public class AppSearchManagerService extends SystemService { @NonNull String databaseName, @NonNull List schemaBundles, boolean forceOverride, - @NonNull AndroidFuture callback) { + @NonNull IAppSearchResultCallback callback) { Preconditions.checkNotNull(databaseName); Preconditions.checkNotNull(schemaBundles); - Preconditions.checkNotNull(callback); int callingUid = Binder.getCallingUidOrThrow(); int callingUserId = UserHandle.getUserId(callingUid); final long callingIdentity = Binder.clearCallingIdentity(); @@ -76,9 +80,10 @@ public class AppSearchManagerService extends SystemService { AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId); databaseName = rewriteDatabaseNameWithUid(databaseName, callingUid); impl.setSchema(databaseName, schemas, forceOverride); - callback.complete(AppSearchResult.newSuccessfulResult(/*result=*/ null)); + invokeCallbackOnResult(callback, + AppSearchResult.newSuccessfulResult(/*result=*/ null)); } catch (Throwable t) { - callback.complete(throwableToFailedResult(t)); + invokeCallbackOnError(callback, t); } finally { Binder.restoreCallingIdentity(callingIdentity); } @@ -88,7 +93,7 @@ public class AppSearchManagerService extends SystemService { public void putDocuments( @NonNull String databaseName, @NonNull List documentBundles, - @NonNull AndroidFuture callback) { + @NonNull IAppSearchBatchResultCallback callback) { Preconditions.checkNotNull(databaseName); Preconditions.checkNotNull(documentBundles); Preconditions.checkNotNull(callback); @@ -96,22 +101,24 @@ public class AppSearchManagerService extends SystemService { int callingUserId = UserHandle.getUserId(callingUid); final long callingIdentity = Binder.clearCallingIdentity(); try { - AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId); - databaseName = rewriteDatabaseNameWithUid(databaseName, callingUid); AppSearchBatchResult.Builder resultBuilder = new AppSearchBatchResult.Builder<>(); + AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId); + databaseName = rewriteDatabaseNameWithUid(databaseName, callingUid); for (int i = 0; i < documentBundles.size(); i++) { GenericDocument document = new GenericDocument(documentBundles.get(i)); try { + // TODO(b/173451571): reduce burden of binder thread by enqueue request onto + // a separate thread. impl.putDocument(databaseName, document); resultBuilder.setSuccess(document.getUri(), /*result=*/ null); } catch (Throwable t) { resultBuilder.setResult(document.getUri(), throwableToFailedResult(t)); } } - callback.complete(resultBuilder.build()); + invokeCallbackOnResult(callback, resultBuilder.build()); } catch (Throwable t) { - callback.completeExceptionally(t); + invokeCallbackOnError(callback, t); } finally { Binder.restoreCallingIdentity(callingIdentity); } @@ -119,7 +126,8 @@ public class AppSearchManagerService extends SystemService { @Override public void getDocuments(@NonNull String databaseName, @NonNull String namespace, - @NonNull List uris, @NonNull AndroidFuture callback) { + @NonNull List uris, + @NonNull IAppSearchBatchResultCallback callback) { Preconditions.checkNotNull(databaseName); Preconditions.checkNotNull(namespace); Preconditions.checkNotNull(uris); @@ -128,10 +136,10 @@ public class AppSearchManagerService extends SystemService { int callingUserId = UserHandle.getUserId(callingUid); final long callingIdentity = Binder.clearCallingIdentity(); try { - AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId); - databaseName = rewriteDatabaseNameWithUid(databaseName, callingUid); AppSearchBatchResult.Builder resultBuilder = new AppSearchBatchResult.Builder<>(); + AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId); + databaseName = rewriteDatabaseNameWithUid(databaseName, callingUid); for (int i = 0; i < uris.size(); i++) { String uri = uris.get(i); try { @@ -141,9 +149,9 @@ public class AppSearchManagerService extends SystemService { resultBuilder.setResult(uri, throwableToFailedResult(t)); } } - callback.complete(resultBuilder.build()); + invokeCallbackOnResult(callback, resultBuilder.build()); } catch (Throwable t) { - callback.completeExceptionally(t); + invokeCallbackOnError(callback, t); } finally { Binder.restoreCallingIdentity(callingIdentity); } @@ -182,19 +190,19 @@ public class AppSearchManagerService extends SystemService { @Override public void removeByUri(@NonNull String databaseName, @NonNull String namespace, - List uris, AndroidFuture callback) { + @NonNull List uris, + @NonNull IAppSearchBatchResultCallback callback) { Preconditions.checkNotNull(databaseName); - Preconditions.checkNotNull(namespace); Preconditions.checkNotNull(uris); Preconditions.checkNotNull(callback); int callingUid = Binder.getCallingUidOrThrow(); int callingUserId = UserHandle.getUserId(callingUid); final long callingIdentity = Binder.clearCallingIdentity(); + AppSearchBatchResult.Builder resultBuilder = + new AppSearchBatchResult.Builder<>(); try { AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId); databaseName = rewriteDatabaseNameWithUid(databaseName, callingUid); - AppSearchBatchResult.Builder resultBuilder = - new AppSearchBatchResult.Builder<>(); for (int i = 0; i < uris.size(); i++) { String uri = uris.get(i); try { @@ -204,9 +212,9 @@ public class AppSearchManagerService extends SystemService { resultBuilder.setResult(uri, throwableToFailedResult(t)); } } - callback.complete(resultBuilder.build()); + invokeCallbackOnResult(callback, resultBuilder.build()); } catch (Throwable t) { - callback.completeExceptionally(t); + invokeCallbackOnError(callback, t); } finally { Binder.restoreCallingIdentity(callingIdentity); } @@ -237,6 +245,21 @@ public class AppSearchManagerService extends SystemService { } } + @Override + public void initialize(@NonNull IAppSearchResultCallback callback) { + int callingUid = Binder.getCallingUidOrThrow(); + int callingUserId = UserHandle.getUserId(callingUid); + final long callingIdentity = Binder.clearCallingIdentity(); + try { + ImplInstanceManager.getInstance(getContext(), callingUserId); + invokeCallbackOnResult(callback, AppSearchResult.newSuccessfulResult(null)); + } catch (Throwable t) { + invokeCallbackOnError(callback, t); + } finally { + Binder.restoreCallingIdentity(callingIdentity); + } + } + /** * Rewrites the database name by adding a prefix of unique name for the given uid. * @@ -256,23 +279,51 @@ public class AppSearchManagerService extends SystemService { return callingUidName + CALLING_NAME_DATABASE_DELIMITER + databaseName; } - private AppSearchResult throwableToFailedResult( - @NonNull Throwable t) { - if (t instanceof AppSearchException) { - return ((AppSearchException) t).toAppSearchResult(); + /** Invokes the {@link IAppSearchResultCallback} with the result. */ + private void invokeCallbackOnResult(IAppSearchResultCallback callback, + AppSearchResult result) { + try { + callback.onResult(result); + } catch (RemoteException e) { + Log.d(TAG, "Unable to send result to the callback", e); } + } - @AppSearchResult.ResultCode int resultCode; - if (t instanceof IllegalStateException) { - resultCode = AppSearchResult.RESULT_INTERNAL_ERROR; - } else if (t instanceof IllegalArgumentException) { - resultCode = AppSearchResult.RESULT_INVALID_ARGUMENT; - } else if (t instanceof IOException) { - resultCode = AppSearchResult.RESULT_IO_ERROR; - } else { - resultCode = AppSearchResult.RESULT_UNKNOWN_ERROR; + /** Invokes the {@link IAppSearchBatchResultCallback} with the result. */ + private void invokeCallbackOnResult(IAppSearchBatchResultCallback callback, + AppSearchBatchResult result) { + try { + callback.onResult(result); + } catch (RemoteException e) { + Log.d(TAG, "Unable to send result to the callback", e); + } + } + + /** + * Invokes the {@link IAppSearchResultCallback} with an throwable. + * + *

The throwable is convert to a {@link AppSearchResult}; + */ + private void invokeCallbackOnError(IAppSearchResultCallback callback, Throwable throwable) { + try { + callback.onResult(throwableToFailedResult(throwable)); + } catch (RemoteException e) { + Log.d(TAG, "Unable to send result to the callback", e); + } + } + + /** + * Invokes the {@link IAppSearchBatchResultCallback} with an throwable. + * + *

The throwable is converted to {@link ParcelableException}. + */ + private void invokeCallbackOnError(IAppSearchBatchResultCallback callback, + Throwable throwable) { + try { + callback.onSystemError(new ParcelableException(throwable)); + } catch (RemoteException e) { + Log.d(TAG, "Unable to send error to the callback", e); } - return AppSearchResult.newFailedResult(resultCode, t.getMessage()); } } }