From 773b82b1684894fd7a023b6915c518d5ed57e10d Mon Sep 17 00:00:00 2001 From: Alexander Dorokhine Date: Fri, 6 Mar 2020 10:43:16 -0800 Subject: [PATCH] Implement getDocuments() support in AppSearchImpl. This is a second attempt to submit ag/10081469. Bug: 146526096 Test: AppSearchManagerTest Change-Id: Ic294679c29cd86b52dfdd1c7169396483c881c96 --- .../app/appsearch/AppSearchManager.java | 51 ++++++++++++++- .../app/appsearch/AppSearchSchema.java | 20 ++---- .../app/appsearch/IAppSearchManager.aidl | 10 +++ .../appsearch/AppSearchManagerService.java | 24 +++++++ .../server/appsearch/impl/AppSearchImpl.java | 63 ++++++++++++++++--- 5 files changed, 140 insertions(+), 28 deletions(-) diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java index 16d1b2ccb5e92..cecf2fe8e7001 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java +++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java @@ -23,6 +23,7 @@ import android.os.RemoteException; import com.android.internal.infra.AndroidFuture; +import com.google.android.icing.proto.DocumentProto; import com.google.android.icing.proto.SchemaProto; import com.google.android.icing.proto.SearchResultProto; import com.google.android.icing.proto.SearchSpecProto; @@ -185,6 +186,53 @@ public class AppSearchManager { return getFutureOrThrow(future); } + /** + * Retrieves {@link AppSearchDocument}s by URI. + * + *

You should not call this method directly; instead, use the + * {@code AppSearch#getDocuments()} API provided by JetPack. + * + * @param uris URIs of the documents to look up. + * @param executor Executor on which to invoke the callback. + * @param callback Callback to receive the documents or error. + */ + public void getDocuments( + @NonNull List uris, + @NonNull @CallbackExecutor Executor executor, + @NonNull BiConsumer, ? super Throwable> callback) { + AndroidFuture> future = new AndroidFuture<>(); + future.whenCompleteAsync((documentProtos, err) -> { + if (err != null) { + callback.accept(null, err); + return; + } + if (documentProtos != null) { + List results = new ArrayList<>(documentProtos.size()); + for (int i = 0; i < documentProtos.size(); i++) { + DocumentProto documentProto; + try { + documentProto = DocumentProto.parseFrom(documentProtos.get(i)); + } catch (InvalidProtocolBufferException e) { + callback.accept(null, e); + return; + } + results.add(new AppSearchDocument(documentProto)); + } + callback.accept(results, null); + return; + } + // Nothing was supplied in the future at all + callback.accept(null, new IllegalStateException( + "Unknown failure occurred while retrieving documents")); + }, executor); + // TODO(b/146386470) stream uris? + try { + mService.getDocuments(uris.toArray(new String[uris.size()]), future); + } catch (RemoteException e) { + future.completeExceptionally(e); + } + } + /** * This method searches for documents based on a given query string. It also accepts * specifications regarding how to search and format the results. @@ -242,7 +290,6 @@ public class AppSearchManager { callback.accept(null, err); return; } - if (searchResultBytes != null) { SearchResultProto searchResultProto; try { @@ -262,12 +309,10 @@ public class AppSearchManager { callback.accept(searchResults, null); return; } - // Nothing was supplied in the future at all callback.accept( null, new IllegalStateException("Unknown failure occurred while querying")); }, executor); - try { SearchSpecProto searchSpecProto = searchSpec.getSearchSpecProto(); searchSpecProto = searchSpecProto.toBuilder().setQuery(queryExpression).build(); diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchSchema.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchSchema.java index a0a2f3cd9454e..4b0b41b1a282f 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/AppSearchSchema.java +++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchSchema.java @@ -104,10 +104,7 @@ public final class AppSearchSchema { * a property. */ public static final class PropertyConfig { - /** - * Physical data-types of the contents of the property. - * @hide - */ + /** Physical data-types of the contents of the property. */ // NOTE: The integer values of these constants must match the proto enum constants in // com.google.android.icing.proto.PropertyConfigProto.DataType.Code. @IntDef(prefix = {"DATA_TYPE_"}, value = { @@ -136,10 +133,7 @@ public final class AppSearchSchema { */ public static final int DATA_TYPE_DOCUMENT = 6; - /** - * The cardinality of the property (whether it is required, optional or repeated). - * @hide - */ + /** The cardinality of the property (whether it is required, optional or repeated). */ // NOTE: The integer values of these constants must match the proto enum constants in // com.google.android.icing.proto.PropertyConfigProto.Cardinality.Code. @IntDef(prefix = {"CARDINALITY_"}, value = { @@ -159,10 +153,7 @@ public final class AppSearchSchema { /** Exactly one value [1]. */ public static final int CARDINALITY_REQUIRED = 3; - /** - * Encapsulates the configurations on how AppSearch should query/index these terms. - * @hide - */ + /** Encapsulates the configurations on how AppSearch should query/index these terms. */ @IntDef(prefix = {"INDEXING_TYPE_"}, value = { INDEXING_TYPE_NONE, INDEXING_TYPE_EXACT_TERMS, @@ -197,10 +188,7 @@ public final class AppSearchSchema { */ public static final int INDEXING_TYPE_PREFIXES = 2; - /** - * Configures how tokens should be extracted from this property. - * @hide - */ + /** Configures how tokens should be extracted from this property. */ // NOTE: The integer values of these constants must match the proto enum constants in // com.google.android.icing.proto.IndexingConfig.TokenizerType.Code. @IntDef(prefix = {"TOKENIZER_TYPE_"}, value = { diff --git a/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl b/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl index f3a8043ba0707..8c8daef6ed6e9 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl +++ b/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl @@ -47,6 +47,16 @@ interface IAppSearchManager { */ void putDocuments(in List documentsBytes, in AndroidFuture callback); + /** + * Retrieves documents from the index. + * + * @param uris The URIs of the documents to retrieve + * @param callback {@link AndroidFuture}<{@link List}<byte[]>>. Will be completed + * with a {@link List} containing serialized DocumentProtos, or completed exceptionally if + * get fails. + */ + void getDocuments(in String[] uris, in AndroidFuture callback); + /** * Searches a document based on a given specifications. * 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 47c8aad33e6d8..bd99f2ddb7fa1 100644 --- a/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java +++ b/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java @@ -37,6 +37,7 @@ import com.google.android.icing.proto.SearchSpecProto; import com.google.android.icing.protobuf.InvalidProtocolBufferException; import java.io.IOException; +import java.util.ArrayList; import java.util.List; /** @@ -110,6 +111,29 @@ public class AppSearchManagerService extends SystemService { } } + @Override + public void getDocuments(String[] uris, AndroidFuture callback) { + Preconditions.checkNotNull(uris); + Preconditions.checkNotNull(callback); + int callingUid = Binder.getCallingUidOrThrow(); + int callingUserId = UserHandle.getUserId(callingUid); + long callingIdentity = Binder.clearCallingIdentity(); + try { + AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId); + // Contains serialized DocumentProto. byte[][] is not transmissible via Binder. + List results = new ArrayList<>(uris.length); + for (String uri : uris) { + DocumentProto result = impl.getDocument(callingUid, uri); + results.add(result.toByteArray()); + } + callback.complete(results); + } catch (Throwable t) { + callback.completeExceptionally(t); + } finally { + Binder.restoreCallingIdentity(callingIdentity); + } + } + // TODO(sidchhabra):Init FakeIcing properly. // TODO(sidchhabra): Do this in a threadpool. @Override diff --git a/apex/appsearch/service/java/com/android/server/appsearch/impl/AppSearchImpl.java b/apex/appsearch/service/java/com/android/server/appsearch/impl/AppSearchImpl.java index 04b4b14273286..7442d0625280b 100644 --- a/apex/appsearch/service/java/com/android/server/appsearch/impl/AppSearchImpl.java +++ b/apex/appsearch/service/java/com/android/server/appsearch/impl/AppSearchImpl.java @@ -17,6 +17,7 @@ package com.android.server.appsearch.impl; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.UserIdInt; import android.content.Context; @@ -106,30 +107,65 @@ public final class AppSearchImpl { // Rewrite the type names to include the app's prefix String typePrefix = getTypePrefix(callingUid); DocumentProto.Builder documentBuilder = origDocument.toBuilder(); - rewriteDocumentTypes(typePrefix, documentBuilder); + rewriteDocumentTypes(typePrefix, documentBuilder, /*add=*/ true); mFakeIcing.put(documentBuilder.build()); } /** - * Rewrites all types mentioned anywhere in {@code documentBuilder} to prepend + * Retrieves a document from the AppSearch index by URI. + * + * @param callingUid The uid of the app calling AppSearch. + * @param uri The URI of the document to get. + * @return The Document contents, or {@code null} if no such URI exists in the system. + */ + @Nullable + public DocumentProto getDocument(int callingUid, @NonNull String uri) { + String typePrefix = getTypePrefix(callingUid); + DocumentProto document = mFakeIcing.get(uri); + // Rewrite the type names to remove the app's prefix + DocumentProto.Builder documentBuilder = document.toBuilder(); + rewriteDocumentTypes(typePrefix, documentBuilder, /*add=*/ false); + return documentBuilder.build(); + } + + /** + * Rewrites all types mentioned anywhere in {@code documentBuilder} to prepend or remove * {@code typePrefix}. * - * @param typePrefix The prefix to add + * @param typePrefix The prefix to add or remove * @param documentBuilder The document to mutate + * @param add Whether to add typePrefix to the types. If {@code false}, typePrefix will be + * removed from the types. + * @throws IllegalArgumentException If {@code add=false} and the document has a type that + * doesn't start with {@code typePrefix}. */ @VisibleForTesting void rewriteDocumentTypes( @NonNull String typePrefix, - @NonNull DocumentProto.Builder documentBuilder) { - // Rewrite the type name to include the app's prefix - String newSchema = typePrefix + documentBuilder.getSchema(); + @NonNull DocumentProto.Builder documentBuilder, + boolean add) { + // Rewrite the type name to include/remove the app's prefix + String newSchema; + if (add) { + newSchema = typePrefix + documentBuilder.getSchema(); + } else { + newSchema = removePrefix(typePrefix, documentBuilder.getSchema()); + } documentBuilder.setSchema(newSchema); - // Add namespace. If we ever allow users to set their own namespaces, this will have + // Add/remove namespace. If we ever allow users to set their own namespaces, this will have // to change to prepend the prefix instead of setting the whole namespace. We will also have // to store the namespaces in a map similar to the type map so we can rewrite queries with // empty namespaces. - documentBuilder.setNamespace(typePrefix); + if (add) { + documentBuilder.setNamespace(typePrefix); + } else if (!documentBuilder.getNamespace().equals(typePrefix)) { + throw new IllegalStateException( + "Unexpected namespace \"" + documentBuilder.getNamespace() + + "\" (expected \"" + typePrefix + "\")"); + } else { + documentBuilder.clearNamespace(); + } // Recurse into derived documents for (int propertyIdx = 0; @@ -142,7 +178,7 @@ public final class AppSearchImpl { for (int documentIdx = 0; documentIdx < documentCount; documentIdx++) { DocumentProto.Builder derivedDocumentBuilder = propertyBuilder.getDocumentValues(documentIdx).toBuilder(); - rewriteDocumentTypes(typePrefix, derivedDocumentBuilder); + rewriteDocumentTypes(typePrefix, derivedDocumentBuilder, add); propertyBuilder.setDocumentValues(documentIdx, derivedDocumentBuilder); } documentBuilder.setProperties(propertyIdx, propertyBuilder); @@ -165,4 +201,13 @@ public final class AppSearchImpl { } return callingUidName + "@" + mUserId + "/"; } + + @NonNull + private static String removePrefix(@NonNull String prefix, @NonNull String input) { + if (!input.startsWith(prefix)) { + throw new IllegalArgumentException( + "Input \"" + input + "\" does not start with \"" + prefix + "\""); + } + return input.substring(prefix.length()); + } }