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}&lt;{@link Void}&gt;. 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}&lt;{@link Void}&gt;. 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));