Implement putDocuments() support in AppSearchImpl.
Bug: 143789408
Test: AppSearchManagerTest
Change-Id: I4ef0540e0655a0b65a32b8d6a903ae955c687f4f
diff --git a/framework/java/android/app/appsearch/AppSearch.java b/framework/java/android/app/appsearch/AppSearch.java
index 5b41249..fd20186 100644
--- a/framework/java/android/app/appsearch/AppSearch.java
+++ b/framework/java/android/app/appsearch/AppSearch.java
@@ -20,6 +20,7 @@
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.appsearch.AppSearchSchema.PropertyConfig;
import android.os.Bundle;
import android.util.Log;
@@ -574,10 +575,6 @@
* @hide
*/
public static class Email extends Document {
-
- /** The name of the schema type for {@link Email} documents.*/
- public static final String SCHEMA_TYPE = "builtin:Email";
-
private static final String KEY_FROM = "from";
private static final String KEY_TO = "to";
private static final String KEY_CC = "cc";
@@ -585,14 +582,53 @@
private static final String KEY_SUBJECT = "subject";
private static final String KEY_BODY = "body";
- /**
- * Creates a new {@link Email} from the contents of an existing {@link Document}.
- *
- * @param document The {@link Document} containing the email content.
- */
- public Email(@NonNull Document document) {
- super(document);
- }
+ /** The name of the schema type for {@link Email} documents.*/
+ public static final String SCHEMA_TYPE = "builtin:Email";
+
+ public static final AppSearchSchema SCHEMA = AppSearchSchema.newBuilder(SCHEMA_TYPE)
+ .addProperty(AppSearchSchema.newPropertyBuilder(KEY_FROM)
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build()
+
+ ).addProperty(AppSearchSchema.newPropertyBuilder(KEY_TO)
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+ .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build()
+
+ ).addProperty(AppSearchSchema.newPropertyBuilder(KEY_CC)
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+ .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build()
+
+ ).addProperty(AppSearchSchema.newPropertyBuilder(KEY_BCC)
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+ .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build()
+
+ ).addProperty(AppSearchSchema.newPropertyBuilder(KEY_SUBJECT)
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build()
+
+ ).addProperty(AppSearchSchema.newPropertyBuilder(KEY_BODY)
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build()
+
+ ).build();
/**
* Creates a new {@link Email.Builder}.
@@ -604,6 +640,15 @@
}
/**
+ * Creates a new {@link Email} from the contents of an existing {@link Document}.
+ *
+ * @param document The {@link Document} containing the email content.
+ */
+ public Email(@NonNull Document document) {
+ super(document);
+ }
+
+ /**
* Get the from address of {@link Email}.
*
* @return Returns the subject of {@link Email} or {@code null} if it's not been set yet.
@@ -615,10 +660,10 @@
}
/**
- * Get the destination address of {@link Email}.
+ * Get the destination addresses of {@link Email}.
*
- * @return Returns the destination address of {@link Email} or {@code null} if it's not been
- * set yet.
+ * @return Returns the destination addresses of {@link Email} or {@code null} if it's not
+ * been set yet.
* @hide
*/
@Nullable
diff --git a/framework/java/android/app/appsearch/AppSearchManager.java b/framework/java/android/app/appsearch/AppSearchManager.java
index 33f69a4..66cba52 100644
--- a/framework/java/android/app/appsearch/AppSearchManager.java
+++ b/framework/java/android/app/appsearch/AppSearchManager.java
@@ -54,7 +54,7 @@
}
/**
- * Sets the schema being used by documents provided to the #put method.
+ * Sets the schema being used by documents provided to the {@link #putDocuments} method.
*
* <p>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
@@ -106,13 +106,12 @@
*
* @hide
*/
- // TODO(b/143789408): linkify #put after that API is created
public void setSchema(@NonNull AppSearchSchema... schemas) {
setSchema(Arrays.asList(schemas), /*forceOverride=*/false);
}
/**
- * Sets the schema being used by documents provided to the #put method.
+ * Sets the schema being used by documents provided to the {@link #putDocuments} method.
*
* <p>This method is similar to {@link #setSchema(AppSearchSchema...)}, except for the
* {@code forceOverride} parameter. If a backwards-incompatible schema is specified but the
@@ -129,7 +128,6 @@
*
* @hide
*/
- // TODO(b/143789408): linkify #put after that API is created
public void setSchema(@NonNull List<AppSearchSchema> schemas, boolean forceOverride) {
// Prepare the merged schema for transmission.
SchemaProto.Builder schemaProtoBuilder = SchemaProto.newBuilder();
@@ -151,26 +149,28 @@
}
/**
- * Index {@link Document} to AppSearch
+ * Index {@link android.app.appsearch.AppSearch.Document Documents} into AppSearch.
*
- * <p>You should not call this method directly; instead, use the {@code AppSearch#put()} API
- * provided by JetPack.
+ * <p>You should not call this method directly; instead, use the
+ * {@code AppSearch#putDocuments()} API provided by JetPack.
*
- * <p>The schema should be set via {@link #setSchema} method.
+ * <p>Each {@link AppSearch.Document Document's} {@code schemaType} field must be set to the
+ * 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 resulting from setting the schema. If the
- * operation succeeds, the callback will be invoked with {@code null}.
+ * @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.
*/
- public void put(@NonNull List<Document> documents,
+ public void putDocuments(
+ @NonNull List<Document> documents,
@NonNull @CallbackExecutor Executor executor,
@NonNull Consumer<? super Throwable> callback) {
AndroidFuture<Void> future = new AndroidFuture<>();
for (Document document : documents) {
// TODO(b/146386470) batching Document protos
try {
- mService.put(document.getProto().toByteArray(), future);
+ mService.putDocument(document.getProto().toByteArray(), future);
} catch (RemoteException e) {
future.completeExceptionally(e);
break;
diff --git a/framework/java/android/app/appsearch/IAppSearchManager.aidl b/framework/java/android/app/appsearch/IAppSearchManager.aidl
index 194e43e..c9c5d7f 100644
--- a/framework/java/android/app/appsearch/IAppSearchManager.aidl
+++ b/framework/java/android/app/appsearch/IAppSearchManager.aidl
@@ -22,15 +22,25 @@
/**
* Sets the schema.
*
- * @param schemaProto Serialized SchemaProto.
+ * @param schemaBytes Serialized SchemaProto.
* @param forceOverride Whether to apply the new schema even if it is incompatible. All
* incompatible documents will be deleted.
* @param callback {@link AndroidFuture}<{@link Void}>. Will be completed with
* {@code null} upon successful completion of the setSchema call, or completed
* exceptionally if setSchema fails.
*/
- void setSchema(in byte[] schemaProto, boolean forceOverride, in AndroidFuture callback);
- void put(in byte[] documentBytes, in AndroidFuture callback);
+ void setSchema(in byte[] schemaBytes, boolean forceOverride, in AndroidFuture callback);
+
+ /**
+ * Inserts a document 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.
+ */
+ void putDocument(in byte[] documentBytes, in AndroidFuture callback);
+
/**
* Searches a document based on a given query string.
*
diff --git a/service/java/com/android/server/appsearch/AppSearchManagerService.java b/service/java/com/android/server/appsearch/AppSearchManagerService.java
index 5d6d3f0..6929202 100644
--- a/service/java/com/android/server/appsearch/AppSearchManagerService.java
+++ b/service/java/com/android/server/appsearch/AppSearchManagerService.java
@@ -28,6 +28,7 @@
import com.android.server.appsearch.impl.FakeIcing;
import com.android.server.appsearch.impl.ImplInstanceManager;
+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;
@@ -71,11 +72,21 @@
}
@Override
- public void put(byte[] documentBytes, AndroidFuture callback) {
+ public void putDocument(byte[] documentBytes, AndroidFuture callback) {
+ Preconditions.checkNotNull(documentBytes);
+ Preconditions.checkNotNull(callback);
+ int callingUid = Binder.getCallingUidOrThrow();
+ int callingUserId = UserHandle.getUserId(callingUid);
+ long callingIdentity = Binder.clearCallingIdentity();
try {
- throw new UnsupportedOperationException("Put document not yet implemented");
+ DocumentProto document = DocumentProto.parseFrom(documentBytes);
+ AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId);
+ impl.putDocument(callingUid, document);
+ callback.complete(null);
} catch (Throwable t) {
callback.completeExceptionally(t);
+ } finally {
+ Binder.restoreCallingIdentity(callingIdentity);
}
}
// TODO(sidchhabra):Init FakeIcing properly.
diff --git a/service/java/com/android/server/appsearch/impl/AppSearchImpl.java b/service/java/com/android/server/appsearch/impl/AppSearchImpl.java
index 177c910..04b4b14 100644
--- a/service/java/com/android/server/appsearch/impl/AppSearchImpl.java
+++ b/service/java/com/android/server/appsearch/impl/AppSearchImpl.java
@@ -22,7 +22,9 @@
import com.android.internal.annotations.VisibleForTesting;
+import com.google.android.icing.proto.DocumentProto;
import com.google.android.icing.proto.PropertyConfigProto;
+import com.google.android.icing.proto.PropertyProto;
import com.google.android.icing.proto.SchemaProto;
import com.google.android.icing.proto.SchemaTypeConfigProto;
@@ -95,6 +97,60 @@
}
/**
+ * Adds a document to the AppSearch index.
+ *
+ * @param callingUid The uid of the app calling AppSearch.
+ * @param origDocument The document to index.
+ */
+ public void putDocument(int callingUid, @NonNull DocumentProto origDocument) {
+ // Rewrite the type names to include the app's prefix
+ String typePrefix = getTypePrefix(callingUid);
+ DocumentProto.Builder documentBuilder = origDocument.toBuilder();
+ rewriteDocumentTypes(typePrefix, documentBuilder);
+ mFakeIcing.put(documentBuilder.build());
+ }
+
+ /**
+ * Rewrites all types mentioned anywhere in {@code documentBuilder} to prepend
+ * {@code typePrefix}.
+ *
+ * @param typePrefix The prefix to add
+ * @param documentBuilder The document to mutate
+ */
+ @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();
+ documentBuilder.setSchema(newSchema);
+
+ // Add 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);
+
+ // Recurse into derived documents
+ for (int propertyIdx = 0;
+ propertyIdx < documentBuilder.getPropertiesCount();
+ propertyIdx++) {
+ int documentCount = documentBuilder.getProperties(propertyIdx).getDocumentValuesCount();
+ if (documentCount > 0) {
+ PropertyProto.Builder propertyBuilder =
+ documentBuilder.getProperties(propertyIdx).toBuilder();
+ for (int documentIdx = 0; documentIdx < documentCount; documentIdx++) {
+ DocumentProto.Builder derivedDocumentBuilder =
+ propertyBuilder.getDocumentValues(documentIdx).toBuilder();
+ rewriteDocumentTypes(typePrefix, derivedDocumentBuilder);
+ propertyBuilder.setDocumentValues(documentIdx, derivedDocumentBuilder);
+ }
+ documentBuilder.setProperties(propertyIdx, propertyBuilder);
+ }
+ }
+ }
+
+ /**
* Returns a type prefix in a format like {@code com.example.package@1000/} or
* {@code com.example.sharedname:5678@1000/}.
*/
diff --git a/testing/coretests/src/android/app/appsearch/AppSearchDocumentTest.java b/testing/coretests/src/android/app/appsearch/AppSearchDocumentTest.java
index 9e4440a..abba7fc 100644
--- a/testing/coretests/src/android/app/appsearch/AppSearchDocumentTest.java
+++ b/testing/coretests/src/android/app/appsearch/AppSearchDocumentTest.java
@@ -40,14 +40,14 @@
@Test
public void testDocumentEquals_Identical() {
Document document1 = Document.newBuilder("uri1", "schemaType1")
- .setCreationTimestampMillis(0L)
+ .setCreationTimestampMillis(5L)
.setProperty("longKey1", 1L, 2L, 3L)
.setProperty("doubleKey1", 1.0, 2.0, 3.0)
.setProperty("booleanKey1", true, false, true)
.setProperty("stringKey1", "test-value1", "test-value2", "test-value3")
.build();
Document document2 = Document.newBuilder("uri1", "schemaType1")
- .setCreationTimestampMillis(0L)
+ .setCreationTimestampMillis(5L)
.setProperty("longKey1", 1L, 2L, 3L)
.setProperty("doubleKey1", 1.0, 2.0, 3.0)
.setProperty("booleanKey1", true, false, true)
@@ -60,7 +60,7 @@
@Test
public void testDocumentEquals_DifferentOrder() {
Document document1 = Document.newBuilder("uri1", "schemaType1")
- .setCreationTimestampMillis(0L)
+ .setCreationTimestampMillis(5L)
.setProperty("longKey1", 1L, 2L, 3L)
.setProperty("doubleKey1", 1.0, 2.0, 3.0)
.setProperty("booleanKey1", true, false, true)
@@ -69,7 +69,7 @@
// Create second document with same parameter but different order.
Document document2 = Document.newBuilder("uri1", "schemaType1")
- .setCreationTimestampMillis(0L)
+ .setCreationTimestampMillis(5L)
.setProperty("booleanKey1", true, false, true)
.setProperty("stringKey1", "test-value1", "test-value2", "test-value3")
.setProperty("doubleKey1", 1.0, 2.0, 3.0)
@@ -82,11 +82,13 @@
@Test
public void testDocumentEquals_Failure() {
Document document1 = Document.newBuilder("uri1", "schemaType1")
+ .setCreationTimestampMillis(5L)
.setProperty("longKey1", 1L, 2L, 3L)
.build();
// Create second document with same order but different value.
Document document2 = Document.newBuilder("uri1", "schemaType1")
+ .setCreationTimestampMillis(5L)
.setProperty("longKey1", 1L, 2L, 4L) // Different
.build();
assertThat(document1).isNotEqualTo(document2);
@@ -96,11 +98,13 @@
@Test
public void testDocumentEquals_Failure_RepeatedFieldOrder() {
Document document1 = Document.newBuilder("uri1", "schemaType1")
+ .setCreationTimestampMillis(5L)
.setProperty("booleanKey1", true, false, true)
.build();
// Create second document with same order but different value.
Document document2 = Document.newBuilder("uri1", "schemaType1")
+ .setCreationTimestampMillis(5L)
.setProperty("booleanKey1", true, true, false) // Different
.build();
assertThat(document1).isNotEqualTo(document2);
@@ -110,12 +114,16 @@
@Test
public void testDocumentGetSingleValue() {
Document document = Document.newBuilder("uri1", "schemaType1")
+ .setCreationTimestampMillis(5L)
+ .setScore(1)
.setProperty("longKey1", 1L)
.setProperty("doubleKey1", 1.0)
.setProperty("booleanKey1", true)
.setProperty("stringKey1", "test-value1").build();
assertThat(document.getUri()).isEqualTo("uri1");
assertThat(document.getSchemaType()).isEqualTo("schemaType1");
+ assertThat(document.getCreationTimestampMillis()).isEqualTo(5);
+ assertThat(document.getScore()).isEqualTo(1);
assertThat(document.getPropertyLong("longKey1")).isEqualTo(1L);
assertThat(document.getPropertyDouble("doubleKey1")).isEqualTo(1.0);
assertThat(document.getPropertyBoolean("booleanKey1")).isTrue();
@@ -125,7 +133,7 @@
@Test
public void testDocumentGetArrayValues() {
Document document = Document.newBuilder("uri1", "schemaType1")
- .setScore(1)
+ .setCreationTimestampMillis(5L)
.setProperty("longKey1", 1L, 2L, 3L)
.setProperty("doubleKey1", 1.0, 2.0, 3.0)
.setProperty("booleanKey1", true, false, true)
@@ -134,7 +142,6 @@
assertThat(document.getUri()).isEqualTo("uri1");
assertThat(document.getSchemaType()).isEqualTo("schemaType1");
- assertThat(document.getScore()).isEqualTo(1);
assertThat(document.getPropertyLongArray("longKey1")).asList().containsExactly(1L, 2L, 3L);
assertThat(document.getPropertyDoubleArray("doubleKey1")).usingExactEquality()
.containsExactly(1.0, 2.0, 3.0);
@@ -181,8 +188,8 @@
@Test
public void testDocumentProtoPopulation() {
Document document = Document.newBuilder("uri1", "schemaType1")
+ .setCreationTimestampMillis(5L)
.setScore(1)
- .setCreationTimestampMillis(0)
.setProperty("longKey1", 1L)
.setProperty("doubleKey1", 1.0)
.setProperty("booleanKey1", true)
@@ -191,7 +198,7 @@
// Create the Document proto. Need to sort the property order by key.
DocumentProto.Builder documentProtoBuilder = DocumentProto.newBuilder()
- .setUri("uri1").setSchema("schemaType1").setScore(1).setCreationTimestampMs(0);
+ .setUri("uri1").setSchema("schemaType1").setScore(1).setCreationTimestampMs(5L);
HashMap<String, PropertyProto.Builder> propertyProtoMap = new HashMap<>();
propertyProtoMap.put("longKey1",
PropertyProto.newBuilder().setName("longKey1").addInt64Values(1L));